feat: add live executor observability

This commit is contained in:
liujing
2026-07-17 00:48:52 +08:00
parent 3566a73a03
commit c9d76d8f58
13 changed files with 1546 additions and 28 deletions
+133
View File
@@ -1,4 +1,6 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { Writable } from "node:stream";
import { test } from "vitest";
import { spawnBufferedChild, spawnStreamingChild } from "./child-process.js";
@@ -117,3 +119,134 @@ test("spawnStreamingChild escalates ignored SIGTERM to SIGKILL after the abort g
assert.equal(child.signal, "SIGKILL");
assert.match(chunks.join("") + child.stdout, /ready/);
});
// ---------------------------------------------------------------------------
// logFilePath tee (real-time observability)
// ---------------------------------------------------------------------------
test("logFilePath: stdout is readable from the log file before the child exits", async () => {
const tmpDir = fs.mkdtempSync("/tmp/agent-workflow-tee-test-");
try {
const logFile = path.join(tmpDir, "exec.log");
const { stream } = collectSink();
const script = [
"process.stdout.write('pre-delay\\n');",
"setTimeout(() => { process.stdout.write('post-delay\\n'); }, 200);",
].join("");
const childPromise = spawnStreamingChild(process.execPath, ["-e", script], {
stdoutSink: stream,
stderrSink: stream,
logFilePath: logFile,
});
// Before the child finishes, the log file should already have 'pre-delay'
await new Promise<void>((resolve, reject) => {
const check = () => {
try {
if (fs.existsSync(logFile) && fs.readFileSync(logFile, "utf8").includes("pre-delay")) return resolve();
setTimeout(check, 10);
} catch { setTimeout(check, 10); }
};
setTimeout(() => reject(new Error("timed out waiting for pre-delay in log")), 2000);
check();
});
await childPromise;
// After completion, the log file contains all output
const fullLog = fs.readFileSync(logFile, "utf8");
assert.match(fullLog, /pre-delay/);
assert.match(fullLog, /post-delay/);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test("logFilePath: stderr tee is also captured in the log file", async () => {
const tmpDir = fs.mkdtempSync("/tmp/agent-workflow-tee-test-");
try {
const logFile = path.join(tmpDir, "exec.log");
const { stream } = collectSink();
const r = await spawnStreamingChild(
process.execPath,
["-e", "process.stderr.write('stderr-msg\\n'); process.stdout.write('stdout-msg\\n');"],
{ stdoutSink: stream, stderrSink: stream, logFilePath: logFile },
);
assert.equal(r.status, 0);
const logContent = fs.readFileSync(logFile, "utf8");
assert.match(logContent, /stderr-msg/);
assert.match(logContent, /stdout-msg/);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test("logFilePath: Chinese UTF-8 is preserved correctly", async () => {
const tmpDir = fs.mkdtempSync("/tmp/agent-workflow-utf8-test-");
try {
const logFile = path.join(tmpDir, "exec.log");
const { stream } = collectSink();
const r = await spawnStreamingChild(
process.execPath,
["-e", "console.log('你好世界'); console.log('日本語'); console.log('🚀');"],
{ stdoutSink: stream, stderrSink: stream, logFilePath: logFile },
);
assert.equal(r.status, 0);
const logContent = fs.readFileSync(logFile, "utf8");
assert.match(logContent, /你好世界/);
assert.match(logContent, /日本語/);
assert.match(logContent, /🚀/);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test("logFilePath: onProgress reports pid and bytesLogged", async () => {
const tmpDir = fs.mkdtempSync("/tmp/agent-workflow-progress-test-");
try {
const logFile = path.join(tmpDir, "exec.log");
const { stream } = collectSink();
const progressUpdates: Array<{ pid?: number; bytesLogged: number }> = [];
const r = await spawnStreamingChild(
process.execPath,
["-e", "process.stdout.write('hello world');"],
{
stdoutSink: stream,
stderrSink: stream,
logFilePath: logFile,
onProgress: (info) => { progressUpdates.push(info); },
},
);
assert.equal(r.status, 0);
assert.ok(progressUpdates.length >= 1);
assert.ok(progressUpdates.some((p) => p.bytesLogged > 0));
if (progressUpdates[0].pid) assert.ok(progressUpdates[0].pid! > 0);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test("logFilePath: write stream error resolves spawnStreamingChild with result.error", async () => {
const tmpDir = fs.mkdtempSync("/tmp/agent-workflow-log-err-test-");
try {
// Parent does not exist → ENOENT on first write
const badLogFile = path.join(tmpDir, "nonexistent", "exec.log");
const { stream } = collectSink();
const r = await spawnStreamingChild(
process.execPath,
["-e", "console.log('hello');"],
{
stdoutSink: stream,
stderrSink: stream,
logFilePath: badLogFile,
},
);
assert.equal(r.status, 0);
assert.ok(r.error !== undefined, "expected result.error from log stream failure");
assert.match(String(r.error), /ENOENT|no such file|open/);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});