Files
WeiCHEN 77028af546 fix(pi-runner): cap /v1/run response size to stop typed reject
Truncate execution.log patches and keep members aligned with patch bodies so
verbose MiniMax-M3 runs no longer exceed the Go 8MB fail-closed limit.
Surface body-too-large vs typed-reject reasons and map contract failures to
fatal restart instead of misleading resumable TASK_INTERRUPTED.
2026-09-23 10:22:23 +08:00

129 lines
4.4 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import {
withResultV1,
membersDigest,
typedErrorCode,
capPatches,
truncateExecutionLogContent,
estimateWireBytes,
MAX_RESULT_MEMBER_BYTES,
MAX_RESULT_WIRE_BYTES,
EXECUTION_LOG_PATCH_BYTES,
} from "./result-v1.mjs";
test("withResultV1 is additive and strips stderr", () => {
const out = withResultV1({
ok: true,
run_id: "run-1",
stderr: "secret-trace",
patches: [{ rel_path: "output.md", content: "hello" }],
});
assert.equal(out.ok, true);
assert.equal(out.patches[0].rel_path, "output.md");
assert.equal(out.stderr, undefined);
assert.equal(out.capabilities.typed_result, true);
assert.equal(out.runner_protocol, "typed-v1");
assert.equal(out.result_v1.schema_version, 1);
assert.equal(out.result_v1.run_id, "run-1");
assert.equal(out.result_v1.outcome, "success");
assert.equal(out.result_v1.members.length, 1);
assert.equal(out.result_v1.content_digest, membersDigest(out.result_v1.members));
});
test("typedErrorCode is allowlisted and ignores raw stderr", () => {
assert.equal(typedErrorCode({ ok: false, fallback_reason: "prompt_empty" }), "PROMPT_EMPTY");
assert.equal(typedErrorCode({ ok: false, fallback_reason: "Connection error." }), "TRANSIENT");
const out = withResultV1({
ok: false,
run_id: "run-2",
retryable: true,
fallback_reason: "Connection error. /tmp/secret",
stderr: "token=abc",
patches: [],
});
assert.equal(out.stderr, undefined);
assert.equal(out.result_v1.error_code, "TRANSIENT");
assert.equal(out.result_v1.retryable, true);
assert.equal(out.result_v1.outcome, "error");
});
test("path escape members are omitted", () => {
const out = withResultV1({
ok: true,
run_id: "run-3",
patches: [
{ rel_path: "../etc/passwd", content: "x" },
{ rel_path: "/abs.md", content: "y" },
{ rel_path: "output.md", content: "ok" },
],
});
assert.deepEqual(out.result_v1.members.map((m) => m.path), ["output.md"]);
assert.deepEqual(out.patches.map((p) => p.rel_path), ["output.md"]);
});
test("truncateExecutionLogContent keeps only the tail", () => {
const big = "A".repeat(EXECUTION_LOG_PATCH_BYTES + 4096);
const out = truncateExecutionLogContent(big);
assert.ok(out.startsWith("[truncated "));
assert.ok(out.length < big.length);
assert.ok(Buffer.byteLength(out, "utf8") <= EXECUTION_LOG_PATCH_BYTES + 64);
});
test("oversized patch content is dropped from both members and patches", () => {
const huge = "x".repeat(MAX_RESULT_MEMBER_BYTES + 1);
const out = withResultV1({
ok: true,
run_id: "run-4",
patches: [
{ rel_path: "output.md", content: "# ok\n" },
{ rel_path: "blob.bin", content: huge },
],
});
assert.deepEqual(out.patches.map((p) => p.rel_path), ["output.md"]);
assert.deepEqual(out.result_v1.members.map((m) => m.path), ["output.md"]);
});
test("capPatches truncates execution.log and enforces total wire budget", () => {
const log = "L".repeat(EXECUTION_LOG_PATCH_BYTES * 4);
const patches = capPatches([
{ rel_path: "output.md", content: "# deliverable\n" },
{ rel_path: "execution.log", content: log },
]);
assert.equal(patches.length, 2);
assert.ok(patches.find((p) => p.rel_path === "execution.log").content.startsWith("[truncated "));
const total = patches.reduce((n, p) => n + Buffer.byteLength(p.content, "utf8"), 0);
assert.ok(total <= MAX_RESULT_WIRE_BYTES);
// Force total budget with a custom soft cap under the member limit.
const budget = 1024;
const filler = "F".repeat(budget - 2);
const tight = capPatches(
[
{ rel_path: "a.md", content: filler },
{ rel_path: "b.md", content: "too-much" },
],
{ maxTotalBytes: budget, maxMemberBytes: budget },
);
assert.deepEqual(tight.map((p) => p.rel_path), ["a.md"]);
});
test("withResultV1 wire estimate stays under soft budget for truncated logs", () => {
const log = "E".repeat(8 * 1024 * 1024);
const out = withResultV1({
ok: true,
run_id: "run-big",
patches: [
{ rel_path: "output.md", content: "# intake\n".repeat(100) },
{ rel_path: "execution.log", content: log },
],
});
assert.ok(estimateWireBytes(out) < MAX_RESULT_WIRE_BYTES);
assert.equal(out.result_v1.members.length, out.patches.length);
for (const m of out.result_v1.members) {
const body = out.patches.find((p) => p.rel_path === m.path);
assert.ok(body);
assert.equal(Buffer.byteLength(body.content, "utf8"), m.size);
}
});