@sean.holung/minicode 0.3.4 → 0.3.6

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 (72) hide show
  1. package/README.md +25 -47
  2. package/dist/scripts/run-benchmarks.js +73 -28
  3. package/dist/src/agent/config.js +51 -66
  4. package/dist/src/agent/editable-config.js +50 -58
  5. package/dist/src/agent/home-env.js +74 -0
  6. package/dist/src/benchmark/runner.js +142 -59
  7. package/dist/src/cli/config-slash-command.js +15 -13
  8. package/dist/src/indexer/project-index.js +49 -13
  9. package/dist/src/serve/agent-bridge.js +99 -31
  10. package/dist/src/serve/mcp-server.js +70 -21
  11. package/dist/src/serve/server.js +198 -8
  12. package/dist/src/session/session-preview.js +14 -0
  13. package/dist/src/shared/graph-search.js +80 -0
  14. package/dist/src/shared/graph-selection.js +40 -0
  15. package/dist/src/shared/graph-symbols.js +82 -0
  16. package/dist/src/shared/symbol-resolution.js +33 -0
  17. package/dist/src/tools/find-path.js +15 -6
  18. package/dist/src/tools/find-references.js +7 -2
  19. package/dist/src/tools/get-dependencies.js +8 -3
  20. package/dist/src/tools/read-symbol.js +9 -3
  21. package/dist/src/tools/registry.js +4 -1
  22. package/dist/src/tools/search-code-map.js +18 -3
  23. package/dist/src/web/app.js +646 -87
  24. package/dist/src/web/index.html +68 -6
  25. package/dist/src/web/style.css +208 -1
  26. package/dist/tests/benchmark-harness.test.js +100 -0
  27. package/dist/tests/config-api.test.js +5 -5
  28. package/dist/tests/config-integration.test.js +130 -56
  29. package/dist/tests/config-slash-command.test.js +12 -11
  30. package/dist/tests/config.test.js +12 -4
  31. package/dist/tests/editable-config.test.js +15 -12
  32. package/dist/tests/file-tools.test.js +34 -1
  33. package/dist/tests/find-path.test.js +43 -2
  34. package/dist/tests/find-references.test.js +49 -0
  35. package/dist/tests/get-dependencies.test.js +23 -0
  36. package/dist/tests/graph-onboarding.test.js +10 -1
  37. package/dist/tests/graph-search.test.js +66 -0
  38. package/dist/tests/graph-selection.test.js +58 -0
  39. package/dist/tests/graph-symbols.test.js +45 -0
  40. package/dist/tests/home-env.test.js +56 -0
  41. package/dist/tests/indexer.test.js +6 -0
  42. package/dist/tests/read-symbol.test.js +35 -0
  43. package/dist/tests/request-tracker.test.js +15 -0
  44. package/dist/tests/run-benchmarks.test.js +117 -33
  45. package/dist/tests/search-code-map.test.js +2 -0
  46. package/dist/tests/serve.integration.test.js +338 -9
  47. package/dist/tests/session-preview.test.js +56 -0
  48. package/dist/tests/session-ui.test.js +4 -0
  49. package/dist/tests/settings-ui.test.js +18 -0
  50. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
  51. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +2 -1
  52. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
  53. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +1 -1
  54. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
  55. package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
  56. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts +3 -0
  57. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts.map +1 -1
  58. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts +3 -0
  59. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts.map +1 -1
  60. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js +4 -1
  61. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js.map +1 -1
  62. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts +11 -1
  63. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts.map +1 -1
  64. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js +4 -1
  65. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js.map +1 -1
  66. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.d.ts.map +1 -1
  67. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js +16 -8
  68. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js.map +1 -1
  69. package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.js +19 -2
  70. package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.js.map +1 -1
  71. package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +1 -1
@@ -1,6 +1,8 @@
1
1
  import assert from "node:assert/strict";
2
2
  import path from "node:path";
3
3
  import { test } from "node:test";
4
+ import { mkdtemp, writeFile } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
4
6
  import { buildProjectIndex } from "../src/indexer/project-index.js";
5
7
  import { createFindReferencesTool } from "../src/tools/find-references.js";
6
8
  test("find_references returns symbols that reference ProjectIndex", async () => {
@@ -28,3 +30,50 @@ test("find_references appears in tool registry when projectIndex provided", asyn
28
30
  const findRefs = schemas.find((s) => s.name === "find_references");
29
31
  assert.ok(findRefs);
30
32
  });
33
+ test("find_references returns disambiguation list for ambiguous bare symbol names", async () => {
34
+ const workspaceRoot = await mkdtemp(path.join(tmpdir(), "minicode-find-references-collisions-"));
35
+ await writeFile(path.join(workspaceRoot, "sample.ts"), `export type Review = { id: string };
36
+
37
+ export function serializeReview(review: Review) {
38
+ return review.id;
39
+ }
40
+
41
+ export class Review {
42
+ constructor(public id: string) {}
43
+ }
44
+
45
+ export function createReview(id: string) {
46
+ return new Review(id);
47
+ }
48
+ `, "utf8");
49
+ const projectIndex = await buildProjectIndex(workspaceRoot);
50
+ const tool = createFindReferencesTool(projectIndex);
51
+ const result = await tool.execute({ name: "Review" });
52
+ assert.ok(result.includes('Symbol "Review" is ambiguous'));
53
+ assert.ok(result.includes("Review (type)"));
54
+ assert.ok(result.includes("Review (class)"));
55
+ assert.ok(result.includes("qualified: Review#type"));
56
+ assert.ok(result.includes("qualified: Review#class"));
57
+ });
58
+ test("find_references accepts qualified names for colliding symbols", async () => {
59
+ const workspaceRoot = await mkdtemp(path.join(tmpdir(), "minicode-find-references-qualified-"));
60
+ await writeFile(path.join(workspaceRoot, "sample.ts"), `export type Review = { id: string };
61
+
62
+ export function serializeReview(review: Review) {
63
+ return review.id;
64
+ }
65
+
66
+ export class Review {
67
+ constructor(public id: string) {}
68
+ }
69
+
70
+ export function createReview(id: string) {
71
+ return new Review(id);
72
+ }
73
+ `, "utf8");
74
+ const projectIndex = await buildProjectIndex(workspaceRoot);
75
+ const tool = createFindReferencesTool(projectIndex);
76
+ const result = await tool.execute({ name: "Review#class" });
77
+ assert.ok(result.includes("# References to Review (class)"));
78
+ assert.ok(result.includes("createReview"));
79
+ });
@@ -1,6 +1,8 @@
1
1
  import assert from "node:assert/strict";
2
2
  import path from "node:path";
3
3
  import { test } from "node:test";
4
+ import { mkdtemp, writeFile } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
4
6
  import { buildProjectIndex } from "../src/indexer/project-index.js";
5
7
  import { createGetDependenciesTool } from "../src/tools/get-dependencies.js";
6
8
  test("get_dependencies returns dependency cone for createModelClient", async () => {
@@ -33,3 +35,24 @@ test("get_dependencies returns error for unknown symbol", async () => {
33
35
  const result = await tool.execute({ name: "NonExistent" });
34
36
  assert.ok(result.includes("not found"));
35
37
  });
38
+ test("get_dependencies returns disambiguation list for ambiguous bare symbol names", async () => {
39
+ const workspaceRoot = await mkdtemp(path.join(tmpdir(), "minicode-get-dependencies-collisions-"));
40
+ await writeFile(path.join(workspaceRoot, "sample.ts"), `export type Review = { id: string };
41
+
42
+ export class Review {
43
+ constructor(public id: string) {}
44
+ }
45
+
46
+ export function createReview(id: string) {
47
+ return new Review(id);
48
+ }
49
+ `, "utf8");
50
+ const projectIndex = await buildProjectIndex(workspaceRoot);
51
+ const tool = createGetDependenciesTool(projectIndex);
52
+ const result = await tool.execute({ name: "Review" });
53
+ assert.ok(result.includes('Symbol "Review" is ambiguous'));
54
+ assert.ok(result.includes("Review (type)"));
55
+ assert.ok(result.includes("Review (class)"));
56
+ assert.ok(result.includes("qualified: Review#type"));
57
+ assert.ok(result.includes("qualified: Review#class"));
58
+ });
@@ -9,6 +9,7 @@ test('built CSS contains graph-onboarding styles', () => {
9
9
  assert.ok(css.includes('.graph-onboarding-icon'), 'CSS should contain .graph-onboarding-icon class');
10
10
  assert.ok(css.includes('.graph-onboarding-title'), 'CSS should contain .graph-onboarding-title class');
11
11
  assert.ok(css.includes('.graph-onboarding-subtitle'), 'CSS should contain .graph-onboarding-subtitle class');
12
+ assert.ok(css.includes('.search-result-subtitle'), 'CSS should contain secondary search result text styling');
12
13
  assert.ok(css.includes('pointer-events: none'), 'onboarding overlay should not block interactions');
13
14
  assert.ok(css.includes('width: 380px;'), 'symbol detail panel should have a wider default width');
14
15
  });
@@ -22,11 +23,12 @@ test('built HTML contains #cy graph container', () => {
22
23
  const html = readFileSync(join(distWeb, 'index.html'), 'utf-8');
23
24
  assert.ok(html.includes('id="cy"'), 'HTML should contain the #cy graph container');
24
25
  assert.ok(html.includes('id="graph-pane"'), 'HTML should contain the #graph-pane wrapper');
26
+ assert.ok(html.includes('Search symbols or files...'), 'HTML should expose mixed symbol/file search');
25
27
  });
26
28
  test('onboarding hint includes user-facing guidance text in built JS', () => {
27
29
  const js = readFileSync(join(distWeb, 'app.js'), 'utf-8');
28
30
  assert.ok(js.includes('Code dependency graph'), 'onboarding title should mention the code dependency graph');
29
- assert.ok(js.includes('Search for a symbol'), 'onboarding subtitle should guide users to search');
31
+ assert.ok(js.includes('Search for a symbol or file'), 'onboarding subtitle should guide users to search');
30
32
  });
31
33
  test('onboarding hint is re-shown on clear in built JS', () => {
32
34
  const js = readFileSync(join(distWeb, 'app.js'), 'utf-8');
@@ -53,3 +55,10 @@ test('built JS auto-opens symbol details for agent activity and graph search sel
53
55
  assert.ok(js.includes('openDetail: true'), 'JS should request the detail panel when focusing symbols from agent activity or search');
54
56
  assert.ok(js.includes('await showDetail(node, detailEl)'), 'JS should populate the symbol detail panel when focus requests it');
55
57
  });
58
+ test('built JS supports file search results and file-centered neighborhood rendering', () => {
59
+ const js = readFileSync(join(distWeb, 'app.js'), 'utf-8');
60
+ assert.ok(js.includes('focusFileInGraph'), 'JS should define a file-focused graph seeding helper');
61
+ assert.ok(js.includes('buildGraphSearchResults'), 'JS should use the shared mixed search result helper');
62
+ assert.ok(js.includes('el.dataset.type') && js.includes('focusFileInGraph(id)'), 'JS should branch on file search results when handling selection');
63
+ assert.ok(js.includes('buildFileFocusedSelection'), 'file-focused graph seeding should use the shared file selection helper');
64
+ });
@@ -0,0 +1,66 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { buildGraphFileIndex, buildGraphSearchResults, compareGraphFilePaths, matchesGraphFileQuery, } from "../src/shared/graph-search.js";
4
+ test("buildGraphFileIndex groups graph nodes by file path", () => {
5
+ const nodes = new Map([
6
+ ["Review#class", { name: "Review (class)", qualifiedName: "Review#class", filePath: "src/review.ts", exported: true }],
7
+ ["Review#type", { name: "Review (type)", qualifiedName: "Review#type", filePath: "src/review.ts", exported: true }],
8
+ ["Session.trim", { name: "trim", qualifiedName: "Session.trim", filePath: "src/session.ts", exported: false }],
9
+ ]);
10
+ const fileIndex = buildGraphFileIndex(nodes);
11
+ assert.deepEqual(fileIndex.get("src/review.ts"), ["Review#class", "Review#type"]);
12
+ assert.deepEqual(fileIndex.get("src/session.ts"), ["Session.trim"]);
13
+ });
14
+ test("compareGraphFilePaths prefers files with more indexed symbols", () => {
15
+ const fileIndex = new Map([
16
+ ["src/review.ts", ["Review#class", "Review#type"]],
17
+ ["src/session.ts", ["Session.trim"]],
18
+ ]);
19
+ const ranked = [...fileIndex.keys()].sort((a, b) => compareGraphFilePaths(a, b, fileIndex));
20
+ assert.deepEqual(ranked, ["src/review.ts", "src/session.ts"]);
21
+ });
22
+ test("matchesGraphFileQuery matches file path substrings case-insensitively", () => {
23
+ assert.equal(matchesGraphFileQuery("review", "src/review.ts"), true);
24
+ assert.equal(matchesGraphFileQuery("SRC/REVIEW", "src/review.ts"), true);
25
+ assert.equal(matchesGraphFileQuery("session", "src/review.ts"), false);
26
+ });
27
+ test("buildGraphSearchResults returns mixed symbol and file matches", () => {
28
+ const nodes = new Map([
29
+ ["Review#class", { name: "Review (class)", qualifiedName: "Review#class", kind: "class", filePath: "src/review.ts", exported: true }],
30
+ ["Review#type", { name: "Review (type)", qualifiedName: "Review#type", kind: "type", filePath: "src/review.ts", exported: true }],
31
+ ["Session.trim", { name: "trim", qualifiedName: "Session.trim", kind: "method", filePath: "src/session.ts", exported: false }],
32
+ ]);
33
+ const rankedSymbols = [...nodes.keys()];
34
+ const fileIndex = buildGraphFileIndex(nodes);
35
+ const results = buildGraphSearchResults({
36
+ query: "review",
37
+ symbolIds: rankedSymbols,
38
+ nodes,
39
+ fileIndex,
40
+ });
41
+ assert.equal(results[0]?.type, "symbol");
42
+ assert.equal(results[0]?.id, "Review#class");
43
+ assert.equal(results[1]?.type, "symbol");
44
+ assert.equal(results[2]?.type, "file");
45
+ assert.equal(results[2]?.id, "src/review.ts");
46
+ });
47
+ test("buildGraphSearchResults returns default file suggestions for short queries", () => {
48
+ const nodes = new Map([
49
+ ["Review#class", { name: "Review (class)", qualifiedName: "Review#class", kind: "class", filePath: "src/review.ts", exported: true }],
50
+ ["Review#type", { name: "Review (type)", qualifiedName: "Review#type", kind: "type", filePath: "src/review.ts", exported: true }],
51
+ ["Session.trim", { name: "trim", qualifiedName: "Session.trim", kind: "method", filePath: "src/session.ts", exported: false }],
52
+ ]);
53
+ const rankedSymbols = [...nodes.keys()];
54
+ const fileIndex = buildGraphFileIndex(nodes);
55
+ const results = buildGraphSearchResults({
56
+ query: "",
57
+ symbolIds: rankedSymbols,
58
+ nodes,
59
+ fileIndex,
60
+ symbolLimit: 2,
61
+ fileLimit: 1,
62
+ });
63
+ assert.equal(results.length, 3);
64
+ assert.equal(results[2]?.type, "file");
65
+ assert.equal(results[2]?.id, "src/review.ts");
66
+ });
@@ -0,0 +1,58 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { buildFileFocusedSelection, buildGraphEdgeId, buildGraphEdgeIndex, } from "../src/shared/graph-selection.js";
4
+ test("buildGraphEdgeIndex indexes edges by both source and target", () => {
5
+ const edges = [
6
+ { source: "Review#class", target: "Session.trim", kind: "references" },
7
+ { source: "renderReview", target: "Review#type", kind: "references" },
8
+ ];
9
+ const edgeIndex = buildGraphEdgeIndex(edges);
10
+ assert.deepEqual(edgeIndex.get("Review#class"), [edges[0]]);
11
+ assert.deepEqual(edgeIndex.get("Session.trim"), [edges[0]]);
12
+ assert.deepEqual(edgeIndex.get("renderReview"), [edges[1]]);
13
+ assert.deepEqual(edgeIndex.get("Review#type"), [edges[1]]);
14
+ });
15
+ test("buildFileFocusedSelection includes file symbols and touching neighbors", () => {
16
+ const fileIndex = new Map([
17
+ ["src/review.ts", ["Review#class", "Review#type"]],
18
+ ]);
19
+ const edges = [
20
+ { source: "Review#class", target: "Session.trim", kind: "references" },
21
+ { source: "renderReview", target: "Review#type", kind: "references" },
22
+ { source: "Review#class", target: "Review#type", kind: "references" },
23
+ ];
24
+ const selection = buildFileFocusedSelection({
25
+ filePath: "src/review.ts",
26
+ fileIndex,
27
+ edgeIndex: buildGraphEdgeIndex(edges),
28
+ });
29
+ assert.deepEqual(new Set(selection.nodeIds), new Set(["Review#class", "Review#type", "Session.trim", "renderReview"]));
30
+ assert.deepEqual(new Set(selection.edges.map((edge) => buildGraphEdgeId(edge))), new Set(edges.map((edge) => buildGraphEdgeId(edge))));
31
+ });
32
+ test("buildFileFocusedSelection excludes edges between external neighbors", () => {
33
+ const fileIndex = new Map([
34
+ ["src/review.ts", ["Review#class"]],
35
+ ]);
36
+ const reviewToSession = { source: "Review#class", target: "Session.trim", kind: "references" };
37
+ const reviewToRender = { source: "Review#class", target: "renderReview", kind: "references" };
38
+ const sessionToRender = { source: "Session.trim", target: "renderReview", kind: "references" };
39
+ const edges = [reviewToSession, reviewToRender, sessionToRender];
40
+ const selection = buildFileFocusedSelection({
41
+ filePath: "src/review.ts",
42
+ fileIndex,
43
+ edgeIndex: buildGraphEdgeIndex(edges),
44
+ });
45
+ assert.deepEqual(new Set(selection.nodeIds), new Set(["Review#class", "Session.trim", "renderReview"]));
46
+ assert.deepEqual(new Set(selection.edges.map((edge) => buildGraphEdgeId(edge))), new Set([
47
+ buildGraphEdgeId(reviewToSession),
48
+ buildGraphEdgeId(reviewToRender),
49
+ ]));
50
+ });
51
+ test("buildFileFocusedSelection returns an empty selection for unknown files", () => {
52
+ const selection = buildFileFocusedSelection({
53
+ filePath: "src/missing.ts",
54
+ fileIndex: new Map(),
55
+ edgeIndex: buildGraphEdgeIndex([]),
56
+ });
57
+ assert.deepEqual(selection, { nodeIds: [], edges: [] });
58
+ });
@@ -0,0 +1,45 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { compareGraphNodeIds, getGraphNodeLabel, matchesGraphNodeQuery, resolveGraphNodeIds, } from "../src/shared/graph-symbols.js";
4
+ test("getGraphNodeLabel prefers display names for graph collisions", () => {
5
+ assert.equal(getGraphNodeLabel({ name: "Review (class)", qualifiedName: "Review#class" }), "Review (class)");
6
+ assert.equal(getGraphNodeLabel({ qualifiedName: "Session.trim" }), "trim");
7
+ });
8
+ test("resolveGraphNodeIds returns all exact bare-name collisions", () => {
9
+ const nodes = new Map([
10
+ ["Review#type", { name: "Review (type)", qualifiedName: "Review#type", exported: true }],
11
+ ["Review#class", { name: "Review (class)", qualifiedName: "Review#class", exported: true }],
12
+ ["Review.constructor", { name: "Review.constructor", qualifiedName: "Review.constructor", exported: false }],
13
+ ]);
14
+ assert.deepEqual(resolveGraphNodeIds(nodes, "Review"), [
15
+ "Review#class",
16
+ "Review#type",
17
+ ]);
18
+ assert.deepEqual(resolveGraphNodeIds(nodes, "Review (class)"), [
19
+ "Review#class",
20
+ ]);
21
+ assert.deepEqual(resolveGraphNodeIds(nodes, "Review#type"), [
22
+ "Review#type",
23
+ ]);
24
+ });
25
+ test("matchesGraphNodeQuery supports disambiguated labels and bare collision names", () => {
26
+ const typeNode = { name: "Review (type)", qualifiedName: "Review#type" };
27
+ const classNode = { name: "Review (class)", qualifiedName: "Review#class" };
28
+ assert.equal(matchesGraphNodeQuery("Review", typeNode, "Review#type"), true);
29
+ assert.equal(matchesGraphNodeQuery("Review", classNode, "Review#class"), true);
30
+ assert.equal(matchesGraphNodeQuery("class", classNode, "Review#class"), true);
31
+ assert.equal(matchesGraphNodeQuery("type", classNode, "Review#class"), false);
32
+ });
33
+ test("compareGraphNodeIds sorts exported collisions by label", () => {
34
+ const nodes = new Map([
35
+ ["Review#type", { name: "Review (type)", qualifiedName: "Review#type", exported: true }],
36
+ ["Review#class", { name: "Review (class)", qualifiedName: "Review#class", exported: true }],
37
+ ["internalReviewHelper", { name: "internalReviewHelper", qualifiedName: "internalReviewHelper", exported: false }],
38
+ ]);
39
+ const sorted = [...nodes.keys()].sort((a, b) => compareGraphNodeIds(a, b, nodes));
40
+ assert.deepEqual(sorted, [
41
+ "Review#class",
42
+ "Review#type",
43
+ "internalReviewHelper",
44
+ ]);
45
+ });
@@ -0,0 +1,56 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { afterEach, test } from "node:test";
6
+ import { getHomeEnvPath, upsertHomeEnvValues } from "../src/agent/home-env.js";
7
+ const tempDirs = [];
8
+ afterEach(async () => {
9
+ await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
10
+ });
11
+ test("upsertHomeEnvValues creates ~/.minicode/.env when missing", async () => {
12
+ const minicodeHome = await mkdtemp(path.join(os.tmpdir(), "minicode-home-env-"));
13
+ tempDirs.push(minicodeHome);
14
+ const result = await upsertHomeEnvValues({
15
+ minicodeHome,
16
+ values: {
17
+ MODEL_PROVIDER: "openai-compatible",
18
+ OPENAI_BASE_URL: "https://openrouter.ai/api/v1",
19
+ OPENROUTER_API_KEY: "sk-or-v1-test",
20
+ },
21
+ });
22
+ assert.equal(result.path, getHomeEnvPath(minicodeHome));
23
+ const contents = await readFile(result.path, "utf8");
24
+ assert.match(contents, /^MODEL_PROVIDER=openai-compatible/m);
25
+ assert.match(contents, /^OPENAI_BASE_URL=https:\/\/openrouter\.ai\/api\/v1/m);
26
+ assert.match(contents, /^OPENROUTER_API_KEY=sk-or-v1-test/m);
27
+ });
28
+ test("upsertHomeEnvValues replaces existing keys and preserves unrelated lines", async () => {
29
+ const minicodeHome = await mkdtemp(path.join(os.tmpdir(), "minicode-home-env-"));
30
+ tempDirs.push(minicodeHome);
31
+ const envPath = getHomeEnvPath(minicodeHome);
32
+ await writeFile(envPath, [
33
+ "# Existing config",
34
+ "MODEL_PROVIDER=anthropic",
35
+ "OPENAI_BASE_URL=https://example.invalid/v1",
36
+ "OPENROUTER_API_KEY=old-key",
37
+ "OPENROUTER_API_KEY=older-key",
38
+ "MODEL=existing-model",
39
+ "",
40
+ ].join("\n"), "utf8");
41
+ await upsertHomeEnvValues({
42
+ minicodeHome,
43
+ values: {
44
+ MODEL_PROVIDER: "openai-compatible",
45
+ OPENAI_BASE_URL: "https://openrouter.ai/api/v1",
46
+ OPENROUTER_API_KEY: "sk-or-v1-new",
47
+ },
48
+ });
49
+ const contents = await readFile(envPath, "utf8");
50
+ assert.match(contents, /^# Existing config/m);
51
+ assert.match(contents, /^MODEL_PROVIDER=openai-compatible$/m);
52
+ assert.match(contents, /^OPENAI_BASE_URL=https:\/\/openrouter\.ai\/api\/v1$/m);
53
+ assert.match(contents, /^OPENROUTER_API_KEY=sk-or-v1-new$/m);
54
+ assert.match(contents, /^MODEL=existing-model$/m);
55
+ assert.equal(contents.match(/^OPENROUTER_API_KEY=/gm)?.length, 1);
56
+ });
@@ -232,6 +232,12 @@ export class Employee {
232
232
  const resolved = index.getSymbol("Employee");
233
233
  assert.ok(resolved, "bare lookup should still resolve one of the colliding symbols");
234
234
  assert.ok(resolved.displayName === "Employee (class)" || resolved.displayName === "Employee (interface)", "resolved symbol should use a disambiguated display name");
235
+ const matches = index.getSymbolMatches("Employee");
236
+ assert.equal(matches.length, 2, "should list every colliding symbol for ambiguous lookups");
237
+ assert.deepEqual(matches.map((symbol) => symbol.displayName).sort(), ["Employee (class)", "Employee (interface)"].sort());
238
+ assert.deepEqual(matches.map((symbol) => symbol.qualifiedName).sort(), [employeeClass.qualifiedName, employeeInterface.qualifiedName].sort());
239
+ assert.deepEqual(index.getSymbolMatches("Employee (class)").map((symbol) => symbol.qualifiedName), [employeeClass.qualifiedName]);
240
+ assert.deepEqual(index.getSymbolMatches("Employee#class").map((symbol) => symbol.qualifiedName), [employeeClass.qualifiedName]);
235
241
  });
236
242
  test("TypeScript plugin indexes class expressions assigned to variables", () => {
237
243
  const code = `const MyClass = class MyClass {
@@ -1,6 +1,8 @@
1
1
  import assert from "node:assert/strict";
2
2
  import path from "node:path";
3
3
  import { test } from "node:test";
4
+ import { mkdtemp, writeFile } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
4
6
  import { buildProjectIndex } from "../src/indexer/project-index.js";
5
7
  import { createReadSymbolTool } from "../src/tools/read-symbol.js";
6
8
  import { createToolRegistry } from "../src/tools/registry.js";
@@ -73,6 +75,39 @@ test("read_symbol includes Referenced Types section for createToolRegistry", asy
73
75
  assert.ok(result.includes("# createToolRegistry"));
74
76
  assert.ok(result.includes("src/tools/registry.ts"));
75
77
  });
78
+ test("read_symbol returns disambiguation list for ambiguous bare symbol names", async () => {
79
+ const workspaceRoot = await mkdtemp(path.join(tmpdir(), "minicode-read-symbol-collisions-"));
80
+ await writeFile(path.join(workspaceRoot, "sample.ts"), `export type Review = { id: string };
81
+
82
+ export class Review {
83
+ constructor(public id: string) {}
84
+ }
85
+ `, "utf8");
86
+ const config = createTestAgentConfig(workspaceRoot);
87
+ const projectIndex = await buildProjectIndex(workspaceRoot);
88
+ const tool = createReadSymbolTool(config, projectIndex);
89
+ const result = await tool.execute({ name: "Review" });
90
+ assert.ok(result.includes('Symbol "Review" is ambiguous'));
91
+ assert.ok(result.includes("Review (type)"));
92
+ assert.ok(result.includes("Review (class)"));
93
+ assert.ok(result.includes("qualified: Review#type"));
94
+ assert.ok(result.includes("qualified: Review#class"));
95
+ });
96
+ test("read_symbol accepts qualified names for colliding symbols", async () => {
97
+ const workspaceRoot = await mkdtemp(path.join(tmpdir(), "minicode-read-symbol-qualified-"));
98
+ await writeFile(path.join(workspaceRoot, "sample.ts"), `export type Review = { id: string };
99
+
100
+ export class Review {
101
+ constructor(public id: string) {}
102
+ }
103
+ `, "utf8");
104
+ const config = createTestAgentConfig(workspaceRoot);
105
+ const projectIndex = await buildProjectIndex(workspaceRoot);
106
+ const tool = createReadSymbolTool(config, projectIndex);
107
+ const result = await tool.execute({ name: "Review#class", includeBody: false });
108
+ assert.ok(result.includes("# Review (class) (class)"));
109
+ assert.ok(result.includes("sample.ts"));
110
+ });
76
111
  test("read_symbol is not in tool registry when projectIndex is undefined", () => {
77
112
  const config = createTestAgentConfig("/tmp");
78
113
  const registry = createToolRegistry(config);
@@ -0,0 +1,15 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { createLatestRequestTracker } from "../src/web/request-tracker.js";
4
+ test("latest request tracker only accepts the most recent request token", () => {
5
+ const tracker = createLatestRequestTracker();
6
+ const first = tracker.begin();
7
+ const second = tracker.begin();
8
+ assert.equal(tracker.isCurrent(first), false);
9
+ assert.equal(tracker.isCurrent(second), true);
10
+ });
11
+ test("latest request tracker treats the current token as active until superseded", () => {
12
+ const tracker = createLatestRequestTracker();
13
+ const token = tracker.begin();
14
+ assert.equal(tracker.isCurrent(token), true);
15
+ });
@@ -3,7 +3,7 @@ import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import { test } from "node:test";
6
- import { parseArgs, buildConfig, loadTasks } from "../scripts/run-benchmarks.js";
6
+ import { parseArgs, buildConfig, loadTasks, getBenchmarkConfigPath } from "../scripts/run-benchmarks.js";
7
7
  // ─── parseArgs ────────────────────────────────────────────────────
8
8
  test("parseArgs: defaults to variant 'ci' with no flags", () => {
9
9
  const args = parseArgs([]);
@@ -43,52 +43,136 @@ test("parseArgs: ignores unknown flags", () => {
43
43
  assert.equal(args.variant, "test");
44
44
  });
45
45
  // ─── buildConfig ──────────────────────────────────────────────────
46
- test("buildConfig: returns defaults when no env vars set", () => {
47
- const originalProvider = process.env.MODEL_PROVIDER;
48
- const originalModel = process.env.MODEL;
49
- delete process.env.MODEL_PROVIDER;
50
- delete process.env.MODEL;
46
+ test("getBenchmarkConfigPath resolves under benchmarks/", () => {
47
+ const repoRoot = "/tmp/minicode-repo";
48
+ assert.equal(getBenchmarkConfigPath(repoRoot), path.join(repoRoot, "benchmarks", "benchmark.config.json"));
49
+ });
50
+ test("buildConfig: reads benchmark config file defaults", async () => {
51
+ const tmpRoot = await mkdtemp(path.join(tmpdir(), "bench-config-root-"));
52
+ const configPath = path.join(tmpRoot, "benchmarks", "benchmark.config.json");
53
+ await mkdir(path.dirname(configPath), { recursive: true });
54
+ await writeFile(configPath, JSON.stringify({
55
+ modelProvider: "openai-compatible",
56
+ model: "google/gemini-3-flash-preview",
57
+ openAiBaseUrl: "https://openrouter.ai/api/v1",
58
+ maxSteps: 42,
59
+ maxContextTokens: 12345,
60
+ }));
51
61
  try {
52
- const config = buildConfig();
62
+ const config = buildConfig({
63
+ repoRoot: tmpRoot,
64
+ env: {},
65
+ homeEnvPath: path.join(tmpRoot, ".missing-home-env"),
66
+ configPath,
67
+ });
53
68
  assert.equal(config.modelProvider, "openai-compatible");
54
- assert.equal(config.model, "test-model");
55
- assert.equal(config.maxSteps, 50);
56
- assert.equal(config.maxTokens, 4096);
69
+ assert.equal(config.model, "google/gemini-3-flash-preview");
70
+ assert.equal(config.openAiBaseUrl, "https://openrouter.ai/api/v1");
71
+ assert.equal(config.maxSteps, 42);
72
+ assert.equal(config.maxContextTokens, 12345);
73
+ }
74
+ finally {
75
+ await rm(tmpRoot, { recursive: true, force: true });
76
+ }
77
+ });
78
+ test("buildConfig: home .env provides benchmark API keys", async () => {
79
+ const tmpRoot = await mkdtemp(path.join(tmpdir(), "bench-config-home-env-"));
80
+ const configPath = path.join(tmpRoot, "benchmarks", "benchmark.config.json");
81
+ const homeEnvPath = path.join(tmpRoot, "home.env");
82
+ await mkdir(path.dirname(configPath), { recursive: true });
83
+ await writeFile(configPath, JSON.stringify({
84
+ modelProvider: "openai-compatible",
85
+ model: "google/gemini-3-flash-preview",
86
+ openAiBaseUrl: "https://openrouter.ai/api/v1",
87
+ }));
88
+ await writeFile(homeEnvPath, "OPENROUTER_API_KEY=test-openrouter-key\n");
89
+ try {
90
+ const config = buildConfig({
91
+ repoRoot: tmpRoot,
92
+ env: {},
93
+ homeEnvPath,
94
+ configPath,
95
+ });
96
+ assert.equal(config.openAiApiKey, "test-openrouter-key");
97
+ }
98
+ finally {
99
+ await rm(tmpRoot, { recursive: true, force: true });
100
+ }
101
+ });
102
+ test("buildConfig: benchmark config wins over home .env for non-secret settings", async () => {
103
+ const tmpRoot = await mkdtemp(path.join(tmpdir(), "bench-config-env-"));
104
+ const configPath = path.join(tmpRoot, "benchmarks", "benchmark.config.json");
105
+ const homeEnvPath = path.join(tmpRoot, "home.env");
106
+ await mkdir(path.dirname(configPath), { recursive: true });
107
+ await writeFile(configPath, JSON.stringify({
108
+ modelProvider: "openai-compatible",
109
+ model: "google/gemini-3-flash-preview",
110
+ openAiBaseUrl: "https://openrouter.ai/api/v1",
111
+ maxContextTokens: 32000,
112
+ }));
113
+ await writeFile(homeEnvPath, "MODEL=google/gemini-home\nOPENAI_BASE_URL=https://example.invalid/v1\nOPENROUTER_API_KEY=home-openrouter-key\n");
114
+ try {
115
+ const config = buildConfig({
116
+ repoRoot: tmpRoot,
117
+ env: {},
118
+ homeEnvPath,
119
+ configPath,
120
+ });
121
+ assert.equal(config.modelProvider, "openai-compatible");
122
+ assert.equal(config.model, "google/gemini-3-flash-preview");
123
+ assert.equal(config.openAiBaseUrl, "https://openrouter.ai/api/v1");
124
+ assert.equal(config.openAiApiKey, "home-openrouter-key");
57
125
  assert.equal(config.maxContextTokens, 32000);
58
- assert.equal(config.confirmDestructive, false);
59
126
  }
60
127
  finally {
61
- if (originalProvider !== undefined)
62
- process.env.MODEL_PROVIDER = originalProvider;
63
- if (originalModel !== undefined)
64
- process.env.MODEL = originalModel;
128
+ await rm(tmpRoot, { recursive: true, force: true });
65
129
  }
66
130
  });
67
- test("buildConfig: reads MODEL_PROVIDER and MODEL from env", () => {
68
- const originalProvider = process.env.MODEL_PROVIDER;
69
- const originalModel = process.env.MODEL;
70
- process.env.MODEL_PROVIDER = "anthropic";
71
- process.env.MODEL = "claude-test";
131
+ test("buildConfig: shell env overrides benchmark config and home env", async () => {
132
+ const tmpRoot = await mkdtemp(path.join(tmpdir(), "bench-config-shell-env-"));
133
+ const configPath = path.join(tmpRoot, "benchmarks", "benchmark.config.json");
134
+ const homeEnvPath = path.join(tmpRoot, "home.env");
135
+ await mkdir(path.dirname(configPath), { recursive: true });
136
+ await writeFile(configPath, JSON.stringify({
137
+ modelProvider: "openai-compatible",
138
+ model: "google/gemini-3-flash-preview",
139
+ openAiBaseUrl: "https://openrouter.ai/api/v1",
140
+ maxContextTokens: 32000,
141
+ }));
142
+ await writeFile(homeEnvPath, "OPENROUTER_API_KEY=home-openrouter-key\n");
72
143
  try {
73
- const config = buildConfig();
144
+ const config = buildConfig({
145
+ repoRoot: tmpRoot,
146
+ env: {
147
+ MODEL_PROVIDER: "anthropic",
148
+ MODEL: "claude-test",
149
+ OPENAI_BASE_URL: "https://override.example/v1",
150
+ MAX_CONTEXT_TOKENS: "64000",
151
+ },
152
+ homeEnvPath,
153
+ configPath,
154
+ });
74
155
  assert.equal(config.modelProvider, "anthropic");
75
156
  assert.equal(config.model, "claude-test");
157
+ assert.equal(config.openAiBaseUrl, "https://override.example/v1");
158
+ assert.equal(config.maxContextTokens, 64000);
76
159
  }
77
160
  finally {
78
- if (originalProvider !== undefined) {
79
- process.env.MODEL_PROVIDER = originalProvider;
80
- }
81
- else {
82
- delete process.env.MODEL_PROVIDER;
83
- }
84
- if (originalModel !== undefined) {
85
- process.env.MODEL = originalModel;
86
- }
87
- else {
88
- delete process.env.MODEL;
89
- }
161
+ await rm(tmpRoot, { recursive: true, force: true });
90
162
  }
91
163
  });
164
+ test("buildConfig: falls back to hardcoded defaults when config file is missing", () => {
165
+ const config = buildConfig({
166
+ repoRoot: "/tmp/bench-missing-config",
167
+ env: {},
168
+ homeEnvPath: "/tmp/bench-missing-home-env",
169
+ configPath: "/tmp/bench-missing-config/benchmarks/missing.json",
170
+ });
171
+ assert.equal(config.modelProvider, "openai-compatible");
172
+ assert.equal(config.model, "test-model");
173
+ assert.equal(config.openAiBaseUrl, "http://localhost:1234/v1");
174
+ assert.equal(config.maxSteps, 50);
175
+ });
92
176
  // ─── loadTasks ────────────────────────────────────────────────────
93
177
  let tmpDir;
94
178
  async function setupTempTasks() {
@@ -45,4 +45,6 @@ export class Employee {
45
45
  const result = await tool.execute({ pattern: "Employee" });
46
46
  assert.ok(result.includes("Employee (interface)"));
47
47
  assert.ok(result.includes("Employee (class)"));
48
+ assert.ok(result.includes("qualified: Employee#interface"));
49
+ assert.ok(result.includes("qualified: Employee#class"));
48
50
  });