mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
@@ -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",
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user