mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(coding-agent): load codemode worker in Windows binary
closes #10204
This commit is contained in:
parent
d2931ad3d5
commit
4a42f8fafa
14 changed files with 253 additions and 21 deletions
8
.github/workflows/build-binaries.yml
vendored
8
.github/workflows/build-binaries.yml
vendored
|
|
@ -142,7 +142,14 @@ jobs:
|
||||||
actions: read
|
actions: read
|
||||||
env:
|
env:
|
||||||
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
|
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
|
||||||
|
SOURCE_REF: ${{ github.event.inputs.source_ref || github.event.inputs.tag || github.ref_name }}
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout smoke tests
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
ref: ${{ env.SOURCE_REF }}
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download binary archives
|
- name: Download binary archives
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||||
with:
|
with:
|
||||||
|
|
@ -198,6 +205,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
"${{ steps.binary.outputs.binary }}" --help
|
"${{ steps.binary.outputs.binary }}" --help
|
||||||
"${{ steps.binary.outputs.binary }}" --version
|
"${{ steps.binary.outputs.binary }}" --version
|
||||||
|
node scripts/smoke-test-codemode-binary.mjs "${{ steps.binary.outputs.binary }}"
|
||||||
|
|
||||||
stage-github-release:
|
stage-github-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Allowed `CodemodeSandbox.workerUrl` to be a string, as required for embedded worker entrypoints in Bun compiled executables ([#10204](https://github.com/earendil-works/pi/issues/10204)).
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed `image()` accepting malformed base64 data and unsupported image types. It now throws a `TypeError` unless the data is valid base64 of a PNG, JPEG, GIF, or WebP image, derives the MIME type from the image signature instead of the declared type, and strips line breaks from wrapped base64 ([#10215](https://github.com/earendil-works/pi/issues/10215)).
|
- Fixed `image()` accepting malformed base64 data and unsupported image types. It now throws a `TypeError` unless the data is valid base64 of a PNG, JPEG, GIF, or WebP image, derives the MIME type from the image signature instead of the declared type, and strips line breaks from wrapped base64 ([#10215](https://github.com/earendil-works/pi/issues/10215)).
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,10 @@ const sandbox = new CodemodeSandbox({
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`workerUrl` accepts a URL or string. For a Bun compiled executable, include the worker as an
|
||||||
|
additional build entrypoint and pass its relative source path as a string, for example
|
||||||
|
`"./src/codemode-worker.ts"`; Bun resolves that form from its embedded module graph.
|
||||||
|
|
||||||
## Declarations for the model
|
## Declarations for the model
|
||||||
|
|
||||||
Tools and globals can carry `description`, `inputSchema`, and `outputSchema` (JSON Schema). `renderDeclarations()` turns them into TypeScript declarations for a model-facing tool description:
|
Tools and globals can carry `description`, `inputSchema`, and `outputSchema` (JSON Schema). `renderDeclarations()` turns them into TypeScript declarations for a model-facing tool description:
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ interface ExecutionOptions {
|
||||||
memoryLimitBytes: number | undefined;
|
memoryLimitBytes: number | undefined;
|
||||||
store: Record<string, string>;
|
store: Record<string, string>;
|
||||||
wasm: Promise<CodemodeWasmModule>;
|
wasm: Promise<CodemodeWasmModule>;
|
||||||
workerUrl: URL;
|
workerUrl: string | URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -288,7 +288,7 @@ export class CodemodeSandbox {
|
||||||
private readonly timeoutMs: number;
|
private readonly timeoutMs: number;
|
||||||
private readonly memoryLimitBytes: number | undefined;
|
private readonly memoryLimitBytes: number | undefined;
|
||||||
private readonly wasm: CodemodeWasmModule | Promise<CodemodeWasmModule> | undefined;
|
private readonly wasm: CodemodeWasmModule | Promise<CodemodeWasmModule> | undefined;
|
||||||
private readonly workerUrl: URL;
|
private readonly workerUrl: string | URL;
|
||||||
private readonly running = new Set<Execution>();
|
private readonly running = new Set<Execution>();
|
||||||
private closed = false;
|
private closed = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
* Importing this module starts the worker. Hosts that bundle their code (for
|
* Importing this module starts the worker. Hosts that bundle their code (for
|
||||||
* example a Bun compiled executable) add a file that imports
|
* example a Bun compiled executable) add a file that imports
|
||||||
* `@earendil-works/pi-codemode/worker` as a separate entrypoint and pass its URL
|
* `@earendil-works/pi-codemode/worker` as a separate entrypoint and pass its URL
|
||||||
* as `workerUrl`.
|
* or embedded-module string specifier as `workerUrl`.
|
||||||
*/
|
*/
|
||||||
import { parentPort, workerData } from "node:worker_threads";
|
import { parentPort, workerData } from "node:worker_threads";
|
||||||
import { JSException, type JSValueHandle, MAX_STACK_SIZE, QuickJS } from "quickjs-wasi";
|
import { JSException, type JSValueHandle, MAX_STACK_SIZE, QuickJS } from "quickjs-wasi";
|
||||||
|
|
|
||||||
|
|
@ -118,9 +118,10 @@ export interface CodemodeSandboxOptions {
|
||||||
/**
|
/**
|
||||||
* Worker entry that imports `@earendil-works/pi-codemode/worker`. Default: this package's own
|
* Worker entry that imports `@earendil-works/pi-codemode/worker`. Default: this package's own
|
||||||
* worker file. Pass it when this package is bundled, since the default is resolved relative to
|
* worker file. Pass it when this package is bundled, since the default is resolved relative to
|
||||||
* the module that creates the sandbox.
|
* the module that creates the sandbox. Bun compiled executables require the relative string
|
||||||
|
* specifier of an embedded build entrypoint; other hosts usually use a URL.
|
||||||
*/
|
*/
|
||||||
workerUrl?: URL;
|
workerUrl?: string | URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CodemodeExecuteOptions {
|
export interface CodemodeExecuteOptions {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
import vm from "node:vm";
|
import vm from "node:vm";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
import { CodemodeSandbox, type CodemodeTool } from "../src/index.ts";
|
import { CodemodeSandbox, type CodemodeTool } from "../src/index.ts";
|
||||||
|
|
@ -561,6 +562,13 @@ describe("limits and lifetime", () => {
|
||||||
expect(result).toMatchObject({ ok: true, value: ["RangeError", true] });
|
expect(result).toMatchObject({ ok: true, value: ["RangeError", true] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts a worker path string", async () => {
|
||||||
|
const workerPath = fileURLToPath(new URL("../src/runtime/worker.ts", import.meta.url));
|
||||||
|
const sandbox = new CodemodeSandbox({ workerUrl: workerPath });
|
||||||
|
sandboxes.push(sandbox);
|
||||||
|
expect(await sandbox.execute("return 1")).toMatchObject({ ok: true, value: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
it("reports a missing worker file as a sandbox error", async () => {
|
it("reports a missing worker file as a sandbox error", async () => {
|
||||||
const sandbox = new CodemodeSandbox({ workerUrl: new URL("./does-not-exist.js", import.meta.url) });
|
const sandbox = new CodemodeSandbox({ workerUrl: new URL("./does-not-exist.js", import.meta.url) });
|
||||||
sandboxes.push(sandbox);
|
sandboxes.push(sandbox);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
- Fixed new sessions intermittently ignoring the saved default model, or warning that no models are available, when it belongs to an extension-registered native provider with a stored credential ([#9962](https://github.com/earendil-works/pi/issues/9962)).
|
- Fixed new sessions intermittently ignoring the saved default model, or warning that no models are available, when it belongs to an extension-registered native provider with a stored credential ([#9962](https://github.com/earendil-works/pi/issues/9962)).
|
||||||
- Fixed the `/mcp` sign-in URL not being clickable when it wraps across lines, by emitting it as a terminal hyperlink with a `Cmd/Ctrl+click to open` line like `/login` ([#10186](https://github.com/earendil-works/pi/issues/10186)).
|
- Fixed the `/mcp` sign-in URL not being clickable when it wraps across lines, by emitting it as a terminal hyperlink with a `Cmd/Ctrl+click to open` line like `/login` ([#10186](https://github.com/earendil-works/pi/issues/10186)).
|
||||||
- Fixed codemode `image()` accepting malformed base64 data or unsupported image types, which persisted an invalid image block that made every later provider request fail with HTTP 400 ([#10215](https://github.com/earendil-works/pi/issues/10215)).
|
- Fixed codemode `image()` accepting malformed base64 data or unsupported image types, which persisted an invalid image block that made every later provider request fail with HTTP 400 ([#10215](https://github.com/earendil-works/pi/issues/10215)).
|
||||||
|
- Fixed codemode failing to start its script worker from the standalone Windows executable ([#10204](https://github.com/earendil-works/pi/issues/10204)).
|
||||||
|
|
||||||
## [0.99.1] - 2026-09-29
|
## [0.99.1] - 2026-09-29
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -489,21 +489,28 @@ export function getQuickJSWasmPath(): string {
|
||||||
return embeddedQuickJSWasmPath ?? createRequire(import.meta.url).resolve("quickjs-wasi/quickjs.wasm");
|
return embeddedQuickJSWasmPath ?? createRequire(import.meta.url).resolve("quickjs-wasi/quickjs.wasm");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Resolve the codemode worker entry for a release runtime. */
|
||||||
* Get the URL of the codemode worker entry (`src/extensions/codemode/worker.ts`), or undefined to use
|
export function resolveCodemodeWorkerSpecifier(
|
||||||
* the worker file that ships next to pi-codemode's own module.
|
runtime: "bun-binary" | "bundled-node" | "unbundled",
|
||||||
* - For Bun binary: the build passes the worker as an extra entrypoint. Bun embeds it at its path
|
moduleUrl: string,
|
||||||
* relative to the common directory of all entrypoints (the package root, since the main entry is
|
): string | URL | undefined {
|
||||||
* dist/bun/cli.js) with a .js extension, and all bundled code sees the executable as import.meta.url.
|
// Bun embeds explicit source entrypoints, but on Windows Bun 1.3 cannot map an absolute
|
||||||
* - For the Node bundle: the build emits codemode-worker.js next to the chunk that contains this module.
|
// B:\~BUN URL back to one. A relative string with the original source extension works on
|
||||||
* - For Node.js (dist/) and tsx (src/): pi-codemode's own worker.
|
// every Bun platform.
|
||||||
*/
|
if (runtime === "bun-binary") return "./src/extensions/codemode/worker.ts";
|
||||||
export function getCodemodeWorkerUrl(): URL | undefined {
|
if (runtime === "bundled-node") return new URL("./codemode-worker.js", moduleUrl);
|
||||||
if (isBunBinary) return new URL("./src/extensions/codemode/worker.js", import.meta.url);
|
|
||||||
if (isBundledNode) return new URL("./codemode-worker.js", import.meta.url);
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the codemode worker entry, or undefined to use the worker that ships next to pi-codemode.
|
||||||
|
* The Bun and Node release builds both pass the worker as an extra entrypoint.
|
||||||
|
*/
|
||||||
|
export function getCodemodeWorkerSpecifier(): string | URL | undefined {
|
||||||
|
const runtime = isBunBinary ? "bun-binary" : isBundledNode ? "bundled-node" : "unbundled";
|
||||||
|
return resolveCodemodeWorkerSpecifier(runtime, import.meta.url);
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// App Config (from package.json piConfig)
|
// App Config (from package.json piConfig)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import {
|
||||||
renderToolSample,
|
renderToolSample,
|
||||||
toCodemodeIdentifier,
|
toCodemodeIdentifier,
|
||||||
} from "@earendil-works/pi-codemode";
|
} from "@earendil-works/pi-codemode";
|
||||||
import { getCodemodeWorkerUrl, getQuickJSWasmPath } from "../../config.ts";
|
import { getCodemodeWorkerSpecifier, getQuickJSWasmPath } from "../../config.ts";
|
||||||
import type { ExtensionToolContext } from "../../core/extensions/types.ts";
|
import type { ExtensionToolContext } from "../../core/extensions/types.ts";
|
||||||
import type { SessionEntry } from "../../core/session-manager.ts";
|
import type { SessionEntry } from "../../core/session-manager.ts";
|
||||||
import { combineUsage } from "../../core/usage-totals.ts";
|
import { combineUsage } from "../../core/usage-totals.ts";
|
||||||
|
|
@ -277,7 +277,7 @@ export async function executeCodemode(
|
||||||
timeoutMs: sourceOptions.timeoutMs ?? Number.POSITIVE_INFINITY,
|
timeoutMs: sourceOptions.timeoutMs ?? Number.POSITIVE_INFINITY,
|
||||||
memoryLimitBytes: CODEMODE_MEMORY_LIMIT_BYTES,
|
memoryLimitBytes: CODEMODE_MEMORY_LIMIT_BYTES,
|
||||||
wasm: loadQuickJSWasm(getQuickJSWasmPath()),
|
wasm: loadQuickJSWasm(getQuickJSWasmPath()),
|
||||||
workerUrl: getCodemodeWorkerUrl(),
|
workerUrl: getCodemodeWorkerSpecifier(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let result: CodemodeResult;
|
let result: CodemodeResult;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* Worker entry for the codemode sandbox in bundled builds. The Node bundle and the Bun binary
|
* Worker entry for the codemode sandbox in bundled builds. The Node bundle and the Bun binary
|
||||||
* build this file as a separate entrypoint because pi-codemode's own worker file is not on disk
|
* build this file as a separate entrypoint because pi-codemode's own worker file is not on disk
|
||||||
* there; `getCodemodeWorkerUrl()` in config.ts resolves it.
|
* there; `getCodemodeWorkerSpecifier()` in config.ts resolves it.
|
||||||
*/
|
*/
|
||||||
import "@earendil-works/pi-codemode/worker";
|
import "@earendil-works/pi-codemode/worker";
|
||||||
|
|
|
||||||
21
packages/coding-agent/test/codemode-worker-config.test.ts
Normal file
21
packages/coding-agent/test/codemode-worker-config.test.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { resolveCodemodeWorkerSpecifier } from "../src/config.ts";
|
||||||
|
|
||||||
|
describe("resolveCodemodeWorkerSpecifier", () => {
|
||||||
|
// Regression test for #10204.
|
||||||
|
test("uses a relative source entrypoint in Bun binaries", () => {
|
||||||
|
expect(resolveCodemodeWorkerSpecifier("bun-binary", "file:///B:/~BUN/root/config.js")).toBe(
|
||||||
|
"./src/extensions/codemode/worker.ts",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses the emitted worker beside the bundled Node module", () => {
|
||||||
|
expect(resolveCodemodeWorkerSpecifier("bundled-node", "file:///app/chunks/config.js")).toEqual(
|
||||||
|
new URL("file:///app/chunks/codemode-worker.js"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses the pi-codemode worker when unbundled", () => {
|
||||||
|
expect(resolveCodemodeWorkerSpecifier("unbundled", import.meta.url)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -124,7 +124,7 @@ for platform in "${PLATFORMS[@]}"; do
|
||||||
# Bun compiled executables only embed worker scripts when they are passed as
|
# Bun compiled executables only embed worker scripts when they are passed as
|
||||||
# explicit build entrypoints. Bun places them at their path relative to the
|
# explicit build entrypoints. Bun places them at their path relative to the
|
||||||
# common directory of all entrypoints, so the main entry must stay in dist/
|
# common directory of all entrypoints, so the main entry must stay in dist/
|
||||||
# for the worker URLs in src/config.ts to resolve.
|
# for the worker specifiers in the runtime to resolve.
|
||||||
#
|
#
|
||||||
# Disable cwd bunfig.toml autoload so project preload scripts cannot crash the
|
# Disable cwd bunfig.toml autoload so project preload scripts cannot crash the
|
||||||
# standalone binary before pi starts (see #7684).
|
# standalone binary before pi starts (see #7684).
|
||||||
|
|
|
||||||
178
scripts/smoke-test-codemode-binary.mjs
Normal file
178
scripts/smoke-test-codemode-binary.mjs
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||||
|
import { createServer } from "node:http";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { extname, join, resolve } from "node:path";
|
||||||
|
|
||||||
|
const WORKER_MARKER = "PI_CODEMODE_WORKER_OK";
|
||||||
|
const SUCCESS_MARKER = "PI_CODEMODE_BINARY_SMOKE_OK";
|
||||||
|
const FAILURE_MARKER = "PI_CODEMODE_BINARY_SMOKE_FAILED";
|
||||||
|
const TIMEOUT_MS = 30_000;
|
||||||
|
|
||||||
|
function completionChunk(id, delta, finishReason = null, usage) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
object: "chat.completion.chunk",
|
||||||
|
created: 0,
|
||||||
|
model: "codemode-smoke",
|
||||||
|
choices: [{ index: 0, delta, finish_reason: finishReason }],
|
||||||
|
...(usage ? { usage } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendCompletion(response, chunks) {
|
||||||
|
response.writeHead(200, {
|
||||||
|
"content-type": "text/event-stream",
|
||||||
|
"cache-control": "no-cache",
|
||||||
|
connection: "close",
|
||||||
|
});
|
||||||
|
for (const chunk of chunks) response.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
||||||
|
response.end("data: [DONE]\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readRequest(request) {
|
||||||
|
let body = "";
|
||||||
|
for await (const chunk of request) body += chunk.toString();
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toolResultContainsMarker(body) {
|
||||||
|
const payload = JSON.parse(body);
|
||||||
|
return (
|
||||||
|
Array.isArray(payload.messages) &&
|
||||||
|
payload.messages.some(
|
||||||
|
(message) => message?.role === "tool" && JSON.stringify(message.content).includes(WORKER_MARKER),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const binaryArg = process.argv[2];
|
||||||
|
if (!binaryArg || process.argv.length !== 3) {
|
||||||
|
throw new Error("Usage: node scripts/smoke-test-codemode-binary.mjs <pi-binary>");
|
||||||
|
}
|
||||||
|
const binary = resolve(binaryArg);
|
||||||
|
const tempDir = await mkdtemp(join(tmpdir(), "pi-codemode-binary-smoke-"));
|
||||||
|
let requestCount = 0;
|
||||||
|
const server = createServer(async (request, response) => {
|
||||||
|
if (request.method !== "POST" || !request.url?.endsWith("/chat/completions")) {
|
||||||
|
response.writeHead(404).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const body = await readRequest(request);
|
||||||
|
requestCount++;
|
||||||
|
const id = `chatcmpl-codemode-smoke-${requestCount}`;
|
||||||
|
if (requestCount === 1) {
|
||||||
|
sendCompletion(response, [
|
||||||
|
completionChunk(id, {
|
||||||
|
role: "assistant",
|
||||||
|
tool_calls: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
id: "call_codemode_smoke",
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "codemode",
|
||||||
|
arguments: JSON.stringify({ code: `text("${WORKER_MARKER}")` }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
completionChunk(id, {}, "tool_calls", { prompt_tokens: 1, completion_tokens: 1 }),
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const marker = toolResultContainsMarker(body) ? SUCCESS_MARKER : FAILURE_MARKER;
|
||||||
|
sendCompletion(response, [
|
||||||
|
completionChunk(id, { role: "assistant", content: marker }),
|
||||||
|
completionChunk(id, {}, "stop", { prompt_tokens: 1, completion_tokens: 1 }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise((resolveListen, reject) => {
|
||||||
|
server.once("error", reject);
|
||||||
|
server.listen(0, "127.0.0.1", resolveListen);
|
||||||
|
});
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === "string") throw new Error("Smoke-test server did not bind to a TCP port");
|
||||||
|
await writeFile(
|
||||||
|
join(tempDir, "models.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
providers: {
|
||||||
|
"codemode-smoke": {
|
||||||
|
baseUrl: `http://127.0.0.1:${address.port}/v1`,
|
||||||
|
api: "openai-completions",
|
||||||
|
apiKey: "smoke-test",
|
||||||
|
models: [{ id: "codemode-smoke" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const piArgs = [
|
||||||
|
"--provider",
|
||||||
|
"codemode-smoke",
|
||||||
|
"--model",
|
||||||
|
"codemode-smoke",
|
||||||
|
"--tools",
|
||||||
|
"codemode",
|
||||||
|
"--print",
|
||||||
|
"--no-session",
|
||||||
|
"--offline",
|
||||||
|
"--no-context-files",
|
||||||
|
"--no-skills",
|
||||||
|
"--no-prompt-templates",
|
||||||
|
"--no-themes",
|
||||||
|
"Run the codemode binary smoke test.",
|
||||||
|
];
|
||||||
|
const javaScript = extname(binary) === ".js";
|
||||||
|
const child = spawn(javaScript ? process.execPath : binary, javaScript ? [binary, ...piArgs] : piArgs, {
|
||||||
|
cwd: tempDir,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
PI_CODING_AGENT_DIR: tempDir,
|
||||||
|
PI_OFFLINE: "1",
|
||||||
|
},
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
child.stdout.on("data", (chunk) => {
|
||||||
|
stdout += chunk.toString();
|
||||||
|
});
|
||||||
|
child.stderr.on("data", (chunk) => {
|
||||||
|
stderr += chunk.toString();
|
||||||
|
});
|
||||||
|
const exitCode = await new Promise((resolveExit, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
child.kill();
|
||||||
|
reject(new Error(`Smoke test timed out after ${TIMEOUT_MS} ms`));
|
||||||
|
}, TIMEOUT_MS);
|
||||||
|
child.once("error", (error) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
child.once("exit", (code) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolveExit(code);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (exitCode !== 0 || !stdout.includes(SUCCESS_MARKER) || requestCount < 2) {
|
||||||
|
throw new Error(
|
||||||
|
`Codemode binary smoke test failed (exit ${exitCode}, requests ${requestCount})\nstdout:\n${stdout}\nstderr:\n${stderr}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
process.stdout.write(stdout);
|
||||||
|
} finally {
|
||||||
|
await new Promise((resolveClose) => server.close(resolveClose));
|
||||||
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error instanceof Error ? error.message : error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue