mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 20:46:28 +01:00
fix(runner): recreate clean-room scratch roots
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
3165f057a3
commit
cf27ba2869
3 files changed
+43
-8
No files matched your search
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<title>🫧 Mock Paperclip · Issue thread</title>
|
||||
<title>🧯 Mock Paperclip · Issue thread</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
@@ -120,8 +120,12 @@ function scratchRoot() {
|
||||
return process.env.PAPERCLIP_RUN_SCRATCH_DIR ?? process.env.PAPERCLIP_SCRATCH_DIR ?? tmpdir();
|
||||
}
|
||||
|
||||
async function createWorkingDirectory(prefix = "phase7-issue-thread-") {
|
||||
return mkdtemp(resolve(scratchRoot(), prefix));
|
||||
async function createWorkingDirectory(root, prefix = "phase7-issue-thread-") {
|
||||
// Managed previews can outlive the heartbeat that launched them. Paperclip
|
||||
// removes that heartbeat's scratch directory when the run ends, so ensure
|
||||
// the inherited parent still exists before every later session is minted.
|
||||
await mkdir(root, { recursive: true });
|
||||
return mkdtemp(resolve(root, prefix));
|
||||
}
|
||||
|
||||
/** Mock-only fixture seed. Identifiers use the reserved `MCK-` prefix (§1.3). */
|
||||
@@ -171,6 +175,7 @@ class RouteError extends Error {
|
||||
|
||||
export function createPhase7IssueThreadMiddleware(options = {}) {
|
||||
const load = options.loadRunner ?? loadRunner;
|
||||
const workingDirectoryRoot = options.scratchRoot ?? scratchRoot();
|
||||
let bootstrap = null;
|
||||
let bindHost = options.bindHost ?? "127.0.0.1";
|
||||
/**
|
||||
@@ -195,7 +200,8 @@ export function createPhase7IssueThreadMiddleware(options = {}) {
|
||||
bootstrap = (async () => {
|
||||
const runner = await load();
|
||||
runner.assertPhase4bLoopbackBindHost(bindHost);
|
||||
const workingDirectory = options.workingDirectory ?? (await createWorkingDirectory());
|
||||
const workingDirectory =
|
||||
options.workingDirectory ?? (await createWorkingDirectory(workingDirectoryRoot));
|
||||
const service = new runner.Phase7LiveSessionService({
|
||||
store: new runner.InMemoryPhase7LiveSessionStore(),
|
||||
...(options.transportFactory === undefined
|
||||
@@ -322,7 +328,10 @@ export function createPhase7IssueThreadMiddleware(options = {}) {
|
||||
for (const stale of open.slice(0, Math.max(0, open.length - (MAX_CLEAN_ROOM_SESSIONS - 1)))) {
|
||||
await retire(service, stale.session.id, "clean-room capacity");
|
||||
}
|
||||
const workingDirectory = await createWorkingDirectory("phase7-clean-room-");
|
||||
const workingDirectory = await createWorkingDirectory(
|
||||
workingDirectoryRoot,
|
||||
"phase7-clean-room-",
|
||||
);
|
||||
const { identity, input } = runner.createPhase7CleanRoomSessionInput({ workingDirectory });
|
||||
let session;
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { once } from "node:events";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { createServer, type Server } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { tmpdir } from "node:os";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
@@ -468,13 +471,16 @@ let server: Server;
|
||||
let origin: string;
|
||||
let middleware: Awaited<ReturnType<typeof createMiddleware>>;
|
||||
let providerState: FakeProviderState;
|
||||
let scratchParent: string;
|
||||
let scratchRoot: string;
|
||||
const requestedUrls: string[] = [];
|
||||
|
||||
async function createMiddleware(state: FakeProviderState) {
|
||||
async function createMiddleware(state: FakeProviderState, root: string) {
|
||||
const module = await import("../../scripts/phase7-issue-thread-server.mjs");
|
||||
return module.createPhase7IssueThreadMiddleware({
|
||||
bindHost: "127.0.0.1",
|
||||
loadRunner: async () => await import("../index.js"),
|
||||
scratchRoot: root,
|
||||
transportFactory: fakeTransportFactory(state),
|
||||
});
|
||||
}
|
||||
@@ -520,6 +526,10 @@ async function status(path: string, body?: unknown, browser: Jar = jar): Promise
|
||||
|
||||
beforeEach(async () => {
|
||||
requestedUrls.length = 0;
|
||||
scratchParent = await mkdtemp(resolve(tmpdir(), "phase7-clean-room-server-test-"));
|
||||
// Start with a missing child so the normal open path also proves that the
|
||||
// middleware creates its configured scratch root.
|
||||
scratchRoot = resolve(scratchParent, "run-scratch");
|
||||
jar = new Jar();
|
||||
providerState = {
|
||||
threadId: "codex-thread-clean-room",
|
||||
@@ -529,7 +539,7 @@ beforeEach(async () => {
|
||||
gate: new Gate(),
|
||||
diagnostic: null,
|
||||
};
|
||||
middleware = await createMiddleware(providerState);
|
||||
middleware = await createMiddleware(providerState, scratchRoot);
|
||||
server = createServer((request, response) => {
|
||||
void middleware(request, response, () => {
|
||||
response.statusCode = 404;
|
||||
@@ -545,6 +555,7 @@ afterEach(async () => {
|
||||
await middleware.close();
|
||||
server.close();
|
||||
await once(server, "close");
|
||||
await rm(scratchParent, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("Phase 7 clean-room chat server", () => {
|
||||
@@ -570,6 +581,21 @@ describe("Phase 7 clean-room chat server", () => {
|
||||
expect(opened.view.evidence.traceability).toEqual([]);
|
||||
});
|
||||
|
||||
it("recreates a removed run scratch root before starting a later chat", async () => {
|
||||
const opened = await call("/api/phase7/ui/cleanroom/session");
|
||||
|
||||
// Managed services survive the heartbeat that launched them, while the
|
||||
// harness removes this run-owned parent as soon as that heartbeat exits.
|
||||
await rm(scratchRoot, { recursive: true, force: true });
|
||||
|
||||
const fresh = await call("/api/phase7/ui/cleanroom/session", {
|
||||
sessionId: opened.sessionId,
|
||||
});
|
||||
expect(fresh.sessionId).not.toBe(opened.sessionId);
|
||||
expect(fresh.identity.token).not.toBe(opened.identity.token);
|
||||
expect(fresh.view.composer.state).toBe("ready");
|
||||
});
|
||||
|
||||
it("publishes the real-API block as an inspectable record", async () => {
|
||||
const opened = await call("/api/phase7/ui/cleanroom/session");
|
||||
const guard = opened.view.evidence.control_plane.find((record) =>
|
||||
|
||||
Reference in new issue
Block a user