From 8136de7ac368bcca85b311634110ef86d1c5460f Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Fri, 3 Jul 2026 00:04:44 +0300 Subject: [PATCH] =?UTF-8?q?test(ai-autopilot):=20sandboxed=20boot-and-serv?= =?UTF-8?q?e=20=E2=80=94=20serveCheck=20=C3=97=20DockerRunner=20E2E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exercises serveCheck end-to-end against a real container: a real npm install, start the dev server, preview (readiness probed inside the container), fetch a health path -> empty-blockers verdict. Plus the failure modes (5xx, exits-before-serving, install-failure) each surface as a blocker. Proves the production-grade check works through the DockerRunner sandbox, not just the host. Skips cleanly with no daemon. Test-only, no changeset. --- .../src/bootstrap/serve-check.docker.test.ts | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 packages/ai-autopilot/src/bootstrap/serve-check.docker.test.ts diff --git a/packages/ai-autopilot/src/bootstrap/serve-check.docker.test.ts b/packages/ai-autopilot/src/bootstrap/serve-check.docker.test.ts new file mode 100644 index 0000000..f449227 --- /dev/null +++ b/packages/ai-autopilot/src/bootstrap/serve-check.docker.test.ts @@ -0,0 +1,74 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { serveCheck } from './serve-check.js' +import { DockerRunner, dockerAvailable } from '../runner/docker.js' +import type { LoopPassContext } from './types.js' + +/** + * The sandboxed boot-and-serve proof. `serveCheck` is exercised end-to-end + * against a REAL container: it installs, `start`s a dev server, `preview`s it + * (readiness probed inside the container), and fetches a health path — the same + * production-grade check the full-fledged loop runs, now through the `DockerRunner` + * sandbox instead of the host. Skips cleanly when no daemon is reachable. + */ +const skip = (await dockerAvailable()) ? false : 'no docker daemon available' + +/** A minimal loop-pass context — serveCheck ignores it, but the contract needs one. */ +const ctx: LoopPassContext = { pass: 1, plan: { stack: '', narration: '', decisions: [] }, intent: '', blockers: [] } + +/** A server bound to 0.0.0.0 so the container's published port reaches it from the host. */ +const server = (port: number, status: number, body: string): string => + `const http=require('http');http.createServer((_,res)=>{res.writeHead(${status});res.end(${JSON.stringify(body)})}).listen(${port},'0.0.0.0')` + +const pkg = JSON.stringify({ name: 'sandbox-app', private: true }) + '\n' + +// DockerRunner publishes previewPort 3000 at boot; the app and serveCheck both default to 3000. +const PORT = 3000 + +describe('serveCheck × DockerRunner (sandboxed boot-and-serve)', { skip }, () => { + it('installs, serves, and fetches a healthy app inside a real container', async () => { + const s = await new DockerRunner().boot({ + files: { 'package.json': pkg, 'server.js': server(PORT, 200, 'ok') }, + }) + try { + // A real `npm install` (no deps → fast) proves the install prerequisite runs in the sandbox. + const verdict = await serveCheck(s, { install: 'npm install', serve: 'node server.js', healthPath: '/health' })(ctx) + assert.deepEqual(verdict.blockers, []) + } finally { + await s.dispose() + } + }) + + it('flags a server error (5xx) as a blocker', async () => { + const s = await new DockerRunner().boot({ files: { 'server.js': server(PORT, 500, 'boom') } }) + try { + const verdict = await serveCheck(s, { serve: 'node server.js' })(ctx) + assert.equal(verdict.blockers.length, 1) + assert.match(verdict.blockers[0]!, /responded 500/) + } finally { + await s.dispose() + } + }) + + it('flags a server that exits before serving as a blocker', async () => { + const s = await new DockerRunner().boot({ files: { 'boot.js': 'process.exit(1)' } }) + try { + const verdict = await serveCheck(s, { serve: 'node boot.js', waitMs: 3000 })(ctx) + assert.equal(verdict.blockers.length, 1) + assert.match(verdict.blockers[0]!, /exited before serving/) + } finally { + await s.dispose() + } + }) + + it('flags a failing install as a blocker before anything is served', async () => { + const s = await new DockerRunner().boot({ files: { 'server.js': server(PORT, 200, 'ok') } }) + try { + const verdict = await serveCheck(s, { install: 'exit 3', serve: 'node server.js' })(ctx) + assert.equal(verdict.blockers.length, 1) + assert.match(verdict.blockers[0]!, /install failed/) + } finally { + await s.dispose() + } + }) +})