@sean.holung/minicode 0.3.2 → 0.3.3

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 (107) hide show
  1. package/README.md +48 -43
  2. package/dist/scripts/run-benchmarks.js +147 -0
  3. package/dist/src/agent/config.js +149 -40
  4. package/dist/src/agent/editable-config.js +314 -0
  5. package/dist/src/analysis/structural-analysis.js +379 -0
  6. package/dist/src/benchmark/evaluator.js +79 -0
  7. package/dist/src/benchmark/index.js +4 -0
  8. package/dist/src/benchmark/reporter.js +177 -0
  9. package/dist/src/benchmark/runner.js +100 -0
  10. package/dist/src/benchmark/task-loader.js +78 -0
  11. package/dist/src/benchmark/types.js +5 -0
  12. package/dist/src/cli/args.js +10 -0
  13. package/dist/src/cli/config-slash-command.js +135 -0
  14. package/dist/src/cli/plugin-install.js +69 -0
  15. package/dist/src/index.js +76 -6
  16. package/dist/src/indexer/cache.js +6 -4
  17. package/dist/src/indexer/code-map.js +41 -13
  18. package/dist/src/indexer/plugins/typescript.js +70 -23
  19. package/dist/src/indexer/project-index.js +175 -36
  20. package/dist/src/indexer/symbol-names.js +92 -0
  21. package/dist/src/model-utils.js +18 -0
  22. package/dist/src/serve/agent-bridge.js +203 -24
  23. package/dist/src/serve/mcp-server.js +405 -0
  24. package/dist/src/serve/server.js +165 -10
  25. package/dist/src/serve/websocket.js +8 -0
  26. package/dist/src/shared/graph-styles.js +119 -0
  27. package/dist/src/tools/find-path.js +75 -0
  28. package/dist/src/tools/find-references.js +7 -2
  29. package/dist/src/tools/get-dependencies.js +3 -2
  30. package/dist/src/tools/read-symbol.js +12 -5
  31. package/dist/src/tools/registry.js +3 -1
  32. package/dist/src/tools/search-code-map.js +4 -2
  33. package/dist/src/ui/app.js +1 -1
  34. package/dist/src/ui/cli-ink.js +79 -4
  35. package/dist/src/ui/components/header-bar.js +6 -2
  36. package/dist/src/ui/state/ui-store.js +5 -0
  37. package/dist/src/web/app.js +1124 -176
  38. package/dist/src/web/index.html +113 -3
  39. package/dist/src/web/style.css +973 -55
  40. package/dist/tests/agent.test.js +31 -0
  41. package/dist/tests/analysis-helpers.test.js +89 -0
  42. package/dist/tests/analysis-ui.test.js +29 -0
  43. package/dist/tests/benchmark-harness.test.js +527 -0
  44. package/dist/tests/config-api.test.js +143 -0
  45. package/dist/tests/config-integration.test.js +751 -0
  46. package/dist/tests/config-slash-command.test.js +106 -0
  47. package/dist/tests/config.test.js +42 -1
  48. package/dist/tests/context-indicator.test.js +220 -0
  49. package/dist/tests/editable-config.test.js +109 -0
  50. package/dist/tests/find-path.test.js +183 -0
  51. package/dist/tests/focus-tracker.test.js +62 -0
  52. package/dist/tests/graph-onboarding.test.js +55 -0
  53. package/dist/tests/graph-styles.test.js +65 -0
  54. package/dist/tests/indexer.test.js +137 -0
  55. package/dist/tests/mcp-and-plugin.test.js +186 -0
  56. package/dist/tests/model-client-openai.test.js +29 -0
  57. package/dist/tests/model-selection.test.js +136 -0
  58. package/dist/tests/model-utils.test.js +22 -0
  59. package/dist/tests/reasoning-effort.test.js +264 -0
  60. package/dist/tests/run-benchmarks.test.js +161 -0
  61. package/dist/tests/search-code-map.test.js +18 -0
  62. package/dist/tests/serve.integration.test.js +218 -2
  63. package/dist/tests/session-ui.test.js +21 -0
  64. package/dist/tests/session.test.js +50 -0
  65. package/dist/tests/settings-ui.test.js +30 -0
  66. package/dist/tests/structural-analysis.test.js +218 -0
  67. package/node_modules/@minicode/agent-sdk/README.md +80 -51
  68. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts +16 -5
  69. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
  70. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +51 -33
  71. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
  72. package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts +14 -0
  73. package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts.map +1 -1
  74. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +3 -2
  75. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
  76. package/node_modules/@minicode/agent-sdk/dist/src/index.js +2 -0
  77. package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
  78. package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.d.ts +35 -0
  79. package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.d.ts.map +1 -0
  80. package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.js +64 -0
  81. package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.js.map +1 -0
  82. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts +7 -0
  83. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts.map +1 -1
  84. package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts +5 -1
  85. package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts.map +1 -1
  86. package/node_modules/@minicode/agent-sdk/dist/src/model/client.js +83 -11
  87. package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -1
  88. package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.d.ts +1 -0
  89. package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.d.ts.map +1 -1
  90. package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.js +8 -1
  91. package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.js.map +1 -1
  92. package/node_modules/@minicode/agent-sdk/dist/src/session/session.d.ts.map +1 -1
  93. package/node_modules/@minicode/agent-sdk/dist/src/session/session.js +4 -1
  94. package/node_modules/@minicode/agent-sdk/dist/src/session/session.js.map +1 -1
  95. package/node_modules/@minicode/agent-sdk/dist/tests/agent.test.js +3 -1
  96. package/node_modules/@minicode/agent-sdk/dist/tests/agent.test.js.map +1 -1
  97. package/node_modules/@minicode/agent-sdk/dist/tests/guardrails.test.js +8 -2
  98. package/node_modules/@minicode/agent-sdk/dist/tests/guardrails.test.js.map +1 -1
  99. package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
  100. package/package.json +9 -5
  101. package/plugin/.claude-plugin/plugin.json +12 -0
  102. package/plugin/.mcp.json +8 -0
  103. package/plugin/CLAUDE.md +26 -0
  104. package/plugin/skills/analyze/SKILL.md +12 -0
  105. package/plugin/skills/focus/SKILL.md +20 -0
  106. package/plugin/skills/graph/SKILL.md +13 -0
  107. package/plugin/skills/symbols/SKILL.md +13 -0
@@ -0,0 +1,136 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { createServer } from "node:http";
4
+ import { OpenAICompatibleModelClient } from "@minicode/agent-sdk";
5
+ import { createRequestHandler } from "../src/serve/server.js";
6
+ import { AgentBridge } from "../src/serve/agent-bridge.js";
7
+ import { createTestAgentConfig } from "./test-utils.js";
8
+ // ── OpenAI-compatible listModels ──
9
+ test("openai-compatible listModels fetches /models and parses response", async () => {
10
+ const fetchImpl = async (input) => {
11
+ assert.ok(String(input).endsWith("/models"));
12
+ return new Response(JSON.stringify({
13
+ data: [
14
+ { id: "model-a", name: "Model A" },
15
+ { id: "model-b" },
16
+ ],
17
+ }), { status: 200, headers: { "content-type": "application/json" } });
18
+ };
19
+ const client = new OpenAICompatibleModelClient({
20
+ baseUrl: "http://localhost:1234/v1",
21
+ fetchImpl,
22
+ });
23
+ const models = await client.listModels();
24
+ assert.equal(models.length, 2);
25
+ assert.equal(models[0].id, "model-a");
26
+ assert.equal(models[0].name, "Model A");
27
+ assert.equal(models[1].id, "model-b");
28
+ assert.equal(models[1].name, "model-b"); // falls back to id
29
+ });
30
+ test("openai-compatible listModels returns empty array on error", async () => {
31
+ const fetchImpl = async () => {
32
+ return new Response("Server error", { status: 500 });
33
+ };
34
+ const client = new OpenAICompatibleModelClient({
35
+ baseUrl: "http://localhost:1234/v1",
36
+ fetchImpl,
37
+ });
38
+ const models = await client.listModels();
39
+ assert.deepEqual(models, []);
40
+ });
41
+ test("openai-compatible listModels returns empty array on network failure", async () => {
42
+ const fetchImpl = async () => {
43
+ throw new Error("Connection refused");
44
+ };
45
+ const client = new OpenAICompatibleModelClient({
46
+ baseUrl: "http://localhost:1234/v1",
47
+ fetchImpl,
48
+ });
49
+ const models = await client.listModels();
50
+ assert.deepEqual(models, []);
51
+ });
52
+ // ── Serve API /api/models and /api/model ──
53
+ class MockBridgeForModels extends AgentBridge {
54
+ constructor() {
55
+ super(() => { }, false);
56
+ }
57
+ isBusy() {
58
+ return false;
59
+ }
60
+ getConfig() {
61
+ return createTestAgentConfig("/tmp/test-workspace");
62
+ }
63
+ async listModels() {
64
+ return [
65
+ { id: "model-z", name: "Zulu" },
66
+ { id: "model-a", name: "Alpha" },
67
+ { id: "model-m" },
68
+ ];
69
+ }
70
+ switchModel(modelId) {
71
+ this.getConfig().model = modelId;
72
+ }
73
+ async runTurn(message) {
74
+ return { text: `Echo: ${message}`, usage: { inputTokens: 1, outputTokens: 1 } };
75
+ }
76
+ async listSess() { return []; }
77
+ hasIndex() { return false; }
78
+ }
79
+ function startTestServer(bridge) {
80
+ return new Promise((resolve) => {
81
+ const handler = createRequestHandler(bridge);
82
+ const server = createServer(handler);
83
+ server.listen(0, "127.0.0.1", () => {
84
+ const addr = server.address();
85
+ const port = typeof addr === "string" ? 0 : addr.port;
86
+ resolve({ server, port });
87
+ });
88
+ });
89
+ }
90
+ test("GET /api/models returns model list and active model", async () => {
91
+ const bridge = new MockBridgeForModels();
92
+ const { server, port } = await startTestServer(bridge);
93
+ try {
94
+ const res = await fetch(`http://127.0.0.1:${port}/api/models`);
95
+ assert.equal(res.status, 200);
96
+ const data = (await res.json());
97
+ assert.equal(data.models.length, 3);
98
+ assert.deepEqual(data.models.map((model) => model.id), ["model-a", "model-m", "model-z"]);
99
+ assert.equal(data.activeModel, "test-model");
100
+ }
101
+ finally {
102
+ server.close();
103
+ }
104
+ });
105
+ test("POST /api/model switches the active model", async () => {
106
+ const bridge = new MockBridgeForModels();
107
+ const { server, port } = await startTestServer(bridge);
108
+ try {
109
+ const res = await fetch(`http://127.0.0.1:${port}/api/model`, {
110
+ method: "POST",
111
+ headers: { "Content-Type": "application/json" },
112
+ body: JSON.stringify({ model: "model-y" }),
113
+ });
114
+ assert.equal(res.status, 200);
115
+ const data = (await res.json());
116
+ assert.equal(data.model, "model-y");
117
+ }
118
+ finally {
119
+ server.close();
120
+ }
121
+ });
122
+ test("POST /api/model returns 400 when model is missing", async () => {
123
+ const bridge = new MockBridgeForModels();
124
+ const { server, port } = await startTestServer(bridge);
125
+ try {
126
+ const res = await fetch(`http://127.0.0.1:${port}/api/model`, {
127
+ method: "POST",
128
+ headers: { "Content-Type": "application/json" },
129
+ body: JSON.stringify({}),
130
+ });
131
+ assert.equal(res.status, 400);
132
+ }
133
+ finally {
134
+ server.close();
135
+ }
136
+ });
@@ -0,0 +1,22 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { sortModelsAlphabetically } from "../src/model-utils.js";
4
+ test("sortModelsAlphabetically sorts by display name without mutating input", () => {
5
+ const models = [
6
+ { id: "zeta-2", name: "Zeta 2" },
7
+ { id: "alpha-10", name: "alpha 10" },
8
+ { id: "alpha-2", name: "Alpha 2" },
9
+ { id: "beta-id" },
10
+ ];
11
+ const sorted = sortModelsAlphabetically(models);
12
+ assert.deepEqual(sorted.map((model) => model.id), ["alpha-2", "alpha-10", "beta-id", "zeta-2"]);
13
+ assert.deepEqual(models.map((model) => model.id), ["zeta-2", "alpha-10", "alpha-2", "beta-id"]);
14
+ });
15
+ test("sortModelsAlphabetically uses id as a stable tiebreaker", () => {
16
+ const models = [
17
+ { id: "gpt-4.1-b", name: "GPT-4.1" },
18
+ { id: "gpt-4.1-a", name: "GPT-4.1" },
19
+ ];
20
+ const sorted = sortModelsAlphabetically(models);
21
+ assert.deepEqual(sorted.map((model) => model.id), ["gpt-4.1-a", "gpt-4.1-b"]);
22
+ });
@@ -0,0 +1,264 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { OpenAICompatibleModelClient, CodingAgent, ToolRegistry, } from "@minicode/agent-sdk";
4
+ import { createTestAgentConfig } from "./test-utils.js";
5
+ import { loadAgentConfig } from "../src/agent/config.js";
6
+ // ---------------------------------------------------------------------------
7
+ // Config: REASONING_EFFORT env var
8
+ // ---------------------------------------------------------------------------
9
+ test("loadAgentConfig parses REASONING_EFFORT env var", async () => {
10
+ const prev = process.env.REASONING_EFFORT;
11
+ try {
12
+ process.env.REASONING_EFFORT = "high";
13
+ const config = await loadAgentConfig("/tmp");
14
+ assert.equal(config.reasoningEffort, "high");
15
+ }
16
+ finally {
17
+ if (prev === undefined) {
18
+ delete process.env.REASONING_EFFORT;
19
+ }
20
+ else {
21
+ process.env.REASONING_EFFORT = prev;
22
+ }
23
+ }
24
+ });
25
+ test("loadAgentConfig ignores invalid REASONING_EFFORT values", async () => {
26
+ const prev = process.env.REASONING_EFFORT;
27
+ try {
28
+ process.env.REASONING_EFFORT = "ultra";
29
+ const config = await loadAgentConfig("/tmp");
30
+ assert.equal(config.reasoningEffort, undefined);
31
+ }
32
+ finally {
33
+ if (prev === undefined) {
34
+ delete process.env.REASONING_EFFORT;
35
+ }
36
+ else {
37
+ process.env.REASONING_EFFORT = prev;
38
+ }
39
+ }
40
+ });
41
+ test("loadAgentConfig leaves reasoningEffort undefined when env var is unset", async () => {
42
+ const prev = process.env.REASONING_EFFORT;
43
+ try {
44
+ delete process.env.REASONING_EFFORT;
45
+ const config = await loadAgentConfig("/tmp");
46
+ assert.equal(config.reasoningEffort, undefined);
47
+ }
48
+ finally {
49
+ if (prev === undefined) {
50
+ delete process.env.REASONING_EFFORT;
51
+ }
52
+ else {
53
+ process.env.REASONING_EFFORT = prev;
54
+ }
55
+ }
56
+ });
57
+ test("loadAgentConfig normalizes REASONING_EFFORT case", async () => {
58
+ const prev = process.env.REASONING_EFFORT;
59
+ try {
60
+ process.env.REASONING_EFFORT = "MEDIUM";
61
+ const config = await loadAgentConfig("/tmp");
62
+ assert.equal(config.reasoningEffort, "medium");
63
+ }
64
+ finally {
65
+ if (prev === undefined) {
66
+ delete process.env.REASONING_EFFORT;
67
+ }
68
+ else {
69
+ process.env.REASONING_EFFORT = prev;
70
+ }
71
+ }
72
+ });
73
+ // ---------------------------------------------------------------------------
74
+ // OpenAI-compatible client: reasoning field in request body
75
+ // ---------------------------------------------------------------------------
76
+ test("openai-compatible client includes reasoning field when reasoningEffort is set", async () => {
77
+ let capturedBody = "";
78
+ const fetchImpl = async (_input, init) => {
79
+ capturedBody = String(init?.body ?? "");
80
+ return new Response(JSON.stringify({
81
+ choices: [
82
+ {
83
+ finish_reason: "stop",
84
+ message: { content: "Hello", tool_calls: [] },
85
+ },
86
+ ],
87
+ usage: { prompt_tokens: 10, completion_tokens: 5 },
88
+ }), { status: 200, headers: { "content-type": "application/json" } });
89
+ };
90
+ const client = new OpenAICompatibleModelClient({
91
+ baseUrl: "http://localhost:1234/v1",
92
+ fetchImpl,
93
+ });
94
+ await client.chat({
95
+ model: "test-model",
96
+ system: "System",
97
+ messages: [{ role: "user", content: "Hi" }],
98
+ tools: [],
99
+ maxTokens: 1024,
100
+ reasoningEffort: "high",
101
+ });
102
+ const parsed = JSON.parse(capturedBody);
103
+ assert.deepEqual(parsed.reasoning, { effort: "high" });
104
+ });
105
+ test("openai-compatible client omits reasoning field when reasoningEffort is not set", async () => {
106
+ let capturedBody = "";
107
+ const fetchImpl = async (_input, init) => {
108
+ capturedBody = String(init?.body ?? "");
109
+ return new Response(JSON.stringify({
110
+ choices: [
111
+ {
112
+ finish_reason: "stop",
113
+ message: { content: "Hello", tool_calls: [] },
114
+ },
115
+ ],
116
+ usage: { prompt_tokens: 10, completion_tokens: 5 },
117
+ }), { status: 200, headers: { "content-type": "application/json" } });
118
+ };
119
+ const client = new OpenAICompatibleModelClient({
120
+ baseUrl: "http://localhost:1234/v1",
121
+ fetchImpl,
122
+ });
123
+ await client.chat({
124
+ model: "test-model",
125
+ system: "System",
126
+ messages: [{ role: "user", content: "Hi" }],
127
+ tools: [],
128
+ maxTokens: 1024,
129
+ });
130
+ const parsed = JSON.parse(capturedBody);
131
+ assert.equal(parsed.reasoning, undefined);
132
+ });
133
+ test("openai-compatible client sends all valid effort levels", async () => {
134
+ const levels = ["xhigh", "high", "medium", "low", "minimal", "none"];
135
+ for (const level of levels) {
136
+ let capturedBody = "";
137
+ const fetchImpl = async (_input, init) => {
138
+ capturedBody = String(init?.body ?? "");
139
+ return new Response(JSON.stringify({
140
+ choices: [
141
+ {
142
+ finish_reason: "stop",
143
+ message: { content: "Ok", tool_calls: [] },
144
+ },
145
+ ],
146
+ usage: { prompt_tokens: 5, completion_tokens: 2 },
147
+ }), { status: 200, headers: { "content-type": "application/json" } });
148
+ };
149
+ const client = new OpenAICompatibleModelClient({
150
+ baseUrl: "http://localhost:1234/v1",
151
+ fetchImpl,
152
+ });
153
+ await client.chat({
154
+ model: "test-model",
155
+ system: "System",
156
+ messages: [{ role: "user", content: "Hi" }],
157
+ tools: [],
158
+ maxTokens: 1024,
159
+ reasoningEffort: level,
160
+ });
161
+ const parsed = JSON.parse(capturedBody);
162
+ assert.deepEqual(parsed.reasoning, { effort: level }, `Expected reasoning.effort="${level}"`);
163
+ }
164
+ });
165
+ // ---------------------------------------------------------------------------
166
+ // CodingAgent: get/set reasoning effort
167
+ // ---------------------------------------------------------------------------
168
+ test("CodingAgent.getReasoningEffort returns config value", () => {
169
+ const config = {
170
+ ...createTestAgentConfig("/tmp"),
171
+ reasoningEffort: "medium",
172
+ };
173
+ const mockClient = {
174
+ async chat() {
175
+ return {
176
+ text: "done",
177
+ toolCalls: [],
178
+ stopReason: "end_turn",
179
+ usage: { inputTokens: 0, outputTokens: 0 },
180
+ };
181
+ },
182
+ };
183
+ const agent = new CodingAgent({
184
+ config,
185
+ modelClient: mockClient,
186
+ toolRegistry: new ToolRegistry([]),
187
+ });
188
+ assert.equal(agent.getReasoningEffort(), "medium");
189
+ });
190
+ test("CodingAgent.setReasoningEffort updates reasoning effort", () => {
191
+ const config = createTestAgentConfig("/tmp");
192
+ const mockClient = {
193
+ async chat() {
194
+ return {
195
+ text: "done",
196
+ toolCalls: [],
197
+ stopReason: "end_turn",
198
+ usage: { inputTokens: 0, outputTokens: 0 },
199
+ };
200
+ },
201
+ };
202
+ const agent = new CodingAgent({
203
+ config,
204
+ modelClient: mockClient,
205
+ toolRegistry: new ToolRegistry([]),
206
+ });
207
+ assert.equal(agent.getReasoningEffort(), undefined);
208
+ agent.setReasoningEffort("high");
209
+ assert.equal(agent.getReasoningEffort(), "high");
210
+ agent.setReasoningEffort("none");
211
+ assert.equal(agent.getReasoningEffort(), "none");
212
+ agent.setReasoningEffort(undefined);
213
+ assert.equal(agent.getReasoningEffort(), undefined);
214
+ });
215
+ // ---------------------------------------------------------------------------
216
+ // Agent loop passes reasoningEffort to model client
217
+ // ---------------------------------------------------------------------------
218
+ test("agent loop passes reasoningEffort to model client chat call", async () => {
219
+ let capturedReasoningEffort;
220
+ const config = {
221
+ ...createTestAgentConfig("/tmp"),
222
+ reasoningEffort: "low",
223
+ };
224
+ const mockClient = {
225
+ async chat(params) {
226
+ capturedReasoningEffort = params.reasoningEffort;
227
+ return {
228
+ text: "Response",
229
+ toolCalls: [],
230
+ stopReason: "end_turn",
231
+ usage: { inputTokens: 10, outputTokens: 5 },
232
+ };
233
+ },
234
+ };
235
+ const agent = new CodingAgent({
236
+ config,
237
+ modelClient: mockClient,
238
+ toolRegistry: new ToolRegistry([]),
239
+ });
240
+ await agent.runTurn("Hello");
241
+ assert.equal(capturedReasoningEffort, "low");
242
+ });
243
+ test("agent loop omits reasoningEffort when not configured", async () => {
244
+ let capturedParams = {};
245
+ const config = createTestAgentConfig("/tmp");
246
+ const mockClient = {
247
+ async chat(params) {
248
+ capturedParams = params;
249
+ return {
250
+ text: "Response",
251
+ toolCalls: [],
252
+ stopReason: "end_turn",
253
+ usage: { inputTokens: 10, outputTokens: 5 },
254
+ };
255
+ },
256
+ };
257
+ const agent = new CodingAgent({
258
+ config,
259
+ modelClient: mockClient,
260
+ toolRegistry: new ToolRegistry([]),
261
+ });
262
+ await agent.runTurn("Hello");
263
+ assert.equal(capturedParams.reasoningEffort, undefined);
264
+ });
@@ -0,0 +1,161 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { test } from "node:test";
6
+ import { parseArgs, buildConfig, loadTasks } from "../scripts/run-benchmarks.js";
7
+ // ─── parseArgs ────────────────────────────────────────────────────
8
+ test("parseArgs: defaults to variant 'ci' with no flags", () => {
9
+ const args = parseArgs([]);
10
+ assert.equal(args.variant, "ci");
11
+ assert.equal(args.category, undefined);
12
+ assert.equal(args.task, undefined);
13
+ assert.equal(args.out, undefined);
14
+ });
15
+ test("parseArgs: parses --variant flag", () => {
16
+ const args = parseArgs(["--variant", "nightly"]);
17
+ assert.equal(args.variant, "nightly");
18
+ });
19
+ test("parseArgs: parses --category flag", () => {
20
+ const args = parseArgs(["--category", "navigation"]);
21
+ assert.equal(args.category, "navigation");
22
+ });
23
+ test("parseArgs: parses --task flag", () => {
24
+ const args = parseArgs(["--task", "navigation/find-symbol-definition"]);
25
+ assert.equal(args.task, "navigation/find-symbol-definition");
26
+ });
27
+ test("parseArgs: parses --out flag", () => {
28
+ const args = parseArgs(["--out", "report.json"]);
29
+ assert.equal(args.out, "report.json");
30
+ });
31
+ test("parseArgs: parses all flags together", () => {
32
+ const args = parseArgs([
33
+ "--category", "editing",
34
+ "--variant", "v2",
35
+ "--out", "/tmp/report.json",
36
+ ]);
37
+ assert.equal(args.category, "editing");
38
+ assert.equal(args.variant, "v2");
39
+ assert.equal(args.out, "/tmp/report.json");
40
+ });
41
+ test("parseArgs: ignores unknown flags", () => {
42
+ const args = parseArgs(["--unknown", "value", "--variant", "test"]);
43
+ assert.equal(args.variant, "test");
44
+ });
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;
51
+ try {
52
+ const config = buildConfig();
53
+ 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);
57
+ assert.equal(config.maxContextTokens, 32000);
58
+ assert.equal(config.confirmDestructive, false);
59
+ }
60
+ finally {
61
+ if (originalProvider !== undefined)
62
+ process.env.MODEL_PROVIDER = originalProvider;
63
+ if (originalModel !== undefined)
64
+ process.env.MODEL = originalModel;
65
+ }
66
+ });
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";
72
+ try {
73
+ const config = buildConfig();
74
+ assert.equal(config.modelProvider, "anthropic");
75
+ assert.equal(config.model, "claude-test");
76
+ }
77
+ 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
+ }
90
+ }
91
+ });
92
+ // ─── loadTasks ────────────────────────────────────────────────────
93
+ let tmpDir;
94
+ async function setupTempTasks() {
95
+ tmpDir = await mkdtemp(path.join(tmpdir(), "bench-cli-test-"));
96
+ await mkdir(path.join(tmpDir, "navigation", "find-foo"), { recursive: true });
97
+ await mkdir(path.join(tmpDir, "editing", "fix-bar"), { recursive: true });
98
+ await writeFile(path.join(tmpDir, "navigation", "find-foo", "task.json"), JSON.stringify({
99
+ title: "Find foo",
100
+ prompt: "Find foo",
101
+ rubric: { expectedOutputPatterns: ["foo"] },
102
+ }));
103
+ await writeFile(path.join(tmpDir, "editing", "fix-bar", "task.json"), JSON.stringify({
104
+ title: "Fix bar",
105
+ prompt: "Fix bar",
106
+ rubric: { expectedOutputPatterns: ["bar"] },
107
+ }));
108
+ return tmpDir;
109
+ }
110
+ test("loadTasks: loads all tasks when no filter", async () => {
111
+ const dir = await setupTempTasks();
112
+ try {
113
+ const tasks = await loadTasks(dir, { variant: "ci" });
114
+ assert.equal(tasks.length, 2);
115
+ }
116
+ finally {
117
+ await rm(dir, { recursive: true, force: true });
118
+ }
119
+ });
120
+ test("loadTasks: filters by category", async () => {
121
+ const dir = await setupTempTasks();
122
+ try {
123
+ const tasks = await loadTasks(dir, { variant: "ci", category: "navigation" });
124
+ assert.equal(tasks.length, 1);
125
+ assert.ok(tasks[0]);
126
+ assert.equal(tasks[0].category, "navigation");
127
+ }
128
+ finally {
129
+ await rm(dir, { recursive: true, force: true });
130
+ }
131
+ });
132
+ test("loadTasks: loads single task by id", async () => {
133
+ const dir = await setupTempTasks();
134
+ try {
135
+ const tasks = await loadTasks(dir, { variant: "ci", task: "navigation/find-foo" });
136
+ assert.equal(tasks.length, 1);
137
+ assert.ok(tasks[0]);
138
+ assert.equal(tasks[0].id, "navigation/find-foo");
139
+ }
140
+ finally {
141
+ await rm(dir, { recursive: true, force: true });
142
+ }
143
+ });
144
+ test("loadTasks: throws for unknown task id", async () => {
145
+ const dir = await setupTempTasks();
146
+ try {
147
+ await assert.rejects(() => loadTasks(dir, { variant: "ci", task: "navigation/nonexistent" }), { message: "Task not found: navigation/nonexistent" });
148
+ }
149
+ finally {
150
+ await rm(dir, { recursive: true, force: true });
151
+ }
152
+ });
153
+ test("loadTasks: throws for unknown category", async () => {
154
+ const dir = await setupTempTasks();
155
+ try {
156
+ await assert.rejects(() => loadTasks(dir, { variant: "ci", category: "nonexistent" }), { message: "No tasks found for category: nonexistent" });
157
+ }
158
+ finally {
159
+ await rm(dir, { recursive: true, force: true });
160
+ }
161
+ });
@@ -1,5 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
+ import { mkdtemp, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
4
+ import { tmpdir } from "node:os";
3
5
  import { test } from "node:test";
4
6
  import { buildProjectIndex } from "../src/indexer/project-index.js";
5
7
  import { createSearchCodeMapTool } from "../src/tools/search-code-map.js";
@@ -28,3 +30,19 @@ test("search_code_map appears in tool registry when projectIndex provided", asyn
28
30
  const searchCodeMap = schemas.find((s) => s.name === "search_code_map");
29
31
  assert.ok(searchCodeMap);
30
32
  });
33
+ test("search_code_map shows colliding symbols with disambiguated display names", async () => {
34
+ const workspaceRoot = await mkdtemp(path.join(tmpdir(), "minicode-search-collisions-"));
35
+ await writeFile(path.join(workspaceRoot, "sample.ts"), `export interface Employee {
36
+ id: string;
37
+ }
38
+
39
+ export class Employee {
40
+ constructor(public id: string) {}
41
+ }
42
+ `, "utf8");
43
+ const projectIndex = await buildProjectIndex(workspaceRoot);
44
+ const tool = createSearchCodeMapTool(projectIndex);
45
+ const result = await tool.execute({ pattern: "Employee" });
46
+ assert.ok(result.includes("Employee (interface)"));
47
+ assert.ok(result.includes("Employee (class)"));
48
+ });