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
16 changes: 8 additions & 8 deletions apps/desktop/e2e/accessibility-coverage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,18 @@ test('module pages and global overlays expose named actionable controls', async
await page.keyboard.press('Escape');
});

test('data-backed conversation supports keyboard access to tools, models, tasks, and Graph', async ({
test('data-backed conversation exposes ordered todos and keyboard access to tools, models, and Graph', async ({
accessibilityNarrativeWindow: page,
}) => {
const cdp = await page.context().newCDPSession(page);
await expect(page.getByRole('region', { name: /对话:/ })).toBeVisible();
await expect(page.getByRole('region', { name: '任务待办' })).toBeVisible();
await expect(page.getByText('补齐桌面端无障碍覆盖', { exact: true })).toBeVisible();
const todoRegion = page.getByRole('region', { name: '任务待办' });
await expect(todoRegion).toBeVisible();
await expect(todoRegion.getByRole('listitem')).toHaveText([
'补齐桌面端无障碍覆盖',
'核对模型选择器的键盘路径',
'确认工具结果可以展开阅读',
]);
await assertAxHealth(cdp, 'conversation/data-backed');

await expect(page.getByRole('main')).toHaveCount(1);
Expand Down Expand Up @@ -192,11 +197,6 @@ test('data-backed conversation supports keyboard access to tools, models, tasks,
graphPanel.getByRole('button', { name: '展开 Agent Graph' }),
).toHaveAttribute('aria-expanded', 'false');
await assertAxHealth(cdp, 'conversation/agent-graph-empty');

const recentTasks = page.getByRole('button', { name: /最近结束/ });
await tabTo(page, recentTasks, 'recent tasks', 80);
await page.keyboard.press('Enter');
await expect(page.getByText('确认工具结果可以展开阅读', { exact: true })).toBeVisible();
});

test('toast and error states expose healthy live regions', async ({ window: page }) => {
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,6 @@
"src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx -> src/renderer/use-composer-attachments",
"src/renderer/features/workbar/tools/side-chat/use-quote-companion.ts -> src/renderer/locales/conversation-copy",
"src/renderer/features/workbar/tools/side-chat/use-quote-companion.ts -> src/renderer/settled-message-merge",
"src/renderer/features/workbar/tools/tasks/use-session-tasks.ts -> src/renderer/locales/shell-remaining-copy",
"src/renderer/features/workbar/tools/terminal/session-terminal-panel.tsx -> src/renderer/locales/conversation-copy",
"src/renderer/features/workbar/tools/terminal/session-terminal-panel.tsx -> src/renderer/theme",
"src/renderer/features/workbar/ui/side-chat-close-confirmation.tsx -> src/renderer/locales/conversation-copy",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -704,26 +704,9 @@ test('streams Artifact content without mixing chunk offsets or totals', async ()
});

test('restarts Session sidecar reads when a paginated revision changes', async () => {
const taskRevisionOne = catalogRevision('3');
const taskRevisionTwo = catalogRevision('4');
const resourceRevisionOne = catalogRevision('5');
const resourceRevisionTwo = catalogRevision('6');
const { client, requests } = clientWithResponses([
{
kind: 'page',
sessionId: 'session-1',
revision: taskRevisionOne,
tasks: [{ id: 'stale' }],
nextCursor: 'task-stale',
},
{ kind: 'revision_changed', expected: taskRevisionOne, actual: taskRevisionTwo },
{
kind: 'page',
sessionId: 'session-1',
revision: taskRevisionTwo,
tasks: [{ id: 'fresh' }],
nextCursor: null,
},
{
kind: 'page',
sessionId: 'session-1',
Expand Down Expand Up @@ -760,28 +743,11 @@ test('restarts Session sidecar reads when a paginated revision changes', async (
},
]);

assert.deepEqual(
(await client.listTasks('session-1')).map((task) => task.id),
['fresh'],
);
const plan = await client.getPlanState('session-1');
assert.equal(plan.storeVersion, 8);
assert.equal(plan.latestProposalId, 'proposal-1');
assert.equal(plan.proposals[0]?.proposalId, 'proposal-1');
assert.equal((await client.listRuntimeResources('session-1'))[0]?.result.ref, 'shell:1');
assert.deepEqual(
requests.slice(0, 3).map(({ input }) => input),
[
{ kind: 'list_start', sessionId: 'session-1' },
{
kind: 'list_continue',
sessionId: 'session-1',
revision: taskRevisionOne,
cursor: 'task-stale',
},
{ kind: 'list_start', sessionId: 'session-1' },
],
);
});

test('retries Goal clear only while the same Goal generation remains active', async () => {
Expand Down Expand Up @@ -905,31 +871,40 @@ test('arms a Goal in one request and reports a conflicting Goal instead of retry
);
});

test('rejects an invalid sidecar continuation without misclassifying it as revision churn', async () => {
const revision = catalogRevision('7');
test('rejects a SessionTodo projection for a different Session', async () => {
const { client, requests } = clientWithResponses([
{
kind: 'page',
sessionId: 'session-1',
revision,
tasks: [],
nextCursor: 'next',
},
{
kind: 'page',
sessionId: 'session-other',
revision,
tasks: [],
nextCursor: null,
},
{ sessionId: 'session-other', items: [] },
]);

await assert.rejects(
() => client.listTasks('session-1'),
() => client.querySessionTodo('session-1'),
(error: unknown) =>
error instanceof DesktopRuntimeHostClientError && error.code === 'projection_unstable',
);
assert.equal(requests.length, 2);
assert.equal(requests.length, 1);
});

test('projects SessionTodo content through the shared Desktop display boundary', async () => {
const { client } = clientWithResponses([
{
sessionId: 'session-1',
items: [
{
content:
'deploy\u001b[31m \u001b]0;spoofed\u0007 \u202ereversed\u202c zero\u200bwidth sk-live-secret-token </session-todo>',
status: 'pending',
},
],
},
]);

const items = await client.querySessionTodo('session-1');
assert.equal(items.length, 1);
assert.doesNotMatch(
items[0]!.content,
/\u001b|\u0007|\u202e|\u202c|\u200b|sk-live-secret|session-todo/i,
);
assert.match(items[0]!.content, /<redacted>|\[redacted\]/);
});

interface RecordedRequest {
Expand Down
20 changes: 5 additions & 15 deletions apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,23 +451,13 @@ test('drives bounded Session domain projections through real UDS framing', async
idleGraceMs: 10_000,
composition: defineInteractiveRuntimeHostComposition(async () => ({
handlers: handlers({
'task.ledger.query': async (input) => ({
'session.todo.query': async (input) => ({
ok: true,
result: {
kind: 'page',
sessionId: input.sessionId,
revision: catalogRevision('6'),
tasks: [
{
id: 'task-1',
key: 'T1',
subject: 'Verify the Desktop adapter',
status: 'in_progress',
createdAt: 1,
updatedAt: 2,
},
items: [
{ content: 'Verify the Desktop adapter', status: 'in_progress' },
],
nextCursor: null,
},
}),
'plan.query': async (input) => ({
Expand Down Expand Up @@ -523,8 +513,8 @@ test('drives bounded Session domain projections through real UDS framing', async
);

assert.equal(
((await ipc.invoke('tasks:list', 'session-1')) as Array<{ id: string }>)[0]?.id,
'task-1',
((await ipc.invoke('todo:read', 'session-1')) as Array<{ content: string }>)[0]?.content,
'Verify the Desktop adapter',
);
assert.deepEqual(await ipc.invoke('plan-mode:getState', 'session-1'), {
schemaVersion: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ test('goal:arm takes the Session from the scoped channel and refuses any other k
test('adapts Host Goal, Task, Deep Research, and Resource projections', async () => {
const controls: unknown[] = [];
const client = domainClient({
listTasks: async () => [{ id: 'task-1' }] as never,
querySessionTodo: async () => [{ content: 'todo-1', status: 'pending' }] as never,
listRuntimeResources: async () => [{ sessionId: 'session-1', result: { ref: 'shell:1' } }] as never,
queryGoal: async () => ({
sessionId: 'session-1',
Expand All @@ -469,7 +469,7 @@ test('adapts Host Goal, Task, Deep Research, and Resource projections', async ()
const ipc = ipcHarness();
registerDomainsIpc({ client, emitModeChanged() {} }, ipc);

assert.equal(((await ipc.invoke('tasks:list', 'session-1')) as Array<{ id: string }>)[0]?.id, 'task-1');
assert.equal(((await ipc.invoke('todo:read', 'session-1')) as Array<{ content: string }>)[0]?.content, 'todo-1');
assert.equal(
((await ipc.invoke('shell-runs:list', 'session-1')) as Array<{ result: { ref: string } }>)[0]
?.result.ref,
Expand Down Expand Up @@ -1004,7 +1004,7 @@ test('publishes typed invalidations and refreshes only changed Runtime Resources
ipc,
);

handle.sessionDomainChanged({ sessionId: 'session-1', domain: 'task' });
handle.sessionDomainChanged({ sessionId: 'session-1', domain: 'todo' });
handle.sessionDomainChanged({ sessionId: 'session-1', domain: 'deep_research' });
handle.sessionDomainChanged({ sessionId: 'session-1', domain: 'plan' });
handle.sessionDomainChanged({ sessionId: 'session-1', domain: 'usage' });
Expand All @@ -1030,8 +1030,8 @@ test('publishes typed invalidations and refreshes only changed Runtime Resources
assert.deepEqual(gets, [{ sessionId: 'session-1', ref: update.result.ref }]);
assert.deepEqual(sent, [
{
channel: 'tasks:changed',
payload: { sessionId: 'session-1', taskIds: [], at: 12 },
channel: 'todo:changed',
payload: { sessionId: 'session-1', at: 12 },
},
{
channel: 'deepResearch:changed',
Expand Down Expand Up @@ -1073,8 +1073,8 @@ test('publishes typed invalidations and refreshes only changed Runtime Resources
handle.sessionSubscriptionRecovered('session-1');
assert.deepEqual(sent, [
{
channel: 'tasks:changed',
payload: { sessionId: 'session-1', taskIds: [], at: 12 },
channel: 'todo:changed',
payload: { sessionId: 'session-1', at: 12 },
},
{
channel: 'deepResearch:changed',
Expand Down Expand Up @@ -1181,7 +1181,7 @@ function domainClient(overrides: Partial<DomainClient>): DomainClient {
listRuntimeResources: unavailable,
listAgentGraphEpochs: unavailable,
listCurrentAgentGraphEpochs: unavailable,
listTasks: unavailable,
querySessionTodo: unavailable,
queryAgentGraph: unavailable,
queryAgentGraphOperator: unavailable,
queryDeepResearch: unavailable,
Expand Down
12 changes: 6 additions & 6 deletions apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function createBridgeRecorder(): {
'sessions.subscribeEvents',
'shellRuns.subscribePtyData',
'shellRuns.subscribeResync',
'tasks.subscribeChanges',
'todo.subscribeChanges',
'browser.setActiveSession',
'browser.setViewport',
'browser.onState',
Expand Down Expand Up @@ -70,7 +70,7 @@ function createBridgeRecorder(): {
gitReview: domain('gitReview'),
sessions: domain('sessions'),
shellRuns: domain('shellRuns'),
tasks: domain('tasks'),
todo: domain('todo'),
browser: domain('browser'),
artifacts: domain('artifacts'),
app: domain('app'),
Expand Down Expand Up @@ -122,8 +122,8 @@ describe('createDesktopWorkbarServices', () => {
services.terminal.subscribePtyData(eventHandler)();
services.terminal.subscribeResync(eventHandler)();

await services.tasks.list('s');
services.tasks.subscribeChanges(eventHandler)();
await services.todo.read('s');
services.todo.subscribeChanges(eventHandler)();

services.browser.setActiveSession('s');
services.browser.setViewport({ sessionId: 's', rect: null });
Expand Down Expand Up @@ -194,8 +194,8 @@ describe('createDesktopWorkbarServices', () => {
'shellRuns.write',
'shellRuns.subscribePtyData',
'shellRuns.subscribeResync',
'tasks.list',
'tasks.subscribeChanges',
'todo.read',
'todo.subscribeChanges',
'browser.setActiveSession',
'browser.setViewport',
'browser.navigate',
Expand Down
40 changes: 9 additions & 31 deletions apps/desktop/src/main/e2e-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
resolveStorageRoot,
tryAcquireInteractiveRootOwner,
} from '@maka/storage/root-authority';
import { openInteractiveTaskLedgerStoreForWrite } from '@maka/storage/task-ledger-authority';
import { openInteractiveSessionTodoStoreForWrite } from '@maka/storage/session-todo-authority';
import { openInteractiveUsageStoresForWrite } from '@maka/storage/usage-stores';
import {
E2E_FIXTURE_NOW,
Expand Down Expand Up @@ -231,39 +231,17 @@ export async function seedE2eFixture(input: {

if (scenario === 'turn-narrative' || scenario === 'turn-narrative-browser') {
const owner = await tryAcquireInteractiveRootOwner(storageRoot);
if (!owner) throw new Error('Unable to acquire the E2E fixture task-ledger root');
if (!owner) throw new Error('Unable to acquire the E2E fixture SessionTodo root');
try {
const tasks = await openInteractiveTaskLedgerStoreForWrite(owner.lease);
const todos = await openInteractiveSessionTodoStoreForWrite(owner.lease);
try {
const created = await tasks.create(
TURN_SESSION_ID,
[
{ subject: '补齐桌面端无障碍覆盖' },
{ subject: '核对模型选择器的键盘路径' },
{ subject: '确认工具结果可以展开阅读' },
],
{ source: 'import', actor: 'system' },
);
await tasks.update(
TURN_SESSION_ID,
created.created[0]!.id,
{ status: 'in_progress' },
{ source: 'import', actor: 'system' },
);
await tasks.update(
TURN_SESSION_ID,
created.created[2]!.id,
{ status: 'in_progress' },
{ source: 'import', actor: 'system' },
);
await tasks.update(
TURN_SESSION_ID,
created.created[2]!.id,
{ status: 'completed', completionEvidence: '工具输出已成功显示。' },
{ source: 'import', actor: 'system' },
);
await todos.replaceAll(TURN_SESSION_ID, [
{ content: '补齐桌面端无障碍覆盖', status: 'in_progress' },
{ content: '核对模型选择器的键盘路径', status: 'pending' },
{ content: '确认工具结果可以展开阅读', status: 'completed' },
]);
} finally {
tasks.close();
todos.close();
}
} finally {
await owner.close();
Expand Down
Loading
Loading