Download apps/vscode/test/workspace-paths.test.ts from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 16.4 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/apps/vscode/test/workspace-paths.test.ts
- Command line
-
hf download hf://SaylorTwift/kimi-code/apps/vscode/test/workspace-paths.test.ts
-
curl -L -o workspace-paths.test.ts https://huggingface.co/SaylorTwift/kimi-code/resolve/main/apps/vscode/test/workspace-paths.test.ts
16.4 kB
| /** | |
| * Scenario: Webview file paths and the selected working directory. | |
| * Responsibilities: directory/search/open paths are scoped, normalized, and symlink-safe; | |
| * editor mentions use relative paths inside the working directory and absolute paths outside. | |
| * Wiring: real temporary local files plus the public handler/bridge surfaces; | |
| * VS Code host APIs are the only stubbed boundary. | |
| * Run: pnpm --filter kimi-code exec vitest run --config vitest.config.ts test/workspace-paths.test.ts | |
| */ | |
| import { mkdtemp, mkdir, readFile, rm, stat, symlink, writeFile } from "node:fs/promises"; | |
| import { join } from "node:path"; | |
| import { tmpdir } from "node:os"; | |
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | |
| import type { Event, Session } from "@moonshot-ai/kimi-code-sdk"; | |
| import type * as vscode from "vscode"; | |
| import { Methods } from "../shared/bridge"; | |
| import { BridgeHandler } from "../src/bridge-handler"; | |
| import { fileHandlers } from "../src/handlers/file.handler"; | |
| import type { HandlerContext } from "../src/handlers/types"; | |
| import { FileManager } from "../src/managers/file.manager"; | |
| import { SessionRuntime } from "../src/runtime/session-runtime"; | |
| import { areSameFsPath, isFsPathInsideOrEqual } from "../src/utils/fs-path"; | |
| import { relativeWorkspacePath } from "../src/utils/workspace-path"; | |
| const vscodeHost = vi.hoisted(() => { | |
| function normalizeUriPath(value: string): string { | |
| const segments: string[] = []; | |
| for (const segment of value.replaceAll("\\", "/").split("/")) { | |
| if (!segment || segment === ".") continue; | |
| if (segment === "..") segments.pop(); | |
| else segments.push(segment); | |
| } | |
| return `/${segments.join("/")}`; | |
| } | |
| class Uri { | |
| readonly query: string; | |
| constructor( | |
| readonly scheme: string, | |
| readonly authority: string, | |
| readonly path: string, | |
| readonly fsPath: string, | |
| query = "", | |
| ) { | |
| this.query = query; | |
| } | |
| static file(fsPath: string): Uri { | |
| return new Uri("file", "", normalizeUriPath(fsPath), fsPath); | |
| } | |
| static remote(authority: string, fsPath: string): Uri { | |
| return new Uri("vscode-remote", authority, normalizeUriPath(fsPath), fsPath); | |
| } | |
| static joinPath(base: Uri, ...segments: string[]): Uri { | |
| const path = normalizeUriPath(`${base.path}/${segments.join("/")}`); | |
| const suffix = segments.join("/"); | |
| const separator = base.fsPath.endsWith("/") ? "" : "/"; | |
| return new Uri(base.scheme, base.authority, path, `${base.fsPath}${separator}${suffix}`); | |
| } | |
| static from(parts: { scheme: string; path?: string; query?: string }): Uri { | |
| const path = parts.path ?? ""; | |
| return new Uri(parts.scheme, "", path, path, parts.query); | |
| } | |
| toString(): string { | |
| return `${this.scheme}://${this.authority}${this.path}${this.query ? `?${this.query}` : ""}`; | |
| } | |
| } | |
| class RelativePattern { | |
| readonly baseUri: Uri; | |
| readonly base: string; | |
| constructor(base: Uri, readonly pattern: string) { | |
| this.baseUri = base; | |
| this.base = base.fsPath; | |
| } | |
| } | |
| const readDirectory = vi.fn(); | |
| const stat = vi.fn(); | |
| const readFile = vi.fn(); | |
| const findFiles = vi.fn(); | |
| const executeCommand = vi.fn(); | |
| const showWarningMessage = vi.fn(); | |
| return { | |
| Uri, | |
| RelativePattern, | |
| readDirectory, | |
| stat, | |
| readFile, | |
| findFiles, | |
| executeCommand, | |
| showWarningMessage, | |
| workspaceFolders: [] as Array<{ uri: Uri }>, | |
| }; | |
| }); | |
| vi.mock("vscode", () => ({ | |
| Uri: vscodeHost.Uri, | |
| RelativePattern: vscodeHost.RelativePattern, | |
| FileType: { Unknown: 0, File: 1, Directory: 2, SymbolicLink: 64 }, | |
| QuickPickItemKind: { Separator: -1 }, | |
| workspace: { | |
| get workspaceFolders() { | |
| return vscodeHost.workspaceFolders; | |
| }, | |
| fs: { | |
| readDirectory: vscodeHost.readDirectory, | |
| stat: vscodeHost.stat, | |
| readFile: vscodeHost.readFile, | |
| }, | |
| findFiles: vscodeHost.findFiles, | |
| createFileSystemWatcher: () => ({ | |
| onDidChange: vi.fn(), | |
| onDidCreate: vi.fn(), | |
| onDidDelete: vi.fn(), | |
| dispose: vi.fn(), | |
| }), | |
| getConfiguration: () => ({ get: (_key: string, fallback: unknown) => fallback }), | |
| textDocuments: [], | |
| }, | |
| commands: { executeCommand: vscodeHost.executeCommand }, | |
| window: { | |
| activeTextEditor: undefined, | |
| showWarningMessage: vscodeHost.showWarningMessage, | |
| showQuickPick: vi.fn(), | |
| showOpenDialog: vi.fn(), | |
| }, | |
| })); | |
| vi.mock("@moonshot-ai/kimi-code-sdk", async (importOriginal) => { | |
| const original = await importOriginal<typeof import("@moonshot-ai/kimi-code-sdk")>(); | |
| return { | |
| ...original, | |
| createKimiHarness: () => ({ | |
| homeDir: "/tmp/kimi-code-test-home", | |
| close: vi.fn(), | |
| }), | |
| }; | |
| }); | |
| let root: string; | |
| let fileManager: FileManager; | |
| let bridges: BridgeHandler[]; | |
| let sessionRuntimes: SessionRuntime[]; | |
| let extraRoots: string[]; | |
| beforeEach(async () => { | |
| root = await mkdtemp(join(tmpdir(), "kimi-vscode-workspace-paths-")); | |
| vscodeHost.workspaceFolders.splice(0, vscodeHost.workspaceFolders.length, { uri: vscodeHost.Uri.file(root) }); | |
| vscodeHost.stat.mockImplementation((uri: { fsPath: string }) => stat(uri.fsPath)); | |
| vscodeHost.readFile.mockImplementation((uri: { fsPath: string }) => readFile(uri.fsPath)); | |
| vscodeHost.findFiles.mockResolvedValue([]); | |
| fileManager = new FileManager({} as never, vi.fn()); | |
| bridges = []; | |
| sessionRuntimes = []; | |
| extraRoots = []; | |
| }); | |
| afterEach(async () => { | |
| await Promise.all(sessionRuntimes.map((runtime) => runtime.close())); | |
| await Promise.all(bridges.map((bridge) => bridge.dispose())); | |
| fileManager.dispose(); | |
| vi.clearAllMocks(); | |
| await Promise.all([root, ...extraRoots].map((directory) => rm(directory, { recursive: true, force: true }))); | |
| }); | |
| describe("Webview workspace paths (selected-directory containment)", () => { | |
| it("searches within the selected subdirectory using work-directory-relative results", async () => { | |
| const workDir = join(root, "project", "subproject"); | |
| const inside = join(workDir, "src", "inside.ts"); | |
| const sibling = join(root, "project", "sibling.ts"); | |
| await mkdir(join(workDir, "src"), { recursive: true }); | |
| await Promise.all([writeFile(inside, "inside"), writeFile(sibling, "sibling")]); | |
| vscodeHost.findFiles.mockResolvedValue([vscodeHost.Uri.file(inside), vscodeHost.Uri.file(sibling)]); | |
| const files = await getProjectFiles(workDir, { query: "inside" }); | |
| const include = vscodeHost.findFiles.mock.calls[0]?.[0] as InstanceType<typeof vscodeHost.RelativePattern>; | |
| expect(include.baseUri.fsPath).toBe(workDir); | |
| expect(files).toEqual([{ path: "src/inside.ts", name: "inside.ts", isDirectory: false }]); | |
| }); | |
| it("maps engine suggestions without touching the local file search", async () => { | |
| const workDir = join(root, "project"); | |
| await mkdir(workDir); | |
| const ctx = createContext(vscodeHost.Uri.file(workDir)); | |
| const suggestFiles = vi.mocked(ctx.harness.suggestFiles); | |
| suggestFiles.mockResolvedValue({ | |
| items: [ | |
| { path: "src/app.ts", name: "app.ts", kind: "file", matchPositions: [4, 5, 6] }, | |
| { path: "src", name: "src", kind: "directory", matchPositions: [] }, | |
| ], | |
| truncated: false, | |
| }); | |
| const files = await fileHandlers[Methods.GetProjectFiles]!({ query: "app" }, ctx); | |
| expect(suggestFiles).toHaveBeenCalledWith(workDir, { query: "app", limit: 20 }); | |
| expect(vscodeHost.findFiles).not.toHaveBeenCalled(); | |
| expect(files).toEqual([ | |
| { path: "src/app.ts", name: "app.ts", isDirectory: false, matchPositions: [4, 5, 6] }, | |
| { path: "src", name: "src", isDirectory: true, matchPositions: [] }, | |
| ]); | |
| }); | |
| it("degrades to an empty list when engine suggestions fail", async () => { | |
| const workDir = join(root, "project"); | |
| await mkdir(workDir); | |
| const ctx = createContext(vscodeHost.Uri.file(workDir)); | |
| vi.mocked(ctx.harness.suggestFiles).mockRejectedValue(new Error("engine down")); | |
| const files = await fileHandlers[Methods.GetProjectFiles]!({ query: "app" }, ctx); | |
| expect(files).toEqual([]); | |
| expect(ctx.logError).toHaveBeenCalled(); | |
| expect(vscodeHost.findFiles).not.toHaveBeenCalled(); | |
| }); | |
| it("refuses to open a symlink whose target lies outside the selected working directory", async () => { | |
| const workDir = join(root, "project"); | |
| const outside = join(root, "outside.txt"); | |
| await mkdir(workDir); | |
| await writeFile(outside, "secret"); | |
| await symlink(outside, join(workDir, "secret.txt")); | |
| const ctx = createContext(vscodeHost.Uri.file(workDir)); | |
| const result = await fileHandlers[Methods.OpenFile]!({ filePath: "secret.txt" }, ctx); | |
| expect(result).toEqual({ ok: false }); | |
| expect(vscodeHost.executeCommand).not.toHaveBeenCalled(); | |
| }); | |
| it("omits an outside symlink when an SDK Write event requests baseline capture", async () => { | |
| const workDir = join(root, "project"); | |
| const outsideRoot = await mkdtemp(join(tmpdir(), "kimi-vscode-baseline-outside-")); | |
| extraRoots.push(outsideRoot); | |
| const outside = join(outsideRoot, "outside.txt"); | |
| const linkedFile = join(workDir, "linked.txt"); | |
| await mkdir(workDir); | |
| await writeFile(outside, "secret"); | |
| await symlink(outside, linkedFile); | |
| const bridge = createBridge(); | |
| let emit!: (event: Event) => void; | |
| const session = { | |
| id: "session-1", | |
| workDir, | |
| summary: { id: "session-1", workDir }, | |
| setApprovalHandler: vi.fn(), | |
| setQuestionHandler: vi.fn(), | |
| onEvent(listener: (event: Event) => void) { | |
| emit = listener; | |
| return vi.fn(); | |
| }, | |
| close: vi.fn(), | |
| } as unknown as Session; | |
| const runtime = new SessionRuntime({ | |
| session, | |
| legacyApproval: { yolo: false, afk: false }, | |
| broadcast: vi.fn(), | |
| captureBaseline: (summary, filePath, webviewIds) => { | |
| bridge.captureFileBaseline(summary, filePath, webviewIds); | |
| }, | |
| log: vi.fn(), | |
| }); | |
| sessionRuntimes.push(runtime); | |
| emit({ | |
| type: "tool.call.started", | |
| sessionId: "session-1", | |
| agentId: "main", | |
| turnId: 1, | |
| toolCallId: "tool-1", | |
| name: "Write", | |
| args: { path: linkedFile }, | |
| }); | |
| await expect(bridge.baselineManager.getChanges({ id: "session-1", workDir })).resolves.toEqual([]); | |
| }); | |
| it("builds an editor mention relative to the selected working directory", async () => { | |
| const workDir = join(root, "project", "subproject"); | |
| const inside = join(workDir, "src", "inside.ts"); | |
| await mkdir(join(workDir, "src"), { recursive: true }); | |
| await writeFile(inside, "inside"); | |
| const bridge = createBridge(); | |
| await bridge.handle({ id: "set", method: Methods.SetWorkDir, params: { workDir } }, "view-1"); | |
| const mention = await bridge.getEditorMention( | |
| "view-1", | |
| vscodeHost.Uri.file(inside) as vscode.Uri, | |
| emptySelection(), | |
| ); | |
| expect(mention).toBe("@src/inside.ts"); | |
| }); | |
| it("builds an absolute editor mention when the file is outside the selected working directory", async () => { | |
| const workDir = join(root, "project", "subproject"); | |
| const sibling = join(root, "project", "sibling.ts"); | |
| await mkdir(workDir, { recursive: true }); | |
| await writeFile(sibling, "sibling"); | |
| const bridge = createBridge(); | |
| await bridge.handle({ id: "set", method: Methods.SetWorkDir, params: { workDir } }, "view-1"); | |
| const mention = await bridge.getEditorMention( | |
| "view-1", | |
| vscodeHost.Uri.file(sibling) as vscode.Uri, | |
| emptySelection(), | |
| ); | |
| expect(mention).toBe(`@${sibling}`); | |
| }); | |
| it("builds an absolute editor mention when the file is outside the workspace root", async () => { | |
| const otherRoot = await mkdtemp(join(tmpdir(), "kimi-vscode-mention-outside-")); | |
| extraRoots.push(otherRoot); | |
| const outside = join(otherRoot, "App.java"); | |
| await writeFile(outside, "class App {}"); | |
| const bridge = createBridge(); | |
| const mention = await bridge.getEditorMention( | |
| "view-1", | |
| vscodeHost.Uri.file(outside) as vscode.Uri, | |
| emptySelection(), | |
| ); | |
| expect(mention).toBe(`@${outside}`); | |
| }); | |
| it("quotes an absolute editor mention whose path contains spaces", async () => { | |
| const otherRoot = await mkdtemp(join(tmpdir(), "kimi vscode mention space-")); | |
| extraRoots.push(otherRoot); | |
| const outside = join(otherRoot, "App.java"); | |
| await writeFile(outside, "class App {}"); | |
| const bridge = createBridge(); | |
| const mention = await bridge.getEditorMention( | |
| "view-1", | |
| vscodeHost.Uri.file(outside) as vscode.Uri, | |
| emptySelection(), | |
| ); | |
| expect(mention).toBe(`@"${outside}"`); | |
| }); | |
| it("places the line range after the closing quote of a mention with spaces", async () => { | |
| const workDir = join(root, "project"); | |
| const inside = join(workDir, "some dir", "inside.ts"); | |
| await mkdir(join(workDir, "some dir"), { recursive: true }); | |
| await writeFile(inside, "inside"); | |
| const bridge = createBridge(); | |
| await bridge.handle({ id: "set", method: Methods.SetWorkDir, params: { workDir } }, "view-1"); | |
| const mention = await bridge.getEditorMention( | |
| "view-1", | |
| vscodeHost.Uri.file(inside) as vscode.Uri, | |
| { isEmpty: false, start: { line: 2 }, end: { line: 4 } } as vscode.Selection, | |
| ); | |
| expect(mention).toBe('@"some dir/inside.ts":3-5'); | |
| }); | |
| it("omits an editor mention for a document that is not on the workspace file system", async () => { | |
| const bridge = createBridge(); | |
| const untitled = vscodeHost.Uri.from({ scheme: "untitled", path: "Untitled-1" }); | |
| const mention = await bridge.getEditorMention( | |
| "view-1", | |
| untitled as vscode.Uri, | |
| emptySelection(), | |
| ); | |
| expect(mention).toBeNull(); | |
| }); | |
| it("rejects a selected working directory whose symlink target leaves the workspace", async () => { | |
| const outside = await mkdtemp(join(tmpdir(), "kimi-vscode-outside-")); | |
| extraRoots.push(outside); | |
| const linkedWorkDir = join(root, "linked-project"); | |
| await symlink(outside, linkedWorkDir); | |
| const bridge = createBridge(); | |
| const result = await bridge.handle( | |
| { id: "set", method: Methods.SetWorkDir, params: { workDir: linkedWorkDir } }, | |
| "view-1", | |
| ); | |
| expect(result).toEqual({ id: "set", result: { ok: false } }); | |
| }); | |
| }); | |
| describe("native workspace path comparison (Windows drive and UNC semantics)", () => { | |
| it("treats slash and casing differences as the same Windows drive path", () => { | |
| expect(areSameFsPath("C:\\Users\\Example User\\项目", "c:/users/example user/项目")).toBe(true); | |
| }); | |
| it("keeps an in-share UNC child relative to its workspace", () => { | |
| const rootUri = vscodeHost.Uri.file("\\\\Server\\Share\\Workspace"); | |
| const childUri = vscodeHost.Uri.file("\\\\server\\share\\workspace\\src\\app.ts"); | |
| expect(relativeWorkspacePath(rootUri as vscode.Uri, childUri as vscode.Uri)).toBe("src/app.ts"); | |
| }); | |
| it("rejects a UNC path from another share", () => { | |
| expect( | |
| isFsPathInsideOrEqual( | |
| "\\\\Server\\Share\\Workspace", | |
| "\\\\Server\\OtherShare\\Workspace\\app.ts", | |
| ), | |
| ).toBe(false); | |
| }); | |
| }); | |
| async function getProjectFiles(workDir: string, params: { query?: string }) { | |
| return fileHandlers[Methods.GetProjectFiles]!(params, createContext(vscodeHost.Uri.file(workDir))); | |
| } | |
| function createContext(workDirUri: InstanceType<typeof vscodeHost.Uri>): HandlerContext { | |
| return { | |
| webviewId: "view-1", | |
| workDir: workDirUri.fsPath, | |
| workDirUri: workDirUri as vscode.Uri, | |
| workspaceRoot: root, | |
| workspaceRootUri: vscodeHost.workspaceFolders[0]!.uri as vscode.Uri, | |
| workspaceState: {} as vscode.Memento, | |
| requireWorkDir: () => workDirUri.fsPath, | |
| requireWorkDirUri: () => workDirUri as vscode.Uri, | |
| fileManager, | |
| harness: { suggestFiles: vi.fn(async () => undefined) }, | |
| logError: vi.fn(), | |
| } as unknown as HandlerContext; | |
| } | |
| function createBridge(): BridgeHandler { | |
| const bridge = new BridgeHandler( | |
| vi.fn(), | |
| { get: vi.fn(), update: vi.fn() } as unknown as vscode.Memento, | |
| join(root, "global-storage"), | |
| vi.fn(), | |
| vi.fn(), | |
| vi.fn(), | |
| ); | |
| bridges.push(bridge); | |
| return bridge; | |
| } | |
| function emptySelection(): vscode.Selection { | |
| return { | |
| isEmpty: true, | |
| start: { line: 0 }, | |
| end: { line: 0 }, | |
| } as vscode.Selection; | |
| } | |