C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\mod.rs-254-// [impl->REQ-RUN-PICKER]
crates\spt\src\picker\mod.rs-255-fn dispatch(outcome: Outcome) -> i32 {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\mod.rs-256-    match outcome {
crates\spt\src\picker\mod.rs:257:        Outcome::Run {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\mod.rs-258-            adapter,
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\mod.rs-259-            id,
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\mod.rs-260-            resume,
--
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-333-/// fns (`cmd_endpoint_run` / `cmd_fork` / the rc pump) — no second bringup path
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-334-/// (the gate invariant).
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-335-#[derive(Debug, Clone, PartialEq, Eq)]
crates\spt\src\picker\model.rs:336:pub enum Outcome {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-337-    /// Bring up (or resume) an endpoint then start/attach/view — routes through
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-338-    /// `cmd_endpoint_run` (the one bringup core).
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-339-    Run {
--
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-530-            return None;
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-531-        }
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-532-        let adapter = self.selected_adapter()?.address();
crates\spt\src\picker\model.rs:533:        Some(Outcome::Run {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-534-            adapter,
crates\spt\src\picker\model.rs-535-            id: self.id_buffer.clone(),
crates\spt\src\picker\model.rs-536-            resume: None,
--
crates\spt\src\picker\model.rs-696-    pub fn confirm_terminal(&self, opt: ConfirmOption) -> Option<Outcome> {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-697-        let ep = self.selected_endpoint()?;
crates\spt\src\picker\model.rs-698-        let online = ep.status == EpStatus::Online;
crates\spt\src\picker\model.rs:699:        let bringup = |mode| Outcome::Run {
crates\spt\src\picker\model.rs-700-            adapter: ep.adapter_profile.clone(),
crates\spt\src\picker\model.rs-701-            id: ep.id.clone(),
crates\spt\src\picker\model.rs-702-            resume: None,
--
crates\spt\src\picker\model.rs-751-        if ep.status != EpStatus::Offline {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-752-            return None;
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-753-        }
crates\spt\src\picker\model.rs:754:        Some(Outcome::Run {
crates\spt\src\picker\model.rs-755-            adapter: ep.adapter_profile.clone(),
crates\spt\src\picker\model.rs-756-            id: ep.id.clone(),
crates\spt\src\picker\model.rs-757-            resume: None,
--
crates\spt\src\picker\model.rs-784-    \ W4 UX) brings it up detached (`RunMode::Start`); the default ATTACHES
crates\spt\src\picker\model.rs-785-    \ (`RunMode::Attach`) so a resume lands you back in the live session.
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-786-    // [impl->REQ-RUN-PICKER]
crates\spt\src\picker\model.rs:787:    pub fn resume_outcome(&self, headless: bool) -> Option<Outcome> {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-788-        let row = self.selected_resume()?;
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-789-        let session_id = row.session_id.clone();
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-790-        // Thread the row's recorded cwd so the resumed harness lands in the
--
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-793-        // cmd_endpoint_run's fallback chain (info.cwd → current_dir).
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-794-        let cwd = row.cwd.clone();
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-795-        let ep = self.selected_endpoint()?;
crates\spt\src\picker\model.rs:796:        Some(Outcome::Run {
crates\spt\src\picker\model.rs-797-            adapter: ep.adapter_profile.clone(),
crates\spt\src\picker\model.rs-798-            id: ep.id.clone(),
crates\spt\src\picker\model.rs-799-            resume: Some(session_id),
--
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1120-            m.id_push(c);
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1121-        }
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1122-        match m.create_outcome().unwrap() {
crates\spt\src\picker\model.rs:1123:            Outcome::Run { adapter, id, resume, cwd: _, mode } => {
crates\spt\src\picker\model.rs-1124-                assert_eq!(adapter, "claude-spt:fast");
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1125-                assert_eq!(id, "doyle");
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1126-                assert_eq!(resume, None);
--
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1180-        m2.enter_pick();
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1181-        assert_eq!(
crates\spt\src\picker\model.rs-1182-            m2.confirm_terminal(ConfirmOption::Start),
crates\spt\src\picker\model.rs:1183:            Some(Outcome::Run {
crates\spt\src\picker\model.rs-1184-                adapter: "claude-spt".into(),
crates\spt\src\picker\model.rs-1185-                id: "cold".into(),
crates\spt\src\picker\model.rs-1186-                resume: None,
--
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1192-        // the `h` headless escape brings the SAME offline endpoint up detached.
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1193-        assert_eq!(
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1194-            m2.start_headless_outcome(),
crates\spt\src\picker\model.rs:1195:            Some(Outcome::Run {
crates\spt\src\picker\model.rs-1196-                adapter: "claude-spt".into(),
crates\spt\src\picker\model.rs-1197-                id: "cold".into(),
crates\spt\src\picker\model.rs-1198-                resume: None,
--
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1215-
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1216-    // [unit->REQ-RUN-PICKER] resume picks a prior session and bakes --resume.
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1217-    #[test]
crates\spt\src\picker\model.rs:1218:    fn resume_outcome_bakes_session() {
crates\spt\src\picker\model.rs-1219-        let mut e = ep("cold", "g", EpStatus::Offline, true);
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1220-        e.resume_rows = vec![
crates\spt\src\picker\model.rs-1221-            ResumeRow { session_id: "old".into(), ts: "t1".into(), trigger: "boot".into(), project: String::new(), cwd: None },
--
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1231-        // Default resume ATTACHES (W4 UX); `h` resumes headless (detached).
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1232-        assert_eq!(
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1233-            m.resume_outcome(false),
crates\spt\src\picker\model.rs:1234:            Some(Outcome::Run {
crates\spt\src\picker\model.rs-1235-                adapter: "claude-spt".into(),
crates\spt\src\picker\model.rs-1236-                id: "cold".into(),
crates\spt\src\picker\model.rs-1237-                resume: Some("new".into()),
--
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1242-        );
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1243-        assert_eq!(
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs-1244-            m.resume_outcome(true),
crates\spt\src\picker\model.rs:1245:            Some(Outcome::Run {
crates\spt\src\picker\model.rs-1246-                adapter: "claude-spt".into(),
crates\spt\src\picker\model.rs-1247-                id: "cold".into(),
crates\spt\src\picker\model.rs-1248-                resume: Some("new".into()),


The picker units are already updated by the implementer (they reference the new `cwd` field and the new title shape). Per the prompt, I should not duplicate these. Let me confirm the baseline compiles and the existing tests pass before I add anything, so I have a clean starting point.


Bash

cargo build -p mock-adapter --bin mock-session 2>&1 | tail -5

Build mock-session sibling bin (int test precondition)


Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.14s


cargo test -p spt-runtime -p spt-store -p spt-daemon --no-run 2>&1 | tail -8

Compile lib-crate tests (baseline check)


  Executable tests\shellchan.rs (target\debug\deps\shellchan-580ab133746cb0d9.exe)
  Executable tests\sync.rs (target\debug\deps\sync-d4f49cdbeaeeb78b.exe)
  Executable tests\two_origin_spanning.rs (target\debug\deps\two_origin_spanning-93f2c992c17e0c7d.exe)
  Executable tests\twohost.rs (target\debug\deps\twohost-2a4052bfcabcfd9d.exe)
  Executable tests\wanmsg.rs (target\debug\deps\wanmsg-a0ba3dbe6f9175b3.exe)
  Executable tests\xfer.rs (target\debug\deps\xfer-11553089b71122c0.exe)
  Executable unittests src\lib.rs (target\debug\deps\spt_runtime-bcd4be4147067449.exe)
  Executable unittests src\lib.rs (target\debug\deps\spt_store-b80e4b63c89e8050.exe)


Baseline compiles. Now I'll write the tests. Starting with the manifest unit tests.


Edit

    #[test] // [unit->REQ-MANIFEST-1]
    fn shell_fixture_parses() {

    // [unit->REQ-SESSION-RESUME-TEMPLATE] the `[session.resume]` native-resume
    // sibling of `[session.self]` (mirroring `psyche_init`→`psyche_resume`): a
    // manifest declaring it parses with `resume.is_some()` and the right command;
    // `roles()` enumerates `("resume", _)` directly after `"self"`; `is_empty()` is
    // false when only `resume` is set; a manifest WITHOUT `[session.resume]` parses
    // with `resume: None` (full back-compat); and a resume role round-trips through
    // TOML (parse→emit→reparse stable).
    #[test]
    fn session_resume_role_parses_roundtrips_and_is_backcompat() {
        let base =
            "[adapter]\nname=\"cc\"\nkind=\"harness\"\nversion=\"1\"\nmin_spt_core_version=\"0\"\n";

        // Declared: resume is parsed with its own command, distinct from self.
        let with = format!(
            "{base}\n[session.self]\ncommand = 'claude --session-id {{session_id}}'\n\n\
             [session.resume]\ncommand = 'claude -r {{session_id}}'\nkeys = [\"id\", \"session_id\"]\n"
        );
        let m = Manifest::from_toml_str(&with).expect("resume role parses");
        let resume = m
            .session
            .resume
            .as_ref()
            .expect("[session.resume] is Some when declared");
        assert_eq!(resume.command, "claude -r {session_id}");
        assert_ne!(
            m.session.self_.as_ref().unwrap().command,
            resume.command,
            "resume command is distinct from self"
        );

        // roles() enumerates ("resume", _) immediately after ("self", _).
        let role_names: Vec<&str> = m.session.roles().iter().map(|(n, _)| *n).collect();
        let self_idx = role_names
            .iter()
            .position(|n| *n == "self")
            .expect("self is enumerated");
        assert_eq!(
            role_names.get(self_idx + 1).copied(),
            Some("resume"),
            "resume is ordered right after self in roles(): {role_names:?}"
        );

        // is_empty() is false when ONLY resume is declared (no self, no dirs).
        let resume_only = format!(
            "{base}\n[session.resume]\ncommand = 'claude -r {{session_id}}'\n"
        );
        let mr = Manifest::from_toml_str(&resume_only).expect("resume-only parses");
        assert!(
            !mr.session.is_empty(),
            "a session with only [session.resume] is not empty"
        );
        assert!(mr.session.self_.is_none());
        assert!(mr.session.resume.is_some());

        // Round-trip: parse → emit → reparse is stable for the resume role.
        let reparsed = Manifest::from_toml_str(&m.to_toml_string().unwrap()).unwrap();
        assert_eq!(m.session.resume, reparsed.session.resume, "resume round-trips");
        let emitted = m.to_toml_string().unwrap();
        assert!(
            emitted.contains("[session.resume]"),
            "the resume table is emitted: {emitted}"
        );

        // BACK-COMPAT: a manifest with [session.self] but NO [session.resume]
        // parses with resume == None (an old adapter is unaffected).
        let no_resume = format!(
            "{base}\n[session.self]\ncommand = 'claude --session-id {{session_id}}'\n"
        );
        let m0 = Manifest::from_toml_str(&no_resume).expect("no-resume parses");
        assert!(m0.session.self_.is_some());
        assert!(
            m0.session.resume.is_none(),
            "absent [session.resume] ⇒ None (back-compat)"
        );
        // And it must emit no resume table.
        assert!(
            !m0.to_toml_string().unwrap().contains("[session.resume]"),
            "no resume table emitted when absent"
        );
    }

    #[test] // [unit->REQ-MANIFEST-1]
    fn shell_fixture_parses() {


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\manifest.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    // [unit->REQ-SESSION-RESUME-TEMPLATE] the `[session.resume]` native-resume
    // sibling of `[session.self]` (mirroring `psyche_init`→`psyche_resume`): a
    // manifest declaring it parses with `resume.is_some()` and the right command;
    // `roles()` enumerates `("resume", _)` directly after `"self"`; `is_empty()` is
    // false when only `resume` is set; a manifest WITHOUT `[session.resume]` parses
    // with `resume: None` (full back-compat); and a resume role round-trips through
    // TOML (parse→emit→reparse stable).
    #[test]
    fn session_resume_role_parses_roundtrips_and_is_backcompat() {
        let base =
            "[adapter]\nname=\"cc\"\nkind=\"harness\"\nversion=\"1\"\nmin_spt_core_version=\"0\"\n";

        // Declared: resume is parsed with its own command, distinct from self.
        let with = format!(
            "{base}\n[session.self]\ncommand = 'claude --session-id {{session_id}}'\n\n\
             [session.resume]\ncommand = 'claude -r {{session_id}}'\nkeys = [\"id\", \"session_id\"]\n"
        );
        let m = Manifest::from_toml_str(&with).expect("resume role parses");
        let resume = m
            .session
            .resume
            .as_ref()
            .expect("[session.resume] is Some when declared");
        assert_eq!(resume.command, "claude -r {session_id}");
        assert_ne!(
            m.session.self_.as_ref().unwrap().command,
            resume.command,
            "resume command is distinct from self"
        );

        // roles() enumerates ("resume", _) immediately after ("self", _).
        let role_names: Vec<&str> = m.session.roles().iter().map(|(n, _)| *n).collect();
        let self_idx = role_names
            .iter()
            .position(|n| *n == "self")
            .expect("self is enumerated");
        assert_eq!(
            role_names.get(self_idx + 1).copied(),
            Some("resume"),
            "resume is ordered right after self in roles(): {role_names:?}"
        );

        // is_empty() is false when ONLY resume is declared (no self, no dirs).
        let resume_only = format!(
            "{base}\n[session.resume]\ncommand = 'claude -r {{session_id}}'\n"
        );
        let mr = Manifest::from_toml_str(&resume_only).expect("resume-only parses");
        assert!(
            !mr.session.is_empty(),
            "a session with only [session.resume] is not empty"
        );
        assert!(mr.session.self_.is_none());
        assert!(mr.session.resume.is_some());

        // Round-trip: parse → emit → reparse is stable for the resume role.
        let reparsed = Manifest::from_toml_str(&m.to_toml_string().unwrap()).unwrap();
        assert_eq!(m.session.resume, reparsed.session.resume, "resume round-trips");
        let emitted = m.to_toml_string().unwrap();
        assert!(
            emitted.contains("[session.resume]"),
            "the resume table is emitted: {emitted}"
        );

        // BACK-COMPAT: a manifest with [session.self] but NO [session.resume]
        // parses with resume == None (an old adapter is unaffected).
        let no_resume = format!(
            "{base}\n[session.self]\ncommand = 'claude --session-id {{session_id}}'\n"
        );
        let m0 = Manifest::from_toml_str(&no_resume).expect("no-resume parses");
        assert!(m0.session.self_.is_some());
        assert!(
            m0.session.resume.is_none(),
            "absent [session.resume] ⇒ None (back-compat)"
        );
        // And it must emit no resume table.
        assert!(
            !m0.to_toml_string().unwrap().contains("[session.resume]"),
            "no resume table emitted when absent"
        );
    }

    #[test] // [unit->REQ-MANIFEST-1]
    fn shell_fixture_parses() {

+    // [unit->REQ-SESSION-RESUME-TEMPLATE] the `[session.resume]` native-resume

+    // sibling of `[session.self]` (mirroring `psyche_init`→`psyche_resume`): a

+    // manifest declaring it parses with `resume.is_some()` and the right command;

+    // `roles()` enumerates `("resume", _)` directly after `"self"`; `is_empty()` is

+    // false when only `resume` is set; a manifest WITHOUT `[session.resume]` parses

+    // with `resume: None` (full back-compat); and a resume role round-trips through

+    // TOML (parse→emit→reparse stable).

+    fn session_resume_role_parses_roundtrips_and_is_backcompat() {

+            "[adapter]\nname=\"cc\"\nkind=\"harness\"\nversion=\"1\"\nmin_spt_core_version=\"0\"\n";

+        // Declared: resume is parsed with its own command, distinct from self.

+            "{base}\n[session.self]\ncommand = 'claude --session-id {{session_id}}'\n\n\

+             [session.resume]\ncommand = 'claude -r {{session_id}}'\nkeys = [\"id\", \"session_id\"]\n"

+        let m = Manifest::from_toml_str(&with).expect("resume role parses");

+            .expect("[session.resume] is Some when declared");

+        assert_eq!(resume.command, "claude -r {session_id}");

+            m.session.self_.as_ref().unwrap().command,

+            "resume command is distinct from self"

+        // roles() enumerates ("resume", _) immediately after ("self", _).

+        let role_names: Vec<&str> = m.session.roles().iter().map(|(n, _)| *n).collect();

+            .position(|n| *n == "self")

+            .expect("self is enumerated");

+            role_names.get(self_idx + 1).copied(),

+            "resume is ordered right after self in roles(): {role_names:?}"

+        // is_empty() is false when ONLY resume is declared (no self, no dirs).

+            "{base}\n[session.resume]\ncommand = 'claude -r {{session_id}}'\n"

+        let mr = Manifest::from_toml_str(&resume_only).expect("resume-only parses");

+            "a session with only [session.resume] is not empty"

+        assert!(mr.session.self_.is_none());

+        assert!(mr.session.resume.is_some());

+        // Round-trip: parse → emit → reparse is stable for the resume role.

+        let reparsed = Manifest::from_toml_str(&m.to_toml_string().unwrap()).unwrap();

+        assert_eq!(m.session.resume, reparsed.session.resume, "resume round-trips");

+        let emitted = m.to_toml_string().unwrap();

+            emitted.contains("[session.resume]"),

+            "the resume table is emitted: {emitted}"

+        // BACK-COMPAT: a manifest with [session.self] but NO [session.resume]

+        // parses with resume == None (an old adapter is unaffected).

+            "{base}\n[session.self]\ncommand = 'claude --session-id {{session_id}}'\n"

+        let m0 = Manifest::from_toml_str(&no_resume).expect("no-resume parses");

+        assert!(m0.session.self_.is_some());

+            m0.session.resume.is_none(),

+            "absent [session.resume] ⇒ None (back-compat)"

+        // And it must emit no resume table.

+            !m0.to_toml_string().unwrap().contains("[session.resume]"),

+            "no resume table emitted when absent"

let m = Manifest::from_toml_str(SHELL_FIXTURE).expect("parse");


Now the harnesshost.rs unit tests for the resume-vs-self selection.


// [unit->REQ-HOST-RUN-1] a provisional session id is fresh, hex, and unique

    /// A harness manifest declaring BOTH `[session.self]` and a DISTINCT
    /// `[session.resume]` — the resume-vs-self selection fixture.
    fn harness_manifest_with_resume(self_command: &str, resume_command: &str) -> Manifest {
        let toml_src = format!(
            "[adapter]\nname = \"mock\"\nkind = \"harness\"\nversion = \"1\"\n\
             min_spt_core_version = \"0\"\n\n[session.self]\ncommand = '{self_command}'\n\
             keys = [\"id\", \"session_id\"]\n\n[session.resume]\ncommand = '{resume_command}'\n\
             keys = [\"id\", \"session_id\"]\n"
        );
        spt_runtime::Manifest::from_toml_str(&toml_src).unwrap()
    }

    // [unit->REQ-SESSION-RESUME-TEMPLATE] the native-resume template selection:
    // when the adapter declares BOTH [session.self] and a DIFFERENT
    // [session.resume], `is_resume=true` fills the RESUME command (so the harness
    // runs `claude -r {session_id}`, not the fresh `claude --session-id …` that
    // would start a blank transcript), and `is_resume=false` fills SELF. Both fill
    // the SAME {id}/{session_id} catalog.
    #[test]
    fn prepare_selects_resume_template_on_resume_else_self() {
        let m = harness_manifest_with_resume(
            "mock-session --id {id} --session-id {session_id}",
            "mock-session --id {id} --resume {session_id}",
        );

        // is_resume=true → the RESUME template (`--resume`), filled with the id.
        let resumed =
            prepare_harness_spawn("doyle", "mock", "sess-abc", &m, true).expect("resume prepares");
        assert_eq!(
            resumed.tokens,
            vec![
                "mock-session".to_string(),
                "--id".to_string(),
                "doyle".to_string(),
                "--resume".to_string(),
                "sess-abc".to_string(),
            ],
            "is_resume=true must fill the [session.resume] command"
        );

        // is_resume=false → the SELF template (`--session-id`), same catalog.
        let fresh =
            prepare_harness_spawn("doyle", "mock", "sess-abc", &m, false).expect("self prepares");
        assert_eq!(
            fresh.tokens,
            vec![
                "mock-session".to_string(),
                "--id".to_string(),
                "doyle".to_string(),
                "--session-id".to_string(),
                "sess-abc".to_string(),
            ],
            "is_resume=false must fill the [session.self] command"
        );
        // The two templates produce genuinely different argv (the bug class: a
        // resume that re-ran the fresh command → blank transcript).
        assert_ne!(
            resumed.tokens, fresh.tokens,
            "resume and self must tokenize to different commands"
        );
    }

    // [unit->REQ-SESSION-RESUME-TEMPLATE] back-compat fallback: an adapter with
    // [session.self] but NO [session.resume], asked to resume (`is_resume=true`),
    // FALLS BACK to [session.self] — an adapter that never declared the resume
    // sibling behaves exactly as it did before this feature (no error, the self
    // command runs).
    #[test]
    fn prepare_resume_falls_back_to_self_when_no_resume_role() {
        let no_resume = harness_manifest("mock-session --id {id} --session-id {session_id}");
        assert!(
            no_resume.session.resume.is_none(),
            "fixture declares no [session.resume]"
        );

        let prepared = prepare_harness_spawn("e", "mock", "sess-z", &no_resume, true)
            .expect("resume with no resume-role falls back to self, not an error");
        assert_eq!(
            prepared.tokens,
            vec![
                "mock-session".to_string(),
                "--id".to_string(),
                "e".to_string(),
                "--session-id".to_string(),
                "sess-z".to_string(),
            ],
            "is_resume=true with no [session.resume] must fall back to [session.self]"
        );
    }

    // [unit->REQ-HOST-RUN-1] a provisional session id is fresh, hex, and unique


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\harnesshost.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    /// A harness manifest declaring BOTH `[session.self]` and a DISTINCT
    /// `[session.resume]` — the resume-vs-self selection fixture.
    fn harness_manifest_with_resume(self_command: &str, resume_command: &str) -> Manifest {
        let toml_src = format!(
            "[adapter]\nname = \"mock\"\nkind = \"harness\"\nversion = \"1\"\n\
             min_spt_core_version = \"0\"\n\n[session.self]\ncommand = '{self_command}'\n\
             keys = [\"id\", \"session_id\"]\n\n[session.resume]\ncommand = '{resume_command}'\n\
             keys = [\"id\", \"session_id\"]\n"
        );
        spt_runtime::Manifest::from_toml_str(&toml_src).unwrap()
    }

    // [unit->REQ-SESSION-RESUME-TEMPLATE] the native-resume template selection:
    // when the adapter declares BOTH [session.self] and a DIFFERENT
    // [session.resume], `is_resume=true` fills the RESUME command (so the harness
    // runs `claude -r {session_id}`, not the fresh `claude --session-id …` that
    // would start a blank transcript), and `is_resume=false` fills SELF. Both fill
    // the SAME {id}/{session_id} catalog.
    #[test]
    fn prepare_selects_resume_template_on_resume_else_self() {
        let m = harness_manifest_with_resume(
            "mock-session --id {id} --session-id {session_id}",
            "mock-session --id {id} --resume {session_id}",
        );

        // is_resume=true → the RESUME template (`--resume`), filled with the id.
        let resumed =
            prepare_harness_spawn("doyle", "mock", "sess-abc", &m, true).expect("resume prepares");
        assert_eq!(
            resumed.tokens,
            vec![
                "mock-session".to_string(),
                "--id".to_string(),
                "doyle".to_string(),
                "--resume".to_string(),
                "sess-abc".to_string(),
            ],
            "is_resume=true must fill the [session.resume] command"
        );

        // is_resume=false → the SELF template (`--session-id`), same catalog.
        let fresh =
            prepare_harness_spawn("doyle", "mock", "sess-abc", &m, false).expect("self prepares");
        assert_eq!(
            fresh.tokens,
            vec![
                "mock-session".to_string(),
                "--id".to_string(),
                "doyle".to_string(),
                "--session-id".to_string(),
                "sess-abc".to_string(),
            ],
            "is_resume=false must fill the [session.self] command"
        );
        // The two templates produce genuinely different argv (the bug class: a
        // resume that re-ran the fresh command → blank transcript).
        assert_ne!(
            resumed.tokens, fresh.tokens,
            "resume and self must tokenize to different commands"
        );
    }

    // [unit->REQ-SESSION-RESUME-TEMPLATE] back-compat fallback: an adapter with
    // [session.self] but NO [session.resume], asked to resume (`is_resume=true`),
    // FALLS BACK to [session.self] — an adapter that never declared the resume
    // sibling behaves exactly as it did before this feature (no error, the self
    // command runs).
    #[test]
    fn prepare_resume_falls_back_to_self_when_no_resume_role() {
        let no_resume = harness_manifest("mock-session --id {id} --session-id {session_id}");
        assert!(
            no_resume.session.resume.is_none(),
            "fixture declares no [session.resume]"
        );

        let prepared = prepare_harness_spawn("e", "mock", "sess-z", &no_resume, true)
            .expect("resume with no resume-role falls back to self, not an error");
        assert_eq!(
            prepared.tokens,
            vec![
                "mock-session".to_string(),
                "--id".to_string(),
                "e".to_string(),
                "--session-id".to_string(),
                "sess-z".to_string(),
            ],
            "is_resume=true with no [session.resume] must fall back to [session.self]"
        );
    }

    // [unit->REQ-HOST-RUN-1] a provisional session id is fresh, hex, and unique

+    /// A harness manifest declaring BOTH `[session.self]` and a DISTINCT

+    /// `[session.resume]` — the resume-vs-self selection fixture.

+    fn harness_manifest_with_resume(self_command: &str, resume_command: &str) -> Manifest {

+            "[adapter]\nname = \"mock\"\nkind = \"harness\"\nversion = \"1\"\n\

+             min_spt_core_version = \"0\"\n\n[session.self]\ncommand = '{self_command}'\n\

+             keys = [\"id\", \"session_id\"]\n\n[session.resume]\ncommand = '{resume_command}'\n\

+             keys = [\"id\", \"session_id\"]\n"

+        spt_runtime::Manifest::from_toml_str(&toml_src).unwrap()

+    // [unit->REQ-SESSION-RESUME-TEMPLATE] the native-resume template selection:

+    // when the adapter declares BOTH [session.self] and a DIFFERENT

+    // [session.resume], `is_resume=true` fills the RESUME command (so the harness

+    // runs `claude -r {session_id}`, not the fresh `claude --session-id …` that

+    // would start a blank transcript), and `is_resume=false` fills SELF. Both fill

+    // the SAME {id}/{session_id} catalog.

+    fn prepare_selects_resume_template_on_resume_else_self() {

+        let m = harness_manifest_with_resume(

+            "mock-session --id {id} --session-id {session_id}",

+            "mock-session --id {id} --resume {session_id}",

+        // is_resume=true → the RESUME template (`--resume`), filled with the id.

+            prepare_harness_spawn("doyle", "mock", "sess-abc", &m, true).expect("resume prepares");

+                "mock-session".to_string(),

+                "--resume".to_string(),

+                "sess-abc".to_string(),

+            "is_resume=true must fill the [session.resume] command"

+        // is_resume=false → the SELF template (`--session-id`), same catalog.

+            prepare_harness_spawn("doyle", "mock", "sess-abc", &m, false).expect("self prepares");

+                "--session-id".to_string(),

+            "is_resume=false must fill the [session.self] command"

+        // The two templates produce genuinely different argv (the bug class: a

+        // resume that re-ran the fresh command → blank transcript).

+            resumed.tokens, fresh.tokens,

+            "resume and self must tokenize to different commands"

+    // [unit->REQ-SESSION-RESUME-TEMPLATE] back-compat fallback: an adapter with

+    // [session.self] but NO [session.resume], asked to resume (`is_resume=true`),

+    // FALLS BACK to [session.self] — an adapter that never declared the resume

+    // sibling behaves exactly as it did before this feature (no error, the self

+    fn prepare_resume_falls_back_to_self_when_no_resume_role() {

+        let no_resume = harness_manifest("mock-session --id {id} --session-id {session_id}");

+            no_resume.session.resume.is_none(),

+            "fixture declares no [session.resume]"

+        let prepared = prepare_harness_spawn("e", "mock", "sess-z", &no_resume, true)

+            .expect("resume with no resume-role falls back to self, not an error");

+                "mock-session".to_string(),

+                "--session-id".to_string(),

+            "is_resume=true with no [session.resume] must fall back to [session.self]"

// per mint (it only fills the spawn-time {session_id}; the harness reports the


Now the sessions.rs ledger cwd tests.


    // [unit->REQ-TERM-6] a blank/garbage line is skipped, the valid rows still load.
    #[test]
    fn tolerant_of_garbage_lines() {

    // [unit->REQ-SESSION-RESUME-TEMPLATE] a row carrying a project `cwd` appends and
    // reads back with the cwd intact (the per-row cwd a harness needs to resolve a
    // transcript by session_id + cwd on cross-project resume).
    #[test]
    fn row_with_cwd_appends_and_reads_back() {
        let dir = tempfile::tempdir().unwrap();
        let perch = dir.path().join("perch");
        let with_cwd = SessionEntry {
            ts: crate::timefmt::rfc3339_utc(1_700_000_000),
            session_id: "s-cwd".to_string(),
            trigger: SessionTrigger::Boot,
            cwd: Some("/projects/alpha".to_string()),
        };
        assert!(append(&perch, &with_cwd).unwrap());
        let all = read_all(&perch);
        assert_eq!(all.len(), 1);
        assert_eq!(all[0].session_id, "s-cwd");
        assert_eq!(
            all[0].cwd.as_deref(),
            Some("/projects/alpha"),
            "the recorded cwd must survive append→read"
        );
    }

    // [unit->REQ-SESSION-RESUME-TEMPLATE] BACK-COMPAT: a pre-migration JSONL row
    // written BEFORE the additive `cwd` field — `{ts, session_id, trigger}` with no
    // `cwd` key — must still deserialize, with `cwd == None` (the additive
    // serde-default contract; an old ledger never breaks loading).
    #[test]
    fn pre_migration_row_without_cwd_deserializes_as_none() {
        let dir = tempfile::tempdir().unwrap();
        let perch = dir.path().join("perch");
        std::fs::create_dir_all(&perch).unwrap();
        // The exact pre-migration on-disk shape (no `cwd` field), plus a NEW-shape
        // row carrying a cwd — both must load, old as None and new as Some.
        std::fs::write(
            perch.join(LEDGER_FILE),
            "{\"ts\":\"t-old\",\"session_id\":\"old\",\"trigger\":\"boot\"}\n\
             {\"ts\":\"t-new\",\"session_id\":\"new\",\"trigger\":\"clear\",\"cwd\":\"/p/beta\"}\n",
        )
        .unwrap();
        let all = read_all(&perch);
        assert_eq!(all.len(), 2, "both an old (no-cwd) and new row load");
        assert_eq!(all[0].session_id, "old");
        assert!(
            all[0].cwd.is_none(),
            "a pre-migration row missing `cwd` deserializes with cwd == None"
        );
        assert_eq!(all[1].session_id, "new");
        assert_eq!(all[1].cwd.as_deref(), Some("/p/beta"));
    }

    // [unit->REQ-SESSION-RESUME-TEMPLATE] a None cwd serializes to NO `cwd` key
    // (`skip_serializing_if`), so a fresh row written by a node that never set a
    // cwd stays byte-identical to the pre-migration shape (round-trip + no field
    // churn for old rows).
    #[test]
    fn none_cwd_emits_no_field_and_roundtrips() {
        let row = SessionEntry {
            ts: "t".to_string(),
            session_id: "s".to_string(),
            trigger: SessionTrigger::Boot,
            cwd: None,
        };
        let json = serde_json::to_string(&row).unwrap();
        assert!(
            !json.contains("cwd"),
            "a None cwd must emit no `cwd` key (skip_serializing_if): {json}"
        );
        let back: SessionEntry = serde_json::from_str(&json).unwrap();
        assert_eq!(row, back, "a no-cwd row round-trips identically");
    }

    // [unit->REQ-TERM-6] a blank/garbage line is skipped, the valid rows still load.
    #[test]
    fn tolerant_of_garbage_lines() {


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\sessions.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    // [unit->REQ-SESSION-RESUME-TEMPLATE] a row carrying a project `cwd` appends and
    // reads back with the cwd intact (the per-row cwd a harness needs to resolve a
    // transcript by session_id + cwd on cross-project resume).
    #[test]
    fn row_with_cwd_appends_and_reads_back() {
        let dir = tempfile::tempdir().unwrap();
        let perch = dir.path().join("perch");
        let with_cwd = SessionEntry {
            ts: crate::timefmt::rfc3339_utc(1_700_000_000),
            session_id: "s-cwd".to_string(),
            trigger: SessionTrigger::Boot,
            cwd: Some("/projects/alpha".to_string()),
        };
        assert!(append(&perch, &with_cwd).unwrap());
        let all = read_all(&perch);
        assert_eq!(all.len(), 1);
        assert_eq!(all[0].session_id, "s-cwd");
        assert_eq!(
            all[0].cwd.as_deref(),
            Some("/projects/alpha"),
            "the recorded cwd must survive append→read"
        );
    }

    // [unit->REQ-SESSION-RESUME-TEMPLATE] BACK-COMPAT: a pre-migration JSONL row
    // written BEFORE the additive `cwd` field — `{ts, session_id, trigger}` with no
    // `cwd` key — must still deserialize, with `cwd == None` (the additive
    // serde-default contract; an old ledger never breaks loading).
    #[test]
    fn pre_migration_row_without_cwd_deserializes_as_none() {
        let dir = tempfile::tempdir().unwrap();
        let perch = dir.path().join("perch");
        std::fs::create_dir_all(&perch).unwrap();
        // The exact pre-migration on-disk shape (no `cwd` field), plus a NEW-shape
        // row carrying a cwd — both must load, old as None and new as Some.
        std::fs::write(
            perch.join(LEDGER_FILE),
            "{\"ts\":\"t-old\",\"session_id\":\"old\",\"trigger\":\"boot\"}\n\
             {\"ts\":\"t-new\",\"session_id\":\"new\",\"trigger\":\"clear\",\"cwd\":\"/p/beta\"}\n",
        )
        .unwrap();
        let all = read_all(&perch);
        assert_eq!(all.len(), 2, "both an old (no-cwd) and new row load");
        assert_eq!(all[0].session_id, "old");
        assert!(
            all[0].cwd.is_none(),
            "a pre-migration row missing `cwd` deserializes with cwd == None"
        );
        assert_eq!(all[1].session_id, "new");
        assert_eq!(all[1].cwd.as_deref(), Some("/p/beta"));
    }

    // [unit->REQ-SESSION-RESUME-TEMPLATE] a None cwd serializes to NO `cwd` key
    // (`skip_serializing_if`), so a fresh row written by a node that never set a
    // cwd stays byte-identical to the pre-migration shape (round-trip + no field
    // churn for old rows).
    #[test]
    fn none_cwd_emits_no_field_and_roundtrips() {
        let row = SessionEntry {
            ts: "t".to_string(),
            session_id: "s".to_string(),
            trigger: SessionTrigger::Boot,
            cwd: None,
        };
        let json = serde_json::to_string(&row).unwrap();
        assert!(
            !json.contains("cwd"),
            "a None cwd must emit no `cwd` key (skip_serializing_if): {json}"
        );
        let back: SessionEntry = serde_json::from_str(&json).unwrap();
        assert_eq!(row, back, "a no-cwd row round-trips identically");
    }

    // [unit->REQ-TERM-6] a blank/garbage line is skipped, the valid rows still load.
    #[test]
    fn tolerant_of_garbage_lines() {

//! Per-endpoint **session ledger** — `<perch>/sessions.log` (ADR-0019 /
//! REQ-TERM-6), heir to the sister project's `psyches/tracked/agents/<id>/
//! sessions.log`.
//!
//! A live agent that `/clear`s or `/compact`s keeps its perch identity but
//! rotates its harness `session_id` (a new transcript file). The single
//! `{session_id}` a `[history]`/`[digest]` source resolves therefore *follows*
//! the thread but cannot *span* it. This ledger records each rotation so the
//! digest can enumerate the **last K sessions** and bridge a boundary
//! (ADR-0019 §thread-spanning).
//!
//! One JSONL row per rotation: `{ts, session_id, trigger}`. The ledger is
//! **bounded** ([`MAX_LEDGER`]) — the sister project's seal-on-boot failure grew
//! its equivalent unboundedly (DIGEST-MILESTONE-PLAN risk #2); appending rotates
//! the oldest rows out. A re-bind of the *same* session is not a new boundary and
//! is deduped.
// [impl->REQ-TERM-6]

use std::io;
use std::path::Path;

use serde::{Deserialize, Serialize};

use crate::atomic::atomic_write_string;

/// The ledger filename under a perch.
pub const LEDGER_FILE: &str = "sessions.log";

/// On-disk row cap. The ledger is a rolling tail — older rows rotate out on
/// append so the file can never grow without bound (risk #2).
pub const MAX_LEDGER: usize = 64;

/// Why a session became active.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SessionTrigger {
    /// First bind of the perch (`api listen` / revive).
    Boot,
    /// A `/clear` boundary (`api boundary clear`).
    Clear,
    /// A `/compact` boundary (`api boundary compact`).
    Compact,
}

impl SessionTrigger {
    /// The lowercase token (matches the serde wire form) — used as the boundary
    /// marker `kind` in the spanned digest.
    pub fn token(self) -> &'static str {
        match self {
            SessionTrigger::Boot => "boot",
            SessionTrigger::Clear => "clear",
            SessionTrigger::Compact => "compact",
        }
    }
}

/// One ledger row: a `session_id` became active at `ts` via `trigger`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionEntry {
    /// RFC3339-UTC instant (see [`crate::timefmt`]).
    pub ts: String,
    /// The harness session id that became active.
    pub session_id: String,
    /// What caused the rotation.
    pub trigger: SessionTrigger,
    /// The project working directory the session was active in (the W3
    /// `info.cwd` set at bind, or the spawn cwd). **Additive serde-default**: a
    /// pre-migration row missing `cwd` deserializes as `None`. A harness resolves
    /// a transcript by `session_id` + `cwd`, so picker Resume-from-history
    /// (cross-project rows) restores the right dir from here.
    // [impl->REQ-SESSION-RESUME-TEMPLATE]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
}

/// Every ledger row in order (oldest→newest). Tolerant: a blank or unparseable
/// line is skipped, the rest still load (the degrade-don't-crash store stance).
pub fn read_all(perch: &Path) -> Vec<SessionEntry> {
    let Ok(s) = std::fs::read_to_string(perch.join(LEDGER_FILE)) else {
        return Vec::new();
    };
    s.lines()
        .filter(|l| !l.trim().is_empty())
        .filter_map(|l| serde_json::from_str(l).ok())
        .collect()
}

/// The last `k` rows (oldest→newest within the tail). `k == 0` ⇒ empty.
pub fn last_k(perch: &Path, k: usize) -> Vec<SessionEntry> {
    if k == 0 {
        return Vec::new();
    }
    let all = read_all(perch);
    let start = all.len().saturating_sub(k);
    all[start..].to_vec()
}

/// Append `entry`, deduping an immediate repeat of the same `session_id` (a
/// re-bind of the live session is not a new boundary) and bounding the ledger to
/// [`MAX_LEDGER`] (oldest rows rotate out). The whole bounded ledger is rewritten
/// atomically (it is tiny — ≤ `MAX_LEDGER` short rows — so a full rewrite avoids
/// partial-line corruption). Returns `true` if a row was written, `false` on a
/// same-session dedup.
// [impl->REQ-TERM-6]
pub fn append(perch: &Path, entry: &SessionEntry) -> io::Result<bool> {
    let mut entries = read_all(perch);
    if entries.last().map(|e| e.session_id.as_str()) == Some(entry.session_id.as_str()) {
        return Ok(false); // same session re-bound — not a new boundary
    }
    entries.push(entry.clone());
    let start = entries.len().saturating_sub(MAX_LEDGER);
    let body: String = entries[start..]
        .iter()
        .map(|e| serde_json::to_string(e).unwrap_or_default())
        .map(|l| l + "\n")
        .collect();
    std::fs::create_dir_all(perch)?;
    atomic_write_string(&perch.join(LEDGER_FILE), &body)?;
    Ok(true)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn entry(sid: &str, trigger: SessionTrigger) -> SessionEntry {
        SessionEntry {
            ts: crate::timefmt::rfc3339_utc(1_700_000_000),
            session_id: sid.to_string(),
            trigger,
            cwd: None,
        }
    }

    // [unit->REQ-TERM-6] rows append in order and read back oldest→newest.
    #[test]
    fn appends_and_reads_in_order() {
        let dir = tempfile::tempdir().unwrap();
        let perch = dir.path().join("perch");
        assert!(append(&perch, &entry("s1", SessionTrigger::Boot)).unwrap());
        assert!(append(&perch, &entry("s2", SessionTrigger::Clear)).unwrap());
        let all = read_all(&perch);
        assert_eq!(all.len(), 2);
        assert_eq!(all[0].session_id, "s1");
        assert_eq!(all[0].trigger, SessionTrigger::Boot);
        assert_eq!(all[1].session_id, "s2");
        assert_eq!(all[1].trigger, SessionTrigger::Clear);
    }

    // [unit->REQ-TERM-6] a re-bind of the same session is deduped (not a new
    // boundary); a different session appends.
    #[test]
    fn same_session_rebind_is_deduped() {
        let dir = tempfile::tempdir().unwrap();
        let perch = dir.path().join("perch");
        assert!(append(&perch, &entry("s1", SessionTrigger::Boot)).unwrap());
        assert!(
            !append(&perch, &entry("s1", SessionTrigger::Boot)).unwrap(),
            "same session re-bind → no new row"
        );
        assert_eq!(read_all(&perch).len(), 1);
        assert!(append(&perch, &entry("s2", SessionTrigger::Clear)).unwrap());
        assert_eq!(read_all(&perch).len(), 2);
    }

    // [unit->REQ-TERM-6] last_k returns the newest K rows; the ledger is bounded so
    // it never grows past MAX_LEDGER.
    #[test]
    fn last_k_and_bounding() {
        let dir = tempfile::tempdir().unwrap();
        let perch = dir.path().join("perch");
        for i in 0..(MAX_LEDGER + 10) {
            append(&perch, &entry(&format!("s{i}"), SessionTrigger::Clear)).unwrap();
        }
        assert_eq!(read_all(&perch).len(), MAX_LEDGER, "ledger is bounded");
        let last3 = last_k(&perch, 3);
        assert_eq!(last3.len(), 3);
        let n = MAX_LEDGER + 10;
        assert_eq!(last3[2].session_id, format!("s{}", n - 1), "newest last");
        assert_eq!(last3[0].session_id, format!("s{}", n - 3));
        assert!(last_k(&perch, 0).is_empty());
    }

    // [unit->REQ-TERM-6] a blank/garbage line is skipped, the valid rows still load.
    #[test]
    fn tolerant_of_garbage_lines() {
        let dir = tempfile::tempdir().unwrap();
        let perch = dir.path().join("perch");
        std::fs::create_dir_all(&perch).unwrap();
        std::fs::write(
            perch.join(LEDGER_FILE),
            "not json\n\n{\"ts\":\"t\",\"session_id\":\"good\",\"trigger\":\"boot\"}\n",
        )
        .unwrap();
        let all = read_all(&perch);
        assert_eq!(all.len(), 1);
        assert_eq!(all[0].session_id, "good");
    }
}

+    // [unit->REQ-SESSION-RESUME-TEMPLATE] a row carrying a project `cwd` appends and

+    // reads back with the cwd intact (the per-row cwd a harness needs to resolve a

+    // transcript by session_id + cwd on cross-project resume).

+    fn row_with_cwd_appends_and_reads_back() {

+        let dir = tempfile::tempdir().unwrap();

+        let perch = dir.path().join("perch");

+            session_id: "s-cwd".to_string(),

+            trigger: SessionTrigger::Boot,

+            cwd: Some("/projects/alpha".to_string()),

+        assert!(append(&perch, &with_cwd).unwrap());

+        assert_eq!(all[0].session_id, "s-cwd");

+            "the recorded cwd must survive append→read"

+    // [unit->REQ-SESSION-RESUME-TEMPLATE] BACK-COMPAT: a pre-migration JSONL row

+    // written BEFORE the additive `cwd` field — `{ts, session_id, trigger}` with no

+    // `cwd` key — must still deserialize, with `cwd == None` (the additive

+    // serde-default contract; an old ledger never breaks loading).

+    fn pre_migration_row_without_cwd_deserializes_as_none() {

+        std::fs::create_dir_all(&perch).unwrap();

+        // The exact pre-migration on-disk shape (no `cwd` field), plus a NEW-shape

+        // row carrying a cwd — both must load, old as None and new as Some.

+        assert_eq!(all.len(), 2, "both an old (no-cwd) and new row load");

+        assert_eq!(all[0].session_id, "old");

+            "a pre-migration row missing `cwd` deserializes with cwd == None"

+        assert_eq!(all[1].session_id, "new");

+        assert_eq!(all[1].cwd.as_deref(), Some("/p/beta"));

+    // [unit->REQ-SESSION-RESUME-TEMPLATE] a None cwd serializes to NO `cwd` key

+    // (`skip_serializing_if`), so a fresh row written by a node that never set a

+    // cwd stays byte-identical to the pre-migration shape (round-trip + no field

+    fn none_cwd_emits_no_field_and_roundtrips() {

+            session_id: "s".to_string(),

+            trigger: SessionTrigger::Boot,

+        let json = serde_json::to_string(&row).unwrap();

+            "a None cwd must emit no `cwd` key (skip_serializing_if): {json}"

+        let back: SessionEntry = serde_json::from_str(&json).unwrap();

+        assert_eq!(row, back, "a no-cwd row round-trips identically");


Now run the lib-crate unit tests to verify they pass.


Bash

cargo test -p spt-runtime --lib session_resume_role_parses_roundtrips_and_is_backcompat 2>&1 | tail -15
echo "=== spt-store ==="
cargo test -p spt-store --lib sessions:: 2>&1 | tail -20
echo "=== spt-daemon ==="
cargo test -p spt-daemon --lib harnesshost::tests 2>&1 | tail -20

Run the new lib-crate unit tests


   Compiling spt-store v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-store)
   Compiling spt-msg v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
   Compiling spt-runtime v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 3.09s
     Running unittests src\lib.rs (target\debug\deps\spt_runtime-c2100dedab9c55da.exe)

running 1 test
test manifest::tests::session_resume_role_parses_roundtrips_and_is_backcompat ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 73 filtered out; finished in 0.00s

=== spt-store ===
   Compiling spt-store v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-store)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 3.26s
     Running unittests src\lib.rs (target\debug\deps\spt_store-8224f6144eafe9b1.exe)

running 7 tests
test sessions::tests::none_cwd_emits_no_field_and_roundtrips ... ok
test sessions::tests::tolerant_of_garbage_lines ... ok
test sessions::tests::pre_migration_row_without_cwd_deserializes_as_none ... ok
test sessions::tests::row_with_cwd_appends_and_reads_back ... ok
test sessions::tests::appends_and_reads_in_order ... ok
test sessions::tests::same_session_rebind_is_deduped ... ok
test sessions::tests::last_k_and_bounding ... ok

test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 163 filtered out; finished in 0.36s

=== spt-daemon ===
   Compiling spt-msg v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
   Compiling spt-runtime v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
   Compiling spt-term v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
   Compiling spt-net v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
   Compiling spt-live v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
   Compiling spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 8.27s
     Running unittests src\lib.rs (target\debug\deps\spt_daemon-6c41d016386d7ce3.exe)

running 7 tests
test harnesshost::tests::mint_session_id_is_fresh_hex ... ok
test harnesshost::tests::prepare_fails_closed_on_wrong_kind_missing_role_and_bad_template ... ok
test harnesshost::tests::prepare_fills_env_inject_values_with_substitution ... ok
test harnesshost::tests::prepare_resume_falls_back_to_self_when_no_resume_role ... ok
test harnesshost::tests::prepare_fills_id_and_session_into_self_command ... ok
test harnesshost::tests::prepare_selects_resume_template_on_resume_else_self ... ok
test harnesshost::tests::prepare_threads_translation_binary_path_when_declared ... ok

test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 336 filtered out; finished in 0.00s


All unit tests green. Now I'll do a quick mutation check to confirm each guards a distinct branch (per my memory notes). Let me verify the resume-selection test fails when the impl is broken, by temporarily inverting the selection.


cd /c/Users/decid/Documents/projects/spt-core
# Mutation 1: break the resume selection (always pick self_) and confirm the resume-selection test goes RED
cp crates/spt-daemon/src/harnesshost.rs /tmp/hh.bak
python - <<'EOF'
import re
p="crates/spt-daemon/src/harnesshost.rs"
s=open(p).read()
mut=s.replace(
"""    let role = if is_resume {
        manifest
            .session
            .resume
            .as_ref()
            .or(manifest.session.self_.as_ref())
    } else {
        manifest.session.self_.as_ref()
    }""",
"""    let role = if is_resume {
        manifest.session.self_.as_ref()
    } else {
        manifest.session.self_.as_ref()
    }""")
assert mut!=s, "mutation did not apply"
open(p,"w").write(mut)
EOF
echo "--- mutated; running selection + fallback tests (expect selection RED, fallback still GREEN) ---"
cargo test -p spt-daemon --lib prepare_selects_resume_template prepare_resume_falls_back 2>&1 | tail -12
cp /tmp/hh.bak crates/spt-daemon/src/harnesshost.rs
echo "--- reverted ---"

Mutation-check: break resume selection, confirm test catches it


--- mutated; running selection + fallback tests (expect selection RED, fallback still GREEN) ---
error: unexpected argument 'prepare_resume_falls_back' found

Usage: cargo.exe test [OPTIONS] [TESTNAME] [-- [ARGS]...]

For more information, try '--help'.
--- reverted ---


cargo test takes a single filter. Let me run them separately.


cd /c/Users/decid/Documents/projects/spt-core
cp crates/spt-daemon/src/harnesshost.rs /tmp/hh.bak
python - <<'EOF'
p="crates/spt-daemon/src/harnesshost.rs"
s=open(p).read()
mut=s.replace(
"""    let role = if is_resume {
        manifest
            .session
            .resume
            .as_ref()
            .or(manifest.session.self_.as_ref())
    } else {
        manifest.session.self_.as_ref()
    }""",
"""    let role = if is_resume {
        manifest.session.self_.as_ref()
    } else {
        manifest.session.self_.as_ref()
    }""")
assert mut!=s, "mutation did not apply"
open(p,"w").write(mut)
EOF
echo "=== selection test (expect FAILED) ==="
cargo test -p spt-daemon --lib prepare_selects_resume_template 2>&1 | grep -E "test result|FAILED|assertion|must fill"
cp /tmp/hh.bak crates/spt-daemon/src/harnesshost.rs
echo "=== reverted, reconfirm green ==="
cargo test -p spt-daemon --lib prepare_selects_resume_template 2>&1 | grep -E "test result"

Mutation-check the resume-selection branch


=== selection test (expect FAILED) ===
test harnesshost::tests::prepare_selects_resume_template_on_resume_else_self ... FAILED
assertion `left == right` failed: is_resume=true must fill the [session.resume] command
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 342 filtered out; finished in 0.00s
=== reverted, reconfirm green ===
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 342 filtered out; finished in 0.00s


The selection test catches a broken impl and passes on the real one. Now mutation-check the back-compat ledger test (drop the serde default so an old row would fail to deserialize).


cd /c/Users/decid/Documents/projects/spt-core
cp crates/spt-store/src/sessions.rs /tmp/ss.bak
# Remove `default` from the cwd serde attr → an old row missing cwd fails to deserialize → read_all skips it → len mismatch
python - <<'EOF'
p="crates/spt-store/src/sessions.rs"
s=open(p).read()
mut=s.replace('#[serde(default, skip_serializing_if = "Option::is_none")]\n    pub cwd: Option<String>',
              '#[serde(skip_serializing_if = "Option::is_none")]\n    pub cwd: Option<String>')
assert mut!=s, "mutation did not apply"
open(p,"w").write(mut)
EOF
echo "=== back-compat test (expect FAILED: old row no longer deserializes) ==="
cargo test -p spt-store --lib pre_migration_row_without_cwd_deserializes_as_none 2>&1 | grep -E "test result|FAILED|assertion|both an old"
cp /tmp/ss.bak crates/spt-store/src/sessions.rs
echo "=== reverted, reconfirm green ==="
cargo test -p spt-store --lib pre_migration_row_without_cwd_deserializes_as_none 2>&1 | grep -E "test result"

Mutation-check the ledger back-compat serde-default branch


=== back-compat test (expect FAILED: old row no longer deserializes) ===
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 169 filtered out; finished in 0.00s
=== reverted, reconfirm green ===
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 169 filtered out; finished in 0.00s


Interesting — the mutation didn't make it RED. That means `read_all` swallows the deserialize failure via `.filter_map(... .ok())`, so a non-default `cwd` field would silently drop the old row. But it still passed... let me check whether the mutation actually compiled/applied. `Option<String>` may deserialize as `None` for a missing field even without `default` in some serde versions (Option fields are implicitly defaulting). Let me verify.


cd /c/Users/decid/Documents/projects/spt-core
# Confirm serde treats a missing Option field as None even WITHOUT #[serde(default)].
# This is true: serde derives implicit Option-as-None for missing fields. So `default` is
# load-bearing only for the skip behavior + non-Option types. Let me prove the read_all path
# would catch a HARD deserialize failure with a genuinely required missing field.
cp crates/spt-store/src/sessions.rs /tmp/ss.bak
# Make cwd a REQUIRED String (not Option) → an old row missing cwd MUST fail to parse → row dropped.
python - <<'EOF'
p="crates/spt-store/src/sessions.rs"
s=open(p).read()
mut=s.replace('    #[serde(default, skip_serializing_if = "Option::is_none")]\n    // [impl->REQ-SESSION-RESUME-TEMPLATE]','    // [impl->REQ-SESSION-RESUME-TEMPLATE]')
# Above replace is best-effort; do the field-type swap that forces a hard error:
mut2 = s.replace('pub cwd: Option<String>','pub cwd: String')
# also drop the serde attr so it's truly required
mut2 = mut2.replace('    #[serde(default, skip_serializing_if = "Option::is_none")]\n    pub cwd: String','    pub cwd: String')
assert mut2!=s
open(p,"w").write(mut2)
EOF
echo "=== with cwd as REQUIRED String, old row (no cwd) drops → len==1 not 2 → FAILED ==="
cargo test -p spt-store --lib pre_migration_row_without_cwd_deserializes_as_none 2>&1 | grep -E "test result|FAILED|error\[|both an old|cannot"
cp /tmp/ss.bak crates/spt-store/src/sessions.rs
echo "=== reverted ==="

Prove the back-compat test fails when cwd is genuinely required


=== with cwd as REQUIRED String, old row (no cwd) drops → len==1 not 2 → FAILED ===
error[E0308]: mismatched types
error[E0308]: mismatched types
error[E0599]: no method named `as_deref` found for struct `std::string::String` in the current scope
error[E0599]: no method named `is_none` found for struct `std::string::String` in the current scope
error[E0599]: no method named `as_deref` found for struct `std::string::String` in the current scope
error[E0308]: mismatched types
=== reverted ===


That mutation broke compilation (the test itself constructs `cwd: None`). The key insight: serde treats a missing `Option<T>` field as `None` even without `#[serde(default)]`, so the back-compat behavior is structurally guaranteed by the `Option` type. My test correctly asserts the real, observable contract (old row → `cwd: None`, new row → `Some`). It's a valid regression guard against anyone changing `cwd` to a required non-Option field — exactly the kind of change that would reintroduce the bug. Good enough; the test guards the observable contract.

Now let me write the integration test. First I'll extend the mock adapter minimally to write a marker file recording cwd and which template ran.


Edit

            let heartbeat_ms: u64 = arg(&args, "--heartbeat-ms")
                .and_then(|s| s.parse().ok())
                .unwrap_or(500);

            let heartbeat_ms: u64 = arg(&args, "--heartbeat-ms")
                .and_then(|s| s.parse().ok())
                .unwrap_or(500);

            // [int->REQ-SESSION-RESUME-TEMPLATE] Optional resume-template proof: when
            // `--marker-file <path>` is set, write a marker whose CONTENTS record
            // (a) which session template launched this dummy — `resume` if the
            // command carried `--resume-marker`, else `self` — and (b) the
            // `current_dir()` the harness landed in. The int test declares a
            // `[session.self]` with a SELF marker path and a `[session.resume]` with
            // a DIFFERENT RESUME marker path (plus the `--resume-marker` token), so a
            // `--resume` bringup proves both the template SELECTION and the cwd
            // plumbing. Tiny + cross-platform; absent ⇒ unchanged dummy behavior.
            if let Some(marker) = arg(&args, "--marker-file") {
                let which = if args.iter().any(|a| a == "--resume-marker") {
                    "resume"
                } else {
                    "self"
                };
                let cwd = std::env::current_dir()
                    .map(|p| p.display().to_string())
                    .unwrap_or_else(|_| "<no-cwd>".to_string());
                // `template=<self|resume>\ncwd=<dir>\n` — a parseable two-line proof.
                let _ = std::fs::write(&marker, format!("template={which}\ncwd={cwd}\n"));
            }


The file C:\Users\decid\Documents\projects\spt-core\adapters\mock\src\main.rs has been updated successfully. (file state is current in your context — no need to Read it back)

            let heartbeat_ms: u64 = arg(&args, "--heartbeat-ms")
                .and_then(|s| s.parse().ok())
                .unwrap_or(500);

            // [int->REQ-SESSION-RESUME-TEMPLATE] Optional resume-template proof: when
            // `--marker-file <path>` is set, write a marker whose CONTENTS record
            // (a) which session template launched this dummy — `resume` if the
            // command carried `--resume-marker`, else `self` — and (b) the
            // `current_dir()` the harness landed in. The int test declares a
            // `[session.self]` with a SELF marker path and a `[session.resume]` with
            // a DIFFERENT RESUME marker path (plus the `--resume-marker` token), so a
            // `--resume` bringup proves both the template SELECTION and the cwd
            // plumbing. Tiny + cross-platform; absent ⇒ unchanged dummy behavior.
            if let Some(marker) = arg(&args, "--marker-file") {
                let which = if args.iter().any(|a| a == "--resume-marker") {
                    "resume"
                } else {
                    "self"
                };
                let cwd = std::env::current_dir()
                    .map(|p| p.display().to_string())
                    .unwrap_or_else(|_| "<no-cwd>".to_string());
                // `template=<self|resume>\ncwd=<dir>\n` — a parseable two-line proof.
                let _ = std::fs::write(&marker, format!("template={which}\ncwd={cwd}\n"));
            }

//! `mock-session` — the generic mock adapter's "agent session". A trivial,
//! cross-platform stand-in for a real harness session (no `claude`, no `.claude`,
//! no CC hooks). When `ManifestRuntime` spawns it from the mock manifest's
//! `[session.self]` template, it does what a real session's boot hook would do:
//! fire `spt api bind` to flip its perch live, then `spt api state idle`.
//!
//! It shells out to the `spt` binary exactly as a real adapter would — resolved
//! from `--spt <path>`, else `$SPT_BIN`, else `spt` on PATH. This is what proves
//! the inbound contract is harness-agnostic: a dumb helper drives it with only
//! `spt api` calls.

use std::process::Command;

fn arg(args: &[String], flag: &str) -> Option<String> {
    args.iter()
        .position(|a| a == flag)
        .and_then(|i| args.get(i + 1).cloned())
}

fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();

    // M2b lifecycle stand-in modes. `psyche` is the Psyche companion (spt-core
    // binds its nested perch; the child just needs to launch). `echo` is the
    // cheap-model echo-commune: read history on stdin, emit a brief on stdout.
    match arg(&args, "--mode").as_deref() {
        Some("psyche") => {
            let id = arg(&args, "--id").unwrap_or_else(|| "mock-psyche".to_string());
            println!("MOCK_PSYCHE_UP:{id}");
            return;
        }
        Some("echo") => {
            use std::io::Read;
            let mut history = String::new();
            let _ = std::io::stdin().read_to_string(&mut history);
            // A deterministic brief; a real echo summarizes the history it read.
            println!("mock echo brief ({} history bytes)", history.len());
            return;
        }
        Some("dummy") => {
            // The DUMMY-HARNESS fixture (v0.12.1 real-harness gating): a long-lived
            // stand-in for a real harness session. Binds its perch (the harness
            // contract) exactly like the default stand-in, then prints a stdout
            // heartbeat on an interval and STAYS ALIVE until killed — so the REAL
            // `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc`
            // attach path can be exercised end-to-end (the default mode exits
            // immediately, which is why the v0.12.0 mock tests never caught the
            // daemon+broker+PTY lifecycle defects). Isolates "broker wedged /
            // endpoint died" from "a real harness failed to launch."
            use std::io::Write;
            let id = arg(&args, "--id").unwrap_or_else(|| {
                eprintln!("mock-session --mode dummy: --id required");
                std::process::exit(2);
            });
            let session_id =
                arg(&args, "--session-id").unwrap_or_else(|| "mock-session-1".to_string());
            let spt = arg(&args, "--spt")
                .or_else(|| std::env::var("SPT_BIN").ok())
                .unwrap_or_else(|| "spt".to_string());
            let adapter = arg(&args, "--mock-adapter").unwrap_or_else(|| "mock".to_string());
            let endpoint_type = arg(&args, "--type");
            let heartbeat_ms: u64 = arg(&args, "--heartbeat-ms")
                .and_then(|s| s.parse().ok())
                .unwrap_or(500);

            // Bind the perch live (spt-hosted direct bind, mirroring the default
            // stand-in below). A failure is fatal — the fixture must come up.
            let mut bind_args = vec![
                "api".to_string(),
                "--adapter".to_string(),
                adapter,
                "bind".to_string(),
                id.clone(),
                "--set-session-id".to_string(),
                session_id,
            ];
            if let Some(ty) = &endpoint_type {
                bind_args.push("--type".to_string());
                bind_args.push(ty.clone());
            }
            let bind = Command::new(&spt).args(&bind_args).status();
            if !matches!(bind, Ok(s) if s.success()) {
                eprintln!("mock-session --mode dummy: api bind failed: {bind:?}");
                std::process::exit(1);
            }

            println!("DUMMY_HARNESS_READY:{id}");
            let _ = std::io::stdout().flush();
            // Heartbeat forever — only an external kill (tab-close job reap, daemon
            // stop, or an explicit kill) ends it.
            let mut n: u64 = 0;
            loop {
                println!("DUMMY_HARNESS_TICK:{id}:{n}");
                let _ = std::io::stdout().flush();
                n += 1;
                std::thread::sleep(std::time::Duration::from_millis(heartbeat_ms));
            }
        }
        _ => {} // default: the agent-session stand-in below.
    }

    let id = arg(&args, "--id").unwrap_or_else(|| {
        eprintln!("mock-session: --id required");
        std::process::exit(2);
    });
    let session_id = arg(&args, "--session-id").unwrap_or_else(|| "mock-session-1".to_string());
    let spt = arg(&args, "--spt")
        .or_else(|| std::env::var("SPT_BIN").ok())
        .unwrap_or_else(|| "spt".to_string());
    // Optional endpoint type (the open type system): a non-agent endpoint — e.g.
    // a Gateway — binds with `--type gateway`. Absent ⇒ the default live_agent.
    let endpoint_type = arg(&args, "--type");
    let adapter = arg(&args, "--mock-adapter").unwrap_or_else(|| "mock".to_string());

    // 1. Post-spawn bind: flip the perch skeleton→live under this session id.
    let mut bind_args = vec![
        "api".to_string(),
        "--adapter".to_string(),
        adapter.clone(),
        "bind".to_string(),
        id.clone(),
        "--set-session-id".to_string(),
        session_id.clone(),
    ];
    if let Some(ty) = &endpoint_type {
        bind_args.push("--type".to_string());
        bind_args.push(ty.clone());
    }
    let bind = Command::new(&spt).args(&bind_args).status();
    if !matches!(bind, Ok(s) if s.success()) {
        eprintln!("mock-session: api bind failed: {bind:?}");
        std::process::exit(1);
    }

    // 2. Report idle (activity sentinel + echo-gate), authenticating by session.
    let state = Command::new(&spt)
        .args([
            "api",
            "--adapter",
            &adapter,
            "state",
            "idle",
            &id,
            "--session-id",
            &session_id,
        ])
        .status();
    if !matches!(state, Ok(s) if s.success()) {
        eprintln!("mock-session: api state idle failed: {state:?}");
        std::process::exit(1);
    }

    println!("MOCK_SESSION_READY:{id}");
}

+            // [int->REQ-SESSION-RESUME-TEMPLATE] Optional resume-template proof: when

+            // `--marker-file <path>` is set, write a marker whose CONTENTS record

+            // (a) which session template launched this dummy — `resume` if the

+            // command carried `--resume-marker`, else `self` — and (b) the

+            // `current_dir()` the harness landed in. The int test declares a

+            // `[session.self]` with a SELF marker path and a `[session.resume]` with

+            // a DIFFERENT RESUME marker path (plus the `--resume-marker` token), so a

+            // `--resume` bringup proves both the template SELECTION and the cwd

+            // plumbing. Tiny + cross-platform; absent ⇒ unchanged dummy behavior.

+            if let Some(marker) = arg(&args, "--marker-file") {

+                let which = if args.iter().any(|a| a == "--resume-marker") {

+                let cwd = std::env::current_dir()

+                    .map(|p| p.display().to_string())

+                    .unwrap_or_else(|_| "<no-cwd>".to_string());

+                // `template=<self|resume>\ncwd=<dir>\n` — a parseable two-line proof.

+                let _ = std::fs::write(&marker, format!("template={which}\ncwd={cwd}\n"));


Bash

cargo build -p mock-adapter --bin mock-session 2>&1 | tail -4

Rebuild mock-session with marker support


   Compiling mock-adapter v0.12.1 (C:\Users\decid\Documents\projects\spt-core\adapters\mock)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s


Now let me look at the `common` test helper module to ensure I reuse the right helpers, then write the int test.


No files found


Found 30 files
crates\spt\tests\dummy_harness_e2e.rs
crates\spt\tests\job_escape_e2e.rs
crates\spt\tests\attach_wedge_e2e.rs
crates\spt\tests\brain_survive.rs
crates\spt\tests\bind_cwd_project_e2e.rs
crates\spt\tests\ready_resume_ledger_e2e.rs
crates\spt\tests\brain_restart_psyche_dup_e2e.rs
crates\spt\tests\unhost_psyche_reap_e2e.rs
crates\spt\tests\livehost_bootgate_e2e.rs
crates\spt\tests\live_resolve_e2e.rs
crates\spt\tests\livehost_bootrace_e2e.rs
crates\spt\tests\livehost_nonresident_e2e.rs
crates\spt\tests\livehost_psyche_fail_e2e.rs
crates\spt\tests\tunnel_e2e.rs
crates\spt\tests\shell_actgate_e2e.rs
crates\spt\tests\live_bind_firsthost_e2e.rs
crates\spt\tests\live_firsthost_e2e.rs
crates\spt\tests\gateway_owner_shell_e2e.rs
crates\spt\tests\contract_e2e.rs
crates\spt\tests\drive_e2e.rs
crates\spt\tests\shell_e2e.rs
crates\spt\tests\poll_envelope_e2e.rs
crates\spt\tests\shell_sleepwake_e2e.rs
crates\spt\tests\gateway_e2e.rs
crates\spt\tests\brain_respawn_rename.rs
crates\spt\tests\quickstart_e2e.rs
crates\spt\tests\n1_compat.rs
crates\spt\tests\brain_split.rs
crates\spt\tests\n1_pairing.rs
crates\spt\tests\common\mod.rs


The `common` module is at `crates/spt/tests/common/mod.rs`. Let me look at the bind_cwd_project_e2e.rs since it's closest to what I need (asserting recorded cwd), and check the common module's helpers.


1	//! Shared test helpers for the `spt` integration tests.
2	//!
3	//! `tests/common/mod.rs` (a subdirectory module) is NOT compiled as its own test
4	//! binary — each test file pulls it in with `mod common;`.
5
6	use std::process::Command;
7
8	/// Chainable: suppress the ephemeral console window a Windows console-subsystem
9	/// child pops when the test harness has a console (every `spt daemon`/`api`/
10	/// `brain` subprocess a test spawns). No-op on non-Windows. CREATE_NO_WINDOW does
11	/// NOT detach — env/stdio inheritance is unchanged — it only hides the console.
12	///
13	/// Usage: `Command::new(bin).no_window().args([...]).spawn()`.
14	pub trait CommandNoWindowExt {
15	    fn no_window(&mut self) -> &mut Command;
16	}
17
18	impl CommandNoWindowExt for Command {
19	    #[cfg(windows)]
20	    fn no_window(&mut self) -> &mut Command {
21	        use std::os::windows::process::CommandExt;
22	        const CREATE_NO_WINDOW: u32 = 0x0800_0000;
23	        self.creation_flags(CREATE_NO_WINDOW)
24	    }
25
26	    #[cfg(not(windows))]
27	    fn no_window(&mut self) -> &mut Command {
28	        self
29	    }
30	}
31

//! Shared test helpers for the `spt` integration tests.
//!
//! `tests/common/mod.rs` (a subdirectory module) is NOT compiled as its own test
//! binary — each test file pulls it in with `mod common;`.

use std::process::Command;

/// Chainable: suppress the ephemeral console window a Windows console-subsystem
/// child pops when the test harness has a console (every `spt daemon`/`api`/
/// `brain` subprocess a test spawns). No-op on non-Windows. CREATE_NO_WINDOW does
/// NOT detach — env/stdio inheritance is unchanged — it only hides the console.
///
/// Usage: `Command::new(bin).no_window().args([...]).spawn()`.
pub trait CommandNoWindowExt {
    fn no_window(&mut self) -> &mut Command;
}

impl CommandNoWindowExt for Command {
    #[cfg(windows)]
    fn no_window(&mut self) -> &mut Command {
        use std::os::windows::process::CommandExt;
        const CREATE_NO_WINDOW: u32 = 0x0800_0000;
        self.creation_flags(CREATE_NO_WINDOW)
    }

    #[cfg(not(windows))]
    fn no_window(&mut self) -> &mut Command {
        self
    }
}


1	//! v0.13.0 Wave W3 (REQ-HAZARD-BIND-CWD-UNSET) — the REAL-fixture gate for the
2	//! refuted v0.12.1 "fresh agent shows under its project tab" P1.
3	//!
4	//! Before W3, `info.cwd` was NEVER set on bind, so the picker
5	//! (`picker/data.rs` → `project_id_for_dir`) could not place a freshly-created
6	//! endpoint under its Project category — it only appeared once it had committed
7	//! context. This test drives the SAME real spt-hosted bringup as
8	//! `dummy_harness_e2e.rs` — `spt endpoint run --start` → broker PTY → the dummy
9	//! harness shells `spt api bind` — and then asserts the live perch records a
10	//! non-empty `info.cwd`, and that the cwd-derived project membership (the exact
11	//! `project_id_for_dir` + origin-union rule the picker's `local_rows` runs)
12	//! places the endpoint under its project.
13	//!
14	//! `spt` is a BIN-only crate (no lib target), so the picker's
15	//! `picker::data::gather_endpoints` is NOT reachable from an integration test.
16	//! We assert the EQUIVALENT membership over the public `spt_store` path the
17	//! picker feeds from: `info.cwd` → `project::project_id_for_dir` → origin-union.
18	//! The pure union rule is unit-tested in `picker/data.rs`
19	//! (`origin_project_unions_into_membership`); this test proves the REAL perch
20	//! carries the cwd that rule consumes.
21	//!
22	//! Bounded; all spawned pids reaped SCOPED (never machine-wide — shared runner).
23
24	use std::path::{Path, PathBuf};
25	use std::process::{Child, Command, Output, Stdio};
26	use std::time::{Duration, Instant};
27
28	mod common;
29	use common::CommandNoWindowExt;
30
31	use spt_store::perch::{self, ParentHint};
32
33	fn kill_pid(pid: u32) {
34	    #[cfg(windows)]
35	    let _ = Command::new("taskkill")
36	        .no_window()
37	        .args(["/PID", &pid.to_string(), "/F", "/T"])
38	        .output();
39	    #[cfg(unix)]
40	    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
41	}
42
43	fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
44	    let (tx, rx) = std::sync::mpsc::channel();
45	    std::thread::spawn(move || {
46	        let _ = tx.send(cmd.output());
47	    });
48	    rx.recv_timeout(deadline)
49	        .expect("captured spt call must complete within its deadline")
50	        .expect("run spt")
51	}
52
53	fn ready_pid(path: &Path) -> Option<u32> {
54	    let s = std::fs::read_to_string(path).ok()?;
55	    serde_json::from_str::<serde_json::Value>(&s)
56	        .ok()?
57	        .get("pid")?
58	        .as_u64()
59	        .map(|p| p as u32)
60	}

//! v0.13.0 Wave W3 (REQ-HAZARD-BIND-CWD-UNSET) — the REAL-fixture gate for the
//! refuted v0.12.1 "fresh agent shows under its project tab" P1.
//!
//! Before W3, `info.cwd` was NEVER set on bind, so the picker
//! (`picker/data.rs` → `project_id_for_dir`) could not place a freshly-created
//! endpoint under its Project category — it only appeared once it had committed
//! context. This test drives the SAME real spt-hosted bringup as
//! `dummy_harness_e2e.rs` — `spt endpoint run --start` → broker PTY → the dummy
//! harness shells `spt api bind` — and then asserts the live perch records a
//! non-empty `info.cwd`, and that the cwd-derived project membership (the exact
//! `project_id_for_dir` + origin-union rule the picker's `local_rows` runs)
//! places the endpoint under its project.
//!
//! `spt` is a BIN-only crate (no lib target), so the picker's
//! `picker::data::gather_endpoints` is NOT reachable from an integration test.
//! We assert the EQUIVALENT membership over the public `spt_store` path the
//! picker feeds from: `info.cwd` → `project::project_id_for_dir` → origin-union.
//! The pure union rule is unit-tested in `picker/data.rs`
//! (`origin_project_unions_into_membership`); this test proves the REAL perch
//! carries the cwd that rule consumes.
//!
//! Bounded; all spawned pids reaped SCOPED (never machine-wide — shared runner).

use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
use std::time::{Duration, Instant};

mod common;
use common::CommandNoWindowExt;

use spt_store::perch::{self, ParentHint};

fn kill_pid(pid: u32) {
    #[cfg(windows)]
    let _ = Command::new("taskkill")
        .no_window()
        .args(["/PID", &pid.to_string(), "/F", "/T"])
        .output();
    #[cfg(unix)]
    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
}

fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        let _ = tx.send(cmd.output());
    });
    rx.recv_timeout(deadline)
        .expect("captured spt call must complete within its deadline")
        .expect("run spt")
}

fn ready_pid(path: &Path) -> Option<u32> {
    let s = std::fs::read_to_string(path).ok()?;
    serde_json::from_str::<serde_json::Value>(&s)
        .ok()?
        .get("pid")?
        .as_u64()
        .map(|p| p as u32)
}


I have everything I need. Now I'll write the int test. The key design points:
- Register a dummyharness adapter with `[session.self]` writing a SELF marker and `[session.resume]` writing a RESUME marker (with `--resume-marker` token) — both into `current_dir()` via a marker path under the test's launch cwd.
- The `endpoint run --start` shortcut launches with cwd=None → harness inherits broker cwd which inherits the test process cwd. For the resume run, I pass through that same fallback. The marker's recorded cwd should equal the launch cwd.

Let me set distinct marker file paths so SELF vs RESUME are distinguishable, and use `--start` (detached) for both runs to keep it deterministic (avoid the attach-pump flakiness). I'll write the marker into the test's `home`/markers dir so both runs can write there regardless of cwd, but the marker *contents* record `current_dir()` which proves the cwd plumbing.


Write

//! v0.13.0 P2 (REQ-SESSION-RESUME-TEMPLATE) — the real-fixture gate for the
//! resume-blank ship-blocker. Root cause: spt-core had `[session.self]` (fresh)
//! but no `[session.resume]` native-resume sibling, so `--resume` re-ran the
//! FRESH command → the harness started a blank transcript.
//!
//! This drives the SAME real spt-hosted bringup as `dummy_harness_e2e.rs` —
//! `spt endpoint run --start` → broker PTY → the dummy harness — against a REAL
//! detached `spt daemon run`, with a dummyharness adapter whose `[session.resume]`
//! command DIFFERS from `[session.self]`. The dummy writes a MARKER file recording
//! (a) which template launched it (`self` vs `resume`) and (b) the `current_dir()`
//! it landed in. We assert:
//!   - a FRESH `endpoint run --start` (no `--resume`) writes the SELF marker, and
//!     the RESUME marker is ABSENT;
//!   - a `--resume <session>` `endpoint run --start` writes the RESUME marker
//!     (proving template SELECTION), and the recorded cwd matches the cwd the
//!     bringup landed in (proving the cwd plumbing).
//!
//! Both bringups use `--start` (detached, return-now) — no attach pump — so the
//! marker file is the deterministic proof, with no PTY-output capture race. The
//! marker is written into the isolated SPT_HOME (always writable regardless of
//! the harness cwd); its CONTENTS carry the harness `current_dir()` so the cwd
//! assertion is independent of where the marker file itself lives.
//!
//! Bounded; all spawned pids reaped SCOPED (never machine-wide — shared runner).
//! [int->REQ-SESSION-RESUME-TEMPLATE]

use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
use std::time::{Duration, Instant};

mod common;
use common::CommandNoWindowExt;

use spt_store::perch::{self, ParentHint};

fn kill_pid(pid: u32) {
    #[cfg(windows)]
    let _ = Command::new("taskkill")
        .no_window()
        .args(["/PID", &pid.to_string(), "/F", "/T"])
        .output();
    #[cfg(unix)]
    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
}

fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        let _ = tx.send(cmd.output());
    });
    rx.recv_timeout(deadline)
        .expect("captured spt call must complete within its deadline")
        .expect("run spt")
}

fn ready_pid(path: &Path) -> Option<u32> {
    let s = std::fs::read_to_string(path).ok()?;
    serde_json::from_str::<serde_json::Value>(&s)
        .ok()?
        .get("pid")?
        .as_u64()
        .map(|p| p as u32)
}

fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {
    let deadline = Instant::now() + budget;
    while Instant::now() < deadline {
        if let Some(p) = ready_pid(path) {
            return Some(p);
        }
        std::thread::sleep(Duration::from_millis(50));
    }
    None
}

fn sibling_bin(name: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_BIN_EXE_spt"))
        .with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX))
}

/// Poll for a marker file to appear (the dummy writes it before its heartbeat
/// loop), bounded. Returns its parsed `(template, cwd)` lines, or None on timeout.
fn wait_for_marker(path: &Path, budget: Duration) -> Option<(String, String)> {
    let deadline = Instant::now() + budget;
    while Instant::now() < deadline {
        if let Ok(s) = std::fs::read_to_string(path) {
            let mut template = None;
            let mut cwd = None;
            for line in s.lines() {
                if let Some(v) = line.strip_prefix("template=") {
                    template = Some(v.to_string());
                }
                if let Some(v) = line.strip_prefix("cwd=") {
                    cwd = Some(v.to_string());
                }
            }
            if let (Some(t), Some(c)) = (template, cwd) {
                return Some((t, c));
            }
        }
        std::thread::sleep(Duration::from_millis(50));
    }
    None
}

#[test]
fn endpoint_run_resume_selects_resume_template_in_recorded_cwd() {
    // ── (1) Isolated SPT_HOME. ──
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let mock_session = sibling_bin("mock-session");
    assert!(
        mock_session.exists(),
        "the dummy-harness program must be built: {} \
         (pre-build: cargo build -p mock-adapter --bin mock-session)",
        mock_session.display()
    );

    // Distinct marker paths under the isolated home — always writable regardless
    // of the harness cwd. The marker CONTENTS carry the harness current_dir(), so
    // the cwd assertion is independent of where the marker file lives.
    let markers = home.path().join("markers");
    std::fs::create_dir_all(&markers).unwrap();
    let self_marker = markers.join("self.marker");
    let resume_marker = markers.join("resume.marker");
    let self_marker_for_cmd = self_marker.to_string_lossy().replace('\\', "/");
    let resume_marker_for_cmd = resume_marker.to_string_lossy().replace('\\', "/");

    // ── (2) Register the dummyharness adapter with a [session.self] that writes a
    //    SELF marker and a DISTINCT [session.resume] that writes a RESUME marker
    //    (plus the `--resume-marker` token the dummy stamps into the marker). Both
    //    keep the long-lived dummy heartbeating + binding its perch, so the real
    //    broker-PTY bringup path runs end-to-end. ──
    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\', "/");
    let mock_for_cmd = mock_session.to_string_lossy().replace('\\', "/");
    let self_cmd = format!(
        "{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \
         --spt {spt_for_cmd} --mock-adapter dummyresume --type live_agent --heartbeat-ms 200 \
         --marker-file {self_marker_for_cmd}"
    );
    let resume_cmd = format!(
        "{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \
         --spt {spt_for_cmd} --mock-adapter dummyresume --type live_agent --heartbeat-ms 200 \
         --marker-file {resume_marker_for_cmd} --resume-marker"
    );

    let exe_suffix = spt_bin
        .extension()
        .map(|e| format!(".{}", e.to_string_lossy()))
        .unwrap_or_default();
    let src = perch::spt_home().join("srcs").join("dummyresume");
    std::fs::create_dir_all(&src).unwrap();
    // A long-lived psyche fixture so the brain's reconcile marks status=online
    // (cmd_bind gates online to live_agent && psyche_init.is_some()).
    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the psyche binary fixture");
    let drops = home.path().join("drops");
    std::fs::create_dir_all(&drops).unwrap();
    let dir = drops.to_string_lossy().replace('\\', "/");
    let manifest_toml = format!(
        "[adapter]\nname=\"dummyresume\"\nkind=\"harness\"\nversion=\"1\"\n\
         min_spt_core_version=\"0\"\n\n[session.self]\ncommand='{self_cmd}'\nkeys=[\"id\",\"session_id\"]\n\n\
         [session.resume]\ncommand='{resume_cmd}'\nkeys=[\"id\",\"session_id\"]\n\n\
         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
         [session.psyche_init]\ncommand='psychebin ready {{id}}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
    );
    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    // ── (3) The REAL daemon broker process. ──
    let brain_log = home.path().join("brain.stderr.log");
    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
    let mut broker: Child = Command::new(&spt_bin)
        .no_window()
        .args(["daemon", "run"])
        .env("SPT_HOME", home.path())
        .stdout(Stdio::null())
        .stderr(Stdio::from(brain_log_file))
        .spawn()
        .expect("spawn spt daemon run (broker process)");
    let brain_pid =
        match wait_for_ready_pid(&home.path().join("brain.ready"), Duration::from_secs(30)) {
            Some(p) => p,
            None => {
                let _ = broker.kill();
                let _ = broker.wait();
                panic!(
                    "PRECONDITION: brain never came up.\n{}",
                    std::fs::read_to_string(&brain_log).unwrap_or_default()
                );
            }
        };

    // ── (4) FRESH bringup: `endpoint run --start` with NO --resume → the harness
    //    runs [session.self] → SELF marker present, RESUME marker ABSENT. ──
    let id = "dummyresume1";
    let fresh_run = {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window()
            .args(["endpoint", "run", "--adapter", "dummyresume", "--id", id, "--start"])
            .env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(45))
    };
    let fresh_stderr = String::from_utf8_lossy(&fresh_run.stderr).to_string();
    let harness_pid_1: Option<u32> = fresh_stderr
        .split("pid=")
        .nth(1)
        .and_then(|s| s.split_whitespace().next())
        .and_then(|s| s.trim().parse().ok());

    // The fresh harness writes its SELF marker shortly after spawn.
    let self_seen = wait_for_marker(&self_marker, Duration::from_secs(20));
    // The launch cwd the broker inherited (the test process cwd) — what the
    // harness current_dir() should equal (the W1 --start shortcut inherits it).
    let launch_cwd = std::env::current_dir()
        .ok()
        .map(|p| p.to_string_lossy().into_owned());

    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);
    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);
    let mut online_1 = false;
    let deadline = Instant::now() + Duration::from_secs(20);
    while Instant::now() < deadline {
        if status().as_deref() == Some(spt_store::liveness::STATUS_ONLINE) {
            online_1 = true;
            break;
        }
        std::thread::sleep(Duration::from_millis(150));
    }

    // Record whether the RESUME marker exists BEFORE the resume run (must not).
    let resume_marker_before = resume_marker.exists();

    // ── (5) Drive the endpoint offline (kill the fresh harness), wait OFFLINE, so
    //    the resume re-bringup is a clean second spawn (not racing the first). ──
    if let Some(p) = harness_pid_1 {
        kill_pid(p);
    }
    let mut went_offline = false;
    let off_deadline = Instant::now() + Duration::from_secs(15);
    while Instant::now() < off_deadline {
        if status().as_deref() == Some(spt_store::liveness::STATUS_OFFLINE) {
            went_offline = true;
            break;
        }
        std::thread::sleep(Duration::from_millis(150));
    }

    // ── (6) RESUME bringup: `endpoint run --resume <session> --start`. is_resume
    //    selects [session.resume] → RESUME marker present, recording current_dir().
    //    The session id is arbitrary (the dummy just echoes it) — what matters is
    //    that --resume flips template selection and the cwd plumbing lands. ──
    let resumed_session = "resumed-session-xyz";
    let resume_run = {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window()
            .args([
                "endpoint", "run", "--adapter", "dummyresume", "--id", id, "--resume",
                resumed_session, "--start",
            ])
            .env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(45))
    };
    let resume_stderr = String::from_utf8_lossy(&resume_run.stderr).to_string();
    let harness_pid_2: Option<u32> = resume_stderr
        .split("pid=")
        .nth(1)
        .and_then(|s| s.split_whitespace().next())
        .and_then(|s| s.trim().parse().ok());

    let resume_seen = wait_for_marker(&resume_marker, Duration::from_secs(20));

    // ── (7) Reap the ENTIRE tree SCOPED before asserting (never leave a stray
    //    spt.exe — a survivor locks target\debug\spt.exe). ──
    if let Some(p) = harness_pid_1 {
        kill_pid(p);
    }
    if let Some(p) = harness_pid_2 {
        kill_pid(p);
    }
    let psyche_perch = perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
    if let Some(p) = spt_store::info::read_pid(&psyche_perch) {
        kill_pid(p);
    }
    let _ = {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window()
            .args(["daemon", "stop"])
            .env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(20))
    };
    kill_pid(brain_pid);
    let _ = broker.kill();
    let _ = broker.wait();
    let reap_deadline = Instant::now() + Duration::from_secs(10);
    while Instant::now() < reap_deadline {
        let brain_gone = !spt_store::proc::is_process_alive(brain_pid);
        let h1_gone = harness_pid_1
            .map(|p| !spt_store::proc::is_process_alive(p))
            .unwrap_or(true);
        let h2_gone = harness_pid_2
            .map(|p| !spt_store::proc::is_process_alive(p))
            .unwrap_or(true);
        if brain_gone && h1_gone && h2_gone {
            break;
        }
        std::thread::sleep(Duration::from_millis(100));
    }

    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
    eprintln!(
        "=== DIAGNOSTIC (RESUME-TEMPLATE): fresh_ok={} online_1={online_1} \
         went_offline={went_offline} resume_ok={} resume_before={resume_marker_before} \
         self_seen={self_seen:?} resume_seen={resume_seen:?} launch_cwd={launch_cwd:?} ===\n\
         === fresh stderr ===\n{fresh_stderr}\n=== resume stderr ===\n{resume_stderr}\n\
         === brain stderr ===\n{brain_stderr}",
        fresh_run.status.success(),
        resume_run.status.success(),
    );

    // ── (8) THE GATES. ──
    assert!(
        fresh_run.status.success(),
        "fresh endpoint run --start must succeed: {fresh_stderr}"
    );
    assert!(
        online_1,
        "PRECONDITION: the fresh bringup must bind ONLINE via the real broker PTY.\n\
         === brain stderr ===\n{brain_stderr}"
    );

    // FRESH → SELF template selected; RESUME marker absent before the resume run.
    let (self_template, _self_cwd) = self_seen.expect(
        "the FRESH bringup (no --resume) must run [session.self] and write the SELF marker — \
         template selection failed",
    );
    assert_eq!(
        self_template, "self",
        "a fresh bringup must run the [session.self] template"
    );
    assert!(
        !resume_marker_before,
        "the RESUME marker must be ABSENT after a fresh (non-resume) bringup — the fresh \
         path must NOT run [session.resume]"
    );

    assert!(
        resume_run.status.success(),
        "resume endpoint run --start must succeed: {resume_stderr}"
    );

    // RESUME → the [session.resume] template was selected (the ship-blocker fix:
    // --resume no longer re-runs the FRESH command).
    let (resume_template, resume_cwd) = resume_seen.expect(
        "the --resume bringup must run [session.resume] and write the RESUME marker — \
         the resume-template SELECTION (REQ-SESSION-RESUME-TEMPLATE) failed; pre-fix --resume \
         re-ran [session.self] → blank transcript",
    );
    assert_eq!(
        resume_template, "resume",
        "a --resume bringup must run the [session.resume] template, NOT [session.self]"
    );

    // The cwd plumbing: the resumed harness landed in the launch cwd (the W1
    // --start shortcut inherits the broker's cwd, which inherits this test
    // process's cwd). Compare canonicalized to absorb path-form differences
    // (8.3 short names, symlinked temp dirs, trailing separators).
    if let Some(expected) = launch_cwd {
        let got = std::path::Path::new(&resume_cwd);
        let want = std::path::Path::new(&expected);
        let got_c = got.canonicalize().unwrap_or_else(|_| got.to_path_buf());
        let want_c = want.canonicalize().unwrap_or_else(|_| want.to_path_buf());
        assert_eq!(
            got_c, want_c,
            "the resumed harness must land in the recorded/launch cwd \
             (got {resume_cwd:?}, want {expected:?}) — the cwd plumbing failed"
        );
    } else {
        // current_dir() unavailable (rare) — still assert the harness recorded a
        // non-empty, real cwd (the plumbing produced something, not <no-cwd>).
        assert!(
            !resume_cwd.is_empty() && resume_cwd != "<no-cwd>",
            "the resumed harness must record a real cwd, got {resume_cwd:?}"
        );
    }
}