Skip to content

Commit a5b5a7d

Browse files
committed
refactor(daemon): drop the vestigial close-time rollback, add router-level #1391 coverage
Review feedback on #1392 (thymikee), P2 items: - The close-time save-script failure's session.actions rollback (finalizeOrdinaryCloseScript) was left over from an earlier design where a failed save could keep the session alive for retry. It never does now — runCloseTeardownAndRelease always tears the session down regardless of the outcome — so there is no surviving session for a later write to duplicate the close action on. Drop the rollback; the durable events.ndjson entry (which the rollback never touched anyway) and the in-memory action now agree, both accurately recording that the close happened. - Add a request-router-level regression (request-router-typed-error.test.ts, alongside the existing repair-close BLOCKER 2 test it mirrors) proving the normalized JSON error shape a real client sees: top-level retriable:false, details.reason/path preserved, and the session torn down — not just that handleCloseCommand throws the right AppError when called directly.
1 parent 85a6c4f commit a5b5a7d

2 files changed

Lines changed: 51 additions & 7 deletions

File tree

src/daemon/__tests__/request-router-typed-error.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { test, expect, vi, beforeEach } from 'vitest';
2+
import fs from 'node:fs';
23
import os from 'node:os';
34
import path from 'node:path';
45

@@ -167,3 +168,44 @@ test('BLOCKER 2 (second follow-up): a repair-close platform-close failure surfac
167168
// The session is retained (not torn down), addressable for the retry.
168169
expect(sessionStore.get('typed-error')).toBeDefined();
169170
});
171+
172+
// #1391 P2: the handler-level test (`session-device-claims.test.ts`) proves
173+
// `toOrdinaryCloseSaveScriptFailure` builds the right `AppError`, but calls
174+
// `handleCloseCommand` directly — it never exercises `normalizeError`/
175+
// `enrichDaemonError`'s own hoisting of `details.retriable` to the TOP-level
176+
// wire field, the same layer BLOCKER 2's test above exists to catch a
177+
// regression in. Exercised through the REAL router boundary so a regression
178+
// in either layer is caught, unlike the direct-call test.
179+
test('#1391: an ordinary close-time script-save failure surfaces details.reason/path and retriable:false through the router, and the session is torn down', async () => {
180+
const { sessionStore, handler } = makeHandler();
181+
const session = makeIosSession('typed-error');
182+
session.recordSession = true;
183+
const targetPath = path.join(
184+
os.tmpdir(),
185+
`agent-device-router-typed-error-${Date.now()}-${Math.random().toString(36).slice(2)}.ad`,
186+
);
187+
fs.writeFileSync(targetPath, 'pre-existing\n');
188+
session.saveScriptPath = targetPath;
189+
sessionStore.set('typed-error', session);
190+
191+
try {
192+
// Untargeted close: no positionals, so no platform close is dispatched
193+
// (`shouldDispatchPlatformClose`) — isolates the script-save failure from
194+
// any platform-close error, matching BLOCKER 2's own targeted-vs-untargeted
195+
// distinction above.
196+
const response = await handler(request('close'));
197+
198+
expect(response.ok).toBe(false);
199+
if (response.ok) return;
200+
expect(response.error.code).toBe('COMMAND_FAILED');
201+
expect(response.error.retriable).toBe(false);
202+
expect(response.error.details?.reason).toBe('script_target_exists');
203+
expect(response.error.details?.path).toBe(targetPath);
204+
// Unlike the repair-armed case above, an ordinary session's teardown
205+
// never withholds on a failed script save — it is always torn down.
206+
expect(sessionStore.get('typed-error')).toBeUndefined();
207+
expect(mockDispatch).not.toHaveBeenCalled();
208+
} finally {
209+
fs.rmSync(targetPath, { force: true });
210+
}
211+
});

src/daemon/handlers/session-close.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -251,11 +251,15 @@ async function runSessionCloseTeardown(params: {
251251
* call site's own comment for why a repair-armed session skips this entirely.
252252
*
253253
* #1391: the write can refuse to publish (no-clobber target-exists, or any
254-
* other fs `AppError`). Roll back the just-recorded `close` action on failure
255-
* — mirroring `commitRepairBeforeClose`'s own rollback — so neither the
256-
* session/device claim leaks (item 1) nor an unrecorded `close` survives for a
257-
* later successful write to duplicate (item 2); the caller still completes
258-
* teardown regardless of the outcome returned here.
254+
* other fs `AppError`). Unlike a repair-armed commit failure — which keeps
255+
* its session alive so `commitRepairBeforeClose` must roll back its
256+
* just-recorded `close` action to keep a later retry from duplicating it —
257+
* this caller (`runSessionCloseTeardown`) always completes teardown and
258+
* deletes the session regardless of the outcome returned here, so there is
259+
* no surviving session for a retry to duplicate anything on. No rollback:
260+
* the recorded `close` action (and its durable `events.ndjson` entry) stay
261+
* exactly as they are — an accurate record that the close itself happened,
262+
* independent of whether the script also got saved.
259263
*/
260264
function finalizeOrdinaryCloseScript(params: {
261265
req: DaemonRequest;
@@ -264,7 +268,6 @@ function finalizeOrdinaryCloseScript(params: {
264268
platformCloseError: unknown;
265269
}): AppError | undefined {
266270
const { req, session, sessionStore, platformCloseError } = params;
267-
const actionsBeforeClose = session.actions.length;
268271
if (!platformCloseError) {
269272
recordSessionAction(sessionStore, session, req, 'close', {
270273
session: session.name,
@@ -278,7 +281,6 @@ function finalizeOrdinaryCloseScript(params: {
278281
sessionStore.writeSessionLog(session, { force: resolveEffectiveSaveScriptForce(req, session) });
279282
return undefined;
280283
} catch (error) {
281-
session.actions.length = actionsBeforeClose;
282284
return toOrdinaryCloseSaveScriptFailure(error);
283285
}
284286
}

0 commit comments

Comments
 (0)