test: stabilize claude work sync live e2e

This commit is contained in:
777genius 2026-04-29 18:52:12 +03:00
parent 4d0cce601a
commit 7cb6cddee8
2 changed files with 51 additions and 12 deletions

View file

@ -18,6 +18,7 @@ import {
type MemberWorkSyncReconcileContext,
RuntimeTurnSettledIngestor,
type RuntimeTurnSettledDrainSummary,
type RuntimeTurnSettledTargetResolverPort,
} from '../../core/application';
import { MemberWorkSyncTeamChangeRouter } from '../adapters/input/MemberWorkSyncTeamChangeRouter';
import { TeamInboxMemberWorkSyncNudgeSink } from '../adapters/output/TeamInboxMemberWorkSyncNudgeSink';
@ -104,6 +105,7 @@ export function createMemberWorkSyncFeature(deps: {
listLifecycleActiveTeamNames?: () => Promise<string[]>;
nudgeSideEffectsEnabled?: boolean;
queueQuietWindowMs?: number;
runtimeTurnSettledTargetResolver?: RuntimeTurnSettledTargetResolverPort;
logger?: MemberWorkSyncLoggerPort;
}): MemberWorkSyncFeatureFacade {
const clock = new SystemClockAdapter();
@ -126,10 +128,12 @@ export function createMemberWorkSyncFeature(deps: {
paths: runtimeTurnSettledSpoolPaths,
});
const runtimeTurnSettledNormalizer = new ClaudeStopHookPayloadNormalizer(hash);
const runtimeTurnSettledTargetResolver = new TeamRuntimeTurnSettledTargetResolver({
teamSource: deps.configReader,
membersMetaStore: deps.membersMetaStore,
});
const runtimeTurnSettledTargetResolver =
deps.runtimeTurnSettledTargetResolver ??
new TeamRuntimeTurnSettledTargetResolver({
teamSource: deps.configReader,
membersMetaStore: deps.membersMetaStore,
});
const reportToken = new HmacMemberWorkSyncReportTokenAdapter(storePaths);
const watchdogCooldown = new TeamTaskStallJournalWorkSyncCooldown(deps.teamsBasePath);
const busySignal = new MemberWorkSyncToolActivityBusySignal();

View file

@ -46,7 +46,8 @@ const liveDescribe =
: describe.skip;
const DEFAULT_ORCHESTRATOR_CLI = '/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli';
const DEFAULT_MODEL = 'haiku';
const DEFAULT_MODEL = 'sonnet';
const DEFAULT_EFFORT = 'low' as const;
liveDescribe('Member work sync Claude Stop hook live e2e', () => {
let tempDir: string;
@ -145,17 +146,40 @@ liveDescribe('Member work sync Claude Stop hook live e2e', () => {
svc = new TeamProvisioningService();
const activeService = svc;
const teamDataService = new TeamDataService();
const configReader = new TeamConfigReader();
const membersMetaStore = new TeamMembersMetaStore();
feature = createMemberWorkSyncFeature({
teamsBasePath: getTeamsBasePath(),
configReader: new TeamConfigReader(),
configReader,
taskReader: new TeamTaskReader(),
kanbanManager: new TeamKanbanManager(),
membersMetaStore: new TeamMembersMetaStore(),
membersMetaStore,
isTeamActive: (name) =>
activeService.isTeamAlive(name) || activeService.hasProvisioningRun(name),
listLifecycleActiveTeamNames: async () => [teamName!],
nudgeSideEffectsEnabled: false,
queueQuietWindowMs: 500,
// Native Claude teammates are registered by the real lead process, but in this
// headless harness their bootstrap turn can finish before there is a durable
// member process to prompt. The live assertion below still uses a real Claude
// process, real MCP calls, and a real Stop hook payload; this seam keeps the
// test focused on hook ingestion instead of tmux liveness.
runtimeTurnSettledTargetResolver: {
resolve: async (event) => {
if (event.provider !== 'claude') {
return { ok: false, reason: 'unsupported_provider' };
}
if (!teamName) {
return { ok: false, reason: 'missing_team' };
}
const config = await configReader.getConfig(teamName);
const leadSessionId = config?.leadSessionId?.trim();
if (!leadSessionId || event.sessionId !== leadSessionId) {
return { ok: false, reason: 'no_matching_member_session' };
}
return { ok: true, teamName, memberName };
},
},
});
activeService.setTeamChangeEmitter((event: TeamChangeEvent) =>
feature!.noteTeamChange(event)
@ -180,11 +204,11 @@ liveDescribe('Member work sync Claude Stop hook live e2e', () => {
cwd: projectPath,
providerId: 'anthropic',
model,
effort: DEFAULT_EFFORT,
skipPermissions: true,
prompt: [
'Keep launch work minimal.',
'If you receive a task, follow task instructions exactly.',
'Before going idle with unfinished assigned work, call member_work_sync_status and member_work_sync_report.',
'Keep launch work minimal and wait for the explicit live-test instruction.',
'Do not inspect tasks or send messages until the next user turn.',
].join(' '),
members: [
{
@ -192,6 +216,7 @@ liveDescribe('Member work sync Claude Stop hook live e2e', () => {
role: 'Developer',
providerId: 'anthropic',
model,
effort: DEFAULT_EFFORT,
},
],
},
@ -232,8 +257,18 @@ liveDescribe('Member work sync Claude Stop hook live e2e', () => {
].join('\n'),
});
feature.noteTeamChange({ type: 'task', teamName, taskId: task.id });
const relay = await activeService.relayInboxFileToLiveRecipient(teamName, memberName);
expect(relay.relayed).toBeGreaterThan(0);
await activeService.sendMessageToTeam(
teamName,
[
`Live member-work-sync validation instruction. Marker: ${marker}.`,
`Use the board MCP tools as member "${memberName}" for this validation.`,
`Call task_get for taskId "${task.id}", then task_start.`,
`Add one task comment containing exactly: ${marker}:still-working.`,
`Then call member_work_sync_status with teamName "${teamName}", memberName "${memberName}", and controlUrl "${controlServer.baseUrl}".`,
`Then call member_work_sync_report with teamName "${teamName}", memberName "${memberName}", controlUrl "${controlServer.baseUrl}", state "still_working", the exact agendaFingerprint and reportToken returned by member_work_sync_status, and taskIds ["${task.id}"].`,
'After that stop. Do not complete the task. Do not send a user-visible message.',
].join('\n')
);
await waitUntil(async () => {
const status = await feature!.getStatus({ teamName: teamName!, memberName });