@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,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Integration tests for the config system.
|
|
3
3
|
*
|
|
4
|
-
* Tests the
|
|
5
|
-
* getConfigSetupMessage / getConfigMissing,
|
|
4
|
+
* Tests the current config resolution chain: ~/.minicode/.env → shell env vars,
|
|
5
|
+
* getConfigSetupMessage / getConfigMissing, minicode home bootstrapping, the /api/status needsSetup flag,
|
|
6
6
|
* and AgentBridge graceful degradation when model client cannot initialize.
|
|
7
7
|
*/
|
|
8
8
|
import assert from "node:assert/strict";
|
|
@@ -11,7 +11,7 @@ import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { afterEach, describe, test } from "node:test";
|
|
14
|
-
import { loadAgentConfig, resolveConfigEnv, getConfigSetupMessage, getConfigMissing,
|
|
14
|
+
import { loadAgentConfig, resolveConfigEnv, getConfigSetupMessage, getConfigMissing, } from "../src/agent/config.js";
|
|
15
15
|
import { buildStructuredConfigPayload } from "../src/agent/editable-config.js";
|
|
16
16
|
import { createRequestHandler } from "../src/serve/server.js";
|
|
17
17
|
import { AgentBridge } from "../src/serve/agent-bridge.js";
|
|
@@ -25,14 +25,46 @@ afterEach(async () => {
|
|
|
25
25
|
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
26
26
|
});
|
|
27
27
|
/** Create an isolated minicode home directory with optional config and .env files. */
|
|
28
|
+
function serializeConfigEnv(config) {
|
|
29
|
+
const lines = [];
|
|
30
|
+
const push = (key, value) => {
|
|
31
|
+
if (value === undefined || value === null) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
lines.push(`${key}=${String(value)}`);
|
|
35
|
+
};
|
|
36
|
+
push("MODEL_PROVIDER", config.modelProvider);
|
|
37
|
+
push("MODEL", config.model);
|
|
38
|
+
push("MAX_STEPS", config.maxSteps);
|
|
39
|
+
push("MAX_TOKENS", config.maxTokens);
|
|
40
|
+
push("MAX_CONTEXT_TOKENS", config.maxContextTokens);
|
|
41
|
+
push("WORKSPACE_ROOT", config.workspaceRoot);
|
|
42
|
+
push("COMMAND_TIMEOUT_MS", config.commandTimeout);
|
|
43
|
+
if (Array.isArray(config.commandDenylist)) {
|
|
44
|
+
lines.push(`COMMAND_DENYLIST=${JSON.stringify(config.commandDenylist)}`);
|
|
45
|
+
}
|
|
46
|
+
push("CONFIRM_DESTRUCTIVE", config.confirmDestructive);
|
|
47
|
+
push("MAX_FILE_SIZE_BYTES", config.maxFileSizeBytes);
|
|
48
|
+
push("KEEP_RECENT_MESSAGES", config.keepRecentMessages);
|
|
49
|
+
push("LOOP_DETECTION_WINDOW", config.loopDetectionWindow);
|
|
50
|
+
push("MAX_TOOL_OUTPUT_CHARS", config.maxToolOutputChars);
|
|
51
|
+
push("OPENAI_BASE_URL", config.openAiBaseUrl);
|
|
52
|
+
push("OPENAI_API_KEY", config.openAiApiKey);
|
|
53
|
+
push("ENABLE_FILE_READ_DEDUP", config.enableFileReadDedup);
|
|
54
|
+
push("ENABLE_ADAPTIVE_KEEP_RECENT", config.enableAdaptiveKeepRecent);
|
|
55
|
+
push("ENABLE_TOOL_OUTPUT_TRUNCATION", config.enableToolOutputTruncation);
|
|
56
|
+
push("COMPACTION_THRESHOLD", config.compactionThreshold);
|
|
57
|
+
push("COMPACTION_MODEL", config.compactionModel);
|
|
58
|
+
push("REASONING_EFFORT", config.reasoningEffort);
|
|
59
|
+
push("ENABLE_DYNAMIC_PROMPT", config.enableDynamicPrompt);
|
|
60
|
+
return lines.length > 0 ? `${lines.join("\n")}\n` : "";
|
|
61
|
+
}
|
|
28
62
|
async function createTestHome(options = {}) {
|
|
29
63
|
const home = await mkdtemp(path.join(os.tmpdir(), "minicode-integ-"));
|
|
30
64
|
tempDirs.push(home);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (options.dotenv !== undefined) {
|
|
35
|
-
await writeFile(path.join(home, ".env"), options.dotenv, "utf8");
|
|
65
|
+
const envContent = `${options.config ? serializeConfigEnv(options.config) : ""}${options.dotenv ?? ""}`;
|
|
66
|
+
if (envContent.length > 0) {
|
|
67
|
+
await writeFile(path.join(home, ".env"), envContent, "utf8");
|
|
36
68
|
}
|
|
37
69
|
return home;
|
|
38
70
|
}
|
|
@@ -102,27 +134,25 @@ function startServer(bridge, options = {}) {
|
|
|
102
134
|
});
|
|
103
135
|
}
|
|
104
136
|
// ═══════════════════════════════════════════════════════════════════
|
|
105
|
-
//
|
|
137
|
+
// Home env loading via loadAgentConfig
|
|
106
138
|
// ═══════════════════════════════════════════════════════════════════
|
|
107
|
-
describe("
|
|
108
|
-
test("
|
|
109
|
-
const result = await loadConfigFile("/tmp/nonexistent-minicode-test/config.json");
|
|
110
|
-
assert.deepEqual(result, {});
|
|
111
|
-
});
|
|
112
|
-
test("parses valid JSON config file", async () => {
|
|
139
|
+
describe("loadAgentConfig home env", () => {
|
|
140
|
+
test("uses ~/.minicode/.env values when present", async () => {
|
|
113
141
|
const home = await createTestHome({
|
|
114
|
-
|
|
142
|
+
dotenv: "MODEL=test-model\nMAX_STEPS=25\n",
|
|
143
|
+
});
|
|
144
|
+
await withEnv({ MODEL: undefined, MAX_STEPS: undefined }, async () => {
|
|
145
|
+
const config = await loadAgentConfig("/tmp", { minicodeHome: home });
|
|
146
|
+
assert.equal(config.model, "test-model");
|
|
147
|
+
assert.equal(config.maxSteps, 25);
|
|
115
148
|
});
|
|
116
|
-
const result = await loadConfigFile(path.join(home, "agent.config.json"));
|
|
117
|
-
assert.equal(result.model, "test-model");
|
|
118
|
-
assert.equal(result.maxSteps, 25);
|
|
119
149
|
});
|
|
120
150
|
});
|
|
121
151
|
// ═══════════════════════════════════════════════════════════════════
|
|
122
|
-
//
|
|
152
|
+
// minicode home bootstrapping
|
|
123
153
|
// ═══════════════════════════════════════════════════════════════════
|
|
124
154
|
describe("ensureMinicodeHome (via loadAgentConfig)", () => {
|
|
125
|
-
test("creates
|
|
155
|
+
test("creates the minicode home directory when it does not exist", async () => {
|
|
126
156
|
const base = await mkdtemp(path.join(os.tmpdir(), "minicode-integ-"));
|
|
127
157
|
tempDirs.push(base);
|
|
128
158
|
const minicodeHome = path.join(base, "fresh-home");
|
|
@@ -131,22 +161,18 @@ describe("ensureMinicodeHome (via loadAgentConfig)", () => {
|
|
|
131
161
|
});
|
|
132
162
|
// Directory should exist
|
|
133
163
|
await access(minicodeHome);
|
|
134
|
-
|
|
135
|
-
const content = JSON.parse(await readFile(path.join(minicodeHome, "agent.config.json"), "utf8"));
|
|
136
|
-
assert.equal(content.model, "");
|
|
137
|
-
assert.equal(content.modelProvider, "openai-compatible");
|
|
138
|
-
assert.equal(content.openAiBaseUrl, "http://localhost:1234/v1");
|
|
164
|
+
await assert.rejects(access(path.join(minicodeHome, "agent.config.json")));
|
|
139
165
|
});
|
|
140
|
-
test("does not overwrite existing
|
|
166
|
+
test("does not overwrite an existing home .env file", async () => {
|
|
141
167
|
const home = await createTestHome({
|
|
142
|
-
|
|
168
|
+
dotenv: "MODEL=my-model\nMAX_STEPS=99\n",
|
|
143
169
|
});
|
|
144
170
|
await withEnv({ MODEL: undefined }, async () => {
|
|
145
171
|
await loadAgentConfig("/tmp", { minicodeHome: home });
|
|
146
172
|
});
|
|
147
|
-
const content =
|
|
148
|
-
assert.
|
|
149
|
-
assert.
|
|
173
|
+
const content = await readFile(path.join(home, ".env"), "utf8");
|
|
174
|
+
assert.match(content, /^MODEL=my-model$/m);
|
|
175
|
+
assert.match(content, /^MAX_STEPS=99$/m);
|
|
150
176
|
});
|
|
151
177
|
});
|
|
152
178
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -207,7 +233,7 @@ describe("resolveConfigEnv precedence", () => {
|
|
|
207
233
|
// loadAgentConfig — full precedence chain
|
|
208
234
|
// ═══════════════════════════════════════════════════════════════════
|
|
209
235
|
describe("loadAgentConfig precedence", () => {
|
|
210
|
-
test("
|
|
236
|
+
test("home .env value is used when no shell override exists", async () => {
|
|
211
237
|
const home = await createTestHome({
|
|
212
238
|
config: { model: "config-model", maxSteps: 42 },
|
|
213
239
|
});
|
|
@@ -217,7 +243,7 @@ describe("loadAgentConfig precedence", () => {
|
|
|
217
243
|
assert.equal(config.maxSteps, 42);
|
|
218
244
|
});
|
|
219
245
|
});
|
|
220
|
-
test("home .env
|
|
246
|
+
test("later home .env entries override earlier persisted values", async () => {
|
|
221
247
|
const home = await createTestHome({
|
|
222
248
|
config: { model: "config-model", maxSteps: 42 },
|
|
223
249
|
dotenv: "MODEL=dotenv-model\n",
|
|
@@ -225,11 +251,10 @@ describe("loadAgentConfig precedence", () => {
|
|
|
225
251
|
await withEnv({ MODEL: undefined }, async () => {
|
|
226
252
|
const config = await loadAgentConfig("/tmp", { minicodeHome: home });
|
|
227
253
|
assert.equal(config.model, "dotenv-model");
|
|
228
|
-
// maxSteps not in .env, so config file value is used
|
|
229
254
|
assert.equal(config.maxSteps, 42);
|
|
230
255
|
});
|
|
231
256
|
});
|
|
232
|
-
test("shell env overrides
|
|
257
|
+
test("shell env overrides home .env", async () => {
|
|
233
258
|
const home = await createTestHome({
|
|
234
259
|
config: { model: "config-model", maxSteps: 42 },
|
|
235
260
|
dotenv: "MODEL=dotenv-model\nMAX_STEPS=77\n",
|
|
@@ -285,18 +310,13 @@ describe("loadAgentConfig precedence", () => {
|
|
|
285
310
|
assert.equal(config.openAiApiKey, "sk-shell-key");
|
|
286
311
|
});
|
|
287
312
|
});
|
|
288
|
-
test("OPENAI_API_KEY
|
|
313
|
+
test("OPENAI_API_KEY is unset when not provided in env", async () => {
|
|
289
314
|
const home = await createTestHome({
|
|
290
|
-
|
|
291
|
-
modelProvider: "openai-compatible",
|
|
292
|
-
model: "test-model",
|
|
293
|
-
openAiBaseUrl: "http://localhost:1234/v1",
|
|
294
|
-
openAiApiKey: "sk-file-key",
|
|
295
|
-
},
|
|
315
|
+
dotenv: "MODEL_PROVIDER=openai-compatible\nMODEL=test-model\nOPENAI_BASE_URL=http://localhost:1234/v1\n",
|
|
296
316
|
});
|
|
297
317
|
await withEnv({ OPENAI_API_KEY: undefined, MODEL: undefined, MODEL_PROVIDER: undefined }, async () => {
|
|
298
318
|
const config = await loadAgentConfig("/tmp", { minicodeHome: home });
|
|
299
|
-
assert.equal(config.openAiApiKey,
|
|
319
|
+
assert.equal(config.openAiApiKey, undefined);
|
|
300
320
|
});
|
|
301
321
|
});
|
|
302
322
|
test("OpenRouter base URL resolves OPENROUTER_API_KEY with fallback to OPENAI_API_KEY", async () => {
|
|
@@ -412,6 +432,26 @@ describe("getConfigMissing and getConfigSetupMessage", () => {
|
|
|
412
432
|
assert.deepEqual(getConfigMissing(config), []);
|
|
413
433
|
});
|
|
414
434
|
});
|
|
435
|
+
test("returns OPENROUTER_API_KEY missing for OpenRouter without a key", () => {
|
|
436
|
+
const config = {
|
|
437
|
+
...createTestAgentConfig("/tmp"),
|
|
438
|
+
modelProvider: "openai-compatible",
|
|
439
|
+
model: "google/gemini-2.5-flash",
|
|
440
|
+
openAiBaseUrl: "https://openrouter.ai/api/v1",
|
|
441
|
+
};
|
|
442
|
+
const missing = getConfigMissing(config);
|
|
443
|
+
assert.ok(missing.some((m) => m.includes("OPENROUTER_API_KEY")));
|
|
444
|
+
});
|
|
445
|
+
test("does not require OPENROUTER_API_KEY when OpenRouter key is present in config", () => {
|
|
446
|
+
const config = {
|
|
447
|
+
...createTestAgentConfig("/tmp"),
|
|
448
|
+
modelProvider: "openai-compatible",
|
|
449
|
+
model: "google/gemini-2.5-flash",
|
|
450
|
+
openAiBaseUrl: "https://openrouter.ai/api/v1",
|
|
451
|
+
openAiApiKey: "sk-or-v1-test",
|
|
452
|
+
};
|
|
453
|
+
assert.deepEqual(getConfigMissing(config), []);
|
|
454
|
+
});
|
|
415
455
|
test("can return multiple missing items simultaneously", async () => {
|
|
416
456
|
await withEnv({ ANTHROPIC_API_KEY: undefined }, async () => {
|
|
417
457
|
const config = {
|
|
@@ -430,7 +470,7 @@ describe("getConfigMissing and getConfigSetupMessage", () => {
|
|
|
430
470
|
const message = getConfigSetupMessage(config);
|
|
431
471
|
assert.ok(message.includes("MODEL is not set"));
|
|
432
472
|
assert.ok(message.includes("~/.minicode/.env"));
|
|
433
|
-
assert.ok(message.includes("
|
|
473
|
+
assert.ok(message.includes("saved back to ~/.minicode/.env"));
|
|
434
474
|
});
|
|
435
475
|
});
|
|
436
476
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -504,6 +544,25 @@ describe("loadAgentConfig → getConfigMissing integration", () => {
|
|
|
504
544
|
assert.ok(missing.some((m) => m.includes("ANTHROPIC_API_KEY")));
|
|
505
545
|
});
|
|
506
546
|
});
|
|
547
|
+
test("OpenRouter provider without API key reports missing", async () => {
|
|
548
|
+
const home = await createTestHome({
|
|
549
|
+
config: {
|
|
550
|
+
modelProvider: "openai-compatible",
|
|
551
|
+
model: "google/gemini-2.5-flash",
|
|
552
|
+
openAiBaseUrl: "https://openrouter.ai/api/v1",
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
await withEnv({
|
|
556
|
+
OPENROUTER_API_KEY: undefined,
|
|
557
|
+
OPENAI_API_KEY: undefined,
|
|
558
|
+
MODEL: undefined,
|
|
559
|
+
MODEL_PROVIDER: undefined,
|
|
560
|
+
}, async () => {
|
|
561
|
+
const config = await loadAgentConfig("/tmp", { minicodeHome: home });
|
|
562
|
+
const missing = getConfigMissing(config);
|
|
563
|
+
assert.ok(missing.some((m) => m.includes("OPENROUTER_API_KEY")));
|
|
564
|
+
});
|
|
565
|
+
});
|
|
507
566
|
});
|
|
508
567
|
// ═══════════════════════════════════════════════════════════════════
|
|
509
568
|
// /api/status endpoint — needsSetup and missing fields
|
|
@@ -551,6 +610,20 @@ describe("/api/status needsSetup", () => {
|
|
|
551
610
|
assert.ok(body.missing.some((m) => m.includes("ANTHROPIC_API_KEY")));
|
|
552
611
|
});
|
|
553
612
|
});
|
|
613
|
+
test("returns needsSetup: true for OpenRouter without API key", async () => {
|
|
614
|
+
const config = {
|
|
615
|
+
...createTestAgentConfig("/tmp"),
|
|
616
|
+
modelProvider: "openai-compatible",
|
|
617
|
+
model: "google/gemini-2.5-flash",
|
|
618
|
+
openAiBaseUrl: "https://openrouter.ai/api/v1",
|
|
619
|
+
};
|
|
620
|
+
const bridge = new ConfigurableBridge(config);
|
|
621
|
+
const base = await startServer(bridge);
|
|
622
|
+
const res = await fetch(`${base}/api/status`);
|
|
623
|
+
const body = await res.json();
|
|
624
|
+
assert.equal(body.needsSetup, true);
|
|
625
|
+
assert.ok(body.missing.some((m) => m.includes("OPENROUTER_API_KEY")));
|
|
626
|
+
});
|
|
554
627
|
test("status endpoint includes model and provider info", async () => {
|
|
555
628
|
const config = {
|
|
556
629
|
...createTestAgentConfig("/tmp"),
|
|
@@ -600,7 +673,7 @@ describe("buildStructuredConfigPayload env source tracking", () => {
|
|
|
600
673
|
assert.equal(modelEntry.envSource, "process");
|
|
601
674
|
});
|
|
602
675
|
});
|
|
603
|
-
test("
|
|
676
|
+
test("treats ~/.minicode/.env as persisted settings rather than an override", async () => {
|
|
604
677
|
const home = await createTestHome({
|
|
605
678
|
config: { model: "file-model" },
|
|
606
679
|
dotenv: "MAX_STEPS=88\n",
|
|
@@ -609,13 +682,14 @@ describe("buildStructuredConfigPayload env source tracking", () => {
|
|
|
609
682
|
const config = await loadAgentConfig("/tmp", { minicodeHome: home });
|
|
610
683
|
const payload = await buildStructuredConfigPayload(config, home);
|
|
611
684
|
const maxStepsEntry = payload.entries.find((e) => e.key === "maxSteps");
|
|
612
|
-
assert.equal(maxStepsEntry.overriddenByEnv,
|
|
613
|
-
assert.equal(maxStepsEntry.
|
|
614
|
-
assert.equal(maxStepsEntry.
|
|
615
|
-
assert.equal(maxStepsEntry.
|
|
685
|
+
assert.equal(maxStepsEntry.overriddenByEnv, false);
|
|
686
|
+
assert.equal(maxStepsEntry.persistedValue, 88);
|
|
687
|
+
assert.equal(maxStepsEntry.envValue, null);
|
|
688
|
+
assert.equal(maxStepsEntry.envSource, null);
|
|
689
|
+
assert.equal(maxStepsEntry.envSourcePath, null);
|
|
616
690
|
});
|
|
617
691
|
});
|
|
618
|
-
test("
|
|
692
|
+
test("shows home .env values as both effective and persisted", async () => {
|
|
619
693
|
const home = await createTestHome({
|
|
620
694
|
config: { maxSteps: 42 },
|
|
621
695
|
});
|
|
@@ -624,6 +698,7 @@ describe("buildStructuredConfigPayload env source tracking", () => {
|
|
|
624
698
|
const payload = await buildStructuredConfigPayload(config, home);
|
|
625
699
|
const maxStepsEntry = payload.entries.find((e) => e.key === "maxSteps");
|
|
626
700
|
assert.equal(maxStepsEntry.overriddenByEnv, false);
|
|
701
|
+
assert.equal(maxStepsEntry.effectiveValue, 42);
|
|
627
702
|
assert.equal(maxStepsEntry.persistedValue, 42);
|
|
628
703
|
});
|
|
629
704
|
});
|
|
@@ -643,7 +718,7 @@ describe("buildStructuredConfigPayload env source tracking", () => {
|
|
|
643
718
|
// Realistic user scenario tests
|
|
644
719
|
// ═══════════════════════════════════════════════════════════════════
|
|
645
720
|
describe("realistic user scenarios", () => {
|
|
646
|
-
test("OpenRouter setup:
|
|
721
|
+
test("OpenRouter setup: home .env + shell OPENAI_API_KEY → no setup needed", async () => {
|
|
647
722
|
// User has modelProvider and openAiBaseUrl in config, OPENAI_API_KEY in shell
|
|
648
723
|
const home = await createTestHome({
|
|
649
724
|
config: {
|
|
@@ -695,7 +770,7 @@ describe("realistic user scenarios", () => {
|
|
|
695
770
|
assert.ok(missing[0].includes("MODEL"));
|
|
696
771
|
});
|
|
697
772
|
});
|
|
698
|
-
test("local LM Studio setup: only
|
|
773
|
+
test("local LM Studio setup: only home .env, no shell env vars needed", async () => {
|
|
699
774
|
const home = await createTestHome({
|
|
700
775
|
config: {
|
|
701
776
|
modelProvider: "openai-compatible",
|
|
@@ -733,15 +808,14 @@ describe("realistic user scenarios", () => {
|
|
|
733
808
|
assert.deepEqual(getConfigMissing(config), []);
|
|
734
809
|
});
|
|
735
810
|
});
|
|
736
|
-
test("fresh install:
|
|
811
|
+
test("fresh install: missing model still triggers setup overlay", async () => {
|
|
737
812
|
const base = await mkdtemp(path.join(os.tmpdir(), "minicode-integ-"));
|
|
738
813
|
tempDirs.push(base);
|
|
739
814
|
const minicodeHome = path.join(base, "new-home");
|
|
740
815
|
await withEnv({ MODEL: undefined, MODEL_PROVIDER: undefined }, async () => {
|
|
741
816
|
const config = await loadAgentConfig("/tmp", { minicodeHome });
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
assert.ok(fileContent.includes('"model": ""'));
|
|
817
|
+
await access(minicodeHome);
|
|
818
|
+
await assert.rejects(access(path.join(minicodeHome, "agent.config.json")));
|
|
745
819
|
// Empty model → needs setup
|
|
746
820
|
assert.equal(config.model, "");
|
|
747
821
|
const missing = getConfigMissing(config);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import {
|
|
2
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { afterEach, test } from "node:test";
|
|
@@ -18,14 +18,15 @@ test("setPersistedConfigValue writes mapped keys and unset removes empty config
|
|
|
18
18
|
key: "commandTimeoutMs",
|
|
19
19
|
rawValue: "45000",
|
|
20
20
|
});
|
|
21
|
-
assert.equal(setResult.path, path.join(home, "
|
|
22
|
-
const file =
|
|
23
|
-
assert.
|
|
21
|
+
assert.equal(setResult.path, path.join(home, ".env"));
|
|
22
|
+
const file = await readFile(setResult.path, "utf8");
|
|
23
|
+
assert.match(file, /^COMMAND_TIMEOUT_MS=45000$/m);
|
|
24
24
|
await unsetPersistedConfigValue({
|
|
25
25
|
minicodeHome: home,
|
|
26
26
|
key: "commandTimeoutMs",
|
|
27
27
|
});
|
|
28
|
-
await
|
|
28
|
+
const updated = await readFile(setResult.path, "utf8");
|
|
29
|
+
assert.doesNotMatch(updated, /^COMMAND_TIMEOUT_MS=/m);
|
|
29
30
|
});
|
|
30
31
|
test("handleConfigSlashCommand persists config and reports env overrides", async () => {
|
|
31
32
|
const home = await mkdtemp(path.join(os.tmpdir(), "minicode-config-"));
|
|
@@ -41,9 +42,9 @@ test("handleConfigSlashCommand persists config and reports env overrides", async
|
|
|
41
42
|
});
|
|
42
43
|
assert.equal(result.handled, true);
|
|
43
44
|
assert.match(result.message ?? "", /Saved config: maxSteps = 64/);
|
|
44
|
-
assert.match(result.message ?? "", /MAX_STEPS is currently
|
|
45
|
-
const persisted =
|
|
46
|
-
assert.
|
|
45
|
+
assert.match(result.message ?? "", /MAX_STEPS is currently exported in your shell/);
|
|
46
|
+
const persisted = await readFile(path.join(home, ".env"), "utf8");
|
|
47
|
+
assert.match(persisted, /^MAX_STEPS=64$/m);
|
|
47
48
|
}
|
|
48
49
|
finally {
|
|
49
50
|
if (previous === undefined) {
|
|
@@ -76,8 +77,8 @@ test("handleConfigSlashCommand reports config layers with /config get", async ()
|
|
|
76
77
|
});
|
|
77
78
|
assert.equal(getResult.handled, true);
|
|
78
79
|
assert.match(getResult.message ?? "", /effective: openai-compatible/);
|
|
79
|
-
assert.match(getResult.message ?? "", /
|
|
80
|
-
assert.match(getResult.message ?? "", /env override \(MODEL_PROVIDER\): openai-compatible/);
|
|
80
|
+
assert.match(getResult.message ?? "", /saved in ~\/\.minicode\/\.env: openai-compatible/);
|
|
81
|
+
assert.match(getResult.message ?? "", /exported env override \(MODEL_PROVIDER\): openai-compatible/);
|
|
81
82
|
}
|
|
82
83
|
finally {
|
|
83
84
|
if (previous === undefined) {
|
|
@@ -102,5 +103,5 @@ test("handleConfigSlashCommand rejects non-editable keys and keeps secrets env-o
|
|
|
102
103
|
});
|
|
103
104
|
test("getGlobalConfigPath resolves to minicode home", () => {
|
|
104
105
|
const home = "/tmp/example-home";
|
|
105
|
-
assert.equal(getGlobalConfigPath(home), path.join(home, "
|
|
106
|
+
assert.equal(getGlobalConfigPath(home), path.join(home, ".env"));
|
|
106
107
|
});
|
|
@@ -47,8 +47,7 @@ test("loadAgentConfig uses global config and env vars with correct precedence",
|
|
|
47
47
|
tempDirs.push(base);
|
|
48
48
|
const minicodeHome = path.join(base, "home");
|
|
49
49
|
await mkdir(minicodeHome, { recursive: true });
|
|
50
|
-
await writeFile(path.join(minicodeHome, "
|
|
51
|
-
await writeFile(path.join(minicodeHome, ".env"), "MODEL=home-env-model\n", "utf8");
|
|
50
|
+
await writeFile(path.join(minicodeHome, ".env"), "MODEL=home-env-model\nMAX_STEPS=33\n", "utf8");
|
|
52
51
|
const previousMaxSteps = process.env.MAX_STEPS;
|
|
53
52
|
const previousModel = process.env.MODEL;
|
|
54
53
|
try {
|
|
@@ -56,9 +55,7 @@ test("loadAgentConfig uses global config and env vars with correct precedence",
|
|
|
56
55
|
process.env.MAX_STEPS = "120";
|
|
57
56
|
delete process.env.MODEL;
|
|
58
57
|
const config = await loadAgentConfig("/tmp", { minicodeHome });
|
|
59
|
-
// MODEL from ~/.minicode/.env (no shell override)
|
|
60
58
|
assert.equal(config.model, "home-env-model");
|
|
61
|
-
// MAX_STEPS from shell env (overrides config file value of 33)
|
|
62
59
|
assert.equal(config.maxSteps, 120);
|
|
63
60
|
}
|
|
64
61
|
finally {
|
|
@@ -76,3 +73,14 @@ test("loadAgentConfig uses global config and env vars with correct precedence",
|
|
|
76
73
|
}
|
|
77
74
|
}
|
|
78
75
|
});
|
|
76
|
+
test("loadAgentConfig appends COMMAND_DENYLIST patterns from env", async () => {
|
|
77
|
+
const base = await mkdtemp(path.join(os.tmpdir(), "minicode-config-test-"));
|
|
78
|
+
tempDirs.push(base);
|
|
79
|
+
const minicodeHome = path.join(base, "home");
|
|
80
|
+
await mkdir(minicodeHome, { recursive: true });
|
|
81
|
+
await writeFile(path.join(minicodeHome, ".env"), 'MODEL=test-model\nCOMMAND_DENYLIST=["custom-danger","^wipe-db$"]\n', "utf8");
|
|
82
|
+
const config = await loadAgentConfig("/tmp", { minicodeHome });
|
|
83
|
+
const serialized = config.commandDenylist.map((pattern) => pattern.source);
|
|
84
|
+
assert.ok(serialized.includes("custom-danger"));
|
|
85
|
+
assert.ok(serialized.includes("^wipe-db$"));
|
|
86
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import {
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { afterEach, test } from "node:test";
|
|
@@ -47,7 +47,7 @@ test("buildStructuredConfigPayload reports effective values and env overrides",
|
|
|
47
47
|
maxSteps: 120,
|
|
48
48
|
model: "global-model",
|
|
49
49
|
}, home);
|
|
50
|
-
assert.equal(payload.configPath, path.join(home, "
|
|
50
|
+
assert.equal(payload.configPath, path.join(home, ".env"));
|
|
51
51
|
const maxSteps = payload.entries.find((entry) => entry.key === "maxSteps");
|
|
52
52
|
assert.equal(maxSteps?.effectiveValue, 120);
|
|
53
53
|
assert.equal(maxSteps?.persistedValue, 77);
|
|
@@ -73,13 +73,14 @@ test("buildStructuredConfigPayload reports home dotenv env source", async () =>
|
|
|
73
73
|
maxSteps: 88,
|
|
74
74
|
}, home);
|
|
75
75
|
const maxSteps = payload.entries.find((entry) => entry.key === "maxSteps");
|
|
76
|
-
assert.equal(maxSteps?.
|
|
77
|
-
assert.equal(maxSteps?.
|
|
78
|
-
assert.equal(maxSteps?.
|
|
79
|
-
assert.equal(maxSteps?.
|
|
76
|
+
assert.equal(maxSteps?.persistedValue, 88);
|
|
77
|
+
assert.equal(maxSteps?.envValue, null);
|
|
78
|
+
assert.equal(maxSteps?.envSource, null);
|
|
79
|
+
assert.equal(maxSteps?.envSourcePath, null);
|
|
80
|
+
assert.equal(maxSteps?.overriddenByEnv, false);
|
|
80
81
|
});
|
|
81
82
|
});
|
|
82
|
-
test("applyPersistedConfigUpdates writes
|
|
83
|
+
test("applyPersistedConfigUpdates writes ~/.minicode/.env and removes keys when unset", async () => {
|
|
83
84
|
const home = await mkdtemp(path.join(os.tmpdir(), "minicode-editable-config-"));
|
|
84
85
|
tempDirs.push(home);
|
|
85
86
|
const result = await applyPersistedConfigUpdates({
|
|
@@ -94,10 +95,10 @@ test("applyPersistedConfigUpdates writes global config and removes files when ev
|
|
|
94
95
|
{ key: "keepRecentMessages", value: 18 },
|
|
95
96
|
{ key: "enableFileReadDedup", value: false },
|
|
96
97
|
]);
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
assert.
|
|
100
|
-
assert.
|
|
98
|
+
const envPath = path.join(home, ".env");
|
|
99
|
+
let persisted = await readFile(envPath, "utf8");
|
|
100
|
+
assert.match(persisted, /^KEEP_RECENT_MESSAGES=18$/m);
|
|
101
|
+
assert.match(persisted, /^ENABLE_FILE_READ_DEDUP=false$/m);
|
|
101
102
|
await applyPersistedConfigUpdates({
|
|
102
103
|
minicodeHome: home,
|
|
103
104
|
updates: {
|
|
@@ -105,5 +106,7 @@ test("applyPersistedConfigUpdates writes global config and removes files when ev
|
|
|
105
106
|
enableFileReadDedup: null,
|
|
106
107
|
},
|
|
107
108
|
});
|
|
108
|
-
await
|
|
109
|
+
persisted = await readFile(envPath, "utf8");
|
|
110
|
+
assert.doesNotMatch(persisted, /^KEEP_RECENT_MESSAGES=/m);
|
|
111
|
+
assert.doesNotMatch(persisted, /^ENABLE_FILE_READ_DEDUP=/m);
|
|
109
112
|
});
|
|
@@ -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
|
+
});
|