@sean.holung/minicode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +241 -0
  3. package/dist/src/agent/agent.js +209 -0
  4. package/dist/src/agent/config.js +151 -0
  5. package/dist/src/agent/types.js +1 -0
  6. package/dist/src/index.js +138 -0
  7. package/dist/src/indexer/cache.js +121 -0
  8. package/dist/src/indexer/code-map.js +92 -0
  9. package/dist/src/indexer/plugin-loader.js +78 -0
  10. package/dist/src/indexer/plugins/typescript.js +327 -0
  11. package/dist/src/indexer/project-index.js +145 -0
  12. package/dist/src/indexer/types.js +1 -0
  13. package/dist/src/model/client.js +374 -0
  14. package/dist/src/prompt/system-prompt.js +91 -0
  15. package/dist/src/safety/guardrails.js +55 -0
  16. package/dist/src/session/session.js +95 -0
  17. package/dist/src/tools/edit-file.js +73 -0
  18. package/dist/src/tools/find-references.js +52 -0
  19. package/dist/src/tools/get-dependencies.js +56 -0
  20. package/dist/src/tools/helpers.js +42 -0
  21. package/dist/src/tools/list-files.js +63 -0
  22. package/dist/src/tools/read-file.js +79 -0
  23. package/dist/src/tools/read-symbol.js +96 -0
  24. package/dist/src/tools/registry.js +68 -0
  25. package/dist/src/tools/run-command.js +92 -0
  26. package/dist/src/tools/search-code-map.js +72 -0
  27. package/dist/src/tools/search.js +153 -0
  28. package/dist/src/tools/write-file.js +44 -0
  29. package/dist/src/ui/app.js +31 -0
  30. package/dist/src/ui/cli-ink.js +168 -0
  31. package/dist/src/ui/components/activity-pane.js +35 -0
  32. package/dist/src/ui/components/header-bar.js +6 -0
  33. package/dist/src/ui/components/input-composer.js +46 -0
  34. package/dist/src/ui/components/tool-timeline-item.js +37 -0
  35. package/dist/src/ui/events.js +1 -0
  36. package/dist/src/ui/state/ui-store.js +89 -0
  37. package/dist/src/ui/theme.js +23 -0
  38. package/dist/tests/agent.test.js +130 -0
  39. package/dist/tests/cache.test.js +37 -0
  40. package/dist/tests/config.test.js +37 -0
  41. package/dist/tests/dependency-graph.test.js +27 -0
  42. package/dist/tests/file-tools.test.js +73 -0
  43. package/dist/tests/find-references.test.js +30 -0
  44. package/dist/tests/get-dependencies.test.js +35 -0
  45. package/dist/tests/guardrails.test.js +18 -0
  46. package/dist/tests/indexer.test.js +201 -0
  47. package/dist/tests/model-client-openai.test.js +84 -0
  48. package/dist/tests/read-symbol.test.js +83 -0
  49. package/dist/tests/search-code-map.test.js +30 -0
  50. package/dist/tests/session.test.js +37 -0
  51. package/dist/tests/system-prompt.test.js +82 -0
  52. package/dist/tests/test-utils.js +18 -0
  53. package/dist/tests/tool-registry.test.js +41 -0
  54. package/package.json +43 -0
@@ -0,0 +1,130 @@
1
+ import assert from "node:assert/strict";
2
+ import path from "node:path";
3
+ import { test } from "node:test";
4
+ import { CodingAgent } from "../src/agent/agent.js";
5
+ import { buildProjectIndex } from "../src/indexer/project-index.js";
6
+ import { ToolRegistry } from "../src/tools/registry.js";
7
+ import { createTestAgentConfig } from "./test-utils.js";
8
+ class SequenceModelClient {
9
+ responses;
10
+ constructor(responses) {
11
+ this.responses = [...responses];
12
+ }
13
+ async chat(params) {
14
+ void params;
15
+ const next = this.responses.shift();
16
+ if (!next) {
17
+ throw new Error("No queued model response.");
18
+ }
19
+ return next;
20
+ }
21
+ }
22
+ class RepeatingModelClient {
23
+ async chat(params) {
24
+ void params;
25
+ return {
26
+ text: "running tool",
27
+ toolCalls: [{ id: "tool-1", name: "echo_tool", input: { value: "same" } }],
28
+ stopReason: "tool_use",
29
+ usage: { inputTokens: 1, outputTokens: 1 },
30
+ };
31
+ }
32
+ }
33
+ function createEchoTool() {
34
+ return {
35
+ name: "echo_tool",
36
+ description: "Echoes a value",
37
+ inputSchema: {
38
+ type: "object",
39
+ properties: {
40
+ value: { type: "string" },
41
+ },
42
+ required: ["value"],
43
+ },
44
+ execute: async (input) => `echo:${String(input.value)}`,
45
+ };
46
+ }
47
+ test("agent executes tool calls and returns final assistant text", async () => {
48
+ const responses = [
49
+ {
50
+ text: "I will use a tool first.",
51
+ toolCalls: [{ id: "tool-1", name: "echo_tool", input: { value: "ok" } }],
52
+ stopReason: "tool_use",
53
+ usage: { inputTokens: 10, outputTokens: 8 },
54
+ },
55
+ {
56
+ text: "Done. Change applied.",
57
+ toolCalls: [],
58
+ stopReason: "end_turn",
59
+ usage: { inputTokens: 12, outputTokens: 6 },
60
+ },
61
+ ];
62
+ const agent = new CodingAgent({
63
+ config: createTestAgentConfig("/tmp"),
64
+ modelClient: new SequenceModelClient(responses),
65
+ toolRegistry: new ToolRegistry([createEchoTool()]),
66
+ });
67
+ const { text } = await agent.runTurn("Make a change");
68
+ assert.equal(text, "Done. Change applied.");
69
+ const messages = agent.getSession().getMessages();
70
+ assert.equal(messages.length, 4);
71
+ assert.equal(messages[0]?.role, "user");
72
+ assert.equal(messages[1]?.role, "assistant");
73
+ assert.equal(messages[2]?.role, "tool");
74
+ assert.equal(messages[3]?.role, "assistant");
75
+ });
76
+ test("agent stops on repeated identical tool calls", async () => {
77
+ const agent = new CodingAgent({
78
+ config: createTestAgentConfig("/tmp"),
79
+ modelClient: new RepeatingModelClient(),
80
+ toolRegistry: new ToolRegistry([createEchoTool()]),
81
+ });
82
+ const { text } = await agent.runTurn("Do something");
83
+ assert.match(text, /repeated identical tool calls/);
84
+ });
85
+ test("agent omits code map when projectIndex is not provided", async () => {
86
+ let capturedSystem = "";
87
+ const spyClient = {
88
+ async chat(params) {
89
+ capturedSystem = params.system;
90
+ return {
91
+ text: "Done.",
92
+ toolCalls: [],
93
+ stopReason: "end_turn",
94
+ usage: { inputTokens: 1, outputTokens: 1 },
95
+ };
96
+ },
97
+ };
98
+ const agent = new CodingAgent({
99
+ config: createTestAgentConfig("/tmp"),
100
+ modelClient: spyClient,
101
+ toolRegistry: new ToolRegistry([createEchoTool()]),
102
+ });
103
+ await agent.runTurn("Hello");
104
+ assert.ok(!capturedSystem.includes("[Project Code Map]"));
105
+ });
106
+ test("agent includes code map in system prompt when projectIndex is provided", async () => {
107
+ const root = path.resolve(import.meta.dirname, "..");
108
+ const projectIndex = await buildProjectIndex(root);
109
+ let capturedSystem = "";
110
+ const spyClient = {
111
+ async chat(params) {
112
+ capturedSystem = params.system;
113
+ return {
114
+ text: "Task complete.",
115
+ toolCalls: [],
116
+ stopReason: "end_turn",
117
+ usage: { inputTokens: 1, outputTokens: 1 },
118
+ };
119
+ },
120
+ };
121
+ const agent = new CodingAgent({
122
+ config: createTestAgentConfig(root),
123
+ modelClient: spyClient,
124
+ toolRegistry: new ToolRegistry([createEchoTool()]),
125
+ projectIndex,
126
+ });
127
+ await agent.runTurn("List the project structure");
128
+ assert.ok(capturedSystem.includes("[Project Code Map]"));
129
+ assert.ok(capturedSystem.includes("CodingAgent"));
130
+ });
@@ -0,0 +1,37 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { test } from "node:test";
6
+ import { computeFileHashes, getWorkspaceCacheDir, loadIndex, saveIndex, } from "../src/indexer/cache.js";
7
+ import { buildProjectIndex } from "../src/indexer/project-index.js";
8
+ test("saveIndex and loadIndex round-trip", async () => {
9
+ const workspaceRoot = await mkdtemp(path.join(tmpdir(), "minicode-cache-"));
10
+ const samplePath = path.join(workspaceRoot, "sample.ts");
11
+ const content = `export function greet(name: string): string {
12
+ return \`Hello, \${name}\`;
13
+ }
14
+ `;
15
+ await writeFile(samplePath, content, "utf8");
16
+ const index = await buildProjectIndex(workspaceRoot);
17
+ const fileHashes = await computeFileHashes(workspaceRoot);
18
+ const cacheDir = getWorkspaceCacheDir(workspaceRoot);
19
+ await saveIndex(index, cacheDir, fileHashes);
20
+ const loaded = await loadIndex(cacheDir, fileHashes);
21
+ assert.ok(loaded, "should load from cache");
22
+ assert.equal(loaded.getSymbol("greet")?.qualifiedName, "greet");
23
+ assert.ok(loaded.getCodeMap().text.includes("greet"));
24
+ });
25
+ test("loadIndex returns null when file hashes differ", async () => {
26
+ const workspaceRoot = await mkdtemp(path.join(tmpdir(), "minicode-cache2-"));
27
+ const samplePath = path.join(workspaceRoot, "sample.ts");
28
+ await writeFile(samplePath, "export function a(): void {}", "utf8");
29
+ const index = await buildProjectIndex(workspaceRoot);
30
+ const fileHashes = await computeFileHashes(workspaceRoot);
31
+ const cacheDir = getWorkspaceCacheDir(workspaceRoot);
32
+ await saveIndex(index, cacheDir, fileHashes);
33
+ await writeFile(samplePath, "export function b(): void {}", "utf8");
34
+ const newHashes = await computeFileHashes(workspaceRoot);
35
+ const loaded = await loadIndex(cacheDir, newHashes);
36
+ assert.equal(loaded, null, "should invalidate cache when file changes");
37
+ });
@@ -0,0 +1,37 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { loadAgentConfig } from "../src/agent/config.js";
4
+ test("loadAgentConfig normalizes openai-compatible provider aliases", async () => {
5
+ const previousProvider = process.env.MODEL_PROVIDER;
6
+ const previousBaseUrl = process.env.OPENAI_BASE_URL;
7
+ const previousModel = process.env.MODEL;
8
+ try {
9
+ process.env.MODEL_PROVIDER = "lmstudio";
10
+ process.env.OPENAI_BASE_URL = "http://127.0.0.1:1234/v1";
11
+ process.env.MODEL = "qwen2.5-coder-7b-instruct";
12
+ const config = await loadAgentConfig("/tmp");
13
+ assert.equal(config.modelProvider, "openai-compatible");
14
+ assert.equal(config.openAiBaseUrl, "http://127.0.0.1:1234/v1");
15
+ assert.equal(config.model, "qwen2.5-coder-7b-instruct");
16
+ }
17
+ finally {
18
+ if (previousProvider === undefined) {
19
+ delete process.env.MODEL_PROVIDER;
20
+ }
21
+ else {
22
+ process.env.MODEL_PROVIDER = previousProvider;
23
+ }
24
+ if (previousBaseUrl === undefined) {
25
+ delete process.env.OPENAI_BASE_URL;
26
+ }
27
+ else {
28
+ process.env.OPENAI_BASE_URL = previousBaseUrl;
29
+ }
30
+ if (previousModel === undefined) {
31
+ delete process.env.MODEL;
32
+ }
33
+ else {
34
+ process.env.MODEL = previousModel;
35
+ }
36
+ }
37
+ });
@@ -0,0 +1,27 @@
1
+ import assert from "node:assert/strict";
2
+ import path from "node:path";
3
+ import { test } from "node:test";
4
+ import { buildProjectIndex } from "../src/indexer/project-index.js";
5
+ test("buildProjectIndex produces dependency edges", async () => {
6
+ const root = path.resolve(import.meta.dirname, "..");
7
+ const index = await buildProjectIndex(root);
8
+ assert.ok(index.dependencyEdges.length > 0, "should have dependency edges");
9
+ const parseResponseRefs = index.dependencyEdges.filter((e) => e.from === "parseResponse");
10
+ assert.ok(parseResponseRefs.some((e) => e.to === "ModelResponse"), "parseResponse should reference ModelResponse");
11
+ assert.ok(parseResponseRefs.some((e) => e.to === "ToolCall"), "parseResponse should reference ToolCall");
12
+ });
13
+ test("createModelClient has expected dependencies", async () => {
14
+ const root = path.resolve(import.meta.dirname, "..");
15
+ const index = await buildProjectIndex(root);
16
+ const edges = index.dependencyEdges.filter((e) => e.from === "createModelClient");
17
+ assert.ok(edges.some((e) => e.to === "AgentConfig"), "createModelClient should reference AgentConfig");
18
+ assert.ok(edges.length >= 1, "createModelClient should have at least one dependency");
19
+ });
20
+ test("AnthropicModelClient implements ModelClient", async () => {
21
+ const root = path.resolve(import.meta.dirname, "..");
22
+ const index = await buildProjectIndex(root);
23
+ const implementsEdge = index.dependencyEdges.find((e) => e.from === "AnthropicModelClient" &&
24
+ e.to === "ModelClient" &&
25
+ e.kind === "implements");
26
+ assert.ok(implementsEdge, "AnthropicModelClient should implement ModelClient");
27
+ });
@@ -0,0 +1,73 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { test } from "node:test";
6
+ import { buildProjectIndex } from "../src/indexer/project-index.js";
7
+ import { createEditFileTool } from "../src/tools/edit-file.js";
8
+ import { createReadFileTool } from "../src/tools/read-file.js";
9
+ import { createTestAgentConfig } from "./test-utils.js";
10
+ async function createTempWorkspace() {
11
+ return mkdtemp(path.join(tmpdir(), "minicode-tests-"));
12
+ }
13
+ test("edit_file replaces exactly one match", async () => {
14
+ const workspaceRoot = await createTempWorkspace();
15
+ const filePath = path.join(workspaceRoot, "sample.txt");
16
+ await writeFile(filePath, "hello world", "utf8");
17
+ const editTool = createEditFileTool(createTestAgentConfig(workspaceRoot));
18
+ const result = await editTool.execute({
19
+ path: "sample.txt",
20
+ old_string: "world",
21
+ new_string: "there",
22
+ });
23
+ assert.match(result, /Updated "sample\.txt" successfully\./);
24
+ const updated = await readFile(filePath, "utf8");
25
+ assert.equal(updated, "hello there");
26
+ });
27
+ test("edit_file fails when old_string is not unique", async () => {
28
+ const workspaceRoot = await createTempWorkspace();
29
+ const filePath = path.join(workspaceRoot, "sample.txt");
30
+ await writeFile(filePath, "repeat repeat repeat", "utf8");
31
+ const editTool = createEditFileTool(createTestAgentConfig(workspaceRoot));
32
+ await assert.rejects(() => editTool.execute({
33
+ path: "sample.txt",
34
+ old_string: "repeat",
35
+ new_string: "once",
36
+ }), /matched 3 times/);
37
+ const unchanged = await readFile(filePath, "utf8");
38
+ assert.equal(unchanged, "repeat repeat repeat");
39
+ });
40
+ test("edit_file triggers reindex when projectIndex provided", async () => {
41
+ const workspaceRoot = await createTempWorkspace();
42
+ const { mkdir } = await import("node:fs/promises");
43
+ const filePath = path.join(workspaceRoot, "src", "util.ts");
44
+ await mkdir(path.dirname(filePath), { recursive: true });
45
+ const initialContent = `export function add(a: number, b: number): number {
46
+ return a + b;
47
+ }
48
+ `;
49
+ await writeFile(filePath, initialContent, "utf8");
50
+ const index = await buildProjectIndex(workspaceRoot);
51
+ const before = index.getSymbol("add");
52
+ assert.ok(before?.signature.includes("a: number, b: number"));
53
+ const editTool = createEditFileTool(createTestAgentConfig(workspaceRoot), index);
54
+ await editTool.execute({
55
+ path: "src/util.ts",
56
+ old_string: "a: number, b: number",
57
+ new_string: "a: number, b: number, c?: number",
58
+ });
59
+ const after = index.getSymbol("add");
60
+ assert.ok(after?.signature.includes("c?: number"), "index should reflect edit");
61
+ });
62
+ test("read_file supports negative offset and line limits", async () => {
63
+ const workspaceRoot = await createTempWorkspace();
64
+ const filePath = path.join(workspaceRoot, "lines.txt");
65
+ await writeFile(filePath, "a\nb\nc\nd", "utf8");
66
+ const readTool = createReadFileTool(createTestAgentConfig(workspaceRoot));
67
+ const output = await readTool.execute({
68
+ path: "lines.txt",
69
+ offset: -2,
70
+ limit: 2,
71
+ });
72
+ assert.equal(output, "3|c\n4|d");
73
+ });
@@ -0,0 +1,30 @@
1
+ import assert from "node:assert/strict";
2
+ import path from "node:path";
3
+ import { test } from "node:test";
4
+ import { buildProjectIndex } from "../src/indexer/project-index.js";
5
+ import { createFindReferencesTool } from "../src/tools/find-references.js";
6
+ test("find_references returns symbols that reference ModelResponse", async () => {
7
+ const root = path.resolve(import.meta.dirname, "..");
8
+ const projectIndex = await buildProjectIndex(root);
9
+ const tool = createFindReferencesTool(projectIndex);
10
+ const result = await tool.execute({ name: "ModelResponse" });
11
+ assert.ok(result.includes("# References to ModelResponse"));
12
+ assert.ok(result.includes("parseResponse"));
13
+ });
14
+ test("find_references returns error for unknown symbol", async () => {
15
+ const root = path.resolve(import.meta.dirname, "..");
16
+ const projectIndex = await buildProjectIndex(root);
17
+ const tool = createFindReferencesTool(projectIndex);
18
+ const result = await tool.execute({ name: "NonExistentSymbol" });
19
+ assert.ok(result.includes("not found"));
20
+ });
21
+ test("find_references appears in tool registry when projectIndex provided", async () => {
22
+ const root = path.resolve(import.meta.dirname, "..");
23
+ const projectIndex = await buildProjectIndex(root);
24
+ const { ToolRegistry } = await import("../src/tools/registry.js");
25
+ const { createTestAgentConfig } = await import("./test-utils.js");
26
+ const registry = ToolRegistry.createDefault(createTestAgentConfig(root), projectIndex);
27
+ const schemas = registry.getToolSchemas();
28
+ const findRefs = schemas.find((s) => s.name === "find_references");
29
+ assert.ok(findRefs);
30
+ });
@@ -0,0 +1,35 @@
1
+ import assert from "node:assert/strict";
2
+ import path from "node:path";
3
+ import { test } from "node:test";
4
+ import { buildProjectIndex } from "../src/indexer/project-index.js";
5
+ import { createGetDependenciesTool } from "../src/tools/get-dependencies.js";
6
+ test("get_dependencies returns dependency cone for createModelClient", async () => {
7
+ const root = path.resolve(import.meta.dirname, "..");
8
+ const projectIndex = await buildProjectIndex(root);
9
+ const tool = createGetDependenciesTool(projectIndex);
10
+ const result = await tool.execute({ name: "createModelClient" });
11
+ assert.ok(result.includes("# Dependencies of createModelClient"));
12
+ assert.ok(result.includes("AgentConfig"));
13
+ });
14
+ test("get_dependencies respects depth parameter", async () => {
15
+ const root = path.resolve(import.meta.dirname, "..");
16
+ const projectIndex = await buildProjectIndex(root);
17
+ const tool = createGetDependenciesTool(projectIndex);
18
+ const resultDepth1 = await tool.execute({
19
+ name: "parseResponse",
20
+ depth: 1,
21
+ });
22
+ const resultDepth2 = await tool.execute({
23
+ name: "parseResponse",
24
+ depth: 2,
25
+ });
26
+ assert.ok(resultDepth1.includes("parseResponse"));
27
+ assert.ok(resultDepth2.includes("parseResponse"));
28
+ });
29
+ test("get_dependencies returns error for unknown symbol", async () => {
30
+ const root = path.resolve(import.meta.dirname, "..");
31
+ const projectIndex = await buildProjectIndex(root);
32
+ const tool = createGetDependenciesTool(projectIndex);
33
+ const result = await tool.execute({ name: "NonExistent" });
34
+ assert.ok(result.includes("not found"));
35
+ });
@@ -0,0 +1,18 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { resolveWorkspacePath, validateCommand, validatePath, } from "../src/safety/guardrails.js";
4
+ test("validatePath allows files within workspace", () => {
5
+ const workspaceRoot = "/tmp/workspace";
6
+ assert.equal(validatePath("src/index.ts", workspaceRoot), true);
7
+ });
8
+ test("validatePath rejects escape attempts", () => {
9
+ const workspaceRoot = "/tmp/workspace";
10
+ assert.equal(validatePath("../etc/passwd", workspaceRoot), false);
11
+ });
12
+ test("resolveWorkspacePath rejects absolute escape paths", () => {
13
+ const workspaceRoot = "/tmp/workspace";
14
+ assert.throws(() => resolveWorkspacePath("/etc/passwd", workspaceRoot), /outside workspace root/);
15
+ });
16
+ test("validateCommand blocks denylisted commands", () => {
17
+ assert.throws(() => validateCommand("rm -rf /", [/\brm\s+-rf\s+\//i]), /blocked by safety denylist/);
18
+ });
@@ -0,0 +1,201 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { test } from "node:test";
6
+ import { generateCodeMap } from "../src/indexer/code-map.js";
7
+ import { getPluginForFile, loadPlugins } from "../src/indexer/plugin-loader.js";
8
+ import { buildProjectIndex } from "../src/indexer/project-index.js";
9
+ import { typescriptPlugin } from "../src/indexer/plugins/typescript.js";
10
+ const SAMPLE_TS = `
11
+ export interface Foo {
12
+ bar: string;
13
+ }
14
+
15
+ export function hello(name: string): string {
16
+ return \`Hello, \${name}\`;
17
+ }
18
+
19
+ export class CodingAgent {
20
+ constructor(params: { config: unknown }) {}
21
+
22
+ async runTurn(input: string): Promise<string> {
23
+ return input;
24
+ }
25
+ }
26
+
27
+ const arrowFn = (x: number) => x + 1;
28
+ `;
29
+ test("TypeScript plugin extracts functions, classes, interfaces", () => {
30
+ const symbols = typescriptPlugin.indexFile("sample.ts", SAMPLE_TS);
31
+ const names = symbols.map((s) => s.qualifiedName);
32
+ assert.ok(names.includes("Foo"), "should extract interface Foo");
33
+ assert.ok(names.includes("hello"), "should extract function hello");
34
+ assert.ok(names.includes("CodingAgent"), "should extract class CodingAgent");
35
+ assert.ok(names.includes("CodingAgent.runTurn"), "should extract method runTurn");
36
+ assert.ok(names.includes("arrowFn"), "should extract arrow function");
37
+ });
38
+ test("TypeScript plugin returns correct line numbers", () => {
39
+ const symbols = typescriptPlugin.indexFile("sample.ts", SAMPLE_TS);
40
+ const hello = symbols.find((s) => s.name === "hello");
41
+ assert.ok(hello, "should find hello");
42
+ assert.ok(hello.startLine >= 6 && hello.startLine <= 10);
43
+ assert.ok(hello.endLine > hello.startLine);
44
+ });
45
+ test("TypeScript plugin handles arrow functions assigned to const", () => {
46
+ const symbols = typescriptPlugin.indexFile("sample.ts", SAMPLE_TS);
47
+ const arrow = symbols.find((s) => s.name === "arrowFn");
48
+ assert.ok(arrow, "should find arrowFn");
49
+ assert.equal(arrow.kind, "function");
50
+ });
51
+ test("Plugin loader returns the built-in TypeScript plugin", async () => {
52
+ const plugins = await loadPlugins("/tmp");
53
+ assert.ok(plugins.length >= 1);
54
+ assert.ok(plugins.some((p) => p.name === "typescript"));
55
+ });
56
+ test("getPluginForFile routes .ts files to TypeScript plugin", async () => {
57
+ const plugins = await loadPlugins("/tmp");
58
+ const plugin = getPluginForFile("src/agent/agent.ts", plugins);
59
+ assert.ok(plugin);
60
+ assert.equal(plugin.name, "typescript");
61
+ });
62
+ test("verify-index fixture exercises full indexing pipeline", async () => {
63
+ const root = path.resolve(import.meta.dirname, "..");
64
+ const fixtureRoot = path.join(root, "test-programs", "verify-index");
65
+ const index = await buildProjectIndex(fixtureRoot);
66
+ assert.ok(index.getSymbol("Processor"), "Processor class");
67
+ assert.ok(index.getSymbol("Processor.run"), "Processor.run method");
68
+ assert.ok(index.getSymbol("parse"), "parse function");
69
+ assert.ok(index.getSymbol("Task"), "Task interface");
70
+ const taskRefs = index.dependencyEdges.filter((e) => e.to === "Task");
71
+ assert.ok(taskRefs.length >= 2, "Task should have multiple references");
72
+ const implementsEdges = index.dependencyEdges.filter((e) => e.kind === "implements" && e.from === "Processor");
73
+ assert.ok(implementsEdges.length >= 1, "Processor should implement TaskRunner");
74
+ const codeMap = index.getCodeMap();
75
+ assert.ok(codeMap.text.includes("Processor"));
76
+ assert.ok(codeMap.text.includes("parseAndProcess"));
77
+ });
78
+ test("Code map generator produces expected format", () => {
79
+ const symbols = typescriptPlugin.indexFile("sample.ts", SAMPLE_TS);
80
+ const byFile = new Map([["sample.ts", symbols]]);
81
+ const result = generateCodeMap(byFile);
82
+ assert.ok(result.text.includes("# Project Code Map"));
83
+ assert.ok(result.text.includes("sample.ts"));
84
+ assert.ok(result.text.includes("CodingAgent"));
85
+ assert.ok(result.text.includes("runTurn"));
86
+ });
87
+ test("Code map respects token budget", () => {
88
+ const symbols = typescriptPlugin.indexFile("sample.ts", SAMPLE_TS);
89
+ const byFile = new Map([["sample.ts", symbols]]);
90
+ const result = generateCodeMap(byFile, 50);
91
+ assert.ok(result.text.length < 300);
92
+ assert.ok(result.text.includes("... and") || result.text.length > 0, "should truncate or fit within budget");
93
+ });
94
+ test("buildProjectIndex works on minicode src/", async () => {
95
+ const root = path.resolve(import.meta.dirname, "..");
96
+ const index = await buildProjectIndex(root);
97
+ assert.ok(index.symbols.size > 0);
98
+ assert.ok(index.files.size > 0);
99
+ const agentSymbols = index.getSymbolsInFile("src/agent/agent.ts");
100
+ assert.ok(agentSymbols.length >= 1);
101
+ assert.ok(agentSymbols.some((s) => s.name === "CodingAgent"));
102
+ const codeMap = index.getCodeMap();
103
+ assert.ok(codeMap.text.includes("CodingAgent"));
104
+ assert.ok(codeMap.text.includes("runTurn"));
105
+ });
106
+ test("getPluginForFile routes .tsx, .js, .jsx to TypeScript plugin", async () => {
107
+ const plugins = await loadPlugins("/tmp");
108
+ const tsx = getPluginForFile("src/component.tsx", plugins);
109
+ const js = getPluginForFile("src/legacy.js", plugins);
110
+ const jsx = getPluginForFile("src/component.jsx", plugins);
111
+ assert.ok(tsx);
112
+ assert.ok(js);
113
+ assert.ok(jsx);
114
+ assert.equal(tsx.name, "typescript");
115
+ });
116
+ test("TypeScript plugin extracts constructor", () => {
117
+ const symbols = typescriptPlugin.indexFile("sample.ts", SAMPLE_TS);
118
+ const ctor = symbols.find((s) => s.qualifiedName === "CodingAgent.constructor");
119
+ assert.ok(ctor, "should extract constructor");
120
+ assert.equal(ctor.kind, "method");
121
+ });
122
+ test("TypeScript plugin extracts type alias", () => {
123
+ const code = `export type UserId = string;`;
124
+ const symbols = typescriptPlugin.indexFile("types.ts", code);
125
+ const typeSym = symbols.find((s) => s.name === "UserId");
126
+ assert.ok(typeSym, "should extract type alias");
127
+ assert.equal(typeSym.kind, "type");
128
+ });
129
+ test("TypeScript plugin returns empty array for empty file", () => {
130
+ const symbols = typescriptPlugin.indexFile("empty.ts", "");
131
+ assert.equal(symbols.length, 0);
132
+ });
133
+ test("TypeScript plugin handles comments-only file", () => {
134
+ const symbols = typescriptPlugin.indexFile("comments.ts", "// comment only\n/* block */");
135
+ assert.equal(symbols.length, 0);
136
+ });
137
+ test("TypeScript plugin handles malformed syntax", () => {
138
+ const symbols = typescriptPlugin.indexFile("bad.ts", "function { broken");
139
+ assert.ok(Array.isArray(symbols));
140
+ });
141
+ test("ProjectIndex getSymbol finds by qualifiedName and by name", async () => {
142
+ const root = path.resolve(import.meta.dirname, "..");
143
+ const index = await buildProjectIndex(root);
144
+ const byQualified = index.getSymbol("CodingAgent.runTurn");
145
+ assert.ok(byQualified);
146
+ assert.equal(byQualified.qualifiedName, "CodingAgent.runTurn");
147
+ const byName = index.getSymbol("runTurn");
148
+ assert.ok(byName, "getSymbol should find by name when unique");
149
+ });
150
+ test("ProjectIndex getSymbolsInFile returns empty for non-existent file", async () => {
151
+ const root = path.resolve(import.meta.dirname, "..");
152
+ const index = await buildProjectIndex(root);
153
+ const symbols = index.getSymbolsInFile("src/nonexistent.ts");
154
+ assert.ok(Array.isArray(symbols));
155
+ assert.equal(symbols.length, 0);
156
+ });
157
+ test("ProjectIndex getDependencyCone returns target and dependencies", async () => {
158
+ const root = path.resolve(import.meta.dirname, "..");
159
+ const index = await buildProjectIndex(root);
160
+ const cone = index.getDependencyCone("parseResponse", 1);
161
+ assert.ok(Array.isArray(cone));
162
+ assert.ok(cone.length >= 1, "should include target symbol");
163
+ assert.ok(cone.some((s) => s.qualifiedName === "parseResponse"), "should include parseResponse");
164
+ });
165
+ test("Code map handles empty symbols map", () => {
166
+ const result = generateCodeMap(new Map());
167
+ assert.ok(result.text.includes("# Project Code Map"));
168
+ assert.ok(result.text.length < 100);
169
+ });
170
+ test("Code map nests methods under class", () => {
171
+ const symbols = typescriptPlugin.indexFile("sample.ts", SAMPLE_TS);
172
+ const byFile = new Map([["sample.ts", symbols]]);
173
+ const result = generateCodeMap(byFile);
174
+ assert.ok(result.text.includes("class CodingAgent"));
175
+ assert.ok(result.text.includes("runTurn"));
176
+ const runTurnLine = result.text.split("\n").find((l) => l.includes("runTurn"));
177
+ assert.ok(runTurnLine?.startsWith(" "), "method should be indented under class");
178
+ });
179
+ test("reindexFile updates symbols and code map after file change", async () => {
180
+ const workspaceRoot = await mkdtemp(path.join(tmpdir(), "minicode-reindex-"));
181
+ const samplePath = path.join(workspaceRoot, "sample.ts");
182
+ const initialContent = `export function greet(name: string): string {
183
+ return \`Hello, \${name}\`;
184
+ }
185
+ `;
186
+ await writeFile(samplePath, initialContent, "utf8");
187
+ const index = await buildProjectIndex(workspaceRoot);
188
+ const sym = index.getSymbol("greet");
189
+ assert.ok(sym, "should find greet");
190
+ assert.ok(sym.signature.includes("name: string"), "initial signature");
191
+ const updatedContent = `export function greet(name: string, title?: string): string {
192
+ return title ? \`Hello, \${title} \${name}\` : \`Hello, \${name}\`;
193
+ }
194
+ `;
195
+ index.reindexFile("sample.ts", updatedContent);
196
+ const updatedSym = index.getSymbol("greet");
197
+ assert.ok(updatedSym, "should still find greet");
198
+ assert.ok(updatedSym.signature.includes("title?: string"), "signature should reflect updated params");
199
+ const codeMap = index.getCodeMap();
200
+ assert.ok(codeMap.text.includes("title?: string"), "code map should reflect new signature");
201
+ });