Files
orca/src/main/plugins/plugin-worker-controller.test.ts
T
89acf1e1fa fix(plugins): release diagnostic logs after successful uninstall (#21185)
* fix(plugins): retire log owners after successful uninstall

* fix: address memory PR review regressions and withdraw false positives

* fix(plugins): fence stale activation after uninstall

* chore: allow durable plugin uninstall audit evidence

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-09-19 14:53:28 -07:00

208 lines
7.2 KiB
TypeScript

import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createPluginExtensionRegistry } from '../../shared/plugins/plugin-extension-registry'
import { pluginManifestSchema } from '../../shared/plugins/plugin-manifest'
import type { PluginContentVerifier } from './plugin-content-integrity'
import type { ValidDiscoveredPlugin } from './plugin-discovery'
import type { PluginWorkerHandle } from './plugin-host-process'
import { PluginWorkerController } from './plugin-worker-controller'
import type { PluginWorkerFactory } from './plugin-worker-manager'
import { PluginLogBuffer } from './plugin-log-buffer'
const roots: string[] = []
afterEach(async () => {
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
})
async function plugin(): Promise<ValidDiscoveredPlugin> {
const rootDir = await mkdtemp(join(tmpdir(), 'orca-plugin-worker-controller-'))
roots.push(rootDir)
await writeFile(join(rootDir, 'main.mjs'), 'export default function activate() {}')
return {
pluginKey: 'orca-samples.demo',
rootDir,
manifest: pluginManifestSchema.parse({
manifestVersion: 1,
id: 'demo',
publisher: 'orca-samples',
name: 'Demo',
version: '1.0.0',
engines: { orca: '>=1.0.0' },
pluginApi: 1,
main: 'main.mjs',
contributes: {
panels: [],
commands: [{ id: 'run', title: 'Run' }],
events: []
},
capabilities: []
}),
consentFingerprint: 'sha256-current',
contentHash: null,
isDev: true
}
}
function worker(commands: string[]): PluginWorkerHandle & { dispose: ReturnType<typeof vi.fn> } {
return {
commands,
invokeCommand: vi.fn(async () => null),
deliverEvent: vi.fn(),
lastActivityAt: () => Date.now(),
inFlightCount: () => 0,
dispose: vi.fn(async () => undefined),
kill: vi.fn(),
onExit: vi.fn()
}
}
function controller(options: {
factory: PluginWorkerFactory
verify: () => Promise<void>
isApproved: () => boolean
logs?: PluginLogBuffer
}): PluginWorkerController {
return new PluginWorkerController({
entryPath: '/host-entry.js',
workerFactory: options.factory,
registry: createPluginExtensionRegistry(),
contentVerifier: { verify: options.verify } as unknown as PluginContentVerifier,
capabilities: () => (options.isApproved() ? [] : null),
isCurrentApproved: () => options.isApproved(),
invokeCommand: vi.fn(async () => null),
executeHostCall: vi.fn(async () => ({ ok: true as const, value: null })),
log: (key) => options.logs?.capture(key) ?? vi.fn(),
onStateChanged: vi.fn(),
onWorkerGone: vi.fn()
})
}
describe('PluginWorkerController activation authority', () => {
it('does not recreate logs or start a revision after uninstall overtakes deactivation', async () => {
const subjectPlugin = await plugin()
const logs = new PluginLogBuffer()
const capture = vi.spyOn(logs, 'capture')
let approved = true
let finishStop!: () => void
const stopped = new Promise<void>((resolve) => {
finishStop = resolve
})
const oldWorker = worker(['run'])
oldWorker.dispose.mockImplementation(() => stopped)
const factory = vi.fn<PluginWorkerFactory>().mockResolvedValue(oldWorker)
const subject = controller({
factory,
verify: async () => undefined,
isApproved: () => approved,
logs
})
await subject.ensure(subjectPlugin)
logs.append(subjectPlugin.pluginKey, 'info', 'old installation')
const newRevision = {
...subjectPlugin,
manifest: { ...subjectPlugin.manifest, version: '2.0.0' }
}
const staleActivation = subject.ensure(newRevision)
await vi.waitFor(() => expect(oldWorker.dispose).toHaveBeenCalledOnce())
approved = false
await subject.deactivate(subjectPlugin.pluginKey)
logs.clear(subjectPlugin.pluginKey)
const capturesBeforeStop = capture.mock.calls.length
finishStop()
await expect(staleActivation).rejects.toThrow('no longer approved')
expect(factory).toHaveBeenCalledOnce()
expect(capture).toHaveBeenCalledTimes(capturesBeforeStop)
expect(logs.get(subjectPlugin.pluginKey)).toEqual([])
await subject.dispose()
})
it('does not start code after approval is revoked during integrity verification', async () => {
const subjectPlugin = await plugin()
let approved = true
let finishVerification!: () => void
const verification = new Promise<void>((resolve) => {
finishVerification = resolve
})
const factory = vi.fn<PluginWorkerFactory>()
const subject = controller({ factory, verify: () => verification, isApproved: () => approved })
const activation = subject.ensure(subjectPlugin)
approved = false
finishVerification()
await expect(activation).rejects.toThrow('no longer approved')
expect(factory).not.toHaveBeenCalled()
await subject.dispose()
})
it('disposes a worker whose approval changes while the process starts', async () => {
const subjectPlugin = await plugin()
let approved = true
let finishStart!: (handle: PluginWorkerHandle) => void
const factory = vi.fn<PluginWorkerFactory>(
() => new Promise<PluginWorkerHandle>((resolve) => (finishStart = resolve))
)
const subject = controller({
factory,
verify: async () => undefined,
isApproved: () => approved
})
const startedWorker = worker(['run'])
const activation = subject.ensure(subjectPlugin)
await vi.waitFor(() => expect(factory).toHaveBeenCalledOnce())
approved = false
finishStart(startedWorker)
await expect(activation).rejects.toThrow('disabled during activation')
expect(startedWorker.dispose).toHaveBeenCalledOnce()
await subject.dispose()
})
it('rejects and stops workers that register undeclared commands', async () => {
const subjectPlugin = await plugin()
const startedWorker = worker(['run', 'undeclared'])
const subject = controller({
factory: vi.fn<PluginWorkerFactory>().mockResolvedValue(startedWorker),
verify: async () => undefined,
isApproved: () => true
})
await expect(subject.ensure(subjectPlugin)).rejects.toThrow(
'registered undeclared command undeclared'
)
expect(startedWorker.dispose).toHaveBeenCalledOnce()
await subject.dispose()
})
it('rejects workers that register declarative action aliases', async () => {
const base = await plugin()
const subjectPlugin: ValidDiscoveredPlugin = {
...base,
manifest: pluginManifestSchema.parse({
...base.manifest,
contributes: {
...base.manifest.contributes,
commands: [{ id: 'tasks', title: 'Tasks', action: 'view.tasks' }]
}
})
}
const startedWorker = worker(['tasks'])
const subject = controller({
factory: vi.fn<PluginWorkerFactory>().mockResolvedValue(startedWorker),
verify: async () => undefined,
isApproved: () => true
})
await expect(subject.ensure(subjectPlugin)).rejects.toThrow(
'registered undeclared command tasks'
)
expect(startedWorker.dispose).toHaveBeenCalledOnce()
await subject.dispose()
})
})