Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/coding-agent/test/bash-session-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,17 @@ async function createSession(options: { persisted?: boolean; stage?: string } =
}

afterEach(() => {
for (const root of roots.splice(0)) if (existsSync(root)) rmSync(root, { recursive: true, force: true });
// Windows locks a directory while a spawned shell still has it as its cwd, so a
// detached async job can outlive its test and make cleanup fail with EBUSY.
// Retry, then give up: reclaiming an OS temp dir is never the assertion.
for (const root of roots.splice(0)) {
if (!existsSync(root)) continue;
try {
rmSync(root, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
} catch {
// Left for the OS to reclaim.
}
}
});

describe("session-aware bash environment", () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/coding-agent/test/footer-width.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { sep } from "node:path";
import { visibleWidth } from "@earendil-works/pi-tui";
import { beforeAll, describe, expect, it } from "vitest";
import type { AgentSession } from "../src/core/agent-session.ts";
Expand Down Expand Up @@ -115,7 +116,7 @@ describe("formatCwdForFooter", () => {

it("abbreviates the home directory and descendants", () => {
expect(formatCwdForFooter("/home/user", "/home/user")).toBe("~");
expect(formatCwdForFooter("/home/user/project", "/home/user")).toBe("~/project");
expect(formatCwdForFooter("/home/user/project", "/home/user")).toBe(`~${sep}project`);
});
});

Expand Down
9 changes: 9 additions & 0 deletions packages/coding-agent/test/interactive-auth-login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ import { LoginDialogComponent } from "../src/modes/interactive/components/login-
import { InteractiveModeBase } from "../src/modes/interactive/interactive-mode-base.ts";
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
import "../src/modes/interactive/interactive-auth-login.ts";
import { openBrowser } from "../src/utils/open-browser.ts";

// Driving the real LoginDialogComponent through onAuth opens the platform browser.
// On Windows that spawns a detached `rundll32 url.dll,FileProtocolHandler`, which
// inherits this process's stdout/stderr write handles; on a runner with no browser
// it never exits, the suite's pipes never reach EOF, and the CI step hangs until the
// job budget expires. Mock the launcher, as pi's login-dialog suites do.
vi.mock("../src/utils/open-browser.ts", () => ({ openBrowser: vi.fn() }));

beforeAll(() => {
initTheme("dark");
Expand Down Expand Up @@ -153,6 +161,7 @@ describe("interactive OAuth cancellation", () => {
const dialog = addedChildren[0] as LoginDialogComponent;
expect(dialog.render(100).join("\n")).toContain("Sign in to Corp");
expect(completeProviderAuthentication).toHaveBeenCalledOnce();
expect(vi.mocked(openBrowser)).toHaveBeenCalledWith("https://corp.invalid/login");
}, 1_000);

it("keeps a post-login refresh AbortError visible", async () => {
Expand Down
17 changes: 11 additions & 6 deletions packages/coding-agent/test/sdk-session-manager.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { existsSync, mkdirSync, realpathSync, rmSync } from "node:fs";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { join, sep } from "node:path";
import { getModel } from "@earendil-works/pi-ai/compat";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.ts";
Expand Down Expand Up @@ -92,7 +92,7 @@ describe("createAgentSession session manager defaults", () => {
const sessionFile = session.sessionManager.getSessionFile();

expect(sessionDir).toBe(expectedSessionDir);
expect(sessionFile?.startsWith(`${expectedSessionDir}/`)).toBe(true);
expect(sessionFile?.startsWith(`${expectedSessionDir}${sep}`)).toBe(true);

session.dispose();
});
Expand Down Expand Up @@ -129,17 +129,22 @@ describe("createAgentSession session manager defaults", () => {
});

expect(session.sessionManager).toBe(sessionManager);
expect(session.systemPrompt).toContain(`Current working directory: ${sessionCwd}`);
// The system prompt always renders the cwd with POSIX separators (system-prompt.ts).
expect(session.systemPrompt).toContain(`Current working directory: ${sessionCwd.replaceAll("\\", "/")}`);

const bashTool = session.agent.state.tools.find((tool) => tool.name === "bash");
expect(bashTool).toBeTruthy();
const result = await bashTool!.execute("test", { command: "pwd" });
// Prove the tool's working directory by reading a marker only resolvable from
// sessionCwd. Comparing `pwd` text is not portable: on Windows the bash tool runs
// under Git Bash, which reports MSYS paths (/tmp/...) that Node cannot resolve.
writeFileSync(join(sessionCwd, "cwd-marker.txt"), "session-cwd-marker");
const result = await bashTool!.execute("test", { command: "cat cwd-marker.txt" });
const output = result.content
.filter((item): item is { type: "text"; text: string } => item.type === "text")
.map((item) => item.text)
.join("");

expect(realpathSync(output.trim())).toBe(realpathSync(sessionCwd));
expect(output.trim()).toBe("session-cwd-marker");

session.dispose();
});
Expand Down
31 changes: 27 additions & 4 deletions scripts/run-flaky-test-suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,39 @@ function safeName(value: string): string {
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "tests";
}

/** Tee one child stream to the live step log while capturing it for diagnostics. */
async function pump(stream: ReadableStream<Uint8Array> | undefined, sink: NodeJS.WriteStream): Promise<string> {
if (!stream) return "";
const decoder = new TextDecoder();
let text = "";
for await (const chunk of stream) {
const piece = decoder.decode(chunk, { stream: true });
text += piece;
sink.write(piece);
}
const tail = decoder.decode();
if (tail) {
text += tail;
sink.write(tail);
}
return text;
}

/**
* Stream child output as it arrives instead of buffering it until the child exits.
* A suite that overruns the job's `timeout-minutes` budget is killed mid-run, and a
* buffered attempt discards every line it had already collected, leaving the cancelled
* step with no record of which test stalled. Teeing preserves the retry and diagnostics
* contract while making a timed-out attempt self-describing in the step log.
*/
async function runAttempt(command: string[], logPath: string, persist: boolean): Promise<{ code: number; output: string }> {
const child = Bun.spawn(command, { stdout: "pipe", stderr: "pipe", env: process.env });
const [stdout, stderr, code] = await Promise.all([
new Response(child.stdout).text(),
new Response(child.stderr).text(),
pump(child.stdout, process.stdout),
pump(child.stderr, process.stderr),
child.exited,
]);
const output = `${stdout}${stderr}`;
process.stdout.write(stdout);
process.stderr.write(stderr);
if (persist) writeFileSync(logPath, output);
return { code, output };
}
Expand Down
Loading