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;
|
||||
}
|
||||
|
||||
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 {
|
||||
private readonly runtimeLaneCoordinator = createTeamRuntimeLaneCoordinator();
|
||||
private readonly providerConnectionService = ProviderConnectionService.getInstance();
|
||||
|
|
@ -23567,13 +23608,7 @@ export class TeamProvisioningService {
|
|||
if (typeof message.text !== 'string' || message.text.trim().length === 0) return false;
|
||||
return this.hasStableMessageId(message);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
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);
|
||||
})
|
||||
.sort(compareOpenCodeInboxRelayMessagesByPriority)
|
||||
.slice(0, 10);
|
||||
|
||||
let taskRefInferenceTasks: Promise<readonly TeamTask[]> | null = null;
|
||||
|
|
@ -24491,13 +24526,26 @@ export class TeamProvisioningService {
|
|||
if (actionableUnread.length === 0) return 0;
|
||||
|
||||
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)
|
||||
);
|
||||
const replyVisibility: 'user' | 'internal_activity' =
|
||||
userOriginatedUnread.length > 0 ? 'user' : 'internal_activity';
|
||||
const batchSource = userOriginatedUnread.length > 0 ? userOriginatedUnread : actionableUnread;
|
||||
const batchSource =
|
||||
priorityUnread.length > 0
|
||||
? priorityUnread
|
||||
: userOriginatedUnread.length > 0
|
||||
? userOriginatedUnread
|
||||
: prioritizedActionableUnread;
|
||||
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 hasPendingFollowUpRelay = unread.some(
|
||||
(message) => !batchIds.has(message.messageId) && !readOnlyIgnoredIds.has(message.messageId)
|
||||
|
|
@ -24540,7 +24588,7 @@ export class TeamProvisioningService {
|
|||
|
||||
const message = [
|
||||
`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.`,
|
||||
...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.`,
|
||||
|
|
|
|||
|
|
@ -220,6 +220,38 @@ type LeadWorkSyncTestInboxMessage = {
|
|||
messageKind?: string;
|
||||
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 = {
|
||||
hasAcceptedLeadWorkSyncReport(input: { teamName: string; leadName: string }): Promise<boolean>;
|
||||
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 () => {
|
||||
const svc = new TeamProvisioningService();
|
||||
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
|
||||
|
|
@ -12536,7 +12650,7 @@ describe('TeamProvisioningService', () => {
|
|||
responsePending: true,
|
||||
responseState: 'prompt_delivered_no_assistant_message',
|
||||
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]);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const svc = new TeamProvisioningService();
|
||||
const harness = leadWorkSyncReadCommitHarness(svc);
|
||||
|
|
|
|||
|
|
@ -228,11 +228,15 @@ describe('useRuntimeProviderManagement', () => {
|
|||
const root = createRoot(host);
|
||||
await act(async () => {
|
||||
root.render(React.createElement(ConfigurableHarness, { enabled: true }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(state?.error ?? '').toContain('wrong runtime binary');
|
||||
await act(async () => {
|
||||
await vi.waitFor(() => {
|
||||
expect(state?.error ?? '').toContain('wrong runtime binary');
|
||||
});
|
||||
});
|
||||
|
||||
expect(state?.errorDiagnostics?.binaryPath).toBe('/opt/homebrew/bin/opencode');
|
||||
|
||||
await act(async () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue