Fix Windows workspace provider loading (#1329)
* test: cover Windows provider snapshot cwd updates * fix: normalize Windows provider snapshot paths * fix: preserve Claude history path candidates * fix: match Claude project paths on Windows
This commit is contained in:
@@ -39,4 +39,13 @@ describe("providers snapshot query scope", () => {
|
||||
providers: ["codex"],
|
||||
});
|
||||
});
|
||||
|
||||
it("uses one query scope for Windows cwd values with either separator", () => {
|
||||
expect(normalizeProvidersSnapshotCwd("C:\\Users\\Ezekiel Bulver\\project")).toBe(
|
||||
"C:/Users/Ezekiel Bulver/project",
|
||||
);
|
||||
expect(providersSnapshotQueryKey("server-1", "C:\\Users\\Ezekiel Bulver\\project")).toEqual(
|
||||
providersSnapshotQueryKey("server-1", "C:/Users/Ezekiel Bulver/project"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
|
||||
import { normalizeWorkspacePath } from "@/utils/workspace-identity";
|
||||
|
||||
export const PROVIDERS_SNAPSHOT_QUERY_ROOT = "providersSnapshot";
|
||||
|
||||
export function normalizeProvidersSnapshotCwd(cwd?: string | null): string | null {
|
||||
const trimmed = cwd?.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
return normalizeWorkspacePath(cwd);
|
||||
}
|
||||
|
||||
export function providersSnapshotQueryRoot(serverId: string | null) {
|
||||
|
||||
@@ -250,6 +250,27 @@ describe("applyProvidersSnapshotUpdate", () => {
|
||||
providersSnapshot([]),
|
||||
);
|
||||
});
|
||||
|
||||
it("applies Windows daemon updates to app-normalized workspace paths", () => {
|
||||
const workspaceCwd = "C:/Users/Ezekiel Bulver/project";
|
||||
const daemonCwd = "C:\\Users\\Ezekiel Bulver\\project";
|
||||
queryClient.setQueryData(
|
||||
providersSnapshotQueryKey(serverId, workspaceCwd),
|
||||
providersSnapshot([codexEntry("loading")]),
|
||||
);
|
||||
|
||||
applyProvidersSnapshotUpdate({
|
||||
serverId,
|
||||
queryClient,
|
||||
message: updateMessage([codexEntry("ready", [readyCodexModel])], daemonCwd),
|
||||
});
|
||||
|
||||
expect(queryClient.getQueryData(providersSnapshotQueryKey(serverId, workspaceCwd))).toEqual({
|
||||
entries: [codexEntry("ready", [readyCodexModel])],
|
||||
generatedAt: "2026-01-01T00:00:01.000Z",
|
||||
requestId: "providers_snapshot_update",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectorOpenRefetchDecision", () => {
|
||||
|
||||
@@ -45,6 +45,7 @@ import { renderPromptAttachmentAsText } from "../../prompt-attachments.js";
|
||||
import { claudeQuery, type ClaudeOptions, type ClaudeQueryFactory } from "./query.js";
|
||||
import { realClaudeRewindSdk, revertClaudeConversation, revertClaudeFiles } from "./rewind.js";
|
||||
import { normalizeProviderReplayTimestamp } from "../../provider-history-timestamps.js";
|
||||
import { claudeProjectDirSync } from "./project-dir.js";
|
||||
|
||||
import {
|
||||
getAgentStreamEventTurnId,
|
||||
@@ -339,10 +340,6 @@ function isClaudeThinkingEffort(value: string | null | undefined): value is Clau
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeClaudeProjectPath(cwd: string): string {
|
||||
return cwd.replace(/[\\/._:]/g, "-");
|
||||
}
|
||||
|
||||
interface ClaudeOptionsLogSummary {
|
||||
cwd: string | null;
|
||||
permissionMode: string | null;
|
||||
@@ -3985,14 +3982,15 @@ class ClaudeAgentSession implements AgentSession {
|
||||
// Fall back to the configured cwd when the path has already disappeared.
|
||||
}
|
||||
for (const candidate of candidates) {
|
||||
const sanitized = sanitizeClaudeProjectPath(candidate);
|
||||
const historyPath = path.join(configDir, "projects", sanitized, `${sessionId}.jsonl`);
|
||||
const historyPath = path.join(
|
||||
claudeProjectDirSync(candidate, { configDir }),
|
||||
`${sessionId}.jsonl`,
|
||||
);
|
||||
if (fs.existsSync(historyPath)) {
|
||||
return historyPath;
|
||||
}
|
||||
}
|
||||
const sanitized = sanitizeClaudeProjectPath(cwd);
|
||||
return path.join(configDir, "projects", sanitized, `${sessionId}.jsonl`);
|
||||
return path.join(claudeProjectDirSync(cwd, { configDir }), `${sessionId}.jsonl`);
|
||||
}
|
||||
|
||||
private convertHistoryEntry(entry: ClaudeHistoryEntry): AgentTimelineItem[] {
|
||||
|
||||
+3
-7
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { createTestLogger } from "../../../../test-utils/test-logger.js";
|
||||
import { ClaudeAgentClient } from "./agent.js";
|
||||
import { claudeProjectDirSync } from "./project-dir.js";
|
||||
import { streamSession } from "../test-utils/session-stream-adapter.js";
|
||||
import type { AgentPersistenceHandle, AgentStreamEvent } from "../../agent-sdk-types.js";
|
||||
|
||||
@@ -15,10 +16,6 @@ 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 = [
|
||||
{
|
||||
@@ -100,12 +97,11 @@ describe("ClaudeAgentSession history replay regression", () => {
|
||||
});
|
||||
|
||||
tempRoot = mkdtempSync(path.join(os.tmpdir(), "claude-history-regression-"));
|
||||
cwd = path.join(tempRoot, "repo");
|
||||
cwd = path.join(tempRoot, "Michael Depies", "repo");
|
||||
configDir = path.join(tempRoot, "claude-config");
|
||||
mkdirSync(cwd, { recursive: true });
|
||||
|
||||
const sanitized = sanitizeClaudeProjectPath(cwd);
|
||||
const historyDir = path.join(configDir, "projects", sanitized);
|
||||
const historyDir = claudeProjectDirSync(cwd, { configDir });
|
||||
mkdirSync(historyDir, { recursive: true });
|
||||
const historyPath = path.join(historyDir, "history-session.jsonl");
|
||||
writeFileSync(
|
||||
|
||||
@@ -7,7 +7,7 @@ import { join } from "node:path";
|
||||
import { getSessionInfo } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
||||
|
||||
import { claudeProjectDir } from "./project-dir.js";
|
||||
import { claudeProjectDir, claudeProjectDirSync } from "./project-dir.js";
|
||||
|
||||
// Parity oracle: the Claude SDK's getSessionInfo({ dir }) canonicalizes the
|
||||
// given dir with the SDK's own encoder and looks for `<sessionId>.jsonl` under
|
||||
@@ -101,6 +101,12 @@ describe("claudeProjectDir parity with Claude Agent SDK", () => {
|
||||
expect(info?.sessionId).toBe(sessionId);
|
||||
});
|
||||
}
|
||||
|
||||
test("sync helper uses the same directory as the SDK-parity helper", async () => {
|
||||
const cwd = await ensureDir(join(workspaceRoot, "sync path with spaces"));
|
||||
|
||||
await expect(claudeProjectDir(cwd)).resolves.toBe(claudeProjectDirSync(cwd));
|
||||
});
|
||||
});
|
||||
|
||||
async function ensureDir(path: string): Promise<string> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { realpathSync } from "node:fs";
|
||||
import { realpath } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -23,6 +24,12 @@ export async function claudeProjectDir(
|
||||
return join(projectsRoot, encode(canonical));
|
||||
}
|
||||
|
||||
export function claudeProjectDirSync(cwd: string, options?: ClaudeProjectDirOptions): string {
|
||||
const canonical = canonicalizeSync(cwd);
|
||||
const projectsRoot = join(resolveConfigDir(options), "projects");
|
||||
return join(projectsRoot, encode(canonical));
|
||||
}
|
||||
|
||||
async function canonicalize(input: string): Promise<string> {
|
||||
try {
|
||||
return (await realpath(input)).normalize("NFC");
|
||||
@@ -31,6 +38,14 @@ async function canonicalize(input: string): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
function canonicalizeSync(input: string): string {
|
||||
try {
|
||||
return realpathSync.native(input).normalize("NFC");
|
||||
} catch {
|
||||
return input.normalize("NFC");
|
||||
}
|
||||
}
|
||||
|
||||
function encode(input: string): string {
|
||||
const replaced = input.replace(/[^a-zA-Z0-9]/g, "-");
|
||||
if (replaced.length <= PROJECT_DIR_LENGTH_CAP) {
|
||||
|
||||
Reference in New Issue
Block a user