test: stabilize claude work sync live e2e
This commit is contained in:
parent
4d0cce601a
commit
7cb6cddee8
2 changed files with 51 additions and 12 deletions
|
|
@ -18,6 +18,7 @@ import {
|
||||||
type MemberWorkSyncReconcileContext,
|
type MemberWorkSyncReconcileContext,
|
||||||
RuntimeTurnSettledIngestor,
|
RuntimeTurnSettledIngestor,
|
||||||
type RuntimeTurnSettledDrainSummary,
|
type RuntimeTurnSettledDrainSummary,
|
||||||
|
type RuntimeTurnSettledTargetResolverPort,
|
||||||
} from '../../core/application';
|
} from '../../core/application';
|
||||||
import { MemberWorkSyncTeamChangeRouter } from '../adapters/input/MemberWorkSyncTeamChangeRouter';
|
import { MemberWorkSyncTeamChangeRouter } from '../adapters/input/MemberWorkSyncTeamChangeRouter';
|
||||||
import { TeamInboxMemberWorkSyncNudgeSink } from '../adapters/output/TeamInboxMemberWorkSyncNudgeSink';
|
import { TeamInboxMemberWorkSyncNudgeSink } from '../adapters/output/TeamInboxMemberWorkSyncNudgeSink';
|
||||||
|
|
@ -104,6 +105,7 @@ export function createMemberWorkSyncFeature(deps: {
|
||||||
listLifecycleActiveTeamNames?: () => Promise<string[]>;
|
listLifecycleActiveTeamNames?: () => Promise<string[]>;
|
||||||
nudgeSideEffectsEnabled?: boolean;
|
nudgeSideEffectsEnabled?: boolean;
|
||||||
queueQuietWindowMs?: number;
|
queueQuietWindowMs?: number;
|
||||||
|
runtimeTurnSettledTargetResolver?: RuntimeTurnSettledTargetResolverPort;
|
||||||
logger?: MemberWorkSyncLoggerPort;
|
logger?: MemberWorkSyncLoggerPort;
|
||||||
}): MemberWorkSyncFeatureFacade {
|
}): MemberWorkSyncFeatureFacade {
|
||||||
const clock = new SystemClockAdapter();
|
const clock = new SystemClockAdapter();
|
||||||
|
|
@ -126,10 +128,12 @@ export function createMemberWorkSyncFeature(deps: {
|
||||||
paths: runtimeTurnSettledSpoolPaths,
|
paths: runtimeTurnSettledSpoolPaths,
|
||||||
});
|
});
|
||||||
const runtimeTurnSettledNormalizer = new ClaudeStopHookPayloadNormalizer(hash);
|
const runtimeTurnSettledNormalizer = new ClaudeStopHookPayloadNormalizer(hash);
|
||||||
const runtimeTurnSettledTargetResolver = new TeamRuntimeTurnSettledTargetResolver({
|
const runtimeTurnSettledTargetResolver =
|
||||||
teamSource: deps.configReader,
|
deps.runtimeTurnSettledTargetResolver ??
|
||||||
membersMetaStore: deps.membersMetaStore,
|
new TeamRuntimeTurnSettledTargetResolver({
|
||||||
});
|
teamSource: deps.configReader,
|
||||||
|
membersMetaStore: deps.membersMetaStore,
|
||||||
|
});
|
||||||
const reportToken = new HmacMemberWorkSyncReportTokenAdapter(storePaths);
|
const reportToken = new HmacMemberWorkSyncReportTokenAdapter(storePaths);
|
||||||
const watchdogCooldown = new TeamTaskStallJournalWorkSyncCooldown(deps.teamsBasePath);
|
const watchdogCooldown = new TeamTaskStallJournalWorkSyncCooldown(deps.teamsBasePath);
|
||||||
const busySignal = new MemberWorkSyncToolActivityBusySignal();
|
const busySignal = new MemberWorkSyncToolActivityBusySignal();
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ const liveDescribe =
|
||||||
: describe.skip;
|
: describe.skip;
|
||||||
|
|
||||||
const DEFAULT_ORCHESTRATOR_CLI = '/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli';
|
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', () => {
|
liveDescribe('Member work sync Claude Stop hook live e2e', () => {
|
||||||
let tempDir: string;
|
let tempDir: string;
|
||||||
|
|
@ -145,17 +146,40 @@ liveDescribe('Member work sync Claude Stop hook live e2e', () => {
|
||||||
svc = new TeamProvisioningService();
|
svc = new TeamProvisioningService();
|
||||||
const activeService = svc;
|
const activeService = svc;
|
||||||
const teamDataService = new TeamDataService();
|
const teamDataService = new TeamDataService();
|
||||||
|
const configReader = new TeamConfigReader();
|
||||||
|
const membersMetaStore = new TeamMembersMetaStore();
|
||||||
feature = createMemberWorkSyncFeature({
|
feature = createMemberWorkSyncFeature({
|
||||||
teamsBasePath: getTeamsBasePath(),
|
teamsBasePath: getTeamsBasePath(),
|
||||||
configReader: new TeamConfigReader(),
|
configReader,
|
||||||
taskReader: new TeamTaskReader(),
|
taskReader: new TeamTaskReader(),
|
||||||
kanbanManager: new TeamKanbanManager(),
|
kanbanManager: new TeamKanbanManager(),
|
||||||
membersMetaStore: new TeamMembersMetaStore(),
|
membersMetaStore,
|
||||||
isTeamActive: (name) =>
|
isTeamActive: (name) =>
|
||||||
activeService.isTeamAlive(name) || activeService.hasProvisioningRun(name),
|
activeService.isTeamAlive(name) || activeService.hasProvisioningRun(name),
|
||||||
listLifecycleActiveTeamNames: async () => [teamName!],
|
listLifecycleActiveTeamNames: async () => [teamName!],
|
||||||
nudgeSideEffectsEnabled: false,
|
nudgeSideEffectsEnabled: false,
|
||||||
queueQuietWindowMs: 500,
|
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) =>
|
activeService.setTeamChangeEmitter((event: TeamChangeEvent) =>
|
||||||
feature!.noteTeamChange(event)
|
feature!.noteTeamChange(event)
|
||||||
|
|
@ -180,11 +204,11 @@ liveDescribe('Member work sync Claude Stop hook live e2e', () => {
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
providerId: 'anthropic',
|
providerId: 'anthropic',
|
||||||
model,
|
model,
|
||||||
|
effort: DEFAULT_EFFORT,
|
||||||
skipPermissions: true,
|
skipPermissions: true,
|
||||||
prompt: [
|
prompt: [
|
||||||
'Keep launch work minimal.',
|
'Keep launch work minimal and wait for the explicit live-test instruction.',
|
||||||
'If you receive a task, follow task instructions exactly.',
|
'Do not inspect tasks or send messages until the next user turn.',
|
||||||
'Before going idle with unfinished assigned work, call member_work_sync_status and member_work_sync_report.',
|
|
||||||
].join(' '),
|
].join(' '),
|
||||||
members: [
|
members: [
|
||||||
{
|
{
|
||||||
|
|
@ -192,6 +216,7 @@ liveDescribe('Member work sync Claude Stop hook live e2e', () => {
|
||||||
role: 'Developer',
|
role: 'Developer',
|
||||||
providerId: 'anthropic',
|
providerId: 'anthropic',
|
||||||
model,
|
model,
|
||||||
|
effort: DEFAULT_EFFORT,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -232,8 +257,18 @@ liveDescribe('Member work sync Claude Stop hook live e2e', () => {
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
});
|
});
|
||||||
feature.noteTeamChange({ type: 'task', teamName, taskId: task.id });
|
feature.noteTeamChange({ type: 'task', teamName, taskId: task.id });
|
||||||
const relay = await activeService.relayInboxFileToLiveRecipient(teamName, memberName);
|
await activeService.sendMessageToTeam(
|
||||||
expect(relay.relayed).toBeGreaterThan(0);
|
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 () => {
|
await waitUntil(async () => {
|
||||||
const status = await feature!.getStatus({ teamName: teamName!, memberName });
|
const status = await feature!.getStatus({ teamName: teamName!, memberName });
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue