mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-01 22:23:07 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82f1bea6b1 |
@@ -0,0 +1,78 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { searchDirectoryEntries } from "./directory-suggestions.js";
|
||||
|
||||
const reportedFiles = [
|
||||
"data/prices/claude.json",
|
||||
"docs/architecture.ts",
|
||||
"docs/reference/ai模型价格-Claude.md",
|
||||
"scripts/cleanup_codex_cua_orphans.sh",
|
||||
"src/chat-input.tsx",
|
||||
"src/chronicle.ts",
|
||||
];
|
||||
const longPath = `needle/${"a".repeat(80)}/${"b".repeat(80)}/file.txt`;
|
||||
|
||||
// Keep diagnostic fixtures in the workspace, separate from the user's files.
|
||||
function fixture(files: string[]): string {
|
||||
const artifacts = path.resolve(import.meta.dirname, "../../../../.artifacts");
|
||||
mkdirSync(artifacts, { recursive: true });
|
||||
const root = mkdtempSync(path.join(artifacts, "file-search-7650-"));
|
||||
for (const file of files) {
|
||||
const target = path.join(root, file);
|
||||
mkdirSync(path.dirname(target), { recursive: true });
|
||||
writeFileSync(target, "");
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
async function search(root: string, query: string): Promise<string[]> {
|
||||
return (
|
||||
await searchDirectoryEntries({
|
||||
root,
|
||||
query,
|
||||
pathFormat: "relative",
|
||||
pathQueryPolicy: "slashes",
|
||||
includeFiles: true,
|
||||
includeDirectories: false,
|
||||
})
|
||||
).map((entry) => entry.path);
|
||||
}
|
||||
|
||||
describe("#7650 reported search expectations", () => {
|
||||
let root: string;
|
||||
beforeAll(() => {
|
||||
root = fixture(reportedFiles);
|
||||
});
|
||||
afterAll(() => rmSync(root, { recursive: true, force: true }));
|
||||
|
||||
it("requires both parts of a mixed-script query", async () => {
|
||||
expect(await search(root, "claude价格")).toEqual(["docs/reference/ai模型价格-Claude.md"]);
|
||||
});
|
||||
|
||||
it("prefers the filename abbreviation over a mid-word substring", async () => {
|
||||
expect(await search(root, "chi")).toEqual([
|
||||
"src/chat-input.tsx",
|
||||
"src/chronicle.ts",
|
||||
"docs/architecture.ts",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#7650 existing search compatibility", () => {
|
||||
let root: string;
|
||||
beforeAll(() => {
|
||||
root = fixture(["привет.md", "설정.md", "résumé.md", "---.md", longPath]);
|
||||
});
|
||||
afterAll(() => rmSync(root, { recursive: true, force: true }));
|
||||
|
||||
it.each([
|
||||
["привет", "привет.md"],
|
||||
["설정", "설정.md"],
|
||||
["résumé", "résumé.md"],
|
||||
["---", "---.md"],
|
||||
["needle", longPath],
|
||||
])("preserves matching for %s", async (query, expected) => {
|
||||
expect(await search(root, query)).toContain(expected);
|
||||
});
|
||||
});
|
||||
@@ -250,84 +250,6 @@ describe("cursor usage source", () => {
|
||||
expect(cursor.status).toBe("available");
|
||||
});
|
||||
|
||||
describe.each([
|
||||
["modern state.vscdb", "v1"],
|
||||
["modern state.vscdb", "v99"],
|
||||
["legacy cursorAuthStatus", "v1"],
|
||||
["auth.json", "v1"],
|
||||
["CURSOR_ACCESS_TOKEN", "v1"],
|
||||
["CURSOR_TOKEN", "v1"],
|
||||
])("encrypted %s (%s)", (store, version) => {
|
||||
beforeEach(() => {
|
||||
const encrypted = `cursor-safe-storage:${version}:${Buffer.from("ciphertext").toString("base64")}`;
|
||||
if (store === "modern state.vscdb") {
|
||||
writeCursorStateDb(homeDir, { "cursorAuth/accessToken": encrypted });
|
||||
} else if (store === "legacy cursorAuthStatus") {
|
||||
writeCursorStateDb(homeDir, {
|
||||
cursorAuthStatus: Buffer.from(JSON.stringify({ accessToken: encrypted }), "utf8"),
|
||||
});
|
||||
} else if (store === "auth.json") {
|
||||
writeCursorAuthJson(homeDir, encrypted);
|
||||
} else {
|
||||
process.env[store] = encrypted;
|
||||
}
|
||||
});
|
||||
|
||||
it("discovers no account when the only credential is encrypted", async () => {
|
||||
expect(await discover()).toEqual([]);
|
||||
});
|
||||
|
||||
it("never sends an encrypted bearer token when fetching from a stored locator", async () => {
|
||||
let input: Parameters<typeof fetchUsage>[0];
|
||||
if (store === "modern state.vscdb" || store === "legacy cursorAuthStatus") {
|
||||
input = {
|
||||
store: "sqlite",
|
||||
locator: join(homeDir, ".config", "Cursor", "User", "globalStorage", "state.vscdb"),
|
||||
};
|
||||
} else if (store === "auth.json") {
|
||||
input = { store: "file", locator: join(homeDir, ".config", "cursor", "auth.json") };
|
||||
} else {
|
||||
input = { store: "env", locator: store };
|
||||
}
|
||||
const requests: RequestInit[] = [];
|
||||
fetchApi = async (_url, init) => {
|
||||
requests.push(init ?? {});
|
||||
return new Response(null, { status: 401 });
|
||||
};
|
||||
|
||||
await expect(fetchUsage(input, fetchApi)).rejects.toThrow(
|
||||
"Cursor login store no longer exists",
|
||||
);
|
||||
expect(requests).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it("discovers and uses auth.json after skipping an encrypted desktop token", async () => {
|
||||
writeCursorStateDb(homeDir, {
|
||||
"cursorAuth/accessToken": `cursor-safe-storage:v1:${Buffer.from("ciphertext").toString("base64")}`,
|
||||
});
|
||||
writeCursorAuthJson(homeDir, "cursor_cli_jwt");
|
||||
expect(await discover()).toEqual([
|
||||
{
|
||||
key: expect.stringMatching(/^store-[a-f0-9]{64}$/),
|
||||
input: { store: "file", locator: join(homeDir, ".config", "cursor", "auth.json") },
|
||||
},
|
||||
]);
|
||||
const authorizations: string[] = [];
|
||||
fetchApi = async (_url, init) => {
|
||||
authorizations.push(new Headers(init?.headers).get("Authorization") ?? "");
|
||||
return jsonResponse({
|
||||
planUsage: { totalSpend: "0", remaining: "100", limit: "100" },
|
||||
billingCycleStart: null,
|
||||
billingCycleEnd: null,
|
||||
});
|
||||
};
|
||||
|
||||
const cursor = findProvider(await service().listUsage(), "cursor");
|
||||
expect(authorizations).toEqual(["Bearer cursor_cli_jwt"]);
|
||||
expect(cursor.status).toBe("available");
|
||||
});
|
||||
|
||||
it("omits unreadable state.vscdb", async () => {
|
||||
const directory = join(homeDir, ".config", "Cursor", "User", "globalStorage");
|
||||
mkdirSync(directory, { recursive: true });
|
||||
|
||||
@@ -27,11 +27,9 @@ function toIsoStringOrNull(timestampMs: number): string | null {
|
||||
return Number.isFinite(date.getTime()) ? date.toISOString() : null;
|
||||
}
|
||||
|
||||
// Cursor desktop stores auth in VS Code's ItemTable (state.vscdb). The modern
|
||||
// `cursorAuth/accessToken` value is a plain JWT or encrypted at rest. Any
|
||||
// `cursor-safe-storage:` value (any version) is Electron safeStorage ciphertext
|
||||
// and is skipped. Older builds kept a JSON blob under `cursorAuthStatus`.
|
||||
// Read it with node:sqlite so we don't
|
||||
// Cursor desktop stores auth in VS Code's ItemTable (state.vscdb). Modern builds keep
|
||||
// the access token as a plain JWT string under `cursorAuth/accessToken`; older builds
|
||||
// kept a JSON blob under `cursorAuthStatus`. Read it with node:sqlite so we don't
|
||||
// depend on a `sqlite3` CLI, which isn't installed by default on Windows (or on many
|
||||
// Linux hosts) — a missing binary silently rendered Cursor usage unavailable.
|
||||
// Headless hosts (VPS, cursor-agent only) have no desktop db; their session lives in
|
||||
@@ -108,21 +106,15 @@ function readItemTableValue(db: CursorStateDatabase, key: string): string | null
|
||||
return null;
|
||||
}
|
||||
|
||||
function usableCursorToken(value: string | null | undefined): string | null {
|
||||
const token = value?.trim();
|
||||
// Electron safeStorage ciphertext cannot authenticate an API request.
|
||||
return token && !token.startsWith("cursor-safe-storage:") ? token : null;
|
||||
}
|
||||
|
||||
function cursorTokenFromDb(db: CursorStateDatabase): string | null {
|
||||
const modern = usableCursorToken(readItemTableValue(db, CURSOR_ACCESS_TOKEN_KEY));
|
||||
const modern = readItemTableValue(db, CURSOR_ACCESS_TOKEN_KEY)?.trim();
|
||||
if (modern) return modern;
|
||||
|
||||
const legacy = readItemTableValue(db, CURSOR_LEGACY_AUTH_KEY);
|
||||
if (legacy) {
|
||||
try {
|
||||
const parsed = CursorAuthStatusSchema.parse(JSON.parse(legacy));
|
||||
return usableCursorToken(parsed.accessToken);
|
||||
if (parsed.accessToken) return parsed.accessToken;
|
||||
} catch {
|
||||
// ignore a malformed legacy blob
|
||||
}
|
||||
@@ -157,7 +149,7 @@ async function readCursorTokenFromAuthJson(path: string): Promise<string | null>
|
||||
if (!existsSync(path)) return null;
|
||||
try {
|
||||
const parsed = CursorAuthStatusSchema.parse(JSON.parse(await readFile(path, "utf8")));
|
||||
return usableCursorToken(parsed.accessToken);
|
||||
return parsed.accessToken?.trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -238,7 +230,7 @@ export async function fetchUsage(
|
||||
}
|
||||
|
||||
async function readToken(input: UsageInput): Promise<string | undefined> {
|
||||
if (input.store === "env") return usableCursorToken(process.env[input.locator]) ?? undefined;
|
||||
if (input.store === "env") return process.env[input.locator];
|
||||
const token =
|
||||
input.store === "sqlite"
|
||||
? await readCursorTokenFromSqlite(input.locator)
|
||||
|
||||
Reference in New Issue
Block a user