Delete the migration-era read-back tests and the leaf's overlapping real-store gate
mind::tests::snapshot_admits_and_reads_back (an O(N^2) per-document Mind::view loop plus the six idunn-watchdog resolutions and over-64-byte withdrawals of the 2026-10-01 migration) and eureka-pipeline tests::stored_documents_read_back go, with the leaf README's real-store section. The one gate follows in huginn-mind. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
dd07485b4e
commit
fe9069e528
4 changed files with 4 additions and 174 deletions
|
|
@ -18,13 +18,6 @@ and every encoding-relevant variant against `golden/envelopes.txt`. A diff
|
|||
there is a wire change; do not regenerate it to make the test pass. An additive
|
||||
field adds golden lines and never changes an existing one.
|
||||
|
||||
Opt-in, against a real store: `stored_documents_read_back` decodes, validates
|
||||
and keys every stored pipeline document and asserts that it re-encodes to the
|
||||
stored key, type and payload bytes. Take a copy of the store (the test copies
|
||||
the file again before opening it, so the copy may be read-only), never open the
|
||||
live one:
|
||||
|
||||
```sh
|
||||
sudo cp /var/lib/gamecult/huginn/minds/eureka/mind.redb "$SNAP/minds/eureka/mind.redb"
|
||||
HUGINN_MIND_SNAPSHOT="$SNAP" cargo test -p eureka-pipeline -- --ignored --exact tests::stored_documents_read_back --nocapture
|
||||
```
|
||||
The real-store gate is huginn-mind's `#[ignore]` `query::tests::stored_documents_read_back`,
|
||||
which checks these stored bytes and the read path together (the leaf cannot see the read
|
||||
path); the Huginn Upgrade runbook in gamecult-ops runs it.
|
||||
|
|
|
|||
|
|
@ -2747,79 +2747,6 @@ mod tests {
|
|||
assert!(!Sha("5f98228a".into()).names_same_commit(&Sha("5f98228b".into())), "equal length, differing");
|
||||
}
|
||||
|
||||
/// stored-documents-valid and stored-bytes-reproduce, on the real mind.
|
||||
/// `HUGINN_MIND_SNAPSHOT` is a copy of a state root holding
|
||||
/// `minds/eureka/mind.redb`; the test copies the store file into a scratch
|
||||
/// directory first (redb opens read-write), so the snapshot may be
|
||||
/// read-only. Read through cultcache-rs's own store. Each pipeline
|
||||
/// document must re-prepare to its stored envelope: key, type and payload
|
||||
/// bytes.
|
||||
#[test]
|
||||
#[ignore = "needs HUGINN_MIND_SNAPSHOT, a copy of a Huginn state root; see the crate README"]
|
||||
fn stored_documents_read_back() {
|
||||
use cultcache_rs::{CacheBackingStore, OwnedRedbMessagePackBackingStore};
|
||||
let root = std::env::var("HUGINN_MIND_SNAPSHOT").expect("HUGINN_MIND_SNAPSHOT names the snapshot state root");
|
||||
let scratch = std::env::temp_dir().join(format!("eureka-pipeline-readback-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&scratch).expect("scratch directory");
|
||||
let working = scratch.join("mind.redb");
|
||||
std::fs::copy(Path::new(&root).join("minds").join("eureka").join("mind.redb"), &working).expect("the snapshot copies");
|
||||
let store = OwnedRedbMessagePackBackingStore::new(&working).expect("the snapshot opens");
|
||||
let cache = schema_cache().expect("the registrar registers");
|
||||
let envelopes = store.pull_all().expect("the snapshot reads");
|
||||
let mut stored = Vec::new();
|
||||
for envelope in &envelopes {
|
||||
match PipelineDocument::decode(envelope) {
|
||||
Ok(document) => {
|
||||
assert_eq!(document.validate(), Ok(()), "{}: valid", envelope.key);
|
||||
assert_eq!(pipeline_key(&document).as_deref(), Ok(envelope.key.as_str()), "{}: key derives byte for byte", envelope.key);
|
||||
let again = document.prepare(&cache).unwrap_or_else(|error| panic!("{}: does not re-prepare: {error}", envelope.key));
|
||||
assert_eq!(
|
||||
(&again.key, &again.r#type, &again.payload),
|
||||
(&envelope.key, &envelope.r#type, &envelope.payload),
|
||||
"{}: re-encodes to the stored bytes",
|
||||
envelope.key
|
||||
);
|
||||
stored.push((document.kind(), envelope.key.clone()));
|
||||
}
|
||||
Err(PipelineRefusal::ForeignStore { .. }) => {}
|
||||
Err(error) => panic!("{}: does not decode: {error}", envelope.key),
|
||||
}
|
||||
}
|
||||
assert!(stored.len() > 200, "{} pipeline documents read", stored.len());
|
||||
let mut subjects = 0;
|
||||
let resolvable = |kind: &PipelineKind| !matches!(kind, PipelineKind::Resolution | PipelineKind::Campaign | PipelineKind::Instance);
|
||||
for (kind, key) in stored.iter().filter(|(kind, _)| resolvable(kind)) {
|
||||
let one = resolved_at(*kind, key, 1).unwrap_or_else(|error| panic!("{key}: no resolution keys: {error}"));
|
||||
let two = resolved_at(PipelineKind::Resolution, &one, 1).unwrap_or_else(|error| panic!("{key}: no withdrawal keys: {error}"));
|
||||
for derived in [&one, &two] {
|
||||
pipeline_id("read_back", derived, PipelineKind::Resolution).unwrap_or_else(|error| panic!("{derived}: does not parse: {error}"));
|
||||
}
|
||||
subjects += 1;
|
||||
}
|
||||
for (_, key) in stored.iter().filter(|(kind, _)| *kind == PipelineKind::Resolution) {
|
||||
resolved_at(PipelineKind::Resolution, key, 1).unwrap_or_else(|error| panic!("{key}: cannot be withdrawn: {error}"));
|
||||
}
|
||||
// The six findings that could not be resolved before this cut.
|
||||
let stuck = [
|
||||
"cut-bifrost-retire-alarm.s2.verb-default-accepts-malformed",
|
||||
"cut-ops-notice-deploy.s2.pinned-bifrost-predates-retry",
|
||||
"cut-bifrost-notice-retry.s1.unknown-test-ignores-backoff",
|
||||
"cut-bifrost-notice-retry.s2.closed-unknown-retry-unpinned",
|
||||
"cut-bifrost-notice-retry.s2.flapping-clock-retries-every-tick",
|
||||
"cut-idunn-topology-lock.s2.boot-reconcile-skipped-on-contention",
|
||||
];
|
||||
for label in stuck {
|
||||
let found = stored
|
||||
.iter()
|
||||
.find(|(kind, key)| *kind == PipelineKind::Finding && key.ends_with(label))
|
||||
.unwrap_or_else(|| panic!("{label}: not in the snapshot"));
|
||||
let one = resolved_at(PipelineKind::Finding, &found.1, 1).unwrap_or_else(|error| panic!("{label}: still stuck: {error}"));
|
||||
assert!(one.split(':').nth(2).is_some_and(|local| local.len() > SUBJECT_LOCAL_MAX), "{label}: the old bound refused it");
|
||||
}
|
||||
eprintln!("read back {} pipeline documents, {subjects} subjects", stored.len());
|
||||
let _ = std::fs::remove_dir_all(&scratch);
|
||||
}
|
||||
|
||||
/// The total bound, in the one place it lives: parts that are each a legal
|
||||
/// label compose a local wider than 64 bytes and are refused as a whole.
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -786,94 +786,4 @@ mod tests {
|
|||
}))
|
||||
);
|
||||
}
|
||||
|
||||
/// stored-documents-valid and every-subject-resolvable, against the real
|
||||
/// mind: every stored document views back unchanged; the six findings the
|
||||
/// 64-byte bound left unresolvable take a resolution; and a stored
|
||||
/// resolution whose withdrawal key is over 64 bytes is withdrawn. Run it
|
||||
/// on a scratch copy of the state root only: it writes.
|
||||
#[test]
|
||||
#[ignore = "reads and writes the scratch copy of a live state root named by HUGINN_MIND_SNAPSHOT"]
|
||||
fn snapshot_admits_and_reads_back() {
|
||||
use crate::fixtures::{admit, r, resolution, withdrawn};
|
||||
use eureka_pipeline::{PipelineRef, ResolutionOutcome, Short, pipeline_key};
|
||||
|
||||
let root = std::env::var("HUGINN_MIND_SNAPSHOT").expect("HUGINN_MIND_SNAPSHOT names a scratch copy of the state root");
|
||||
let mut mind = Mind::open(Path::new(&root), &slug("eureka")).unwrap();
|
||||
|
||||
// The census is taken apart from the loop that checks each document.
|
||||
let pipeline_envelopes = mind
|
||||
.envelopes()
|
||||
.iter()
|
||||
.filter(|envelope| PipelineKind::ALL.iter().any(|kind| kind.type_id() == envelope.r#type))
|
||||
.count();
|
||||
assert!(pipeline_envelopes > 0, "the snapshot held no pipeline document");
|
||||
|
||||
let mut viewed = 0;
|
||||
let mut resolutions = Vec::new();
|
||||
for envelope in mind.envelopes() {
|
||||
let Some(kind) = PipelineKind::ALL.iter().copied().find(|kind| kind.type_id() == envelope.r#type) else { continue };
|
||||
let stored = PipelineDocument::decode(envelope).unwrap();
|
||||
stored.validate().unwrap_or_else(|refusal| panic!("{} no longer validates: {refusal:?}", envelope.key));
|
||||
let rekeyed = pipeline_key(&stored).unwrap_or_else(|refusal| panic!("{} no longer keys: {refusal:?}", envelope.key));
|
||||
assert_eq!(rekeyed, envelope.key, "stored key differs from the key the leaf computes");
|
||||
let view = mind
|
||||
.view(&PipelineRef { kind, id: Short(envelope.key.clone()) })
|
||||
.unwrap()
|
||||
.unwrap_or_else(|| panic!("{} does not view", envelope.key));
|
||||
assert_eq!(view.document, stored, "{}", envelope.key);
|
||||
if let PipelineDocument::Resolution(held) = &stored
|
||||
&& !matches!(held.outcome, eureka_pipeline::ResolutionOutcome::Withdrawn { .. })
|
||||
&& !envelope.key.contains(":resolution:resolution.")
|
||||
{
|
||||
resolutions.push(envelope.key.clone());
|
||||
}
|
||||
viewed += 1;
|
||||
}
|
||||
assert_eq!(viewed, pipeline_envelopes, "every pipeline envelope validates, rekeys and views back");
|
||||
println!("validated, rekeyed and viewed {viewed} stored documents unchanged");
|
||||
|
||||
let stuck = [
|
||||
"cut-bifrost-retire-alarm.s2.verb-default-accepts-malformed",
|
||||
"cut-ops-notice-deploy.s2.pinned-bifrost-predates-retry",
|
||||
"cut-bifrost-notice-retry.s1.unknown-test-ignores-backoff",
|
||||
"cut-bifrost-notice-retry.s2.closed-unknown-retry-unpinned",
|
||||
"cut-bifrost-notice-retry.s2.flapping-clock-retries-every-tick",
|
||||
"cut-idunn-topology-lock.s2.boot-reconcile-skipped-on-contention",
|
||||
];
|
||||
let mut resolved = 0;
|
||||
for local in stuck {
|
||||
let subject = r(PipelineKind::Finding, &format!("idunn-watchdog:finding:{local}"));
|
||||
let outcome = admit(&mut mind, vec![resolution(subject, ResolutionOutcome::Recorded { reason: "snapshot read-back".into() })]);
|
||||
match outcome {
|
||||
PipelineAdmissionOutcome::Committed { writes, .. } => {
|
||||
assert_eq!(writes.len(), 1, "{local}");
|
||||
resolved += 1;
|
||||
}
|
||||
other => panic!("stuck finding {local} was not resolved: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(resolved, stuck.len(), "every stuck finding resolves");
|
||||
|
||||
let mut candidates = Vec::new();
|
||||
for key in resolutions {
|
||||
let withdrawal = resolution(r(PipelineKind::Resolution, &key), withdrawn());
|
||||
let withdrawal_key = pipeline_key(&withdrawal).unwrap_or_else(|refusal| panic!("withdrawal of {key} does not key: {refusal:?}"));
|
||||
if withdrawal_key.rsplit(':').next().unwrap().len() > 64 {
|
||||
candidates.push((key, withdrawal));
|
||||
}
|
||||
}
|
||||
assert!(!candidates.is_empty(), "no stored resolution had a withdrawal key over 64 bytes");
|
||||
let wanted = candidates.len();
|
||||
let mut withdrew = 0;
|
||||
for (key, withdrawal) in candidates {
|
||||
match admit(&mut mind, vec![withdrawal]) {
|
||||
PipelineAdmissionOutcome::Committed { .. } => withdrew += 1,
|
||||
other => panic!("withdrawal of {key} was not committed: {other:?}"),
|
||||
}
|
||||
}
|
||||
assert_eq!(withdrew, wanted, "every candidate withdraws");
|
||||
println!("withdrew {withdrew} resolutions whose withdrawal keys exceed 64 bytes");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -326,7 +326,7 @@ pipeline kinds are in Huginn `docs/eureka-substrate-map.md`.
|
|||
| Kind | Identity (what names it) | Lifecycle (what happens over time) | Authority (who decides) |
|
||||
| --- | --- | --- | --- |
|
||||
| Pipeline wire ids | `epiphany.pipeline.<kind>.v2`, epoch `epiphany.pipeline.epoch.v2`, defined by the leaf, the Huginn crate `crates/eureka-pipeline` (lib `eureka_pipeline`), ids unchanged. | Renamed once to `eureka.pipeline.<kind>.v3` by one `mind.redb` migration, at the next breaking leaf change or before the first hosted mind (ruling `wire-names-rename-before-hosting`). Adding the `run` kind or the `Mind` authority variant is additive (old stored documents decode; no migration), so it does not trigger the rename; a field added with the leaf's `= absent` marker is additive too (leaf-read-anchors r2: old bytes decode, empty fields encode byte-identically, `golden/envelopes.txt` gains lines and loses none); a field added any other way would. | Huginn owns the leaf. The migration is a live-mind upgrade, which is the operator's (`mind-rules-which-forks`). |
|
||||
| Leaf crate and schema catalogue | Huginn `crates/eureka-pipeline`, `schemas/cultnet/` (13 pipeline schemas, `gamecult.persona_state.v0`, index, README). `epiphany.work_organ_state.v0` dies: no reader outside Epiphany (grep, F1/F3). | Landed at Huginn `afc35f9` (leaf-into-huginn). Wire pinned by `crates/eureka-pipeline/golden/envelopes.txt` and the opt-in `stored_documents_read_back`. Huginn is AGPL-3.0-only (ruling `eureka-body:ruling:leaf-relicensed-agpl`). | Huginn. Forbidden: any Cargo source resolving into GameCult/Epiphany (`nothing-runs-from-epiphany`). |
|
||||
| Leaf crate and schema catalogue | Huginn `crates/eureka-pipeline`, `schemas/cultnet/` (13 pipeline schemas, `gamecult.persona_state.v0`, index, README). `epiphany.work_organ_state.v0` dies: no reader outside Epiphany (grep, F1/F3). | Landed at Huginn `afc35f9` (leaf-into-huginn). Wire pinned by `crates/eureka-pipeline/golden/envelopes.txt` and huginn-mind's opt-in `query::tests::stored_documents_read_back`. Huginn is AGPL-3.0-only (ruling `eureka-body:ruling:leaf-relicensed-agpl`). | Huginn. Forbidden: any Cargo source resolving into GameCult/Epiphany (`nothing-runs-from-epiphany`). |
|
||||
| `run` | `<instance>:run:<label>` (cut-run-kind r2). Label `mind-<YYYYMMDD>T<HHMMSS><mmm>Z` from mind-launch's `run_label`; two launches in one millisecond produce the same key with different content, which admission answers `Conflict`, so the name stays injective. Order is admission ordinal (F15), not label. | Each wake opens a PersonaTurn run with no claims (ruling `eureka-substrate:ruling:every-wake-a-run`); the Persona tool opens SelfRun runs with claims; both go through `open_and_launch`, and the unit starts only after the run commits (the run is the grant). The Self in a unit admits under its label and ends it `Recorded { reason }`. A run whose unit is not alive past a 120 s start grace is closed `Recorded { reason: "unit not active" }` by the next `open_and_launch` of the same turn, before it checks anything else. It is Recorded, not Withdrawn, so a unit that keeps dying counts as an empty ended run and trips the breaker. Only a unit whose start failed in the launch itself is Withdrawn. Cadence comes from the newest PersonaTurn run's `admitted_at` (any status), not `started_on` (a date cannot carry seconds). | Opening: `open_and_launch`, called by the waker for PersonaTurn and by the Persona tool for SelfRun, each with a `Grant` that only `read_effective` constructs. One live run of hers per turn and instance: huginn-mind admission refuses a second in-force run with `operated_by: Mind` of the same turn and instance as `AlreadyLive { run }` (ruling `one-live-self-run`, option `one-live-per-turn`; cut run-live-admission). `open_and_launch` answers `Busy` from that refusal, or earlier, when the holder it has just read is alive. Operator runs never count. `operated_by` is declared by the writer (follow-up `eureka-substrate:follow_up:operator-authority-needs-operator-credential`). Ending: the Self that worked it; a dead one is closed by the next launch of its turn (cut mind-launch r3). No sweep exists elsewhere. Forbidden: Hands, Soul, Imagination, Life. |
|
||||
| Repetition breaker | No state of its own (rulings `self-waker`, `merge-gate-own-soul`). Trip condition derived from one selection: the documents citing the item, in any role. Counted runs are the item's ended SelfRun runs of hers (`operated_by: Mind`, not withdrawn) admitted after the newest question `raised_in` the item. Trip when the last 3 counted runs are empty, or 10 of the last 50. A run is empty when no citing document other than a run has an ordinal in its window (its admission up to the next run's, or now). Documents that cite something else never count, wherever they were admitted (F21). | Evaluated in `open_and_launch` when a Self run claims the item, not by the waker. When tripped, it admits one `question` labelled `breaker-<run label>` under the item's campaign, `raised_in` the item, and launches nothing. The run label is the label the refused launch would have used, so every trip has its own key. The existing blocked-spec recipe then removes the item from the queue. Answered by a ruling or withdrawn: either reopens the item, and that question is the boundary, so a reopened item starts a fresh count. | `open_and_launch` admits the question; any Self may answer it (substrate-internal, `mind-rules-which-forks`). |
|
||||
| Brake | One document per instance in a root-owned CultCache store `/etc/gamecult/minds/<instance>/control.cc`, type `eureka.control.brake.v1 { released: bool, set_at, set_by }`. | Replaced whole by the operator's CLI; never appended. Absent or undecodable reads as held. `mind-control brake hold` writes it held and then, in the same command, stops her running units: `systemctl stop` (blocking) over `mind-self@<i>:*` and `mind-persona@<i>:*`, through `SystemdLauncher::stop_all` (cut brake-stop; ruling `self-run-actuator`; F18-F20). | Operator only (ruling `brake-dial-root-store-now`); her units read it, and the file is not writable by any user she runs as. The hold is the one stopper of her units. `read_effective` is the one reader. Its `Released` answer carries a `Grant` (cadence and run cap, private fields, not `Clone`), which `open_and_launch` takes by value, so no run opens without a released reading (cut mind-launch r3). |
|
||||
|
|
|
|||
Loading…
Reference in a new issue