mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 21:49:18 +01:00
Merge master b189f586d into Codex runner default candidate
Preserve master transport, lifecycle, and release verification controls alongside macOS qualification. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
f4abdc2cfc
749 files changed
+124181
-68418
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { createServer } from "node:net";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Agent, Company, InstanceExperimentalSettings } from "@paperclipai/shared";
|
||||
import {
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
redactTestDriveText,
|
||||
resolveTestDriveBootstrap,
|
||||
resolveTestDriveDataDir,
|
||||
resolveTestDriveServerPort,
|
||||
testDriveCommand,
|
||||
type TestDriveApi,
|
||||
type TestDriveHarness,
|
||||
@@ -86,6 +88,32 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("test-drive data isolation", () => {
|
||||
it.each([false, true])("reuses the saved port, with occupied=%s", async (occupied) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-test-drive-port-"));
|
||||
cleanupDirectories.push(root);
|
||||
const listener = createServer();
|
||||
await new Promise<void>((resolve) => listener.listen(0, "127.0.0.1", resolve));
|
||||
const savedPort = (listener.address() as { port: number }).port;
|
||||
const close = () => new Promise<void>((resolve, reject) => listener.close(error => error ? reject(error) : resolve()));
|
||||
if (!occupied) await close();
|
||||
const configPath = path.join(root, "instances", "default", "config.json");
|
||||
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
||||
fs.writeFileSync(configPath, JSON.stringify({
|
||||
$meta: { version: 1, updatedAt: new Date().toISOString(), source: "onboard" },
|
||||
database: {}, logging: { mode: "file" }, telemetry: { enabled: false },
|
||||
server: { port: savedPort },
|
||||
}));
|
||||
try {
|
||||
await prepareTestDriveEnvironment({ dataDir: root }, os.tmpdir());
|
||||
if (occupied) { expect(Number(process.env.PORT)).not.toBe(savedPort); expect(Number(process.env.PORT)).toBeGreaterThanOrEqual(3100); }
|
||||
else expect(Number(process.env.PORT)).toBe(savedPort);
|
||||
expect(JSON.parse(fs.readFileSync(configPath, "utf8")).server.port).toBe(savedPort);
|
||||
expect(process.env.HOST).toBe("127.0.0.1");
|
||||
} finally {
|
||||
if (occupied) await close();
|
||||
}
|
||||
});
|
||||
|
||||
it("creates unique retained OS temporary directories and reports absolute paths", () => {
|
||||
const first = resolveTestDriveDataDir();
|
||||
const second = resolveTestDriveDataDir();
|
||||
@@ -600,3 +628,16 @@ describe("test-drive foreground lifecycle", () => {
|
||||
expect(openBrowser).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("test drive port recovery", () => {
|
||||
it("falls back from an occupied saved port at the top of the range", async () => {
|
||||
const available = vi.fn(async (port: number) => port === 3101);
|
||||
await expect(resolveTestDriveServerPort(65535, available)).resolves.toBe(3101);
|
||||
expect(available.mock.calls.map(([port]) => port)).toEqual([65535, 3100, 3101]);
|
||||
});
|
||||
it("preserves a free saved port before searching the default range", async () => {
|
||||
const available = vi.fn(async () => true);
|
||||
await expect(resolveTestDriveServerPort(3110, available)).resolves.toBe(3110);
|
||||
expect(available).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -59,6 +59,7 @@ interface IssueCreateOptions extends BaseClientOptions {
|
||||
projectId?: string;
|
||||
goalId?: string;
|
||||
parentId?: string;
|
||||
standalone?: boolean;
|
||||
requestDepth?: string;
|
||||
billingCode?: string;
|
||||
}
|
||||
@@ -286,10 +287,17 @@ export function registerIssueCommands(program: Command): void {
|
||||
.option("--project-id <id>", "Project ID")
|
||||
.option("--goal-id <id>", "Goal ID")
|
||||
.option("--parent-id <id>", "Parent issue ID")
|
||||
.option(
|
||||
"--standalone",
|
||||
"Create a top-level issue. Without --parent-id, an agent run's issue otherwise becomes the parent.",
|
||||
)
|
||||
.option("--request-depth <n>", "Request depth integer")
|
||||
.option("--billing-code <code>", "Billing code")
|
||||
.action(async (opts: IssueCreateOptions) => {
|
||||
try {
|
||||
if (opts.standalone && opts.parentId) {
|
||||
throw new Error("--standalone cannot be combined with --parent-id");
|
||||
}
|
||||
const ctx = resolveCommandContext(opts, { requireCompany: true });
|
||||
const payload = createIssueSchema.parse({
|
||||
title: opts.title,
|
||||
@@ -299,7 +307,7 @@ export function registerIssueCommands(program: Command): void {
|
||||
assigneeAgentId: opts.assigneeAgentId,
|
||||
projectId: opts.projectId,
|
||||
goalId: opts.goalId,
|
||||
parentId: opts.parentId,
|
||||
parentId: opts.standalone ? null : opts.parentId,
|
||||
requestDepth: parseOptionalInt(opts.requestDepth),
|
||||
billingCode: opts.billingCode,
|
||||
});
|
||||
|
||||
@@ -150,11 +150,14 @@ async function loopbackPortAvailable(port: number): Promise<boolean> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveTestDriveServerPort(preferredPort = 3100): Promise<number> {
|
||||
for (let port = preferredPort; port <= 65_535; port += 1) {
|
||||
if (await loopbackPortAvailable(port)) return port;
|
||||
export async function resolveTestDriveServerPort(preferredPort = 3100, available = loopbackPortAvailable): Promise<number> {
|
||||
if (await available(preferredPort)) return preferredPort;
|
||||
// A saved high port can be occupied. Search the ordinary range instead of
|
||||
// trapping the restarted test drive at the top of the TCP port space.
|
||||
for (let port = 3100; port <= 65_535; port += 1) {
|
||||
if (port !== preferredPort && await available(port)) return port;
|
||||
}
|
||||
throw new Error(`No available loopback port found at or above ${preferredPort}.`);
|
||||
throw new Error("No available loopback port found at or above 3100.");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,7 +197,15 @@ export async function prepareTestDriveEnvironment(
|
||||
process.env.PAPERCLIP_DEPLOYMENT_EXPOSURE = "private";
|
||||
process.env.PAPERCLIP_BIND = "loopback";
|
||||
process.env.HOST = "127.0.0.1";
|
||||
process.env.PORT = String(await resolveTestDriveServerPort());
|
||||
let preferredPort = 3100;
|
||||
try {
|
||||
preferredPort = readConfig(process.env.PAPERCLIP_CONFIG)?.server.port ?? preferredPort;
|
||||
} catch {
|
||||
// The normal startup doctor owns invalid-config diagnostics and repair.
|
||||
}
|
||||
// Reusing a test drive must preserve callback and enrollment origins when
|
||||
// its saved port is available. Only move forward if that port is occupied.
|
||||
process.env.PORT = String(await resolveTestDriveServerPort(preferredPort));
|
||||
|
||||
return { dataDir, linkedWorktree };
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user