@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.
- package/README.md +25 -47
- package/dist/scripts/run-benchmarks.js +73 -28
- package/dist/src/agent/config.js +51 -66
- package/dist/src/agent/editable-config.js +50 -58
- package/dist/src/agent/home-env.js +74 -0
- package/dist/src/benchmark/runner.js +142 -59
- package/dist/src/cli/config-slash-command.js +15 -13
- package/dist/src/indexer/project-index.js +49 -13
- package/dist/src/serve/agent-bridge.js +99 -31
- package/dist/src/serve/mcp-server.js +70 -21
- package/dist/src/serve/server.js +198 -8
- package/dist/src/session/session-preview.js +14 -0
- package/dist/src/shared/graph-search.js +80 -0
- package/dist/src/shared/graph-selection.js +40 -0
- package/dist/src/shared/graph-symbols.js +82 -0
- package/dist/src/shared/symbol-resolution.js +33 -0
- package/dist/src/tools/find-path.js +15 -6
- package/dist/src/tools/find-references.js +7 -2
- package/dist/src/tools/get-dependencies.js +8 -3
- package/dist/src/tools/read-symbol.js +9 -3
- package/dist/src/tools/registry.js +4 -1
- package/dist/src/tools/search-code-map.js +18 -3
- package/dist/src/web/app.js +646 -87
- package/dist/src/web/index.html +68 -6
- package/dist/src/web/style.css +208 -1
- package/dist/tests/benchmark-harness.test.js +100 -0
- package/dist/tests/config-api.test.js +5 -5
- package/dist/tests/config-integration.test.js +130 -56
- package/dist/tests/config-slash-command.test.js +12 -11
- package/dist/tests/config.test.js +12 -4
- package/dist/tests/editable-config.test.js +15 -12
- package/dist/tests/file-tools.test.js +34 -1
- package/dist/tests/find-path.test.js +43 -2
- package/dist/tests/find-references.test.js +49 -0
- package/dist/tests/get-dependencies.test.js +23 -0
- package/dist/tests/graph-onboarding.test.js +10 -1
- package/dist/tests/graph-search.test.js +66 -0
- package/dist/tests/graph-selection.test.js +58 -0
- package/dist/tests/graph-symbols.test.js +45 -0
- package/dist/tests/home-env.test.js +56 -0
- package/dist/tests/indexer.test.js +6 -0
- package/dist/tests/read-symbol.test.js +35 -0
- package/dist/tests/request-tracker.test.js +15 -0
- package/dist/tests/run-benchmarks.test.js +117 -33
- package/dist/tests/search-code-map.test.js +2 -0
- package/dist/tests/serve.integration.test.js +338 -9
- package/dist/tests/session-preview.test.js +56 -0
- package/dist/tests/session-ui.test.js +4 -0
- package/dist/tests/settings-ui.test.js +18 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +2 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts +3 -0
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts +3 -0
- package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js +4 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts +11 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js +4 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/search.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js +16 -8
- package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.js +19 -2
- package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
- 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("
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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, "
|
|
55
|
-
assert.equal(config.
|
|
56
|
-
assert.equal(config.
|
|
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
|
-
|
|
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:
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
});
|