Skip to content
Merged
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
30 changes: 30 additions & 0 deletions apps/desktop/src/main/__tests__/desktop-session-projection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { EMPTY_USAGE_PROVENANCE } from '@maka/core/usage-ledger-merge';
import {
projectDesktopSessionEvent,
projectDesktopSessionSummary,
projectDesktopStoredMessage,
projectDesktopTurnRecord,
projectDesktopUsageStats,
} from '../../shared/desktop-session-projection.js';
Expand Down Expand Up @@ -199,6 +200,35 @@ test('projects only present Usage Session ids into the Desktop host namespace',
assert.equal(projected.logs[1]?.sessionId, undefined);
});

test('projects durable WorkHub delegation targets into the Desktop host namespace', () => {
const projected = projectDesktopStoredMessage(
{ hostId: 'remote-root' },
{
type: 'workhub_coordination',
id: 'delegation-assignment-message',
turnId: 'coordination-turn',
ts: 2,
schemaVersion: 1,
kind: 'delegation_assigned',
actionId: 'action-id',
actionFingerprint: `sha256:${'a'.repeat(64)}`,
coordinationTurnId: 'coordination-turn',
targetSessionId: 'payments',
disposition: 'delegate_existing',
userText: 'Continue payment work',
delegationId: 'delegation-id',
targetTurnId: 'payments-turn',
targetMessageId: 'payments-message',
targetSessionName: 'Payments',
},
);

assert.equal(projected.type, 'workhub_coordination');
if (projected.type === 'workhub_coordination') {
assert.equal(projected.targetSessionId, JSON.stringify(['remote-root', 'payments']));
}
});

function summary(id: string): SessionSummary {
return {
id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import assert from 'node:assert/strict';
import test from 'node:test';
import { RuntimeHostOperationError } from '@maka/runtime-host/client';
import { registerRuntimeHostWorkHubIpc } from '../runtime-host-workhub-ipc-main.js';

test('projects WorkHub coordination resolution through its dedicated IPC domain', async () => {
Expand Down Expand Up @@ -111,9 +112,12 @@ test('projects WorkHub coordination resolution through its dedicated IPC domain'
},
}),
{
disposition: 'create_new',
targetSessionId: createdSessionId,
targetTurnId: 'created-turn',
ok: true,
result: {
disposition: 'create_new',
targetSessionId: createdSessionId,
targetTurnId: 'created-turn',
},
},
);
assert.deepEqual(actions, [{
Expand All @@ -126,3 +130,43 @@ test('projects WorkHub coordination resolution through its dedicated IPC domain'
}]);
assert.deepEqual(changes, [{ reason: 'created', sessionId: createdSessionId }]);
});

test('serializes typed WorkHub action failures across Electron IPC', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
registerRuntimeHostWorkHubIpc(
{
actWorkHubCoordination: async () => {
throw new RuntimeHostOperationError(
'workhub.coordination.act',
'operation_conflict',
'WorkHub action is permanently abandoned',
);
},
} as never,
{
handle: (channel: string, handler: (...args: unknown[]) => unknown) => {
handlers.set(channel, handler);
},
} as never,
{
resolveCreateProject: async () => ({ kind: 'host_path', path: '/workspace' }),
emitSessionsChanged: () => undefined,
},
);

assert.deepEqual(
await handlers.get('workhub:act')?.({}, {
actionId: 'abandoned-action',
userText: 'Continue payment work',
candidateSetId: `sha256:${'a'.repeat(64)}`,
proposal: { disposition: 'delegate_existing', candidateRef: 'candidate' },
}),
{
ok: false,
error: {
code: 'operation_conflict',
message: 'WorkHub action is permanently abandoned',
},
},
);
});
48 changes: 48 additions & 0 deletions apps/desktop/src/main/__tests__/workhub-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2515,6 +2515,54 @@ test('production submission delegates only through the Runtime-owned candidate r
}]);
});

test('production retry reaches durable Action Gate replay while target is waiting', async () => {
const actions: unknown[] = [];
const sessions = port([
session('payment', { state: 'waiting_for_user' }),
]);
const controller = createGatedWorkHubController({
sessions,
coordination: {
open: async () => ({ close: async () => undefined }),
answer: async (input) => ({ turnId: input.turnId }),
record: async (input) => ({ turnId: input.turnId }),
candidates: async () => ({
candidateSetId: `sha256:${'c'.repeat(64)}`,
candidates: [{
candidateRef: 'candidate-payment',
sessionId: 'payment',
sessionName: 'payment',
workspace: {
target: { kind: 'host_path', path: '/workspace/payment' },
hostCwd: '/workspace/payment',
},
state: 'waiting_for_user',
updatedAt: 2,
}],
}),
act: async (input) => {
actions.push(input);
return {
disposition: 'delegate_existing',
targetSessionId: 'payment',
targetTurnId: 'already-committed-turn',
};
},
},
});

const result = await controller.submit({
requestId: 'summary-recovery-action',
text: '继续支付工作',
explicitTarget: { sessionId: 'payment' },
retryAction: true,
});

assert.equal(result.kind, 'submitted');
assert.equal(result.kind === 'submitted' ? result.turnId : undefined, 'already-committed-turn');
assert.equal(actions.length, 1);
});

test('production defers destructive correction until persistent delegation exists', async () => {
const actions: unknown[] = [];
const sessions = port([session('source'), session('target')]);
Expand Down
35 changes: 33 additions & 2 deletions apps/desktop/src/main/__tests__/workhub-session-port.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,41 @@ test('projects the durable Coordination transcript into the WorkHub conversation
status: 'completed',
partialOutputRetained: true,
},
{
type: 'workhub_coordination',
id: 'assignment-1',
turnId: 'action-1',
ts: 20,
schemaVersion: 1,
kind: 'delegation_assigned',
actionId: 'action-1',
actionFingerprint: `sha256:${'a'.repeat(64)}`,
coordinationTurnId: 'action-1',
targetSessionId: 'payments',
targetSessionName: 'Payments',
targetTurnId: 'payments-turn',
targetMessageId: 'payments-message',
delegationId: 'payments-delegation',
disposition: 'delegate_existing',
userText: 'Continue payments',
},
]), [{
messageId: 'user-1',
turnId: 'turn-1',
text: 'What is next?',
result: 'Slice 3 is next.',
state: 'completed',
updatedAt: 11,
}, {
messageId: 'assignment-1',
turnId: 'action-1',
text: 'Continue payments',
state: 'completed',
assignment: {
targetSessionId: 'payments',
targetSessionName: 'Payments',
},
updatedAt: 20,
}]);
});

Expand Down Expand Up @@ -168,8 +196,11 @@ test('Coordination transcript adapter emits an initial empty ready snapshot and
candidates: [],
}),
act: async () => ({
disposition: 'answer_here',
coordinationTurnId: 'coordination-turn',
ok: true,
result: {
disposition: 'answer_here',
coordinationTurnId: 'coordination-turn',
},
}),
});

Expand Down
Loading