Skip to content
Closed
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
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ CI also refuses Cargo path dependencies that resolve outside this repository and

The `rig-kill-hook` feature adds a one-shot kill switch used only by the test rig; a production build never enables it.

Mock hosts, fake providers and test-only runtime helpers require `cfg(test)` or
the owning crate's `test-support` feature. Development dependencies enable that
feature for `cargo test`; production builds do not enable it. The harness and
testkit binaries also require `test-support` when built by hand, for example
`cargo build -p basal-module --bin ck-basal-harness --features test-support`.
Production staging rejects the retained test-support marker as well as the rig
kill-hook marker, even if the linker removes unused test doubles.

## Mutation proofs

A passing safety test proves little until it has been seen to fail. [`mutations.d/`](mutations.d) is the checked-in catalogue of source edits and the exact full libtest paths that must catch them, one TOML file per crate or area. The shared `ckdev-mutate` runner builds each mutant separately, runs its target's tests, requires the named test to fail, and restores the saved source bytes and checks `Cargo.lock`. The existing proofs name one guard each; other tests may also catch the mutant (`only = false`). Install the reviewed, immutable revision (ckdev-mutate 0.12.0):
Expand All @@ -66,7 +74,7 @@ The runner reads every visible `*.toml` file directly inside `mutations.d/`, in

Moving rows between files is not a change to any proof, and `ckdev-mutate run --diff` does not select a row only because it moved. `python3 script/mutation_catalogue.py --same-rows-as <revision>` compares every row with the catalogue at a Git revision, field for field, and fails on any added, removed or changed row, so a reorganisation can show it moved rows without editing them.

New rows can narrow two things the runner otherwise applies to a whole file or a whole target; the existing rows use neither:
Rows can narrow two things the runner otherwise applies to a whole file or a whole target; the catalogue uses both `scope` and `select`:

- **Anchor scope.** `scope = { after = "fn verify_signature(", before = "\nfn " }` bounds where `old` may match. `after` must occur exactly once in the file, and the region starts right after it; the region ends at the first `before` after that, or at the end of the file when `before` is omitted. `old` must occur exactly once inside the region, though it may appear elsewhere in the file. Use it when the anchor text could also appear in another function: a file-wide anchor that still matches once after a refactor could otherwise edit the wrong code. `check` reports a missing or ambiguous boundary, or an `old` that is not unique in the region. A row has one scope, so all its `edits` must be in one file. `prove --scope-after <text> [--scope-before <text>]` writes it.
- **Run set.** `select = { filter = "trust_roster_" }` runs only the target's tests whose full name contains that literal text (not a regex), and `select = { tests = ["tests::a", "tests::b"] }` runs exactly the listed full names. The set must contain every `expect_red` name and at least one other test that executes; otherwise use `select = "expected"`. With `only = true`, a failure of any other test in the set grades `WRONG_TEST`; tests outside the set do not run, and `--broad` does not widen it. Command rows (`runner = "command"`) and HUB rows (rows with `hub` and `hub_targets`, described below) cannot use a run set. `prove --select-filter <text>` or `prove --select-tests <a>,<b>` writes it.
Expand Down
7 changes: 7 additions & 0 deletions crates/basal-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ license.workspace = true
publish.workspace = true
rust-version.workspace = true

[features]
test-support = ["basal-host/test-support"]

# basal-core owns every durable thing and talks to hosts only through the Host
# trait. It never depends on basal-worker: workers are separate processes
# reached through the WorkerChannel trait.
Expand All @@ -29,3 +32,7 @@ sha2.workspace = true
serde_json = { workspace = true, features = ["float_roundtrip"] }
subc-protocol.workspace = true
tracing.workspace = true

[dev-dependencies]
# Use a distinct name so package-qualified feature flags still name the package.
basal-core-test-support = { package = "basal-core", path = ".", features = ["test-support"] }
5 changes: 2 additions & 3 deletions crates/basal-core/src/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
use basal_proto::{ArgsDigest, CallKind};
use rusqlite::{Connection, Transaction, params};

use crate::error::{CoreError, Result};
use crate::error::Result;
use crate::model::to_u64;

/// The outcome recorded for a call that was allowed.
Expand All @@ -30,8 +30,7 @@ pub fn record(
outcome: &str,
at: i64,
) -> Result<()> {
let position =
i64::try_from(position).map_err(|_| CoreError::Invalid(format!("position {position}")))?;
let position = crate::model::position_i64(position)?;
tx.execute(
"INSERT INTO call_audit (run_id, position, flow_id, op, args_digest, outcome, at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
Expand Down
14 changes: 6 additions & 8 deletions crates/basal-core/src/authorize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,7 @@ impl Refusal {

/// The rejection codes basal journals for calls it refuses.
pub mod codes {
/// Not allowed by the approved manifest or by the flow profile.
pub const DENIED: &str = "denied";
/// Arguments that are not JSON or do not have the call's shape.
pub const INVALID_ARGUMENTS: &str = "invalid_arguments";
/// An argument over the byte cap, refused before it was parsed.
pub const TOO_LARGE: &str = "too_large";
pub use basal_host::builtins::codes::{DENIED, INVALID_ARGUMENTS, TOO_LARGE};
/// The flow is disabled: it makes no new calls outside its VM.
pub const FLOW_DISABLED: &str = "flow_disabled";
/// The flow's dispatch budget for the current window is spent.
Expand Down Expand Up @@ -236,8 +231,11 @@ pub fn check(
Ok(())
}
CallKind::Primitive(p) => match p {
Primitive::Now | Primitive::Random => Ok(()),
Primitive::KvGet | Primitive::KvSet | Primitive::KvDelete => Ok(()),
Primitive::Now
| Primitive::Random
| Primitive::KvGet
| Primitive::KvSet
| Primitive::KvDelete => Ok(()),
Primitive::InstanceEnd => {
if manifest.params.is_some() {
Ok(())
Expand Down
3 changes: 1 addition & 2 deletions crates/basal-core/src/codemode/model_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,9 @@ impl Host for CommittedBroca {
fn configure_flow(
&self,
flow_id: &str,
agent_owned: bool,
scope: Option<basal_host::flow_scope::RegisteredScope>,
) {
self.host.configure_flow(flow_id, agent_owned, scope);
self.host.configure_flow(flow_id, scope);
}
fn classify(&self, kind: &CallKind) -> CallClass {
self.host.classify(kind)
Expand Down
6 changes: 3 additions & 3 deletions crates/basal-core/src/codemode/supervisor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,11 +590,11 @@ impl Driver {
}
self.shared
.transport
.configure_flow(&self.key(), false, Some(self.start.scope.clone()));
.configure_flow(&self.key(), Some(self.start.scope.clone()));
if let Some(models) = self.shared.models.lock().unwrap().as_ref() {
models
.host
.configure_flow(&self.key(), false, Some(self.start.scope.clone()));
.configure_flow(&self.key(), Some(self.start.scope.clone()));
}
let scope_events = self.event_tx.clone();
self.shared.transport.watch_run_scope(
Expand Down Expand Up @@ -1381,7 +1381,7 @@ fn terminate(
.write(|tx| store::settle_pending_calls(tx, id, at))?;
shared
.transport
.configure_flow(&format!("codemode:{id}"), false, None);
.configure_flow(&format!("codemode:{id}"), None);
shared
.store
.write(|tx| store::commit_terminal(tx, id, &terminal, at))?;
Expand Down
11 changes: 5 additions & 6 deletions crates/basal-core/src/codemode/supervisor_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ type Provider = dyn Fn(&str, Value, &str) -> std::result::Result<Value, WireErro
struct FakeTransport {
attempts: mpsc::Sender<Attempt>,
events: Arc<Mutex<Vec<String>>>,
registrations: Mutex<Vec<(String, bool, Option<RegisteredScope>)>>,
registrations: Mutex<Vec<(String, Option<RegisteredScope>)>>,
catalog_error: AtomicBool,
broca_opted_in: AtomicBool,
readiness: Mutex<Option<FlowRefusal>>,
Expand Down Expand Up @@ -95,11 +95,11 @@ impl Transport for FakeTransport {
)
}
}
fn configure_flow(&self, key: &str, agent_owned: bool, scope: Option<RegisteredScope>) {
fn configure_flow(&self, key: &str, scope: Option<RegisteredScope>) {
self.registrations
.lock()
.unwrap()
.push((key.into(), agent_owned, scope.clone()));
.push((key.into(), scope.clone()));
if scope.is_some() {
self.events.lock().unwrap().push("register".into());
} else {
Expand Down Expand Up @@ -707,14 +707,13 @@ fn supervisor_registers_routes_records_before_delivery_and_releases() {
let registrations = f.host.registrations.lock().unwrap();
assert_eq!(registrations.len(), 2);
assert_eq!(registrations[0].0, "codemode:r");
assert!(!registrations[0].1);
let scope = registrations[0].2.as_ref().unwrap();
let scope = registrations[0].1.as_ref().unwrap();
assert_eq!(
serde_json::to_value(&scope.selector).unwrap(),
json!({"owner":{"kind":"reserved","module_id":"core"},"ref":"scope-r","epoch":7})
);
assert!(scope.targets.contains("notes"));
assert_eq!(registrations[1], ("codemode:r".into(), false, None));
assert_eq!(registrations[1], ("codemode:r".into(), None));
f.assert_order();
}

Expand Down
8 changes: 2 additions & 6 deletions crates/basal-core/src/decisions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,13 +462,9 @@ pub fn all(conn: &Connection) -> Result<Vec<DecisionRecord>> {
select(conn, "ORDER BY seq", [])
}

fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
use basal_proto::util::hex;

fn pos(p: u64) -> Result<i64> {
i64::try_from(p).map_err(|_| CoreError::Invalid(format!("position {p}")))
}
use crate::model::position_i64 as pos;

/// Writes or updates the reconcile card intent of every unknown call of
/// every run in `needs_reconcile`. Cheap when nothing changed: it reads the
Expand Down
Loading
Loading