Skip to content

Commit 7e9fe8e

Browse files
committed
test: trace SessionEnd broker methods
1 parent 3c1f350 commit 7e9fe8e

2 files changed

Lines changed: 19 additions & 3 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { appendFileSync } from 'node:fs';
2+
import net from 'node:net';
3+
4+
const recordPath = process.env.ZCODE_TEST_SOCKET_METHOD_RECORD;
5+
if (recordPath) {
6+
const write = net.Socket.prototype.write;
7+
net.Socket.prototype.write = function instrumentedWrite(chunk, ...args) {
8+
try {
9+
const frame = JSON.parse(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk));
10+
if (typeof frame?.method === 'string') appendFileSync(recordPath, `${frame.method}\n`);
11+
} catch { /* Only complete JSON-RPC request frames are diagnostic evidence. */ }
12+
return write.call(this, chunk, ...args);
13+
};
14+
}

tests/hooks.test.mjs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { cleanupSession, resolveForwardingExecutor } from '../hooks/lib/hook-sta
1717

1818
const root = fileURLToPath(new URL('../', import.meta.url));
1919
const fakeZCode = join(root, 'tests/fixtures/fake-zcode-cli.mjs');
20+
const socketMethodRecorder = new URL('./fixtures/record-socket-methods.mjs', import.meta.url).href;
2021
const legacyBroker = join(root, 'tests/fixtures/legacy-zcode-broker-v1.mjs');
2122
const ownerStoreLockHolder = join(root, 'tests/fixtures/owner-store-lock-holder.mjs');
2223
// Parallel Windows runners can spend more than 750 ms scheduling a legacy
@@ -277,7 +278,7 @@ test('SessionEnd removes only its session contexts and leaves sibling jobs/sessi
277278
});
278279

279280
test('SessionEnd releases only its broker owner sessions and lets the idle broker exit', async () => {
280-
const { cwd, data, env } = await workspace(); const record = join(data, 'zcode-calls.jsonl'); await writeFile(record, '');
281+
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, '');
281282
const launch = { command: process.execPath, args: [fakeZCode], target: fakeZCode }; const clients = [];
282283
for (const sessionId of ['a', 'b']) {
283284
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);
@@ -286,12 +287,13 @@ test('SessionEnd releases only its broker owner sessions and lets the idle broke
286287
}
287288
for (const client of clients) await client.close();
288289
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();
289-
const ended = await runHook('session-end-hook.mjs', { session_id: 'a', cwd, hook_event_name: 'SessionEnd', transcript_path: null, reason: 'other' }, env); const hookElapsedMs = Date.now() - hookStartedAt;
290+
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;
290291
assert.equal(ended.code, 0);
291292
const owners = JSON.parse(await readFile(ownershipPath, 'utf8'));
292293
let releaseDiagnostic = 'owner release succeeded without diagnostic collection';
293-
if (JSON.stringify(owners.sessions) !== JSON.stringify({ 'zcode-b': ownerIdForSession('b') })) { 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; 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 } }; } releaseDiagnostic = `release-stage ${JSON.stringify({ hookElapsedMs, stopObserved: callsAtFailure.some((call) => call.method === 'session/stop' && call.params?.sessionId === 'zcode-a'), ownerStoreReplaced: ownershipAfter.ino !== ownershipBefore.ino || ownershipAfter.mtimeMs !== ownershipBefore.mtimeMs, healthyAfterFailure, childPidProbe, ownedJobsProbe, hookCode: ended.code, hookDiagnostic: ended.stderr.trim() || null })}`; }
294+
if (JSON.stringify(owners.sessions) !== JSON.stringify({ 'zcode-b': ownerIdForSession('b') })) { 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; 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 } }; } releaseDiagnostic = `release-stage ${JSON.stringify({ hookElapsedMs, stopObserved: callsAtFailure.some((call) => call.method === 'session/stop' && call.params?.sessionId === 'zcode-a'), hookSocketMethods: (await readFile(socketMethods, 'utf8')).trim().split('\n').filter(Boolean), ownerStoreReplaced: ownershipAfter.ino !== ownershipBefore.ino || ownershipAfter.mtimeMs !== ownershipBefore.mtimeMs, healthyAfterFailure, childPidProbe, ownedJobsProbe, hookCode: ended.code, hookDiagnostic: ended.stderr.trim() || null })}`; }
294295
assert.deepEqual(owners.sessions, { 'zcode-b': ownerIdForSession('b') }, releaseDiagnostic);
296+
assert.deepEqual((await readFile(socketMethods, 'utf8')).trim().split('\n'), ['broker/auth', 'broker/health', 'broker/releaseOwner']);
295297
const calls = (await readFile(record, 'utf8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
296298
assert.ok(calls.some((call) => call.method === 'session/stop' && call.params?.sessionId === 'zcode-a'));
297299
assert.ok(!calls.some((call) => call.method === 'session/stop' && call.params?.sessionId === 'zcode-b'));

0 commit comments

Comments
 (0)