mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
test: remove capability inventories and projection receipts (#36018)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -3141,7 +3141,6 @@
|
||||
"src/main/ipc/repos/repo-creation-git-availability.test.ts": 124,
|
||||
"src/main/ipc/repos/repo-ipc-wsl-move-fence.test.ts": 2855,
|
||||
"src/main/ipc/repos/repo-remove-for-host-teardown.test.ts": 423,
|
||||
"src/main/ipc/runtime-agent-launch-capability.test.ts": 1720,
|
||||
"src/main/ipc/runtime-environments/orcad-delta-move-handlers.test.ts": 949,
|
||||
"src/main/ipc/runtime-environments/orcad-runtime-conversion-handlers.test.ts": 569,
|
||||
"src/main/ipc/runtime-environments/orcad-runtime-lifecycle-handlers.test.ts": 32,
|
||||
@@ -3989,7 +3988,6 @@
|
||||
"src/main/opencode/overlay-dir-gc-lifecycle.test.ts": 67,
|
||||
"src/main/opencode/overlay-dir-gc.test.ts": 81,
|
||||
"src/main/opencode/overlay-source-ownership.test.ts": 78,
|
||||
"src/main/opencode/status-plugin-shared-server-capability.test.ts": 42,
|
||||
"src/main/opencode2/status-plugin-setup-lifetime.test.ts": 110,
|
||||
"src/main/orca-profiles/other-profile-workspace-catalog.test.ts": 377,
|
||||
"src/main/orca-profiles/profile-active-transfer-worker.test.ts": 2010,
|
||||
@@ -5459,7 +5457,6 @@
|
||||
"src/main/runtime/rpc/methods/orchestration/worker/worker-start-prompt-contract.test.ts": 3293,
|
||||
"src/main/runtime/rpc/methods/orchestration/worker/worker-start-readiness-settlement.test.ts": 541,
|
||||
"src/main/runtime/rpc/methods/orchestration/worker/worker-start-turn-observation.test.ts": 43,
|
||||
"src/main/runtime/rpc/methods/orchestration/worker/worker-stop-capability.test.ts": 478,
|
||||
"src/main/runtime/rpc/methods/orchestration/worker/worker-stop-exit-race.test.ts": 3668,
|
||||
"src/main/runtime/rpc/methods/orchestration/worker/worker-stop-liveness-verdict.test.ts": 3343,
|
||||
"src/main/runtime/rpc/methods/orchestration/worker/worker-terminal-custody-at-creation.test.ts": 3239,
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
/** Exercise advertised IPC capabilities through the real host gate, including the local-renderer launch path. */
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { RuntimeCapability } from '../../shared/protocol-version'
|
||||
import { AGENT_LAUNCH_RUNTIME_CAPABILITY } from '../../shared/agent-launch-runtime-capability'
|
||||
import {
|
||||
resolveRpcCallerIdentity,
|
||||
rpcCallerOperationKey,
|
||||
type RpcCallerIdentity
|
||||
} from '../runtime/rpc/rpc-caller-identity'
|
||||
|
||||
type AdvertisedClient = {
|
||||
clientKind?: 'mobile' | 'runtime'
|
||||
clientCapabilities?: readonly RuntimeCapability[]
|
||||
caller?: RpcCallerIdentity
|
||||
pairedDeviceId?: string
|
||||
}
|
||||
|
||||
const { handlers, advertised } = vi.hoisted(
|
||||
(): {
|
||||
handlers: Map<string, (event: unknown, args?: unknown) => unknown>
|
||||
advertised: { unary: AdvertisedClient[]; streaming: AdvertisedClient[] }
|
||||
} => ({
|
||||
handlers: new Map(),
|
||||
advertised: { unary: [], streaming: [] }
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
BrowserWindow: { fromWebContents: vi.fn() },
|
||||
ipcMain: {
|
||||
handle: vi.fn((channel: string, handler: (event: unknown, args?: unknown) => unknown) => {
|
||||
handlers.set(channel, handler)
|
||||
}),
|
||||
on: vi.fn(),
|
||||
removeAllListeners: vi.fn(),
|
||||
removeHandler: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('../runtime/rpc/dispatcher', () => ({
|
||||
RpcDispatcher: class {
|
||||
dispatch(_request: unknown, options: AdvertisedClient): Promise<unknown> {
|
||||
advertised.unary.push(options)
|
||||
return Promise.resolve({ ok: true, result: {} })
|
||||
}
|
||||
|
||||
dispatchStreaming(
|
||||
_request: unknown,
|
||||
_emit: (response: string) => void,
|
||||
options: AdvertisedClient
|
||||
): Promise<void> {
|
||||
advertised.streaming.push(options)
|
||||
// Keep streaming open; the handler attaches only a cleanup callback.
|
||||
return new Promise<void>(() => {})
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
const { registerRuntimeHandlers } = await import('./runtime')
|
||||
const { supportsAgentLaunch } = await import('../runtime/rpc/methods/agent-launch')
|
||||
const { agentLaunchOperationCallerKey } = await import('../runtime/rpc/methods/agent-launch-replay')
|
||||
|
||||
function rendererEvent() {
|
||||
const mainFrame = {}
|
||||
return {
|
||||
sender: { id: 1, mainFrame, on: vi.fn(), once: vi.fn(), isDestroyed: () => false },
|
||||
senderFrame: mainFrame
|
||||
}
|
||||
}
|
||||
|
||||
function invoke(channel: string, args: unknown): void {
|
||||
const handler = handlers.get(channel)
|
||||
if (!handler) {
|
||||
throw new Error(`no handler registered for ${channel}`)
|
||||
}
|
||||
void handler(rendererEvent(), args)
|
||||
}
|
||||
|
||||
/** Missing dispatch must fail rather than pass the gate via undefined clientKind. */
|
||||
function onlyAdvertisedClient(records: readonly AdvertisedClient[]): AdvertisedClient {
|
||||
const client = records[0]
|
||||
if (!client) {
|
||||
throw new Error('the handler dispatched nothing')
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
function withoutLaunchCapability(client: AdvertisedClient): AdvertisedClient {
|
||||
return {
|
||||
clientKind: client.clientKind,
|
||||
clientCapabilities: client.clientCapabilities?.filter(
|
||||
(capability) => capability !== AGENT_LAUNCH_RUNTIME_CAPABILITY
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
describe('desktop renderer reaching agent.launch on its own main process', () => {
|
||||
beforeEach(() => {
|
||||
handlers.clear()
|
||||
advertised.unary.length = 0
|
||||
advertised.streaming.length = 0
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: registerRuntimeHandlers reaches only the sender-lifecycle cleanup on this path; any other runtime method it called would throw here rather than read a wrong value.
|
||||
registerRuntimeHandlers({ cleanupSubscriptionsForConnection: vi.fn() } as never)
|
||||
})
|
||||
|
||||
it('advertises the launch capability on the unary path', () => {
|
||||
invoke('runtime:call', {
|
||||
method: 'agent.launch',
|
||||
params: { agent: 'claude', target: { kind: 'existing', worktree: 'id:wt-7' } }
|
||||
})
|
||||
|
||||
const client = onlyAdvertisedClient(advertised.unary)
|
||||
expect(client.clientKind).toBe('runtime')
|
||||
expect(supportsAgentLaunch(client)).toBe(true)
|
||||
expect(supportsAgentLaunch(withoutLaunchCapability(client))).toBe(false)
|
||||
})
|
||||
|
||||
it('advertises the same set on the streaming path', () => {
|
||||
invoke('runtime:call', { method: 'status.get' })
|
||||
invoke('runtime:subscribe', { subscriptionId: 'sub-1', method: 'session.tabs.watch' })
|
||||
|
||||
const streaming = onlyAdvertisedClient(advertised.streaming)
|
||||
expect(streaming.clientCapabilities).toEqual(
|
||||
onlyAdvertisedClient(advertised.unary).clientCapabilities
|
||||
)
|
||||
expect(supportsAgentLaunch(streaming)).toBe(true)
|
||||
})
|
||||
|
||||
it('names the desktop as its caller on both paths, so its launches replay under one identity', () => {
|
||||
invoke('runtime:call', { method: 'status.get' })
|
||||
invoke('runtime:subscribe', { subscriptionId: 'sub-1', method: 'session.tabs.watch' })
|
||||
|
||||
for (const client of [advertised.unary, advertised.streaming].map(onlyAdvertisedClient)) {
|
||||
const caller = resolveRpcCallerIdentity(client)
|
||||
expect(caller).toEqual({ kind: 'desktop' })
|
||||
expect(agentLaunchOperationCallerKey({ caller })).toBe('trusted-local:desktop')
|
||||
}
|
||||
const { caller: _named, ...unnamed } = onlyAdvertisedClient(advertised.unary)
|
||||
expect(resolveRpcCallerIdentity(unnamed)).toBeUndefined()
|
||||
expect(rpcCallerOperationKey({ kind: 'local-cli' })).toBe('trusted-local:runtime')
|
||||
})
|
||||
})
|
||||
@@ -1,53 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getStatusPluginPostSource } from './status-plugin-post-source'
|
||||
import { getStatusPluginRuntimeStateSource } from './status-plugin-runtime-state-source'
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
vi.unstubAllGlobals()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
describe('generated shared-server capability without a test-only environment flag', () => {
|
||||
it.each([
|
||||
{ argv: ['opencode', 'serve'], major: 0, capability: '1', expected: true },
|
||||
{
|
||||
argv: ['opencode', '/embedded/cli.js', '--log-level', 'none', 'serve'],
|
||||
major: 0,
|
||||
capability: '1',
|
||||
expected: true
|
||||
},
|
||||
{
|
||||
argv: ['opencode', '--log-level', 'serve', 'run', 'serve'],
|
||||
major: 0,
|
||||
capability: '1',
|
||||
expected: false
|
||||
},
|
||||
{ argv: ['opencode', 'run', '--', 'serve'], major: 0, capability: '1', expected: false },
|
||||
{ argv: ['opencode', '--', 'serve'], major: 0, capability: '1', expected: false },
|
||||
{ argv: ['opencode', 'serve'], major: 2, capability: '1', expected: false },
|
||||
{ argv: ['opencode', 'serve'], major: 0, capability: '', expected: false }
|
||||
])(
|
||||
'recognizes only an opted-in legacy serve context: %j',
|
||||
async ({ argv, major, capability, expected }) => {
|
||||
vi.stubEnv('ORCA_PANE_KEY', 'tab-a:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa')
|
||||
vi.stubEnv('ORCA_OPENCODE_PLUGIN_API', undefined)
|
||||
vi.spyOn(process, 'argv', 'get').mockReturnValue(argv)
|
||||
const posts: Record<string, unknown>[] = []
|
||||
vi.stubGlobal('fetch', async (_input: unknown, init: RequestInit) => {
|
||||
posts.push(JSON.parse(String(init.body)))
|
||||
return new Response('{}', { status: 200 })
|
||||
})
|
||||
const source = [
|
||||
...getStatusPluginRuntimeStateSource(),
|
||||
...getStatusPluginPostSource('/hook/opencode'),
|
||||
`reportingOpenCodeMajor = ${major};`,
|
||||
'return post("SessionBusy", { sessionID: "ses_test" });'
|
||||
].join('\n')
|
||||
const run = new Function('resolveHookCoords', source)
|
||||
await run(() => ({ port: 12345, token: 'fixture', openCodeTui: capability }))
|
||||
expect(posts).toHaveLength(1)
|
||||
expect(posts[0].opencodeSharedServer === 1).toBe(expected)
|
||||
expect(posts[0].opencodeMajor).toBe(major || undefined)
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -1,42 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { pluginMarketplaceListingSchema } from '../../../shared/plugins/catalog/plugin-marketplace'
|
||||
import { pluginMarketplaceListing } from './plugin-marketplace-projection'
|
||||
|
||||
describe('pluginMarketplaceListing trust', () => {
|
||||
const listing = (catalogUrl: string, pluginKey: string) => {
|
||||
const source = { kind: 'git' as const, url: catalogUrl, ref: 'main' }
|
||||
const marketplace = pluginMarketplaceListingSchema.parse({
|
||||
name: 'Orca Plugins',
|
||||
owner: 'stablyai',
|
||||
plugins: [
|
||||
{
|
||||
id: pluginKey,
|
||||
source: { kind: 'git', url: 'https://github.com/stablyai/orca-plugins.git', ref: 'v1' }
|
||||
}
|
||||
]
|
||||
})
|
||||
return pluginMarketplaceListing({
|
||||
source: { id: 'a'.repeat(32), source, addedAt: 0 },
|
||||
snapshot: {
|
||||
schemaVersion: 2,
|
||||
sourceId: 'a'.repeat(32),
|
||||
source,
|
||||
marketplaceCommit: 'b'.repeat(40),
|
||||
fetchedAt: 0,
|
||||
marketplace
|
||||
},
|
||||
entry: marketplace.plugins[0]!,
|
||||
hostVersion: null,
|
||||
blocked: null
|
||||
})
|
||||
}
|
||||
|
||||
it('marks official what the official catalog lists, whatever the id', () => {
|
||||
const official = 'https://github.com/stablyai/orca-plugins.git'
|
||||
expect(listing(official, 'stablyai.spreadsheet').official).toBe(true)
|
||||
expect(listing(official, 'stablyai.orca-gleam').official).toBe(true)
|
||||
expect(
|
||||
listing('https://github.com/acme/orca-plugins.git', 'stablyai.orca-gleam').official
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -67,89 +67,7 @@ function serviceWith(
|
||||
} as unknown as PluginService
|
||||
}
|
||||
|
||||
describe('buildPluginList contribution icons', () => {
|
||||
it('projects editor and new-file icons as installed image URLs, and new files without paths', async () => {
|
||||
const drawManifest = pluginManifestSchema.parse({
|
||||
manifestVersion: 2,
|
||||
execution: 'sandboxed-ui',
|
||||
id: 'draw',
|
||||
publisher: 'samples',
|
||||
name: 'Draw',
|
||||
version: '1.0.0',
|
||||
engines: { orca: '>=1.4.245' },
|
||||
contributes: {
|
||||
customEditors: [
|
||||
{ id: 'draw', title: 'Draw', extensions: ['.draw'], entry: 'e.html', icon: 'tab.png' }
|
||||
],
|
||||
newFiles: [
|
||||
{ id: 'drawing', title: 'New drawing', extension: '.draw', template: 'empty.draw' }
|
||||
]
|
||||
}
|
||||
})
|
||||
const hash = 'c'.repeat(64)
|
||||
const [entry] = await buildPluginList(
|
||||
serviceWith({
|
||||
pluginKey: 'samples.draw',
|
||||
rootDir: join(tmpdir(), 'plugins', 'draw'),
|
||||
manifest: drawManifest,
|
||||
consentFingerprint: 'sha256-current',
|
||||
consentContentHash: hash,
|
||||
contentHash: hash,
|
||||
isDev: false
|
||||
}),
|
||||
emptyPluginLockfile()
|
||||
)
|
||||
expect(entry?.customEditors?.[0]?.icon).toBe(
|
||||
`orca-plugin-image://installed/samples.draw/${hash}/editor-0`
|
||||
)
|
||||
// Why no template path: clients ask main for the text, never for a file.
|
||||
expect(entry?.newFiles).toEqual([{ id: 'drawing', title: 'New drawing', extension: '.draw' }])
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildPluginList consent identity', () => {
|
||||
it('projects the exact current fingerprint for an optimistic consent write', async () => {
|
||||
const plugin: ValidDiscoveredPlugin = {
|
||||
pluginKey: 'samples.demo',
|
||||
rootDir: join(tmpdir(), 'plugins', 'demo'),
|
||||
manifest,
|
||||
consentFingerprint: 'sha256-current',
|
||||
contentHash: null,
|
||||
isDev: true
|
||||
}
|
||||
|
||||
expect((await buildPluginList(serviceWith(plugin), emptyPluginLockfile()))[0]).toMatchObject({
|
||||
pluginKey: plugin.pluginKey,
|
||||
consentFingerprint: 'sha256-current',
|
||||
status: 'pending',
|
||||
// Why declarative: a capability makes it more than content, with no worker in sight.
|
||||
execution: 'declarative'
|
||||
})
|
||||
})
|
||||
|
||||
it('projects supervised backoff as restarting instead of running', async () => {
|
||||
const plugin: ValidDiscoveredPlugin = {
|
||||
pluginKey: 'samples.demo',
|
||||
rootDir: join(tmpdir(), 'plugins', 'demo'),
|
||||
manifest,
|
||||
consentFingerprint: 'sha256-current',
|
||||
contentHash: null,
|
||||
isDev: true
|
||||
}
|
||||
|
||||
expect(
|
||||
(
|
||||
await buildPluginList(
|
||||
serviceWith(plugin, {
|
||||
activation: 'approved',
|
||||
worker: { state: 'restarting', restarts: 2 }
|
||||
}),
|
||||
emptyPluginLockfile()
|
||||
)
|
||||
)[0]
|
||||
).toMatchObject({ status: 'restarting', restarts: 2 })
|
||||
})
|
||||
|
||||
it('does not attribute a shadowing dev plugin to the installed source', async () => {
|
||||
const plugin: ValidDiscoveredPlugin = {
|
||||
pluginKey: 'samples.demo',
|
||||
@@ -193,104 +111,6 @@ describe('buildPluginList consent identity', () => {
|
||||
expect(projected.name).toBe('invalid-development-plugin-1')
|
||||
expect(JSON.stringify(projected)).not.toContain(invalid.rootDir)
|
||||
})
|
||||
|
||||
it('projects exact VM lifecycle commands for declarative consent', async () => {
|
||||
const recipeManifest = pluginManifestSchema.parse({
|
||||
...manifest,
|
||||
contributes: { vmRecipes: [{ path: 'recipes/cloud.json' }] }
|
||||
})
|
||||
const plugin: ValidDiscoveredPlugin = {
|
||||
pluginKey: 'samples.demo',
|
||||
rootDir: join(tmpdir(), 'plugins', 'demo'),
|
||||
manifest: recipeManifest,
|
||||
consentFingerprint: 'sha256-current',
|
||||
consentContentHash: 'a'.repeat(64),
|
||||
contentHash: null,
|
||||
isDev: true
|
||||
}
|
||||
|
||||
expect(
|
||||
(
|
||||
await buildPluginList(
|
||||
serviceWith(plugin, {
|
||||
vmRecipes: [
|
||||
{
|
||||
pluginKey: plugin.pluginKey,
|
||||
recipe: {
|
||||
id: 'cloud',
|
||||
name: 'Cloud',
|
||||
create: './create.sh',
|
||||
destroyDisabled: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}),
|
||||
emptyPluginLockfile()
|
||||
)
|
||||
)[0]?.vmRecipes
|
||||
).toEqual([
|
||||
{
|
||||
id: 'cloud',
|
||||
name: 'Cloud',
|
||||
commands: [
|
||||
{ phase: 'create', command: './create.sh' },
|
||||
{ phase: 'destroy', command: 'none' }
|
||||
]
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('projects command handlers and normalized keybindings for consent and dispatch', async () => {
|
||||
const commandManifest = pluginManifestSchema.parse({
|
||||
...manifest,
|
||||
contributes: {
|
||||
commands: [
|
||||
{ id: 'tasks', title: 'Open Tasks', context: 'workspace', action: 'view.tasks' }
|
||||
],
|
||||
keybindings: [{ command: 'tasks', key: 'mod+alt+t' }]
|
||||
}
|
||||
})
|
||||
const plugin: ValidDiscoveredPlugin = {
|
||||
pluginKey: 'samples.demo',
|
||||
rootDir: join(tmpdir(), 'plugins', 'demo'),
|
||||
manifest: commandManifest,
|
||||
consentFingerprint: 'sha256-current',
|
||||
consentContentHash: 'a'.repeat(64),
|
||||
contentHash: null,
|
||||
isDev: true
|
||||
}
|
||||
|
||||
const [entry] = await buildPluginList(serviceWith(plugin), emptyPluginLockfile())
|
||||
// Why: a worker-less commands plugin still runs instructions, so the page must not say content only.
|
||||
expect(entry).toMatchObject({ hasWorker: false, execution: 'declarative' })
|
||||
expect(
|
||||
(
|
||||
await buildPluginList(
|
||||
serviceWith(plugin, {
|
||||
commands: [
|
||||
{
|
||||
pluginKey: plugin.pluginKey,
|
||||
id: 'tasks',
|
||||
title: 'Open Tasks',
|
||||
context: 'workspace',
|
||||
handler: { type: 'built-in', action: 'view.tasks' },
|
||||
keybindings: [{ key: 'Mod+Alt+T', when: 'workspace' }]
|
||||
}
|
||||
]
|
||||
}),
|
||||
emptyPluginLockfile()
|
||||
)
|
||||
)[0]?.commands
|
||||
).toEqual([
|
||||
{
|
||||
id: 'tasks',
|
||||
title: 'Open Tasks',
|
||||
context: 'workspace',
|
||||
handler: { type: 'built-in', action: 'view.tasks' },
|
||||
keybindings: [{ key: 'Mod+Alt+T', when: 'workspace' }]
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildPluginList I/O', () => {
|
||||
@@ -320,41 +140,4 @@ describe('buildPluginList I/O', () => {
|
||||
expect(entry?.source?.kind).toBe('git')
|
||||
expect(fsCalls.count).toBe(0)
|
||||
})
|
||||
|
||||
it('reports the catalog ref a marketplace install came from', async () => {
|
||||
const plugin: ValidDiscoveredPlugin = {
|
||||
pluginKey: 'samples.demo',
|
||||
rootDir: join(tmpdir(), 'plugins', 'samples.demo', 'a'.repeat(64)),
|
||||
manifest,
|
||||
consentFingerprint: 'sha256-current',
|
||||
contentHash: 'a'.repeat(64),
|
||||
isDev: false
|
||||
}
|
||||
const lock = emptyPluginLockfile()
|
||||
lock.plugins[plugin.pluginKey] = {
|
||||
pluginKey: plugin.pluginKey,
|
||||
version: '1.0.0',
|
||||
source: {
|
||||
kind: 'marketplace',
|
||||
marketplace: {
|
||||
url: 'https://github.com/acme/catalog.git',
|
||||
ref: 'stable',
|
||||
resolvedCommit: 'f'.repeat(40)
|
||||
},
|
||||
plugin: { url: 'https://github.com/samples/demo.git', ref: 'main' }
|
||||
},
|
||||
resolvedCommit: 'c'.repeat(40),
|
||||
contentHash: 'a'.repeat(64),
|
||||
consentFingerprint: 'sha256-current',
|
||||
installedAt: 1
|
||||
}
|
||||
|
||||
const [entry] = await buildPluginList(serviceWith(plugin), lock)
|
||||
|
||||
expect(entry?.source?.marketplace).toEqual({
|
||||
reference: 'https://github.com/acme/catalog.git',
|
||||
ref: 'stable',
|
||||
resolvedCommit: 'f'.repeat(40)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { createRuntime } from '../orca-runtime-test-fixtures.spec'
|
||||
import { resetBundledMobileWebBundleCacheForTests } from '../bundled-mobile-web-bundle'
|
||||
import {
|
||||
installMobileWebBundleAppPath,
|
||||
writeSyntheticMobileWebBundle
|
||||
} from '../rpc/methods/mobile-web-bundle.test-fixture'
|
||||
import { MOBILE_WEB_BUNDLE_CAPABILITY } from '../../../shared/mobile-web-bundle/mobile-web-bundle-capability'
|
||||
|
||||
// The aggregate shares this process cache; each fixture must restore it.
|
||||
function statusCapabilitiesForInstall(appPath: string): readonly string[] {
|
||||
resetBundledMobileWebBundleCacheForTests()
|
||||
installMobileWebBundleAppPath(appPath)
|
||||
const { capabilities } = createRuntime().getStatus()
|
||||
if (!capabilities) {
|
||||
// A missing capability list would make absence assertions pass vacuously.
|
||||
throw new Error('status.get answered no capability list')
|
||||
}
|
||||
return capabilities
|
||||
}
|
||||
|
||||
function withInstallRoot(prefix: string, run: (install: string) => void): void {
|
||||
const install = mkdtempSync(join(tmpdir(), prefix))
|
||||
try {
|
||||
run(install)
|
||||
} finally {
|
||||
rmSync(install, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
describe('OrcaRuntimeService mobile web bundle capability', () => {
|
||||
afterEach(() => {
|
||||
resetBundledMobileWebBundleCacheForTests()
|
||||
})
|
||||
|
||||
it('omits the mobile web bundle capability where the install carries no bundle', () => {
|
||||
withInstallRoot('orca-mobile-web-absent-', (install) => {
|
||||
expect(statusCapabilitiesForInstall(install)).not.toContain(MOBILE_WEB_BUNDLE_CAPABILITY)
|
||||
})
|
||||
})
|
||||
|
||||
it('advertises the mobile web bundle capability where the install carries one', () => {
|
||||
withInstallRoot('orca-mobile-web-present-', (install) => {
|
||||
writeSyntheticMobileWebBundle(join(install, 'out', 'mobile-web'), 4)
|
||||
expect(statusCapabilitiesForInstall(install)).toContain(MOBILE_WEB_BUNDLE_CAPABILITY)
|
||||
})
|
||||
})
|
||||
|
||||
it('omits the capability where the bundle manifest does not parse', () => {
|
||||
withInstallRoot('orca-mobile-web-unparseable-', (install) => {
|
||||
const root = join(install, 'out', 'mobile-web')
|
||||
writeSyntheticMobileWebBundle(root, 5)
|
||||
writeFileSync(join(root, 'manifest.json'), '{"schemaVersion":2}', 'utf8')
|
||||
expect(statusCapabilitiesForInstall(install)).not.toContain(MOBILE_WEB_BUNDLE_CAPABILITY)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -12,7 +12,6 @@ await import('./orca-runtime-tests/paired-settings.spec')
|
||||
await import('./orca-runtime-tests/runtime-availability.spec')
|
||||
await import('./orca-runtime-tests/browser-capabilities.spec')
|
||||
await import('./orca-runtime-tests/browser-capabilities-part-02.spec')
|
||||
await import('./orca-runtime-tests/mobile-web-bundle-capability.spec')
|
||||
await import('./orca-runtime-tests/managed-server-capability.spec')
|
||||
await import('./orca-runtime-tests/window-authority.spec')
|
||||
await import('./orca-runtime-tests/terminal-handles.spec')
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { ORCHESTRATION_CONTRACT_RUNTIME_CAPABILITY } from '../../../../../../shared/protocol-version'
|
||||
import type { OrcaRuntimeService } from '../../../../orca-runtime'
|
||||
import type { OrchestrationDb } from '../../../../orchestration/db'
|
||||
import { ORCHESTRATION_WORKER_STOP_METHODS } from './worker-stop'
|
||||
|
||||
describe('federated worker stop capability', () => {
|
||||
it('does not trust a legacy server stop receipt', async () => {
|
||||
const markWorkerStopUnknown = vi.fn((_dispatchId: string, reason: string) => ({
|
||||
state: 'stop_unknown',
|
||||
last_error: reason
|
||||
}))
|
||||
const db = {
|
||||
getFederatedDispatch: vi.fn(() => ({
|
||||
dispatch_id: 'ctx_remote',
|
||||
environment_id: 'environment_linux',
|
||||
environment_name: 'linux',
|
||||
peer_fingerprint: 'peer-linux'
|
||||
})),
|
||||
beginWorkerStop: vi.fn(() => ({ disposition: 'started' })),
|
||||
markWorkerStopUnknown
|
||||
} as unknown as OrchestrationDb
|
||||
const callOrchestrationWorkerServer = vi.fn(async (_environmentId, method) => {
|
||||
if (method === 'status.get') {
|
||||
return { capabilities: [ORCHESTRATION_CONTRACT_RUNTIME_CAPABILITY] }
|
||||
}
|
||||
return { state: 'stopped', processAction: 'closed_agent_terminal' }
|
||||
})
|
||||
const runtime = {
|
||||
getOrchestrationDb: () => db,
|
||||
getRuntimeId: () => 'runtime_current',
|
||||
resolveOrchestrationWorkerServer: () => ({
|
||||
environmentId: 'environment_linux',
|
||||
name: 'linux',
|
||||
peerFingerprint: 'peer-linux'
|
||||
}),
|
||||
callOrchestrationWorkerServer
|
||||
} as unknown as OrcaRuntimeService
|
||||
const method = ORCHESTRATION_WORKER_STOP_METHODS[0]!
|
||||
|
||||
await expect(
|
||||
method.handler(method.params!.parse({ dispatch: 'ctx_remote' }), {
|
||||
runtime,
|
||||
orchestrationMutation: {
|
||||
callerFingerprint: 'coordinator',
|
||||
requestId: 'request_stop',
|
||||
method: 'orchestration.workerStop',
|
||||
payloadHash: 'hash'
|
||||
}
|
||||
})
|
||||
).resolves.toMatchObject({ state: 'stop_unknown', processAction: 'none' })
|
||||
expect(db.beginWorkerStop).toHaveBeenCalledWith('ctx_remote', 'runtime_current')
|
||||
expect(markWorkerStopUnknown).toHaveBeenCalledWith(
|
||||
'ctx_remote',
|
||||
'Connected server linux cannot prove the worker stop outcome.'
|
||||
)
|
||||
expect(callOrchestrationWorkerServer).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -1,129 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { ALL_RPC_METHODS } from './methods'
|
||||
import { buildRegistry } from './core'
|
||||
import { ACCOUNT_METHODS } from './methods/accounts'
|
||||
import { resolveOrcadFeatureRequirement, type OrcadFeature } from './orcad-feature-readiness'
|
||||
|
||||
// Every method's declared needs for these params; param-level ones read them.
|
||||
const REGISTRY = buildRegistry(ALL_RPC_METHODS)
|
||||
const requiresOf = (name: string, params: unknown = {}): readonly OrcadFeature[] => {
|
||||
const method = REGISTRY.get(name)
|
||||
expect(method, name).toBeDefined()
|
||||
return resolveOrcadFeatureRequirement(method?.requires, params)
|
||||
}
|
||||
|
||||
// Launches that start an agent (and so read its account home); a terminal create is one only with an agent.
|
||||
const AGENT_LAUNCHES = [
|
||||
'terminal.createAgentSession',
|
||||
'agentSession.create',
|
||||
'git.generateCommitMessage',
|
||||
'orchestration.workerStart',
|
||||
'orchestration.federationAttachStart'
|
||||
]
|
||||
const PARAM_GATED_LAUNCHES = [
|
||||
'terminal.ensureAgentSession',
|
||||
'terminal.create',
|
||||
'session.tabs.createTerminal',
|
||||
'agent.launch',
|
||||
'agent.launchReplay'
|
||||
]
|
||||
|
||||
describe('which RPC methods wait for an orcad startup phase', () => {
|
||||
it('gates the account services, search index, orchestration and orphan adoption', () => {
|
||||
for (const method of ALL_RPC_METHODS) {
|
||||
const expected: OrcadFeature[] = []
|
||||
// Claude/Codex account services only; usage credentials and Antigravity live elsewhere.
|
||||
if (
|
||||
ACCOUNT_METHODS.some((account) => account.name === method.name) &&
|
||||
method.name !== 'accounts.setHostLaunchEnv' &&
|
||||
method.name !== 'accounts.unsubscribe'
|
||||
) {
|
||||
expected.push('accounts')
|
||||
}
|
||||
if (AGENT_LAUNCHES.includes(method.name)) {
|
||||
expected.push('accounts')
|
||||
}
|
||||
if (method.name.startsWith('orchestration.')) {
|
||||
expected.push('orchestration')
|
||||
}
|
||||
if (/^aiVault\.(searchSessions|searchStatus|setSearchEnabled)$/.test(method.name)) {
|
||||
expected.push('sessionSearch')
|
||||
}
|
||||
if (method.name === 'terminal.adoptOrphans') {
|
||||
expected.push('unshownPlacement')
|
||||
}
|
||||
if (PARAM_GATED_LAUNCHES.includes(method.name)) {
|
||||
expect(typeof method.requires, method.name).toBe('function')
|
||||
continue
|
||||
}
|
||||
expect({ name: method.name, requires: requiresOf(method.name) }).toEqual({
|
||||
name: method.name,
|
||||
requires: expected
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('gates a terminal create only when it starts an agent; plain shells stay ungated', () => {
|
||||
for (const name of ['terminal.create', 'session.tabs.createTerminal']) {
|
||||
expect(requiresOf(name, {}), name).toEqual([])
|
||||
expect(requiresOf(name, { command: 'ls -la' }), name).toEqual([])
|
||||
expect(requiresOf(name, { command: 'claude --resume' }), name).toEqual(['accounts'])
|
||||
expect(requiresOf(name, { command: 'codex' }), name).toEqual(['accounts'])
|
||||
expect(requiresOf(name, { launchAgent: 'claude' }), name).toEqual(['accounts'])
|
||||
expect(requiresOf(name, { launchConfig: { agentArgs: '', agentEnv: {} } }), name).toEqual([
|
||||
'accounts'
|
||||
])
|
||||
expect(requiresOf(name, { resumeProviderSession: { id: 's' } }), name).toEqual(['accounts'])
|
||||
}
|
||||
expect(requiresOf('session.tabs.createTerminal', { agent: 'claude' })).toEqual(['accounts'])
|
||||
})
|
||||
|
||||
it('gates only an explicit ensureAgentSession; an attach is restore latency', () => {
|
||||
expect(requiresOf('terminal.ensureAgentSession', { kind: 'explicit' })).toEqual(['accounts'])
|
||||
expect(requiresOf('terminal.ensureAgentSession', { kind: 'automatic' })).toEqual([])
|
||||
})
|
||||
|
||||
it('gates agent.launch unless it reuses a live terminal', () => {
|
||||
for (const name of ['agent.launch', 'agent.launchReplay']) {
|
||||
expect(requiresOf(name, {}), name).toEqual(['accounts'])
|
||||
expect(requiresOf(name, { reuseTerminal: { handle: 'h' } }), name).toEqual([])
|
||||
}
|
||||
})
|
||||
|
||||
it('never gates what every client calls at connect', () => {
|
||||
for (const name of [
|
||||
'status.get',
|
||||
'repo.list',
|
||||
'session.tabs.subscribe',
|
||||
'terminal.list',
|
||||
'worktree.teardownMissingTerminals',
|
||||
'accounts.setHostLaunchEnv'
|
||||
]) {
|
||||
expect(ALL_RPC_METHODS.some((method) => method.name === name)).toBe(true)
|
||||
expect(requiresOf(name)).toEqual([])
|
||||
}
|
||||
})
|
||||
|
||||
// No behavior change yet: orcad opens every latch before RPC starts accepting calls.
|
||||
it('opens every latch before orcad starts RPC', () => {
|
||||
const read = (file: string): string =>
|
||||
readFileSync(join(__dirname, '../../orcad', file), 'utf8')
|
||||
const entry = read('orcad-entry.ts')
|
||||
const restore = read('orcad-restored-terminal-authority.ts')
|
||||
const rpcStart = entry.indexOf("await timed('rpc', () => rpc.start())")
|
||||
const restored = entry.indexOf('await restoreOrcadTerminalAuthority(')
|
||||
expect(rpcStart).toBeGreaterThan(0)
|
||||
expect(restored).toBeGreaterThan(0)
|
||||
expect(restored).toBeLessThan(rpcStart)
|
||||
for (const feature of ['accounts', 'sessionSearch']) {
|
||||
const opened = entry.indexOf(`featureReadiness.open('${feature}')`)
|
||||
expect(opened, feature).toBeGreaterThan(0)
|
||||
expect(opened, feature).toBeLessThan(rpcStart)
|
||||
}
|
||||
for (const feature of ['orchestration', 'unshownPlacement']) {
|
||||
expect(restore).toContain(`readiness.open('${feature}')`)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,5 @@
|
||||
import { vi } from 'vitest'
|
||||
import './runtime-client-settings-linear-team-projection.spec'
|
||||
import './runtime-client-settings-source-control-projection.spec'
|
||||
|
||||
vi.mock('../claude/agent-view-service', () => ({
|
||||
getClaudeAgentViewService: () => ({ reconcile: async () => {} })
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { RuntimeClientSettingsController } from './runtime-client-settings'
|
||||
import { createGlobalSettingsFixture } from '../../shared/global-settings-test-fixture'
|
||||
import type { GlobalSettings } from '../../shared/global-settings-types'
|
||||
import { SourceControlAiSettings } from '../../shared/rpc-contract/git-params'
|
||||
import { liftActionPermissionArgs } from '../../shared/source-control-action-permission-lift'
|
||||
import { getDefaultSourceControlAiSettings } from '../../shared/source-control-ai'
|
||||
|
||||
// settings.get is an allowlist: omitted per-action agent recipes look unsaved to paired AI buttons.
|
||||
function projectionOf(settings: Partial<GlobalSettings>) {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: get() reads nothing but store.getSettings(); every other RuntimeStore member is unreachable from that path.
|
||||
return new RuntimeClientSettingsController({ getSettings: () => settings } as never).get()
|
||||
}
|
||||
|
||||
function hostSettings(sourceControlAi: GlobalSettings['sourceControlAi']): Partial<GlobalSettings> {
|
||||
return { ...createGlobalSettingsFixture({ workspaceDir: '/w' }), sourceControlAi }
|
||||
}
|
||||
|
||||
describe('RuntimeClientSettingsController source control launch recipes', () => {
|
||||
it('publishes the recipe saved for a launch action', () => {
|
||||
const recipe = liftActionPermissionArgs({
|
||||
agentId: 'codex' as const,
|
||||
agentArgs: '-c model=o3 --fast',
|
||||
launchPreferences: {
|
||||
agentId: 'claude',
|
||||
permissionMode: 'ask',
|
||||
modelSelection: {
|
||||
base: 'inherit',
|
||||
options: { effort: { agentId: 'codex', value: null } }
|
||||
}
|
||||
}
|
||||
})
|
||||
const source = { ...getDefaultSourceControlAiSettings(), actions: { fixChecks: recipe } }
|
||||
expect(SourceControlAiSettings.parse(source).actions?.fixChecks).toEqual(recipe)
|
||||
const projected = projectionOf(hostSettings(source))
|
||||
expect(projected.sourceControlAi.actions.fixChecks?.launchPreferences).toEqual(
|
||||
recipe.launchPreferences
|
||||
)
|
||||
expect(projected.sourceControlAi.actions.fixChecks).toMatchObject({
|
||||
agentId: 'codex',
|
||||
agentArgs: '--fast',
|
||||
argumentChoices: { syntaxAgent: 'codex', nodes: [{ at: 0, text: '-c model=o3' }] }
|
||||
})
|
||||
})
|
||||
|
||||
it('publishes a recipe saved under the legacy launch defaults key', () => {
|
||||
const projected = projectionOf(
|
||||
hostSettings({
|
||||
...getDefaultSourceControlAiSettings(),
|
||||
actions: undefined,
|
||||
launchActionDefaults: { resolveConflicts: { agentId: 'claude' } }
|
||||
})
|
||||
)
|
||||
expect(projected.sourceControlAi.actions.resolveConflicts).toMatchObject({ agentId: 'claude' })
|
||||
})
|
||||
|
||||
it('publishes launch actions only, not the text generation recipes', () => {
|
||||
const projected = projectionOf(hostSettings(getDefaultSourceControlAiSettings()))
|
||||
expect(Object.keys(projected.sourceControlAi.actions).sort()).toEqual([
|
||||
'fixChecks',
|
||||
'fixCommitFailure',
|
||||
'fixPushFailure',
|
||||
'resolveComments',
|
||||
'resolveConflicts'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('RuntimeClientSettingsController source control list order', () => {
|
||||
it('publishes the group order so paired clients list changes the same way', () => {
|
||||
expect(projectionOf({ sourceControlGroupOrder: 'staged-first' }).sourceControlGroupOrder).toBe(
|
||||
'staged-first'
|
||||
)
|
||||
expect(projectionOf({}).sourceControlGroupOrder).toBe('changes-first')
|
||||
})
|
||||
|
||||
it('publishes the compare base and whitespace choices desktop diffs use', () => {
|
||||
const projected = projectionOf({
|
||||
sourceControlCompareAgainstUpstream: true,
|
||||
diffShowWhitespace: true
|
||||
})
|
||||
expect(projected).toMatchObject({
|
||||
sourceControlCompareAgainstUpstream: true,
|
||||
diffShowWhitespace: true
|
||||
})
|
||||
expect(projectionOf({})).toMatchObject({
|
||||
sourceControlCompareAgainstUpstream: false,
|
||||
diffShowWhitespace: false
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user