143 lines
4.7 KiB
TypeScript
143 lines
4.7 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import { RuntimeTurnSettledIngestor } from '@features/member-work-sync/core/application';
|
|
import { ClaudeStopHookPayloadNormalizer } from '@features/member-work-sync/main/infrastructure/ClaudeStopHookPayloadNormalizer';
|
|
import { NodeHashAdapter } from '@features/member-work-sync/main/infrastructure/NodeHashAdapter';
|
|
|
|
import type {
|
|
RuntimeTurnSettledClaimedPayload,
|
|
RuntimeTurnSettledEventStorePort,
|
|
RuntimeTurnSettledInvalidResult,
|
|
RuntimeTurnSettledProcessedResult,
|
|
RuntimeTurnSettledTargetResolverPort,
|
|
} from '@features/member-work-sync/core/application';
|
|
|
|
function makePayload(raw: string): RuntimeTurnSettledClaimedPayload {
|
|
return {
|
|
id: 'event-1.claude.json',
|
|
fileName: 'event-1.claude.json',
|
|
filePath: '/tmp/event-1.claude.json',
|
|
provider: 'claude',
|
|
raw,
|
|
claimedAt: '2026-04-29T12:00:00.000Z',
|
|
};
|
|
}
|
|
|
|
describe('RuntimeTurnSettledIngestor', () => {
|
|
it('normalizes Claude Stop payloads and enqueues resolved member reconcile', async () => {
|
|
const payload = makePayload(
|
|
JSON.stringify({
|
|
hook_event_name: 'Stop',
|
|
session_id: 'ses-1',
|
|
transcript_path: '/tmp/ses-1.jsonl',
|
|
})
|
|
);
|
|
const processed: RuntimeTurnSettledProcessedResult[] = [];
|
|
const store: RuntimeTurnSettledEventStorePort = {
|
|
claimPending: vi.fn(async () => [payload]),
|
|
markProcessed: vi.fn(async (_payload, result) => {
|
|
processed.push(result);
|
|
}),
|
|
markInvalid: vi.fn(),
|
|
};
|
|
const resolver: RuntimeTurnSettledTargetResolverPort = {
|
|
resolve: vi.fn(async () => ({ ok: true as const, teamName: 'team-a', memberName: 'alice' })),
|
|
};
|
|
const enqueueRuntimeTurnSettled = vi.fn();
|
|
|
|
const ingestor = new RuntimeTurnSettledIngestor({
|
|
eventStore: store,
|
|
normalizer: new ClaudeStopHookPayloadNormalizer(new NodeHashAdapter()),
|
|
targetResolver: resolver,
|
|
reconcileQueue: { enqueueRuntimeTurnSettled },
|
|
clock: { now: () => new Date('2026-04-29T12:01:00.000Z') },
|
|
});
|
|
|
|
await expect(ingestor.drainPending()).resolves.toEqual({
|
|
claimed: 1,
|
|
enqueued: 1,
|
|
unresolved: 0,
|
|
invalid: 0,
|
|
failed: 0,
|
|
});
|
|
expect(enqueueRuntimeTurnSettled).toHaveBeenCalledWith({
|
|
teamName: 'team-a',
|
|
memberName: 'alice',
|
|
event: expect.objectContaining({
|
|
provider: 'claude',
|
|
hookEventName: 'Stop',
|
|
sessionId: 'ses-1',
|
|
transcriptPath: '/tmp/ses-1.jsonl',
|
|
}),
|
|
});
|
|
expect(processed[0]).toMatchObject({
|
|
outcome: 'enqueued',
|
|
teamName: 'team-a',
|
|
memberName: 'alice',
|
|
});
|
|
});
|
|
|
|
it('quarantines malformed payloads without enqueueing reconcile', async () => {
|
|
const payload = makePayload('not-json');
|
|
const invalid: RuntimeTurnSettledInvalidResult[] = [];
|
|
const store: RuntimeTurnSettledEventStorePort = {
|
|
claimPending: vi.fn(async () => [payload]),
|
|
markProcessed: vi.fn(),
|
|
markInvalid: vi.fn(async (_payload, result) => {
|
|
invalid.push(result);
|
|
}),
|
|
};
|
|
|
|
const ingestor = new RuntimeTurnSettledIngestor({
|
|
eventStore: store,
|
|
normalizer: new ClaudeStopHookPayloadNormalizer(new NodeHashAdapter()),
|
|
targetResolver: { resolve: vi.fn() },
|
|
reconcileQueue: { enqueueRuntimeTurnSettled: vi.fn() },
|
|
clock: { now: () => new Date('2026-04-29T12:01:00.000Z') },
|
|
});
|
|
|
|
await expect(ingestor.drainPending()).resolves.toMatchObject({
|
|
claimed: 1,
|
|
invalid: 1,
|
|
enqueued: 0,
|
|
});
|
|
expect(invalid[0]).toMatchObject({ reason: 'invalid_json' });
|
|
});
|
|
|
|
it('records unresolved Stop events as processed diagnostics', async () => {
|
|
const payload = makePayload(
|
|
JSON.stringify({ hook_event_name: 'Stop', transcript_path: '/tmp/unknown.jsonl' })
|
|
);
|
|
const processed: RuntimeTurnSettledProcessedResult[] = [];
|
|
const store: RuntimeTurnSettledEventStorePort = {
|
|
claimPending: vi.fn(async () => [payload]),
|
|
markProcessed: vi.fn(async (_payload, result) => {
|
|
processed.push(result);
|
|
}),
|
|
markInvalid: vi.fn(),
|
|
};
|
|
|
|
const ingestor = new RuntimeTurnSettledIngestor({
|
|
eventStore: store,
|
|
normalizer: new ClaudeStopHookPayloadNormalizer(new NodeHashAdapter()),
|
|
targetResolver: {
|
|
resolve: vi.fn(async () => ({
|
|
ok: false as const,
|
|
reason: 'no_matching_member_session',
|
|
})),
|
|
},
|
|
reconcileQueue: { enqueueRuntimeTurnSettled: vi.fn() },
|
|
clock: { now: () => new Date('2026-04-29T12:01:00.000Z') },
|
|
});
|
|
|
|
await expect(ingestor.drainPending()).resolves.toMatchObject({
|
|
claimed: 1,
|
|
unresolved: 1,
|
|
enqueued: 0,
|
|
});
|
|
expect(processed[0]).toMatchObject({
|
|
outcome: 'unresolved',
|
|
reason: 'no_matching_member_session',
|
|
});
|
|
});
|
|
});
|