Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ test('projects WorkHub coordination resolution through its dedicated IPC domain'
listWorkHubCoordinationCandidates: async () => ({
candidateSetId: `sha256:${'a'.repeat(64)}`,
candidates: [],
delegations: [{ actionId: 'action-a', targetSessionId: 'session-a', sequence: 3 }],
}),
actWorkHubCoordination: async (input: unknown) => {
actions.push(input);
Expand Down Expand Up @@ -90,6 +91,7 @@ test('projects WorkHub coordination resolution through its dedicated IPC domain'
assert.deepEqual(await handlers.get('workhub:candidates')?.({}), {
candidateSetId: `sha256:${'a'.repeat(64)}`,
candidates: [],
delegations: [{ actionId: 'action-a', targetSessionId: 'session-a', sequence: 3 }],
});
assert.deepEqual(
await handlers.get('workhub:act')?.({}, {
Expand Down
118 changes: 117 additions & 1 deletion apps/desktop/src/main/__tests__/workhub-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { WorkHubCoordinationActInput } from '@maka/runtime-host/protocol';
import {
createWorkHubController as createGatedWorkHubController,
WORKHUB_ROUTING_STRATEGY_ID,
WorkHubCoordinationFailure,
type WorkHubSessionFacts,
type WorkHubSessionPort,
type WorkHubCoordinationTurn,
Expand All @@ -32,7 +33,6 @@ import {
createWorkHubRoutePolicy,
workHubNewSessionName,
} from '../../renderer/workhub-route-policy.js';
import { WorkHubCoordinationFailure } from '../../renderer/workhub-coordination-port.js';

const appShellUrl = [
new URL('../../renderer/app-shell.tsx', import.meta.url),
Expand Down Expand Up @@ -193,6 +193,14 @@ function createWorkHubController({ sessions }: { sessions: TestSessionPort }) {
targetSessionId: input.proposal.expects.targetSessionId,
};
}
if (input.proposal.disposition === 'resume_work') {
return {
disposition: 'resume_work',
outcome: 'resume_started',
targetSessionId: input.proposal.expects.targetSessionId,
targetTurnId: 'resumed-turn',
};
}
const target = candidateByRef.get(input.proposal.candidateRef);
if (!target) throw new Error('unknown test candidate');
const admitted = await sessions.submit(target.target, input.userText, input.actionId);
Expand Down Expand Up @@ -420,6 +428,114 @@ test('an anaphoric stop asks for a fresh named imperative without offering a rou
await handle.close();
});

test('a named resume submits and reports what the Host did', async () => {
const sessions = port([session('payments', { sessionName: 'Payments' })]);
const actions: WorkHubCoordinationActInput[] = [];
const controller = createGatedWorkHubController({
sessions,
coordination: {
open: async (handler) => {
handler([], []);
return { close: async () => undefined };
},
record: async (input) => ({ turnId: input.turnId }),
candidates: async () => assert.fail('a resume must not read route candidates'),
act: async (input) => {
actions.push(input);
return {
disposition: 'resume_work',
outcome: 'resume_started',
targetSessionId: 'payments',
targetTurnId: 'resumed-turn',
};
},
},
});
const handle = await controller.openConversation(() => undefined, () => undefined);

const result = await controller.submit({ requestId: 'resume-1', text: 'Resume Payments' });

assert.deepEqual(result, {
kind: 'resume',
strategyId: WORKHUB_ROUTING_STRATEGY_ID,
requestId: 'resume-1',
target: { sessionId: 'payments' },
outcome: 'resume_started',
targetTurnId: 'resumed-turn',
});
// The proposal names the Session and carries no confirmation: resume ends
// nothing, so it needs no authority a delegation did not already grant.
assert.deepEqual(actions, [{
actionId: 'resume-1',
userText: 'Resume Payments',
proposal: { disposition: 'resume_work', expects: { targetSessionId: 'payments' } },
}]);
await handle.close();
});

test('a resume the Host will not admit becomes its clarification', async () => {
const sessions = port([session('payments', { sessionName: 'Payments' })]);
const controller = createGatedWorkHubController({
sessions,
coordination: {
open: async (handler) => {
handler([], []);
return { close: async () => undefined };
},
record: async (input) => ({ turnId: input.turnId }),
candidates: async () => assert.fail('a resume must not read route candidates'),
act: async () => {
throw new WorkHubCoordinationFailure(
'operation_conflict',
'WorkHub has no active durable delegation to resume on that Session',
);
},
},
});
const handle = await controller.openConversation(() => undefined, () => undefined);

assert.deepEqual(await controller.submit({ requestId: 'resume-2', text: 'Resume Payments' }), {
kind: 'clarification',
strategyId: WORKHUB_ROUTING_STRATEGY_ID,
requestId: 'resume-2',
text: 'Resume Payments',
options: [],
reason: 'resume_target_unavailable',
});
await handle.close();
});

test('a Runtime Host without safe-boundary resume explains why it cannot resume', async () => {
const controller = createGatedWorkHubController({
sessions: port([session('payments', { sessionName: 'Payments' })]),
coordination: {
open: async (handler) => {
handler([], []);
return { close: async () => undefined };
},
record: async (input) => ({ turnId: input.turnId }),
candidates: async () => assert.fail('a resume must not read route candidates'),
act: async () => {
throw new WorkHubCoordinationFailure(
'operation_unavailable',
'Safe-boundary resume is disabled for this Runtime Host',
);
},
},
});
const handle = await controller.openConversation(() => undefined, () => undefined);

assert.deepEqual(await controller.submit({ requestId: 'resume-disabled', text: 'Resume Payments' }), {
kind: 'clarification',
strategyId: WORKHUB_ROUTING_STRATEGY_ID,
requestId: 'resume-disabled',
text: 'Resume Payments',
options: [],
reason: 'resume_target_unavailable',
});
await handle.close();
});

test('a named stop reports the Gate refusal instead of judging the target itself', async () => {
// The renderer no longer decides whether a Session can be stopped, so it
// submits and lets the Gate answer. Its refusal is the clarification, which
Expand Down
Loading