mirror of
https://github.com/Dicklesworthstone/coding_agent_session_search.git
synced 2026-09-22 11:59:25 +08:00
5f05938484
Correction to 1f642a24's message: the golden leak was not caused by the golden harness's own stale-on-read refresh — verify7 reproduced it with that opt-out in place. The writer is tests/cli_robot.rs: 52 call sites use tests/fixtures/search_demo_data IN PLACE through a base_cmd() that never disabled auto-refresh, and on a fleet worker the checkout is not under the OS temp dir, so a robot-CLI search spawned `cass index --background` into the committed fixture and ingested that worker's real sessions before the golden tests copied it. base_cmd() now sets CASS_AUTO_REFRESH=0. Proof: verify9 ran the in-place robot tests before golden regeneration in one job and search_robot/stats_full_payload came back byte-identical to HEAD. pages key (ctigq): `cass pages key list|add-password|add-recovery|revoke| rotate --archive <bundle>` over the complete key_management engine (3,915 lines, zero callers until now). Passwords via prompt or --password-stdin (current on line 1, new on line 2), never argv; --json; exit 1 engine refused, 2 usage, 3 not a bundle, 6 password required. Four unit tests on a real encrypted bundle plus a CLI suite (tests/pages_key_cli.rs). docs/RECOVERY.md rewritten to the real surface; README row. WAL hygiene (z2uon): forget --apply, dedup --apply and analytics validate --fix close through the index run's checkpointing close; doctor `archive_wal` (pass ≤ 64 MiB; warn with size and remedy; warn-and-defer under an index run; --fix checkpoints and reports pass / blocked / failed truthfully) with a sparse-WAL e2e in tests/cli_doctor.rs. status/health e2e for db_bytes / wal_bytes / shm_present, now on the `status` surface too (q2vn9). Segment pressure (wpe6s, GH #441): quill_bridge::segment_file_count (metadata-only upper bound), status/health `index.segment_files`, doctor `index_segments` (warn above 8× the merge threshold, `cass index --full` remedy), CASS_TEST_SKIP_POST_RUN_LEXICAL_MAINTENANCE hook and an e2e that builds a 40-segment generation, sees the warning, and proves a plain `cass index` consolidates it. GH #439 (vg2e1) and GH #440 (tcrxi) end-to-end proofs: FTS-repair hooks (sleep per page after the heartbeat; park before the first page) — a slow repair survives a 20 s abort window, a parked one exits 70 with the `index-stalled` envelope; a hook parks a force-rebuild between the staged engine commit and the checkpoint write, records the gap, and a plain `cass index` resumes with exit 0 and no duplicate identities. sources (vdyxd, G.1): setup no longer marks the final sync complete without running it — interactive setup runs `cass sources sync` afterwards and records it only on success; --json setup defers it and reports sync.status=pending. Remote install walks candidate_methods() (binstall → verified prebuilt → cargo install → bootstrap) and falls through on failure instead of stopping at the first choice. `cass sources sync --all` exists (jwox6): doctor, robot-docs and the fleet rehearsal recommended it while clap rejected it. installer (apriu, G.2a): install.sh probes host glibc and falls back to build-from-source below 2.38; proven on ubuntu:22.04 (2.35 → source route) and ubuntu:24.04 (2.39 → prebuilt). docs validator (xwuon, A.9): scripts/validate_docs.sh --keys/--flags/--env/ --truth check README key bindings against the TUI key map (123/123, planted fake key fails), every `cass … --flag` usage against `cass … --help`, and every README env var against the code; found and fixed `sources sync --all`, `timeline --days`, the `<mark>` highlight claim and a dead CASS_UI_METRICS row. gate.sh gains an opt-in docs-truth stage. gate (c2xxq): scripts/gate.sh had a false green — verify8 was cut by the fleet's 30-minute SSH ceiling after fmt+clippy and the script still said GREEN. Now a non-zero rch exit is RED, every expected stage (plus a terminal job-complete marker) must report, the receipt captures both streams and survives a RED run, --lib-only/--no-goldens split runs under the ceiling, the lib stage runs under `timeout`, and the fleet target dir no longer inherits the shell's CARGO_TARGET_DIR. contract (hr9rn): introspect/capabilities has_json_output looks through the subcommand tree (11 commands flip to true and gain the robot-format arg). TUI: the unreachable `Alt+W => swarm cockpit` arm removed. CHANGELOG [Unreleased], AGENTS swarm/issue rules, plan §7b progress log. Verification (fleet, scripts/gate.sh STAGE receipts, RUST_MIN_STACK=16777216): - verify9 (2026-09-02 00:35-00:58): fmt 0, clippy 0, goldens-regen 0, goldens 0 with search_robot/stats_full_payload byte-identical to HEAD after the in-place cli_robot tests ran first (leak fix proven); cli_index/cli_robot/cli_status 101 (Codex fixture names, thresholds, health nesting) fixed in tree and re-run below. - verify10 (01:04-01:19): `cargo test --lib` to completion, 6,847 passed / 6 failed / 38 ignored, no wedge; the six (bookmarks missing from CANONICAL_TOP_LEVEL_COMMANDS, four sqlite3-CLI tests on a worker without sqlite3, daemon socket path over sun_path on a long TMPDIR) fixed in tree and re-run below. - verify11 (05:08-05:23): fmt 0, clippy 0, lib-tests 0 (canonical commands, daemon, sqlite, sources install/setup, pages key, quill_bridge, stall diagnostics, fingerprint cache), pages_key_cli 0, cli_robot 0, cli_doctor 0, tui_flows 0, bookmarks_cli 0, goldens-regen 0, goldens 0; cli_index 101 (gh439 reads the last stdout JSON line now) and cli_status 101 (health nests state.database) fixed in tree. - verify12 (05:41-05:47, full lib suite on this tree): fmt 0, clippy 0, lib-tests 0 — 6,855 passed / 0 failed / 38 ignored in 144 s. - verify13 (05:47-05:55, --regen-goldens): fmt 0, clippy 0, lib-tests 0 (quill_bridge, stall diagnostics), cli_status 0, cli_doctor 0, pages_key_cli 0, goldens-regen 0, goldens 0; cli_index 101 because the gh439 slow-repair test parsed the last LINE of a pretty-printed summary (harness bug) - it now stream-parses the last JSON document. - verify14 (05:57-06:00, cli_index gh439 after that fix): fmt 0, clippy 0, test-cli_index 0 (gate: GREEN). Goldens: 12 reviewed diffs (segment_files / db_bytes / wal_bytes / shm_present fields, has_json_output flips, pages key and sources sync --all in the command trees, doctor archive_wal / index_segments checks); the four that differ only by the regeneration writer's dropped final newline were left at HEAD. Measurement (owner's archive: 10.3 GB db, 200 MB WAL, 267 Quill segment files; `search "borrow checker lifetimes" --robot --robot-meta --limit 5`, five warm runs, CASS_AUTO_REFRESH=0): v0.7.1 median wall 15.3 s (search_ms 6.3-7.5 s, other_ms 1.0-1.9 s); this tree (release build) median wall 1.9 s (search_ms 337-406 ms, other_ms 754-810 ms after a first-run 2.6 s sidecar miss). hits=1 on both. Not yet isolated: the engine-time drop happened with the segment count unchanged, so the per-open seal in the old Quill config is the likelier cause than fragmentation on this archive. Consolidation of the owner's 267 segments was NOT achieved: the documented path (`cass index`, and `cass doctor --fix` for the WAL) never leaves the frankensqlite writable open on that archive - one core, 6.7 GB RSS, ~30k 24-byte header reads/s on the 200 MB WAL, no progress in 7 min - while read-only opens take 1.5 s. On a COPY whose WAL C SQLite checkpointed losslessly (1,012 conversations / 538,807 messages before and after), the same `cass index` gets through the open and indexes, so the WAL is the trigger; C SQLite's quick_check on that copy also reports a doubly- referenced freelist page and free-space corruption on ~35 pages that `cass doctor` calls healthy. The archive's last completed index is 2026-08-14. Recorded as a new P0 in the plan (section 7b); the live archive was not modified. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1329 lines
47 KiB
Rust
1329 lines
47 KiB
Rust
use assert_cmd::Command;
|
|
use coding_agent_search::search::tantivy::{SCHEMA_HASH, expected_index_dir};
|
|
use coding_agent_search::sources::config::{SourceDefinition, SourcesConfig, SyncSchedule};
|
|
use coding_agent_search::storage::sqlite::CURRENT_SCHEMA_VERSION;
|
|
use fs2::FileExt;
|
|
use serde_json::json;
|
|
use std::fs::{self, OpenOptions};
|
|
use std::io::Write;
|
|
use std::path::Path;
|
|
use std::time::Duration;
|
|
|
|
fn write_quarantined_manifest(generation_dir: &Path) {
|
|
fs::create_dir_all(generation_dir).expect("create generation dir");
|
|
fs::write(
|
|
generation_dir.join("lexical-generation-manifest.json"),
|
|
serde_json::to_vec_pretty(&json!({
|
|
"manifest_version": 1,
|
|
"generation_id": "gen-quarantined",
|
|
"attempt_id": "attempt-1",
|
|
"created_at_ms": 1_733_000_000_000_i64,
|
|
"updated_at_ms": 1_733_000_000_321_i64,
|
|
"source_db_fingerprint": "fp-test",
|
|
"conversation_count": 3,
|
|
"message_count": 9,
|
|
"indexed_doc_count": 9,
|
|
"equivalence_manifest_fingerprint": null,
|
|
"shard_plan": null,
|
|
"build_budget": null,
|
|
"shards": [{
|
|
"shard_id": "shard-a",
|
|
"shard_ordinal": 0,
|
|
"state": "quarantined",
|
|
"updated_at_ms": 1_733_000_000_222_i64,
|
|
"indexed_doc_count": 9,
|
|
"message_count": 9,
|
|
"artifact_bytes": 512,
|
|
"stable_hash": "stable-hash-a",
|
|
"reclaimable": false,
|
|
"pinned": false,
|
|
"recovery_reason": null,
|
|
"quarantine_reason": "validation_failed"
|
|
}],
|
|
"merge_debt": {
|
|
"state": "none",
|
|
"updated_at_ms": null,
|
|
"pending_shard_count": 0,
|
|
"pending_artifact_bytes": 0,
|
|
"reason": null,
|
|
"controller_reason": null
|
|
},
|
|
"build_state": "failed",
|
|
"publish_state": "quarantined",
|
|
"failure_history": []
|
|
}))
|
|
.expect("serialize manifest"),
|
|
)
|
|
.expect("write manifest");
|
|
}
|
|
|
|
fn write_generation_manifest(
|
|
generation_dir: &Path,
|
|
generation_id: &str,
|
|
build_state: &str,
|
|
publish_state: &str,
|
|
shard_state: &str,
|
|
pinned: bool,
|
|
reclaimable: bool,
|
|
) {
|
|
fs::create_dir_all(generation_dir).expect("create generation dir");
|
|
fs::write(
|
|
generation_dir.join("lexical-generation-manifest.json"),
|
|
serde_json::to_vec_pretty(&json!({
|
|
"manifest_version": 3,
|
|
"generation_id": generation_id,
|
|
"attempt_id": format!("{generation_id}-attempt"),
|
|
"created_at_ms": 1_733_000_010_000_i64,
|
|
"updated_at_ms": 1_733_000_010_321_i64,
|
|
"source_db_fingerprint": "fp-lifecycle-test",
|
|
"conversation_count": 4,
|
|
"message_count": 12,
|
|
"indexed_doc_count": 12,
|
|
"equivalence_manifest_fingerprint": "eq-lifecycle-test",
|
|
"shard_plan": null,
|
|
"build_budget": null,
|
|
"shards": [{
|
|
"shard_id": format!("{generation_id}-shard"),
|
|
"shard_ordinal": 0,
|
|
"state": shard_state,
|
|
"updated_at_ms": 1_733_000_010_222_i64,
|
|
"indexed_doc_count": 12,
|
|
"message_count": 12,
|
|
"artifact_bytes": 256,
|
|
"stable_hash": format!("{generation_id}-stable"),
|
|
"reclaimable": reclaimable,
|
|
"pinned": pinned,
|
|
"recovery_reason": null,
|
|
"quarantine_reason": if shard_state == "quarantined" { Some("validation_failed") } else { None::<&str> }
|
|
}],
|
|
"merge_debt": {
|
|
"state": "none",
|
|
"updated_at_ms": null,
|
|
"pending_shard_count": 0,
|
|
"pending_artifact_bytes": 0,
|
|
"reason": null,
|
|
"controller_reason": null
|
|
},
|
|
"build_state": build_state,
|
|
"publish_state": publish_state,
|
|
"failure_history": []
|
|
}))
|
|
.expect("serialize manifest"),
|
|
)
|
|
.expect("write manifest");
|
|
fs::write(
|
|
generation_dir.join("segment"),
|
|
format!("{generation_id}-artifact-bytes"),
|
|
)
|
|
.expect("write generation artifact");
|
|
}
|
|
|
|
fn write_remote_source_config(config_home: &Path) {
|
|
let mut source = SourceDefinition::ssh("status-remote", "user@status-remote");
|
|
source.paths = vec!["~/.codex/sessions".to_string()];
|
|
source.sync_schedule = SyncSchedule::Hourly;
|
|
SourcesConfig {
|
|
sources: vec![source],
|
|
disabled_agents: Vec::new(),
|
|
}
|
|
.save_to(&config_home.join("cass/sources.toml"))
|
|
.expect("write sources config");
|
|
}
|
|
|
|
fn isolated_cass_cmd(temp_home: &Path) -> Command {
|
|
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("cass"));
|
|
cmd.env("CODING_AGENT_SEARCH_NO_UPDATE_PROMPT", "1");
|
|
cmd.env("CASS_IGNORE_SOURCES_CONFIG", "1");
|
|
cmd.env("HOME", temp_home);
|
|
cmd.env("XDG_DATA_HOME", temp_home.join(".local/share"));
|
|
cmd.env("XDG_CONFIG_HOME", temp_home.join(".config"));
|
|
cmd.env("CODEX_HOME", temp_home.join(".codex"));
|
|
cmd
|
|
}
|
|
|
|
fn write_ingest_quarantine_record(data_dir: &Path) {
|
|
write_ingest_quarantine_records(data_dir, 1);
|
|
}
|
|
|
|
fn write_ingest_quarantine_records(data_dir: &Path, count: usize) {
|
|
let quarantine_dir = data_dir.join("quarantine");
|
|
fs::create_dir_all(&quarantine_dir).expect("create ingest quarantine dir");
|
|
let now_ms = chrono::Utc::now().timestamp_millis();
|
|
let mut lines = String::new();
|
|
for idx in 0..count {
|
|
let started_at = i64::try_from(idx).expect("idx fits i64") + 1;
|
|
let ended_at = started_at + 1;
|
|
let record = json!({
|
|
"schema_version": 1,
|
|
"conversation_id": format!("tester|/logs/demo-{idx}.jsonl|/workspace/demo|poison-{idx}|{started_at}|{ended_at}|1"),
|
|
"schema_version_at_quarantine": CURRENT_SCHEMA_VERSION,
|
|
"first_quarantined_at_ms": now_ms,
|
|
"last_attempt_at_ms": now_ms,
|
|
"attempt_count": 1,
|
|
"reason": "index-ingest-out-of-memory",
|
|
"error_kind": "out-of-memory",
|
|
"last_error": "out of memory",
|
|
"agent_slug": "tester",
|
|
"external_id": format!("poison-{idx}"),
|
|
"source_path": format!("/logs/demo-{idx}.jsonl"),
|
|
"workspace": "/workspace/demo",
|
|
"started_at": started_at,
|
|
"ended_at": ended_at,
|
|
"message_count": 1
|
|
});
|
|
lines.push_str(&format!("{record}\n"));
|
|
}
|
|
fs::write(quarantine_dir.join("index_ingest_poison.jsonl"), lines)
|
|
.expect("write ingest quarantine record");
|
|
}
|
|
|
|
fn seed_active_rebuild_runtime(data_dir: &Path) -> std::fs::File {
|
|
let db_path = data_dir.join("agent_search.db");
|
|
let index_path = expected_index_dir(data_dir);
|
|
fs::create_dir_all(&index_path).expect("create index dir");
|
|
fs::write(
|
|
index_path.join(".lexical-rebuild-state.json"),
|
|
serde_json::to_vec_pretty(&json!({
|
|
"version": 2,
|
|
"schema_hash": SCHEMA_HASH,
|
|
"db": {
|
|
"db_path": db_path.display().to_string(),
|
|
"total_conversations": 10,
|
|
"total_messages": 20,
|
|
"storage_fingerprint": "seed:10"
|
|
},
|
|
"page_size": 1024,
|
|
"committed_offset": 4,
|
|
"committed_conversation_id": 4,
|
|
"processed_conversations": 4,
|
|
"indexed_docs": 20,
|
|
"committed_meta_fingerprint": null,
|
|
"pending": null,
|
|
"completed": false,
|
|
"updated_at_ms": 1_733_000_123_000_i64,
|
|
"runtime": {
|
|
"queue_depth": 3,
|
|
"inflight_message_bytes": 65_536,
|
|
"max_message_bytes_in_flight": 131_072,
|
|
"pending_batch_conversations": 9,
|
|
"pending_batch_message_bytes": 131_072,
|
|
"page_prep_workers": 6,
|
|
"active_page_prep_jobs": 2,
|
|
"ordered_buffered_pages": 4,
|
|
"budget_generation": 1,
|
|
"producer_budget_wait_count": 2,
|
|
"producer_budget_wait_ms": 17,
|
|
"producer_handoff_wait_count": 1,
|
|
"producer_handoff_wait_ms": 9,
|
|
"host_loadavg_1m_milli": 7_250,
|
|
"controller_mode": "pressure_limited",
|
|
"controller_reason": "queue_depth_3_reached_pipeline_capacity_3",
|
|
"staged_merge_workers_max": 3,
|
|
"staged_merge_allowed_jobs": 1,
|
|
"staged_merge_active_jobs": 1,
|
|
"staged_merge_ready_artifacts": 5,
|
|
"staged_merge_ready_groups": 1,
|
|
"staged_merge_controller_reason": "page_prep_workers_saturated_6_of_6",
|
|
"staged_shard_build_workers_max": 6,
|
|
"staged_shard_build_allowed_jobs": 5,
|
|
"staged_shard_build_active_jobs": 4,
|
|
"staged_shard_build_pending_jobs": 2,
|
|
"staged_shard_build_controller_reason": "reserving_1_slots_for_staged_merge_active_jobs_1_ready_groups_1",
|
|
"updated_at_ms": 1_733_000_124_000_i64
|
|
}
|
|
}))
|
|
.expect("serialize rebuild state"),
|
|
)
|
|
.expect("write rebuild state");
|
|
|
|
let lock_path = data_dir.join("index-run.lock");
|
|
let mut lock_file = OpenOptions::new()
|
|
.create(true)
|
|
.truncate(true)
|
|
.read(true)
|
|
.write(true)
|
|
.open(&lock_path)
|
|
.expect("open lock file");
|
|
lock_file.lock_exclusive().expect("hold index lock");
|
|
writeln!(
|
|
lock_file,
|
|
"pid={}\nstarted_at_ms={}\ndb_path={}\nmode=index",
|
|
std::process::id(),
|
|
1_733_000_111_000_i64,
|
|
db_path.display()
|
|
)
|
|
.expect("write lock metadata");
|
|
lock_file.flush().expect("flush lock metadata");
|
|
lock_file
|
|
}
|
|
|
|
#[test]
|
|
fn status_and_health_json_surface_ingest_quarantine_as_nonfatal_degraded() {
|
|
let test_home = tempfile::tempdir().expect("tempdir");
|
|
let data_dir = test_home.path().join("cass-data");
|
|
fs::create_dir_all(&data_dir).expect("create data dir");
|
|
|
|
let index_out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
"index",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
"--no-progress-events",
|
|
])
|
|
.output()
|
|
.expect("run cass index --json");
|
|
assert!(
|
|
index_out.status.success(),
|
|
"cass index --json failed: stdout={} stderr={}",
|
|
String::from_utf8_lossy(&index_out.stdout),
|
|
String::from_utf8_lossy(&index_out.stderr)
|
|
);
|
|
|
|
write_ingest_quarantine_record(&data_dir);
|
|
|
|
let status_out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
"status",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
])
|
|
.output()
|
|
.expect("run cass status --json");
|
|
assert!(
|
|
status_out.status.success(),
|
|
"cass status --json failed: stdout={} stderr={}",
|
|
String::from_utf8_lossy(&status_out.stdout),
|
|
String::from_utf8_lossy(&status_out.stderr)
|
|
);
|
|
let status_payload: serde_json::Value =
|
|
serde_json::from_slice(&status_out.stdout).expect("status JSON");
|
|
assert_eq!(status_payload["status"].as_str(), Some("healthy"));
|
|
assert_eq!(status_payload["healthy"].as_bool(), Some(true));
|
|
assert_eq!(status_payload["health_level"].as_str(), Some("degraded"));
|
|
assert_eq!(
|
|
status_payload["index"]["quarantined_conversations"].as_u64(),
|
|
Some(1)
|
|
);
|
|
assert_eq!(
|
|
status_payload["ingest_quarantine"]["quarantined_conversations"].as_u64(),
|
|
Some(1)
|
|
);
|
|
assert!(
|
|
status_payload["warnings"]
|
|
.as_array()
|
|
.is_some_and(|warnings| !warnings.is_empty()),
|
|
"status should expose a nonfatal ingest-quarantine warning: {status_payload}"
|
|
);
|
|
assert!(
|
|
status_payload["recommended_action"]
|
|
.as_str()
|
|
.is_some_and(|action| action.contains("quarantine")),
|
|
"status should recommend inspecting quarantine state: {status_payload}"
|
|
);
|
|
|
|
let health_out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
"health",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
])
|
|
.output()
|
|
.expect("run cass health --json");
|
|
assert!(
|
|
health_out.status.success(),
|
|
"cass health --json should remain exit 0 for nonfatal ingest quarantine: stdout={} stderr={}",
|
|
String::from_utf8_lossy(&health_out.stdout),
|
|
String::from_utf8_lossy(&health_out.stderr)
|
|
);
|
|
let health_payload: serde_json::Value =
|
|
serde_json::from_slice(&health_out.stdout).expect("health JSON");
|
|
assert_eq!(health_payload["status"].as_str(), Some("healthy"));
|
|
assert_eq!(health_payload["healthy"].as_bool(), Some(true));
|
|
assert_eq!(health_payload["health_level"].as_str(), Some("degraded"));
|
|
assert_eq!(
|
|
health_payload["ingest_quarantine"]["quarantined_conversations"].as_u64(),
|
|
Some(1)
|
|
);
|
|
assert!(
|
|
health_payload["warnings"]
|
|
.as_array()
|
|
.is_some_and(|warnings| !warnings.is_empty()),
|
|
"health should expose a nonfatal ingest-quarantine warning: {health_payload}"
|
|
);
|
|
}
|
|
|
|
/// uojcg.3.3: health/status/triage and search --robot-meta must carry a
|
|
/// structured `search_completeness` verdict so agents can tell a *stale* index
|
|
/// (still complete) from one that *excludes known conversations* (quarantined).
|
|
#[test]
|
|
fn readiness_surfaces_report_degraded_search_completeness_when_quarantined() {
|
|
let test_home = tempfile::tempdir().expect("tempdir");
|
|
let data_dir = test_home.path().join("cass-data");
|
|
fs::create_dir_all(&data_dir).expect("create data dir");
|
|
|
|
let index_out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
"index",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
"--no-progress-events",
|
|
])
|
|
.output()
|
|
.expect("run cass index --json");
|
|
assert!(
|
|
index_out.status.success(),
|
|
"cass index --json failed: stdout={} stderr={}",
|
|
String::from_utf8_lossy(&index_out.stdout),
|
|
String::from_utf8_lossy(&index_out.stderr)
|
|
);
|
|
|
|
write_ingest_quarantine_record(&data_dir);
|
|
|
|
// The degraded (but searchable) verdict every readiness surface must agree on.
|
|
let assert_degraded = |payload: &serde_json::Value, surface: &str| {
|
|
let sc = &payload["search_completeness"];
|
|
assert_eq!(
|
|
sc["quarantine_status"].as_str(),
|
|
Some("degraded"),
|
|
"{surface} search_completeness should be degraded: {payload}"
|
|
);
|
|
assert_eq!(
|
|
sc["quarantined_conversations"].as_u64(),
|
|
Some(1),
|
|
"{surface} should count the quarantined conversation: {payload}"
|
|
);
|
|
assert_eq!(
|
|
sc["complete"].as_bool(),
|
|
Some(false),
|
|
"{surface} coverage is incomplete: {payload}"
|
|
);
|
|
assert_eq!(
|
|
sc["can_search"].as_bool(),
|
|
Some(true),
|
|
"{surface} ordinary search still runs: {payload}"
|
|
);
|
|
assert_eq!(
|
|
sc["coverage_suspect"].as_bool(),
|
|
Some(false),
|
|
"{surface} a single non-burst quarantine is not suspect: {payload}"
|
|
);
|
|
assert!(
|
|
sc["next_command"]
|
|
.as_str()
|
|
.is_some_and(|cmd| cmd.contains("quarantine")),
|
|
"{surface} should point at the quarantine inspect command: {payload}"
|
|
);
|
|
};
|
|
|
|
for surface in ["status", "triage"] {
|
|
let out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
surface,
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
])
|
|
.output()
|
|
.expect("run cass readiness surface --json");
|
|
assert!(
|
|
out.status.success(),
|
|
"cass {surface} --json failed: stdout={} stderr={}",
|
|
String::from_utf8_lossy(&out.stdout),
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
let payload: serde_json::Value =
|
|
serde_json::from_slice(&out.stdout).expect("parse readiness surface JSON");
|
|
assert_degraded(&payload, surface);
|
|
}
|
|
// triage must also surface the quarantine facts inside its readiness block.
|
|
let triage_out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
"triage",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
])
|
|
.output()
|
|
.expect("run cass triage --json");
|
|
let triage_payload: serde_json::Value =
|
|
serde_json::from_slice(&triage_out.stdout).expect("triage JSON");
|
|
assert_eq!(
|
|
triage_payload["readiness"]["ingest_quarantine"]["quarantined_conversations"].as_u64(),
|
|
Some(1),
|
|
"triage readiness should now include ingest_quarantine: {triage_payload}"
|
|
);
|
|
|
|
// health (exit 0 for a single nonfatal quarantine) carries the same verdict.
|
|
let health_out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
"health",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
])
|
|
.output()
|
|
.expect("run cass health --json");
|
|
assert!(
|
|
health_out.status.success(),
|
|
"cass health --json should stay exit 0 for a single nonfatal quarantine: stdout={} stderr={}",
|
|
String::from_utf8_lossy(&health_out.stdout),
|
|
String::from_utf8_lossy(&health_out.stderr)
|
|
);
|
|
let health_payload: serde_json::Value =
|
|
serde_json::from_slice(&health_out.stdout).expect("health JSON");
|
|
assert_degraded(&health_payload, "health");
|
|
|
|
// search --robot-meta carries the compact verdict in its _meta header.
|
|
let search_out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
"search",
|
|
"anything",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--robot",
|
|
"--robot-meta",
|
|
])
|
|
.output()
|
|
.expect("run cass search --robot --robot-meta");
|
|
let search_payload: serde_json::Value =
|
|
serde_json::from_slice(&search_out.stdout).expect("search JSON");
|
|
let search_meta = &search_payload["_meta"]["search_completeness"];
|
|
assert_eq!(
|
|
search_meta["quarantine_status"].as_str(),
|
|
Some("degraded"),
|
|
"search --robot-meta should carry the degraded completeness verdict: {search_payload}"
|
|
);
|
|
assert_eq!(search_meta["quarantined_conversations"].as_u64(), Some(1));
|
|
}
|
|
|
|
/// uojcg.3.3: with no quarantine, every readiness surface reports complete
|
|
/// coverage (`ok`), and search --robot-meta omits the verdict entirely so the
|
|
/// common payload is unchanged.
|
|
#[test]
|
|
fn readiness_surfaces_report_complete_search_coverage_without_quarantine() {
|
|
let test_home = tempfile::tempdir().expect("tempdir");
|
|
let data_dir = test_home.path().join("cass-data");
|
|
fs::create_dir_all(&data_dir).expect("create data dir");
|
|
|
|
let index_out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
"index",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
"--no-progress-events",
|
|
])
|
|
.output()
|
|
.expect("run cass index --json");
|
|
assert!(index_out.status.success());
|
|
|
|
for surface in ["status", "health", "triage"] {
|
|
let out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
surface,
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
])
|
|
.output()
|
|
.expect("run cass readiness surface --json");
|
|
let payload: serde_json::Value =
|
|
serde_json::from_slice(&out.stdout).expect("parse readiness surface JSON");
|
|
let sc = &payload["search_completeness"];
|
|
assert_eq!(
|
|
sc["quarantine_status"].as_str(),
|
|
Some("ok"),
|
|
"{surface} should report complete coverage: {payload}"
|
|
);
|
|
assert_eq!(sc["complete"].as_bool(), Some(true), "{surface}: {payload}");
|
|
assert_eq!(
|
|
sc["quarantined_conversations"].as_u64(),
|
|
Some(0),
|
|
"{surface}: {payload}"
|
|
);
|
|
}
|
|
|
|
// No quarantine => search --robot-meta omits the verdict (no new bytes).
|
|
let search_out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
"search",
|
|
"anything",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--robot",
|
|
"--robot-meta",
|
|
])
|
|
.output()
|
|
.expect("run cass search --robot --robot-meta");
|
|
let search_payload: serde_json::Value =
|
|
serde_json::from_slice(&search_out.stdout).expect("search JSON");
|
|
assert!(
|
|
search_payload["_meta"].get("search_completeness").is_none(),
|
|
"search --robot-meta should omit search_completeness when nothing is quarantined: {search_payload}"
|
|
);
|
|
}
|
|
|
|
/// uojcg.9.2/9.5: status/triage/doctor --json carry a root_cause attribution
|
|
/// (family/locus/confidence/evidence_refs/summary). On a clean, freshly-indexed
|
|
/// dir no signal implicates a family, so the attribution is the explicit
|
|
/// unknown/unknown record — the field is always present and well-shaped.
|
|
#[test]
|
|
fn status_triage_doctor_emit_root_cause_attribution() {
|
|
let test_home = tempfile::tempdir().expect("tempdir");
|
|
let data_dir = test_home.path().join("cass-data");
|
|
fs::create_dir_all(&data_dir).expect("create data dir");
|
|
|
|
let index_out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
"index",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
"--no-progress-events",
|
|
])
|
|
.output()
|
|
.expect("run cass index --json");
|
|
assert!(index_out.status.success());
|
|
|
|
let assert_root_cause = |rc: &serde_json::Value, surface: &str| {
|
|
assert!(
|
|
rc.get("schema_version")
|
|
.and_then(serde_json::Value::as_u64)
|
|
.is_some(),
|
|
"{surface} root_cause needs schema_version: {rc}"
|
|
);
|
|
assert_eq!(
|
|
rc["family"].as_str(),
|
|
Some("unknown"),
|
|
"{surface} clean dir => unknown family: {rc}"
|
|
);
|
|
assert_eq!(
|
|
rc["confidence"].as_str(),
|
|
Some("unknown"),
|
|
"{surface} clean dir => unknown confidence: {rc}"
|
|
);
|
|
assert!(
|
|
rc.get("locus")
|
|
.and_then(serde_json::Value::as_str)
|
|
.is_some()
|
|
);
|
|
assert!(
|
|
rc.get("evidence_refs")
|
|
.map(serde_json::Value::is_array)
|
|
.unwrap_or(false)
|
|
);
|
|
assert!(
|
|
rc.get("summary")
|
|
.and_then(serde_json::Value::as_str)
|
|
.is_some()
|
|
);
|
|
};
|
|
|
|
for surface in ["status", "triage"] {
|
|
let out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
surface,
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
])
|
|
.output()
|
|
.expect("run cass readiness surface --json");
|
|
let payload: serde_json::Value =
|
|
serde_json::from_slice(&out.stdout).expect("parse readiness surface JSON");
|
|
assert_root_cause(&payload["root_cause"], surface);
|
|
}
|
|
|
|
let doctor_out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
"doctor",
|
|
"check",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
])
|
|
.output()
|
|
.expect("run cass doctor check --json");
|
|
let doctor_payload: serde_json::Value =
|
|
serde_json::from_slice(&doctor_out.stdout).expect("parse doctor JSON");
|
|
assert_root_cause(&doctor_payload["root_cause"], "doctor");
|
|
}
|
|
|
|
#[test]
|
|
fn status_and_health_json_escalate_recent_ingest_quarantine_bursts() {
|
|
let test_home = tempfile::tempdir().expect("tempdir");
|
|
let data_dir = test_home.path().join("cass-data");
|
|
fs::create_dir_all(&data_dir).expect("create data dir");
|
|
|
|
let index_out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
"index",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
"--no-progress-events",
|
|
])
|
|
.output()
|
|
.expect("run cass index --json");
|
|
assert!(
|
|
index_out.status.success(),
|
|
"cass index --json failed: stdout={} stderr={}",
|
|
String::from_utf8_lossy(&index_out.stdout),
|
|
String::from_utf8_lossy(&index_out.stderr)
|
|
);
|
|
|
|
write_ingest_quarantine_records(&data_dir, 3);
|
|
|
|
let status_out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
"status",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
])
|
|
.env("CASS_INGEST_QUARANTINE_CIRCUIT_LIMIT", "3")
|
|
.output()
|
|
.expect("run cass status --json");
|
|
assert!(
|
|
status_out.status.success(),
|
|
"cass status --json failed: stdout={} stderr={}",
|
|
String::from_utf8_lossy(&status_out.stdout),
|
|
String::from_utf8_lossy(&status_out.stderr)
|
|
);
|
|
let status_payload: serde_json::Value =
|
|
serde_json::from_slice(&status_out.stdout).expect("status JSON");
|
|
assert_eq!(status_payload["status"].as_str(), Some("unhealthy"));
|
|
assert_eq!(status_payload["healthy"].as_bool(), Some(false));
|
|
assert_eq!(status_payload["health_level"].as_str(), Some("critical"));
|
|
assert_eq!(
|
|
status_payload["ingest_quarantine"]["circuit_breaker_active"].as_bool(),
|
|
Some(true)
|
|
);
|
|
|
|
let health_out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
"health",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
])
|
|
.env("CASS_INGEST_QUARANTINE_CIRCUIT_LIMIT", "3")
|
|
.output()
|
|
.expect("run cass health --json");
|
|
assert!(
|
|
!health_out.status.success(),
|
|
"cass health --json should exit nonzero for critical ingest quarantine readiness: stdout={} stderr={}",
|
|
String::from_utf8_lossy(&health_out.stdout),
|
|
String::from_utf8_lossy(&health_out.stderr)
|
|
);
|
|
let health_payload: serde_json::Value =
|
|
serde_json::from_slice(&health_out.stdout).expect("health JSON");
|
|
assert_eq!(health_payload["status"].as_str(), Some("unhealthy"));
|
|
assert_eq!(health_payload["healthy"].as_bool(), Some(false));
|
|
assert_eq!(health_payload["health_level"].as_str(), Some("critical"));
|
|
assert!(
|
|
health_payload["errors"]
|
|
.as_array()
|
|
.is_some_and(|errors| errors.iter().any(|err| err
|
|
.as_str()
|
|
.is_some_and(|msg| msg.contains("quarantine circuit breaker")))),
|
|
"health should expose the circuit breaker as an error: {health_payload}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn status_and_health_json_surface_remote_source_sync_summary_without_live_probe() {
|
|
let test_home = tempfile::tempdir().expect("tempdir");
|
|
let data_dir = test_home.path().join("cass-data");
|
|
fs::create_dir_all(&data_dir).expect("create data dir");
|
|
write_remote_source_config(test_home.path());
|
|
|
|
let status_out = Command::new(assert_cmd::cargo::cargo_bin!("cass"))
|
|
.args([
|
|
"status",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
])
|
|
.env("CODING_AGENT_SEARCH_NO_UPDATE_PROMPT", "1")
|
|
.env("XDG_DATA_HOME", test_home.path())
|
|
.env("XDG_CONFIG_HOME", test_home.path())
|
|
.env("HOME", test_home.path())
|
|
.output()
|
|
.expect("run cass status --json");
|
|
assert!(
|
|
status_out.status.success(),
|
|
"cass status --json failed: {}",
|
|
String::from_utf8_lossy(&status_out.stderr)
|
|
);
|
|
|
|
let status_payload: serde_json::Value =
|
|
serde_json::from_slice(&status_out.stdout).expect("status JSON");
|
|
let status_remote = &status_payload["remote_source_sync"];
|
|
assert_eq!(status_remote["checked"].as_bool(), Some(true));
|
|
assert_eq!(status_remote["archive_checked"].as_bool(), Some(false));
|
|
assert_eq!(
|
|
status_remote["live_remote_probe_attempted"].as_bool(),
|
|
Some(false)
|
|
);
|
|
assert_eq!(
|
|
status_remote["remote_source_state"].as_str(),
|
|
Some("local_mirror_gap")
|
|
);
|
|
assert_eq!(
|
|
status_remote["sync_staleness"].as_str(),
|
|
Some("never_synced")
|
|
);
|
|
assert_eq!(
|
|
status_remote["local_mirror_state"].as_str(),
|
|
Some("missing")
|
|
);
|
|
assert_eq!(
|
|
status_remote["configured_remote_source_count"].as_u64(),
|
|
Some(1)
|
|
);
|
|
assert_eq!(
|
|
status_remote["recommended_action"].as_str(),
|
|
Some("cass sources sync --all --json")
|
|
);
|
|
assert_eq!(
|
|
status_payload["doctor_summary"]["remote_source_sync"]["remote_source_state"].as_str(),
|
|
Some("local_mirror_gap")
|
|
);
|
|
|
|
let health_out = Command::new(assert_cmd::cargo::cargo_bin!("cass"))
|
|
.args([
|
|
"health",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
])
|
|
.env("CODING_AGENT_SEARCH_NO_UPDATE_PROMPT", "1")
|
|
.env("XDG_DATA_HOME", test_home.path())
|
|
.env("XDG_CONFIG_HOME", test_home.path())
|
|
.env("HOME", test_home.path())
|
|
.output()
|
|
.expect("run cass health --json");
|
|
let health_payload: serde_json::Value =
|
|
serde_json::from_slice(&health_out.stdout).expect("health JSON");
|
|
assert_eq!(
|
|
health_payload["remote_source_sync"]["remote_source_state"].as_str(),
|
|
Some("local_mirror_gap")
|
|
);
|
|
assert_eq!(
|
|
health_payload["remote_source_sync"]["source"].as_str(),
|
|
Some("health-fast-local-config")
|
|
);
|
|
assert_eq!(
|
|
health_payload["remote_source_sync"]["live_remote_probe_attempted"].as_bool(),
|
|
Some(false)
|
|
);
|
|
assert_eq!(
|
|
health_payload["doctor_summary"]["remote_source_sync"]["recommended_action"].as_str(),
|
|
Some("cass sources sync --all --json")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn status_json_surfaces_runtime_queue_and_byte_budget_headroom() {
|
|
let test_home = tempfile::tempdir().expect("tempdir");
|
|
let data_dir = test_home.path().join("cass-data");
|
|
fs::create_dir_all(&data_dir).expect("create data dir");
|
|
let _lock = seed_active_rebuild_runtime(&data_dir);
|
|
|
|
let out = Command::new(assert_cmd::cargo::cargo_bin!("cass"))
|
|
.args([
|
|
"status",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
])
|
|
.env("CODING_AGENT_SEARCH_NO_UPDATE_PROMPT", "1")
|
|
.env("CASS_IGNORE_SOURCES_CONFIG", "1")
|
|
.env("CASS_TANTIVY_REBUILD_PIPELINE_CHANNEL_SIZE", "5")
|
|
.env("XDG_DATA_HOME", test_home.path())
|
|
.env("HOME", test_home.path())
|
|
.output()
|
|
.expect("run cass status --json");
|
|
assert!(
|
|
out.status.success(),
|
|
"cass status --json failed: {:?}",
|
|
out.status
|
|
);
|
|
|
|
let stdout = String::from_utf8(out.stdout).expect("utf8");
|
|
let payload: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
|
|
let runtime = &payload["rebuild"]["pipeline"]["runtime"];
|
|
let rebuild_progress = &payload["rebuild_progress"];
|
|
|
|
assert_eq!(runtime["queue_depth"].as_u64(), Some(3));
|
|
assert_eq!(runtime["queue_capacity"].as_u64(), Some(5));
|
|
assert_eq!(runtime["queue_headroom"].as_u64(), Some(2));
|
|
assert_eq!(runtime["inflight_message_bytes"].as_u64(), Some(65_536));
|
|
assert_eq!(
|
|
runtime["max_message_bytes_in_flight"].as_u64(),
|
|
Some(131_072)
|
|
);
|
|
assert_eq!(
|
|
runtime["inflight_message_bytes_headroom"].as_u64(),
|
|
Some(65_536)
|
|
);
|
|
assert_eq!(rebuild_progress["active"].as_bool(), Some(true));
|
|
assert_eq!(
|
|
rebuild_progress["processed_conversations"].as_u64(),
|
|
Some(4)
|
|
);
|
|
assert_eq!(rebuild_progress["total_conversations"].as_u64(), Some(10));
|
|
assert_eq!(
|
|
rebuild_progress["remaining_conversations"].as_u64(),
|
|
Some(6)
|
|
);
|
|
assert_eq!(rebuild_progress["completion_ratio"].as_f64(), Some(0.4));
|
|
assert_eq!(rebuild_progress["queue_depth"].as_u64(), Some(3));
|
|
assert_eq!(rebuild_progress["queue_capacity"].as_u64(), Some(5));
|
|
assert_eq!(rebuild_progress["queue_headroom"].as_u64(), Some(2));
|
|
assert_eq!(
|
|
rebuild_progress["inflight_message_bytes"].as_u64(),
|
|
Some(65_536)
|
|
);
|
|
assert_eq!(
|
|
rebuild_progress["inflight_message_bytes_headroom"].as_u64(),
|
|
Some(65_536)
|
|
);
|
|
assert_eq!(
|
|
rebuild_progress["controller_mode"].as_str(),
|
|
Some("pressure_limited")
|
|
);
|
|
assert_eq!(
|
|
rebuild_progress["controller_reason"].as_str(),
|
|
Some("queue_depth_3_reached_pipeline_capacity_3")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn status_json_surfaces_quarantine_gc_summary() {
|
|
let test_home = tempfile::tempdir().expect("tempdir");
|
|
let data_dir = test_home.path().join("cass-data");
|
|
let backups_dir = data_dir.join("backups");
|
|
fs::create_dir_all(&backups_dir).expect("create backups dir");
|
|
|
|
let failed_seed_root =
|
|
backups_dir.join("agent_search.db.20260423T120000.12345.deadbeef.failed-baseline-seed.bak");
|
|
fs::write(&failed_seed_root, b"seed-backup").expect("write failed seed bundle");
|
|
fs::write(
|
|
failed_seed_root.with_file_name(format!(
|
|
"{}-wal",
|
|
failed_seed_root
|
|
.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.expect("file name")
|
|
)),
|
|
b"seed-wal",
|
|
)
|
|
.expect("write failed seed wal");
|
|
|
|
let index_path = expected_index_dir(&data_dir);
|
|
fs::create_dir_all(&index_path).expect("create expected index dir");
|
|
let retained_publish_dir = index_path
|
|
.parent()
|
|
.expect("index parent")
|
|
.join(".lexical-publish-backups");
|
|
fs::create_dir_all(&retained_publish_dir).expect("create retained publish dir");
|
|
let older_backup = retained_publish_dir.join("prior-live-older");
|
|
fs::create_dir_all(&older_backup).expect("create older retained backup");
|
|
fs::write(older_backup.join("segment-a"), b"retained-live-segment-old")
|
|
.expect("write older retained publish backup");
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
let newer_backup = retained_publish_dir.join("prior-live-newer");
|
|
fs::create_dir_all(&newer_backup).expect("create newer retained backup");
|
|
fs::write(newer_backup.join("segment-b"), b"retained-live-segment-new")
|
|
.expect("write newer retained publish backup");
|
|
|
|
let generation_dir = index_path
|
|
.parent()
|
|
.expect("index parent")
|
|
.join("generation-quarantined");
|
|
write_quarantined_manifest(&generation_dir);
|
|
fs::write(
|
|
generation_dir.join("segment-a"),
|
|
b"quarantined-generation-bytes",
|
|
)
|
|
.expect("write quarantined generation artifact");
|
|
|
|
let out = Command::new(assert_cmd::cargo::cargo_bin!("cass"))
|
|
.args([
|
|
"status",
|
|
"--json",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
])
|
|
.env("CODING_AGENT_SEARCH_NO_UPDATE_PROMPT", "1")
|
|
.env("CASS_IGNORE_SOURCES_CONFIG", "1")
|
|
.env("CASS_LEXICAL_PUBLISH_BACKUP_RETENTION", "1")
|
|
.env("XDG_DATA_HOME", test_home.path())
|
|
.env("XDG_CONFIG_HOME", test_home.path())
|
|
.env("HOME", test_home.path())
|
|
.output()
|
|
.expect("run cass status --json");
|
|
assert!(
|
|
out.status.success(),
|
|
"cass status --json failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
|
|
let payload: serde_json::Value = serde_json::from_slice(&out.stdout).expect("valid JSON");
|
|
let quarantine = &payload["quarantine"];
|
|
let summary = &quarantine["summary"];
|
|
|
|
assert_eq!(summary["gc_eligible_asset_count"].as_u64(), Some(1));
|
|
assert!(
|
|
summary["gc_eligible_bytes"].as_u64().unwrap_or(0) > 0,
|
|
"one retained publish backup should fall outside the retention cap"
|
|
);
|
|
assert_eq!(summary["inspection_required_asset_count"].as_u64(), Some(3));
|
|
assert!(
|
|
summary["inspection_required_bytes"].as_u64().unwrap_or(0) > 0,
|
|
"failed seed bundles and quarantined lexical generation should remain inspection-only"
|
|
);
|
|
assert_eq!(
|
|
summary["retained_publish_backup_retention_limit"].as_u64(),
|
|
Some(1)
|
|
);
|
|
|
|
let failed_seed_entries = quarantine["failed_seed_bundle_files"]
|
|
.as_array()
|
|
.expect("status.quarantine must expose failed seed bundle entries");
|
|
assert_eq!(
|
|
failed_seed_entries.len(),
|
|
2,
|
|
"status.quarantine must preserve full failed seed bundle inventory, not only summary"
|
|
);
|
|
assert!(failed_seed_entries.iter().all(|entry| {
|
|
entry["path"]
|
|
.as_str()
|
|
.is_some_and(|path| path.contains(".failed-baseline-seed.bak"))
|
|
|| entry["path"].as_str().is_some_and(|path| {
|
|
path.contains(".failed-baseline-seed.bak-wal")
|
|
|| path.ends_with(".failed-baseline-seed.bak-wal")
|
|
})
|
|
}));
|
|
|
|
let retained_backups = quarantine["retained_publish_backups"]
|
|
.as_array()
|
|
.expect("status.quarantine must expose retained publish backups");
|
|
assert_eq!(
|
|
retained_backups.len(),
|
|
2,
|
|
"status.quarantine must preserve retained publish backup details"
|
|
);
|
|
assert!(
|
|
retained_backups
|
|
.iter()
|
|
.any(|entry| entry["safe_to_gc"].as_bool() == Some(true)),
|
|
"one retained publish backup should be GC-eligible outside the cap"
|
|
);
|
|
assert!(
|
|
retained_backups
|
|
.iter()
|
|
.any(|entry| entry["safe_to_gc"].as_bool() == Some(false)),
|
|
"one retained publish backup should remain protected by the cap"
|
|
);
|
|
|
|
let lexical_generations = quarantine["lexical_generations"]
|
|
.as_array()
|
|
.expect("status.quarantine must expose lexical generation inventory");
|
|
assert_eq!(lexical_generations.len(), 1);
|
|
assert_eq!(
|
|
lexical_generations[0]["generation_id"].as_str(),
|
|
Some("gen-quarantined")
|
|
);
|
|
assert_eq!(
|
|
lexical_generations[0]["publish_state"].as_str(),
|
|
Some("quarantined")
|
|
);
|
|
|
|
let inspection_artifacts = quarantine["quarantined_artifacts"]
|
|
.as_array()
|
|
.expect("status.quarantine must expose flattened quarantined artifacts");
|
|
assert!(
|
|
inspection_artifacts.iter().any(|entry| {
|
|
entry["artifact_kind"].as_str() == Some("lexical_generation")
|
|
&& entry["generation_id"].as_str() == Some("gen-quarantined")
|
|
}),
|
|
"status.quarantine must include the quarantined lexical generation artifact"
|
|
);
|
|
assert!(
|
|
inspection_artifacts.iter().any(|entry| {
|
|
entry["artifact_kind"].as_str() == Some("lexical_shard")
|
|
&& entry["generation_id"].as_str() == Some("gen-quarantined")
|
|
&& entry["shard_id"].as_str() == Some("shard-a")
|
|
}),
|
|
"status.quarantine must include the quarantined shard artifact"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn status_json_surfaces_lexical_generation_lifecycle_inventory() {
|
|
let test_home = tempfile::tempdir().expect("tempdir");
|
|
let data_dir = test_home.path().join("cass-data");
|
|
let index_path = expected_index_dir(&data_dir);
|
|
fs::create_dir_all(&index_path).expect("create expected index dir");
|
|
let generation_root = index_path.parent().expect("index parent");
|
|
|
|
write_generation_manifest(
|
|
&generation_root.join("generation-current"),
|
|
"gen-current",
|
|
"validated",
|
|
"published",
|
|
"published",
|
|
true,
|
|
false,
|
|
);
|
|
write_generation_manifest(
|
|
&generation_root.join("generation-staged"),
|
|
"gen-staged",
|
|
"built",
|
|
"staged",
|
|
"staged",
|
|
false,
|
|
true,
|
|
);
|
|
write_generation_manifest(
|
|
&generation_root.join("generation-failed"),
|
|
"gen-failed",
|
|
"failed",
|
|
"staged",
|
|
"abandoned",
|
|
false,
|
|
true,
|
|
);
|
|
write_generation_manifest(
|
|
&generation_root.join("generation-superseded"),
|
|
"gen-superseded",
|
|
"validated",
|
|
"superseded",
|
|
"published",
|
|
false,
|
|
true,
|
|
);
|
|
write_generation_manifest(
|
|
&generation_root.join("generation-quarantined"),
|
|
"gen-quarantined",
|
|
"failed",
|
|
"quarantined",
|
|
"quarantined",
|
|
false,
|
|
false,
|
|
);
|
|
write_generation_manifest(
|
|
&data_dir
|
|
.join("synced-sessions")
|
|
.join("remote-a")
|
|
.join("generation-decoy-outside-index"),
|
|
"gen-decoy-outside-index",
|
|
"validated",
|
|
"published",
|
|
"published",
|
|
false,
|
|
false,
|
|
);
|
|
|
|
let out = Command::new(assert_cmd::cargo::cargo_bin!("cass"))
|
|
.args([
|
|
"status",
|
|
"--json",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
])
|
|
.env("CODING_AGENT_SEARCH_NO_UPDATE_PROMPT", "1")
|
|
.env("CASS_IGNORE_SOURCES_CONFIG", "1")
|
|
.env("XDG_DATA_HOME", test_home.path())
|
|
.env("XDG_CONFIG_HOME", test_home.path())
|
|
.env("HOME", test_home.path())
|
|
.output()
|
|
.expect("run cass status --json");
|
|
assert!(
|
|
out.status.success(),
|
|
"cass status --json failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
|
|
let payload: serde_json::Value = serde_json::from_slice(&out.stdout).expect("valid JSON");
|
|
let summary = &payload["quarantine"]["summary"];
|
|
|
|
assert_eq!(summary["lexical_generation_count"].as_u64(), Some(5));
|
|
assert_eq!(
|
|
summary["lexical_generation_publish_state_counts"]["published"].as_u64(),
|
|
Some(1)
|
|
);
|
|
assert_eq!(
|
|
summary["lexical_generation_publish_state_counts"]["staged"].as_u64(),
|
|
Some(2)
|
|
);
|
|
assert_eq!(
|
|
summary["lexical_generation_publish_state_counts"]["superseded"].as_u64(),
|
|
Some(1)
|
|
);
|
|
assert_eq!(
|
|
summary["lexical_generation_publish_state_counts"]["quarantined"].as_u64(),
|
|
Some(1)
|
|
);
|
|
assert_eq!(
|
|
summary["lexical_generation_build_state_counts"]["validated"].as_u64(),
|
|
Some(2)
|
|
);
|
|
assert_eq!(
|
|
summary["lexical_generation_build_state_counts"]["built"].as_u64(),
|
|
Some(1)
|
|
);
|
|
assert_eq!(
|
|
summary["lexical_generation_build_state_counts"]["failed"].as_u64(),
|
|
Some(2)
|
|
);
|
|
assert_eq!(
|
|
summary["lexical_quarantined_generation_count"].as_u64(),
|
|
Some(1)
|
|
);
|
|
}
|
|
|
|
/// WS-B.4a (q2vn9): `status --json` and `health --json` must expose the
|
|
/// archive's physical footprint from metadata alone, so an untruncated WAL is
|
|
/// visible without a shell. Positive observable: `database.wal_bytes` equals
|
|
/// the sidecar's actual length, including after the sidecar is grown (sparse,
|
|
/// so the test costs no disk). Planted negative: a data dir with no archive
|
|
/// reports `db_bytes` 0, `wal_bytes` 0 and `shm_present` false instead of
|
|
/// omitting the keys. No-claim: nothing here proves the values are cheap to
|
|
/// compute on a large archive; the observation surfaces read metadata only
|
|
/// by construction.
|
|
#[test]
|
|
fn status_and_health_report_archive_footprint_from_metadata() {
|
|
let test_home = tempfile::tempdir().expect("tempdir");
|
|
let data_dir = test_home.path().join("cass-data");
|
|
fs::create_dir_all(&data_dir).expect("create data dir");
|
|
|
|
let index_out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
"index",
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
"--no-progress-events",
|
|
])
|
|
.output()
|
|
.expect("run cass index --json");
|
|
assert!(
|
|
index_out.status.success(),
|
|
"cass index --json failed: stdout={} stderr={}",
|
|
String::from_utf8_lossy(&index_out.stdout),
|
|
String::from_utf8_lossy(&index_out.stderr)
|
|
);
|
|
|
|
let db_path = data_dir.join("agent_search.db");
|
|
let wal_path = data_dir.join("agent_search.db-wal");
|
|
let footprint = |surface: &str| -> serde_json::Value {
|
|
let out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
surface,
|
|
"--data-dir",
|
|
data_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
])
|
|
.output()
|
|
.unwrap_or_else(|err| panic!("run cass {surface} --json: {err}"));
|
|
let payload: serde_json::Value =
|
|
serde_json::from_slice(&out.stdout).unwrap_or_else(|err| {
|
|
panic!(
|
|
"{surface} json: {err}\nstdout={}\nstderr={}",
|
|
String::from_utf8_lossy(&out.stdout),
|
|
String::from_utf8_lossy(&out.stderr)
|
|
)
|
|
});
|
|
// `status` carries the database block at the top level; `health`
|
|
// nests the same state meta under `state`.
|
|
if surface == "health" {
|
|
payload["state"]["database"].clone()
|
|
} else {
|
|
payload["database"].clone()
|
|
}
|
|
};
|
|
|
|
let db_len = fs::metadata(&db_path).expect("db metadata").len();
|
|
let wal_len = fs::metadata(&wal_path).map_or(0, |meta| meta.len());
|
|
for surface in ["status", "health"] {
|
|
let database = footprint(surface);
|
|
assert_eq!(
|
|
database["db_bytes"].as_u64(),
|
|
Some(db_len),
|
|
"{surface}: {database}"
|
|
);
|
|
assert_eq!(
|
|
database["wal_bytes"].as_u64(),
|
|
Some(wal_len),
|
|
"{surface}: {database}"
|
|
);
|
|
assert!(
|
|
database["shm_present"].is_boolean(),
|
|
"{surface}: {database}"
|
|
);
|
|
}
|
|
|
|
// Grow the sidecar past anything the engine would leave behind; a sparse
|
|
// tail of zeros is invalid WAL content the engine ignores, but it is the
|
|
// size an operator would see with `ls -l`.
|
|
let grown: u64 = 48 * 1024 * 1024;
|
|
let wal = fs::OpenOptions::new()
|
|
.create(true)
|
|
.write(true)
|
|
.truncate(false)
|
|
.open(&wal_path)
|
|
.expect("open wal sidecar");
|
|
wal.set_len(grown).expect("grow wal sidecar");
|
|
drop(wal);
|
|
let database = footprint("status");
|
|
assert_eq!(database["wal_bytes"].as_u64(), Some(grown), "{database}");
|
|
assert_eq!(
|
|
fs::metadata(&wal_path).expect("wal metadata").len(),
|
|
grown,
|
|
"status must not checkpoint or truncate the sidecar"
|
|
);
|
|
|
|
// Planted negative: no archive at all still reports the keys, as zeros.
|
|
let empty_dir = test_home.path().join("empty-data");
|
|
fs::create_dir_all(&empty_dir).expect("create empty data dir");
|
|
let out = isolated_cass_cmd(test_home.path())
|
|
.args([
|
|
"status",
|
|
"--data-dir",
|
|
empty_dir.to_str().expect("utf8"),
|
|
"--json",
|
|
])
|
|
.output()
|
|
.expect("run cass status --json on an empty data dir");
|
|
let payload: serde_json::Value =
|
|
serde_json::from_slice(&out.stdout).expect("status json on empty data dir");
|
|
assert_eq!(
|
|
payload["database"]["exists"].as_bool(),
|
|
Some(false),
|
|
"{payload}"
|
|
);
|
|
assert_eq!(
|
|
payload["database"]["db_bytes"].as_u64(),
|
|
Some(0),
|
|
"{payload}"
|
|
);
|
|
assert_eq!(
|
|
payload["database"]["wal_bytes"].as_u64(),
|
|
Some(0),
|
|
"{payload}"
|
|
);
|
|
assert_eq!(
|
|
payload["database"]["shm_present"].as_bool(),
|
|
Some(false),
|
|
"{payload}"
|
|
);
|
|
}
|