mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 14:22:14 +03:00
Keeping the tail is the right direction — `sendPrompt` resolves with the stdout collected since the prompt was written and stderr is read for diagnostics after a failure, so the newest output is what callers actually use. The `[...output truncated...]` marker keeps it from being silent. Resetting `stderrBuffer` alongside `stdoutBuffer` fixes the subtler half: diagnostics for one prompt were carrying stale output from every earlier one. Verified on the tree that actually ships — your branch merged onto the current tip, which already carries #13096: `appendCapped()` and `settle()` coexist cleanly and all 7 assertions across both ACP test files pass together. I reformatted the changelog fragment to the `changelog.d` convention (`- **fix(scope):** …`) before merging — `check:changelog-integrity` rejects a fragment that does not start with a markdown bullet, which is the same gate your #13158 was about. Wording is yours, unchanged in substance. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them. - `typecheck:core` clean - complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline - 71 focused assertions green across the 13 test files this batch adds or touches ⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff. Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
144 lines
4.5 KiB
TypeScript
144 lines
4.5 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { AcpManager } = await import("../../src/lib/acp/manager.ts");
|
|
const { setCustomAgents } = await import("../../src/lib/acp/registry.ts");
|
|
|
|
const AGENT_ID = "buffer-cap-probe";
|
|
const CAP = 1_048_576;
|
|
|
|
/**
|
|
* Spawn a node process that writes `bytes` of stdout (or stderr) and stays alive,
|
|
* so the buffers can be inspected while the session is still running.
|
|
*/
|
|
function makeAgent(stream: "stdout" | "stderr", bytes: number) {
|
|
setCustomAgents([
|
|
{
|
|
id: AGENT_ID,
|
|
name: "Buffer cap probe",
|
|
binary: process.execPath,
|
|
acpSpawnable: true,
|
|
},
|
|
]);
|
|
const script = `
|
|
const chunk = "x".repeat(64 * 1024);
|
|
let written = 0;
|
|
const target = ${bytes};
|
|
while (written < target) {
|
|
process.${stream}.write(chunk);
|
|
written += chunk.length;
|
|
}
|
|
setInterval(() => {}, 1000);
|
|
`;
|
|
return ["-e", script];
|
|
}
|
|
|
|
async function waitForOutput(session: { stdoutBuffer: string; stderrBuffer: string }) {
|
|
// Give the child time to flush everything it intends to write.
|
|
for (let i = 0; i < 60; i++) {
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
if (session.stdoutBuffer.length > CAP / 2 || session.stderrBuffer.length > CAP / 2) break;
|
|
}
|
|
await new Promise((r) => setTimeout(r, 300));
|
|
}
|
|
|
|
test("stdout buffer stays bounded when an agent floods it (#13095)", async () => {
|
|
const mgr = new AcpManager();
|
|
const session = mgr.spawn(AGENT_ID, process.execPath, makeAgent("stdout", 4 * CAP));
|
|
try {
|
|
await waitForOutput(session);
|
|
assert.ok(
|
|
session.stdoutBuffer.length > 0,
|
|
"precondition: the probe agent must have written something"
|
|
);
|
|
assert.ok(
|
|
session.stdoutBuffer.length <= CAP,
|
|
`stdoutBuffer grew to ${session.stdoutBuffer.length} chars, above the ${CAP} cap`
|
|
);
|
|
} finally {
|
|
mgr.kill(session.id);
|
|
}
|
|
});
|
|
|
|
test("stderr buffer stays bounded when an agent floods it (#13095)", async () => {
|
|
const mgr = new AcpManager();
|
|
const session = mgr.spawn(AGENT_ID, process.execPath, makeAgent("stderr", 4 * CAP));
|
|
try {
|
|
await waitForOutput(session);
|
|
assert.ok(
|
|
session.stderrBuffer.length > 0,
|
|
"precondition: the probe agent must have written something"
|
|
);
|
|
assert.ok(
|
|
session.stderrBuffer.length <= CAP,
|
|
`stderrBuffer grew to ${session.stderrBuffer.length} chars, above the ${CAP} cap`
|
|
);
|
|
} finally {
|
|
mgr.kill(session.id);
|
|
}
|
|
});
|
|
|
|
test("truncation keeps the most recent output, not the oldest (#13095)", async () => {
|
|
setCustomAgents([
|
|
{
|
|
id: AGENT_ID,
|
|
name: "Buffer cap probe",
|
|
binary: process.execPath,
|
|
acpSpawnable: true,
|
|
},
|
|
]);
|
|
const script = `
|
|
const chunk = "x".repeat(64 * 1024);
|
|
let written = 0;
|
|
while (written < ${2 * CAP}) { process.stdout.write(chunk); written += chunk.length; }
|
|
process.stdout.write("FINAL-MARKER");
|
|
setInterval(() => {}, 1000);
|
|
`;
|
|
const mgr = new AcpManager();
|
|
const session = mgr.spawn(AGENT_ID, process.execPath, ["-e", script]);
|
|
try {
|
|
await waitForOutput(session);
|
|
// The tail is the part callers use: sendPrompt resolves with stdout, and
|
|
// stderr is read for diagnostics after a failure.
|
|
assert.ok(
|
|
session.stdoutBuffer.endsWith("FINAL-MARKER"),
|
|
"the newest output must survive truncation"
|
|
);
|
|
assert.ok(session.stdoutBuffer.length <= CAP, "buffer must still respect the cap");
|
|
} finally {
|
|
mgr.kill(session.id);
|
|
}
|
|
});
|
|
|
|
test("stderr is reset between prompts so diagnostics are per-prompt (#13095)", async () => {
|
|
setCustomAgents([
|
|
{
|
|
id: AGENT_ID,
|
|
name: "Buffer cap probe",
|
|
binary: process.execPath,
|
|
acpSpawnable: true,
|
|
},
|
|
]);
|
|
// Echoes stdin back on stdout, and writes a fixed line to stderr per prompt.
|
|
const script = `
|
|
process.stdin.on("data", (d) => {
|
|
process.stderr.write("warn:" + d.toString().trim() + "\\n");
|
|
process.stdout.write("ok\\n");
|
|
});
|
|
setInterval(() => {}, 1000);
|
|
`;
|
|
const mgr = new AcpManager();
|
|
const session = mgr.spawn(AGENT_ID, process.execPath, ["-e", script]);
|
|
try {
|
|
await mgr.sendPrompt(session.id, "first", 6000);
|
|
await mgr.sendPrompt(session.id, "second", 6000);
|
|
assert.ok(
|
|
!session.stderrBuffer.includes("warn:first"),
|
|
`stderr from an earlier prompt leaked into the next one: ${JSON.stringify(session.stderrBuffer)}`
|
|
);
|
|
assert.ok(session.stderrBuffer.includes("warn:second"), "current prompt's stderr must be kept");
|
|
} finally {
|
|
mgr.kill(session.id);
|
|
}
|
|
});
|