host: preserve journals, full traps and embedded agent output
Write new journal entries under logs/core and merge read-only legacy history without moving SQLite/WAL files. Atomically spool failed batches, retaining unspoolable entries for event-driven retries. Journal full trap diagnostics before UTF-8-safe status display. Port session overflow archives and followable line/byte excerpts to the still-shipped embedded agent. Document retention/recovery and cover the contracts with regressions. Include one rustfmt-only normalization in the existing SDK WASI test. Card: SAU-143
This commit is contained in:
+81
-5
@@ -129,7 +129,7 @@ async fn stderr_flood_does_not_block_stdin() {
|
||||
let payload = json!({"large":"x".repeat(1024*1024)});
|
||||
assert_eq!(call(&h, "probe.echo", payload.clone()).await, payload);
|
||||
h.stop().await;
|
||||
let db = rusqlite::Connection::open(w.0.join("state/core/journal.sqlite")).unwrap();
|
||||
let db = rusqlite::Connection::open(w.0.join("logs/core/journal.sqlite")).unwrap();
|
||||
let count: i64 = db
|
||||
.query_row(
|
||||
"SELECT count(*) FROM journal WHERE line LIKE 'flood %'",
|
||||
@@ -198,7 +198,7 @@ async fn null_error_is_success() {
|
||||
async fn journal_recovers_after_sqlite_write_failure() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let db = rusqlite::Connection::open(w.0.join("state/core/journal.sqlite")).unwrap();
|
||||
let db = rusqlite::Connection::open(w.0.join("logs/core/journal.sqlite")).unwrap();
|
||||
db.busy_timeout(std::time::Duration::from_secs(5)).unwrap();
|
||||
db.execute_batch("CREATE TRIGGER reject_writes BEFORE INSERT ON journal BEGIN SELECT RAISE(FAIL,'test failure'); END;").unwrap();
|
||||
assert_eq!(
|
||||
@@ -206,6 +206,16 @@ async fn journal_recovers_after_sqlite_write_failure() {
|
||||
json!({"failed_log":true})
|
||||
);
|
||||
call(&h, "core.log", json!({})).await;
|
||||
let batches: Vec<Value> = std::fs::read_dir(w.0.join("logs/core/failed-batches"))
|
||||
.unwrap()
|
||||
.map(|file| serde_json::from_slice(&std::fs::read(file.unwrap().path()).unwrap()).unwrap())
|
||||
.collect();
|
||||
assert!(
|
||||
batches
|
||||
.iter()
|
||||
.flat_map(|batch| batch.as_array().unwrap())
|
||||
.any(|entry| entry["line"].as_str().unwrap().contains("failed_log"))
|
||||
);
|
||||
db.execute_batch("DROP TRIGGER reject_writes;").unwrap();
|
||||
assert_eq!(
|
||||
call(&h, "probe.echo", json!({"recovered":true})).await,
|
||||
@@ -1068,7 +1078,7 @@ async fn journal_filters_and_entries_persist_in_sqlite() {
|
||||
);
|
||||
h.stop().await;
|
||||
assert!(
|
||||
std::fs::read(w.0.join("state/core/journal.sqlite"))
|
||||
std::fs::read(w.0.join("logs/core/journal.sqlite"))
|
||||
.unwrap()
|
||||
.starts_with(b"SQLite format 3")
|
||||
);
|
||||
@@ -1254,8 +1264,9 @@ async fn journal_entries_have_uuid_time_identity_stream_and_line() {
|
||||
async fn journal_has_wal_and_identity_indexes() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
assert!(!w.0.join("state/core/journal.sqlite").exists());
|
||||
h.stop().await;
|
||||
let db = rusqlite::Connection::open(w.0.join("state/core/journal.sqlite")).unwrap();
|
||||
let db = rusqlite::Connection::open(w.0.join("logs/core/journal.sqlite")).unwrap();
|
||||
assert_eq!(
|
||||
db.query_row("PRAGMA journal_mode", [], |r| r.get::<_, String>(0))
|
||||
.unwrap(),
|
||||
@@ -1941,7 +1952,7 @@ async fn core_restart_cleanly_stops_detached_caller() {
|
||||
std::fs::read_to_string(w.0.join("state/restart-stop")).unwrap(),
|
||||
"persisted after EOF"
|
||||
);
|
||||
let db = rusqlite::Connection::open(w.0.join("state/core/journal.sqlite")).unwrap();
|
||||
let db = rusqlite::Connection::open(w.0.join("logs/core/journal.sqlite")).unwrap();
|
||||
let count: i64 = db
|
||||
.query_row(
|
||||
"SELECT count(*) FROM journal WHERE line = 'restart stop hook finished'",
|
||||
@@ -1995,3 +2006,68 @@ async fn core_exit_alias_sets_executable_status_75() {
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
/// PROTOCOL 7: logs move, state and the old journal do not; core.log merges both.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn journal_location_preserves_legacy_history() {
|
||||
let w = TestWorld::new();
|
||||
let old = w.0.join("state/core/journal.sqlite");
|
||||
let db = rusqlite::Connection::open(&old).unwrap();
|
||||
db.execute_batch("CREATE TABLE journal (id TEXT PRIMARY KEY, time TEXT, pid TEXT, plugin TEXT, stream TEXT, line TEXT);
|
||||
INSERT INTO journal VALUES ('0000','old time','old-pid','old-plugin','core','legacy history');").unwrap();
|
||||
drop(db);
|
||||
let before = std::fs::read(&old).unwrap();
|
||||
let history = w.0.join("state/core/console-history");
|
||||
std::fs::write(&history, "keep console history").unwrap();
|
||||
let h = w.start().await;
|
||||
call(&h, "probe.echo", json!({})).await;
|
||||
assert!(w.0.join("logs/core/journal.sqlite").exists());
|
||||
let entries = call(&h, "core.log", json!({"limit":-1})).await;
|
||||
assert_eq!(entries[0]["line"], "legacy history");
|
||||
assert!(entries.as_array().unwrap().len() > 1);
|
||||
assert_eq!(
|
||||
call(&h, "core.log", json!({"pid":"old-pid"})).await[0]["line"],
|
||||
"legacy history"
|
||||
);
|
||||
h.stop().await;
|
||||
assert_eq!(std::fs::read(&old).unwrap(), before);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(history).unwrap(),
|
||||
"keep console history"
|
||||
);
|
||||
}
|
||||
|
||||
/// PROTOCOL 7: the journal retains the full trap, not just its short status reason.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn full_trap_diagnostic_is_journaled() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let error = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(20),
|
||||
h.call("probe.trap", json!({})),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_err();
|
||||
assert_eq!(error["code"], -32002);
|
||||
assert!(
|
||||
error["data"]["status"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.starts_with("trap: wasm trap:")
|
||||
);
|
||||
let entries = call(
|
||||
&h,
|
||||
"core.log",
|
||||
json!({"plugin":"probe","grep":"wasm backtrace"}),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
entries.as_array().unwrap().iter().any(|e| {
|
||||
let text = e["line"].as_str().unwrap();
|
||||
text.contains("Caused by:") && text.contains("wasm trap:")
|
||||
}),
|
||||
"{entries}"
|
||||
);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user