diff --git a/CHANGELOG.md b/CHANGELOG.md index abbf381870..a6cf09cc1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,13 @@ ### Fixed +- **build:** Docker image build (`docker compose --profile cli build`, which runs + `next build` with Turbopack) no longer errors. Two Turbopack-only failures were + fixed: `sqlite-vec` is now externalized so Turbopack stops trying to bundle its + native `vec0.so` ("Unknown module type"), and `manager.stub.ts` now exports + `getAllAgentsStatus` (statically imported by `/api/tools/agent-bridge/state` — the + missing export aborted the build). The webpack-based VM build was unaffected, which + is why the deploy validated while the Docker build errored. (#3066 — thanks @freefrank) - **codex/providers:** `POST /api/providers/[id]/refresh` (the manual/auto "refresh token" endpoint) no longer rotates rotating-refresh providers (Codex/OpenAI share one Auth0 `client_id`). This was the last unguarded proactive-refresh entry point: diff --git a/next.config.mjs b/next.config.mjs index 7a4db27e4e..632bf48b18 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -141,6 +141,11 @@ const nextConfig = { "thread-stream", "pino-abstract-transport", "better-sqlite3", + // sqlite-vec ships a native vec0.so loaded at runtime via createRequire(). + // Turbopack otherwise tries to bundle the .so and fails with "Unknown module + // type"; externalizing it keeps the require at runtime (like better-sqlite3). + // See issue #3066. + "sqlite-vec", "node-machine-id", "keytar", "wreq-js", diff --git a/src/mitm/manager.stub.ts b/src/mitm/manager.stub.ts index d82f24fa4a..9c70a5c647 100644 --- a/src/mitm/manager.stub.ts +++ b/src/mitm/manager.stub.ts @@ -13,6 +13,13 @@ export const clearCachedPassword = () => {}; export const getMitmStatus = async () => { throw new Error(STUB_ERROR); }; +// Statically imported by /api/tools/agent-bridge/state — the stub must export it or +// the Turbopack build fails ("Export getAllAgentsStatus doesn't exist"). MITM/agent +// bridge needs host-level access and is non-functional in the bundled build anyway, +// so this throws like the other heavy ops. See issue #3066. +export const getAllAgentsStatus = (): never => { + throw new Error(STUB_ERROR); +}; export const startMitm = async ( _apiKey: string, _sudoPassword: string, diff --git a/tests/unit/next-config.test.ts b/tests/unit/next-config.test.ts index ae84ec41e0..96430e30ca 100644 --- a/tests/unit/next-config.test.ts +++ b/tests/unit/next-config.test.ts @@ -83,6 +83,9 @@ test("next config declares Turbopack aliases, runtime assets and server external for (const packageName of [ "thread-stream", "better-sqlite3", + // sqlite-vec ships a native vec0.so loaded at runtime; without externalizing it + // the Turbopack build fails with "Unknown module type" on the .so (issue #3066). + "sqlite-vec", "wreq-js", "fs", "path", @@ -95,6 +98,66 @@ test("next config declares Turbopack aliases, runtime assets and server external } }); +// ── manager.stub.ts must cover every static @/mitm/manager import (issue #3066) ── +// +// next.config aliases `@/mitm/manager` → `manager.stub.ts` for the Turbopack build +// (Docker uses Turbopack; the VM/webpack build uses the real module, which is why the +// VM validated while Docker's `npm run build` errored). Any route that statically +// imports a name the stub doesn't export breaks the Turbopack build with +// "Export X doesn't exist in target module". This guard fails on that drift — it is +// what would have caught the missing getAllAgentsStatus export in #3066. + +test("manager.stub.ts exports every name statically imported from @/mitm/manager", async () => { + const fs = await import("node:fs"); + const appDir = path.join(process.cwd(), "src", "app"); + + // Collect named imports from `... from "@/mitm/manager"` (NOT manager.runtime, which + // is loaded via dynamic import() and resolves to the real module at runtime). + const collectImports = (dir: string, acc: Set): void => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + collectImports(full, acc); + continue; + } + if (!/\.(ts|tsx)$/.test(entry.name)) continue; + const src = fs.readFileSync(full, "utf-8"); + const re = /import\s*\{([^}]*)\}\s*from\s*["']@\/mitm\/manager["']/g; + let m: RegExpExecArray | null; + while ((m = re.exec(src)) !== null) { + for (const raw of m[1].split(",")) { + const name = raw.trim().split(/\s+as\s+/)[0].trim(); + if (name) acc.add(name); + } + } + } + }; + + const imported = new Set(); + collectImports(appDir, imported); + + // Sanity: the suite is meaningless if it finds nothing to check. + assert.ok(imported.size > 0, "expected at least one static @/mitm/manager import in src/app"); + assert.ok(imported.has("getAllAgentsStatus"), "fixture: agent-bridge/state imports getAllAgentsStatus"); + + const stubSrc = fs.readFileSync( + path.join(process.cwd(), "src", "mitm", "manager.stub.ts"), + "utf-8" + ); + const stubExports = new Set( + [...stubSrc.matchAll(/export\s+(?:const|function|async\s+function)\s+([A-Za-z0-9_]+)/g)].map( + (m) => m[1] + ) + ); + + const missing = [...imported].filter((name) => !stubExports.has(name)); + assert.deepEqual( + missing, + [], + `manager.stub.ts is missing exports statically imported by routes: ${missing.join(", ")}` + ); +}); + test("next-intl webpack hook preserves caller config and filters known extractor warnings", async () => { const { default: nextConfig } = await loadNextConfig("webpack-pass-through"); const config: any = {