fix(ci): stabilize relay priority tests
This commit is contained in:
parent
d5c40e5a7c
commit
440d37162b
3 changed files with 289 additions and 15 deletions
|
|
@ -3444,6 +3444,47 @@ function getOpenCodeInboxRelayPriority(
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLeadInboxRelayPriority(message: Pick<InboxMessage, 'messageKind'>): number {
|
||||||
|
if (message.messageKind === 'member_work_sync_nudge') {
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareInboxRelayMessages(
|
||||||
|
a: Pick<InboxMessage, 'messageKind' | 'source' | 'timestamp'> & { messageId: string },
|
||||||
|
b: Pick<InboxMessage, 'messageKind' | 'source' | 'timestamp'> & { messageId: string },
|
||||||
|
getPriority: (message: Pick<InboxMessage, 'messageKind' | 'source'>) => number
|
||||||
|
): number {
|
||||||
|
const priorityDelta = getPriority(b) - getPriority(a);
|
||||||
|
if (priorityDelta !== 0) return priorityDelta;
|
||||||
|
const aTime = Date.parse(a.timestamp);
|
||||||
|
const bTime = Date.parse(b.timestamp);
|
||||||
|
if (Number.isFinite(aTime) && Number.isFinite(bTime)) {
|
||||||
|
const timeDelta = aTime - bTime;
|
||||||
|
if (timeDelta !== 0) return timeDelta;
|
||||||
|
} else if (Number.isFinite(aTime)) {
|
||||||
|
return -1;
|
||||||
|
} else if (Number.isFinite(bTime)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return a.messageId.localeCompare(b.messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareOpenCodeInboxRelayMessagesByPriority(
|
||||||
|
a: Pick<InboxMessage, 'messageKind' | 'source' | 'timestamp'> & { messageId: string },
|
||||||
|
b: Pick<InboxMessage, 'messageKind' | 'source' | 'timestamp'> & { messageId: string }
|
||||||
|
): number {
|
||||||
|
return compareInboxRelayMessages(a, b, getOpenCodeInboxRelayPriority);
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareLeadInboxRelayMessagesByPriority(
|
||||||
|
a: Pick<InboxMessage, 'messageKind' | 'source' | 'timestamp'> & { messageId: string },
|
||||||
|
b: Pick<InboxMessage, 'messageKind' | 'source' | 'timestamp'> & { messageId: string }
|
||||||
|
): number {
|
||||||
|
return compareInboxRelayMessages(a, b, getLeadInboxRelayPriority);
|
||||||
|
}
|
||||||
|
|
||||||
export class TeamProvisioningService {
|
export class TeamProvisioningService {
|
||||||
private readonly runtimeLaneCoordinator = createTeamRuntimeLaneCoordinator();
|
private readonly runtimeLaneCoordinator = createTeamRuntimeLaneCoordinator();
|
||||||
private readonly providerConnectionService = ProviderConnectionService.getInstance();
|
private readonly providerConnectionService = ProviderConnectionService.getInstance();
|
||||||
|
|
@ -23567,13 +23608,7 @@ export class TeamProvisioningService {
|
||||||
if (typeof message.text !== 'string' || message.text.trim().length === 0) return false;
|
if (typeof message.text !== 'string' || message.text.trim().length === 0) return false;
|
||||||
return this.hasStableMessageId(message);
|
return this.hasStableMessageId(message);
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort(compareOpenCodeInboxRelayMessagesByPriority)
|
||||||
const priorityDelta = getOpenCodeInboxRelayPriority(a) - getOpenCodeInboxRelayPriority(b);
|
|
||||||
if (priorityDelta !== 0) return priorityDelta;
|
|
||||||
const timeDelta = Date.parse(a.timestamp) - Date.parse(b.timestamp);
|
|
||||||
if (timeDelta !== 0) return timeDelta;
|
|
||||||
return a.messageId.localeCompare(b.messageId);
|
|
||||||
})
|
|
||||||
.slice(0, 10);
|
.slice(0, 10);
|
||||||
|
|
||||||
let taskRefInferenceTasks: Promise<readonly TeamTask[]> | null = null;
|
let taskRefInferenceTasks: Promise<readonly TeamTask[]> | null = null;
|
||||||
|
|
@ -24491,13 +24526,26 @@ export class TeamProvisioningService {
|
||||||
if (actionableUnread.length === 0) return 0;
|
if (actionableUnread.length === 0) return 0;
|
||||||
|
|
||||||
const MAX_RELAY = 10;
|
const MAX_RELAY = 10;
|
||||||
const userOriginatedUnread = actionableUnread.filter((message) =>
|
const prioritizedActionableUnread = [...actionableUnread].sort(
|
||||||
|
compareLeadInboxRelayMessagesByPriority
|
||||||
|
);
|
||||||
|
const priorityUnread = prioritizedActionableUnread.filter(
|
||||||
|
(message) => getLeadInboxRelayPriority(message) > 0
|
||||||
|
);
|
||||||
|
const userOriginatedUnread = prioritizedActionableUnread.filter((message) =>
|
||||||
this.isUserOriginatedLeadRelayMessage(message)
|
this.isUserOriginatedLeadRelayMessage(message)
|
||||||
);
|
);
|
||||||
const replyVisibility: 'user' | 'internal_activity' =
|
const batchSource =
|
||||||
userOriginatedUnread.length > 0 ? 'user' : 'internal_activity';
|
priorityUnread.length > 0
|
||||||
const batchSource = userOriginatedUnread.length > 0 ? userOriginatedUnread : actionableUnread;
|
? priorityUnread
|
||||||
|
: userOriginatedUnread.length > 0
|
||||||
|
? userOriginatedUnread
|
||||||
|
: prioritizedActionableUnread;
|
||||||
const batch = batchSource.slice(0, MAX_RELAY);
|
const batch = batchSource.slice(0, MAX_RELAY);
|
||||||
|
const replyVisibility: 'user' | 'internal_activity' =
|
||||||
|
priorityUnread.length === 0 && userOriginatedUnread.length > 0
|
||||||
|
? 'user'
|
||||||
|
: 'internal_activity';
|
||||||
const batchIds = new Set(batch.map((message) => message.messageId));
|
const batchIds = new Set(batch.map((message) => message.messageId));
|
||||||
const hasPendingFollowUpRelay = unread.some(
|
const hasPendingFollowUpRelay = unread.some(
|
||||||
(message) => !batchIds.has(message.messageId) && !readOnlyIgnoredIds.has(message.messageId)
|
(message) => !batchIds.has(message.messageId) && !readOnlyIgnoredIds.has(message.messageId)
|
||||||
|
|
@ -24540,7 +24588,7 @@ export class TeamProvisioningService {
|
||||||
|
|
||||||
const message = [
|
const message = [
|
||||||
`You have new inbox messages addressed to you (team lead "${leadName}").`,
|
`You have new inbox messages addressed to you (team lead "${leadName}").`,
|
||||||
`Process them in order (oldest first).`,
|
`Process them in the listed order. High-priority work-sync control messages may appear before older routine rows.`,
|
||||||
`If action is required, delegate via task creation or SendMessage, and keep responses minimal.`,
|
`If action is required, delegate via task creation or SendMessage, and keep responses minimal.`,
|
||||||
...replyVisibilityInstruction,
|
...replyVisibilityInstruction,
|
||||||
`If there is no action to take, produce ZERO text output. Do NOT write "No action needed.", status echoes, or any other no-op summary.`,
|
`If there is no action to take, produce ZERO text output. Do NOT write "No action needed.", status echoes, or any other no-op summary.`,
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,38 @@ type LeadWorkSyncTestInboxMessage = {
|
||||||
messageKind?: string;
|
messageKind?: string;
|
||||||
taskRefs?: LeadWorkSyncTestTaskRef[];
|
taskRefs?: LeadWorkSyncTestTaskRef[];
|
||||||
};
|
};
|
||||||
|
type LeadRelayPriorityTestInboxMessage = LeadWorkSyncTestInboxMessage & {
|
||||||
|
source?: string;
|
||||||
|
workSyncIntent?: string;
|
||||||
|
};
|
||||||
|
type LeadRelayPriorityTestRun = ReturnType<typeof createMemberSpawnRun> & {
|
||||||
|
leadRelayCapture?: { resolveOnce(text: string): void } | null;
|
||||||
|
};
|
||||||
|
type LeadRelayPriorityServiceHarness = {
|
||||||
|
runs: Map<string, LeadRelayPriorityTestRun>;
|
||||||
|
aliveRunByTeam: Map<string, string>;
|
||||||
|
configReader: {
|
||||||
|
getConfig(teamName: string): Promise<Record<string, unknown>>;
|
||||||
|
};
|
||||||
|
inboxReader: {
|
||||||
|
getMessagesFor(
|
||||||
|
teamName: string,
|
||||||
|
inboxName: string
|
||||||
|
): Promise<LeadRelayPriorityTestInboxMessage[]>;
|
||||||
|
};
|
||||||
|
confirmSameTeamNativeMatches(input: unknown): Promise<{
|
||||||
|
nativeMatchedMessageIds: Set<string>;
|
||||||
|
persisted: boolean;
|
||||||
|
}>;
|
||||||
|
markInboxMessagesRead(
|
||||||
|
teamName: string,
|
||||||
|
inboxName: string,
|
||||||
|
messages: LeadRelayPriorityTestInboxMessage[]
|
||||||
|
): Promise<void>;
|
||||||
|
resolveControlApiBaseUrl(): Promise<string | null>;
|
||||||
|
scheduleLeadInboxFollowUpRelay(teamName: string): void;
|
||||||
|
sendMessageToRun(run: LeadRelayPriorityTestRun, message: string): Promise<void>;
|
||||||
|
};
|
||||||
type LeadWorkSyncReadCommitTestHarness = {
|
type LeadWorkSyncReadCommitTestHarness = {
|
||||||
hasAcceptedLeadWorkSyncReport(input: { teamName: string; leadName: string }): Promise<boolean>;
|
hasAcceptedLeadWorkSyncReport(input: { teamName: string; leadName: string }): Promise<boolean>;
|
||||||
getLeadRelayReadCommitBatch(input: {
|
getLeadRelayReadCommitBatch(input: {
|
||||||
|
|
@ -11798,6 +11830,88 @@ describe('TeamProvisioningService', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('prioritizes OpenCode work-sync nudges over older ordinary inbox rows', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
|
||||||
|
ok: true,
|
||||||
|
providerId: 'opencode',
|
||||||
|
memberName: String(input.memberName),
|
||||||
|
sessionId: 'oc-session-bob',
|
||||||
|
runtimePromptMessageId: `runtime-${String(input.messageId)}`,
|
||||||
|
prePromptCursor: 'cursor-before',
|
||||||
|
responseObservation: {
|
||||||
|
state: 'responded_non_visible_tool' as const,
|
||||||
|
deliveredUserMessageId: `oc-user-${String(input.messageId)}`,
|
||||||
|
assistantMessageId: `oc-assistant-${String(input.messageId)}`,
|
||||||
|
toolCallNames:
|
||||||
|
input.messageKind === 'member_work_sync_nudge'
|
||||||
|
? ['member_work_sync_status', 'member_work_sync_report']
|
||||||
|
: ['task_get'],
|
||||||
|
visibleMessageToolCallId: null,
|
||||||
|
visibleReplyMessageId: null,
|
||||||
|
visibleReplyCorrelation: null,
|
||||||
|
latestAssistantPreview: null,
|
||||||
|
reason: null,
|
||||||
|
},
|
||||||
|
diagnostics: [],
|
||||||
|
}));
|
||||||
|
await configureOpenCodeBobDeliveryService({ svc, sendMessageToMember });
|
||||||
|
svc.setMemberWorkSyncAcceptedReportChecker(async () => true);
|
||||||
|
|
||||||
|
const inboxDir = path.join(tempTeamsBase, 'team-a', 'inboxes');
|
||||||
|
await fsPromises.mkdir(inboxDir, { recursive: true });
|
||||||
|
await fsPromises.writeFile(
|
||||||
|
path.join(inboxDir, 'bob.json'),
|
||||||
|
`${JSON.stringify(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
from: 'team-lead',
|
||||||
|
to: 'bob',
|
||||||
|
text: 'Older ordinary follow-up.',
|
||||||
|
timestamp: '2026-04-25T09:00:00.000Z',
|
||||||
|
read: false,
|
||||||
|
messageId: 'msg-ordinary-old',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'system',
|
||||||
|
to: 'bob',
|
||||||
|
text: 'Work sync check for #task-1.',
|
||||||
|
timestamp: '2026-04-25T10:00:00.000Z',
|
||||||
|
read: false,
|
||||||
|
messageId: 'msg-work-sync-priority',
|
||||||
|
source: 'system_notification',
|
||||||
|
messageKind: 'member_work_sync_nudge',
|
||||||
|
workSyncIntent: 'agenda_sync',
|
||||||
|
taskRefs: [
|
||||||
|
{
|
||||||
|
taskId: 'task-1',
|
||||||
|
displayId: 'task-1',
|
||||||
|
teamName: 'team-a',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}\n`,
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(svc.relayOpenCodeMemberInboxMessages('team-a', 'bob')).resolves.toMatchObject({
|
||||||
|
attempted: 1,
|
||||||
|
delivered: 1,
|
||||||
|
failed: 0,
|
||||||
|
relayed: 1,
|
||||||
|
});
|
||||||
|
expect(sendMessageToMember).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sendMessageToMember).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
messageId: 'msg-work-sync-priority',
|
||||||
|
messageKind: 'member_work_sync_nudge',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('retries OpenCode direct asks after non-visible tool activity with an explicit retry header', async () => {
|
it('retries OpenCode direct asks after non-visible tool activity with an explicit retry header', async () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
|
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
|
||||||
|
|
@ -12536,7 +12650,7 @@ describe('TeamProvisioningService', () => {
|
||||||
responsePending: true,
|
responsePending: true,
|
||||||
responseState: 'prompt_delivered_no_assistant_message',
|
responseState: 'prompt_delivered_no_assistant_message',
|
||||||
ledgerStatus: 'retry_scheduled',
|
ledgerStatus: 'retry_scheduled',
|
||||||
reason: 'prompt_delivered_no_assistant_message',
|
reason: 'member_work_sync_report_required',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -24353,6 +24467,114 @@ describe('TeamProvisioningService', () => {
|
||||||
expect(readCommitBatch).toEqual([normalMessage]);
|
expect(readCommitBatch).toEqual([normalMessage]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('prioritizes lead work-sync nudges without mixing them into user-visible batches', async () => {
|
||||||
|
const teamName = 'lead-work-sync-priority-team';
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const harness = svc as unknown as LeadRelayPriorityServiceHarness;
|
||||||
|
const run = createMemberSpawnRun({
|
||||||
|
teamName,
|
||||||
|
expectedMembers: ['alice'],
|
||||||
|
}) as LeadRelayPriorityTestRun;
|
||||||
|
run.child = { pid: 123 };
|
||||||
|
run.processKilled = false;
|
||||||
|
run.cancelRequested = false;
|
||||||
|
run.provisioningComplete = true;
|
||||||
|
|
||||||
|
const oldUserMessages = Array.from({ length: 10 }, (_, index) => {
|
||||||
|
const suffix = String(index + 1).padStart(2, '0');
|
||||||
|
return {
|
||||||
|
from: 'user',
|
||||||
|
to: 'team-lead',
|
||||||
|
text: `Older user request ${suffix}.`,
|
||||||
|
timestamp: `2026-04-25T09:${suffix}:00.000Z`,
|
||||||
|
messageId: `msg-user-${suffix}`,
|
||||||
|
source: 'user_sent',
|
||||||
|
read: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const workSyncMessage = {
|
||||||
|
from: 'system',
|
||||||
|
to: 'team-lead',
|
||||||
|
text: 'Work sync required for task-1.',
|
||||||
|
timestamp: '2026-04-25T10:00:00.000Z',
|
||||||
|
messageId: 'msg-work-sync-priority',
|
||||||
|
source: 'system_notification',
|
||||||
|
messageKind: 'member_work_sync_nudge',
|
||||||
|
workSyncIntent: 'agenda_sync',
|
||||||
|
taskRefs: [{ taskId: 'task-1', displayId: 'task-1', teamName }],
|
||||||
|
read: false,
|
||||||
|
};
|
||||||
|
const ordinarySystemMessage = {
|
||||||
|
from: 'system',
|
||||||
|
to: 'team-lead',
|
||||||
|
text: 'Routine system notification.',
|
||||||
|
timestamp: '2026-04-25T09:59:00.000Z',
|
||||||
|
messageId: 'msg-system-routine',
|
||||||
|
source: 'system_notification',
|
||||||
|
read: false,
|
||||||
|
};
|
||||||
|
const inboxMessages: LeadRelayPriorityTestInboxMessage[] = [
|
||||||
|
...oldUserMessages,
|
||||||
|
ordinarySystemMessage,
|
||||||
|
workSyncMessage,
|
||||||
|
];
|
||||||
|
let deliveredPrompt = '';
|
||||||
|
const recoveryScheduler = vi.fn(async () => ({
|
||||||
|
scheduled: true,
|
||||||
|
reason: 'scheduled',
|
||||||
|
}));
|
||||||
|
const sendMessageToRun = vi.fn(
|
||||||
|
async (targetRun: LeadRelayPriorityTestRun, message: string) => {
|
||||||
|
deliveredPrompt = message;
|
||||||
|
targetRun.leadRelayCapture?.resolveOnce('');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
harness.runs.set(run.runId, run);
|
||||||
|
harness.aliveRunByTeam.set(teamName, run.runId);
|
||||||
|
harness.configReader = {
|
||||||
|
getConfig: vi.fn(async () => ({
|
||||||
|
projectPath: '/repo',
|
||||||
|
members: [
|
||||||
|
{ name: 'team-lead', agentType: 'team-lead', role: 'Team Lead' },
|
||||||
|
{ name: 'alice', role: 'Developer' },
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
vi.spyOn(harness.inboxReader, 'getMessagesFor').mockResolvedValue(inboxMessages);
|
||||||
|
harness.confirmSameTeamNativeMatches = vi.fn(async () => ({
|
||||||
|
nativeMatchedMessageIds: new Set<string>(),
|
||||||
|
persisted: true,
|
||||||
|
}));
|
||||||
|
harness.markInboxMessagesRead = vi.fn(async () => undefined);
|
||||||
|
harness.resolveControlApiBaseUrl = vi.fn(async () => null);
|
||||||
|
harness.scheduleLeadInboxFollowUpRelay = vi.fn();
|
||||||
|
harness.sendMessageToRun = sendMessageToRun;
|
||||||
|
svc.setMemberWorkSyncProofMissingRecoveryScheduler(recoveryScheduler);
|
||||||
|
|
||||||
|
await expect(svc.relayLeadInboxMessages(teamName)).resolves.toBe(1);
|
||||||
|
|
||||||
|
expect(sendMessageToRun).toHaveBeenCalledTimes(1);
|
||||||
|
const messagesSection = deliveredPrompt.slice(deliveredPrompt.indexOf('Messages:'));
|
||||||
|
expect(messagesSection).toContain('1) From: system');
|
||||||
|
expect(messagesSection).toContain('Message kind: member_work_sync_nudge');
|
||||||
|
expect(messagesSection).toContain('Work sync required for task-1.');
|
||||||
|
expect(messagesSection).not.toContain('Older user request 01.');
|
||||||
|
expect(messagesSection).not.toContain('Older user request 10.');
|
||||||
|
expect(messagesSection).not.toContain('Routine system notification.');
|
||||||
|
expect(deliveredPrompt).toContain(
|
||||||
|
'Plain text reply visibility for this batch: internal lead activity only.'
|
||||||
|
);
|
||||||
|
expect(recoveryScheduler).toHaveBeenCalledWith({
|
||||||
|
teamName,
|
||||||
|
memberName: 'team-lead',
|
||||||
|
originalMessageId: 'msg-work-sync-priority',
|
||||||
|
taskRefs: [{ taskId: 'task-1', displayId: 'task-1', teamName }],
|
||||||
|
reason: 'lead_member_work_sync_report_required',
|
||||||
|
});
|
||||||
|
expect(harness.scheduleLeadInboxFollowUpRelay).toHaveBeenCalledWith(teamName);
|
||||||
|
});
|
||||||
|
|
||||||
it('read-commits lead work-sync inbox rows after accepted report proof', async () => {
|
it('read-commits lead work-sync inbox rows after accepted report proof', async () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
const harness = leadWorkSyncReadCommitHarness(svc);
|
const harness = leadWorkSyncReadCommitHarness(svc);
|
||||||
|
|
|
||||||
|
|
@ -228,11 +228,15 @@ describe('useRuntimeProviderManagement', () => {
|
||||||
const root = createRoot(host);
|
const root = createRoot(host);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
root.render(React.createElement(ConfigurableHarness, { enabled: true }));
|
root.render(React.createElement(ConfigurableHarness, { enabled: true }));
|
||||||
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
await vi.waitFor(() => {
|
await act(async () => {
|
||||||
expect(state?.error ?? '').toContain('wrong runtime binary');
|
await vi.waitFor(() => {
|
||||||
|
expect(state?.error ?? '').toContain('wrong runtime binary');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(state?.errorDiagnostics?.binaryPath).toBe('/opt/homebrew/bin/opencode');
|
expect(state?.errorDiagnostics?.binaryPath).toBe('/opt/homebrew/bin/opencode');
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue