Skip to content

Commit be0e577

Browse files
committed
test: trace SessionEnd filesystem failures
1 parent ff5bdc4 commit be0e577

2 files changed

Lines changed: 19 additions & 5 deletions

File tree

tests/fixtures/record-socket-methods.mjs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// @ts-nocheck
2-
import { appendFileSync } from 'node:fs';
2+
import fs, { appendFileSync } from 'node:fs';
3+
import { syncBuiltinESMExports } from 'node:module';
34
import net from 'node:net';
45

56
const recordPath = process.env.ZCODE_TEST_SOCKET_METHOD_RECORD;
@@ -13,3 +14,15 @@ if (recordPath) {
1314
return write.call(this, chunk, ...args);
1415
};
1516
}
17+
18+
const fsRecordPath = process.env.ZCODE_TEST_FS_ERROR_RECORD;
19+
if (fsRecordPath) {
20+
for (const method of ['chmod', 'lstat', 'mkdir', 'open', 'opendir', 'readFile', 'realpath', 'rename', 'unlink']) {
21+
const operation = fs.promises[method];
22+
fs.promises[method] = async function instrumentedFileOperation(...args) {
23+
try { return await operation.apply(this, args); }
24+
catch (error) { appendFileSync(fsRecordPath, `${method}:${error?.code ?? 'UNKNOWN'}\n`); throw error; }
25+
};
26+
}
27+
syncBuiltinESMExports();
28+
}

tests/hooks.test.mjs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ test('SessionEnd removes only its session contexts and leaves sibling jobs/sessi
279279
});
280280

281281
test('SessionEnd releases only its broker owner sessions and lets the idle broker exit', async () => {
282-
const { cwd, data, env } = await workspace(); const record = join(data, 'zcode-calls.jsonl'); const socketMethods = join(data, 'hook-socket-methods.txt'); await writeFile(record, ''); await writeFile(socketMethods, '');
282+
const { cwd, data, env } = await workspace(); const record = join(data, 'zcode-calls.jsonl'); const socketMethods = join(data, 'hook-socket-methods.txt'); const fsErrors = join(data, 'hook-fs-errors.txt'); await writeFile(record, ''); await writeFile(socketMethods, ''); await writeFile(fsErrors, '');
283283
const launch = { command: process.execPath, args: [fakeZCode], target: fakeZCode }; const clients = [];
284284
for (const sessionId of ['a', 'b']) {
285285
await runHook('session-lifecycle-hook.mjs', { session_id: sessionId, cwd, hook_event_name: 'SessionStart', transcript_path: null, model: 'gpt', permission_mode: 'default', source: 'startup' }, env);
@@ -288,16 +288,17 @@ test('SessionEnd releases only its broker owner sessions and lets the idle broke
288288
}
289289
for (const client of clients) await client.close();
290290
const storage = await resolveWorkspaceStorage({ dataRoot: data, workspace: cwd }); const identity = JSON.parse(await readFile(join(storage.directory, 'broker/identity.json'), 'utf8')); const ownershipPath = join(storage.directory, 'broker/session-owners.json'); const ownershipBefore = await stat(ownershipPath); const hookStartedAt = Date.now();
291-
const ended = await runHook('session-end-hook.mjs', { session_id: 'a', cwd, hook_event_name: 'SessionEnd', transcript_path: null, reason: 'other' }, { ...env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --import=${socketMethodRecorder}`.trim(), ZCODE_TEST_SOCKET_METHOD_RECORD: socketMethods }); const hookElapsedMs = Date.now() - hookStartedAt;
291+
const ended = await runHook('session-end-hook.mjs', { session_id: 'a', cwd, hook_event_name: 'SessionEnd', transcript_path: null, reason: 'other' }, { ...env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --import=${socketMethodRecorder}`.trim(), ZCODE_TEST_SOCKET_METHOD_RECORD: socketMethods, ZCODE_TEST_FS_ERROR_RECORD: fsErrors }); const hookElapsedMs = Date.now() - hookStartedAt;
292292
assert.equal(ended.code, 0);
293293
const owners = JSON.parse(await readFile(ownershipPath, 'utf8'));
294294
let releaseDiagnostic = 'owner release succeeded without diagnostic collection';
295295
if (JSON.stringify(owners.sessions) !== JSON.stringify({ 'zcode-b': ownerIdForSession('b') })) {
296-
const callsAtFailure = (await readFile(record, 'utf8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line)); const ownershipAfter = await stat(ownershipPath); const healthyAfterFailure = await probeBrokerHealth(identity, 250); const childPidProbe = await probePidFromChild(identity.pid); let ownedJobsProbe;
296+
const callsAtFailure = (await readFile(record, 'utf8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line)); const ownershipAfter = await stat(ownershipPath); const healthyAfterFailure = await probeBrokerHealth(identity, 250); const childPidProbe = await probePidFromChild(identity.pid); let ownedJobsProbe; let markerBeforeRetry;
297+
try { markerBeforeRetry = { value: JSON.parse(await readFile(join(storage.directory, 'job-owners/index.json'), 'utf8')) }; } catch (error) { markerBeforeRetry = { errorCode: error?.code ?? null }; }
297298
try { ownedJobsProbe = { count: (await createStateStore({ dataRoot: data }).listOwnedJobs(cwd, 'a')).length }; } catch (error) { ownedJobsProbe = { error: { code: error?.code ?? null, category: error?.category ?? null, details: error?.details ?? null } }; }
298299
const hookSocketMethods = (await readFile(socketMethods, 'utf8')).trim().split('\n').filter(Boolean); const retrySocketMethodsPath = join(data, 'retry-socket-methods.txt'); await writeFile(retrySocketMethodsPath, '');
299300
const retry = await runHook(ownerReleaseProbe, { dataRoot: data, workspace: cwd, ownerSessionId: 'a', ownerId: ownerIdForSession('a') }, { ...env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --import=${socketMethodRecorder}`.trim(), ZCODE_TEST_SOCKET_METHOD_RECORD: retrySocketMethodsPath }, { absolute: true });
300-
releaseDiagnostic = `release-stage ${JSON.stringify({ hookElapsedMs, stopObserved: callsAtFailure.some((call) => call.method === 'session/stop' && call.params?.sessionId === 'zcode-a'), hookSocketMethods, retrySocketMethods: (await readFile(retrySocketMethodsPath, 'utf8')).trim().split('\n').filter(Boolean), retry: retry.json ?? { code: retry.code, stderr: retry.stderr.trim() || null }, ownerStoreReplaced: ownershipAfter.ino !== ownershipBefore.ino || ownershipAfter.mtimeMs !== ownershipBefore.mtimeMs, healthyAfterFailure, childPidProbe, ownedJobsProbe, hookCode: ended.code, hookDiagnostic: ended.stderr.trim() || null })}`;
301+
releaseDiagnostic = `release-stage ${JSON.stringify({ hookElapsedMs, stopObserved: callsAtFailure.some((call) => call.method === 'session/stop' && call.params?.sessionId === 'zcode-a'), hookSocketMethods, hookFsErrors: (await readFile(fsErrors, 'utf8')).trim().split('\n').filter(Boolean), markerBeforeRetry, retrySocketMethods: (await readFile(retrySocketMethodsPath, 'utf8')).trim().split('\n').filter(Boolean), retry: retry.json ?? { code: retry.code, stderr: retry.stderr.trim() || null }, ownerStoreReplaced: ownershipAfter.ino !== ownershipBefore.ino || ownershipAfter.mtimeMs !== ownershipBefore.mtimeMs, healthyAfterFailure, childPidProbe, ownedJobsProbe, hookCode: ended.code, hookDiagnostic: ended.stderr.trim() || null })}`;
301302
}
302303
assert.deepEqual(owners.sessions, { 'zcode-b': ownerIdForSession('b') }, releaseDiagnostic);
303304
assert.deepEqual((await readFile(socketMethods, 'utf8')).trim().split('\n'), ['broker/auth', 'broker/health', 'broker/releaseOwner']);

0 commit comments

Comments
 (0)