diff --git a/apps/desktop/e2e/composer-inline-completion.spec.ts b/apps/desktop/e2e/composer-inline-completion.spec.ts index 81aeadabf3..96f48e84d6 100644 --- a/apps/desktop/e2e/composer-inline-completion.spec.ts +++ b/apps/desktop/e2e/composer-inline-completion.spec.ts @@ -1,4 +1,4 @@ -import { FAKE_HOLD_OPEN_PROMPT } from '@maka/runtime/fake-backend'; +import { FAKE_HOLD_OPEN_PROMPT } from '@maka/runtime/test-only/fake-backend'; import { test, expect, COMPOSER_INPUT } from './fixtures'; import type { Page } from '@playwright/test'; diff --git a/apps/desktop/e2e/slash-command-menu.spec.ts b/apps/desktop/e2e/slash-command-menu.spec.ts index aa2caff4c5..947782bc78 100644 --- a/apps/desktop/e2e/slash-command-menu.spec.ts +++ b/apps/desktop/e2e/slash-command-menu.spec.ts @@ -1,4 +1,4 @@ -import { FAKE_HOLD_OPEN_PROMPT } from '@maka/runtime/fake-backend'; +import { FAKE_HOLD_OPEN_PROMPT } from '@maka/runtime/test-only/fake-backend'; import { expect, test, COMPOSER_INPUT } from './fixtures'; test('shows only slash commands executable in the current session state', async ({ diff --git a/apps/desktop/e2e/streaming-remount.spec.ts b/apps/desktop/e2e/streaming-remount.spec.ts index 11b044b922..cb64cbf8e6 100644 --- a/apps/desktop/e2e/streaming-remount.spec.ts +++ b/apps/desktop/e2e/streaming-remount.spec.ts @@ -2,7 +2,7 @@ import { FAKE_HOLD_OPEN_PROMPT, FAKE_HOLD_OPEN_REWRITE_PROMPT, FAKE_WAIT_FOR_STEERING_LARGE_RESPONSE_PROMPT, -} from '@maka/runtime/fake-backend'; +} from '@maka/runtime/test-only/fake-backend'; import type { Locator } from '@playwright/test'; import { expect, COMPOSER_INPUT, test } from './fixtures'; diff --git a/apps/desktop/electron-builder.config.mjs b/apps/desktop/electron-builder.config.mjs index 2cabcd3ed6..452335f5a0 100644 --- a/apps/desktop/electron-builder.config.mjs +++ b/apps/desktop/electron-builder.config.mjs @@ -6,7 +6,15 @@ export default { directories: { output: 'release', }, - files: ['dist/**/*', 'dist-renderer/**/*', 'package.json', '!**/__tests__/**'], + files: [ + 'dist/**/*', + 'dist-renderer/**/*', + 'package.json', + '!**/__tests__/**', + // FakeBackend and the Desktop E2E candidate bootstrap live under + // `test-only/`; they must not reach a packaged app. + '!**/test-only/**', + ], extraResources: [ { from: '../../node_modules/dugite/git', diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts index 819392f667..df4dd1f401 100644 --- a/apps/desktop/src/main/runtime-host-boot.ts +++ b/apps/desktop/src/main/runtime-host-boot.ts @@ -456,10 +456,16 @@ runtimeHostManager = await startRuntimeHostDesktopManager( rootPath: workspaceRoot, clientInstanceId: runtimeHostClientInstanceId, generation: runtimeHostGeneration, + // The Desktop E2E composition lives behind its own entry module, which + // release packaging drops: picking it here is what keeps FakeBackend and + // the E2E bootstrap out of the shipped Runtime Host. candidateEntrypoint: new URL( - import.meta.resolve("@maka/runtime-host/execution-candidate-main"), + import.meta.resolve( + isE2e + ? "@maka/runtime-host/test-only/execution-candidate-e2e-main" + : "@maka/runtime-host/execution-candidate-main", + ), ), - ...(isE2e ? { desktopE2e: true } : {}), ipcMain, workspaceRoot, attachmentApprovals, diff --git a/apps/desktop/src/main/runtime-host-desktop-candidate.ts b/apps/desktop/src/main/runtime-host-desktop-candidate.ts index c27b3bb589..f301bef2f8 100644 --- a/apps/desktop/src/main/runtime-host-desktop-candidate.ts +++ b/apps/desktop/src/main/runtime-host-desktop-candidate.ts @@ -135,7 +135,6 @@ export interface DesktopRuntimeHostCandidateStartInput readonly connectTimeoutMs?: number; readonly handshakeTimeoutMs?: number; readonly candidateEntrypoint: string | URL; - readonly desktopE2e?: boolean; readonly generation?: string; readonly takeoverHostEpoch?: string; readonly signal?: AbortSignal; @@ -695,7 +694,6 @@ function connectInput( ...(input.handshakeTimeoutMs === undefined ? {} : { handshakeTimeoutMs: input.handshakeTimeoutMs }), - ...(input.desktopE2e ? { desktopE2e: true } : {}), ...(input.signal === undefined ? {} : { signal: input.signal }), }; } diff --git a/packages/runtime-host/package.json b/packages/runtime-host/package.json index 47a761f4d1..1abbdce21c 100644 --- a/packages/runtime-host/package.json +++ b/packages/runtime-host/package.json @@ -10,7 +10,8 @@ "./protocol": "./dist/protocol/index.js", "./client": "./dist/client/index.js", "./execution-candidate-main": "./dist/execution-candidate-main.js", - "./server": "./dist/server/index.js" + "./server": "./dist/server/index.js", + "./test-only/execution-candidate-e2e-main": "./dist/test-only/execution-candidate-e2e-main.js" }, "scripts": { "clean": "node ../../scripts/clean-paths.mjs dist tsconfig.tsbuildinfo", diff --git a/packages/runtime-host/src/__tests__/candidate-cli.test.ts b/packages/runtime-host/src/__tests__/candidate-cli.test.ts index 929c7e6761..a773f156b9 100644 --- a/packages/runtime-host/src/__tests__/candidate-cli.test.ts +++ b/packages/runtime-host/src/__tests__/candidate-cli.test.ts @@ -5,7 +5,7 @@ import { parseInteractiveRuntimeHostCandidateArguments } from '../candidate-cli. const ROOT_ID = 'a'.repeat(64); const STARTUP_ATTEMPT_ID = '00000000-0000-4000-8000-000000000001'; -test('parses the production candidate flags without a desktop E2E override', () => { +test('parses the production candidate flags', () => { const parsed = parseInteractiveRuntimeHostCandidateArguments([ '--root', '/tmp/workspace', @@ -20,24 +20,11 @@ test('parses the production candidate flags without a desktop E2E override', () assert.equal(parsed.expectedRootId, ROOT_ID); assert.equal(parsed.startupAttemptId, STARTUP_ATTEMPT_ID); assert.equal(parsed.idleGraceMs, 10_000); - assert.equal(parsed.desktopE2e, undefined); }); -test('parses the desktop E2E composition flag', () => { - const parsed = parseInteractiveRuntimeHostCandidateArguments([ - '--root', - '/tmp/workspace', - '--expected-root-id', - ROOT_ID, - '--startup-attempt-id', - STARTUP_ATTEMPT_ID, - '--desktop-e2e', - '1', - ]); - assert.equal(parsed.desktopE2e, true); -}); - -test('rejects an unknown desktop E2E flag value', () => { +// The Desktop E2E composition is selected by its own entry module, not by a +// flag on the production CLI — so `--desktop-e2e` is simply unknown here. +test('rejects the retired desktop E2E flag as an unknown argument', () => { assert.throws( () => parseInteractiveRuntimeHostCandidateArguments([ @@ -48,8 +35,8 @@ test('rejects an unknown desktop E2E flag value', () => { '--startup-attempt-id', STARTUP_ATTEMPT_ID, '--desktop-e2e', - 'true', + '1', ]), - /Invalid --desktop-e2e/, + /Invalid Runtime Host candidate argument: --desktop-e2e/, ); }); diff --git a/packages/runtime-host/src/__tests__/desktop-e2e-execution.test.ts b/packages/runtime-host/src/__tests__/desktop-e2e-execution.test.ts index ec5b7ee87b..08f12deaba 100644 --- a/packages/runtime-host/src/__tests__/desktop-e2e-execution.test.ts +++ b/packages/runtime-host/src/__tests__/desktop-e2e-execution.test.ts @@ -4,7 +4,7 @@ import type { RuntimeEvent } from '@maka/core/runtime-event'; import type { SessionHeader } from '@maka/core/session'; import type { HistoryCompactCheckpoint } from '@maka/runtime/history-compact-checkpoint'; import type { BackendFactoryContext } from '@maka/runtime/session-manager'; -import { DesktopE2eBackend } from '../desktop-e2e-execution.js'; +import { DesktopE2eBackend } from '../test-only/desktop-e2e-execution.js'; function backendContext(overrides: Partial = {}): BackendFactoryContext { return { diff --git a/packages/runtime-host/src/__tests__/execution-candidate-main.test.ts b/packages/runtime-host/src/__tests__/execution-candidate-main.test.ts index bd0c283601..d87c4ce417 100644 --- a/packages/runtime-host/src/__tests__/execution-candidate-main.test.ts +++ b/packages/runtime-host/src/__tests__/execution-candidate-main.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { createHash, randomUUID } from 'node:crypto'; -import { mkdir, mkdtemp, rm } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -30,14 +30,14 @@ test('classifies invalid candidate arguments as an internal startup failure', () '--startup-attempt-id', STARTUP_ATTEMPT_ID, '--desktop-e2e', - 'true', + '1', ], { encoding: 'utf8', timeout: 10_000 }, ); assert.equal(result.status, 70, result.stderr); assert.match(result.stderr, /\[runtime-host\] startup failed:/); - assert.match(result.stderr, /Invalid --desktop-e2e/); + assert.match(result.stderr, /Invalid Runtime Host candidate argument: --desktop-e2e/); }); test('preserves a valid Candidate invocation failure across the detached stderr boundary', async () => { @@ -75,3 +75,72 @@ test('preserves a valid Candidate invocation failure across the detached stderr await rm(controlDirectory, { recursive: true, force: true }); } }); + +/** + * Release packaging drops every `test-only/` module, so the production + * candidate entry must not be able to reach one — statically, not merely at + * runtime. Walk the built module graph across the bundled `@maka/*` packages + * and report every test-only module it can reach. + */ +test('the production candidate entry never reaches a test-only module', async () => { + const entry = new URL('../execution-candidate-main.js', import.meta.url).href; + const seen = new Set([entry]); + const queue: string[] = [entry]; + const reached: string[] = []; + + while (queue.length > 0) { + const current = queue.pop(); + if (current === undefined) break; + if (current.includes('/test-only/')) { + reached.push(current); + continue; + } + let source: string; + try { + source = await readFile(new URL(current), 'utf8'); + } catch { + continue; + } + for (const specifier of staticImportSpecifiers(source)) { + const resolved = resolveModule(specifier, current); + if (resolved === undefined || seen.has(resolved)) continue; + seen.add(resolved); + queue.push(resolved); + } + } + + assert.deepEqual(reached, []); + assert.ok(seen.size > 50, `module graph looks truncated: ${seen.size} modules`); +}); + +function resolveModule(specifier: string, parent: string): string | undefined { + try { + if (specifier.startsWith('.')) return new URL(specifier, parent).href; + if (specifier.startsWith('@maka/')) { + const resolved = import.meta.resolve(specifier); + return resolved.startsWith('file:') ? resolved : undefined; + } + } catch { + return undefined; + } + return undefined; +} + +function staticImportSpecifiers(source: string): string[] { + const specifiers: string[] = []; + for (const match of source.matchAll( + /(?:^|[\s;}])(?:import|export)\b[^'"();]*?from\s*['"]([^'"]+)['"]/g, + )) { + if (match[1] !== undefined) specifiers.push(match[1]); + } + for (const match of source.matchAll(/(?:^|[\s;}])import\s*['"]([^'"]+)['"]/g)) { + if (match[1] !== undefined) specifiers.push(match[1]); + } + // Literal dynamic imports are real edges in the shipped graph — the built + // `dist` already contains several — so a walk that ignored them could pass + // while a production module reached test-only material through `import(…)`. + for (const match of source.matchAll(/\bimport\s*\(\s*['"]([^'"]+)['"]/g)) { + if (match[1] !== undefined) specifiers.push(match[1]); + } + return specifiers; +} diff --git a/packages/runtime-host/src/__tests__/execution-composition.test.ts b/packages/runtime-host/src/__tests__/execution-composition.test.ts index 6083587900..1705444b9d 100644 --- a/packages/runtime-host/src/__tests__/execution-composition.test.ts +++ b/packages/runtime-host/src/__tests__/execution-composition.test.ts @@ -1,4 +1,5 @@ import assert from 'node:assert/strict'; +import { parseNoRealConnectionError } from '@maka/core/connection-error-copy'; import { createRequire } from 'node:module'; import { mkdir, mkdtemp, rm, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; @@ -9,7 +10,7 @@ import type { AgentGraphIntentClaimRequest, } from '@maka/core/agent-graph-control'; import type { ShellRunRecord } from '@maka/core/shell-run'; -import { FAKE_ASK_USER_QUESTION_PROMPT, FakeBackend } from '@maka/runtime/fake-backend'; +import { FAKE_ASK_USER_QUESTION_PROMPT, FakeBackend } from '@maka/runtime/test-only/fake-backend'; import { LOCAL_READ_AGENT_DEFINITION } from '@maka/runtime/agent-catalog'; import { SessionManager } from '@maka/runtime/session-manager'; import { fingerprintAgentGraphRunnableIntent } from '@maka/runtime/stream-graph-admission'; @@ -80,7 +81,7 @@ test('production composition closes long-term memory after a later startup failu const stores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await stores.sessionStore.create({ cwd: root, - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -120,14 +121,14 @@ test('production recovery preserves legacy Automation history and closes an orph const stores = await openInteractiveExecutionStoresForWrite(owner.lease); const historical = await stores.sessionStore.create({ cwd: root, - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', }); const pending = await stores.sessionStore.create({ cwd: root, - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -348,7 +349,7 @@ test('production composition commits automatic titles through Host-owned Session try { const session = await manager.createSession({ cwd: root, - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -378,12 +379,51 @@ test('production composition commits automatic titles through Host-owned Session }); }); +test('a legacy fake-backend session is refused with the product reason, not a registry error', async () => { + await withCompositionRoot(async ({ root, owner }) => { + const { composition, manager } = await createCapturedExecutionComposition(owner); + try { + // Written by an older build: this one never produces `fake`, but the + // durable header survives and activation dispatches straight off it. + const legacy = await manager.createSession({ + cwd: root, + backend: 'fake', + llmConnectionSlug: 'fake', + model: 'fake-model', + permissionMode: 'ask', + }); + const failure = await composition.handlers['turn.start']( + { + sessionId: legacy.id, + turnId: 'turn-legacy-fake', + content: { text: 'resume a retired local simulation' }, + }, + { + hostEpoch: 'execution-composition-test', + connectionId: 'legacy-fake-client', + surface: 'tui', + principal: 'local_os_user', + acquireResidency: () => ({ release() {} }), + }, + ).then( + (result) => result, + (error: unknown) => error, + ); + const message = failure instanceof Error ? failure.message : JSON.stringify(failure); + assert.doesNotMatch(message, /No backend factory registered/); + assert.equal(parseNoRealConnectionError(message).reason, 'fake_backend'); + } finally { + await composition.close(); + } + }); +}); + test('production composition orphans ownerless ShellRuns before serving Resource queries', async () => { await withCompositionRoot(async ({ root, owner }) => { const stores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await stores.sessionStore.create({ cwd: root, - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -431,7 +471,7 @@ test('production Skill catalog resolves a Graph child durable tool surface', asy const stores = await openInteractiveExecutionStoresForWrite(owner.lease); const parent = await stores.sessionStore.create({ cwd: root, - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -599,7 +639,7 @@ test('Skill capability previews omit unavailable Tavily search surfaces', async const stores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await stores.sessionStore.create({ cwd: root, - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: connection.slug, model: 'fake-model', permissionMode: 'bypass', @@ -656,7 +696,7 @@ test('production composition validates graph stop before aborting a claimed chil const claims = createAgentGraphControlStore(root); const parent = await stores.sessionStore.create({ cwd: root, - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -868,7 +908,14 @@ async function createCapturedExecutionComposition(owner: InteractiveRootOwner): return originalRecover.call(this, stores); }; try { - const composition = await createExecutionRuntimeHostComposition(compositionContext(owner)); + // The production composition no longer registers a test backend of its + // own; the deterministic one arrives through the same `primaryBackendFactory` + // seam the Desktop E2E run uses. + const composition = await createExecutionRuntimeHostComposition( + compositionContext(owner), + {}, + { primaryBackendFactory: (backendContext) => new FakeBackend(backendContext) }, + ); await composition.recover(); if (!manager) throw new Error('Production execution composition did not construct Runtime'); return { composition, manager }; @@ -889,7 +936,7 @@ async function createClaimedGraphChild(input: { const child = await input.stores.sessionStore.createSubagent({ cwd: input.root, name: `Graph operator ${input.suffix}`, - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'explore', diff --git a/packages/runtime-host/src/__tests__/execution-host-message.test.ts b/packages/runtime-host/src/__tests__/execution-host-message.test.ts index a40427ea2b..7bbdf059d6 100644 --- a/packages/runtime-host/src/__tests__/execution-host-message.test.ts +++ b/packages/runtime-host/src/__tests__/execution-host-message.test.ts @@ -34,7 +34,7 @@ import { import { FAKE_ASK_USER_QUESTION_PROMPT, FAKE_WAIT_FOR_STEERING_PROMPT, -} from '@maka/runtime/fake-backend'; +} from '@maka/runtime/test-only/fake-backend'; import { type MakaTool, type MakaToolContext } from '@maka/runtime/tool-runtime'; import { openInteractiveExecutionStoresForRead, diff --git a/packages/runtime-host/src/__tests__/execution-host-queue.test.ts b/packages/runtime-host/src/__tests__/execution-host-queue.test.ts index 31ab553d38..99871e061a 100644 --- a/packages/runtime-host/src/__tests__/execution-host-queue.test.ts +++ b/packages/runtime-host/src/__tests__/execution-host-queue.test.ts @@ -34,7 +34,7 @@ import { import { FAKE_ASK_USER_QUESTION_PROMPT, FAKE_WAIT_FOR_STEERING_PROMPT, -} from '@maka/runtime/fake-backend'; +} from '@maka/runtime/test-only/fake-backend'; import { type MakaTool, type MakaToolContext } from '@maka/runtime/tool-runtime'; import { openInteractiveExecutionStoresForRead, diff --git a/packages/runtime-host/src/__tests__/execution-host-recovery.test.ts b/packages/runtime-host/src/__tests__/execution-host-recovery.test.ts index aee148f2e2..d39180a7ed 100644 --- a/packages/runtime-host/src/__tests__/execution-host-recovery.test.ts +++ b/packages/runtime-host/src/__tests__/execution-host-recovery.test.ts @@ -25,7 +25,7 @@ import { import { FAKE_ASK_USER_QUESTION_PROMPT, FAKE_WAIT_FOR_STEERING_PROMPT, -} from '@maka/runtime/fake-backend'; +} from '@maka/runtime/test-only/fake-backend'; import { type MakaTool, type MakaToolContext } from '@maka/runtime/tool-runtime'; import { openInteractiveExecutionStoresForRead, diff --git a/packages/runtime-host/src/__tests__/execution-host.test.ts b/packages/runtime-host/src/__tests__/execution-host.test.ts index c8cc1383c2..9dcd2f325a 100644 --- a/packages/runtime-host/src/__tests__/execution-host.test.ts +++ b/packages/runtime-host/src/__tests__/execution-host.test.ts @@ -36,7 +36,7 @@ import { FAKE_ASK_SANDBOX_BOUNDARY_PROMPT, FAKE_ASK_USER_QUESTION_PROMPT, FAKE_WAIT_FOR_STEERING_PROMPT, -} from '@maka/runtime/fake-backend'; +} from '@maka/runtime/test-only/fake-backend'; import { type MakaTool, type MakaToolContext } from '@maka/runtime/tool-runtime'; import { openInteractiveExecutionStoresForRead, diff --git a/packages/runtime-host/src/__tests__/fixtures/execution-host-suite.ts b/packages/runtime-host/src/__tests__/fixtures/execution-host-suite.ts index c875ec6e08..c22b242dcb 100644 --- a/packages/runtime-host/src/__tests__/fixtures/execution-host-suite.ts +++ b/packages/runtime-host/src/__tests__/fixtures/execution-host-suite.ts @@ -36,7 +36,7 @@ import { FAKE_ASK_USER_QUESTION_PROMPT, FAKE_WAIT_FOR_STEERING_PROMPT, FakeBackend, -} from '@maka/runtime/fake-backend'; +} from '@maka/runtime/test-only/fake-backend'; import { type MakaTool, type MakaToolContext } from '@maka/runtime/tool-runtime'; import { openInteractiveExecutionStoresForRead, @@ -125,7 +125,7 @@ export class ExecutionFixture { stores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await stores.sessionStore.create({ cwd: this.root, - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -271,7 +271,7 @@ export class ExecutionFixture { stores = await openInteractiveExecutionStoresForWrite(owner.lease); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new FakeBackend({ sessionId: ctx.sessionId, @@ -475,7 +475,7 @@ export class ExecutionFixture { const child = await stores.sessionStore.createSubagent({ cwd: this.root, name: `${agentName} ${kind}`, - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'explore', @@ -1025,7 +1025,7 @@ export async function withExecutionRoot( stores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await stores.sessionStore.create({ cwd: root, - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/runtime-host/src/__tests__/fixtures/execution-host.ts b/packages/runtime-host/src/__tests__/fixtures/execution-host.ts index e361b1c90c..0af2d2dd77 100644 --- a/packages/runtime-host/src/__tests__/fixtures/execution-host.ts +++ b/packages/runtime-host/src/__tests__/fixtures/execution-host.ts @@ -1,7 +1,9 @@ import { join } from 'node:path'; import { inspect } from 'node:util'; +import { FakeBackend } from '@maka/runtime/test-only/fake-backend'; import { createSqliteRuntimeStore } from '@maka/storage'; import { startExecutionRuntimeHostCandidate } from '../../server/execution-candidate.js'; +import { createExecutionRuntimeHostComposition } from '../../server/execution-composition.js'; import { runRuntimeHostProcessLifecycle } from '../../server/process-lifecycle.js'; const [rootPath, expectedRootId, idleGraceRaw, recoverySessionId, recoveryRunId] = @@ -19,11 +21,23 @@ if (!Number.isSafeInteger(idleGraceMs) || idleGraceMs < 0) { throw new Error('execution-host requires a non-negative idle grace'); } -const result = await startExecutionRuntimeHostCandidate({ - rootPath, - expectedRootId, - idleGraceMs, -}); +// The production composition registers no test backend. This fixture is a +// candidate host in its own right, so it supplies the deterministic one through +// the composition's `primaryBackendFactory` seam — the same path Desktop E2E +// takes — and its sessions declare the real `ai-sdk` backend kind. +const result = await startExecutionRuntimeHostCandidate( + { + rootPath, + expectedRootId, + idleGraceMs, + }, + { + createComposition: (context, compositionOptions) => + createExecutionRuntimeHostComposition(context, compositionOptions, { + primaryBackendFactory: (backendContext) => new FakeBackend(backendContext), + }), + }, +); if (result.kind === 'loser') process.exit(2); let recoveryOutcome: unknown; diff --git a/packages/runtime-host/src/__tests__/goal-root-authority.test.ts b/packages/runtime-host/src/__tests__/goal-root-authority.test.ts index f2bfb0659d..8ca97aafba 100644 --- a/packages/runtime-host/src/__tests__/goal-root-authority.test.ts +++ b/packages/runtime-host/src/__tests__/goal-root-authority.test.ts @@ -6,7 +6,7 @@ import { join } from 'node:path'; import { test } from 'node:test'; import type { AgentRunHeader } from '@maka/core/agent-run'; import { BackendRegistry, SessionManager } from '@maka/runtime/session-manager'; -import { FakeBackend } from '@maka/runtime/fake-backend'; +import { FakeBackend } from '@maka/runtime/test-only/fake-backend'; import { GOAL_SET_TOOL_NAME } from '@maka/runtime/goal-tools'; import { goalCheckpoint } from '@maka/runtime/goal-state'; import { type RuntimeHostedRootAuthority } from '@maka/runtime/message-authority'; diff --git a/packages/runtime-host/src/__tests__/plan-two-client-uds.test.ts b/packages/runtime-host/src/__tests__/plan-two-client-uds.test.ts index 4233a67c4c..00e8f0930b 100644 --- a/packages/runtime-host/src/__tests__/plan-two-client-uds.test.ts +++ b/packages/runtime-host/src/__tests__/plan-two-client-uds.test.ts @@ -13,8 +13,9 @@ import { type RuntimeHostSessionSubscription, } from '../client/index.js'; import { RUNTIME_HOST_PROTOCOL_VERSION, type SubscriptionFrame } from '../protocol/index.js'; +import { FakeBackend } from '@maka/runtime/test-only/fake-backend'; import { createExecutionRuntimeHostComposition } from '../server/execution-composition.js'; -import { RuntimeHostKernel } from '../server/host-kernel.js'; +import { RuntimeHostKernel, type RuntimeHostCompositionFactory } from '../server/host-kernel.js'; const PROTOCOL = { min: RUNTIME_HOST_PROTOCOL_VERSION, @@ -36,7 +37,7 @@ test('two Clients and a restarted production Host share one retry-safe Plan auth const planStore = await openInteractivePlanStoreForWrite(owner.lease); const session = await setupStores.sessionStore.create({ cwd: root, - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'explore', @@ -62,7 +63,7 @@ test('two Clients and a restarted production Host share one retry-safe Plan auth host = await RuntimeHostKernel.start({ owner, idleGraceMs: 30_000, - composition: defineInteractiveRuntimeHostComposition(createExecutionRuntimeHostComposition), + composition: defineInteractiveRuntimeHostComposition(deterministicBackendComposition), }); owner = undefined; [desktop, tui] = await Promise.all([connect(root, 'desktop'), connect(root, 'tui')]); @@ -115,7 +116,7 @@ test('two Clients and a restarted production Host share one retry-safe Plan auth host = await RuntimeHostKernel.start({ owner, idleGraceMs: 30_000, - composition: defineInteractiveRuntimeHostComposition(createExecutionRuntimeHostComposition), + composition: defineInteractiveRuntimeHostComposition(deterministicBackendComposition), }); owner = undefined; tui = await connect(root, 'tui'); @@ -235,3 +236,14 @@ function withTimeout(promise: Promise, timeoutMs: number, message: string) ); }); } + +/** + * The production composition registers no test backend; the deterministic one + * rides the same `primaryBackendFactory` seam Desktop E2E uses. + */ +const deterministicBackendComposition: RuntimeHostCompositionFactory = (context) => + createExecutionRuntimeHostComposition( + context, + {}, + { primaryBackendFactory: (backendContext) => new FakeBackend(backendContext) }, + ); diff --git a/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts b/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts index ab015a6322..2353c92fa9 100644 --- a/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts @@ -11,7 +11,7 @@ import { classifyTerminalRuntimeLedger, commitTerminalRunWithRuntimeFact, } from '@maka/runtime/terminal-run-commit'; -import { FakeBackend, FAKE_ASK_USER_QUESTION_PROMPT } from '@maka/runtime/fake-backend'; +import { FakeBackend, FAKE_ASK_USER_QUESTION_PROMPT } from '@maka/runtime/test-only/fake-backend'; import { IMPLEMENTATION_AGENT_DEFINITION, LOCAL_READ_AGENT_PROFILE, diff --git a/packages/runtime-host/src/__tests__/runtime-policy-coordinator.test.ts b/packages/runtime-host/src/__tests__/runtime-policy-coordinator.test.ts index 32f37eac2f..3adc25a24f 100644 --- a/packages/runtime-host/src/__tests__/runtime-policy-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/runtime-policy-coordinator.test.ts @@ -10,7 +10,7 @@ import type { CredentialLocator, } from '@maka/core/runtime-policy'; import { REQUEST_BODY_OVERLAY_MAX_BYTES } from '@maka/core/runtime-policy'; -import { FAKE_ASK_USER_QUESTION_PROMPT, FakeBackend } from '@maka/runtime/fake-backend'; +import { FAKE_ASK_USER_QUESTION_PROMPT, FakeBackend } from '@maka/runtime/test-only/fake-backend'; import { type MakaToolContext } from '@maka/runtime/tool-runtime'; import { openInteractiveExecutionStoresForWrite } from '@maka/storage/execution-stores'; import { openInteractiveRuntimePolicyStoresForWrite } from '@maka/storage/runtime-policy-stores'; @@ -105,19 +105,23 @@ test('production composition shares one gate across mutation and backend activat const setupStores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await setupStores.sessionStore.create({ cwd: root, - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', }); - composition = await createExecutionRuntimeHostComposition({ - owner, - hostEpoch: context.hostEpoch, - acquireResidency: context.acquireResidency, - retainUntilProcessExit: () => undefined, - requestDrain: () => undefined, - }); + composition = await createExecutionRuntimeHostComposition( + { + owner, + hostEpoch: context.hostEpoch, + acquireResidency: context.acquireResidency, + retainUntilProcessExit: () => undefined, + requestDrain: () => undefined, + }, + {}, + { primaryBackendFactory: (backendContext) => new FakeBackend(backendContext) }, + ); await composition.recover(); const initial = await composition.handlers['runtime.policy.query']({}, context); @@ -205,18 +209,22 @@ test('production mutation releases the gate before active-turn backend disposal const setupStores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await setupStores.sessionStore.create({ cwd: root, - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', }); - composition = await createExecutionRuntimeHostComposition({ - owner, - hostEpoch: context.hostEpoch, - acquireResidency: context.acquireResidency, - retainUntilProcessExit: () => undefined, - requestDrain: () => undefined, - }); + composition = await createExecutionRuntimeHostComposition( + { + owner, + hostEpoch: context.hostEpoch, + acquireResidency: context.acquireResidency, + retainUntilProcessExit: () => undefined, + requestDrain: () => undefined, + }, + {}, + { primaryBackendFactory: (backendContext) => new FakeBackend(backendContext) }, + ); await composition.recover(); const initial = await composition.handlers['runtime.policy.query']({}, context); @@ -302,21 +310,25 @@ test('production policy mutation drains and poisons activation when cached backe const setupStores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await setupStores.sessionStore.create({ cwd: root, - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', }); - composition = await createExecutionRuntimeHostComposition({ - owner, - hostEpoch: context.hostEpoch, - acquireResidency: context.acquireResidency, - retainUntilProcessExit: () => undefined, - requestDrain: () => { - drainRequests += 1; + composition = await createExecutionRuntimeHostComposition( + { + owner, + hostEpoch: context.hostEpoch, + acquireResidency: context.acquireResidency, + retainUntilProcessExit: () => undefined, + requestDrain: () => { + drainRequests += 1; + }, }, - }); + {}, + { primaryBackendFactory: (backendContext) => new FakeBackend(backendContext) }, + ); await composition.recover(); const firstTurnId = randomUUID(); diff --git a/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts b/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts index b0f3dc6163..68021f80ba 100644 --- a/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts +++ b/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts @@ -10,7 +10,7 @@ import { type AgentGraphOperatorProvisionRequest } from '@maka/core/agent-graph- import { type AgentRunHeader } from '@maka/core/agent-run'; import { type RuntimeEvent } from '@maka/core/runtime-event'; import { agentGraphIdForRootSession } from '@maka/runtime/stream-graph-coordinator'; -import { FAKE_ASK_USER_QUESTION_PROMPT } from '@maka/runtime/fake-backend'; +import { FAKE_ASK_USER_QUESTION_PROMPT } from '@maka/runtime/test-only/fake-backend'; import { createAgentGraphControlStore } from '@maka/storage/agent-graph-control-store'; import { openInteractiveArtifactStoreForWrite } from '@maka/storage/artifact-stores'; import { openInteractiveExecutionStoresForWrite } from '@maka/storage/execution-stores'; @@ -584,7 +584,7 @@ async function seedSource( const source = await execution.sessionStore.create({ cwd: root, name: 'Source Session', - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -592,7 +592,7 @@ async function seedSource( const busy = await execution.sessionStore.create({ cwd: root, name: 'Busy Session', - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -600,7 +600,7 @@ async function seedSource( const linkedChildSource = await execution.sessionStore.create({ cwd: root, name: 'Linked Child Source Session', - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -608,7 +608,7 @@ async function seedSource( const metadataLinkedSource = await execution.sessionStore.create({ cwd: root, name: 'Metadata-linked Source Session', - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -616,7 +616,7 @@ async function seedSource( const archivedOwnedSource = await execution.sessionStore.create({ cwd: root, name: 'Archived-owned Source Session', - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -624,7 +624,7 @@ async function seedSource( const continuationSource = await execution.sessionStore.create({ cwd: root, name: 'Continuation Source Session', - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -894,7 +894,7 @@ async function seedSource( { cwd: root, name: 'Graph Worker', - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -1132,7 +1132,7 @@ async function seedSource( const ordinaryLinkedChild = await execution.sessionStore.createSubagent({ cwd: root, name: 'Metadata-linked Child Session', - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/runtime-host/src/candidate-cli.ts b/packages/runtime-host/src/candidate-cli.ts index c6c7dc7a4f..dfa5319415 100644 --- a/packages/runtime-host/src/candidate-cli.ts +++ b/packages/runtime-host/src/candidate-cli.ts @@ -4,7 +4,6 @@ import { isCandidateStartupAttemptId } from './candidate-startup-failure.js'; export interface ParsedInteractiveRuntimeHostCandidateArguments extends InteractiveRuntimeHostCandidateOptions { readonly startupAttemptId: string; - readonly desktopE2e?: true; } export function parseInteractiveRuntimeHostCandidateArguments( @@ -18,7 +17,6 @@ export function parseInteractiveRuntimeHostCandidateArguments( 'idle-grace-ms', 'handshake-timeout-ms', 'generation', - 'desktop-e2e', ]); const values = new Map(); for (let index = 0; index < args.length; index += 2) { @@ -51,7 +49,6 @@ export function parseInteractiveRuntimeHostCandidateArguments( idleGraceMs: readOptionalInteger(values, 'idle-grace-ms'), handshakeTimeoutMs: readOptionalInteger(values, 'handshake-timeout-ms'), ...(values.has('generation') ? { generation: readGeneration(values) } : {}), - ...(values.has('desktop-e2e') ? { desktopE2e: readDesktopE2e(values) } : {}), }; } @@ -61,11 +58,6 @@ function readGeneration(values: Map): string { return value; } -function readDesktopE2e(values: Map): true { - if (values.get('desktop-e2e') !== '1') throw new Error('Invalid --desktop-e2e'); - return true; -} - function readOptionalInteger(values: Map, key: string): number | undefined { const raw = values.get(key); if (raw === undefined) return undefined; diff --git a/packages/runtime-host/src/candidate-entry.ts b/packages/runtime-host/src/candidate-entry.ts new file mode 100644 index 0000000000..a9a3af2555 --- /dev/null +++ b/packages/runtime-host/src/candidate-entry.ts @@ -0,0 +1,75 @@ +import { + candidateStartupFailureExitCode, + classifyCandidateStartupFailure, +} from './candidate-startup-failure.js'; +import { parseInteractiveRuntimeHostCandidateArguments } from './candidate-cli.js'; +import { writeCandidateStartupDiagnostic } from './control/startup-diagnostic.js'; +import { installRuntimeHostLogCapture, runtimeHostLogBuffer } from './process-diagnostics.js'; +import { + type ExecutionRuntimeHostCandidateDependencies, + type ExecutionRuntimeHostCandidateOptions, + startExecutionRuntimeHostCandidate, +} from './server/execution-candidate.js'; +import type { RuntimeHostKernel } from './server/host-kernel.js'; +import { runRuntimeHostProcessLifecycle } from './server/process-lifecycle.js'; + +export interface ExecutionCandidateEntryHooks { + /** Applied to the parsed command line before the candidate starts. */ + readonly overrideOptions?: ( + options: ExecutionRuntimeHostCandidateOptions, + ) => ExecutionRuntimeHostCandidateOptions; + readonly dependencies?: ExecutionRuntimeHostCandidateDependencies; + /** Runs once the candidate has won; the returned callback stops the watch. */ + readonly onWon?: (host: RuntimeHostKernel) => () => void; +} + +/** + * The bootstrap both candidate entry modules share. The entry file is the only + * thing that differs between the production and Desktop E2E processes, so the + * argument parsing, startup diagnostics, and lifecycle live here instead of + * being copied — a copy is what silently drifts when either one gains a step. + */ +export async function runExecutionCandidateEntry( + argv: readonly string[], + hooks: ExecutionCandidateEntryHooks = {}, +): Promise { + installRuntimeHostLogCapture(); + + let result: Awaited>; + let rootId: string | undefined; + let startupAttemptId: string | undefined; + try { + const parsed = parseInteractiveRuntimeHostCandidateArguments(argv); + rootId = parsed.expectedRootId; + const { startupAttemptId: parsedStartupAttemptId, ...options } = parsed; + startupAttemptId = parsedStartupAttemptId; + result = await startExecutionRuntimeHostCandidate( + hooks.overrideOptions ? hooks.overrideOptions(options) : options, + hooks.dependencies ?? {}, + ); + } catch (error) { + const failure = classifyCandidateStartupFailure(error); + const logs = runtimeHostLogBuffer.snapshot(); + console.error('[runtime-host] startup failed:', error); + if (rootId && startupAttemptId) { + await writeCandidateStartupDiagnostic({ + rootId, + startupAttemptId, + failure, + error, + logs, + }).catch(() => undefined); + } + process.exit(candidateStartupFailureExitCode(failure)); + } + if (result.kind === 'loser') process.exit(2); + + const stopWatch = hooks.onWon?.(result.host); + try { + await runRuntimeHostProcessLifecycle(result.host); + } catch { + process.exitCode = 1; + } finally { + stopWatch?.(); + } +} diff --git a/packages/runtime-host/src/client/connect-or-spawn.ts b/packages/runtime-host/src/client/connect-or-spawn.ts index 511edcea88..10ab806aa9 100644 --- a/packages/runtime-host/src/client/connect-or-spawn.ts +++ b/packages/runtime-host/src/client/connect-or-spawn.ts @@ -52,7 +52,6 @@ export interface ConnectOrSpawnRuntimeHostInput { connectTimeoutMs?: number; handshakeTimeoutMs?: number; candidateEntrypoint: string | URL; - desktopE2e?: boolean; signal?: AbortSignal; } @@ -265,7 +264,6 @@ export async function connectOrSpawnRuntimeHostWithDependencies( entrypoint: input.candidateEntrypoint, initialConnectionTimeoutMs: Math.ceil(remaining), ...(input.generation === undefined ? {} : { generation: input.generation }), - ...(input.desktopE2e ? { desktopE2e: true } : {}), }); const attempt = await settleBeforeDeadline(launch.spawned, deadline, input.signal); if (attempt.startupFailure) { diff --git a/packages/runtime-host/src/client/launcher.ts b/packages/runtime-host/src/client/launcher.ts index a68c4dcafb..5059e42b35 100644 --- a/packages/runtime-host/src/client/launcher.ts +++ b/packages/runtime-host/src/client/launcher.ts @@ -14,7 +14,6 @@ export interface DetachedCandidateInput { initialConnectionTimeoutMs?: number; idleGraceMs?: number; handshakeTimeoutMs?: number; - desktopE2e?: boolean; executable?: string; entrypoint: string | URL; env?: NodeJS.ProcessEnv; @@ -95,7 +94,6 @@ function spawnCandidate( appendArgument(args, '--idle-grace-ms', input.idleGraceMs); appendArgument(args, '--handshake-timeout-ms', input.handshakeTimeoutMs); appendArgument(args, '--generation', input.generation); - if (input.desktopE2e) args.push('--desktop-e2e', '1'); // spawn() commits the side effect synchronously; spawned only reports that commit's outcome. const child = spawn(executable, args, { diff --git a/packages/runtime-host/src/execution-candidate-main.ts b/packages/runtime-host/src/execution-candidate-main.ts index d492d51045..4c6cf4c39f 100644 --- a/packages/runtime-host/src/execution-candidate-main.ts +++ b/packages/runtime-host/src/execution-candidate-main.ts @@ -1,67 +1,4 @@ #!/usr/bin/env node -import { parseInteractiveRuntimeHostCandidateArguments } from './candidate-cli.js'; -import { - createDesktopE2eExecutionCandidateDependencies, - DESKTOP_E2E_IDLE_GRACE_MS, - watchDesktopE2eParentProcess, -} from './desktop-e2e-execution.js'; -import { startExecutionRuntimeHostCandidate } from './server/execution-candidate.js'; -import { runRuntimeHostProcessLifecycle } from './server/process-lifecycle.js'; -import { installRuntimeHostLogCapture, runtimeHostLogBuffer } from './process-diagnostics.js'; -import { - candidateStartupFailureExitCode, - classifyCandidateStartupFailure, -} from './candidate-startup-failure.js'; -import { writeCandidateStartupDiagnostic } from './control/startup-diagnostic.js'; +import { runExecutionCandidateEntry } from './candidate-entry.js'; -installRuntimeHostLogCapture(); - -let result: Awaited>; -let desktopE2e: true | undefined; -let rootId: string | undefined; -let startupAttemptId: string | undefined; -try { - const parsed = parseInteractiveRuntimeHostCandidateArguments(process.argv.slice(2)); - rootId = parsed.expectedRootId; - const { - desktopE2e: parsedDesktopE2e, - startupAttemptId: parsedStartupAttemptId, - ...parsedOptions - } = parsed; - startupAttemptId = parsedStartupAttemptId; - desktopE2e = parsedDesktopE2e; - const options = desktopE2e - ? { ...parsedOptions, idleGraceMs: DESKTOP_E2E_IDLE_GRACE_MS } - : parsedOptions; - result = await startExecutionRuntimeHostCandidate( - options, - desktopE2e ? createDesktopE2eExecutionCandidateDependencies() : {}, - ); -} catch (error) { - const failure = classifyCandidateStartupFailure(error); - const logs = runtimeHostLogBuffer.snapshot(); - console.error('[runtime-host] startup failed:', error); - if (rootId && startupAttemptId) { - await writeCandidateStartupDiagnostic({ - rootId, - startupAttemptId, - failure, - error, - logs, - }).catch(() => undefined); - } - process.exit(candidateStartupFailureExitCode(failure)); -} -if (result.kind === 'loser') process.exit(2); - -const stopParentWatch = desktopE2e - ? watchDesktopE2eParentProcess(() => result.host.close()) - : undefined; - -try { - await runRuntimeHostProcessLifecycle(result.host); -} catch { - process.exitCode = 1; -} finally { - stopParentWatch?.(); -} +await runExecutionCandidateEntry(process.argv.slice(2)); diff --git a/packages/runtime-host/src/server/execution-composition.ts b/packages/runtime-host/src/server/execution-composition.ts index c2ca7fea76..9d1a58fc1f 100644 --- a/packages/runtime-host/src/server/execution-composition.ts +++ b/packages/runtime-host/src/server/execution-composition.ts @@ -1,4 +1,8 @@ import { createHash, randomUUID } from 'node:crypto'; +import { + describeChatConfigurationReason, + NO_REAL_CONNECTION_CODE, +} from '@maka/core/connection-error-copy'; import type { RuntimeExecutionConnection } from '@maka/core/llm-connections'; import { generalizedErrorMessage } from '@maka/core/redaction'; import { emptyPlanSessionState } from '@maka/core/plan'; @@ -24,7 +28,6 @@ import { createFilesystemWorkerLaunchSpecProvider, FilesystemWorkerClient, } from '@maka/runtime/filesystem-worker'; -import { FakeBackend } from '@maka/runtime/fake-backend'; import { isOAuthEnrollmentProviderEnabled } from '@maka/runtime/oauth-provider-contracts'; import { loadHistoryCompactCheckpointsFromRunLedger, @@ -286,7 +289,18 @@ export async function createExecutionRuntimeHostComposition( }); await stores.messageReceiptStore.beginHostEpoch(context.hostEpoch); const backends = new BackendRegistry(); - backends.register('fake', (backendContext) => new FakeBackend(backendContext)); + // `fake` is a retired backend kind: this build never writes it, but a + // session or Automation persisted by an older one still can, and activation + // dispatches straight off that durable value. Registering an explicit + // refusal — rather than the test backend, or a read-path rewrite of the + // durable header — is what turns "no factory for kind=fake" into the + // product's existing answer for these rows: this task came from the retired + // local simulation, configure a real model and start a new one. + backends.register('fake', () => { + throw new Error( + `${NO_REAL_CONNECTION_CODE}:fake_backend: ${describeChatConfigurationReason('fake_backend')}`, + ); + }); const runtimePolicyActivation = new RuntimePolicyActivationGate(); const runtimePolicy = new HostRuntimePolicyCoordinator( runtimePolicyStores, diff --git a/packages/runtime-host/src/desktop-e2e-execution.ts b/packages/runtime-host/src/test-only/desktop-e2e-execution.ts similarity index 94% rename from packages/runtime-host/src/desktop-e2e-execution.ts rename to packages/runtime-host/src/test-only/desktop-e2e-execution.ts index f8e3d4895a..0a3aab8e83 100644 --- a/packages/runtime-host/src/desktop-e2e-execution.ts +++ b/packages/runtime-host/src/test-only/desktop-e2e-execution.ts @@ -3,10 +3,10 @@ import type { BackendCompactHistoryResult, } from '@maka/core/backend-types'; import { buildHistoryCompactCheckpoint } from '@maka/runtime/history-compact-checkpoint'; -import { FakeBackend } from '@maka/runtime/fake-backend'; +import { FakeBackend } from '@maka/runtime/test-only/fake-backend'; import { type BackendFactoryContext } from '@maka/runtime/session-manager'; -import type { ExecutionRuntimeHostCandidateDependencies } from './server/execution-candidate.js'; -import { createExecutionRuntimeHostComposition } from './server/execution-composition.js'; +import type { ExecutionRuntimeHostCandidateDependencies } from '../server/execution-candidate.js'; +import { createExecutionRuntimeHostComposition } from '../server/execution-composition.js'; /** Fresh Desktop E2E workspaces never reconnect; keep election retry, skip production grace. */ export const DESKTOP_E2E_IDLE_GRACE_MS = 500; diff --git a/packages/runtime-host/src/test-only/execution-candidate-e2e-main.ts b/packages/runtime-host/src/test-only/execution-candidate-e2e-main.ts new file mode 100644 index 0000000000..22c3a9d4f0 --- /dev/null +++ b/packages/runtime-host/src/test-only/execution-candidate-e2e-main.ts @@ -0,0 +1,22 @@ +#!/usr/bin/env node +/** + * Desktop E2E candidate entry. It exists so the production entry + * (`execution-candidate-main.ts`) carries no import of the E2E composition and + * no `--desktop-e2e` branch: the entry file IS the switch, which is what keeps + * FakeBackend and this bootstrap out of the release artifacts. + * + * The E2E run still goes through the real Runtime Host composition — only the + * `primaryBackendFactory` seam is substituted. + */ +import { runExecutionCandidateEntry } from '../candidate-entry.js'; +import { + createDesktopE2eExecutionCandidateDependencies, + DESKTOP_E2E_IDLE_GRACE_MS, + watchDesktopE2eParentProcess, +} from './desktop-e2e-execution.js'; + +await runExecutionCandidateEntry(process.argv.slice(2), { + overrideOptions: (options) => ({ ...options, idleGraceMs: DESKTOP_E2E_IDLE_GRACE_MS }), + dependencies: createDesktopE2eExecutionCandidateDependencies(), + onWon: (host) => watchDesktopE2eParentProcess(() => host.close()), +}); diff --git a/packages/runtime/README.md b/packages/runtime/README.md index 02da95c787..bd85a047b2 100644 --- a/packages/runtime/README.md +++ b/packages/runtime/README.md @@ -8,7 +8,7 @@ The package root barrel and the subpaths declared in `package.json` are supporte - `SessionManager` for session and turn orchestration. - `BackendRegistry` and `AgentBackend` for backend selection. -- `AiSdkBackend` and `FakeBackend` for the existing backend implementations. +- `AiSdkBackend` for the shipped backend implementation. `FakeBackend` is test-only: it lives under `test-only/`, is exported as `@maka/runtime/test-only/fake-backend`, and release packaging drops that directory, so no production module may import it. Tests and the Desktop E2E run reach it through the composition's `primaryBackendFactory` seam. - Session execution-boundary APIs for managed sandbox expansion and explicit bypass. - `buildBuiltinTools()` and the workspace executor interfaces for tool composition. - `RuntimeRunner`, runtime events, projections, and recovery helpers for invocation lifecycle. diff --git a/packages/runtime/package.json b/packages/runtime/package.json index e7ce3efa87..71bdc6e8bc 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -21,7 +21,7 @@ "./model-fetcher": "./dist/model-fetcher.js", "./materializer": "./dist/materializer.js", "./session-manager": "./dist/session-manager.js", - "./fake-backend": "./dist/fake-backend.js", + "./test-only/fake-backend": "./dist/test-only/fake-backend.js", "./test-only/observation-text-reader": "./dist/__tests__/observation-text-reader.js", "./filesystem-worker": "./dist/filesystem-worker/index.js", "./sandbox": "./dist/sandbox/index.js", diff --git a/packages/runtime/src/__tests__/fake-backend.test.ts b/packages/runtime/src/__tests__/fake-backend.test.ts index 42b072427c..a5a6e724dc 100644 --- a/packages/runtime/src/__tests__/fake-backend.test.ts +++ b/packages/runtime/src/__tests__/fake-backend.test.ts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; import type { SessionEvent } from '@maka/core/events'; import type { SessionHeader } from '@maka/core/session'; -import { FAKE_ASK_USER_QUESTION_PROMPT, FakeBackend } from '../fake-backend.js'; +import { FAKE_ASK_USER_QUESTION_PROMPT, FakeBackend } from '../test-only/fake-backend.js'; import { RuntimeInteractionInvariantError, bindRuntimeInteractionRun, diff --git a/packages/runtime/src/__tests__/runtime-continuation-crash.test.ts b/packages/runtime/src/__tests__/runtime-continuation-crash.test.ts index 2ae875d675..013f974376 100644 --- a/packages/runtime/src/__tests__/runtime-continuation-crash.test.ts +++ b/packages/runtime/src/__tests__/runtime-continuation-crash.test.ts @@ -15,7 +15,7 @@ import { createSqliteAgentRunStore } from '@maka/storage'; import { type RuntimeContinuationFailpoint } from '../agent-run.js'; import { BackendRegistry, SessionManager } from '../session-manager.js'; -import { FakeBackend } from '../fake-backend.js'; +import { FakeBackend } from '../test-only/fake-backend.js'; import { terminateChildProcessTree } from '../process-tree-terminator.js'; const CRASH_CHILD_ENV = 'MAKA_RUNTIME_CONTINUATION_CRASH_CHILD'; diff --git a/packages/runtime/src/__tests__/session-manager.test.ts b/packages/runtime/src/__tests__/session-manager.test.ts index b294de4031..3606749196 100644 --- a/packages/runtime/src/__tests__/session-manager.test.ts +++ b/packages/runtime/src/__tests__/session-manager.test.ts @@ -77,7 +77,7 @@ import { RuntimeKernel, type RuntimeKernelLike, } from '../runtime-kernel.js'; -import { FAKE_ASK_USER_QUESTION_PROMPT, FakeBackend } from '../fake-backend.js'; +import { FAKE_ASK_USER_QUESTION_PROMPT, FakeBackend } from '../test-only/fake-backend.js'; import { RuntimeReadModel, RuntimeReadModelError } from '../runtime-read-model.js'; import type { AgentBackend } from '@maka/core/backend-types'; import type { MakaTool } from '../tool-runtime.js'; diff --git a/packages/runtime/src/__tests__/stream-graph-coordinator.test.ts b/packages/runtime/src/__tests__/stream-graph-coordinator.test.ts index 9625d44fbc..e554377482 100644 --- a/packages/runtime/src/__tests__/stream-graph-coordinator.test.ts +++ b/packages/runtime/src/__tests__/stream-graph-coordinator.test.ts @@ -23,7 +23,7 @@ import { OPERATIONAL_STATE_DATABASE_NAME, } from '@maka/storage'; import { createSqliteAgentRunStore, createWorkspaceRuntimeStore } from '@maka/storage'; -import { FakeBackend } from '../fake-backend.js'; +import { FakeBackend } from '../test-only/fake-backend.js'; import { BackendRegistry, SessionManager } from '../session-manager.js'; import { SessionActivityRegistry } from '../goal-turn-lifecycle.js'; import { AgentGraphSupervisorWakeCoordinator } from '../agent-graph-supervisor-wake.js'; diff --git a/packages/runtime/src/fake-backend.ts b/packages/runtime/src/test-only/fake-backend.ts similarity index 99% rename from packages/runtime/src/fake-backend.ts rename to packages/runtime/src/test-only/fake-backend.ts index 5c7a555641..8bbb9552b6 100644 --- a/packages/runtime/src/fake-backend.ts +++ b/packages/runtime/src/test-only/fake-backend.ts @@ -16,8 +16,8 @@ import type { UserQuestionResponse } from '@maka/core/user-question'; import { RuntimeInteractionInvariantError, type RuntimeUserQuestionClosureReason, -} from './interaction-authority.js'; -import type { SessionStore } from './session-manager.js'; +} from '../interaction-authority.js'; +import type { SessionStore } from '../session-manager.js'; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export const FAKE_ASK_USER_QUESTION_PROMPT = '__e2e_ask_user_question__'; diff --git a/scripts/release-cli-file-policy.mjs b/scripts/release-cli-file-policy.mjs index ac40480439..3252b5d4b3 100644 --- a/scripts/release-cli-file-policy.mjs +++ b/scripts/release-cli-file-policy.mjs @@ -23,9 +23,15 @@ export function isThirdPartyDevelopmentArtifact(relativePath) { ); } +// Modules that only a test or E2E entry point may import. They are a +// Maka-owned convention, so they are not part of DEVELOPMENT_DIRECTORIES: a +// third-party package is free to ship a directory by that name. +const MAKA_TEST_ONLY_DIRECTORY = 'test-only'; + export function isMakaDevelopmentArtifact(relativePath) { const segments = relativePath.split(/[\\/]/).filter(Boolean); if (segments.some((segment) => segment === 'src')) return true; + if (segments.some((segment) => segment === MAKA_TEST_ONLY_DIRECTORY)) return true; if (segments.some((segment) => DEVELOPMENT_DIRECTORIES.has(segment))) return true; const file = segments.at(-1) ?? ''; diff --git a/scripts/release-cli-file-policy.test.mjs b/scripts/release-cli-file-policy.test.mjs index 64776b96ba..53ea3d6f3b 100644 --- a/scripts/release-cli-file-policy.test.mjs +++ b/scripts/release-cli-file-policy.test.mjs @@ -38,4 +38,19 @@ describe('CLI release file policy', () => { assert.equal(isMakaDevelopmentArtifact('dist/__tests__/fixture.js'), true); assert.equal(isMakaDevelopmentArtifact('dist/index.js'), false); }); + + test('rejects Maka test-only modules so no test backend can ship', () => { + for (const path of [ + 'dist/test-only/fake-backend.js', + 'dist/test-only/execution-candidate-e2e-main.js', + String.raw`dist\test-only\desktop-e2e-execution.js`, + ]) { + assert.equal(isMakaDevelopmentArtifact(path), true, path); + } + assert.equal(isMakaDevelopmentArtifact('dist/execution-candidate-main.js'), false); + }); + + test('leaves a third-party test-only directory alone', () => { + assert.equal(isThirdPartyDevelopmentArtifact('dist/test-only/index.js'), false); + }); }); diff --git a/scripts/release-cli-package.mjs b/scripts/release-cli-package.mjs index 88bec20310..fe8d2e2e6f 100644 --- a/scripts/release-cli-package.mjs +++ b/scripts/release-cli-package.mjs @@ -369,7 +369,12 @@ function copyRuntimeDist(source, destination, options = {}) { copyTreeFiles(sourceDist, join(destination, 'dist'), (relativePath) => { const segments = relativePath.split(sep); const file = segments.at(-1) ?? ''; - if (segments.some((segment) => segment === '__tests__' || segment === '__fixtures__')) { + if ( + segments.some( + (segment) => + segment === '__tests__' || segment === '__fixtures__' || segment === 'test-only', + ) + ) { return false; } if (/(?:^|\.)test\.js$/.test(file) || file.endsWith('.d.ts') || file.endsWith('.map')) {