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 @@ -295,14 +295,52 @@ describe('composer first-send cleanup', () => {
});

try {
assert.equal(await createAppShellChatActions(createActionsDeps()).send('hello'), true);
const actions = createAppShellChatActions(createActionsDeps());
let resolved = 0;
assert.equal(
await actions.send('hello', undefined, {
onSessionResolved: () => {
resolved += 1;
},
}),
true,
);
assert.equal(resolved, 1);
} finally {
restoreWindow();
}

assert.deepEqual(removed, []);
});

it('does not report a resolved session when the first send outcome is unknown', async () => {
let resolved = 0;
const restoreWindow = installWindow({
newTasks: { create: async () => ({ id: 'session-1' }) },
sessions: {
// `outcome_unknown`: the Host may have admitted the Message, so the
// Session is kept and the send counts as landed — but nothing proves
// the outcome, so it must not look like a resolved Session. The Work
// Board only links a task to a Session whose first send projected.
submitMessage: async () => ({ ok: false as const, reason: 'outcome_unknown' as const }),
},
});

try {
const actions = createAppShellChatActions(createActionsDeps());
const result = await actions.send('hello', undefined, {
onSessionResolved: () => {
resolved += 1;
},
});
assert.equal(result, true);
} finally {
restoreWindow();
}

assert.equal(resolved, 0);
});

it('projects the first message before activation while waiting to submit until observation', async () => {
const observation = deferred<void>();
const order: string[] = [];
Expand Down
167 changes: 163 additions & 4 deletions apps/desktop/src/main/__tests__/work-board-ipc-main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ describe('Work Board IPC', () => {
ipcMain: ipc as unknown as Pick<IpcMain, 'handle'>,
workspaceRoot: root,
mainWindowController: window,
validateLinkedSession: async () => true,
});
try {
const created = await ipc.invoke<WorkBoardIpcResult<{ id: string; revision: number }>>(
Expand Down Expand Up @@ -128,6 +129,7 @@ describe('Work Board IPC', () => {
'workBoard:archive',
'workBoard:unarchive',
'workBoard:remove',
'workBoard:linkSession',
]);
} finally {
registration.close();
Expand All @@ -143,11 +145,17 @@ describe('Work Board IPC', () => {
ipcMain: ipc as unknown as Pick<IpcMain, 'handle'>,
workspaceRoot: root,
mainWindowController: window,
validateLinkedSession: async () => true,
});
try {
const created = await ipc.invoke<WorkBoardIpcResult<{ id: string; revision: number }>>(
'workBoard:create',
itemInput(),
{
scope: { kind: 'project', projectId: 'p1' },
title: 'Review auth',
creator: { kind: 'user' },
provenance: { kind: 'manual' },
},
);
assert.ok(created.ok);
const id = created.ok ? created.value.id : '';
Expand All @@ -156,7 +164,15 @@ describe('Work Board IPC', () => {
WorkBoardIpcResult<{ title: string; revision: number; state: string }>
>('workBoard:update', id, { title: 'Review auth v2' });
assert.ok(renamed.ok);
assert.equal(renamed.ok && renamed.value.revision, 2);
assert.equal(renamed.ok && renamed.value.revision, 2);

const linked = await ipc.invoke<WorkBoardIpcResult<{ linkedSessions: unknown[] }>>(
'workBoard:linkSession',
id,
{ profileId: 'profile-1', hostId: 'host-1', sessionId: 'session-1', linkedAt: 103 },
);
assert.equal(linked.ok, true);
assert.equal(linked.ok && linked.value.linkedSessions.length, 1);

const staleRename = await ipc.invoke<WorkBoardIpcResult<unknown>>(
'workBoard:update',
Expand Down Expand Up @@ -214,11 +230,154 @@ describe('Work Board IPC', () => {
assert.ok(page.ok);
assert.equal(page.ok && page.value.items.length, 0);

// create, update, archive, unarchive, archive, remove = 6 mutations
// create, update, link, archive, unarchive, archive, remove = 7 mutations
const changed = window.events.filter(
(event) => event.channel === 'workBoard:changed',
);
assert.equal(changed.length, 6);
assert.equal(changed.length, 7);
} finally {
registration.close();
}
});
});

test('rejects a linked Session that the Host validator cannot prove', async () => {
await withTempRoot(async (root) => {
const ipc = createFakeIpcMain();
const window = createFakeWindowController();
const registration = registerWorkBoardIpc({
ipcMain: ipc as unknown as Pick<IpcMain, 'handle'>,
workspaceRoot: root,
mainWindowController: window,
validateLinkedSession: async () => false,
});
try {
const created = await ipc.invoke<WorkBoardIpcResult<{ id: string }>>(
'workBoard:create',
itemInput(),
);
assert.ok(created.ok);
const linked = await ipc.invoke<WorkBoardIpcResult<unknown>>(
'workBoard:linkSession',
created.ok ? created.value.id : '',
{ profileId: 'profile-1', hostId: 'host-1', sessionId: 'missing', linkedAt: 1 },
);
assert.equal(linked.ok, false);
if (!linked.ok) assert.equal(linked.code, 'invalid_input');
} finally {
registration.close();
}
});
});

test('rejects linking a Session to an Inbox item even when the Host validates', async () => {
await withTempRoot(async (root) => {
const ipc = createFakeIpcMain();
const window = createFakeWindowController();
const registration = registerWorkBoardIpc({
ipcMain: ipc as unknown as Pick<IpcMain, 'handle'>,
workspaceRoot: root,
mainWindowController: window,
validateLinkedSession: async () => true,
});
try {
const created = await ipc.invoke<WorkBoardIpcResult<{ id: string }>>(
'workBoard:create',
itemInput(),
);
assert.ok(created.ok);
const linked = await ipc.invoke<WorkBoardIpcResult<unknown>>(
'workBoard:linkSession',
created.ok ? created.value.id : '',
{ profileId: 'profile-1', hostId: 'host-1', sessionId: 'session-1', linkedAt: 1 },
);
assert.equal(linked.ok, false);
if (!linked.ok) assert.equal(linked.code, 'invalid_input');
} finally {
registration.close();
}
});
});

test('passes the canonical board project to the Host validator for a project-scoped item', async () => {
await withTempRoot(async (root) => {
const ipc = createFakeIpcMain();
const window = createFakeWindowController();
const validated: Array<{ link: unknown; project: string | undefined }> = [];
const registration = registerWorkBoardIpc({
ipcMain: ipc as unknown as Pick<IpcMain, 'handle'>,
workspaceRoot: root,
mainWindowController: window,
validateLinkedSession: async (link, expectedProjectId) => {
validated.push({ link, project: expectedProjectId });
return true;
},
});
try {
const created = await ipc.invoke<WorkBoardIpcResult<{ id: string }>>(
'workBoard:create',
{
scope: { kind: 'project', projectId: 'p1' },
title: 'Review auth',
creator: { kind: 'user' },
provenance: { kind: 'manual' },
},
);
assert.ok(created.ok);
const link = {
profileId: 'profile-1',
hostId: 'host-1',
sessionId: 'session-1',
linkedAt: 1,
};
const linked = await ipc.invoke<WorkBoardIpcResult<unknown>>(
'workBoard:linkSession',
created.ok ? created.value.id : '',
link,
);
assert.equal(linked.ok, true);
assert.deepEqual(validated, [{ link, project: 'p1' }]);
} finally {
registration.close();
}
});
});

test('rejects a project-scoped link whose Session belongs to another project on the same Host', async () => {
await withTempRoot(async (root) => {
const ipc = createFakeIpcMain();
const window = createFakeWindowController();
// The runtime Host validator derives the Session's project (here from
// the session id) and must compare it with the canonical board project.
const registration = registerWorkBoardIpc({
ipcMain: ipc as unknown as Pick<IpcMain, 'handle'>,
workspaceRoot: root,
mainWindowController: window,
validateLinkedSession: async (link, expectedProjectId) => {
const sessionId = (link as { sessionId?: string })?.sessionId ?? '';
return expectedProjectId === sessionId.split('-')[0];
},
});
try {
const created = await ipc.invoke<WorkBoardIpcResult<{ id: string }>>(
'workBoard:create',
{
scope: { kind: 'project', projectId: 'p1' },
title: 'Review auth',
creator: { kind: 'user' },
provenance: { kind: 'manual' },
},
);
assert.ok(created.ok);
// A Session from project p2 on the same Host must not be linked to a
// p1 board item.
const linked = await ipc.invoke<WorkBoardIpcResult<unknown>>(
'workBoard:linkSession',
created.ok ? created.value.id : '',
{ profileId: 'profile-1', hostId: 'host-1', sessionId: 'p2-session-1', linkedAt: 1 },
);
assert.equal(linked.ok, false);
if (!linked.ok) assert.equal(linked.code, 'invalid_input');
} finally {
registration.close();
}
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/main/__tests__/work-board-panel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,13 @@ test('prevents a second Work Board create while the first request is pending', a
createCalls += 1;
return createResult.promise;
});
const input = harness.container.querySelector('input');
const input = harness.container.querySelector('textarea');
assert.ok(input);
input.value = 'Later';
const propsKey = Object.keys(input).find((key) => key.startsWith('__reactProps$'));
assert.ok(propsKey, 'missing React props on input');
const props = (input as unknown as Record<string, unknown>)[propsKey] as {
onChange?: (event: { target: HTMLInputElement; defaultPrevented: boolean }) => void;
onChange?: (event: { target: HTMLTextAreaElement; defaultPrevented: boolean }) => void;
};
assert.ok(props.onChange, 'missing React change handler');
await act(async () => {
Expand Down
109 changes: 109 additions & 0 deletions apps/desktop/src/main/__tests__/work-board-target.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import { resolveWorkBoardStartTarget } from '../../renderer/features/task-entry/testing.js';
import type { TaskEntryCatalog } from '../../renderer/features/task-entry/testing.js';
import type { ProjectRecord } from '@maka/core/project';
import type { WorkBoardItem } from '@maka/core/work-board';

const item = (scope: WorkBoardItem['scope']): WorkBoardItem => ({
schemaVersion: 1,
id: 'item-1',
revision: 1,
scope,
title: 'Review auth',
state: 'todo',
archived: false,
creator: { kind: 'user' },
provenance: { kind: 'manual' },
linkedSessions: [],
createdAt: 1,
updatedAt: 1,
});

const catalog = (projects: readonly ProjectRecord[]): TaskEntryCatalog => ({
defaultProfileId: 'profile-1',
hosts: [{
profile: { id: 'profile-1', name: 'Local', kind: 'local' },
hostId: 'host-1',
readiness: 'ready',
state: 'available',
projects,
capabilities: { chooseClientDirectory: false, chooseHostDirectory: false, selectNoProject: true },
selectedProjectId: null,
chatDefaults: { permissionMode: 'ask', thinkingLevel: 'off' },
}],
});

describe('Work Board Start task target resolution', () => {
test('resolves an available project alias to a canonical Host target', () => {
const result = resolveWorkBoardStartTarget(
item({ kind: 'project', projectId: 'old-project-id' }),
catalog([{ id: 'canonical-project', aliases: ['old-project-id'], name: 'Project', locations: [], available: true }]),
);
assert.equal(result.ok, true);
if (result.ok) assert.deepEqual(result.target, { profileId: 'profile-1', hostId: 'host-1', projectId: 'canonical-project' });
});

test('rejects Inbox and unavailable projects', () => {
const inbox = resolveWorkBoardStartTarget(item({ kind: 'inbox' }), catalog([]));
const missing = resolveWorkBoardStartTarget(item({ kind: 'project', projectId: 'missing' }), catalog([]));
assert.equal(inbox.ok ? 'unexpected' : inbox.reason, 'inbox');
assert.equal(missing.ok ? 'unexpected' : missing.reason, 'unavailable');
});

test('rejects archived and ambiguous projects', () => {
const archived = resolveWorkBoardStartTarget(
item({ kind: 'project', projectId: 'old-project-id' }),
catalog([{ id: 'canonical-project', aliases: ['old-project-id'], name: 'Project', locations: [], available: true, archivedAt: 10 }]),
);
assert.equal(archived.ok ? 'unexpected' : archived.reason, 'unavailable');

const shared = { id: 'p1', aliases: ['shared-id'], name: 'Project', locations: [], available: true };
const multiHost: TaskEntryCatalog = {
defaultProfileId: 'profile-1',
hosts: [
{
profile: { id: 'profile-1', name: 'Local', kind: 'local' },
hostId: 'host-1',
readiness: 'ready',
state: 'available',
projects: [shared],
capabilities: { chooseClientDirectory: false, chooseHostDirectory: false, selectNoProject: true },
selectedProjectId: null,
chatDefaults: { permissionMode: 'ask', thinkingLevel: 'off' },
},
{
profile: { id: 'profile-2', name: 'Remote', kind: 'remote' },
hostId: 'host-2',
readiness: 'ready',
state: 'available',
projects: [{ ...shared, id: 'p2' }],
capabilities: { chooseClientDirectory: false, chooseHostDirectory: false, selectNoProject: true },
selectedProjectId: null,
chatDefaults: { permissionMode: 'ask', thinkingLevel: 'off' },
},
],
};
const ambiguous = resolveWorkBoardStartTarget(item({ kind: 'project', projectId: 'shared-id' }), multiHost);
assert.equal(ambiguous.ok ? 'unexpected' : ambiguous.reason, 'ambiguous');
});
});
Loading