mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-22 17:22:47 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acb96c02cc |
Generated
+10
@@ -39240,6 +39240,7 @@
|
|||||||
"strip-ansi": "^7.1.2",
|
"strip-ansi": "^7.1.2",
|
||||||
"tree-kill": "^1.2.2",
|
"tree-kill": "^1.2.2",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
|
"undici": "^7.30.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"which": "^5.0.0",
|
"which": "^5.0.0",
|
||||||
"ws": "^8.14.2",
|
"ws": "^8.14.2",
|
||||||
@@ -39970,6 +39971,15 @@
|
|||||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/server/node_modules/undici": {
|
||||||
|
"version": "7.30.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz",
|
||||||
|
"integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/server/node_modules/vitest": {
|
"packages/server/node_modules/vitest": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz",
|
||||||
|
|||||||
@@ -141,6 +141,7 @@
|
|||||||
"strip-ansi": "^7.1.2",
|
"strip-ansi": "^7.1.2",
|
||||||
"tree-kill": "^1.2.2",
|
"tree-kill": "^1.2.2",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
|
"undici": "^7.30.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"which": "^5.0.0",
|
"which": "^5.0.0",
|
||||||
"ws": "^8.14.2",
|
"ws": "^8.14.2",
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { Agent, getGlobalDispatcher, setGlobalDispatcher } from "undici";
|
||||||
|
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import { createTestLogger } from "../../../../../test-utils/test-logger.js";
|
||||||
|
import { V2Runtime } from "./runtime.js";
|
||||||
|
|
||||||
|
// Answers every request at once except session.wait, which it holds for WAIT_MS before answering,
|
||||||
|
// the way OpenCode holds the wait response until the session's agent loop is idle.
|
||||||
|
const FAKE_HELPER = `
|
||||||
|
import http from "node:http";
|
||||||
|
const server = http.createServer((request, response) => {
|
||||||
|
if (request.url.endsWith("/wait")) {
|
||||||
|
setTimeout(() => { response.statusCode = 204; response.end(); }, Number(process.env.WAIT_MS));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response.setHeader("content-type", "application/json");
|
||||||
|
response.end("{}");
|
||||||
|
});
|
||||||
|
server.listen(0, "127.0.0.1", () => {
|
||||||
|
console.log("server listening on http://127.0.0.1:" + server.address().port);
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
|
||||||
|
let root: string;
|
||||||
|
const defaultDispatcher = getGlobalDispatcher();
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
root = await mkdtemp(path.join(os.tmpdir(), "paseo-opencode-v2-runtime-"));
|
||||||
|
vi.stubEnv("PASEO_HOME", path.join(root, "home"));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
setGlobalDispatcher(defaultDispatcher);
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
await rm(root, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("session.wait outlasts the fetch header deadline until the helper reports the session idle", async () => {
|
||||||
|
// Stands in for undici's five-minute default, which ended every longer turn.
|
||||||
|
setGlobalDispatcher(new Agent({ headersTimeout: 200 }));
|
||||||
|
const helper = path.join(root, "fake-opencode.mjs");
|
||||||
|
await writeFile(helper, FAKE_HELPER);
|
||||||
|
const runtime = new V2Runtime({
|
||||||
|
logger: createTestLogger(),
|
||||||
|
settings: {
|
||||||
|
command: { mode: "replace", argv: [process.execPath, helper] },
|
||||||
|
env: { WAIT_MS: "2500" },
|
||||||
|
},
|
||||||
|
decorateEnv: async (env) => env,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const connection = await runtime.acquire();
|
||||||
|
await expect(connection.client.session.wait({ sessionID: "ses_long" })).resolves.toBe(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await runtime.shutdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -5,6 +5,7 @@ import type { V2Api } from "./api.js";
|
|||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
import { mkdir } from "node:fs/promises";
|
import { mkdir } from "node:fs/promises";
|
||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
|
import { Agent } from "undici";
|
||||||
import { spawnProcess } from "../../../../../utils/spawn.js";
|
import { spawnProcess } from "../../../../../utils/spawn.js";
|
||||||
import { terminateWithTreeKill } from "../../../../../utils/tree-kill.js";
|
import { terminateWithTreeKill } from "../../../../../utils/tree-kill.js";
|
||||||
import {
|
import {
|
||||||
@@ -174,6 +175,9 @@ export class V2Runtime {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const processAbort = new AbortController();
|
const processAbort = new AbortController();
|
||||||
|
// session.wait answers only once the session is idle, so a header deadline would fail any
|
||||||
|
// turn that outlasts it. Requests end when the helper exits or the caller aborts instead.
|
||||||
|
const dispatcher = new Agent({ headersTimeout: 0 });
|
||||||
const exited = new Promise<Error>((resolve) =>
|
const exited = new Promise<Error>((resolve) =>
|
||||||
process.once("exit", (code) => {
|
process.once("exit", (code) => {
|
||||||
const error = new Error(`OpenCode helper server exited (${code})`);
|
const error = new Error(`OpenCode helper server exited (${code})`);
|
||||||
@@ -199,6 +203,7 @@ export class V2Runtime {
|
|||||||
const stop = () => {
|
const stop = () => {
|
||||||
stopped ??= (async () => {
|
stopped ??= (async () => {
|
||||||
await terminateWithTreeKill(process, { gracefulTimeoutMs: 5_000, forceTimeoutMs: 1_000 });
|
await terminateWithTreeKill(process, { gracefulTimeoutMs: 5_000, forceTimeoutMs: 1_000 });
|
||||||
|
await dispatcher.destroy();
|
||||||
const entry = await record;
|
const entry = await record;
|
||||||
if (entry) await managedProcesses?.remove(entry.id);
|
if (entry) await managedProcesses?.remove(entry.id);
|
||||||
})();
|
})();
|
||||||
@@ -243,7 +248,9 @@ export class V2Runtime {
|
|||||||
const signal = init?.signal
|
const signal = init?.signal
|
||||||
? AbortSignal.any([init.signal, processAbort.signal])
|
? AbortSignal.any([init.signal, processAbort.signal])
|
||||||
: processAbort.signal;
|
: processAbort.signal;
|
||||||
const response = await fetch(request, { ...init, signal });
|
// Node's fetch accepts an undici dispatcher, which the DOM RequestInit type omits.
|
||||||
|
const requestInit: RequestInit & { dispatcher: Agent } = { ...init, signal, dispatcher };
|
||||||
|
const response = await fetch(request, requestInit);
|
||||||
const html = response.headers.get("content-type")?.includes("text/html") ?? false;
|
const html = response.headers.get("content-type")?.includes("text/html") ?? false;
|
||||||
if (!response.ok || html) {
|
if (!response.ok || html) {
|
||||||
const requestUrl = request instanceof Request ? request.url : String(request);
|
const requestUrl = request instanceof Request ? request.url : String(request);
|
||||||
|
|||||||
Reference in New Issue
Block a user