diff --git a/apps/desktop/src/main/__tests__/computer-use-host.test.ts b/apps/desktop/src/main/__tests__/computer-use-host.test.ts index 3fe5bd3a1e..c46ac89732 100644 --- a/apps/desktop/src/main/__tests__/computer-use-host.test.ts +++ b/apps/desktop/src/main/__tests__/computer-use-host.test.ts @@ -64,7 +64,7 @@ describe('Computer Use host health', () => { assert.equal(computerUseServiceHealth('none', undefined).state, 'not_available'); }); - it('constructs a backend only when the local artifact matches the manifest hash', async () => { + it('constructs a backend only when the local artifact matches the manifest hash', async (t) => { const directory = await mkdtemp(join(tmpdir(), 'maka-cu-host-')); try { const binaryPath = join(directory, 'maka-cu'); @@ -126,16 +126,26 @@ describe('Computer Use host health', () => { }); assert.equal(invalid.selected.backendId, 'none'); - const linkedBinaryPath = join(directory, 'linked-maka-cu'); - await symlink(binaryPath, linkedBinaryPath); - const linked = createComputerUseHost({ - isPackaged: false, - resourcesPath: directory, - manifestPath, - binaryPath: linkedBinaryPath, - physicalInputRecentlyActive: () => false, + await t.test('rejects a symlinked binary', async (context) => { + const linkedBinaryPath = join(directory, 'linked-maka-cu'); + try { + await symlink(binaryPath, linkedBinaryPath, 'file'); + } catch (error) { + if (process.platform === 'win32' && (error as NodeJS.ErrnoException).code === 'EPERM') { + context.skip('File symlinks require Developer Mode or symlink privileges on Windows'); + return; + } + throw error; + } + const linked = createComputerUseHost({ + isPackaged: false, + resourcesPath: directory, + manifestPath, + binaryPath: linkedBinaryPath, + physicalInputRecentlyActive: () => false, + }); + assert.equal(linked.selected.backendId, 'none'); }); - assert.equal(linked.selected.backendId, 'none'); } finally { await rm(directory, { recursive: true, force: true }); } diff --git a/apps/desktop/src/main/__tests__/goals-boundary.test.ts b/apps/desktop/src/main/__tests__/goals-boundary.test.ts index e52b4a280c..1db37abeb4 100644 --- a/apps/desktop/src/main/__tests__/goals-boundary.test.ts +++ b/apps/desktop/src/main/__tests__/goals-boundary.test.ts @@ -112,7 +112,7 @@ describe('Goals feature boundary', () => { const source = readFileSync(path, 'utf8'); for (const match of source.matchAll(/from\s+['"]([^'"]+)['"]/g)) { if (match[1]?.includes('controller/use-goal-controller')) { - importers.push(relative(desktopRoot, path)); + importers.push(relative(desktopRoot, path).replace(/\\/g, '/')); } } } diff --git a/apps/desktop/src/main/__tests__/managed-skill-sources.test.ts b/apps/desktop/src/main/__tests__/managed-skill-sources.test.ts index b1946c346a..d5058b9659 100644 --- a/apps/desktop/src/main/__tests__/managed-skill-sources.test.ts +++ b/apps/desktop/src/main/__tests__/managed-skill-sources.test.ts @@ -165,7 +165,7 @@ description: Build decks. const outside = join(root, 'outside-cache-target'); const cacheRoot = join(root, 'cache-link'); await mkdir(outside); - await symlink(outside, cacheRoot); + await symlink(outside, cacheRoot, process.platform === 'win32' ? 'junction' : 'dir'); assert.deepEqual(await importManagedSkillSource({ root: cacheRoot, sourceFile }), { ok: false, diff --git a/apps/desktop/src/main/__tests__/open-path-guard.test.ts b/apps/desktop/src/main/__tests__/open-path-guard.test.ts index ccc26fd2d3..d3a56d6a9a 100644 --- a/apps/desktop/src/main/__tests__/open-path-guard.test.ts +++ b/apps/desktop/src/main/__tests__/open-path-guard.test.ts @@ -77,7 +77,7 @@ describe('open path guard', () => { test('rejects symlink escapes from inside an allowed directory', async () => { await withWorkspace(async (workspaceRoot, outsideRoot) => { await mkdir(outsideRoot, { recursive: true }); - await symlink(outsideRoot, join(workspaceRoot, 'skills')); + await symlink(outsideRoot, join(workspaceRoot, 'skills'), process.platform === 'win32' ? 'junction' : 'dir'); assert.deepEqual(await resolveOpenPath({ key: 'skills', workspaceRoot }), { ok: false, reason: 'not-allowed' }); }); @@ -89,7 +89,7 @@ describe('open path guard', () => { const workspaceLink = join(linkRoot, 'workspace-link'); try { await mkdir(join(realRoot, 'skills'), { recursive: true }); - await symlink(realRoot, workspaceLink); + await symlink(realRoot, workspaceLink, process.platform === 'win32' ? 'junction' : 'dir'); const result = await resolveOpenPath({ key: 'skills', workspaceRoot: workspaceLink }); diff --git a/apps/desktop/src/main/__tests__/project-management-service.test.ts b/apps/desktop/src/main/__tests__/project-management-service.test.ts index fef7a9cb70..58c79e22d6 100644 --- a/apps/desktop/src/main/__tests__/project-management-service.test.ts +++ b/apps/desktop/src/main/__tests__/project-management-service.test.ts @@ -88,6 +88,7 @@ test('owns Project selection and reversible lifecycle actions in Desktop', async assert.equal(relinked.ok, true); assert.equal(selectedPaths.at(-1), await realpath(relocatedPath)); } finally { + catalog.close(); await rm(base, { recursive: true, force: true }); } }); diff --git a/apps/desktop/src/main/__tests__/rive-workflow-tool.test.ts b/apps/desktop/src/main/__tests__/rive-workflow-tool.test.ts index 36dc12ac85..107082544c 100644 --- a/apps/desktop/src/main/__tests__/rive-workflow-tool.test.ts +++ b/apps/desktop/src/main/__tests__/rive-workflow-tool.test.ts @@ -18,7 +18,7 @@ */ import assert from 'node:assert/strict'; -import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, it } from 'node:test'; @@ -141,7 +141,7 @@ describe('RiveWorkflow tool and CLI bridge', { concurrency: false }, () => { }); }); - it('kills and reaps a Rive child that ignores SIGTERM on abort', async () => { + it('kills and reaps an aborted Rive child, including ignored SIGTERM on POSIX', async () => { await withFakeRive('ignore-term', async (riveBin, cwd) => { const pidFile = join(cwd, 'pid'); const controller = new AbortController(); @@ -267,11 +267,13 @@ async function withFakeRive( fn: (riveBin: string, cwd: string) => Promise, ): Promise { const cwd = await mkdtemp(join(tmpdir(), 'maka-rive-tool-')); - const riveBin = join(cwd, 'rive'); - await writeFile(riveBin, fakeRiveScript(mode), 'utf8'); - await chmod(riveBin, 0o755); try { - await fn(riveBin, cwd); + // Node treats the first Rive subcommand as a script path. This exercises a + // real shell-free child process on every platform without a shell shim. + for (const command of ['workflow', 'scheduler', 'work', 'branch']) { + await writeFile(join(cwd, command), fakeRiveScript(mode), 'utf8'); + } + await fn(process.execPath, cwd); } finally { await rm(cwd, { recursive: true, force: true }); } @@ -280,53 +282,33 @@ async function withFakeRive( function fakeRiveScript(mode: string): string { if (mode === 'ignore-term') { return [ - '#!/bin/sh', - 'trap "" TERM', - 'echo $$ > "$PID_FILE"', - 'echo RIVE_CHILD_READY', - 'sleep 20', - 'echo \'{"protocol":{"state":"completed"},"display":{"summary":"late"}}\'', - '', + 'process.on("SIGTERM", () => {});', + 'require("node:fs").writeFileSync(process.env.PID_FILE, String(process.pid));', + 'console.log("RIVE_CHILD_READY");', + 'setTimeout(() => console.log(JSON.stringify({protocol: {state: "completed"}, display: {summary: "late"}})), 20_000);', ].join('\n'); } if (mode === 'sleep') { - return [ - '#!/bin/sh', - 'sleep 5', - 'echo \'{"protocol":{"state":"completed"},"display":{"summary":"late"}}\'', - '', - ].join('\n'); + return 'setTimeout(() => console.log(JSON.stringify({protocol: {state: "completed"}, display: {summary: "late"}})), 5_000);'; } if (mode === 'bad-json') { - return ['#!/bin/sh', 'echo "not json"', ''].join('\n'); + return 'console.log("not json");'; } if (mode === 'failed-envelope') { return [ - '#!/bin/sh', - 'cat <<\'JSON\'', - '{"error":{"code":"workflow_param_missing","message":"workflow missing param: slack_channel","action":"fix_arguments"}}', - 'JSON', - 'exit 1', - '', + 'console.log(JSON.stringify({error: {code: "workflow_param_missing", message: "workflow missing param: slack_channel", action: "fix_arguments"}}));', + 'process.exitCode = 1;', ].join('\n'); } if (mode === 'failed-secret') { return [ - '#!/bin/sh', - 'echo "api_key=abc123-super-secret" >&2', - 'cat <<\'JSON\'', - '{"error":{"code":"auth","message":"token=abc123-super-secret"}}', - 'JSON', - 'exit 1', - '', + 'console.error("api_key=abc123-super-secret");', + 'console.log(JSON.stringify({error: {code: "auth", message: "token=abc123-super-secret"}}));', + 'process.exitCode = 1;', ].join('\n'); } return [ - '#!/bin/sh', - 'echo "token=abc123-super-secret" >&2', - 'cat <<\'JSON\'', - '{"protocol":{"workflow_run_id":"wfrun_fake","scheduler_run_id":"sched_fake","root_work_node_id":"work_root_fake","state":"completed"},"display":{"summary":"Workflow run wfrun_fake root work_root_fake state completed"}}', - 'JSON', - '', + 'console.error("token=abc123-super-secret");', + 'console.log(JSON.stringify({protocol: {workflow_run_id: "wfrun_fake", scheduler_run_id: "sched_fake", root_work_node_id: "work_root_fake", state: "completed"}, display: {summary: "Workflow run wfrun_fake root work_root_fake state completed"}}));', ].join('\n'); } diff --git a/apps/desktop/src/main/__tests__/runtime-host-local-remote-access.test.ts b/apps/desktop/src/main/__tests__/runtime-host-local-remote-access.test.ts index 3c788c7f50..b53d415b1b 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-local-remote-access.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-local-remote-access.test.ts @@ -37,8 +37,8 @@ import type { createDesktopRuntimeHostLocalOperator } from '../runtime-host-loca const testOperator = (modulePath: string) => ({ kind: 'node' as const, - platform: 'posix' as const, - nodePath: '/usr/bin/node', + platform: process.platform === 'win32' ? 'win32' as const : 'posix' as const, + nodePath: process.execPath, modulePath, }); @@ -518,7 +518,9 @@ test('does not persist recoverable setup authority before Desktop ownership comm assert.equal(setupCalls, 0); }); -test('adopts a released handoff through its existing legacy operator', async (t) => { +test('adopts a released handoff through its existing legacy operator', { + skip: process.platform === 'win32' && 'Legacy POSIX handoff requires POSIX deployment paths', +}, async (t) => { const base = await mkdtemp(join(tmpdir(), 'maka-local-remote-access-prestart-')); t.after(() => rm(base, { recursive: true, force: true })); const clientDataRoot = join(base, 'client'); @@ -593,7 +595,8 @@ test('migrates a released managed receipt before exposing it to lifecycle operat const clientDataRoot = join(base, 'client'); const rootPath = join(clientDataRoot, 'workspaces', 'default'); const rootId = 'a'.repeat(64); - const operatorPath = join(base, 'installed', 'operator'); + // The released schema describes a POSIX executable, regardless of the test host. + const operatorPath = '/opt/maka/installed/operator'; const lifecyclePath = join(clientDataRoot, 'runtime-host-local-service.json'); await mkdir(rootPath, { recursive: true }); await writeFile( diff --git a/docs/windows-test-inventory.md b/docs/windows-test-inventory.md index 50f0f66928..06dff8fc42 100644 --- a/docs/windows-test-inventory.md +++ b/docs/windows-test-inventory.md @@ -16,16 +16,17 @@ Locations intentionally omit line numbers so unrelated edits do not invalidate t | Classification | Count | |---|---:| | windows-backend-gap | 27 | -| portable-candidate | 19 | +| portable-candidate | 20 | | platform-contract | 31 | -Total Windows-excluded declarations: **77** +Total Windows-excluded declarations: **78** ## Inventory | Classification | Test | Skip expression | |---|---|---| | platform-contract | `apps/desktop/src/main/__tests__/project-context-root.test.ts` rejects a session cwd without read and traversal access | `process.platform === 'win32' ? 'POSIX permissions are required to make the session cwd inaccessible' : process.getuid?.() === 0` | +| portable-candidate | `apps/desktop/src/main/__tests__/runtime-host-local-remote-access.test.ts` adopts a released handoff through its existing legacy operator | `process.platform === 'win32' && 'Legacy POSIX handoff requires POSIX deployment paths'` | | platform-contract | `apps/desktop/src/main/__tests__/shell-env.test.ts` imports the login PATH without importing application control variables | `process.platform === 'win32'` | | platform-contract | `apps/desktop/src/main/__tests__/shell-env.test.ts` keeps the inherited PATH and does not log shell stderr when capture fails | `process.platform === 'win32'` | | platform-contract | `apps/desktop/src/main/__tests__/shell-env.test.ts` kills login-shell descendants when capture times out | `process.platform === 'win32'` |