test(member-work-sync): restore boundary coverage
This commit is contained in:
parent
9fb9e5f66a
commit
7dc4a1976d
5 changed files with 560 additions and 0 deletions
|
|
@ -0,0 +1,109 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { TeamInboxMemberWorkSyncNudgeSink } from '@features/member-work-sync/main/adapters/output/TeamInboxMemberWorkSyncNudgeSink';
|
||||
|
||||
import type { MemberWorkSyncInboxNudgePort } from '@features/member-work-sync/core/application';
|
||||
|
||||
type NudgeInput = Parameters<MemberWorkSyncInboxNudgePort['insertIfAbsent']>[0];
|
||||
|
||||
function makeInput(overrides: Partial<NudgeInput> = {}): NudgeInput {
|
||||
return {
|
||||
teamName: 'team-a',
|
||||
memberName: 'bob',
|
||||
messageId: 'member-work-sync:team-a:bob:agenda-v1-test',
|
||||
payloadHash: 'payload-hash',
|
||||
timestamp: '2026-04-29T00:00:00.000Z',
|
||||
payload: {
|
||||
from: 'system',
|
||||
to: 'bob',
|
||||
messageKind: 'member_work_sync_nudge',
|
||||
source: 'member-work-sync',
|
||||
actionMode: 'do',
|
||||
text: 'Please reconcile your current work state.',
|
||||
taskRefs: [{ teamName: 'team-a', taskId: 'task-1', displayId: '11111111' }],
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('TeamInboxMemberWorkSyncNudgeSink', () => {
|
||||
it('returns inserted=false when the inbox already contains the stable messageId', async () => {
|
||||
const input = makeInput();
|
||||
const inboxReader = {
|
||||
getMessagesFor: vi.fn(async () => [{ messageId: input.messageId }]),
|
||||
};
|
||||
const inboxWriter = {
|
||||
sendMessage: vi.fn(),
|
||||
};
|
||||
const sink = new TeamInboxMemberWorkSyncNudgeSink(inboxReader as never, inboxWriter as never);
|
||||
|
||||
await expect(sink.insertIfAbsent(input)).resolves.toEqual({
|
||||
inserted: false,
|
||||
messageId: input.messageId,
|
||||
});
|
||||
|
||||
expect(inboxReader.getMessagesFor).toHaveBeenCalledWith('team-a', 'bob');
|
||||
expect(inboxWriter.sendMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('writes a system notification inbox message for a new nudge', async () => {
|
||||
const input = makeInput();
|
||||
const inboxReader = {
|
||||
getMessagesFor: vi.fn(async () => []),
|
||||
};
|
||||
const inboxWriter = {
|
||||
sendMessage: vi.fn(async () => ({ messageId: input.messageId })),
|
||||
};
|
||||
const sink = new TeamInboxMemberWorkSyncNudgeSink(inboxReader as never, inboxWriter as never);
|
||||
|
||||
await expect(sink.insertIfAbsent(input)).resolves.toEqual({
|
||||
inserted: true,
|
||||
messageId: input.messageId,
|
||||
});
|
||||
|
||||
expect(inboxWriter.sendMessage).toHaveBeenCalledWith('team-a', {
|
||||
member: 'bob',
|
||||
from: 'system',
|
||||
to: 'bob',
|
||||
messageId: input.messageId,
|
||||
timestamp: input.timestamp,
|
||||
text: input.payload.text,
|
||||
taskRefs: input.payload.taskRefs,
|
||||
actionMode: 'do',
|
||||
summary: 'Work sync check',
|
||||
source: 'system_notification',
|
||||
messageKind: 'member_work_sync_nudge',
|
||||
});
|
||||
});
|
||||
|
||||
it('propagates reader failures so dispatch can classify the attempt', async () => {
|
||||
const input = makeInput();
|
||||
const inboxReader = {
|
||||
getMessagesFor: vi.fn(async () => {
|
||||
throw new Error('reader failed');
|
||||
}),
|
||||
};
|
||||
const inboxWriter = {
|
||||
sendMessage: vi.fn(),
|
||||
};
|
||||
const sink = new TeamInboxMemberWorkSyncNudgeSink(inboxReader as never, inboxWriter as never);
|
||||
|
||||
await expect(sink.insertIfAbsent(input)).rejects.toThrow('reader failed');
|
||||
expect(inboxWriter.sendMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('propagates writer failures so dispatch can retry or mark terminal', async () => {
|
||||
const input = makeInput();
|
||||
const inboxReader = {
|
||||
getMessagesFor: vi.fn(async () => []),
|
||||
};
|
||||
const inboxWriter = {
|
||||
sendMessage: vi.fn(async () => {
|
||||
throw new Error('writer failed');
|
||||
}),
|
||||
};
|
||||
const sink = new TeamInboxMemberWorkSyncNudgeSink(inboxReader as never, inboxWriter as never);
|
||||
|
||||
await expect(sink.insertIfAbsent(input)).rejects.toThrow('writer failed');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
MEMBER_WORK_SYNC_GET_METRICS,
|
||||
MEMBER_WORK_SYNC_GET_STATUS,
|
||||
MEMBER_WORK_SYNC_REPORT,
|
||||
} from '@features/member-work-sync/contracts';
|
||||
import { registerMemberWorkSyncIpc, removeMemberWorkSyncIpc } from '@features/member-work-sync/main';
|
||||
|
||||
import type {
|
||||
MemberWorkSyncMetricsRequest,
|
||||
MemberWorkSyncReportRequest,
|
||||
MemberWorkSyncStatusRequest,
|
||||
} from '@features/member-work-sync/contracts';
|
||||
import type { MemberWorkSyncFeatureFacade } from '@features/member-work-sync/main';
|
||||
import type { IpcMain } from 'electron';
|
||||
|
||||
vi.mock('@shared/utils/logger', () => ({
|
||||
createLogger: () => ({
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
type IpcHandler = (event: unknown, request: unknown) => Promise<unknown>;
|
||||
|
||||
function makeIpcMain() {
|
||||
const handlers = new Map<string, IpcHandler>();
|
||||
const ipcMain = {
|
||||
handle: vi.fn((channel: string, handler: IpcHandler) => {
|
||||
handlers.set(channel, handler);
|
||||
}),
|
||||
removeHandler: vi.fn((channel: string) => {
|
||||
handlers.delete(channel);
|
||||
}),
|
||||
};
|
||||
|
||||
return { handlers, ipcMain: ipcMain as unknown as IpcMain };
|
||||
}
|
||||
|
||||
function makeFeature(): MemberWorkSyncFeatureFacade {
|
||||
return {
|
||||
getStatus: vi.fn(async (request) => ({
|
||||
teamName: request.teamName,
|
||||
memberName: request.memberName,
|
||||
state: 'unknown' as const,
|
||||
agenda: {
|
||||
teamName: request.teamName,
|
||||
memberName: request.memberName,
|
||||
generatedAt: '2026-04-29T00:00:00.000Z',
|
||||
fingerprint: 'agenda:v1:test',
|
||||
items: [],
|
||||
diagnostics: [],
|
||||
},
|
||||
evaluatedAt: '2026-04-29T00:00:00.000Z',
|
||||
diagnostics: [],
|
||||
})),
|
||||
getMetrics: vi.fn(async (request) => ({
|
||||
teamName: request.teamName,
|
||||
generatedAt: '2026-04-29T00:00:00.000Z',
|
||||
memberCount: 0,
|
||||
stateCounts: {
|
||||
blocked: 0,
|
||||
caught_up: 0,
|
||||
inactive: 0,
|
||||
needs_sync: 0,
|
||||
still_working: 0,
|
||||
unknown: 0,
|
||||
},
|
||||
actionableItemCount: 0,
|
||||
wouldNudgeCount: 0,
|
||||
fingerprintChangeCount: 0,
|
||||
reportAcceptedCount: 0,
|
||||
reportRejectedCount: 0,
|
||||
recentEvents: [],
|
||||
phase2Readiness: {
|
||||
state: 'collecting_shadow_data' as const,
|
||||
reasons: ['insufficient_members' as const],
|
||||
thresholds: {
|
||||
maxFingerprintChangesPerMemberHour: 4,
|
||||
maxReportRejectionRate: 0.2,
|
||||
maxWouldNudgesPerMemberHour: 2,
|
||||
minObservationHours: 24,
|
||||
minObservedMembers: 3,
|
||||
minStatusEvents: 20,
|
||||
},
|
||||
rates: {
|
||||
fingerprintChangesPerMemberHour: 0,
|
||||
observationHours: 0,
|
||||
reportRejectionRate: 0,
|
||||
statusEventCount: 0,
|
||||
wouldNudgesPerMemberHour: 0,
|
||||
},
|
||||
diagnostics: [],
|
||||
},
|
||||
})),
|
||||
report: vi.fn(async (request) => ({
|
||||
accepted: true,
|
||||
code: 'accepted',
|
||||
message: 'Report accepted.',
|
||||
status: {
|
||||
teamName: request.teamName,
|
||||
memberName: request.memberName,
|
||||
state: request.state,
|
||||
agenda: {
|
||||
teamName: request.teamName,
|
||||
memberName: request.memberName,
|
||||
generatedAt: '2026-04-29T00:00:00.000Z',
|
||||
fingerprint: request.agendaFingerprint,
|
||||
items: [],
|
||||
diagnostics: [],
|
||||
},
|
||||
evaluatedAt: '2026-04-29T00:00:00.000Z',
|
||||
diagnostics: [],
|
||||
},
|
||||
})),
|
||||
noteTeamChange: vi.fn(),
|
||||
enqueueStartupScan: vi.fn(),
|
||||
replayPendingReports: vi.fn(),
|
||||
dispatchDueNudges: vi.fn(),
|
||||
buildRuntimeTurnSettledHookSettings: vi.fn(),
|
||||
buildRuntimeTurnSettledEnvironment: vi.fn(),
|
||||
drainRuntimeTurnSettledEvents: vi.fn(),
|
||||
getQueueDiagnostics: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe('registerMemberWorkSyncIpc', () => {
|
||||
it('registers status, metrics and report handlers that delegate requests unchanged', async () => {
|
||||
const { handlers, ipcMain } = makeIpcMain();
|
||||
const feature = makeFeature();
|
||||
|
||||
registerMemberWorkSyncIpc(ipcMain, feature);
|
||||
|
||||
expect(ipcMain.handle).toHaveBeenCalledTimes(3);
|
||||
expect([...handlers.keys()].sort()).toEqual(
|
||||
[MEMBER_WORK_SYNC_GET_METRICS, MEMBER_WORK_SYNC_GET_STATUS, MEMBER_WORK_SYNC_REPORT].sort()
|
||||
);
|
||||
|
||||
const statusRequest: MemberWorkSyncStatusRequest = { teamName: 'team-a', memberName: 'bob' };
|
||||
const metricsRequest: MemberWorkSyncMetricsRequest = { teamName: 'team-a' };
|
||||
const reportRequest: MemberWorkSyncReportRequest = {
|
||||
teamName: 'team-a',
|
||||
memberName: 'bob',
|
||||
state: 'still_working',
|
||||
agendaFingerprint: 'agenda:v1:test',
|
||||
};
|
||||
|
||||
await expect(
|
||||
handlers.get(MEMBER_WORK_SYNC_GET_STATUS)?.({}, statusRequest)
|
||||
).resolves.toMatchObject({ teamName: 'team-a', memberName: 'bob' });
|
||||
await expect(
|
||||
handlers.get(MEMBER_WORK_SYNC_GET_METRICS)?.({}, metricsRequest)
|
||||
).resolves.toMatchObject({ teamName: 'team-a' });
|
||||
await expect(handlers.get(MEMBER_WORK_SYNC_REPORT)?.({}, reportRequest)).resolves.toMatchObject(
|
||||
{ accepted: true, code: 'accepted' }
|
||||
);
|
||||
|
||||
expect(feature.getStatus).toHaveBeenCalledWith(statusRequest);
|
||||
expect(feature.getMetrics).toHaveBeenCalledWith(metricsRequest);
|
||||
expect(feature.report).toHaveBeenCalledWith(reportRequest);
|
||||
});
|
||||
|
||||
it('propagates feature errors so the renderer receives the real status failure', async () => {
|
||||
const { handlers, ipcMain } = makeIpcMain();
|
||||
const feature = makeFeature();
|
||||
const failure = new Error('status failed');
|
||||
vi.mocked(feature.getStatus).mockRejectedValueOnce(failure);
|
||||
|
||||
registerMemberWorkSyncIpc(ipcMain, feature);
|
||||
|
||||
await expect(
|
||||
handlers.get(MEMBER_WORK_SYNC_GET_STATUS)?.({}, { teamName: 'team-a', memberName: 'bob' })
|
||||
).rejects.toThrow('status failed');
|
||||
});
|
||||
|
||||
it('propagates metrics and report errors without replacing them', async () => {
|
||||
const { handlers, ipcMain } = makeIpcMain();
|
||||
const feature = makeFeature();
|
||||
vi.mocked(feature.getMetrics).mockRejectedValueOnce(new Error('metrics failed'));
|
||||
vi.mocked(feature.report).mockRejectedValueOnce(new Error('report failed'));
|
||||
|
||||
registerMemberWorkSyncIpc(ipcMain, feature);
|
||||
|
||||
await expect(
|
||||
handlers.get(MEMBER_WORK_SYNC_GET_METRICS)?.({}, { teamName: 'team-a' })
|
||||
).rejects.toThrow('metrics failed');
|
||||
await expect(
|
||||
handlers.get(MEMBER_WORK_SYNC_REPORT)?.(
|
||||
{},
|
||||
{
|
||||
teamName: 'team-a',
|
||||
memberName: 'bob',
|
||||
state: 'blocked',
|
||||
agendaFingerprint: 'agenda:v1:test',
|
||||
}
|
||||
)
|
||||
).rejects.toThrow('report failed');
|
||||
});
|
||||
|
||||
it('removes exactly the member work sync handlers', () => {
|
||||
const { handlers, ipcMain } = makeIpcMain();
|
||||
const feature = makeFeature();
|
||||
registerMemberWorkSyncIpc(ipcMain, feature);
|
||||
handlers.set('unrelated:channel', vi.fn());
|
||||
|
||||
removeMemberWorkSyncIpc(ipcMain);
|
||||
|
||||
expect(ipcMain.removeHandler).toHaveBeenCalledTimes(3);
|
||||
expect(ipcMain.removeHandler).toHaveBeenCalledWith(MEMBER_WORK_SYNC_GET_STATUS);
|
||||
expect(ipcMain.removeHandler).toHaveBeenCalledWith(MEMBER_WORK_SYNC_GET_METRICS);
|
||||
expect(ipcMain.removeHandler).toHaveBeenCalledWith(MEMBER_WORK_SYNC_REPORT);
|
||||
expect([...handlers.keys()]).toEqual(['unrelated:channel']);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
MEMBER_WORK_SYNC_GET_METRICS,
|
||||
MEMBER_WORK_SYNC_GET_STATUS,
|
||||
MEMBER_WORK_SYNC_REPORT,
|
||||
} from '@features/member-work-sync/contracts';
|
||||
import { createMemberWorkSyncBridge } from '@features/member-work-sync/preload';
|
||||
|
||||
import type {
|
||||
MemberWorkSyncMetricsRequest,
|
||||
MemberWorkSyncReportRequest,
|
||||
MemberWorkSyncStatusRequest,
|
||||
} from '@features/member-work-sync/contracts';
|
||||
import type { IpcRenderer } from 'electron';
|
||||
|
||||
describe('createMemberWorkSyncBridge', () => {
|
||||
it('invokes the status channel without changing the request payload', async () => {
|
||||
const request: MemberWorkSyncStatusRequest = { teamName: 'team-a', memberName: 'bob' };
|
||||
const response = { ok: true };
|
||||
const ipcRenderer = {
|
||||
invoke: vi.fn(async () => response),
|
||||
} as unknown as IpcRenderer;
|
||||
const bridge = createMemberWorkSyncBridge(ipcRenderer);
|
||||
|
||||
await expect(bridge.getStatus(request)).resolves.toBe(response);
|
||||
|
||||
expect(ipcRenderer.invoke).toHaveBeenCalledWith(MEMBER_WORK_SYNC_GET_STATUS, request);
|
||||
});
|
||||
|
||||
it('invokes the metrics channel without changing the request payload', async () => {
|
||||
const request: MemberWorkSyncMetricsRequest = { teamName: 'team-a' };
|
||||
const response = { ok: true };
|
||||
const ipcRenderer = {
|
||||
invoke: vi.fn(async () => response),
|
||||
} as unknown as IpcRenderer;
|
||||
const bridge = createMemberWorkSyncBridge(ipcRenderer);
|
||||
|
||||
await expect(bridge.getMetrics(request)).resolves.toBe(response);
|
||||
|
||||
expect(ipcRenderer.invoke).toHaveBeenCalledWith(MEMBER_WORK_SYNC_GET_METRICS, request);
|
||||
});
|
||||
|
||||
it('invokes the report channel without changing the request payload', async () => {
|
||||
const request: MemberWorkSyncReportRequest = {
|
||||
teamName: 'team-a',
|
||||
memberName: 'bob',
|
||||
state: 'blocked',
|
||||
agendaFingerprint: 'agenda:v1:test',
|
||||
taskIds: ['task-1'],
|
||||
note: 'waiting on reviewer',
|
||||
source: 'app',
|
||||
};
|
||||
const response = { accepted: true };
|
||||
const ipcRenderer = {
|
||||
invoke: vi.fn(async () => response),
|
||||
} as unknown as IpcRenderer;
|
||||
const bridge = createMemberWorkSyncBridge(ipcRenderer);
|
||||
|
||||
await expect(bridge.report(request)).resolves.toBe(response);
|
||||
|
||||
expect(ipcRenderer.invoke).toHaveBeenCalledWith(MEMBER_WORK_SYNC_REPORT, request);
|
||||
});
|
||||
|
||||
it('propagates IPC rejections to the renderer caller', async () => {
|
||||
const failure = new Error('ipc failed');
|
||||
const ipcRenderer = {
|
||||
invoke: vi.fn(async () => {
|
||||
throw failure;
|
||||
}),
|
||||
} as unknown as IpcRenderer;
|
||||
const bridge = createMemberWorkSyncBridge(ipcRenderer);
|
||||
|
||||
await expect(bridge.getStatus({ teamName: 'team-a', memberName: 'bob' })).rejects.toBe(
|
||||
failure
|
||||
);
|
||||
});
|
||||
});
|
||||
67
test/preload/electronApiMemberWorkSync.test.ts
Normal file
67
test/preload/electronApiMemberWorkSync.test.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ElectronAPI } from '@shared/types/api';
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const memberWorkSyncBridge = {
|
||||
getStatus: vi.fn(),
|
||||
getMetrics: vi.fn(),
|
||||
report: vi.fn(),
|
||||
};
|
||||
|
||||
return {
|
||||
contextBridge: {
|
||||
exposeInMainWorld: vi.fn(),
|
||||
},
|
||||
ipcRenderer: {
|
||||
invoke: vi.fn(),
|
||||
on: vi.fn(),
|
||||
send: vi.fn(),
|
||||
},
|
||||
memberWorkSyncBridge,
|
||||
createMemberWorkSyncBridge: vi.fn(() => memberWorkSyncBridge),
|
||||
webUtils: {
|
||||
getPathForFile: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
contextBridge: mocks.contextBridge,
|
||||
ipcRenderer: mocks.ipcRenderer,
|
||||
webUtils: mocks.webUtils,
|
||||
}));
|
||||
|
||||
vi.mock('@features/member-work-sync/preload', () => ({
|
||||
createMemberWorkSyncBridge: mocks.createMemberWorkSyncBridge,
|
||||
}));
|
||||
|
||||
describe('preload electronAPI memberWorkSync wiring', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.useFakeTimers();
|
||||
mocks.contextBridge.exposeInMainWorld.mockClear();
|
||||
mocks.createMemberWorkSyncBridge.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers();
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('exposes the member work sync bridge on the shared Electron API', async () => {
|
||||
await import('../../src/preload/index');
|
||||
|
||||
expect(mocks.createMemberWorkSyncBridge).toHaveBeenCalledWith(mocks.ipcRenderer);
|
||||
expect(mocks.contextBridge.exposeInMainWorld).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [apiName, electronAPI] = mocks.contextBridge.exposeInMainWorld.mock.calls[0] as [
|
||||
string,
|
||||
ElectronAPI,
|
||||
];
|
||||
|
||||
expect(apiName).toBe('electronAPI');
|
||||
expect(electronAPI.memberWorkSync).toBe(mocks.memberWorkSyncBridge);
|
||||
});
|
||||
});
|
||||
88
test/renderer/api/httpClient.memberWorkSync.test.ts
Normal file
88
test/renderer/api/httpClient.memberWorkSync.test.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { HttpAPIClient } from '@renderer/api/httpClient';
|
||||
|
||||
class FakeEventSource {
|
||||
onopen: (() => void) | null = null;
|
||||
onerror: (() => void) | null = null;
|
||||
addEventListener = vi.fn();
|
||||
close = vi.fn();
|
||||
}
|
||||
|
||||
describe('HttpAPIClient memberWorkSync', () => {
|
||||
let fetchMock: ReturnType<typeof vi.fn>;
|
||||
let eventSourceMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock = vi.fn(async () => jsonResponse({ ok: true }));
|
||||
eventSourceMock = vi.fn(() => new FakeEventSource());
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
vi.stubGlobal('EventSource', eventSourceMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('maps browser-mode member work sync calls to the HTTP routes', async () => {
|
||||
const client = new HttpAPIClient('http://127.0.0.1:53123');
|
||||
fetchMock
|
||||
.mockResolvedValueOnce(jsonResponse({ state: 'needs_sync' }))
|
||||
.mockResolvedValueOnce(jsonResponse({ memberCount: 1 }))
|
||||
.mockResolvedValueOnce(jsonResponse({ accepted: true }));
|
||||
|
||||
await expect(
|
||||
client.memberWorkSync.getStatus({ teamName: 'demo team', memberName: 'bob/qa' })
|
||||
).resolves.toEqual({ state: 'needs_sync' });
|
||||
await expect(client.memberWorkSync.getMetrics({ teamName: 'demo team' })).resolves.toEqual({
|
||||
memberCount: 1,
|
||||
});
|
||||
await expect(
|
||||
client.memberWorkSync.report({
|
||||
teamName: 'demo team',
|
||||
memberName: 'bob/qa',
|
||||
state: 'still_working',
|
||||
agendaFingerprint: 'agenda:v1:test',
|
||||
taskIds: ['task-a'],
|
||||
source: 'app',
|
||||
})
|
||||
).resolves.toEqual({ accepted: true });
|
||||
|
||||
expect(eventSourceMock).toHaveBeenCalledWith('http://127.0.0.1:53123/api/events');
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'http://127.0.0.1:53123/api/teams/demo%20team/member-work-sync/bob%2Fqa',
|
||||
expect.objectContaining({ signal: expect.any(AbortSignal) })
|
||||
);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'http://127.0.0.1:53123/api/teams/demo%20team/member-work-sync/metrics',
|
||||
expect.objectContaining({ signal: expect.any(AbortSignal) })
|
||||
);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'http://127.0.0.1:53123/api/teams/demo%20team/member-work-sync/report',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
teamName: 'demo team',
|
||||
memberName: 'bob/qa',
|
||||
state: 'still_working',
|
||||
agendaFingerprint: 'agenda:v1:test',
|
||||
taskIds: ['task-a'],
|
||||
source: 'app',
|
||||
}),
|
||||
signal: expect.any(AbortSignal),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function jsonResponse(body: unknown): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue