From 418c00604a2bdf6ea93abacda88b7f87d4c7611f Mon Sep 17 00:00:00 2001 From: Kashyab Ambarani Date: Fri, 25 Sep 2026 03:47:32 -0700 Subject: [PATCH] test: preserve Windows PATH casing and exercise real fetch aborts --- test/cli.test.mjs | 11 ++++++++--- test/stream-abort.test.mjs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/test/cli.test.mjs b/test/cli.test.mjs index d23c20a..c601acf 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -8,10 +8,12 @@ import { clearDevinBinCache, findDevinBin, whichDevin, runDevin } from "../src/c function fixture(t) { const directory = mkdtempSync(join(tmpdir(), "pi devin cli ")); - const original = { ...process.env }; + // Windows environment lookups ignore case; a spread object does not. + const keys = ["DEVIN_CLI", "LOCALAPPDATA", "ProgramFiles", "PATH"]; + const original = Object.fromEntries(keys.map((key) => [key, process.env[key]])); clearDevinBinCache(); t.after(() => { - for (const key of ["DEVIN_CLI", "LOCALAPPDATA", "ProgramFiles", "PATH"]) { + for (const key of keys) { if (original[key] === undefined) delete process.env[key]; else process.env[key] = original[key]; } clearDevinBinCache(); rmSync(directory, { recursive: true, force: true }); @@ -47,10 +49,13 @@ test("Windows PATH lookup uses where.exe and reads multiple results", { skip: pr process.env.LOCALAPPDATA = directory; process.env.ProgramFiles = directory; const bin = join(directory, "path bin"); mkdirSync(bin); const binary = join(bin, "devin.exe"); writeFileSync(binary, ""); - process.env.PATH = `${bin};${process.env.PATH}`; + const otherBin = join(directory, "other bin"); mkdirSync(otherBin); + writeFileSync(join(otherBin, "devin.exe"), ""); + process.env.PATH = `${bin};${otherBin};${process.env.PATH}`; const located = await whichDevin(); const probe = spawnSync("where.exe", ["devin.exe"], { encoding: "utf8" }); assert.equal(located, binary, JSON.stringify({ status: probe.status, stdout: probe.stdout, stderr: probe.stderr, error: probe.error?.message })); + assert.ok(probe.stdout.trim().split(/\r?\n/).length >= 2); }); test("Windows batch wrappers work in paths with spaces, including inherited stdio", { skip: process.platform !== "win32" }, async (t) => { diff --git a/test/stream-abort.test.mjs b/test/stream-abort.test.mjs index c2c3c34..e5d24b7 100644 --- a/test/stream-abort.test.mjs +++ b/test/stream-abort.test.mjs @@ -67,3 +67,38 @@ test("aborting an in-flight stream reports aborted without leaking a rejection", assert.equal((await stream.result()).stopReason, "aborted"); assert.deepEqual(rejections, []); }); + +test("real fetch cancellation after a response chunk leaves the process usable", { timeout: 10_000 }, async (t) => { + const { createServer } = await import("node:http"); + const { once } = await import("node:events"); + const { encodeString, encodeVarintField, frameConnectStream } = await import("../src/wire.ts"); + clearCachedUserJwt(); t.after(clearCachedUserJwt); + let chats = 0; + const server = createServer((req, res) => { + if (req.url.endsWith("/GetUserJwt")) { + res.end(encodeString(1, "eyJtest.jwt")); return; + } + chats++; + res.writeHead(200, { "Content-Type": "application/connect+proto" }); + res.write(frameConnectStream(encodeString(3, "started"), false)); + if (chats > 1) { + res.write(frameConnectStream(encodeVarintField(5, 0), false)); + res.end(Buffer.from([2, 0, 0, 0, 2, 123, 125])); + } + }); + t.after(() => { server.closeAllConnections(); server.close(); }); + server.listen(0, "127.0.0.1"); await once(server, "listening"); + const ac = new AbortController(); + const options = { apiKey: "synthetic-test-key", env: { DEVIN_API_SERVER_URL: `http://127.0.0.1:${server.address().port}` } }; + const stream = streamDevin(model, normalizeContext({ messages: [user("hello")] }), { ...options, signal: ac.signal }); + let sawText = false; + for await (const event of stream) { + if (event.type === "text_delta") { sawText = true; ac.abort(); } + } + assert.equal(sawText, true); + assert.equal((await stream.result()).stopReason, "aborted"); + const next = streamDevin(model, normalizeContext({ messages: [user("again")] }), options); + for await (const event of next) assert.notEqual(event.type, "error"); + assert.equal((await next.result()).stopReason, "stop"); + assert.equal(chats, 2); +});