返回 CodeWhale
compiled-host.test.mjs
根目录 / crates / tui / extension-host / test / compiled-host.test.mjs
1 // Actual compiled image, same HostRoot/protocol/services, fake Engine authority.
2 // CI must name the image explicitly; normal source suites record a visible skip.
3 import { test } from 'node:test'
4 import assert from 'node:assert/strict'
5 import { execFileSync, spawn } from 'node:child_process'
6 import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
7 import { createServer } from 'node:http'
8 import { tmpdir } from 'node:os'
9 import { join } from 'node:path'
10 import { activate, BUNDLE, sha256File, startHost } from './harness.mjs'
11 import { ErrorCode } from '../dist/protocol.mjs'
12
13 const binary = process.env.CODEWHALE_COMPILED_HOST_TEST_BINARY
14 if (binary && !existsSync(binary)) throw new Error(`compiled image does not exist: ${binary}`)
15 const compiledTest = (name, run) => test(name, { skip: !binary && 'compiled image not requested' }, run)
16 const plugin = (source) => {
17 const dir = mkdtempSync(join(tmpdir(), 'cw-compiled-host-'))
18 const entry = join(dir, 'index.mjs')
19 writeFileSync(entry, source)
20 return { dir, entry, cleanup: () => rmSync(dir, { recursive: true, force: true }) }
21 }
22 const start = (options = {}) => startHost({ ...options, compiledHost: binary })
23
24 compiledTest('compiled identity and both trust tiers match canonical source', async (t) => {
25 const info = JSON.parse(execFileSync(binary, ['--codewhale-host-info'], { encoding: 'utf8', env: { ...process.env, BUN_OPTIONS: '', BUN_BE_BUN: '0' } }))
26 assert.equal(info.kind, 'codewhale-extension-host')
27 assert.equal(info.bundle_sha256, sha256File(BUNDLE))
28 for (const tier of ['plugin', 'builtin']) {
29 const host = await start({ tier })
30 try {
31 assert.equal(host.hello.bundle_sha256, info.bundle_sha256)
32 assert.deepEqual(host.hello.runtime, { name: 'bun', version: info.version })
33 assert.equal(host.hello.tier, tier)
34 assert.ok(host.readyMs <= 1500, `ready ${host.readyMs}ms exceeds existing 1500ms gate`)
35 if (process.platform !== 'win32') {
36 const rss = Number(execFileSync('ps', ['-o', 'rss=', '-p', String(host.child.pid)], { encoding: 'utf8' }).trim()) / 1024
37 assert.ok(rss <= 160, `idle ${rss}MiB exceeds existing 160MiB gate`)
38 t.diagnostic(`${tier}: ready ${host.readyMs.toFixed(1)}ms; RSS ${rss.toFixed(1)}MiB`)
39 }
40 } finally { await host.stop() }
41 }
42 })
43
44 compiledTest('compiled Native modules share services and cancellation withdraws owner', async (t) => {
45 const fixture = plugin(`import { Context } from '@deepseek-ai/cordis'
46 export const inject = ['tools']
47 export function apply(ctx) {
48 if (!Context.is(ctx)) throw new Error('foreign Cordis')
49 ctx.tools.register({ name: 'held', description: '', parameters: { type: 'object', properties: {} }, execute(_input, call) {
50 return new Promise((resolve) => call.signal.addEventListener('abort', () => resolve('aborted'), { once: true }))
51 } })
52 }`)
53 const host = await start()
54 t.after(async () => { await host.stop(); fixture.cleanup() })
55 const { ref, result } = await activate(host, 'compiled-owner', fixture.entry)
56 assert.equal(result.status, 'ok', result.diagnostic)
57 const handle = host.registry.find((entry) => entry.op === 'register').handle
58 const call = host.request('tool/call', { handle, call_id: 'held', input: {}, deadline_ms: 5000 })
59 setTimeout(() => host.cancel(call.id), 50)
60 await assert.rejects(call.promise, (error) => error.code === ErrorCode.Cancelled)
61 await host.call('ext/deactivate', { owner: ref, reason: 'test disposal' })
62 assert.ok(host.registry.some((entry) => entry.op === 'unregister' && entry.handle === handle))
63 await assert.rejects(host.call('tool/call', { handle, call_id: 'retired', input: {}, deadline_ms: 5000 }))
64 })
65
66 compiledTest('compiled launch ignores dotenv, bunfig and inherited runtime switches', async (t) => {
67 const fixture = plugin(`export const inject = ['tools']
68 export function apply(ctx) { ctx.tools.register({ name: 'env', description: '', parameters: { type: 'object', properties: {} }, execute: () => JSON.stringify({ dotenv: process.env.CW_COMPILED_DOTENV, cli: process.env.BUN_BE_BUN, options: process.env.BUN_OPTIONS, execArgv: process.execArgv }) }) }
69 `)
70 writeFileSync(join(fixture.dir, '.env'), 'CW_COMPILED_DOTENV=should-not-load\n')
71 writeFileSync(join(fixture.dir, 'bunfig.toml'), 'preload = ["./missing-preload.mjs"]\n')
72 const host = await start({ cwd: fixture.dir, env: { BUN_BE_BUN: '1', BUN_OPTIONS: '--preload=missing-preload.mjs' } })
73 t.after(async () => { await host.stop(); fixture.cleanup() })
74 const { result } = await activate(host, 'env', fixture.entry)
75 assert.equal(result.status, 'ok', result.diagnostic)
76 const handle = host.registry.find((entry) => entry.op === 'register').handle
77 const value = await host.call('tool/call', { handle, call_id: 'env', input: {}, deadline_ms: 5000 })
78 const expectedExecArgv = process.platform === 'win32'
79 // The Windows LPAC cannot open NUL; compile-host intentionally omits this flag there.
80 ? ['--no-install', '--no-env-file', '--no-addons']
81 : ['--no-install', '--no-env-file', '--config=/dev/null', '--no-addons']
82 assert.deepEqual(JSON.parse(value.content[0].text), { cli: '0', options: '', execArgv: expectedExecArgv })
83 })
84
85 compiledTest('compiled Native imports cannot reach FFI or fresh worker realms', async (t) => {
86 const host = await start()
87 t.after(() => host.stop())
88 for (const [name, source, pattern] of [
89 ['ffi', "export async function apply() { const { dlopen } = await import('bun:ffi'); dlopen('/nonexistent/libc.so', {}) }", /bun:ffi.*not available/],
90 ['ffi-require', "import { createRequire } from 'node:module'; export function apply() { createRequire(import.meta.url)('bun:ffi').dlopen('/nonexistent/libc.so', {}) }", /bun:ffi.*not available/],
91 ['worker', "import { Worker } from 'node:worker_threads'; export function apply() { new Worker('0', { eval: true }) }", /Worker.*not available/],
92 ]) {
93 const fixture = plugin(source)
94 t.after(fixture.cleanup)
95 const { result } = await activate(host, name, fixture.entry)
96 assert.equal(result.status, 'failed')
97 assert.match(result.diagnostic, pattern)
98 }
99 })
100
101 compiledTest('compiled host refuses missing npm imports without registry traffic', async (t) => {
102 const requests = []
103 const server = createServer((request, response) => { requests.push(request.url); response.statusCode = 404; response.end('{}') })
104 await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
105 const url = `http://127.0.0.1:${server.address().port}/`
106 const fixture = plugin("import 'codewhale-compiled-missing-package-0101'; export function apply() {}")
107 const host = await start({ env: { BUN_CONFIG_REGISTRY: url, NPM_CONFIG_REGISTRY: url, BUN_INSTALL_CACHE_DIR: join(fixture.dir, 'cache') } })
108 t.after(async () => { await host.stop(); fixture.cleanup(); await new Promise((resolve) => server.close(resolve)) })
109 const { result } = await activate(host, 'missing', fixture.entry)
110 assert.equal(result.status, 'failed')
111 assert.match(result.diagnostic, /Cannot find package.*codewhale-compiled-missing-package/)
112 assert.deepEqual(requests, [])
113 const bun = process.env.CODEWHALE_BUN_TEST_BINARY
114 assert.ok(bun, 'compiled qualification must name its local Bun control binary')
115 const control = spawn(bun, [fixture.entry], { env: { ...process.env, BUN_OPTIONS: '', BUN_BE_BUN: '0', BUN_CONFIG_REGISTRY: url, NPM_CONFIG_REGISTRY: url, BUN_INSTALL_CACHE_DIR: join(fixture.dir, 'cache') }, stdio: 'ignore' })
116 await new Promise((resolve, reject) => {
117 const timer = setTimeout(() => { control.kill(); reject(new Error('local-registry control timed out')) }, 5000)
118 control.on('error', (error) => { clearTimeout(timer); reject(error) })
119 control.on('exit', () => { clearTimeout(timer); resolve() })
120 })
121 assert.ok(requests.length > 0, 'without --no-install the same missing import must reach the local control registry')
122 })
123
124 // Five direct-image cases apply on every supported OS. This additional macOS
125 // reexec probe is registered only where it applies. All platforms separately
126 // require actual compiled-image memory enforcement in the Rust Native receipt.
127 if (process.platform === 'darwin') compiledTest('compiled macOS reexec keeps PID and kernel kills memory overflow', async (t) => {
128 const fixture = plugin(`export const inject = ['tools']
129 export function apply(ctx) {
130 ctx.tools.register({ name: 'pid', description: '', parameters: { type: 'object', properties: {} }, execute: () => String(process.pid) })
131 ctx.tools.register({ name: 'hog', description: '', parameters: { type: 'object', properties: {} }, async execute() { const chunks = []; for (let i = 0; i < 32; i++) { chunks.push(Buffer.alloc(64 * 1024 * 1024, 1)); await new Promise((resolve) => setTimeout(resolve, 5)) }; return String(chunks.length) } })
132 }`)
133 const host = await start({ env: { CODEWHALE_HOST_MEMORY_LIMIT_MIB: '300' } })
134 t.after(async () => { await host.stop(); fixture.cleanup() })
135 assert.equal(host.hello.memory_limit_mib, 300)
136 const { result } = await activate(host, 'memory', fixture.entry)
137 assert.equal(result.status, 'ok', result.diagnostic)
138 const [pid, hog] = host.registry.filter((entry) => entry.op === 'register').map((entry) => entry.handle)
139 const value = await host.call('tool/call', { handle: pid, call_id: 'pid', input: {}, deadline_ms: 5000 })
140 assert.equal(Number(value.content[0].text), host.child.pid)
141 host.request('tool/call', { handle: hog, call_id: 'hog', input: {}, deadline_ms: 30000 })
142 let timer
143 const ended = await Promise.race([host.exit, new Promise((resolve) => { timer = setTimeout(() => resolve('still running'), 20000) })])
144 clearTimeout(timer)
145 assert.deepEqual(ended, { code: null, signal: 'SIGKILL' })
146 })
147
147 lines Plain Text