Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ test('resolves WorkHub coordination through the dedicated Host operation', async
{ sessionId: 'maka_workhub_coordination' },
{ candidateSetId: `sha256:${'a'.repeat(64)}`, candidates: [] },
{ disposition: 'answer_here', coordinationTurnId: 'action-turn' },
{ turnId: 'summary-turn' },
]);

assert.deepEqual(await client.resolveWorkHubCoordinationSession(), {
Expand All @@ -118,14 +117,6 @@ test('resolves WorkHub coordination through the dedicated Host operation', async
}),
{ disposition: 'answer_here', coordinationTurnId: 'action-turn' },
);
assert.deepEqual(
await client.recordWorkHubCoordination({
turnId: 'summary-turn',
userText: 'Request',
assistantText: 'Summary',
}),
{ turnId: 'summary-turn' },
);
assert.deepEqual(requests, [
{ operation: 'workhub.coordination.resolve', input: {} },
{ operation: 'workhub.coordination.candidates', input: {} },
Expand All @@ -137,14 +128,7 @@ test('resolves WorkHub coordination through the dedicated Host operation', async
proposal: { disposition: 'answer_here' },
},
},
{
operation: 'workhub.coordination.record',
input: {
turnId: 'summary-turn',
userText: 'Request',
assistantText: 'Summary',
},
},

]);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import { registerRuntimeHostWorkHubIpc } from '../runtime-host-workhub-ipc-main.
test('projects WorkHub coordination resolution through its dedicated IPC domain', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
let resolveCalls = 0;
const records: unknown[] = [];
const actions: unknown[] = [];
const changes: unknown[] = [];
const createdSessionId = 'runtime-created-session';
Expand All @@ -35,14 +34,6 @@ test('projects WorkHub coordination resolution through its dedicated IPC domain'
resolveCalls += 1;
return { sessionId: 'maka_workhub_coordination' };
},
recordWorkHubCoordination: async (input: {
turnId: string;
userText: string;
assistantText: string;
}) => {
records.push(input);
return { turnId: input.turnId };
},
listWorkHubCoordinationCandidates: async () => ({
candidateSetId: `sha256:${'a'.repeat(64)}`,
candidates: [],
Expand Down Expand Up @@ -74,19 +65,7 @@ test('projects WorkHub coordination resolution through its dedicated IPC domain'
assert.ok(handler);
assert.deepEqual(await handler({}), { sessionId: 'maka_workhub_coordination' });
assert.equal(resolveCalls, 1);
assert.deepEqual(
await handlers.get('workhub:record')?.({}, {
turnId: 'record',
userText: 'Request',
assistantText: 'Summary',
}),
{ turnId: 'record' },
);
assert.deepEqual(records, [{
turnId: 'record',
userText: 'Request',
assistantText: 'Summary',
}]);
assert.equal(handlers.has('workhub:record'), false);
assert.deepEqual(await handlers.get('workhub:candidates')?.({}), {
candidateSetId: `sha256:${'a'.repeat(64)}`,
candidates: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export function createWorkHubController({
...(routingStrategy ? { routingStrategy } : {}),
coordination: {
open: async (handler) => { handler(transcript); return { close: async () => undefined }; },
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => {
const candidates = (await sessions.list())
.filter((entry) => entry.kind === 'ordinary' && !entry.archived)
Expand Down
47 changes: 22 additions & 25 deletions apps/desktop/src/main/__tests__/workhub-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ test('conversation acknowledges a durable assignment before projecting target ex
handler([assignment]);
return { close: async () => undefined };
},
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => ({ candidateSetId: `sha256:${'a'.repeat(64)}`, candidates: [] }),
act: async () => ({ disposition: 'answer_here', coordinationTurnId: 'unused' }),
},
Expand Down Expand Up @@ -157,7 +157,7 @@ test('conversation feedback never lets an older refresh overwrite newer target s
handler([assignment]);
return { close: async () => undefined };
},
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => ({ candidateSetId: `sha256:${'b'.repeat(64)}`, candidates: [] }),
act: async () => ({ disposition: 'answer_here', coordinationTurnId: 'unused' }),
},
Expand Down Expand Up @@ -199,7 +199,7 @@ test('direct stop bypasses routing candidates and preserves a not_owned delegati
handler([coordinationAssignmentTurn()]);
return { close: async () => undefined };
},
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => {
candidateReads += 1;
return { candidateSetId: `sha256:${'d'.repeat(64)}`, candidates: [] };
Expand Down Expand Up @@ -255,7 +255,7 @@ test('an anaphoric stop asks for a fresh named imperative without offering a rou
handler([]);
return { close: async () => undefined };
},
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => assert.fail('stop clarification must not read route candidates'),
act: async () => assert.fail('anaphoric stop must not reach the Action Gate'),
},
Expand All @@ -282,7 +282,7 @@ test('a named resume submits and reports what the Host did', async () => {
handler([]);
return { close: async () => undefined };
},
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => ({
candidateSetId: `sha256:${'e'.repeat(64)}`,
candidates: [{ candidateRef: 'candidate-payments', sessionId: 'payments', sessionName: 'Payments', latestDelegationActionId: 'source-action', workspace: { target: { kind: 'host_path' as const, path: '/workspace/payments' }, hostCwd: '/workspace/payments' }, state: 'active' as const, updatedAt: 1 }],
Expand Down Expand Up @@ -327,7 +327,7 @@ test('an anaphoric resume asks for a named work item', async () => {
handler([]);
return { close: async () => undefined };
},
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => assert.fail('resume clarification must not read route candidates'),
act: async () => assert.fail('anaphoric resume must not reach the Action Gate'),
},
Expand All @@ -354,7 +354,7 @@ test('a resume the Host will not admit becomes its clarification', async () => {
handler([]);
return { close: async () => undefined };
},
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => ({
candidateSetId: `sha256:${'e'.repeat(64)}`,
candidates: [{ candidateRef: 'candidate-payments', sessionId: 'payments', sessionName: 'Payments', latestDelegationActionId: 'source-action', workspace: { target: { kind: 'host_path' as const, path: '/workspace/payments' }, hostCwd: '/workspace/payments' }, state: 'active' as const, updatedAt: 1 }],
Expand Down Expand Up @@ -392,7 +392,7 @@ test('a resume identity conflict is not mislabeled as a missing target', async (
handler([]);
return { close: async () => undefined };
},
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => ({
candidateSetId: `sha256:${'e'.repeat(64)}`,
candidates: [{ candidateRef: 'candidate-payments', sessionId: 'payments', sessionName: 'Payments', latestDelegationActionId: 'source-action', workspace: { target: { kind: 'host_path' as const, path: '/workspace/payments' }, hostCwd: '/workspace/payments' }, state: 'active' as const, updatedAt: 1 }],
Expand All @@ -419,7 +419,7 @@ test('a Runtime Host without safe-boundary resume explains why it cannot resume'
handler([]);
return { close: async () => undefined };
},
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => ({
candidateSetId: `sha256:${'e'.repeat(64)}`,
candidates: [{ candidateRef: 'candidate-payments', sessionId: 'payments', sessionName: 'Payments', latestDelegationActionId: 'source-action', workspace: { target: { kind: 'host_path' as const, path: '/workspace/payments' }, hostCwd: '/workspace/payments' }, state: 'active' as const, updatedAt: 1 }],
Expand Down Expand Up @@ -453,7 +453,7 @@ test('a recovering Runtime Host tells the user to retry resume', async () => {
handler([]);
return { close: async () => undefined };
},
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => ({
candidateSetId: `sha256:${'e'.repeat(64)}`,
candidates: [{ candidateRef: 'candidate-payments', sessionId: 'payments', sessionName: 'Payments', latestDelegationActionId: 'source-action', workspace: { target: { kind: 'host_path' as const, path: '/workspace/payments' }, hostCwd: '/workspace/payments' }, state: 'active' as const, updatedAt: 1 }],
Expand Down Expand Up @@ -484,7 +484,7 @@ test('a named stop reports the Gate refusal instead of judging the target itself
handler([]);
return { close: async () => undefined };
},
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => assert.fail('stop clarification must not read route candidates'),
act: async () => {
submitted += 1;
Expand Down Expand Up @@ -518,7 +518,7 @@ test('a stop that fails for any other reason is a fault, not a clarification', a
handler([]);
return { close: async () => undefined };
},
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => assert.fail('stop clarification must not read route candidates'),
act: async () => {
throw new WorkHubCoordinationFailure('persistence_failed', 'WorkHub stop state is unavailable');
Expand Down Expand Up @@ -548,7 +548,7 @@ test('stop-shaped ordinary work routes normally instead of looping on clarificat
handler([]);
return { close: async () => undefined };
},
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => ({
candidateSetId: `sha256:${'e'.repeat(64)}`,
candidates: [{
Expand Down Expand Up @@ -1631,7 +1631,7 @@ test('submit keeps unmatched non-executable conversation in WorkHub', async () =
sessions,
coordination: {
open: async () => ({ close: async () => undefined }),
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => ({
candidateSetId: `sha256:${'a'.repeat(64)}`,
candidates: [],
Expand Down Expand Up @@ -1677,7 +1677,7 @@ test('production submission delegates only through the Runtime-owned candidate r
sessions,
coordination: {
open: async () => ({ close: async () => undefined }),
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => ({
candidateSetId: `sha256:${'b'.repeat(64)}`,
candidates: [{
Expand Down Expand Up @@ -1731,7 +1731,7 @@ test('production retry reaches durable Action Gate replay while target is waitin
sessions,
coordination: {
open: async () => ({ close: async () => undefined }),
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => ({
candidateSetId: `sha256:${'c'.repeat(64)}`,
candidates: [{
Expand Down Expand Up @@ -1776,7 +1776,7 @@ test('production sends an explicit correction as a linked replacement', async ()
sessions,
coordination: {
open: async () => ({ close: async () => undefined }),
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => ({
candidateSetId: `sha256:${'d'.repeat(64)}`,
candidates: [
Expand Down Expand Up @@ -1901,7 +1901,7 @@ test('production natural-language corrections retain the prior delegation link',
sessions,
coordination: {
open: async () => ({ close: async () => undefined }),
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => ({
candidateSetId,
candidates: candidates.map((candidate) => {
Expand Down Expand Up @@ -1999,7 +1999,7 @@ test('production correction-shaped creation stays create_new without an existing
sessions: port([]),
coordination: {
open: async () => ({ close: async () => undefined }),
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => ({
candidateSetId: `sha256:${'c'.repeat(64)}`,
candidates: [],
Expand Down Expand Up @@ -2030,9 +2030,6 @@ test('production clarification is persisted through the typed Action Gate dispos
sessions: port([]),
coordination: {
open: async () => ({ close: async () => undefined }),
record: async () => {
throw new Error('legacy summary recording must not persist clarification');
},
candidates: async () => ({
candidateSetId: `sha256:${'c'.repeat(64)}`,
candidates: [],
Expand All @@ -2047,7 +2044,7 @@ test('production clarification is persisted through the typed Action Gate dispos
},
});

assert.deepEqual(await controller.recordConversationTurn({
assert.deepEqual(await controller.requestClarification({
turnId: 'clarification-action',
userText: '继续稳定性问题',
assistantText: '请选择目标 Session',
Expand All @@ -2073,7 +2070,7 @@ test('production creation leaves Session identity and workspace authority to mai
sessions,
coordination: {
open: async () => ({ close: async () => undefined }),
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => ({
candidateSetId: `sha256:${'c'.repeat(64)}`,
candidates: [],
Expand Down Expand Up @@ -3212,7 +3209,7 @@ for (const createStrategy of [createWorkHubR24RoutingStrategy, () => createWorkH
routingStrategy,
coordination: {
open: async () => ({ close: async () => undefined }),
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => ({ candidateSetId: `sha256:${'e'.repeat(64)}`, candidates: [{ candidateRef: 'payments-ref', sessionId: 'payments', sessionName: 'Payments', latestDelegationActionId: 'source-action', workspace: { target: { kind: 'host_path' as const, path: '/workspace/payments' }, hostCwd: '/workspace/payments' }, state: 'active' as const, updatedAt: 1 }] }),
act: async (input) => {
assert.equal(input.proposal.disposition, 'resume_work');
Expand Down
31 changes: 28 additions & 3 deletions apps/desktop/src/main/__tests__/workhub-session-port.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ test('Coordination transcript adapter never replays history and completes only t
};
},
},
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => assert.fail('conversation open must not read route candidates'),
act: async () => ({
ok: true,
Expand Down Expand Up @@ -532,7 +532,7 @@ test('Coordination transcript adapter retries latest-record completion in the sa
};
},
},
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => assert.fail('conversation open must not read route candidates'),
act: async () => ({
ok: true,
Expand Down Expand Up @@ -629,7 +629,7 @@ test('Coordination transcript adapter ignores a stale latest-record failure afte
};
},
},
record: async (input) => ({ turnId: input.turnId }),

candidates: async () => assert.fail('conversation open must not read route candidates'),
act: async () => ({
ok: true,
Expand Down Expand Up @@ -1158,3 +1158,28 @@ test('desktop adapter derives stable origin evidence from the existing Session l
assert.deepEqual(second, first);
assert.equal(reads, 1);
});


test('admitted clarification and resume project receipts without assistant messages', () => {
const turns = projectWorkHubCoordinationTurns([
{ type: 'user', id: 'failed-user', turnId: 'request', ts: 0, text: 'Which task?' },
{ type: 'turn_state', id: 'failed-state', turnId: 'request', ts: 1, status: 'failed' },
{ type: 'user', id: 'u', turnId: 'retry-turn', ts: 1, text: 'Which task?' },
{ type: 'workhub_coordination', kind: 'action_receipt', schemaVersion: 1,
id: 'receipt', turnId: 'retry-turn', ts: 2,
receipt: { actionId: 'request', userText: 'Which task?', clarification: 'Please name a task.',
result: { disposition: 'clarify', coordinationTurnId: 'retry-turn' } } },
{ type: 'turn_state', id: 'done', turnId: 'retry-turn', ts: 3, status: 'completed' },
{ type: 'workhub_coordination', kind: 'action_receipt', schemaVersion: 1,
id: 'resume-receipt', turnId: 'resume-turn', ts: 4,
receipt: { actionId: 'resume', userText: 'Resume Payments',
result: { disposition: 'resume_work', outcome: 'resume_started', targetSessionId: 'payments', targetTurnId: 'target-turn' } } },
{ type: 'turn_state', id: 'resume-done', turnId: 'resume-turn', ts: 5, status: 'completed' },
]);
assert.equal(turns.length, 2);
assert.equal(turns[0]?.turnId, 'request');
assert.equal(turns[0]?.result, 'Please name a task.');
assert.equal(turns[0]?.state, 'completed');
assert.equal(turns[1]?.result, undefined);
assert.deepEqual(turns[1]?.resume, { disposition: 'resume_work', outcome: 'resume_started', targetSessionId: 'payments', targetTurnId: 'target-turn' });
});
Loading
Loading