feat(wrapper): spawn daemon on Linux when hibernated
On non-darwin platforms, the wrapper now attempts to spawn the daemon via 'iai-mcp daemon start' (which uses systemctl --user start) when the daemon socket is unreachable. This allows the daemon to wake from hibernation automatically instead of only writing wake.signal as a passive marker. If spawn fails (binary not found, systemd unavailable), falls back to writing wake.signal for future daemon boots.
This commit is contained in:
parent
a31bbd7f58
commit
7173a446a6
2 changed files with 89 additions and 13 deletions
|
|
@ -1,15 +1,17 @@
|
|||
// Phase 10.5 — tests for `WrapperLifecycle`.
|
||||
//
|
||||
// Eight-test matrix from CONTEXT 10.5:
|
||||
// Nine-test matrix from CONTEXT 10.5 + L5 non-darwin daemon spawn:
|
||||
//
|
||||
// 1. ensureDaemonAlive: socket reachable -> NO subprocess invoked.
|
||||
// 2. ensureDaemonAlive: socket unreachable + darwin -> kickstart called.
|
||||
// 3. ensureDaemonAlive: kickstart throws -> falls back to wake.signal.
|
||||
// 4. ensureDaemonAlive: non-macos -> wake.signal written, no subprocess.
|
||||
// 5. registerHeartbeat: file exists with correct schema.
|
||||
// 6. heartbeat refresh: small interval -> last_refresh updates.
|
||||
// 7. cleanupHeartbeat: file gone, timer cleared.
|
||||
// 8. security: source has no `shell: true` and no shell-interpreting
|
||||
// 4. ensureDaemonAlive: non-macos -> spawnDaemon called, falls back to
|
||||
// wake.signal on spawn failure.
|
||||
// 5. ensureDaemonAlive: non-macos -> successful spawn skips wake.signal.
|
||||
// 6. registerHeartbeat: file exists with correct schema.
|
||||
// 7. heartbeat refresh: small interval -> last_refresh updates.
|
||||
// 8. cleanupHeartbeat: file gone, timer cleared.
|
||||
// 9. security: source has no `shell: true` and no shell-interpreting
|
||||
// subprocess variant in mcp-wrapper/src/.
|
||||
//
|
||||
// Test runner: Node's built-in `node:test` (zero new dep — Node 22 has
|
||||
|
|
@ -126,10 +128,10 @@ describe("WrapperLifecycle.ensureDaemonAlive", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("on non-macos writes wake.signal and never spawns subprocess", async () => {
|
||||
it("on non-macos spawns daemon, falls back to wake.signal on failure", async () => {
|
||||
const tmp = await makeTmp("linux");
|
||||
try {
|
||||
let kickstarts = 0;
|
||||
let daemonSpawns = 0;
|
||||
const lifecycle = new WrapperLifecycle({
|
||||
socketPath: join(tmp, "daemon.sock"),
|
||||
wakeSignalPath: join(tmp, "wake.signal"),
|
||||
|
|
@ -137,13 +139,43 @@ describe("WrapperLifecycle.ensureDaemonAlive", () => {
|
|||
platform: "linux",
|
||||
socketReachable: async () => false,
|
||||
spawnKickstart: async () => {
|
||||
kickstarts += 1;
|
||||
throw new Error("should not be called on non-darwin");
|
||||
},
|
||||
spawnDaemon: async () => {
|
||||
daemonSpawns += 1;
|
||||
throw new Error("daemon spawn simulated failure");
|
||||
},
|
||||
});
|
||||
await lifecycle.ensureDaemonAlive();
|
||||
assert.equal(kickstarts, 0, "subprocess must never be invoked on non-darwin");
|
||||
assert.equal(daemonSpawns, 1, "spawnDaemon must be invoked exactly once on non-darwin");
|
||||
const sigStat = await stat(join(tmp, "wake.signal"));
|
||||
assert.ok(sigStat.isFile(), "wake.signal must exist on non-darwin path");
|
||||
assert.ok(sigStat.isFile(), "wake.signal must exist after spawn failure");
|
||||
} finally {
|
||||
await cleanupTmp(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
it("on non-macos: successful spawn skips wake.signal", async () => {
|
||||
const tmp = await makeTmp("linux-ok");
|
||||
try {
|
||||
const lifecycle = new WrapperLifecycle({
|
||||
socketPath: join(tmp, "daemon.sock"),
|
||||
wakeSignalPath: join(tmp, "wake.signal"),
|
||||
heartbeatPath: join(tmp, "wrappers", "heartbeat-1-x.json"),
|
||||
platform: "linux",
|
||||
socketReachable: async () => false,
|
||||
spawnKickstart: async () => {
|
||||
throw new Error("should not be called on non-darwin");
|
||||
},
|
||||
spawnDaemon: async () => {
|
||||
// Success — no-op in test
|
||||
},
|
||||
});
|
||||
await lifecycle.ensureDaemonAlive();
|
||||
await assert.rejects(
|
||||
stat(join(tmp, "wake.signal")),
|
||||
"wake.signal must NOT be written when spawn succeeds",
|
||||
);
|
||||
} finally {
|
||||
await cleanupTmp(tmp);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue