@sean.holung/minicode 0.3.4 → 0.3.5
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 +5 -3
- package/dist/scripts/run-benchmarks.js +73 -28
- package/dist/src/benchmark/runner.js +142 -59
- package/dist/src/indexer/project-index.js +49 -13
- package/dist/src/serve/agent-bridge.js +12 -3
- package/dist/src/serve/mcp-server.js +70 -21
- package/dist/src/serve/server.js +37 -4
- 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 +154 -33
- package/dist/tests/benchmark-harness.test.js +100 -0
- 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-symbols.test.js +45 -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 +109 -3
- package/dist/tests/session-ui.test.js +2 -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
package/dist/src/web/app.js
CHANGED
|
@@ -1443,6 +1443,86 @@ function buildStylesheet() {
|
|
|
1443
1443
|
return styles;
|
|
1444
1444
|
}
|
|
1445
1445
|
|
|
1446
|
+
// src/shared/graph-symbols.ts
|
|
1447
|
+
function dedupe(values) {
|
|
1448
|
+
return [...new Set(values.filter((value) => value.length > 0))];
|
|
1449
|
+
}
|
|
1450
|
+
function stripCollisionSuffix(value) {
|
|
1451
|
+
const hashIndex = value.indexOf("#");
|
|
1452
|
+
return hashIndex >= 0 ? value.slice(0, hashIndex) : value;
|
|
1453
|
+
}
|
|
1454
|
+
function stripDisplayKindSuffix(value) {
|
|
1455
|
+
return value.replace(/\s+\([^()]+\)$/, "");
|
|
1456
|
+
}
|
|
1457
|
+
function getGraphNodeId(node, fallbackId = "") {
|
|
1458
|
+
return node.qualifiedName || node.id || fallbackId || node.name || "";
|
|
1459
|
+
}
|
|
1460
|
+
function getGraphNodeLabel(node, fallbackId = "") {
|
|
1461
|
+
const label = node.name?.trim();
|
|
1462
|
+
if (label && label.length > 0) {
|
|
1463
|
+
return label;
|
|
1464
|
+
}
|
|
1465
|
+
const id = getGraphNodeId(node, fallbackId);
|
|
1466
|
+
return id.split(".").pop() || id;
|
|
1467
|
+
}
|
|
1468
|
+
function getGraphNodeAliases(node, fallbackId = "") {
|
|
1469
|
+
const id = getGraphNodeId(node, fallbackId);
|
|
1470
|
+
const label = getGraphNodeLabel(node, fallbackId);
|
|
1471
|
+
const shortId = id.split(".").pop() || id;
|
|
1472
|
+
return dedupe([
|
|
1473
|
+
id,
|
|
1474
|
+
node.id ?? "",
|
|
1475
|
+
node.qualifiedName ?? "",
|
|
1476
|
+
label,
|
|
1477
|
+
stripDisplayKindSuffix(label),
|
|
1478
|
+
shortId,
|
|
1479
|
+
stripCollisionSuffix(shortId),
|
|
1480
|
+
stripCollisionSuffix(id)
|
|
1481
|
+
]);
|
|
1482
|
+
}
|
|
1483
|
+
function compareLabels(a, b2) {
|
|
1484
|
+
return a.localeCompare(b2, void 0, { sensitivity: "base" });
|
|
1485
|
+
}
|
|
1486
|
+
function compareGraphNodeIds(a, b2, nodes) {
|
|
1487
|
+
const nodeA = nodes.get(a);
|
|
1488
|
+
const nodeB = nodes.get(b2);
|
|
1489
|
+
const exportedA = nodeA ? Number(!!nodeA.exported) : 0;
|
|
1490
|
+
const exportedB = nodeB ? Number(!!nodeB.exported) : 0;
|
|
1491
|
+
if (exportedA !== exportedB) {
|
|
1492
|
+
return exportedB - exportedA;
|
|
1493
|
+
}
|
|
1494
|
+
const labelA = getGraphNodeLabel(nodeA ?? {}, a);
|
|
1495
|
+
const labelB = getGraphNodeLabel(nodeB ?? {}, b2);
|
|
1496
|
+
const labelComparison = compareLabels(labelA, labelB);
|
|
1497
|
+
if (labelComparison !== 0) {
|
|
1498
|
+
return labelComparison;
|
|
1499
|
+
}
|
|
1500
|
+
return compareLabels(a, b2);
|
|
1501
|
+
}
|
|
1502
|
+
function matchesGraphNodeQuery(query, node, fallbackId = "") {
|
|
1503
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
1504
|
+
if (normalizedQuery.length === 0) {
|
|
1505
|
+
return false;
|
|
1506
|
+
}
|
|
1507
|
+
return getGraphNodeAliases(node, fallbackId).some(
|
|
1508
|
+
(alias) => alias.toLowerCase().includes(normalizedQuery)
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
function resolveGraphNodeIds(nodes, symbolName) {
|
|
1512
|
+
const query = symbolName.trim();
|
|
1513
|
+
if (query.length === 0) {
|
|
1514
|
+
return [];
|
|
1515
|
+
}
|
|
1516
|
+
if (nodes.has(query)) {
|
|
1517
|
+
return [query];
|
|
1518
|
+
}
|
|
1519
|
+
const exactMatches = [...nodes.entries()].filter(([id, node]) => getGraphNodeAliases(node, id).includes(query)).map(([id]) => id).sort((a, b2) => compareGraphNodeIds(a, b2, nodes));
|
|
1520
|
+
if (exactMatches.length > 0) {
|
|
1521
|
+
return exactMatches;
|
|
1522
|
+
}
|
|
1523
|
+
return [...nodes.entries()].filter(([id, node]) => matchesGraphNodeQuery(query, node, id)).map(([id]) => id).sort((a, b2) => compareGraphNodeIds(a, b2, nodes));
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1446
1526
|
// src/web/graph.ts
|
|
1447
1527
|
var cy = null;
|
|
1448
1528
|
var graphNodes = /* @__PURE__ */ new Map();
|
|
@@ -1537,7 +1617,7 @@ async function initGraph() {
|
|
|
1537
1617
|
}
|
|
1538
1618
|
function highlightAgentActivity(symbolName) {
|
|
1539
1619
|
if (!cy) return;
|
|
1540
|
-
void
|
|
1620
|
+
void focusResolvedSymbolsInGraph(symbolName, {
|
|
1541
1621
|
maxDegrees: 0,
|
|
1542
1622
|
pulse: true,
|
|
1543
1623
|
pulseDuration: 2e3,
|
|
@@ -1611,6 +1691,16 @@ function renderNodeNeighborhoodAndLayout(symbolId, maxDegrees = 1) {
|
|
|
1611
1691
|
}
|
|
1612
1692
|
}
|
|
1613
1693
|
async function focusSymbolInGraph(symbolId, options = {}) {
|
|
1694
|
+
await focusSymbolsInGraph([symbolId], options);
|
|
1695
|
+
}
|
|
1696
|
+
async function focusResolvedSymbolsInGraph(symbolName, options = {}) {
|
|
1697
|
+
const matches = resolveGraphNodeIds(graphNodes, symbolName);
|
|
1698
|
+
if (matches.length === 0) {
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
await focusSymbolsInGraph(matches, options);
|
|
1702
|
+
}
|
|
1703
|
+
async function focusSymbolsInGraph(symbolIds, options = {}) {
|
|
1614
1704
|
if (!cy) return;
|
|
1615
1705
|
const {
|
|
1616
1706
|
maxDegrees = 0,
|
|
@@ -1621,21 +1711,38 @@ async function focusSymbolInGraph(symbolId, options = {}) {
|
|
|
1621
1711
|
flashDuration = 1200,
|
|
1622
1712
|
openDetail = false
|
|
1623
1713
|
} = options;
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1714
|
+
const uniqueIds = [...new Set(symbolIds)];
|
|
1715
|
+
let addedNodes = false;
|
|
1716
|
+
for (const symbolId of uniqueIds) {
|
|
1717
|
+
const beforeNodeCount = cy.nodes().length;
|
|
1718
|
+
addNodeNeighborhood(symbolId, maxDegrees);
|
|
1719
|
+
if (cy.nodes().length > beforeNodeCount) {
|
|
1720
|
+
addedNodes = true;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
connectExistingNodes();
|
|
1724
|
+
refreshAnalysisGraphState();
|
|
1725
|
+
if (addedNodes) {
|
|
1726
|
+
runLayout();
|
|
1727
|
+
}
|
|
1728
|
+
const nodes = uniqueIds.map((symbolId) => findNode(symbolId)).filter((node) => node !== null);
|
|
1729
|
+
if (nodes.length === 0) return;
|
|
1730
|
+
const primaryNode = nodes[0];
|
|
1731
|
+
if (animate && primaryNode) {
|
|
1732
|
+
cy.animate({ center: { eles: primaryNode }, zoom }, { duration: 300 });
|
|
1635
1733
|
}
|
|
1636
|
-
|
|
1734
|
+
for (const node of nodes) {
|
|
1735
|
+
if (pulse) {
|
|
1736
|
+
node.addClass("agent-pulse");
|
|
1737
|
+
setTimeout(() => node.removeClass("agent-pulse"), pulseDuration);
|
|
1738
|
+
} else {
|
|
1739
|
+
node.flashClass("highlighted", flashDuration);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
if (openDetail && primaryNode && uniqueIds.length === 1) {
|
|
1637
1743
|
const detailEl = document.getElementById("symbol-detail");
|
|
1638
1744
|
if (detailEl) {
|
|
1745
|
+
const node = primaryNode;
|
|
1639
1746
|
await showDetail(node, detailEl);
|
|
1640
1747
|
}
|
|
1641
1748
|
}
|
|
@@ -1650,7 +1757,7 @@ function addNodeToGraph(id) {
|
|
|
1650
1757
|
const nodeData = graphNodes.get(id);
|
|
1651
1758
|
if (!nodeData) return;
|
|
1652
1759
|
const kind = (nodeData.kind || "function").toLowerCase();
|
|
1653
|
-
const name = nodeData
|
|
1760
|
+
const name = getGraphNodeLabel(nodeData, id);
|
|
1654
1761
|
const file = nodeData.filePath || nodeData.file || "";
|
|
1655
1762
|
cy.add({
|
|
1656
1763
|
data: {
|
|
@@ -1691,11 +1798,10 @@ function findNode(name) {
|
|
|
1691
1798
|
if (!cy) return null;
|
|
1692
1799
|
const node = cy.getElementById(name);
|
|
1693
1800
|
if (node.length > 0) return node;
|
|
1694
|
-
const
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
});
|
|
1801
|
+
const matchingIds = new Set(resolveGraphNodeIds(graphNodes, name));
|
|
1802
|
+
const match = cy.nodes().filter(
|
|
1803
|
+
(n) => matchingIds.has(n.data("qualifiedName") || n.data("id") || "")
|
|
1804
|
+
);
|
|
1699
1805
|
return match.length > 0 ? match : null;
|
|
1700
1806
|
}
|
|
1701
1807
|
function getAnalysisPanelEls() {
|
|
@@ -2335,16 +2441,7 @@ function setupToolbar() {
|
|
|
2335
2441
|
dropdown.className = "search-dropdown hidden";
|
|
2336
2442
|
searchInput.parentNode.style.position = "relative";
|
|
2337
2443
|
searchInput.parentNode.appendChild(dropdown);
|
|
2338
|
-
const rankedSymbols = allSymbolNames.slice().sort((a, b2) =>
|
|
2339
|
-
const nodeA = graphNodes.get(a);
|
|
2340
|
-
const nodeB = graphNodes.get(b2);
|
|
2341
|
-
const expA = nodeA ? !!nodeA.exported : false;
|
|
2342
|
-
const expB = nodeB ? !!nodeB.exported : false;
|
|
2343
|
-
if (expA !== expB) return expA ? -1 : 1;
|
|
2344
|
-
const nameA = (a.split(".").pop() || "").toLowerCase();
|
|
2345
|
-
const nameB = (b2.split(".").pop() || "").toLowerCase();
|
|
2346
|
-
return nameA.localeCompare(nameB);
|
|
2347
|
-
});
|
|
2444
|
+
const rankedSymbols = allSymbolNames.slice().sort((a, b2) => compareGraphNodeIds(a, b2, graphNodes));
|
|
2348
2445
|
function showDropdownResults(matches) {
|
|
2349
2446
|
if (matches.length === 0) {
|
|
2350
2447
|
dropdown.classList.add("hidden");
|
|
@@ -2353,7 +2450,7 @@ function setupToolbar() {
|
|
|
2353
2450
|
dropdown.innerHTML = matches.map((name) => {
|
|
2354
2451
|
const node = graphNodes.get(name);
|
|
2355
2452
|
const kind = node ? (node.kind || "").toLowerCase() : "";
|
|
2356
|
-
const shortName =
|
|
2453
|
+
const shortName = getGraphNodeLabel(node || {}, name);
|
|
2357
2454
|
const kindColor = KIND_COLORS[kind] ? KIND_COLORS[kind].border : "#565f89";
|
|
2358
2455
|
return `<div class="search-result" data-id="${escapeHtml(name)}">
|
|
2359
2456
|
<span class="search-result-name">${escapeHtml(shortName)}</span>
|
|
@@ -2390,8 +2487,7 @@ function setupToolbar() {
|
|
|
2390
2487
|
return;
|
|
2391
2488
|
}
|
|
2392
2489
|
const matches = rankedSymbols.filter((name) => {
|
|
2393
|
-
|
|
2394
|
-
return shortName.includes(query) || name.toLowerCase().includes(query);
|
|
2490
|
+
return matchesGraphNodeQuery(query, graphNodes.get(name) || {}, name);
|
|
2395
2491
|
}).slice(0, 15);
|
|
2396
2492
|
showDropdownResults(matches);
|
|
2397
2493
|
}, 150);
|
|
@@ -2438,6 +2534,20 @@ function setupToolbar() {
|
|
|
2438
2534
|
});
|
|
2439
2535
|
}
|
|
2440
2536
|
|
|
2537
|
+
// src/web/request-tracker.ts
|
|
2538
|
+
function createLatestRequestTracker() {
|
|
2539
|
+
let latestToken = 0;
|
|
2540
|
+
return {
|
|
2541
|
+
begin() {
|
|
2542
|
+
latestToken += 1;
|
|
2543
|
+
return latestToken;
|
|
2544
|
+
},
|
|
2545
|
+
isCurrent(token) {
|
|
2546
|
+
return token === latestToken;
|
|
2547
|
+
}
|
|
2548
|
+
};
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2441
2551
|
// src/web/app.ts
|
|
2442
2552
|
var messagesEl = document.getElementById("messages");
|
|
2443
2553
|
var chatForm = document.getElementById("chat-form");
|
|
@@ -2473,6 +2583,7 @@ var assistantText = "";
|
|
|
2473
2583
|
var hadToolCalls = false;
|
|
2474
2584
|
var settingsPayload = null;
|
|
2475
2585
|
var activeSavedSession = null;
|
|
2586
|
+
var sessionRefreshTracker = createLatestRequestTracker();
|
|
2476
2587
|
var TOOL_RESULT_MAX = 500;
|
|
2477
2588
|
function connect() {
|
|
2478
2589
|
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
@@ -3023,6 +3134,7 @@ saveBtn.addEventListener("click", async () => {
|
|
|
3023
3134
|
const label = requestedLabel || activeSavedSession?.label || void 0;
|
|
3024
3135
|
const isUpdatingCurrentSession = !!activeSavedSession && (requestedLabel.length === 0 || requestedLabel === activeSavedSession.label);
|
|
3025
3136
|
try {
|
|
3137
|
+
saveBtn.setAttribute("disabled", "true");
|
|
3026
3138
|
const res = await fetch("/api/sessions/save", {
|
|
3027
3139
|
method: "POST",
|
|
3028
3140
|
headers: { "Content-Type": "application/json" },
|
|
@@ -3035,9 +3147,11 @@ saveBtn.addEventListener("click", async () => {
|
|
|
3035
3147
|
`${isUpdatingCurrentSession ? "Session updated" : "Session saved"}: "${data.label}"`,
|
|
3036
3148
|
"thinking"
|
|
3037
3149
|
);
|
|
3038
|
-
|
|
3150
|
+
await refreshSessionList();
|
|
3039
3151
|
}
|
|
3040
3152
|
} catch {
|
|
3153
|
+
} finally {
|
|
3154
|
+
saveBtn.removeAttribute("disabled");
|
|
3041
3155
|
}
|
|
3042
3156
|
});
|
|
3043
3157
|
saveLabelInput.addEventListener("keydown", (e) => {
|
|
@@ -3047,9 +3161,13 @@ saveLabelInput.addEventListener("keydown", (e) => {
|
|
|
3047
3161
|
}
|
|
3048
3162
|
});
|
|
3049
3163
|
async function refreshSessionList() {
|
|
3164
|
+
const requestToken = sessionRefreshTracker.begin();
|
|
3050
3165
|
try {
|
|
3051
3166
|
const res = await fetch("/api/sessions");
|
|
3052
3167
|
const data = await res.json();
|
|
3168
|
+
if (!sessionRefreshTracker.isCurrent(requestToken)) {
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
3053
3171
|
const sessions = data.sessions;
|
|
3054
3172
|
activeSavedSession = sessions.find((session) => session.id === data.currentSessionId) ?? null;
|
|
3055
3173
|
if (activeSavedSession) {
|
|
@@ -3075,6 +3193,9 @@ async function refreshSessionList() {
|
|
|
3075
3193
|
sessionList.appendChild(el);
|
|
3076
3194
|
}
|
|
3077
3195
|
} catch {
|
|
3196
|
+
if (!sessionRefreshTracker.isCurrent(requestToken)) {
|
|
3197
|
+
return;
|
|
3198
|
+
}
|
|
3078
3199
|
activeSavedSession = null;
|
|
3079
3200
|
sessionUpdateRow.classList.add("hidden");
|
|
3080
3201
|
sessionList.innerHTML = '<div class="dropdown-empty">Failed to load sessions</div>';
|
|
@@ -40,6 +40,8 @@ function makeTrace(overrides = {}) {
|
|
|
40
40
|
model: "test-model",
|
|
41
41
|
variant: "baseline",
|
|
42
42
|
commitSha: "abc123",
|
|
43
|
+
sourceWorkspaceRoot: "/workspace/source",
|
|
44
|
+
workspaceRoot: "/tmp/minicode-benchmark/task",
|
|
43
45
|
response: "Found foo in src/foo.ts at line 10.",
|
|
44
46
|
toolCalls: [
|
|
45
47
|
{ name: "search", input: { query: "foo" }, output: "src/foo.ts:10", durationMs: 50 },
|
|
@@ -141,6 +143,24 @@ test("loadBenchmarkTask loads a single task by id", async () => {
|
|
|
141
143
|
await rm(tmpDir, { recursive: true });
|
|
142
144
|
}
|
|
143
145
|
});
|
|
146
|
+
test("loadBenchmarkTask preserves workspaceRoot when provided", async () => {
|
|
147
|
+
const tmpDir = await mkdtemp(path.join(tmpdir(), "bench-workspace-root-"));
|
|
148
|
+
await mkdir(path.join(tmpDir, "navigation", "fixture-task"), { recursive: true });
|
|
149
|
+
await writeFile(path.join(tmpDir, "navigation", "fixture-task", "task.json"), JSON.stringify({
|
|
150
|
+
title: "Fixture task",
|
|
151
|
+
prompt: "Use a fixture workspace",
|
|
152
|
+
workspaceRoot: "test-programs/benchmark-index",
|
|
153
|
+
rubric: {},
|
|
154
|
+
}));
|
|
155
|
+
try {
|
|
156
|
+
const task = await loadBenchmarkTask(tmpDir, "navigation/fixture-task");
|
|
157
|
+
assert.ok(task);
|
|
158
|
+
assert.equal(task.workspaceRoot, "test-programs/benchmark-index");
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
144
164
|
test("loadBenchmarkTask returns undefined for missing task", async () => {
|
|
145
165
|
const tmpDir = await createTempTaskDir();
|
|
146
166
|
try {
|
|
@@ -413,6 +433,86 @@ test("runBenchmarkTask tracks symbols from structural tools", async () => {
|
|
|
413
433
|
});
|
|
414
434
|
assert.ok(trace.symbolsQueried.includes("CodingAgent"));
|
|
415
435
|
});
|
|
436
|
+
test("runBenchmarkTask resolves task workspace overrides and isolates the run", async () => {
|
|
437
|
+
const repoRoot = await mkdtemp(path.join(tmpdir(), "bench-repo-root-"));
|
|
438
|
+
const sourceWorkspace = path.join(repoRoot, "fixtures", "sample-project");
|
|
439
|
+
await mkdir(path.join(sourceWorkspace, "src"), { recursive: true });
|
|
440
|
+
await writeFile(path.join(sourceWorkspace, "src", "index.ts"), "export const answer = 42;\n");
|
|
441
|
+
const task = {
|
|
442
|
+
id: "navigation/workspace-root",
|
|
443
|
+
title: "Workspace root test",
|
|
444
|
+
category: "navigation",
|
|
445
|
+
prompt: "Inspect the fixture workspace",
|
|
446
|
+
workspaceRoot: "fixtures/sample-project",
|
|
447
|
+
rubric: {},
|
|
448
|
+
};
|
|
449
|
+
try {
|
|
450
|
+
const config = createTestAgentConfig(repoRoot);
|
|
451
|
+
const trace = await runBenchmarkTask(task, {
|
|
452
|
+
modelClient: new MockModelClient("done"),
|
|
453
|
+
config,
|
|
454
|
+
tools: [],
|
|
455
|
+
variant: "test",
|
|
456
|
+
repoRoot,
|
|
457
|
+
});
|
|
458
|
+
assert.equal(trace.sourceWorkspaceRoot, sourceWorkspace);
|
|
459
|
+
assert.notEqual(trace.workspaceRoot, sourceWorkspace);
|
|
460
|
+
assert.ok(trace.workspaceRoot.includes("minicode-benchmark-"));
|
|
461
|
+
}
|
|
462
|
+
finally {
|
|
463
|
+
await rm(repoRoot, { recursive: true, force: true });
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
test("runBenchmarkTask uses project index metadata to count read_symbol as a file read", async () => {
|
|
467
|
+
const workspaceRoot = path.resolve(import.meta.dirname, "..");
|
|
468
|
+
const task = {
|
|
469
|
+
id: "navigation/structural-file-tracking",
|
|
470
|
+
title: "Structural file tracking",
|
|
471
|
+
category: "navigation",
|
|
472
|
+
prompt: "Read the CodingAgent symbol",
|
|
473
|
+
rubric: {},
|
|
474
|
+
};
|
|
475
|
+
let callCount = 0;
|
|
476
|
+
const mockClient = {
|
|
477
|
+
async chat(params) {
|
|
478
|
+
void params;
|
|
479
|
+
callCount += 1;
|
|
480
|
+
if (callCount === 1) {
|
|
481
|
+
return {
|
|
482
|
+
text: "Looking up symbol",
|
|
483
|
+
toolCalls: [{ id: "t1", name: "read_symbol", input: { name: "CodingAgent" } }],
|
|
484
|
+
stopReason: "tool_use",
|
|
485
|
+
usage: { inputTokens: 100, outputTokens: 50 },
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
text: "Found CodingAgent",
|
|
490
|
+
toolCalls: [],
|
|
491
|
+
stopReason: "end_turn",
|
|
492
|
+
usage: { inputTokens: 100, outputTokens: 50 },
|
|
493
|
+
};
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
const config = createTestAgentConfig(workspaceRoot);
|
|
497
|
+
const trace = await runBenchmarkTask(task, {
|
|
498
|
+
modelClient: mockClient,
|
|
499
|
+
config,
|
|
500
|
+
variant: "v1",
|
|
501
|
+
isolateWorkspace: false,
|
|
502
|
+
createToolset: async (taskConfig) => {
|
|
503
|
+
const { buildProjectIndex } = await import("../src/indexer/project-index.js");
|
|
504
|
+
const { createToolRegistry } = await import("../src/tools/registry.js");
|
|
505
|
+
const projectIndex = await buildProjectIndex(taskConfig.workspaceRoot);
|
|
506
|
+
const toolRegistry = createToolRegistry(taskConfig, projectIndex);
|
|
507
|
+
return {
|
|
508
|
+
tools: toolRegistry.getDefinitions(),
|
|
509
|
+
projectIndex,
|
|
510
|
+
};
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
assert.ok(trace.symbolsQueried.includes("CodingAgent"));
|
|
514
|
+
assert.ok(trace.filesRead.some((file) => file.endsWith("packages/agent-sdk/src/agent/agent.ts")), "read_symbol should count the owning file as read");
|
|
515
|
+
});
|
|
416
516
|
// ─── Reporter Tests ────────────────────────────────────────────
|
|
417
517
|
test("buildReport generates correct summary", () => {
|
|
418
518
|
const tasks = [
|
|
@@ -3,7 +3,7 @@ import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { test } from "node:test";
|
|
6
|
-
import { createEditFileTool, createReadFileTool } from "@minicode/agent-sdk";
|
|
6
|
+
import { createEditFileTool, createReadFileTool, createRunCommandTool, createWriteFileTool } from "@minicode/agent-sdk";
|
|
7
7
|
import { buildProjectIndex } from "../src/indexer/project-index.js";
|
|
8
8
|
import { createTestAgentConfig } from "./test-utils.js";
|
|
9
9
|
async function createTempWorkspace() {
|
|
@@ -58,6 +58,39 @@ test("edit_file triggers reindex when projectIndex provided", async () => {
|
|
|
58
58
|
const after = index.getSymbol("add");
|
|
59
59
|
assert.ok(after?.signature.includes("c?: number"), "index should reflect edit");
|
|
60
60
|
});
|
|
61
|
+
test("write_file triggers reindex when projectIndex provided", async () => {
|
|
62
|
+
const workspaceRoot = await createTempWorkspace();
|
|
63
|
+
const index = await buildProjectIndex(workspaceRoot);
|
|
64
|
+
const writeTool = createWriteFileTool(createTestAgentConfig(workspaceRoot), { afterWrite: (relPath, content) => index.reindexFile(relPath, content) });
|
|
65
|
+
await writeTool.execute({
|
|
66
|
+
path: "src/util.ts",
|
|
67
|
+
content: `export function add(a: number, b: number): number {\n return a + b;\n}\n`,
|
|
68
|
+
});
|
|
69
|
+
const added = index.getSymbol("add");
|
|
70
|
+
assert.ok(added?.signature.includes("a: number, b: number"), "index should reflect newly written file");
|
|
71
|
+
});
|
|
72
|
+
test("run_command refreshes index after shell-created file changes", async () => {
|
|
73
|
+
const workspaceRoot = await createTempWorkspace();
|
|
74
|
+
const index = await buildProjectIndex(workspaceRoot);
|
|
75
|
+
const runTool = createRunCommandTool(createTestAgentConfig(workspaceRoot), { afterCommand: async () => index.refreshFromWorkspace() });
|
|
76
|
+
await runTool.execute({
|
|
77
|
+
command: "mkdir -p src && cat <<'EOF' > src/util.ts\nexport function add(a: number, b: number): number {\n return a + b;\n}\nEOF",
|
|
78
|
+
});
|
|
79
|
+
const added = index.getSymbol("add");
|
|
80
|
+
assert.ok(added?.signature.includes("a: number, b: number"), "index should reflect shell-created file");
|
|
81
|
+
});
|
|
82
|
+
test("run_command refresh removes deleted files from the index", async () => {
|
|
83
|
+
const workspaceRoot = await createTempWorkspace();
|
|
84
|
+
const { mkdir } = await import("node:fs/promises");
|
|
85
|
+
const filePath = path.join(workspaceRoot, "src", "util.ts");
|
|
86
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
87
|
+
await writeFile(filePath, `export function add(a: number, b: number): number {\n return a + b;\n}\n`, "utf8");
|
|
88
|
+
const index = await buildProjectIndex(workspaceRoot);
|
|
89
|
+
assert.ok(index.getSymbol("add"));
|
|
90
|
+
const runTool = createRunCommandTool(createTestAgentConfig(workspaceRoot), { afterCommand: async () => index.refreshFromWorkspace() });
|
|
91
|
+
await runTool.execute({ command: "rm src/util.ts" });
|
|
92
|
+
assert.equal(index.getSymbol("add"), undefined, "deleted file should be removed from the index");
|
|
93
|
+
});
|
|
61
94
|
test("read_file supports negative offset and line limits", async () => {
|
|
62
95
|
const workspaceRoot = await createTempWorkspace();
|
|
63
96
|
const filePath = path.join(workspaceRoot, "lines.txt");
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { test } from "node:test";
|
|
4
|
-
import {
|
|
4
|
+
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { buildProjectIndex, createProjectIndex } from "../src/indexer/project-index.js";
|
|
5
7
|
import { createFindPathTool } from "../src/tools/find-path.js";
|
|
6
8
|
function makeSymbol(name, kind = "function") {
|
|
7
9
|
return {
|
|
@@ -169,7 +171,6 @@ test("find_path tool reports no path when symbols are disconnected", async () =>
|
|
|
169
171
|
assert.ok(result.includes("No path found"));
|
|
170
172
|
});
|
|
171
173
|
test("find_path tool works with real project index", async () => {
|
|
172
|
-
const { buildProjectIndex } = await import("../src/indexer/project-index.js");
|
|
173
174
|
const root = path.resolve(import.meta.dirname, "..");
|
|
174
175
|
const projectIndex = await buildProjectIndex(root);
|
|
175
176
|
const tool = createFindPathTool(projectIndex);
|
|
@@ -181,3 +182,43 @@ test("find_path tool works with real project index", async () => {
|
|
|
181
182
|
assert.ok(result.includes("# Path from createModelClient to AgentConfig"));
|
|
182
183
|
assert.ok(result.includes("symbols"));
|
|
183
184
|
});
|
|
185
|
+
test("find_path returns disambiguation list for ambiguous bare target symbols", async () => {
|
|
186
|
+
const workspaceRoot = await mkdtemp(path.join(tmpdir(), "minicode-find-path-collisions-"));
|
|
187
|
+
await writeFile(path.join(workspaceRoot, "sample.ts"), `export type Review = { id: string };
|
|
188
|
+
|
|
189
|
+
export class Review {
|
|
190
|
+
constructor(public id: string) {}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function createReview(id: string) {
|
|
194
|
+
return new Review(id);
|
|
195
|
+
}
|
|
196
|
+
`, "utf8");
|
|
197
|
+
const projectIndex = await buildProjectIndex(workspaceRoot);
|
|
198
|
+
const tool = createFindPathTool(projectIndex);
|
|
199
|
+
const result = await tool.execute({ from: "createReview", to: "Review" });
|
|
200
|
+
assert.ok(result.includes('Symbol "Review" is ambiguous'));
|
|
201
|
+
assert.ok(result.includes("Review (type)"));
|
|
202
|
+
assert.ok(result.includes("Review (class)"));
|
|
203
|
+
assert.ok(result.includes("qualified: Review#type"));
|
|
204
|
+
assert.ok(result.includes("qualified: Review#class"));
|
|
205
|
+
});
|
|
206
|
+
test("find_path accepts qualified names for colliding symbols", async () => {
|
|
207
|
+
const workspaceRoot = await mkdtemp(path.join(tmpdir(), "minicode-find-path-qualified-"));
|
|
208
|
+
await writeFile(path.join(workspaceRoot, "sample.ts"), `export type Review = { id: string };
|
|
209
|
+
|
|
210
|
+
export class Review {
|
|
211
|
+
constructor(public id: string) {}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function createReview(id: string) {
|
|
215
|
+
return new Review(id);
|
|
216
|
+
}
|
|
217
|
+
`, "utf8");
|
|
218
|
+
const projectIndex = await buildProjectIndex(workspaceRoot);
|
|
219
|
+
const tool = createFindPathTool(projectIndex);
|
|
220
|
+
const result = await tool.execute({ from: "createReview", to: "Review#class" });
|
|
221
|
+
assert.ok(result.includes("# Path from createReview to Review (class)"));
|
|
222
|
+
assert.ok(result.includes("[function] createReview"));
|
|
223
|
+
assert.ok(result.includes("[class] Review (class)"));
|
|
224
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -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
|
+
});
|