Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/scripts/next-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# next-version.sh — compute THIS repo's next release tag for release-on-upstream.yml.
#
# Single source of truth for the version math, exercised in CI by release-selftest.yml so the
# release automation can't silently rot (guard #135.8). Prints "v<MAJOR>.<MINOR>.<PATCH>" to stdout.
# release automation can't silently rot. Prints "v<MAJOR>.<MINOR>.<PATCH>" to stdout.
#
# Inputs (env, all optional):
# INPUT_VERSION explicit version to cut (leading "v" tolerated) -> used verbatim.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-selftest.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# CI self-test for the release-on-upstream version-compute logic (guard #135.8).
# CI self-test for the release-on-upstream version-compute logic.
# Runs the REAL .github/scripts/next-version.sh against synthetic repos and asserts it produces a
# valid next version for BOTH the has-prior-tag and no-prior-tag cases — WITHOUT publishing anything.
# This is what keeps the release automation from silently rotting before the fleet fan-out is armed.
Expand Down
74 changes: 74 additions & 0 deletions store-sqlite-plugin/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2026 Busbar Inc and contributors

//! Support code shared by this crate's integration tests: supplying the secrets a config fixture
//! references so the real `busbar` binary can start.
//!
//! busbar resolves every `env:`/`file:` secret reference during `--validate` AND at boot, and exits
//! non-zero when one of them cannot resolve. A fixture that names a variable nobody exported is
//! therefore a gateway that cannot come up, and the test reports that as an opaque timeout or
//! early-exit a long way from the YAML that caused it.
//!
//! The variable names are derived FROM the fixture text rather than listed here on purpose: the
//! next person to add a secret reference to a config in these tests must not have to also remember
//! to export it, because forgetting produces a failure that does not name the variable.
//!
//! This lives under `tests/common/` rather than `tests/` so cargo does not compile it as a test
//! binary of its own; its own coverage lives in `tests/config_secrets.rs`.

#![allow(dead_code)]
// Each integration test binary compiles this module separately and uses a different subset of it,
// so anything only one of them needs is genuinely dead code from the other's point of view.

use std::path::Path;
use std::process::Command;

/// Stands in for every secret a fixture references but the test does not otherwise set. The content
/// is irrelevant — what matters is only that resolution SUCCEEDS.
pub const SECRET_PLACEHOLDER: &str = "e2e-placeholder";

/// Every environment variable `config` names, whether through a `{ env: NAME }` secret reference or
/// through a legacy `*_env: NAME` field, in first-seen order and without duplicates.
pub fn referenced_env_vars(config: &str) -> Vec<String> {
let mut names = Vec::new();
for (idx, _) in config.match_indices("env:") {
let name: String = config[idx + "env:".len()..]
.trim_start_matches([' ', '\t'])
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect();
// A bare `env:` with nothing usable after it (a line break, or the key of a nested mapping)
// names no variable at all; skip it rather than exporting an empty-named one.
if !name.is_empty() && !names.contains(&name) {
names.push(name);
}
}
names
}

/// Sets [`SECRET_PLACEHOLDER`] for every environment variable `config` references.
///
/// Call this BEFORE any `.env()` carrying a value the test actually asserts on (an admin token, a
/// signing key): the later `.env()` wins, so the placeholder only ever fills in the secrets whose
/// value the test does not care about.
pub fn apply_placeholder_secrets(cmd: &mut Command, config: &str) {
for name in referenced_env_vars(config) {
cmd.env(name, SECRET_PLACEHOLDER);
}
}

/// [`apply_placeholder_secrets`] over the files busbar will actually read.
///
/// Reading the files back, rather than the strings the test built them from, is what makes this
/// impossible to get out of step: whatever landed on disk is exactly what busbar resolves.
pub fn apply_placeholder_secrets_from_files(cmd: &mut Command, paths: &[&Path]) {
for path in paths {
let text = std::fs::read_to_string(path).unwrap_or_else(|e| {
panic!(
"read config fixture {} for its secrets: {e}",
path.display()
)
});
apply_placeholder_secrets(cmd, &text);
}
}
45 changes: 45 additions & 0 deletions store-sqlite-plugin/tests/config_secrets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2026 Busbar Inc and contributors

//! Coverage for `tests/common`'s secret-reference scan.
//!
//! That scan is what keeps the real-binary e2e tests bootable, and when it misses a reference the
//! symptom surfaces a long way from here — as `busbar` exiting 1 before its listener comes up —
//! so it is worth testing directly rather than only through the e2e tests it supports.

mod common;

use common::referenced_env_vars;

#[test]
fn finds_secret_references_and_legacy_env_fields() {
let config = "\
providers:\n mock:\n api_key: { env: MOCK_KEY }\n\
identity-providers:\n admin-tokens: { module: admin-tokens, token: { env: BUSBAR_ADMIN_TOKEN } }\n\
auth:\n signing_key: { env: BUSBAR_SIGNING_KEY }\n";
assert_eq!(
referenced_env_vars(config),
vec!["MOCK_KEY", "BUSBAR_ADMIN_TOKEN", "BUSBAR_SIGNING_KEY"],
);

// The flat providers catalog still names variables through the legacy `*_env:` field rather
// than a secret reference, and busbar resolves those too, so the same scan has to see them.
assert_eq!(
referenced_env_vars("mock:\n api_key_env: MOCK_KEY\n"),
vec!["MOCK_KEY"],
);
}

#[test]
fn deduplicates_and_ignores_references_that_name_nothing() {
assert_eq!(
referenced_env_vars("a: { env: SAME }\nb: { env: SAME }\n"),
vec!["SAME"],
"a variable referenced twice must be exported once, not twice"
);

// A bare `env:` opening a nested mapping, or ending a line, names no variable; exporting an
// empty-named one would be nonsense and on some platforms an outright error.
assert!(referenced_env_vars("a: { env:\n b: c }\n").is_empty());
assert!(referenced_env_vars("listen: \"127.0.0.1:0\"\nauth:\n chain: []\n").is_empty());
}
83 changes: 57 additions & 26 deletions store-sqlite-plugin/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
//! /api/v1/admin/keys`, and independently verifies both landed in the real on-disk file with a
//! second `SqliteStore::open` that never touches the plugin/ABI/admin-API/loader.

mod common;

use busbar_api::{ModelTokens, Store, TierTokens, UsageLedger};
use busbar_store_sqlite::SqliteStore;
use std::path::PathBuf;
Expand Down Expand Up @@ -88,8 +90,22 @@ fn plugin_path() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?; // .../target/<profile>/deps/e2e-<hash>
let profile_dir = exe.parent()?.parent()?; // .../target/<profile>
let name = busbar_plugin_loader::plugin_library_filename("busbar_store_sqlite_plugin");
let candidate = profile_dir.join(&name);
candidate.exists().then_some(candidate)
// BOTH the "uplifted" `<profile>/<name>` copy and the raw `<profile>/deps/<name>` compiler
// output: a bare `cargo test` does not uplift the cdylib, only `cargo build` does, so
// checking the uplifted path alone made this test silently skip itself on a developer
// machine — the exact coverage it exists to provide, gone, with a green result.
let uplifted = profile_dir.join(&name);
let raw = profile_dir.join("deps").join(&name);
[uplifted, raw]
.into_iter()
.filter_map(|p| {
std::fs::metadata(&p)
.and_then(|m| m.modified())
.ok()
.map(|mtime| (p, mtime))
})
.max_by_key(|(_, mtime)| *mtime)
.map(|(p, _)| p)
})();
if candidate.is_none() && std::env::var_os("CI").is_some() {
panic!(
Expand Down Expand Up @@ -200,20 +216,17 @@ fn load_and_exercise_sqlite_plugin_via_file_drop() {
"mock:\n protocol: anthropic\n base_url: \"http://127.0.0.1:9\"\n api_key_env: MOCK_KEY\n",
)
.unwrap();
std::fs::write(
&config,
format!(
"listen: \"127.0.0.1:0\"\n\
store:\n module: sqlite\n settings: {{ db_path: \"{}\" }}\n\
plugins:\n enabled: true\n dir: {}\n trust:\n allow_unsigned: true\n\
auth:\n chain: []\n\
providers:\n mock:\n api_key: {{ env: MOCK_KEY }}\n\
models:\n test-model:\n provider: mock\n",
db_path.display(),
plugins_dir.display()
),
)
.unwrap();
let config_text = format!(
"listen: \"127.0.0.1:0\"\n\
store:\n module: sqlite\n settings: {{ db_path: \"{}\" }}\n\
plugins:\n enabled: true\n dir: {}\n trust:\n allow_unsigned: true\n\
auth:\n chain: []\n\
providers:\n mock:\n api_key: {{ env: MOCK_KEY }}\n\
models:\n test-model:\n provider: mock\n",
db_path.display(),
plugins_dir.display()
);
std::fs::write(&config, &config_text).unwrap();

// `--validate` is DELIBERATELY not used for the load-proof itself: it is manifest-only by
// design ("no server, no network, no state, no dlopen" -- crates/busbar/src/main.rs's own
Expand All @@ -224,7 +237,9 @@ fn load_and_exercise_sqlite_plugin_via_file_drop() {
// plugin passes the trust/manifest gate; then a REAL BOOT (no `--validate` flag) is the only
// thing that actually `dlopen`s the plugin and runs `Store::open`/migration, so that's what
// proves the persistence claim.
let out = Command::new(&busbar_bin)
let mut validate = Command::new(&busbar_bin);
common::apply_placeholder_secrets_from_files(&mut validate, &[&config, &providers]);
let out = validate
.arg("--validate")
.env("BUSBAR_CONFIG", &config)
.env("BUSBAR_PROVIDERS", &providers)
Expand All @@ -241,7 +256,9 @@ fn load_and_exercise_sqlite_plugin_via_file_drop() {
// plugin + config, and poll for the real sqlite file to appear -- the only genuine proof that
// boot actually dlopened the plugin and called Store::open (which creates/migrates the file)
// before ever handling a request.
let mut child = Command::new(&busbar_bin)
let mut boot = Command::new(&busbar_bin);
common::apply_placeholder_secrets_from_files(&mut boot, &[&config, &providers]);
let mut child = boot
.env("BUSBAR_CONFIG", &config)
.env("BUSBAR_PROVIDERS", &providers)
.env("BUSBAR_STATE_FILE", "") // disable the state-snapshot file; not under test here
Expand Down Expand Up @@ -396,7 +413,7 @@ fn wait_for_admin_ready(
}
}

/// THE REAL "PROD READY" PROOF (Matthew's bar): the sqlite plugin installed the way a real operator
/// THE REAL "PROD READY" PROOF: the sqlite plugin installed the way a real operator
/// actually installs it — a `POST /api/v1/admin/plugins` call against a REAL running busbar admin
/// listener, never file-drop, never a direct `load_store()`/loader call — then EXERCISED through
/// that same live instance's own admin API (mint a virtual key + an attached AWS SigV4 credential
Expand All @@ -423,6 +440,10 @@ fn install_sqlite_plugin_via_admin_api_and_verify_persistence() {
std::fs::create_dir_all(&plugins_dir).unwrap();
let db_path = work.join("governance.db");
const ADMIN_TOKEN: &str = "e2e-admin-api-token";
// Fixed ed25519 signing secret (64 hex = 32 bytes). busbar no longer auto-generates one, and
// without it the mint below is refused with a `conflict` ("signed-token minting is unavailable:
// no signing key is configured") rather than anything that points at the config.
const SIGNING_KEY: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";

let providers = work.join("providers.yaml");
std::fs::write(
Expand All @@ -444,7 +465,7 @@ fn install_sqlite_plugin_via_admin_api_and_verify_persistence() {
{store_yaml}\n\
plugins:\n enabled: true\n dir: {}\n trust:\n allow_unsigned: true\n\
identity-providers:\n admin-tokens: {{ module: admin-tokens, token: {{ env: BUSBAR_ADMIN_TOKEN }} }}\n\
auth:\n chain: []\n admin_auth: [admin-tokens]\n\
auth:\n chain: []\n signing_key: {{ env: BUSBAR_SIGNING_KEY }}\n admin_auth: [admin-tokens]\n\
providers:\n mock:\n api_key: {{ env: MOCK_KEY }}\n\
models:\n test-model:\n provider: mock\n",
plugins_dir.display()
Expand Down Expand Up @@ -476,10 +497,13 @@ fn install_sqlite_plugin_via_admin_api_and_verify_persistence() {
};
let admin_addr1 = format!("127.0.0.1:{admin_addr1}");

let mut child1 = Command::new(&busbar_bin)
let mut boot1 = Command::new(&busbar_bin);
common::apply_placeholder_secrets_from_files(&mut boot1, &[&config1, &providers]);
let mut child1 = boot1
.env("BUSBAR_CONFIG", &config1)
.env("BUSBAR_PROVIDERS", &providers)
.env("BUSBAR_ADMIN_TOKEN", ADMIN_TOKEN)
.env("BUSBAR_SIGNING_KEY", SIGNING_KEY)
.env("BUSBAR_STATE_FILE", "")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
Expand Down Expand Up @@ -562,10 +586,13 @@ fn install_sqlite_plugin_via_admin_api_and_verify_persistence() {
};
let admin_addr2 = format!("127.0.0.1:{admin_addr2}");

let mut child2 = Command::new(&busbar_bin)
let mut boot2 = Command::new(&busbar_bin);
common::apply_placeholder_secrets_from_files(&mut boot2, &[&config2, &providers]);
let mut child2 = boot2
.env("BUSBAR_CONFIG", &config2)
.env("BUSBAR_PROVIDERS", &providers)
.env("BUSBAR_ADMIN_TOKEN", ADMIN_TOKEN)
.env("BUSBAR_SIGNING_KEY", SIGNING_KEY)
.env("BUSBAR_STATE_FILE", "")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
Expand All @@ -592,12 +619,16 @@ fn install_sqlite_plugin_via_admin_api_and_verify_persistence() {
}))
.send()
.expect("POST /api/v1/admin/keys");
// Carry the response body into the failure message: a bare status number says nothing about
// WHY the mint was refused, and this assertion has already once cost a debugging round-trip.
let mint_status = mint_resp.status().as_u16();
let mint_body = mint_resp.text().unwrap_or_default();
assert_eq!(
mint_resp.status().as_u16(),
201,
"minting a key + AWS credential through the live sqlite-backed instance must succeed"
mint_status, 201,
"minting a key + AWS credential through the live sqlite-backed instance must succeed: \
{mint_body}"
);
let minted: serde_json::Value = mint_resp.json().unwrap();
let minted: serde_json::Value = serde_json::from_str(&mint_body).unwrap();
let key_id = minted["id"].as_str().expect("minted key id").to_string();
let access_key_id = minted["aws_access_key_id"]
.as_str()
Expand Down
2 changes: 1 addition & 1 deletion store-sqlite/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,7 @@ fn foreign_keys_cascade_is_real_not_just_the_app_level_delete() {
);
}

// ── Mutation-testing regressions (cargo-mutants round 1, store-sqlite/src/lib.rs) ──────────────
// ── Regression guards for `store-sqlite/src/lib.rs` predicate boundaries ────────────────────────

#[test]
fn is_memory_path_rejects_a_plain_file_path() {
Expand Down