fix: harden runtime config helper protocol and lifecycle

This commit is contained in:
2026-08-11 13:03:37 +02:00
parent ec92f7f994
commit 11cc8628cf
4 changed files with 290 additions and 34 deletions
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { WorkspaceRuntimeConfigLeaseFactory } from "../src/workspaces/runtime-config-lease.js";
import { runBoundedHelper, WorkspaceRuntimeConfigLeaseFactory } from "../src/workspaces/runtime-config-lease.js";
import { parse } from "yaml";
import { parseWorkspaceYaml, serializeWorkspaceYaml } from "../src/workspaces/schema.js";
@@ -72,6 +72,7 @@ function fixture(extraEnv: Record<string, string> = {}) {
const factoryInput = {
dataRoot, runtimeSnapshotRoot: snapshots, harnessDir: harness, configPath,
env: {
NODE_ENV: "test",
THT_WS_ABC_DWH_TRANSPORT: "postgres_direct", THT_WS_ABC_DWH_HOST: "dwh",
THT_WS_ABC_DWH_PORT: "5432", THT_WS_ABC_DWH_USER: "reader",
THT_WS_ABC_DWH_PASSWORD_FILE: secret, ...extraEnv,
@@ -258,10 +259,12 @@ test("runtime config symlink replacement is refused", async () => {
function realHarnessBinding(config: string): Record<string, string> {
const helper = join(process.cwd(), "..", "harness", "tht", "runtime_config_lease_io.py");
const python = join(process.cwd(), "..", "harness", ".venv", "bin", "python");
return JSON.parse(execFileSync(python, [helper], {
const result = JSON.parse(execFileSync(python, [helper], {
cwd: join(process.cwd(), "..", "harness"), encoding: "utf8",
input: JSON.stringify({ protocol_version: 1, action: "binding", config_hex: Buffer.from(config).toString("hex") }),
}));
const { protocol_version: _protocol, kind: _kind, ...binding } = result;
return binding;
}
test("explicit installation overlay is canonical and has one real harness binding", async () => {
@@ -609,3 +612,18 @@ else:
expect(output.trim()).toBe("rejected");
} finally { rmSync(f.root, { recursive: true, force: true }); }
});
test("bounded helper settles early stdin close and a never-reading child without crashing", async () => {
const options = (action: string, payload: string, timeoutMs = 200) => ({
cwd: tmpdir(), env: process.env, action, payload, timeoutMs,
});
await expect(runBoundedHelper(process.execPath, ["-e", "process.stdin.destroy(); setTimeout(() => {}, 1000)"],
options("early-close", "x".repeat(16 * 1024 * 1024)))).rejects.toThrow();
const started = Date.now();
const timer = new Promise<void>((resolve) => setTimeout(resolve, 20));
await expect(runBoundedHelper(process.execPath, ["-e", "setTimeout(() => {}, 10000)"],
options("never-read", "x".repeat(16 * 1024 * 1024), 80))).rejects.toThrow(/timed out|pipe|closed/i);
await timer;
expect(Date.now() - started).toBeLessThan(2_000);
});