diff --git a/docs/active-work.md b/docs/active-work.md index 83c3a51..a269a70 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -1,6 +1,6 @@ # Active work — cross-machine resume ledger -**Snapshot: 2026-07-26.** This file records volatile work that has not +**Snapshot: 2026-07-28.** This file records volatile work that has not landed on `main`. Read it after `docs/agent-handoff.md`. Remove completed entries when their PR merges; do not let this become a second permanent backlog. @@ -27,19 +27,12 @@ landed regardless of what a lane says. machine-local: `origin` may name this canonical URL, a release mirror, or something else, and therefore has no authority by name alone. - Canonical base at this snapshot: - `githubsucks/main` @ `42025e4` (Lean 4 Stage 4b #181, atop the dired - Stage 1 landed docs #169 and the PTY-terminate diagnostic #176, - terminal copy mode #178, the GPU-terminal-input landed docs #168, Lean - 4 Stage 4a #179, bottom-panel Stage 2A #177, the bottom-panel Stage 2 - framing #175, terminal configuration Stage 1 #173, Lean 4 Stage 3b - #170, Stage 3a #167, the CRDT undo repro #157, the inline-math - landed-doc refresh #172, the bottom-panel landed-doc refresh #156, the - inline-math slice #158, dired Stage 1 #165, the GPU terminal input fix - #166, Lean 4 Stage 2 #161, the dired framing #164, COHERENCE.md #163, - find-file #162, Lean 4 Stage 1 #160, and the minimap blank-slab fix - #159; protocol v20). The previous snapshot named `74301d1`, and **the - recovery floor advances with it**: the check below now requires - `42025e4` or newer, so a tree at `74301d1` no longer passes. That is + `githubsucks/main` @ `c2d56ff` (Journey Stage 1a #182, which + incorporated terminal configuration + copy mode #180, atop Lean 4 + Stage 4b #181 and the previously recorded landed work; protocol v20). + The previous snapshot named `42025e4`, and **the recovery floor + advances with it**: the check below now requires `c2d56ff` or newer, + so a tree at `42025e4` no longer passes. That is deliberate — the floor moves with the base, because a check that accepts an older commit than the declared base passes on a tree the rest of this file does not describe. @@ -78,7 +71,7 @@ git worktree list git status --short --branch ``` -The `git log` command must expose `42025e4` — the base named above — or a +The `git log` command must expose `c2d56ff` — the base named above — or a newer intentional main. Keep this threshold and the canonical-base line in step: a recovery check that accepts an older commit than the base it declares canonical will pass on a tree the rest of this file does not @@ -178,50 +171,91 @@ If it does not, stop and repair the remote/fetch configuration. to recur; the next occurrence carries its own evidence under whoever's PR, and a Stage B framing follows then. -## Journey Stage 1a — PR #182 OPEN, review round 1 closed +## Journey/GPU directory-target ratchet — PR #183 GATED ON PUBLIC PATH -- Framing `docs/journey-stage1a-framing.md` **rev 8** (four review - rounds, two correction revisions found during implementation, one from - review round 1 of PR #182). - Branch `journey-stage1a-directory-open`, based on `githubsucks/main` - @ `42025e4` (rebased onto `74301d1`, then integrated `42025e4` and the - landed-docs work below by merge — the branch is under review, so its - history is no longer rewritten). -- Recovery: `git fetch githubsucks && git checkout - journey-stage1a-directory-open`. Everything below is committed and - pushed; nothing depends on a worktree or `/tmp`. -- **Ships:** the directory arm on `resolve_target_buffer`, - `EditorState::open` rewritten as a caller of it (the unification), the - `path.open-directory` chain + `pmacs.path.directory_handler` fallback - slot, `pmacs.window.commit_to` with its scoped frontend and preflight, - the nonconstructible destination userdata, the daemon bootstrap arm, - and `tests/journey_acceptance.rs` (**24 pins** as of rev 8 — a count, - not a constant; re-read it rather than quoting this line). No protocol - change — still v20. -- **Doc updates ride the PR** per COHERENCE §25: §2 grade + step-3 - verdict row, §20 Priority 1 + the arc list, the GPU initial-target - framing's Q#GT6 / acceptance 10 supersession, handoff §1. -- **Bite results** (each mutation run against the full suite): scope - stops swapping `core.active_frontend` → N6a + P3 fail, nothing else; - preflight moved after the callback → P1 + P2 fail, nothing else; drop - the `ScopedFrontend` arm from `acting_frontend` → N4b fails, nothing - else. That last mutation is why N4b exists — it left N4 green. - Round 1 of PR #182 added two more: dired's `display` back to - `switch_buffer`, and `prev` read from the ambient window → each fails - **N4c** alone. **The scope pins the frontend, not the window** — every - routing pin before N4c varied frontend identity and none varied the - selected window within one frontend, so 23 green pins missed it. -- Ordering: PR #177 MERGED (2026-07-26), so 1a was unblocked. 1a lands - before dired Stage 2. When 1a lands, Stage 2 must re-scout and revise - its framing around the scoped `pmacs.window.commit_to` boundary before - its implementation branch is cut. That revision is a prerequisite, not - a review-time discovery. -- **Named deferrals carried out of this stage:** dired's *interactive* - paths (`C-x d`, tree descent, refresh) still rely on the ambient - frontend a tick later and are not migrated onto captured destinations; - the stale startup scratch buffer is still not removed (only the false - doc comment is corrected); `resolve_target_buffer`'s directory arm has - no picker, only the chain that leaves room for one. +- **Approved correction, not new product behavior.** GPU initial-target + framing Q#GT6 / acceptance 10 and Journey Stage 1a N2/N5 already make + a directory target a success; `COHERENCE.md` §2 treats that path as a + protected journey. The stale GPU integration test still listed `"."` + among malformed/unloadable targets after #182 landed. +- Branch `journey-gpu-directory-ratchet`, based directly on canonical + `main` @ `c2d56ff`. Recovery: `git fetch githubsucks && git checkout + journey-gpu-directory-ratchet`. Everything described here is + committed and pushed; nothing depends on the `/tmp` worktree. +- PR #183: + . It is intentionally + open and unmerged pending user review. +- **Scope is one acceptance ratchet:** remove `"."` from the four + genuinely invalid cases and drive the public `pmacs --gpu .` root + broker through the real managed GPU connector. The positive requires + snapshot-first + `InitialTargetResult::Opened`, then consumes the + post-quiescence replacement snapshot and requires dired's canonical + header plus a known directory entry before proving the same daemon can + open a following file target. The private display-less acceptance + probe now reports its snapshot count and final materialized text so + that public path is observable. No normal frontend/daemon behavior, + protocol, framing decision, or coherence grade changes. +- **Review round 1: three findings, all real and corrected.** The + first test stopped at the deliberately pre-existing bootstrap + document, so it did not pin the resolver's later dired commit. The + Stage 2 rev-5 recovery bullet named rev 4's framing branch. And the + durable handoff still named pre-Journey `main` even though this ledger + had advanced. The first is now a post-quiescence transport assertion; + the latter two are corrected in this revision. The complete matrix + below is green on the corrected tree. +- **Live public-path check tightened that correction further.** A user + report that `pmacs --gpu .` differed from `pmacs .` did not reproduce: + the live default daemon delivered both snapshots, and a traced + windowed invocation applied both and displayed dired. It nevertheless + exposed that the corrected acceptance still attached a raw protocol + client rather than invoking the public root broker and real GPU + connector. The test now covers those surfaces and passes **15/15**; + its full matrix is green at `34b8f28`. +- **Review round 2 found and fixed one failure-path cleanup gap at + `dc2dc42`.** The public-path test waited directly for dired's second + snapshot, but `ManagedProbe` only retained a spawned daemon's PID after + a successful wait. If the exact missing-snapshot regression recurred, + the timeout would therefore be unable to terminate that daemon. The + test now consumes the initial ready report first, retaining lifecycle + facts before it starts the post-quiescence assertion. +- **Review-round-2 gates are green:** `cargo fmt --check`; strict + workspace clippy; library **1,849 passed / 3 ignored**; CRDT library + **2,034 passed / 4 ignored**; focused public-path test **1/1** and full + GPU invocation suite **15/15**; M4 **121 passed / 3 ignored / 1 + filtered**; required GPU package **202/202**; `git diff --check`. + The first in-sandbox GPU-package attempt reproduced the repository's + documented Unix-socket restriction as three attach failures plus a + blocked peer read; the authoritative out-of-sandbox rerun passed all + **202** tests in under one second. +- **Full gate matrix is green on the public-path revision at `34b8f28`:** + - `cargo fmt --check`; + - strict workspace clippy; + - library **1,849 passed / 3 ignored**; + - CRDT library **2,034 passed / 4 ignored**; + - touched GPU initial-target suite **15/15**; + - connected Journey/Dired/find-file/theme/bottom-panel acceptance + suites **125/125**; + - M4 **121 passed / 3 ignored / 1 filtered**; + - required GPU package **202/202**; + - `git diff --check`; + - isolated-config, one-invocation full workspace sweep green on its + final run. +- **Gate diagnostics retained:** initial fresh-worktree attempts without + the documented `pmacs-gpu` prerequisite and without out-of-sandbox + Unix-socket permission were setup failures, not product evidence. + During the review rerun, an in-sandbox library attempt reproduced the + latter setup failure as `EPERM` in three attach socket tests; the + authoritative out-of-sandbox rerun passed all **1,849** non-ignored + tests. + The first full-workspace attempt then exhausted the `/tmp` filesystem + quota while linking. Moving only the disposable Cargo target to disk + let the sweep run; its first completed pass exposed one transient + `pmacs-gpu` font-facts unit-test red after the standalone GPU gate had + passed. The exact test passed immediately against the identical build, + and the required complete one-invocation rerun was green, including + GPU **202/202**. +- This side quest lands before bottom-panel 2B-1 opens its PR. After it + merges, 2B-1 integrates the new `main` and reruns its full matrix. ## The CRDT half of the test corpus is dark in CI — NEEDS A LANE @@ -358,10 +392,43 @@ If it does not, stop and repair the remote/fetch configuration. never been enforced. Any CI job that compiles the `crdt` targets has to fix them first or it will be red on arrival. -## Bottom-panel lane (Arc 7) — Stages 1, 2A + framing MERGED; 2B is next +## Bottom-panel lane (Arc 7) — 2B-1 GATED; waiting on ratchet side quest -Stage 1, the Stage 2 framing, and Stage 2A are all on `main`. **Stage 2B -has not started.** +Stage 1, the Stage 2 framing, and Stage 2A are on `main`. Framing +revision 5's three-way split of 2B was explicitly approved on +2026-07-27. **Stage 2B-1 is implemented, integrated with canonical +`main`, and pushed at `b9123c2`; no PR is open.** Its own omissions found +by gating are corrected. The Journey/GPU ratchet lane above is its sole +remaining dependency. + +- **Stage 2B-1 branch:** `bottom-panel-stage2b`, based on + `githubsucks/main` @ `c2d56ff` by merge because review had begun. + Recovery: `git fetch githubsucks && git checkout + bottom-panel-stage2b`. Everything is committed and pushed; nothing + depends on a worktree or `/tmp`. +- **Ships only the v21 wire layer:** the four wire shapes, version bump, + shared cell-grid validator, and version-ladder move. It has no + producer, consumer, or capability change; `panel_capable` stays + `false`, so this slice changes no user-visible journey grade. +- **Review round 1 closed:** two P1s and one P2, all corrected at + `9b364ad`: `PanelFrame` now identifies its buffer, the transport + ratchet covers the actual attach path rather than a detached codec + assertion, and shared grid bounds have one validator. +- **The full gate found and corrected two further 2B-1 omissions at + `b9123c2`:** the statusline version ladder still pinned v20/rejected + v21, and Vterm Stage 3 pinned v20 both structurally and in its real + headless probe. Those ratchets now expect v21 and, where applicable, + reject v22. +- **Green evidence except for the main-side stale ratchet this side + quest owns:** formatting and strict Clippy; default/CRDT libraries; + every bottom-panel, folding, GPU-font, statusline, semantic, Vterm, + M4, and required-GPU gate; and the isolated-config full-workspace + sweep. The exact counts and the classified M8 timing rerun live at + `b9123c2`. +- **Next ordering is fixed:** 2B-2 branches from `main` only after 2B-1 + lands; 2B-3 branches only after 2B-2 lands. The daemon epoch machine + belongs to 2B-2; the GPU band and negotiated capability flip belong + to 2B-3. - **Stage 2A MERGED as #177** (`main` @ `0a3fcd1`, 2026-07-26, all twelve checks green at `8424172`, three review rounds). Branch @@ -374,19 +441,7 @@ has not started.** `primary_document_window` returns `view.active` in every existing configuration, so this is seam adoption that becomes load-bearing in 2B. -- **Stage 2B is approved and unstarted.** It branches from `main`, **not - stacked on 2A**, per the framing §9. Scope: protocol v21, the daemon - panel projection, the GPU band, and the negotiated `panel_capable` - flip. `docs/bottom-panel-stage2-framing.md` §7.2 carries its five - acceptance criteria (A2B-1..5) plus the reassertion of parent - criterion 52, and §8 records **no open items**, so 2B needs no further - framing round. Its sharpest trap is §5.3's three-boundary split: the - GPU `text_area_bottom` is `status_band_top`, - `geometry_capacity_bottom` and `document_text_bottom` at once, and a - blanket rewrite moves the status chrome along with the document while - still satisfying an "everything moved" assertion — hence A2B-4's - contrast form. -- Verification on the merge result: `cargo fmt --check` clean; strict +- **Stage 2A verification on its merge result:** `cargo fmt --check` clean; strict workspace Clippy clean; **1,832 default + 2,015 CRDT** library tests; `bottom_panel_stage2a_acceptance` **17**; bottom-panel Stage 1 46; statusline segments 8 CRDT; m11_5 semantic 2 CRDT; GPU initial target @@ -442,26 +497,31 @@ has not started.** `docs/agent-handoff.md` §1; the two round lessons are in §5. - Landed-docs follow-up merged as **#156** (`main` @ `d152120`, 2026-07-25). -- **Stage 2 framing: `docs/bottom-panel-stage2-framing.md` revision 4**, - on branch `githubsucks/bottom-panel-stage2-framing` (three commits, - one per revision), worktree `../pmacs-bp-stage2`, based on - `githubsucks/main` @ `ccf29e3`. Round 1 closed 2 blocking + 3 high; +- **Stage 2 framing: `docs/bottom-panel-stage2-framing.md` revision 5** + is commit `56301ed` on branch `githubsucks/bottom-panel-stage2b`, + worktree `../pmacs-bp-stage2b`. Revisions 1–4 remain on + `githubsucks/bottom-panel-stage2-framing` (head `4fbd47f`, four + framing commits, revision 4 at `49757e5`). Round 1 closed 2 blocking + + 3 high; round 2 closed 1 blocking + 2 high + 1 medium and decided both open items; round 3 closed 1 blocking + 1 high + 1 medium. No open items - remain. The approved + remain. Revision 5 adds no decision; it records the approved + 2B-1/2B-2/2B-3 implementation split. The parent framing `docs/bottom-panel-framing.md` (rev 4) remains authoritative, **including its acceptance criteria 37–55**. - Retained, carrying nothing unmerged: branch `bottom-panel` and worktree `../pmacs-bottom-panel`. -- **Stage 2 ships as two serial slices**, 2A landing before 2B branches: +- **Stage 2 ships as four serial implementation slices**, each landing + before the next branches: **2A** = classified §1.3 census routing + `paint_frame` per-window painter extraction (with the active-window auto-scroll preparation), no - protocol change; **2B** = protocol **v21** + protocol change; **2B-1** = protocol **v21** (`InstanceMessage::PanelFrame` plus `FrontendEvent::{FrontendCellGeometry, PanelResizeRows, PanelPointer}`, gated both directions, each extended enum byte-pinned on its own - previous final variant), daemon panel projection, the GPU band, and the - negotiated `panel_capable` flip. Stage 3 is the adopter default flip. + previous final variant); **2B-2** = daemon panel projection and epoch + machine; **2B-3** = the GPU band and negotiated `panel_capable` flip. + Stage 3 is the adopter default flip. - **Correction — this entry previously mis-stated the census contract.** It is **not** "route every consumer through `primary_document_window`". Q#BP14 classifies the 23 reads into four classes and routes only the diff --git a/docs/agent-handoff.md b/docs/agent-handoff.md index 6607df6..47f7dce 100644 --- a/docs/agent-handoff.md +++ b/docs/agent-handoff.md @@ -1,6 +1,9 @@ # Agent handoff — cross-machine continuity -**Last updated: 2026-07-26, after terminal copy mode (#178) — `C-c C-t` +**Last updated: 2026-07-28, after Journey Stage 1a (#182), which made +directory startup one coherent local/daemon/GPU path and incorporated +the terminal configuration + copy mode landed-doc work (#180); following +terminal copy mode (#178) — `C-c C-t` materializes a terminal's whole retained range into an ordinary buffer, plus `Buffer::set_generated_contents`, the first genuinely immutable generated-buffer write path — and its landed-doc pair (#168); following @@ -39,10 +42,12 @@ reads it the way you just did. For volatile branches, checkpoints, verification, and recovery commands, read `docs/active-work.md` immediately after this file. -## 1. Where the project stands (2026-07-26) +## 1. Where the project stands (2026-07-28) -- `main` @ `42025e4` (Lean 4 Stage 4b #181, atop the dired Stage 1 - landed docs #169 and the PTY-terminate diagnostic #176, terminal copy +- `main` @ `c2d56ff` (Journey Stage 1a #182, incorporating terminal + configuration + copy mode landed docs #180, atop Lean 4 Stage 4b #181, + the dired Stage 1 landed docs #169 and the PTY-terminate diagnostic + #176, terminal copy mode #178, the GPU-terminal-input landed docs #168, Lean 4 Stage 4a #179, bottom-panel Stage 2A #177, the bottom-panel Stage 2 framing #175, terminal configuration diff --git a/pmacs-gpu/src/main.rs b/pmacs-gpu/src/main.rs index a26f340..86132fc 100644 --- a/pmacs-gpu/src/main.rs +++ b/pmacs-gpu/src/main.rs @@ -936,10 +936,20 @@ fn run_headless_managed_probe( } }; let mut client = managed.client; + let initial_message = client.take_initial_message(); let initial_target_ready = matches!( - client.take_initial_message(), + initial_message.as_ref(), Some(InstanceMessage::BufferSnapshot { .. }) ); + let mut buffer_facts = ManagedProbeBufferFacts::default(); + if let Some(message) = initial_message.as_ref() + && let Err(error) = buffer_facts.observe(message) + { + let contents = format!("phase=error\nerror={error}\n"); + let _ = write_probe_report(report, &contents); + eprintln!("pmacs-gpu managed probe: {error}"); + return 7; + } let daemon = managed.daemon; let protocol = client.server_protocol_version(); @@ -961,8 +971,14 @@ fn run_headless_managed_probe( let mut last_wait_result = None; let mut last_disconnect = String::new(); if ready - && let Err(error) = - write_managed_probe_report(report, "ready", protocol, &daemon, &disconnect) + && let Err(error) = write_managed_probe_report( + report, + "ready", + protocol, + &daemon, + &buffer_facts, + &disconnect, + ) { eprintln!( "pmacs-gpu managed probe: writing {} failed: {error}", @@ -976,11 +992,23 @@ fn run_headless_managed_probe( } match event_rx.recv_timeout(Duration::from_millis(50)) { Ok(AttachEvent::Message(message)) => { - if matches!(*message, InstanceMessage::BufferSnapshot { .. }) && !ready { + let is_snapshot = matches!(*message, InstanceMessage::BufferSnapshot { .. }); + if let Err(error) = buffer_facts.observe(&message) { + let contents = format!("phase=error\nerror={error}\n"); + let _ = write_probe_report(report, &contents); + eprintln!("pmacs-gpu managed probe: {error}"); + return 7; + } + if is_snapshot { ready = true; - if let Err(error) = - write_managed_probe_report(report, "ready", protocol, &daemon, &disconnect) - { + if let Err(error) = write_managed_probe_report( + report, + "ready", + protocol, + &daemon, + &buffer_facts, + &disconnect, + ) { eprintln!( "pmacs-gpu managed probe: writing {} failed: {error}", report.display() @@ -1006,9 +1034,14 @@ fn run_headless_managed_probe( || wait_result != last_wait_result || disconnect != last_disconnect) { - if let Err(error) = - write_managed_probe_report(report, "ready", protocol, &daemon, &disconnect) - { + if let Err(error) = write_managed_probe_report( + report, + "ready", + protocol, + &daemon, + &buffer_facts, + &disconnect, + ) { eprintln!( "pmacs-gpu managed probe: writing {} failed: {error}", report.display() @@ -1021,9 +1054,14 @@ fn run_headless_managed_probe( } if ready && stdin_closed { - if let Err(error) = - write_managed_probe_report(report, "complete", protocol, &daemon, &disconnect) - { + if let Err(error) = write_managed_probe_report( + report, + "complete", + protocol, + &daemon, + &buffer_facts, + &disconnect, + ) { eprintln!( "pmacs-gpu managed probe: writing {} failed: {error}", report.display() @@ -1043,11 +1081,42 @@ fn run_headless_managed_probe( } } +#[derive(Default)] +struct ManagedProbeBufferFacts { + snapshots: u32, + last_snapshot_text: String, +} + +impl ManagedProbeBufferFacts { + fn observe(&mut self, message: &InstanceMessage) -> Result<(), String> { + let InstanceMessage::BufferSnapshot { crdt_snapshot, .. } = message else { + return Ok(()); + }; + let doc = loro::LoroDoc::new(); + doc.import(crdt_snapshot) + .map_err(|error| format!("BufferSnapshot import failed: {error:?}"))?; + self.snapshots += 1; + self.last_snapshot_text = doc.get_text(LORO_TEXT_CONTAINER).to_string(); + Ok(()) + } +} + +fn hex_bytes(bytes: &[u8]) -> String { + use std::fmt::Write as _; + + let mut encoded = String::with_capacity(bytes.len() * 2); + for byte in bytes { + let _ = write!(encoded, "{byte:02x}"); + } + encoded +} + fn write_managed_probe_report( report: &Path, phase: &str, protocol: u32, daemon: &attach::ManagedDaemonFacts, + buffer_facts: &ManagedProbeBufferFacts, disconnect: &str, ) -> std::io::Result<()> { use std::fmt::Write as _; @@ -1056,6 +1125,12 @@ fn write_managed_probe_report( let _ = writeln!(out, "phase={phase}"); let _ = writeln!(out, "server_protocol_version={protocol}"); let _ = writeln!(out, "buffer_snapshot=true"); + let _ = writeln!(out, "buffer_snapshots={}", buffer_facts.snapshots); + let _ = writeln!( + out, + "last_snapshot_hex={}", + hex_bytes(buffer_facts.last_snapshot_text.as_bytes()) + ); let _ = writeln!(out, "spawned_daemon={}", daemon.spawned_daemon()); let _ = writeln!( out, diff --git a/tests/gpu_invocation_acceptance.rs b/tests/gpu_invocation_acceptance.rs index 5371438..b4e3102 100644 --- a/tests/gpu_invocation_acceptance.rs +++ b/tests/gpu_invocation_acceptance.rs @@ -121,6 +121,19 @@ mod crdt { .collect() } + fn decode_hex(encoded: &str) -> String { + assert_eq!(encoded.len() % 2, 0, "hex payload must have even length"); + let bytes = encoded + .as_bytes() + .chunks_exact(2) + .map(|pair| { + let pair = std::str::from_utf8(pair).expect("hex pair is UTF-8"); + u8::from_str_radix(pair, 16).expect("decode hex pair") + }) + .collect::>(); + String::from_utf8(bytes).expect("snapshot text is UTF-8") + } + fn wait_for_fact( report: &Path, key: &str, @@ -356,6 +369,16 @@ mod crdt { } impl ManagedProbe { + fn from_child(mut child: Child, report: &Path) -> Self { + let stdin = child.stdin.take().expect("probe stdin"); + Self { + child, + stdin: Some(stdin), + report: report.to_owned(), + daemon_pid: None, + } + } + fn spawn(socket: &Path, report: &Path, daemon_executable: &Path, home: &Path) -> Self { Self::spawn_with_env(socket, report, daemon_executable, home, &[]) } @@ -418,18 +441,11 @@ mod crdt { for (key, value) in envs { command.env(key, value); } - let mut child = command.spawn().expect("spawn managed probe"); - let stdin = child.stdin.take().expect("probe stdin"); - Self { - child, - stdin: Some(stdin), - report: report.to_owned(), - daemon_pid: None, - } + Self::from_child(command.spawn().expect("spawn managed probe"), report) } - fn wait_ready(&mut self) -> HashMap { - let facts = wait_for_fact(&self.report, "phase", "ready", Duration::from_secs(10)); + fn wait_for(&mut self, key: &str, expected: &str) -> HashMap { + let facts = wait_for_fact(&self.report, key, expected, Duration::from_secs(10)); if facts .get("spawned_daemon") .is_some_and(|value| value == "true") @@ -439,6 +455,10 @@ mod crdt { facts } + fn wait_ready(&mut self) -> HashMap { + self.wait_for("phase", "ready") + } + fn close(mut self) -> std::process::ExitStatus { self.stdin.take(); wait_for_fact(&self.report, "phase", "complete", Duration::from_secs(5)); @@ -662,6 +682,75 @@ mod crdt { assert!(probe.close().success()); } + #[test] + fn public_gpu_directory_target_reaches_dired_and_leaves_the_daemon_usable() { + let temp = secure_tempdir(); + let socket = temp.path().join("directory-target.sock"); + let report = temp.path().join("directory-target-report"); + let wrapper = temp.path().join("headless-gpu"); + let listed_name = "listed-before-bootstrap.txt"; + fs::write(temp.path().join(listed_name), "listed\n").expect("write listed file"); + write_script( + &wrapper, + "test \"$1\" = \"--managed-attach\"\n\ + socket=$2\n\ + daemon=$3\n\ + shift 3\n\ + exec \"$PMACS_REAL_GPU\" --headless-managed-probe \ + \"$socket\" \"$PMACS_TEST_REPORT\" \"$daemon\" \"$@\"", + ); + + let mut command = Command::new(pmacs_binary()); + command + .args(["--gpu", "--socket"]) + .arg(&socket) + .arg(".") + .current_dir(temp.path()) + .env(TEST_GPU_OVERRIDE, &wrapper) + .env("PMACS_REAL_GPU", gpu_binary()) + .env("PMACS_TEST_REPORT", &report) + .env("HOME", temp.path()) + .env("XDG_CONFIG_HOME", temp.path()) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + let mut directory = + ManagedProbe::from_child(command.spawn().expect("spawn public GPU command"), &report); + + // The public root broker and real managed GPU connector must stay + // alive through Journey N2's asynchronous dired commit. Snapshot + // one is the deliberately pre-existing bootstrap document; snapshot + // two is the post-quiescence directory surface. + // Capture the spawned daemon's PID from the ready report first so + // ManagedProbe::drop can terminate it if snapshot two never arrives. + let ready = directory.wait_ready(); + assert_eq!( + ready.get("spawned_daemon").map(String::as_str), + Some("true") + ); + let facts = directory.wait_for("buffer_snapshots", "2"); + let listing = decode_hex(&facts["last_snapshot_hex"]); + let canonical = fs::canonicalize(temp.path()).expect("canonical directory"); + let mut lines = listing.lines(); + let expected_header = format!("{}:", canonical.display()); + assert_eq!( + lines.next(), + Some(expected_header.as_str()), + "the replacement snapshot must be dired's directory surface:\n{listing}" + ); + assert!( + lines.any(|line| line.trim_end().ends_with(listed_name)), + "dired must list the file that existed before bootstrap:\n{listing}" + ); + + fs::write(temp.path().join("still-alive.txt"), "alive\n").expect("write survivor"); + let survivor = attach_target(&socket, temp.path(), Path::new("still-alive.txt")); + assert_eq!(survivor.replica.materialize_string(), "alive\n"); + + drop(survivor); + assert!(directory.close().success()); + } + #[test] fn malformed_or_unloadable_targets_fail_closed_without_poisoning_the_daemon() { let temp = secure_tempdir(); @@ -673,7 +762,6 @@ mod crdt { (cwd.clone(), Vec::new()), (cwd.clone(), b"bad\0name".to_vec()), (cwd.clone(), vec![b'x'; 32 * 1024 + 1]), - (cwd.clone(), b".".to_vec()), ]; for (index, (bad_cwd, bad_path)) in invalid.into_iter().enumerate() { let (frontend_id, mut stream, messages) = open_raw_target(&socket, bad_cwd, bad_path);