fix: bind pi runtime config at spawn

This commit is contained in:
2026-08-05 05:23:58 +02:00
parent 2703864572
commit 4422568f61
4 changed files with 375 additions and 46 deletions
+108
View File
@@ -8,6 +8,8 @@ import { buildApp as buildRealApp } from "../src/app.js";
import { loadConfig } from "../src/config.js";
import { SseHub } from "../src/sse/sse-hub.js";
import { MaintenanceBarrier } from "../src/runtime/maintenance-gate.js";
import { PiProcessManager } from "../src/pi/pi-process-manager.js";
import { validateDeclarativePiConfig } from "../src/pi/managed-config.js";
const FAKE = path.resolve("../harness/tests/fake_pi/fake_pi_rpc.mjs");
const SCRIPT = path.resolve("../harness/tests/fake_pi/scripts/f1_disambiguation.json");
@@ -2426,6 +2428,112 @@ test("POST /sessions marks a persisted session failed when runtime construction
expect(failed).toBe(1);
});
test.each([
[
"new",
"auth.json",
'{"unrelated":{"credential":{"nested":["!post-session-auth-command route-secret /private/route-auth"]}}}\n',
"Session startup failed. Check configuration and connectivity, then Resume the session.",
],
[
"resume",
"models.json",
'{"providers":{"local-qwen":{"models":[{"id":"qwen3.6-35b-a3b","headers":{"x":"!post-session-model-command route-secret /private/route-model"}}]}}}\n',
"Session could not be resumed. Check configuration and connectivity, then try again.",
],
] as const)(
"POST %s refuses executable %s changed after session persistence without spawning or leaking",
async (flow, changedFile, unsafeRaw, publicMessage) => {
const agentDir = mkdtempSync(path.join(tmpdir(), "tht-route-runtime-config-"));
const safeAuth = '{"deepseek":{"type":"api_key","key":"safe-token"}}\n';
const safeModels = '{"providers":{"local-qwen":{"baseUrl":"http://model.invalid/v1","models":[{"id":"qwen3.6-35b-a3b"}]}}}\n';
writeFileSync(path.join(agentDir, "auth.json"), safeAuth, { mode: 0o600 });
writeFileSync(path.join(agentDir, "models.json"), safeModels, { mode: 0o600 });
vi.stubEnv("PI_CODING_AGENT_DIR", agentDir);
const cfg = loadConfig({ THT_HARNESS_DIR: "../harness" });
let authResolutions = 0;
let spawns = 0;
const mgr = new PiProcessManager(cfg, {
authProviders: () => { authResolutions += 1; return new Set(); },
spawnFn: () => { spawns += 1; throw new Error("ROUTE_SPAWN_BOUNDARY_REACHED"); },
});
const hub = new SseHub();
const events: Array<{ event: string; data: object }> = [];
const sessionId = `post-persistence-${flow}`;
hub.subscribe(sessionId, (event, data) => events.push({ event, data }));
const failSession = vi.fn(async () => {});
const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
const validateEarlierState = () => {
validateDeclarativePiConfig(readFileSync(path.join(agentDir, "auth.json"), "utf8"));
validateDeclarativePiConfig(readFileSync(path.join(agentDir, "models.json"), "utf8"));
};
if (flow === "resume") {
// This session was admitted and persisted while both mounted files were safe.
validateEarlierState();
writeFileSync(path.join(agentDir, changedFile), unsafeRaw, { mode: 0o600 });
}
const app = buildApp(cfg, {
mgr,
hub,
thtRunner: {
sessionNew: async () => {
// Model admission completed immediately above this persistence boundary.
writeFileSync(path.join(agentDir, changedFile), unsafeRaw, { mode: 0o600 });
return { id: sessionId };
},
failSession,
searchPack: async () => {},
sessionShow: async () => ({
id: sessionId,
status: "open",
archived: false,
provider: "local-qwen",
model: "qwen3.6-35b-a3b",
thinking: "low",
}),
reopenSession: async () => {},
} as any,
readiness: { ensure: async () => ({ ok: true }) } as any,
getSettings: () => ({
workspace: "w",
provider: "local-qwen",
model: "qwen3.6-35b-a3b",
thinking: "low",
}) as any,
listModels: async () => {
validateEarlierState();
return [{
provider: "local-qwen", id: "qwen3.6-35b-a3b", name: "Qwen", reasoning: true,
}];
},
});
try {
const response = flow === "new"
? await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } })
: await app.inject({ method: "POST", url: `/sessions/${sessionId}/resume` });
const logs = consoleError.mock.calls.flat().map(String).join(" ");
expect(response.statusCode).toBe(503);
expect(response.json()).toEqual({ error: publicMessage });
expect({ authResolutions, spawns, runtimes: mgr.count() }).toEqual({
authResolutions: 0, spawns: 0, runtimes: 0,
});
expect(failSession).toHaveBeenCalledTimes(flow === "new" ? 1 : 0);
expect(events).toEqual([]);
expect(`${response.body}\n${logs}`).not.toContain(unsafeRaw.trim());
expect(`${response.body}\n${logs}`).not.toMatch(/route-secret|post-session-(?:auth|model)-command|\/private\/route-|tht-route-runtime-config/);
} finally {
await app.close();
consoleError.mockRestore();
vi.unstubAllEnvs();
rmSync(agentDir, { recursive: true, force: true });
}
},
);
test("POST /sessions/:id/resume returns 409 for a read-only session without calling ollamaEnsure", async () => {
let ensureCalled = false;
const app = mutApp({