fix: Windows test compatibility for worktree, snapshot, and history paths

- Replace `mkdir -p` shell calls with `mkdirSync({ recursive: true })`
  in worktree tests (cmd.exe doesn't support -p)
- Normalize path assertions in provider-snapshot-manager tests for
  Windows drive-letter resolved paths
- Add `:` to Claude history path sanitization so Windows drive letters
  (C:\) don't produce invalid directory names
This commit is contained in:
Mohamed Boudra
2026-04-13 20:27:44 +07:00
parent 95e112c185
commit 6dfcb4daa9
4 changed files with 71 additions and 55 deletions

View File

@@ -1,3 +1,4 @@
import { resolve } from "node:path";
import { describe, expect, test, vi } from "vitest";
import { createTestLogger } from "../../test-utils/test-logger.js";
@@ -44,6 +45,10 @@ const TEST_CAPABILITIES = {
} as const;
describe("ProviderSnapshotManager", () => {
const projectCwd = resolve("/tmp/project");
const projectACwd = resolve("/tmp/project-a");
const projectBCwd = resolve("/tmp/project-b");
test("getSnapshot returns all providers in loading state initially and triggers warmUp", async () => {
const codexModels = deferred<AgentModelDefinition[]>();
const claudeModels = deferred<AgentModelDefinition[]>();
@@ -59,7 +64,7 @@ describe("ProviderSnapshotManager", () => {
]);
const manager = new ProviderSnapshotManager(registry, createTestLogger());
const snapshot = manager.getSnapshot("/tmp/project");
const snapshot = manager.getSnapshot(projectCwd);
expect(snapshot.map((entry) => entry.provider)).toEqual(["codex", "claude"]);
expect(getProviderEntry(snapshot, "claude")).toMatchObject({
@@ -102,14 +107,14 @@ describe("ProviderSnapshotManager", () => {
]);
const manager = new ProviderSnapshotManager(registry, createTestLogger());
manager.getSnapshot("/tmp/project");
manager.getSnapshot(projectCwd);
await vi.waitFor(() => {
expect(getProviderEntry(manager.getSnapshot("/tmp/project"), "claude")?.status).toBe("ready");
expect(getProviderEntry(manager.getSnapshot("/tmp/project"), "codex")?.status).toBe("ready");
expect(getProviderEntry(manager.getSnapshot(projectCwd), "claude")?.status).toBe("ready");
expect(getProviderEntry(manager.getSnapshot(projectCwd), "codex")?.status).toBe("ready");
});
const snapshot = manager.getSnapshot("/tmp/project");
const snapshot = manager.getSnapshot(projectCwd);
expect(getProviderEntry(snapshot, "codex")).toMatchObject({
provider: "codex",
status: "ready",
@@ -142,10 +147,10 @@ describe("ProviderSnapshotManager", () => {
]);
const manager = new ProviderSnapshotManager(registry, createTestLogger());
manager.getSnapshot("/tmp/project");
manager.getSnapshot(projectCwd);
await vi.waitFor(() => {
expect(manager.getSnapshot("/tmp/project")).toEqual([
expect(manager.getSnapshot(projectCwd)).toEqual([
{
provider: "codex",
status: "unavailable",
@@ -173,10 +178,10 @@ describe("ProviderSnapshotManager", () => {
]);
const manager = new ProviderSnapshotManager(registry, createTestLogger());
manager.getSnapshot("/tmp/project");
manager.getSnapshot(projectCwd);
await vi.waitFor(() => {
expect(manager.getSnapshot("/tmp/project")).toEqual([
expect(manager.getSnapshot(projectCwd)).toEqual([
{
provider: "codex",
status: "error",
@@ -215,7 +220,7 @@ describe("ProviderSnapshotManager", () => {
};
manager.on("change", listener);
manager.getSnapshot("/tmp/project");
manager.getSnapshot(projectCwd);
claudeModels.resolve([createModel("claude", "sonnet")]);
claudeModes.resolve([createMode("default")]);
@@ -224,7 +229,7 @@ describe("ProviderSnapshotManager", () => {
expect(changes).toHaveLength(1);
});
expect(changes[0]?.cwd).toBe("/tmp/project");
expect(changes[0]?.cwd).toBe(projectCwd);
expect(getProviderEntry(changes[0]?.entries ?? [], "claude")?.status).toBe("ready");
expect(getProviderEntry(changes[0]?.entries ?? [], "codex")?.status).toBe("loading");
@@ -256,16 +261,16 @@ describe("ProviderSnapshotManager", () => {
]);
const manager = new ProviderSnapshotManager(registry, createTestLogger());
manager.getSnapshot("/tmp/project");
manager.getSnapshot(projectCwd);
await vi.waitFor(() => {
expect(getProviderEntry(manager.getSnapshot("/tmp/project"), "codex")?.models?.[0]?.id).toBe(
expect(getProviderEntry(manager.getSnapshot(projectCwd), "codex")?.models?.[0]?.id).toBe(
"gpt-5.1",
);
});
manager.refresh("/tmp/project");
expect(manager.getSnapshot("/tmp/project")).toEqual([
manager.refresh(projectCwd);
expect(manager.getSnapshot(projectCwd)).toEqual([
{
provider: "codex",
status: "loading",
@@ -276,7 +281,7 @@ describe("ProviderSnapshotManager", () => {
]);
await vi.waitFor(() => {
expect(getProviderEntry(manager.getSnapshot("/tmp/project"), "codex")?.models?.[0]?.id).toBe(
expect(getProviderEntry(manager.getSnapshot(projectCwd), "codex")?.models?.[0]?.id).toBe(
"gpt-5.2",
);
});
@@ -300,9 +305,9 @@ describe("ProviderSnapshotManager", () => {
const changes: ProviderSnapshotEntry[][] = [];
manager.on("change", (entries) => changes.push(entries));
manager.refresh("/tmp/project");
manager.refresh(projectCwd);
expect(manager.getSnapshot("/tmp/project")).toEqual([
expect(manager.getSnapshot(projectCwd)).toEqual([
{
provider: "codex",
status: "loading",
@@ -312,9 +317,9 @@ describe("ProviderSnapshotManager", () => {
},
]);
manager.refresh("/tmp/project");
manager.refresh("/tmp/project");
manager.refresh("/tmp/project");
manager.refresh(projectCwd);
manager.refresh(projectCwd);
manager.refresh(projectCwd);
expect(changes).toHaveLength(1);
expect(handles.codex?.isAvailable).toHaveBeenCalledTimes(1);
@@ -323,7 +328,7 @@ describe("ProviderSnapshotManager", () => {
fetchModes.resolve([createMode("auto")]);
await vi.waitFor(() => {
expect(getProviderEntry(manager.getSnapshot("/tmp/project"), "codex")).toMatchObject({
expect(getProviderEntry(manager.getSnapshot(projectCwd), "codex")).toMatchObject({
provider: "codex",
status: "ready",
models: [createModel("codex", "gpt-5.2")],
@@ -351,9 +356,9 @@ describe("ProviderSnapshotManager", () => {
]);
const manager = new ProviderSnapshotManager(registry, createTestLogger());
manager.getSnapshot("/tmp/project");
manager.getSnapshot("/tmp/project");
manager.getSnapshot("/tmp/project");
manager.getSnapshot(projectCwd);
manager.getSnapshot(projectCwd);
manager.getSnapshot(projectCwd);
await vi.waitFor(() => {
expect(handles.codex?.isAvailable).toHaveBeenCalledTimes(1);
@@ -365,7 +370,7 @@ describe("ProviderSnapshotManager", () => {
codexModels.resolve([createModel("codex", "gpt-5.2")]);
await vi.waitFor(() => {
expect(getProviderEntry(manager.getSnapshot("/tmp/project"), "codex")?.status).toBe("ready");
expect(getProviderEntry(manager.getSnapshot(projectCwd), "codex")?.status).toBe("ready");
});
manager.destroy();
@@ -384,25 +389,21 @@ describe("ProviderSnapshotManager", () => {
]);
const manager = new ProviderSnapshotManager(registry, createTestLogger());
manager.getSnapshot("/tmp/project-a");
manager.getSnapshot("/tmp/project-b");
manager.getSnapshot(projectACwd);
manager.getSnapshot(projectBCwd);
await vi.waitFor(() => {
expect(getProviderEntry(manager.getSnapshot("/tmp/project-a"), "codex")?.status).toBe(
"ready",
);
expect(getProviderEntry(manager.getSnapshot("/tmp/project-b"), "codex")?.status).toBe(
"ready",
);
expect(getProviderEntry(manager.getSnapshot(projectACwd), "codex")?.status).toBe("ready");
expect(getProviderEntry(manager.getSnapshot(projectBCwd), "codex")?.status).toBe("ready");
});
expect(getProviderEntry(manager.getSnapshot("/tmp/project-a"), "codex")?.models?.[0]?.id).toBe(
"model:/tmp/project-a",
expect(getProviderEntry(manager.getSnapshot(projectACwd), "codex")?.models?.[0]?.id).toBe(
`model:${projectACwd}`,
);
expect(getProviderEntry(manager.getSnapshot("/tmp/project-b"), "codex")?.models?.[0]?.id).toBe(
"model:/tmp/project-b",
expect(getProviderEntry(manager.getSnapshot(projectBCwd), "codex")?.models?.[0]?.id).toBe(
`model:${projectBCwd}`,
);
expect(seenCwds).toEqual(["/tmp/project-a", "/tmp/project-b"]);
expect(seenCwds).toEqual([projectACwd, projectBCwd]);
manager.destroy();
});
@@ -420,13 +421,13 @@ describe("ProviderSnapshotManager", () => {
]);
const manager = new ProviderSnapshotManager(registry, createTestLogger());
manager.getSnapshot("/tmp/project");
manager.getSnapshot(projectCwd);
await vi.waitFor(() => {
expect(getProviderEntry(manager.getSnapshot("/tmp/project"), "zai")?.status).toBe("ready");
expect(getProviderEntry(manager.getSnapshot(projectCwd), "zai")?.status).toBe("ready");
});
expect(getProviderEntry(manager.getSnapshot("/tmp/project"), "zai")).toMatchObject({
expect(getProviderEntry(manager.getSnapshot(projectCwd), "zai")).toMatchObject({
provider: "zai",
status: "ready",
label: "ZAI",
@@ -441,7 +442,7 @@ describe("ProviderSnapshotManager", () => {
const { registry } = createRegistry([createMockProvider({ provider: "claude" })]);
const manager = new ProviderSnapshotManager(registry, createTestLogger());
const snapshot = manager.getSnapshot("/tmp/project");
const snapshot = manager.getSnapshot(projectCwd);
expect(snapshot.map((entry) => entry.provider)).toEqual(["claude"]);
expect(getProviderEntry(snapshot, "zai")).toBeUndefined();
@@ -464,7 +465,7 @@ describe("ProviderSnapshotManager", () => {
]);
const manager = new ProviderSnapshotManager(registry, createTestLogger());
expect(manager.getSnapshot("/tmp/project")).toEqual([
expect(manager.getSnapshot(projectCwd)).toEqual([
{
provider: "zai",
status: "loading",
@@ -478,7 +479,7 @@ describe("ProviderSnapshotManager", () => {
modes.resolve([createMode("plan")]);
await vi.waitFor(() => {
expect(getProviderEntry(manager.getSnapshot("/tmp/project"), "zai")).toMatchObject({
expect(getProviderEntry(manager.getSnapshot(projectCwd), "zai")).toMatchObject({
provider: "zai",
status: "ready",
label: "ZAI",

View File

@@ -241,6 +241,10 @@ function isClaudeThinkingEffort(value: string | null | undefined): value is Clau
return value === "low" || value === "medium" || value === "high" || value === "max";
}
function sanitizeClaudeProjectPath(cwd: string): string {
return cwd.replace(/[\\/._:]/g, "-");
}
type ClaudeOptionsLogSummary = {
cwd: string | null;
permissionMode: string | null;
@@ -3260,8 +3264,7 @@ class ClaudeAgentSession implements AgentSession {
private resolveHistoryPath(sessionId: string): string | null {
const cwd = this.config.cwd;
if (!cwd) return null;
// Match Claude CLI's path sanitization: replace slashes, dots, and underscores with dashes
const sanitized = cwd.replace(/[\\/\.]/g, "-").replace(/_/g, "-");
const sanitized = sanitizeClaudeProjectPath(cwd);
const configDir = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
const dir = path.join(configDir, "projects", sanitized);
return path.join(dir, `${sessionId}.jsonl`);

View File

@@ -21,6 +21,10 @@ const LIVE_REPLY_MARKER = "LIVE_ONLY_REPLY_MARKER";
const HISTORY_USER_MARKER = "HISTORY_ONLY_USER_MARKER";
const HISTORY_ASSISTANT_MARKER = "HISTORY_ONLY_ASSISTANT_MARKER";
function sanitizeClaudeProjectPath(cwd: string): string {
return cwd.replace(/[\\/._:]/g, "-");
}
function buildSdkQueryMock() {
const events = [
{
@@ -106,7 +110,7 @@ describe("ClaudeAgentSession history replay regression", () => {
configDir = path.join(tempRoot, "claude-config");
mkdirSync(cwd, { recursive: true });
const sanitized = cwd.replace(/[\\/\.]/g, "-").replace(/_/g, "-");
const sanitized = sanitizeClaudeProjectPath(cwd);
const historyDir = path.join(configDir, "projects", sanitized);
mkdirSync(historyDir, { recursive: true });
const historyPath = path.join(historyDir, "history-session.jsonl");

View File

@@ -13,7 +13,15 @@ import {
} from "./worktree";
import { getPaseoWorktreeMetadataPath } from "./worktree-metadata.js";
import { execSync } from "child_process";
import { mkdtempSync, rmSync, existsSync, realpathSync, writeFileSync, readFileSync } from "fs";
import {
mkdtempSync,
mkdirSync,
rmSync,
existsSync,
realpathSync,
writeFileSync,
readFileSync,
} from "fs";
import { dirname, join } from "path";
import { tmpdir } from "os";
import net from "node:net";
@@ -30,7 +38,7 @@ describe("createWorktree", () => {
paseoHome = join(tempDir, "paseo-home");
// Create a git repo with an initial commit
execSync(`mkdir -p ${repoDir}`);
mkdirSync(repoDir, { recursive: true });
execSync("git init -b main", { cwd: repoDir });
execSync("git config user.email 'test@test.com'", { cwd: repoDir });
execSync("git config user.name 'Test'", { cwd: repoDir });
@@ -68,7 +76,7 @@ describe("createWorktree", () => {
const privateTempDir = realpathSync(varTempDir);
const varRepoDir = join(varTempDir, "test-repo");
const varPaseoHome = join(varTempDir, "paseo-home");
execSync(`mkdir -p ${varRepoDir}`);
mkdirSync(varRepoDir, { recursive: true });
execSync("git init -b main", { cwd: varRepoDir });
execSync("git config user.email 'test@test.com'", { cwd: varRepoDir });
execSync("git config user.name 'Test'", { cwd: varRepoDir });
@@ -118,7 +126,7 @@ describe("createWorktree", () => {
it("treats non-git directories as non-worktrees without throwing", async () => {
const nonGitDir = join(tempDir, "not-a-repo");
execSync(`mkdir -p ${nonGitDir}`);
mkdirSync(nonGitDir, { recursive: true });
const ownership = await isPaseoOwnedWorktreeCwd(nonGitDir, { paseoHome });
@@ -494,7 +502,7 @@ describe("paseo worktree manager", () => {
repoDir = join(tempDir, "test-repo");
paseoHome = join(tempDir, "paseo-home");
execSync(`mkdir -p ${repoDir}`);
mkdirSync(repoDir, { recursive: true });
execSync("git init -b main", { cwd: repoDir });
execSync("git config user.email 'test@test.com'", { cwd: repoDir });
execSync("git config user.name 'Test'", { cwd: repoDir });
@@ -512,7 +520,7 @@ describe("paseo worktree manager", () => {
const repoB = join(tempDir, "team-b", "test-repo");
for (const repo of [repoA, repoB]) {
execSync(`mkdir -p ${repo}`);
mkdirSync(repo, { recursive: true });
execSync("git init -b main", { cwd: repo });
execSync("git config user.email 'test@test.com'", { cwd: repo });
execSync("git config user.name 'Test'", { cwd: repo });
@@ -584,7 +592,7 @@ describe("paseo worktree manager", () => {
});
const nestedDir = join(created.worktreePath, "nested", "dir");
execSync(`mkdir -p ${nestedDir}`);
mkdirSync(nestedDir, { recursive: true });
await deletePaseoWorktree({ cwd: repoDir, worktreePath: nestedDir, paseoHome });
expect(existsSync(created.worktreePath)).toBe(false);