mirror of
https://github.com/harivansh-afk/clanker-agent.git
synced 2026-04-15 15:03:34 +00:00
Complete the remaining pi-to-companion rename across companion-os, web, vm-orchestrator, docker, and archived fixtures. Verification: - semantic rg sweeps for Pi/piConfig/getPi/.pi runtime references - npm run check in apps/companion-os (fails in this worktree: biome not found) Co-authored-by: Codex <noreply@openai.com>
521 lines
15 KiB
TypeScript
521 lines
15 KiB
TypeScript
import assert from "node:assert";
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { afterEach, beforeEach, describe, it, test } from "node:test";
|
|
import { CombinedAutocompleteProvider } from "../src/autocomplete.js";
|
|
|
|
const resolveFdPath = (): string | null => {
|
|
const command = process.platform === "win32" ? "where" : "which";
|
|
const result = spawnSync(command, ["fd"], { encoding: "utf-8" });
|
|
if (result.status !== 0 || !result.stdout) {
|
|
return null;
|
|
}
|
|
|
|
const firstLine = result.stdout.split(/\r?\n/).find(Boolean);
|
|
return firstLine ? firstLine.trim() : null;
|
|
};
|
|
|
|
type FolderStructure = {
|
|
dirs?: string[];
|
|
files?: Record<string, string>;
|
|
};
|
|
|
|
const setupFolder = (
|
|
baseDir: string,
|
|
structure: FolderStructure = {},
|
|
): void => {
|
|
const dirs = structure.dirs ?? [];
|
|
const files = structure.files ?? {};
|
|
|
|
dirs.forEach((dir) => {
|
|
mkdirSync(join(baseDir, dir), { recursive: true });
|
|
});
|
|
Object.entries(files).forEach(([filePath, contents]) => {
|
|
const fullPath = join(baseDir, filePath);
|
|
mkdirSync(dirname(fullPath), { recursive: true });
|
|
writeFileSync(fullPath, contents);
|
|
});
|
|
};
|
|
|
|
const fdPath = resolveFdPath();
|
|
const isFdInstalled = Boolean(fdPath);
|
|
|
|
const requireFdPath = (): string => {
|
|
if (!fdPath) {
|
|
throw new Error("fd is not available");
|
|
}
|
|
return fdPath;
|
|
};
|
|
|
|
describe("CombinedAutocompleteProvider", () => {
|
|
describe("extractPathPrefix", () => {
|
|
it("extracts / from 'hey /' when forced", () => {
|
|
const provider = new CombinedAutocompleteProvider([], "/tmp");
|
|
const lines = ["hey /"];
|
|
const cursorLine = 0;
|
|
const cursorCol = 5; // After the "/"
|
|
|
|
const result = provider.getForceFileSuggestions(
|
|
lines,
|
|
cursorLine,
|
|
cursorCol,
|
|
);
|
|
|
|
assert.notEqual(
|
|
result,
|
|
null,
|
|
"Should return suggestions for root directory",
|
|
);
|
|
if (result) {
|
|
assert.strictEqual(result.prefix, "/", "Prefix should be '/'");
|
|
}
|
|
});
|
|
|
|
it("extracts /A from '/A' when forced", () => {
|
|
const provider = new CombinedAutocompleteProvider([], "/tmp");
|
|
const lines = ["/A"];
|
|
const cursorLine = 0;
|
|
const cursorCol = 2; // After the "A"
|
|
|
|
const result = provider.getForceFileSuggestions(
|
|
lines,
|
|
cursorLine,
|
|
cursorCol,
|
|
);
|
|
|
|
console.log("Result:", result);
|
|
// This might return null if /A doesn't match anything, which is fine
|
|
// We're mainly testing that the prefix extraction works
|
|
if (result) {
|
|
assert.strictEqual(result.prefix, "/A", "Prefix should be '/A'");
|
|
}
|
|
});
|
|
|
|
it("does not trigger for slash commands", () => {
|
|
const provider = new CombinedAutocompleteProvider([], "/tmp");
|
|
const lines = ["/model"];
|
|
const cursorLine = 0;
|
|
const cursorCol = 6; // After "model"
|
|
|
|
const result = provider.getForceFileSuggestions(
|
|
lines,
|
|
cursorLine,
|
|
cursorCol,
|
|
);
|
|
|
|
console.log("Result:", result);
|
|
assert.strictEqual(result, null, "Should not trigger for slash commands");
|
|
});
|
|
|
|
it("triggers for absolute paths after slash command argument", () => {
|
|
const provider = new CombinedAutocompleteProvider([], "/tmp");
|
|
const lines = ["/command /"];
|
|
const cursorLine = 0;
|
|
const cursorCol = 10; // After the second "/"
|
|
|
|
const result = provider.getForceFileSuggestions(
|
|
lines,
|
|
cursorLine,
|
|
cursorCol,
|
|
);
|
|
|
|
console.log("Result:", result);
|
|
assert.notEqual(
|
|
result,
|
|
null,
|
|
"Should trigger for absolute paths in command arguments",
|
|
);
|
|
if (result) {
|
|
assert.strictEqual(result.prefix, "/", "Prefix should be '/'");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("fd @ file suggestions", { skip: !isFdInstalled }, () => {
|
|
let rootDir = "";
|
|
let baseDir = "";
|
|
let outsideDir = "";
|
|
|
|
beforeEach(() => {
|
|
rootDir = mkdtempSync(join(tmpdir(), "companion-autocomplete-root-"));
|
|
baseDir = join(rootDir, "cwd");
|
|
outsideDir = join(rootDir, "outside");
|
|
mkdirSync(baseDir, { recursive: true });
|
|
mkdirSync(outsideDir, { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(rootDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test("returns all files and folders for empty @ query", () => {
|
|
setupFolder(baseDir, {
|
|
dirs: ["src"],
|
|
files: {
|
|
"README.md": "readme",
|
|
},
|
|
});
|
|
|
|
const provider = new CombinedAutocompleteProvider(
|
|
[],
|
|
baseDir,
|
|
requireFdPath(),
|
|
);
|
|
const line = "@";
|
|
const result = provider.getSuggestions([line], 0, line.length);
|
|
|
|
const values = result?.items.map((item) => item.value).sort();
|
|
assert.deepStrictEqual(values, ["@README.md", "@src/"].sort());
|
|
});
|
|
|
|
test("matches file with extension in query", () => {
|
|
setupFolder(baseDir, {
|
|
files: {
|
|
"file.txt": "content",
|
|
},
|
|
});
|
|
|
|
const provider = new CombinedAutocompleteProvider(
|
|
[],
|
|
baseDir,
|
|
requireFdPath(),
|
|
);
|
|
const line = "@file.txt";
|
|
const result = provider.getSuggestions([line], 0, line.length);
|
|
|
|
const values = result?.items.map((item) => item.value);
|
|
assert.ok(values?.includes("@file.txt"));
|
|
});
|
|
|
|
test("filters are case insensitive", () => {
|
|
setupFolder(baseDir, {
|
|
dirs: ["src"],
|
|
files: {
|
|
"README.md": "readme",
|
|
},
|
|
});
|
|
|
|
const provider = new CombinedAutocompleteProvider(
|
|
[],
|
|
baseDir,
|
|
requireFdPath(),
|
|
);
|
|
const line = "@re";
|
|
const result = provider.getSuggestions([line], 0, line.length);
|
|
|
|
const values = result?.items.map((item) => item.value).sort();
|
|
assert.deepStrictEqual(values, ["@README.md"]);
|
|
});
|
|
|
|
test("ranks directories before files", () => {
|
|
setupFolder(baseDir, {
|
|
dirs: ["src"],
|
|
files: {
|
|
"src.txt": "text",
|
|
},
|
|
});
|
|
|
|
const provider = new CombinedAutocompleteProvider(
|
|
[],
|
|
baseDir,
|
|
requireFdPath(),
|
|
);
|
|
const line = "@src";
|
|
const result = provider.getSuggestions([line], 0, line.length);
|
|
|
|
const firstValue = result?.items[0]?.value;
|
|
const hasSrcFile = result?.items?.some(
|
|
(item) => item.value === "@src.txt",
|
|
);
|
|
assert.strictEqual(firstValue, "@src/");
|
|
assert.ok(hasSrcFile);
|
|
});
|
|
|
|
test("returns nested file paths", () => {
|
|
setupFolder(baseDir, {
|
|
files: {
|
|
"src/index.ts": "export {};\n",
|
|
},
|
|
});
|
|
|
|
const provider = new CombinedAutocompleteProvider(
|
|
[],
|
|
baseDir,
|
|
requireFdPath(),
|
|
);
|
|
const line = "@index";
|
|
const result = provider.getSuggestions([line], 0, line.length);
|
|
|
|
const values = result?.items.map((item) => item.value);
|
|
assert.ok(values?.includes("@src/index.ts"));
|
|
});
|
|
|
|
test("matches deeply nested paths", () => {
|
|
setupFolder(baseDir, {
|
|
files: {
|
|
"packages/tui/src/autocomplete.ts": "export {};",
|
|
"packages/ai/src/autocomplete.ts": "export {};",
|
|
},
|
|
});
|
|
|
|
const provider = new CombinedAutocompleteProvider(
|
|
[],
|
|
baseDir,
|
|
requireFdPath(),
|
|
);
|
|
const line = "@tui/src/auto";
|
|
const result = provider.getSuggestions([line], 0, line.length);
|
|
|
|
const values = result?.items.map((item) => item.value);
|
|
assert.ok(values?.includes("@packages/tui/src/autocomplete.ts"));
|
|
assert.ok(!values?.includes("@packages/ai/src/autocomplete.ts"));
|
|
});
|
|
|
|
test("matches directory in middle of path with --full-path", () => {
|
|
setupFolder(baseDir, {
|
|
files: {
|
|
"src/components/Button.tsx": "export {};",
|
|
"src/utils/helpers.ts": "export {};",
|
|
},
|
|
});
|
|
|
|
const provider = new CombinedAutocompleteProvider(
|
|
[],
|
|
baseDir,
|
|
requireFdPath(),
|
|
);
|
|
const line = "@components/";
|
|
const result = provider.getSuggestions([line], 0, line.length);
|
|
|
|
const values = result?.items.map((item) => item.value);
|
|
assert.ok(values?.includes("@src/components/Button.tsx"));
|
|
assert.ok(!values?.includes("@src/utils/helpers.ts"));
|
|
});
|
|
|
|
test("scopes fuzzy search to relative directories and searches recursively", () => {
|
|
setupFolder(outsideDir, {
|
|
files: {
|
|
"nested/alpha.ts": "export {};",
|
|
"nested/deeper/also-alpha.ts": "export {};",
|
|
"nested/deeper/zzz.ts": "export {};",
|
|
},
|
|
});
|
|
|
|
const provider = new CombinedAutocompleteProvider(
|
|
[],
|
|
baseDir,
|
|
requireFdPath(),
|
|
);
|
|
const line = "@../outside/a";
|
|
const result = provider.getSuggestions([line], 0, line.length);
|
|
|
|
const values = result?.items.map((item) => item.value);
|
|
assert.ok(values?.includes("@../outside/nested/alpha.ts"));
|
|
assert.ok(values?.includes("@../outside/nested/deeper/also-alpha.ts"));
|
|
assert.ok(!values?.includes("@../outside/nested/deeper/zzz.ts"));
|
|
});
|
|
|
|
test("quotes paths with spaces for @ suggestions", () => {
|
|
setupFolder(baseDir, {
|
|
dirs: ["my folder"],
|
|
files: {
|
|
"my folder/test.txt": "content",
|
|
},
|
|
});
|
|
|
|
const provider = new CombinedAutocompleteProvider(
|
|
[],
|
|
baseDir,
|
|
requireFdPath(),
|
|
);
|
|
const line = "@my";
|
|
const result = provider.getSuggestions([line], 0, line.length);
|
|
|
|
const values = result?.items.map((item) => item.value);
|
|
assert.ok(values?.includes('@"my folder/"'));
|
|
});
|
|
|
|
test("includes hidden paths but excludes .git", () => {
|
|
setupFolder(baseDir, {
|
|
dirs: [".companion", ".github", ".git"],
|
|
files: {
|
|
".companion/config.json": "{}",
|
|
".github/workflows/ci.yml": "name: ci",
|
|
".git/config": "[core]",
|
|
},
|
|
});
|
|
|
|
const provider = new CombinedAutocompleteProvider(
|
|
[],
|
|
baseDir,
|
|
requireFdPath(),
|
|
);
|
|
const line = "@";
|
|
const result = provider.getSuggestions([line], 0, line.length);
|
|
|
|
const values = result?.items.map((item) => item.value) ?? [];
|
|
assert.ok(values.includes("@.companion/"));
|
|
assert.ok(values.includes("@.github/"));
|
|
assert.ok(
|
|
!values.some(
|
|
(value) => value === "@.git" || value.startsWith("@.git/"),
|
|
),
|
|
);
|
|
});
|
|
|
|
test("continues autocomplete inside quoted @ paths", () => {
|
|
setupFolder(baseDir, {
|
|
files: {
|
|
"my folder/test.txt": "content",
|
|
"my folder/other.txt": "content",
|
|
},
|
|
});
|
|
|
|
const provider = new CombinedAutocompleteProvider(
|
|
[],
|
|
baseDir,
|
|
requireFdPath(),
|
|
);
|
|
const line = '@"my folder/"';
|
|
const result = provider.getSuggestions([line], 0, line.length - 1);
|
|
|
|
assert.notEqual(
|
|
result,
|
|
null,
|
|
"Should return suggestions for quoted folder path",
|
|
);
|
|
const values = result?.items.map((item) => item.value);
|
|
assert.ok(values?.includes('@"my folder/test.txt"'));
|
|
assert.ok(values?.includes('@"my folder/other.txt"'));
|
|
});
|
|
|
|
test("applies quoted @ completion without duplicating closing quote", () => {
|
|
setupFolder(baseDir, {
|
|
files: {
|
|
"my folder/test.txt": "content",
|
|
},
|
|
});
|
|
|
|
const provider = new CombinedAutocompleteProvider(
|
|
[],
|
|
baseDir,
|
|
requireFdPath(),
|
|
);
|
|
const line = '@"my folder/te"';
|
|
const cursorCol = line.length - 1;
|
|
const result = provider.getSuggestions([line], 0, cursorCol);
|
|
|
|
assert.notEqual(
|
|
result,
|
|
null,
|
|
"Should return suggestions for quoted @ path",
|
|
);
|
|
const item = result?.items.find(
|
|
(entry) => entry.value === '@"my folder/test.txt"',
|
|
);
|
|
assert.ok(item, "Should find test.txt suggestion");
|
|
|
|
const applied = provider.applyCompletion(
|
|
[line],
|
|
0,
|
|
cursorCol,
|
|
item!,
|
|
result!.prefix,
|
|
);
|
|
assert.strictEqual(applied.lines[0], '@"my folder/test.txt" ');
|
|
});
|
|
});
|
|
|
|
describe("quoted path completion", () => {
|
|
let baseDir = "";
|
|
|
|
beforeEach(() => {
|
|
baseDir = mkdtempSync(join(tmpdir(), "companion-autocomplete-"));
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(baseDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test("quotes paths with spaces for direct completion", () => {
|
|
setupFolder(baseDir, {
|
|
dirs: ["my folder"],
|
|
files: {
|
|
"my folder/test.txt": "content",
|
|
},
|
|
});
|
|
|
|
const provider = new CombinedAutocompleteProvider([], baseDir);
|
|
const line = "my";
|
|
const result = provider.getForceFileSuggestions([line], 0, line.length);
|
|
|
|
assert.notEqual(
|
|
result,
|
|
null,
|
|
"Should return suggestions for path completion",
|
|
);
|
|
const values = result?.items.map((item) => item.value);
|
|
assert.ok(values?.includes('"my folder/"'));
|
|
});
|
|
|
|
test("continues completion inside quoted paths", () => {
|
|
setupFolder(baseDir, {
|
|
files: {
|
|
"my folder/test.txt": "content",
|
|
"my folder/other.txt": "content",
|
|
},
|
|
});
|
|
|
|
const provider = new CombinedAutocompleteProvider([], baseDir);
|
|
const line = '"my folder/"';
|
|
const result = provider.getForceFileSuggestions(
|
|
[line],
|
|
0,
|
|
line.length - 1,
|
|
);
|
|
|
|
assert.notEqual(
|
|
result,
|
|
null,
|
|
"Should return suggestions for quoted folder path",
|
|
);
|
|
const values = result?.items.map((item) => item.value);
|
|
assert.ok(values?.includes('"my folder/test.txt"'));
|
|
assert.ok(values?.includes('"my folder/other.txt"'));
|
|
});
|
|
|
|
test("applies quoted completion without duplicating closing quote", () => {
|
|
setupFolder(baseDir, {
|
|
files: {
|
|
"my folder/test.txt": "content",
|
|
},
|
|
});
|
|
|
|
const provider = new CombinedAutocompleteProvider([], baseDir);
|
|
const line = '"my folder/te"';
|
|
const cursorCol = line.length - 1;
|
|
const result = provider.getForceFileSuggestions([line], 0, cursorCol);
|
|
|
|
assert.notEqual(
|
|
result,
|
|
null,
|
|
"Should return suggestions for quoted path",
|
|
);
|
|
const item = result?.items.find(
|
|
(entry) => entry.value === '"my folder/test.txt"',
|
|
);
|
|
assert.ok(item, "Should find test.txt suggestion");
|
|
|
|
const applied = provider.applyCompletion(
|
|
[line],
|
|
0,
|
|
cursorCol,
|
|
item!,
|
|
result!.prefix,
|
|
);
|
|
assert.strictEqual(applied.lines[0], '"my folder/test.txt"');
|
|
});
|
|
});
|
|
});
|