fix(desktop): pass-2 review corrections (round 3) — captured respawn, behavioral tests, residue

Global-config respawn: replace live-scope wrappers with one transition epoch.
restart_under_captured_epoch acquires managed_agent_runtime_transition +
managed_agents_store_lock, validates captured generation, stops, builds a
captured spawn context (records/personas/global config/owner hex/scope_id
from captured_scope), spawns/registers, saves — no live wrapper past the stop.
persist_last_error takes captured_scope, validates generation under its own
acquired store lock, never called while that lock is held, fails closed on
poison. Two tauri::test::mock_app() tests: fresh-scope no-runtime → Skipped
(proves generation guard passes + path proceeds), stale-scope → Skipped at
generation step (switch-between-stop-and-spawn test, calls production fn).

Behavioral tests on production paths: tauri = { version = "2", features = ["test"] }
added to dev-dependencies. compensate_drain_for extracted with injectable
start_fn; tests call it directly (compensate_drain_for is the production core).
capture_agent_snapshot_import_entry and capture_team_snapshot_import_entry
extracted as testable entry guards; tests call the real production functions
(no-scope reject, owner-mismatch reject, matching-owner passes + relay verified,
stale-generation rejects validate_scope_generation). registerNestNotifications
extracted from useNestNotifications; test imports and calls the real function,
asserts all three event registrations, workspace-degraded toast payload,
unlisten cleanup per event. fail_if_client_mesh_active and mesh_stop_client
tested via tauri::test::mock_builder() with real AppHandle (no-runtime,
client-runtime, no-runtime stop paths). Generalized fail_if_client_mesh_active
and mesh_stop_client to tauri::Runtime to allow mock_app usage.

Residue: _compensation_gate_removed placeholder deleted from AppState.
identity.rs:346-348 comment corrected (guard passed by value, not dropped).
mesh_llm_scope.rs:59-64 stale re-arm comment replaced with accurate Option A note.
Duplicate generation bump at identity.rs:544 removed (clear_active_scope
calls next_scope_generation internally; no second bump needed).

File-size gate: AgentSnapshotImportEntry + capture_agent_snapshot_import_entry
extracted to import_entry.rs (import.rs: 995 lines); TeamSnapshotImportEntry +
capture_team_snapshot_import_entry extracted to team_snapshot_entry.rs
(team_snapshot.rs: 997 lines). Both within the 1000-line ratchet.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
2026-08-04 01:57:40 -04:00
co-authored by Will Pfleger
parent 6dc4e29155
commit ff16a80b5b
30 changed files with 1699 additions and 926 deletions
@@ -1,126 +1,139 @@
/**
* Behavioral tests for useNestNotifications.
* Behavioral tests for useNestNotifications / registerNestNotifications.
*
* Verifies that:
* - The hook registers a listener for `workspace-degraded` on mount.
* - When the event fires, `toast.error` is called with the payload.
* - The listener is cleaned up (unlisten called) on unmount.
* Tests call `registerNestNotifications` — the extracted production
* registration helper used by `useNestNotifications` inside its `useEffect`.
* This is the real production function, not a reconstruction of its logic.
*
* Uses a module-scope mock for @tauri-apps/api/event and sonner so we can
* drive events synchronously without a real Tauri runtime.
* Proves:
* - `registerNestNotifications` registers listeners for all three event names
* (repos-dir-error, legacy-nest-migrated, workspace-degraded) by inspecting
* the `listenFn` call record.
* - When `workspace-degraded` fires, `toast.error` is called with the payload.
* - The returned cleanup function calls every unlisten function.
* - The event name and payload wiring survive rename/refactor of the production
* code (the test would fail if the event name or toast call were deleted).
*/
import assert from "node:assert/strict";
import test from "node:test";
// ── Minimal React/hook harness ───────────────────────────────────────────────
// useNestNotifications is a React hook (calls useEffect). We run it by
// providing a minimal shim that invokes the cleanup function directly, making
// the test synchronous and environment-free.
const effectCallbacks = [];
const effectCleanups = [];
globalThis.React = {
useEffect: (cb) => {
const cleanup = cb();
effectCallbacks.push(cb);
effectCleanups.push(cleanup);
},
};
import { registerNestNotifications } from "./useNestNotifications.ts";
// ── Mocks for @tauri-apps/api/event and sonner ──────────────────────────────
let registeredListeners = [];
const toastCalls = [];
test("registerNestNotifications: registers listeners for all three events", () => {
const registeredEvents = [];
const unlistenFns = [];
globalThis.__mockListen = (event, handler) => {
registeredListeners.push({ event, handler });
// Return a promise that resolves to the unlisten function (matches Tauri API).
return Promise.resolve(() => {
registeredListeners = registeredListeners.filter(
(l) => l.event !== event || l.handler !== handler,
);
});
};
globalThis.__mockToastError = (title, options) => {
toastCalls.push({ title, options });
};
// ── Module stubs are injected before import via globalThis ──────────────────
// The hook imports "@tauri-apps/api/event" { listen } and "sonner" { toast }.
// We inject module-resolution stubs via the TypeScript transpiler shim.
// Since the test runner resolves bare imports, we patch globalThis so the TS
// transpilation sees them.
//
// Instead of running the hook (which requires a full Node ESM module mock),
// we exercise the behavioral contract directly: a captured `listen` call with
// event="workspace-degraded" whose handler calls toast.error.
test("workspace-degraded listener calls toast.error with payload", () => {
const toastErrorCalls = [];
const unlistenCalls = [];
// Construct the exact handler the hook registers, extracted here for direct
// verification. This mirrors the hook's implementation and verifies the
// behavior described in the work order.
//
// Real contract: listen("workspace-degraded", (event) => toast.error(...))
const fakeToast = {
error: (title, opts) => toastErrorCalls.push({ title, opts }),
const mockListen = (event, _handler) => {
registeredEvents.push(event);
const unlistenFn = () => unlistenFns.push(event);
return Promise.resolve(unlistenFn);
};
const _fakeUnlisten = () => unlistenCalls.push("workspace-degraded");
// Simulate the handler registration and invocation.
const handler = (event) => {
fakeToast.error("Workspace partially degraded", {
description: event.payload,
const mockToast = { error: () => {}, success: () => {} };
const cleanup = registerNestNotifications(mockListen, mockToast);
assert.deepEqual(
registeredEvents.sort(),
["legacy-nest-migrated", "repos-dir-error", "workspace-degraded"].sort(),
"must register listeners for all three events",
);
// Cleanup calls all three unlisten functions.
return Promise.resolve()
.then(() => {
cleanup();
return new Promise((resolve) => setTimeout(resolve, 0));
})
.then(() => {
assert.equal(
unlistenFns.length,
3,
"cleanup must call unlisten for all three events",
);
});
});
test("registerNestNotifications: workspace-degraded fires toast.error with payload", () => {
const toastCalls = [];
let degradedHandler = null;
const mockListen = (event, handler) => {
if (event === "workspace-degraded") {
degradedHandler = handler;
}
return Promise.resolve(() => {});
};
// Invoke handler as if the event fired with a degradation message.
handler({ payload: "restore failed: agent runtime could not be restarted" });
const mockToast = {
error: (title, opts) => toastCalls.push({ title, opts }),
success: () => {},
};
assert.equal(toastErrorCalls.length, 1, "toast.error must be called once");
assert.equal(toastErrorCalls[0].title, "Workspace partially degraded");
registerNestNotifications(mockListen, mockToast);
assert.ok(degradedHandler, "workspace-degraded handler must be registered");
// Fire the event as the Tauri event system would.
degradedHandler({
payload: "restore failed: agent runtime could not be restarted",
});
assert.equal(toastCalls.length, 1, "toast.error must be called once");
assert.equal(toastCalls[0].title, "Workspace partially degraded");
assert.equal(
toastErrorCalls[0].opts.description,
toastCalls[0].opts.description,
"restore failed: agent runtime could not be restarted",
);
});
test("workspace-degraded listener cleanup unlists on unmount", () => {
// Verify that listen returns a promise whose resolution is the unlisten
// function, and that the hook's cleanup calls it.
let unlistenWasCalled = false;
const unlistenFn = () => {
unlistenWasCalled = true;
test("registerNestNotifications: cleanup calls all unlisten functions", () => {
let unlistenCallCount = 0;
const unlisten = () => {
unlistenCallCount++;
};
const listenPromise = Promise.resolve(unlistenFn);
const mockListen = (_event, _handler) => Promise.resolve(unlisten);
const mockToast = { error: () => {}, success: () => {} };
// Simulate the cleanup path: the hook calls `unlisten.then((fn) => fn())`
void listenPromise.then((fn) => fn());
const cleanup = registerNestNotifications(mockListen, mockToast);
// Flush the microtask queue.
return listenPromise.then(() => {
assert.equal(
unlistenWasCalled,
true,
"unlisten must be called when the hook unmounts",
);
});
// Call cleanup after promises resolve.
return new Promise((resolve) => setTimeout(resolve, 0))
.then(() => {
cleanup();
return new Promise((resolve) => setTimeout(resolve, 0));
})
.then(() => {
assert.equal(
unlistenCallCount,
3,
"cleanup must call unlisten for all three registered listeners",
);
});
});
test("workspace-degraded handler passes raw payload as description", () => {
// Boundary check: empty payload, long payload, special characters.
test("registerNestNotifications: workspace-degraded handler passes raw payload as description", () => {
const cases = ["", "a".repeat(500), "error: file not found\npath: /foo/bar"];
for (const payload of cases) {
const toastArgs = [];
const handler = (event) => {
toastArgs.push({
title: "Workspace partially degraded",
description: event.payload,
});
let handler = null;
const mockListen = (event, h) => {
if (event === "workspace-degraded") handler = h;
return Promise.resolve(() => {});
};
const mockToast = {
error: (title, opts) =>
toastArgs.push({ title, description: opts?.description }),
success: () => {},
};
registerNestNotifications(mockListen, mockToast);
assert.ok(handler, "workspace-degraded handler must be set");
handler({ payload });
assert.equal(toastArgs.length, 1);
assert.equal(toastArgs[0].description, payload);
toastArgs.length = 0;
}
});
@@ -5,53 +5,65 @@ import { toast } from "sonner";
const MIGRATION_TOAST_KEY = "buzz-legacy-nest-migrated-notified";
/**
* Surface nest-related backend events as toasts.
* Register all nest-related backend event listeners.
*
* Extracted for testability: accepts `listenFn` and `toastFn` as parameters
* so unit tests can inject mocks without a Tauri runtime. The production hook
* calls this with the real `listen` and `toast`.
*
* Returns a cleanup function that calls every unlisten function.
*
* Covered events:
* - `repos-dir-error`: a configured `repos_dir` failed to validate or its
* symlink could not be applied (invalid path, downgrade refused, external
* target gone). Emitted by `apply_workspace` on both the validate-reject
* and the runtime symlink-failure paths, so a bad `repos_dir` is always
* visibly surfaced rather than silently logged to console.
* symlink could not be applied.
* - `legacy-nest-migrated`: the agent's knowledge was carried over from a
* legacy `~/.sprout` nest. Shown once per machine (deduped via
* localStorage); the backend re-emits each launch while `~/.sprout` exists,
* which also covers the event being emitted before this listener mounts.
* legacy `~/.sprout` nest. Shown once per machine (deduped via localStorage).
* - `workspace-degraded`: a post-commit restore step failed after the workspace
* switch succeeded. The switch is live; the degradation is recoverable by
* restarting the app or re-applying the workspace. Event-sync dispatch failure
* does not emit this event (shutdown-time error, no toast surface exists).
* switch succeeded. Event-sync dispatch failure does not emit this event
* (shutdown-time error, no toast surface exists).
*/
export function registerNestNotifications(
listenFn: typeof listen,
toastFn: typeof toast,
): () => void {
const unlistenReposError = listenFn<string>("repos-dir-error", (event) => {
toastFn.error("Repos directory not applied", {
description: event.payload,
});
});
const unlistenMigrated = listenFn("legacy-nest-migrated", () => {
if (localStorage.getItem(MIGRATION_TOAST_KEY) === "true") {
return;
}
localStorage.setItem(MIGRATION_TOAST_KEY, "true");
toastFn.success("Migrated notes from ~/.sprout", {
description: "You can delete it to reclaim disk space.",
});
});
const unlistenDegraded = listenFn<string>("workspace-degraded", (event) => {
toastFn.error("Workspace partially degraded", {
description: event.payload,
});
});
return () => {
void unlistenReposError.then((fn) => fn());
void unlistenMigrated.then((fn) => fn());
void unlistenDegraded.then((fn) => fn());
};
}
/**
* Surface nest-related backend events as toasts.
*
* Mounted at the app root ahead of the community-init effect so the listener
* is registered before the first `apply_workspace` call.
*/
export function useNestNotifications(): void {
useEffect(() => {
const unlistenReposError = listen<string>("repos-dir-error", (event) => {
toast.error("Repos directory not applied", {
description: event.payload,
});
});
const unlistenMigrated = listen("legacy-nest-migrated", () => {
if (localStorage.getItem(MIGRATION_TOAST_KEY) === "true") {
return;
}
localStorage.setItem(MIGRATION_TOAST_KEY, "true");
toast.success("Migrated notes from ~/.sprout", {
description: "You can delete it to reclaim disk space.",
});
});
const unlistenDegraded = listen<string>("workspace-degraded", (event) => {
toast.error("Workspace partially degraded", {
description: event.payload,
});
});
return () => {
void unlistenReposError.then((fn) => fn());
void unlistenMigrated.then((fn) => fn());
void unlistenDegraded.then((fn) => fn());
};
const cleanup = registerNestNotifications(listen, toast);
return cleanup;
}, []);
}