@sean.holung/minicode 0.3.6 → 0.3.8
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 +2 -1
- package/dist/scripts/run-benchmarks.js +1 -0
- package/dist/src/agent/config.js +27 -0
- package/dist/src/agent/editable-config.js +6 -0
- package/dist/src/model-utils.js +18 -1
- package/dist/src/serve/agent-bridge.js +85 -14
- package/dist/src/serve/mcp-server.js +19 -13
- package/dist/src/serve/server.js +166 -3
- package/dist/src/session/session-store.js +18 -0
- package/dist/src/shared/symbol-search.js +156 -0
- package/dist/src/tools/search-code-map.js +27 -35
- package/dist/src/web/app.js +662 -113
- package/dist/src/web/index.html +128 -8
- package/dist/src/web/style.css +189 -7
- package/dist/tests/agent.test.js +16 -0
- package/dist/tests/config-api.test.js +5 -0
- package/dist/tests/config-integration.test.js +91 -1
- package/dist/tests/config.test.js +9 -0
- package/dist/tests/file-tools.test.js +12 -0
- package/dist/tests/graph-onboarding.test.js +20 -0
- package/dist/tests/mcp-and-plugin.test.js +3 -0
- package/dist/tests/model-client-openai.test.js +41 -0
- package/dist/tests/model-dropdown-ui.test.js +23 -0
- package/dist/tests/model-utils.test.js +26 -1
- package/dist/tests/search-code-map.test.js +9 -0
- package/dist/tests/serve.integration.test.js +189 -0
- package/dist/tests/session-store.test.js +15 -1
- package/dist/tests/settings-ui.test.js +11 -0
- package/dist/tests/setup-overlay-state.test.js +49 -0
- package/dist/tests/system-prompt.test.js +1 -0
- package/dist/tests/test-utils.js +1 -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 +10 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts +1 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts +8 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js +164 -27
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -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 +60 -6
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/model-client-openai.test.js +87 -0
- package/node_modules/@minicode/agent-sdk/dist/tests/model-client-openai.test.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.js +1 -0
- package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -42,6 +42,7 @@ test("MCP POST with malformed JSON returns 400 with JSON-RPC error", async () =>
|
|
|
42
42
|
model: "test",
|
|
43
43
|
maxSteps: 10,
|
|
44
44
|
maxTokens: 1024,
|
|
45
|
+
modelTimeoutSeconds: 60,
|
|
45
46
|
maxContextTokens: 4000,
|
|
46
47
|
workspaceRoot: "/tmp/mcp-test",
|
|
47
48
|
commandTimeoutMs: 5000,
|
|
@@ -88,6 +89,7 @@ test("MCP POST without session ID and non-init request returns 400", async () =>
|
|
|
88
89
|
model: "test",
|
|
89
90
|
maxSteps: 10,
|
|
90
91
|
maxTokens: 1024,
|
|
92
|
+
modelTimeoutSeconds: 60,
|
|
91
93
|
maxContextTokens: 4000,
|
|
92
94
|
workspaceRoot: "/tmp/mcp-test",
|
|
93
95
|
commandTimeoutMs: 5000,
|
|
@@ -132,6 +134,7 @@ test("MCP PATCH returns 405", async () => {
|
|
|
132
134
|
model: "test",
|
|
133
135
|
maxSteps: 10,
|
|
134
136
|
maxTokens: 1024,
|
|
137
|
+
modelTimeoutSeconds: 60,
|
|
135
138
|
maxContextTokens: 4000,
|
|
136
139
|
workspaceRoot: "/tmp/mcp-test",
|
|
137
140
|
commandTimeoutMs: 5000,
|
|
@@ -102,6 +102,47 @@ test("openai-compatible client sends correct app URL in HTTP-Referer header", as
|
|
|
102
102
|
assert.equal(capturedHeaders["HTTP-Referer"], "https://minicode.seanholung.com", "HTTP-Referer should point to minicode.seanholung.com");
|
|
103
103
|
assert.equal(capturedHeaders["X-Title"], "minicode");
|
|
104
104
|
});
|
|
105
|
+
test("openai-compatible client repairs missing tool results before sending", 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: "ok", tool_calls: [] },
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
usage: { prompt_tokens: 5, completion_tokens: 3 },
|
|
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: "sys",
|
|
126
|
+
messages: [
|
|
127
|
+
{ role: "user", content: "start" },
|
|
128
|
+
{
|
|
129
|
+
role: "assistant",
|
|
130
|
+
content: "checking",
|
|
131
|
+
toolCalls: [{ id: "call-missing", name: "read_file", input: { path: "src/index.ts" } }],
|
|
132
|
+
},
|
|
133
|
+
{ role: "user", content: "continue" },
|
|
134
|
+
],
|
|
135
|
+
tools: [],
|
|
136
|
+
maxTokens: 64,
|
|
137
|
+
});
|
|
138
|
+
const parsedBody = JSON.parse(capturedBody);
|
|
139
|
+
const messages = parsedBody.messages;
|
|
140
|
+
assert.equal(messages[2]?.role, "assistant");
|
|
141
|
+
assert.equal(messages[3]?.role, "tool");
|
|
142
|
+
assert.equal(messages[3]?.tool_call_id, "call-missing");
|
|
143
|
+
assert.match(String(messages[3]?.content), /Tool result unavailable/);
|
|
144
|
+
assert.equal(messages[4]?.role, "user");
|
|
145
|
+
});
|
|
105
146
|
test("createModelClient returns openai-compatible client", () => {
|
|
106
147
|
const config = {
|
|
107
148
|
...createTestAgentConfig("/tmp"),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
const distWeb = join(import.meta.dirname, "..", "dist", "src", "web");
|
|
6
|
+
test("built HTML contains model search input", () => {
|
|
7
|
+
const html = readFileSync(join(distWeb, "index.html"), "utf8");
|
|
8
|
+
assert.ok(html.includes('id="model-search"'), "HTML should expose a dedicated model search input");
|
|
9
|
+
assert.ok(html.includes('placeholder="Search models..."'), "HTML should prompt users to search models");
|
|
10
|
+
});
|
|
11
|
+
test("built CSS contains searchable model dropdown styling", () => {
|
|
12
|
+
const css = readFileSync(join(distWeb, "style.css"), "utf8");
|
|
13
|
+
assert.ok(css.includes("#model-search"), "CSS should style the model search input");
|
|
14
|
+
assert.ok(css.includes(".model-item-body"), "CSS should support stacked model result content");
|
|
15
|
+
assert.ok(css.includes(".model-item-badge"), "CSS should style the active model badge");
|
|
16
|
+
});
|
|
17
|
+
test("built JS contains searchable model dropdown behavior", () => {
|
|
18
|
+
const js = readFileSync(join(distWeb, "app.js"), "utf8");
|
|
19
|
+
assert.ok(js.includes("filterModelsByQuery"), "JS should filter models by the dropdown query");
|
|
20
|
+
assert.ok(js.includes("focusModelSearchInput"), "JS should focus the search field when opening the dropdown");
|
|
21
|
+
assert.ok(js.includes("No matching models"), "JS should render an empty state for unmatched queries");
|
|
22
|
+
assert.ok(js.includes('modelSearchInput.addEventListener("input"'), "JS should update results as the user types");
|
|
23
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { test } from "node:test";
|
|
3
|
-
import { sortModelsAlphabetically } from "../src/model-utils.js";
|
|
3
|
+
import { filterModelsByQuery, getModelDisplayName, sortModelsAlphabetically } from "../src/model-utils.js";
|
|
4
4
|
test("sortModelsAlphabetically sorts by display name without mutating input", () => {
|
|
5
5
|
const models = [
|
|
6
6
|
{ id: "zeta-2", name: "Zeta 2" },
|
|
@@ -20,3 +20,28 @@ test("sortModelsAlphabetically uses id as a stable tiebreaker", () => {
|
|
|
20
20
|
const sorted = sortModelsAlphabetically(models);
|
|
21
21
|
assert.deepEqual(sorted.map((model) => model.id), ["gpt-4.1-a", "gpt-4.1-b"]);
|
|
22
22
|
});
|
|
23
|
+
test("getModelDisplayName falls back to id and trims whitespace", () => {
|
|
24
|
+
assert.equal(getModelDisplayName({ id: "google/gemini-2.5-flash-preview", name: " Gemini 2.5 Flash " }), "Gemini 2.5 Flash");
|
|
25
|
+
assert.equal(getModelDisplayName({ id: "qwen/qwen3-coder" }), "qwen/qwen3-coder");
|
|
26
|
+
});
|
|
27
|
+
test("filterModelsByQuery matches on display name and id without mutating order", () => {
|
|
28
|
+
const models = [
|
|
29
|
+
{ id: "z-ai/glm-4.5-air", name: "GLM 4.5 Air" },
|
|
30
|
+
{ id: "google/gemini-2.5-flash-preview", name: "Gemini 2.5 Flash" },
|
|
31
|
+
{ id: "openai/gpt-4.1-mini", name: "GPT-4.1 Mini" },
|
|
32
|
+
];
|
|
33
|
+
const byName = filterModelsByQuery(models, "gemini flash");
|
|
34
|
+
const byId = filterModelsByQuery(models, "glm-4.5");
|
|
35
|
+
const blank = filterModelsByQuery(models, " ");
|
|
36
|
+
assert.deepEqual(byName.map((model) => model.id), ["google/gemini-2.5-flash-preview"]);
|
|
37
|
+
assert.deepEqual(byId.map((model) => model.id), ["z-ai/glm-4.5-air"]);
|
|
38
|
+
assert.deepEqual(blank.map((model) => model.id), models.map((model) => model.id));
|
|
39
|
+
});
|
|
40
|
+
test("filterModelsByQuery returns all tokens match across name and id", () => {
|
|
41
|
+
const models = [
|
|
42
|
+
{ id: "google/gemini-2.5-flash-preview", name: "Gemini Flash Preview" },
|
|
43
|
+
{ id: "google/gemini-2.5-pro-preview", name: "Gemini Pro Preview" },
|
|
44
|
+
];
|
|
45
|
+
const filtered = filterModelsByQuery(models, "google flash");
|
|
46
|
+
assert.deepEqual(filtered.map((model) => model.id), ["google/gemini-2.5-flash-preview"]);
|
|
47
|
+
});
|
|
@@ -20,6 +20,15 @@ test("search_code_map returns empty when no match", async () => {
|
|
|
20
20
|
const result = await tool.execute({ pattern: "XyZNoSymbol123" });
|
|
21
21
|
assert.ok(result.includes("No symbols matching"));
|
|
22
22
|
});
|
|
23
|
+
test("search_code_map returns similar matches when exact substring lookup fails", async () => {
|
|
24
|
+
const root = path.resolve(import.meta.dirname, "..");
|
|
25
|
+
const projectIndex = await buildProjectIndex(root);
|
|
26
|
+
const tool = createSearchCodeMapTool(projectIndex);
|
|
27
|
+
const result = await tool.execute({ pattern: "ModelRespnse" });
|
|
28
|
+
assert.ok(result.includes('No exact substring matches for "ModelRespnse"'));
|
|
29
|
+
assert.ok(result.includes("Showing similar symbols instead"));
|
|
30
|
+
assert.ok(result.includes("ModelResponse"));
|
|
31
|
+
});
|
|
23
32
|
test("search_code_map appears in tool registry when projectIndex provided", async () => {
|
|
24
33
|
const root = path.resolve(import.meta.dirname, "..");
|
|
25
34
|
const projectIndex = await buildProjectIndex(root);
|
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { createRequestHandler, shutdownServe } from "../src/serve/server.js";
|
|
8
8
|
import { AgentBridge } from "../src/serve/agent-bridge.js";
|
|
9
9
|
import { Session } from "@minicode/agent-sdk";
|
|
10
|
+
import { DuplicateSessionLabelError } from "../src/session/session-store.js";
|
|
10
11
|
import { createTestAgentConfig } from "./test-utils.js";
|
|
11
12
|
/**
|
|
12
13
|
* Lightweight AgentBridge subclass for testing.
|
|
@@ -20,6 +21,9 @@ class MockBridge extends AgentBridge {
|
|
|
20
21
|
turnHistory = [];
|
|
21
22
|
openRouterKey;
|
|
22
23
|
openRouterSessionActive = false;
|
|
24
|
+
openAiCompatibleApiKey;
|
|
25
|
+
openAiCompatibleSessionActive = false;
|
|
26
|
+
refreshIndexCount = 0;
|
|
23
27
|
constructor() {
|
|
24
28
|
super(() => { }, false);
|
|
25
29
|
}
|
|
@@ -91,10 +95,26 @@ class MockBridge extends AgentBridge {
|
|
|
91
95
|
connectOpenRouter(apiKey) {
|
|
92
96
|
this.openRouterKey = apiKey;
|
|
93
97
|
this.openRouterSessionActive = true;
|
|
98
|
+
this.openAiCompatibleApiKey = undefined;
|
|
99
|
+
this.openAiCompatibleSessionActive = false;
|
|
94
100
|
this._config.modelProvider = "openai-compatible";
|
|
95
101
|
this._config.openAiBaseUrl = "https://openrouter.ai/api/v1";
|
|
96
102
|
this._config.openAiApiKey = apiKey;
|
|
97
103
|
}
|
|
104
|
+
connectOpenAiCompatible(baseUrl, apiKey) {
|
|
105
|
+
this.openAiCompatibleSessionActive = true;
|
|
106
|
+
this.openRouterKey = undefined;
|
|
107
|
+
this.openRouterSessionActive = false;
|
|
108
|
+
this.openAiCompatibleApiKey = apiKey?.trim() || undefined;
|
|
109
|
+
this._config.modelProvider = "openai-compatible";
|
|
110
|
+
this._config.openAiBaseUrl = baseUrl.trim().replace(/\/+$/, "");
|
|
111
|
+
if (this.openAiCompatibleApiKey) {
|
|
112
|
+
this._config.openAiApiKey = this.openAiCompatibleApiKey;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
delete this._config.openAiApiKey;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
98
118
|
disconnectOpenRouter() {
|
|
99
119
|
if (!this.openRouterSessionActive) {
|
|
100
120
|
return false;
|
|
@@ -110,6 +130,21 @@ class MockBridge extends AgentBridge {
|
|
|
110
130
|
isOpenRouterSessionConnected() {
|
|
111
131
|
return this.openRouterSessionActive;
|
|
112
132
|
}
|
|
133
|
+
disconnectOpenAiCompatible() {
|
|
134
|
+
if (!this.openAiCompatibleSessionActive) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
this.openAiCompatibleSessionActive = false;
|
|
138
|
+
this.openAiCompatibleApiKey = undefined;
|
|
139
|
+
this._config.modelProvider = this._baseConfig.modelProvider;
|
|
140
|
+
this._config.model = this._baseConfig.model;
|
|
141
|
+
this._config.openAiBaseUrl = this._baseConfig.openAiBaseUrl;
|
|
142
|
+
delete this._config.openAiApiKey;
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
isOpenAiCompatibleSessionConnected() {
|
|
146
|
+
return this.openAiCompatibleSessionActive;
|
|
147
|
+
}
|
|
113
148
|
switchModel(modelId) {
|
|
114
149
|
this._config.model = modelId;
|
|
115
150
|
}
|
|
@@ -119,6 +154,10 @@ class MockBridge extends AgentBridge {
|
|
|
119
154
|
hasIndex() {
|
|
120
155
|
return true;
|
|
121
156
|
}
|
|
157
|
+
async refreshIndex() {
|
|
158
|
+
this.refreshIndexCount += 1;
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
122
161
|
getSymbols() {
|
|
123
162
|
return [
|
|
124
163
|
{ name: "foo", qualifiedName: "foo", kind: "function", filePath: "src/foo.ts", startLine: 1, endLine: 5, signature: "function foo(): void", exported: true },
|
|
@@ -351,6 +390,11 @@ class EmptySessionsBridge extends MockBridge {
|
|
|
351
390
|
};
|
|
352
391
|
}
|
|
353
392
|
}
|
|
393
|
+
class DuplicateSessionLabelBridge extends MockBridge {
|
|
394
|
+
async saveSess() {
|
|
395
|
+
throw new DuplicateSessionLabelError("test-session", "sess-1");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
354
398
|
// ── Test harness ──
|
|
355
399
|
let activeServer;
|
|
356
400
|
function startTestServer(bridge, options = {}) {
|
|
@@ -436,6 +480,18 @@ test("POST /api/sessions/save saves a session", async () => {
|
|
|
436
480
|
const body = (await res.json());
|
|
437
481
|
assert.equal(body.label, "my-save");
|
|
438
482
|
});
|
|
483
|
+
test("POST /api/sessions/save rejects duplicate session labels", async () => {
|
|
484
|
+
const bridge = new DuplicateSessionLabelBridge();
|
|
485
|
+
const base = await startTestServer(bridge);
|
|
486
|
+
const res = await fetch(`${base}/api/sessions/save`, {
|
|
487
|
+
method: "POST",
|
|
488
|
+
headers: { "Content-Type": "application/json" },
|
|
489
|
+
body: JSON.stringify({ label: "test-session" }),
|
|
490
|
+
});
|
|
491
|
+
assert.equal(res.status, 409);
|
|
492
|
+
const body = (await res.json());
|
|
493
|
+
assert.match(body.error, /already exists/);
|
|
494
|
+
});
|
|
439
495
|
test("session APIs support the first save when no sessions exist yet", async () => {
|
|
440
496
|
const bridge = new EmptySessionsBridge();
|
|
441
497
|
const base = await startTestServer(bridge);
|
|
@@ -653,6 +709,90 @@ test("GET /api/status exposes OpenRouter session state and base URL", async () =
|
|
|
653
709
|
assert.equal(body.baseUrl, "https://openrouter.ai/api/v1");
|
|
654
710
|
assert.equal(body.sessionOpenRouterConnected, true);
|
|
655
711
|
});
|
|
712
|
+
test("POST /api/openai-compatible/connect stores a session-only endpoint", async () => {
|
|
713
|
+
const bridge = new MockBridge();
|
|
714
|
+
const base = await startTestServer(bridge);
|
|
715
|
+
const res = await fetch(`${base}/api/openai-compatible/connect`, {
|
|
716
|
+
method: "POST",
|
|
717
|
+
headers: { "Content-Type": "application/json" },
|
|
718
|
+
body: JSON.stringify({ baseUrl: "http://localhost:1234/v1/", apiKey: "local-key" }),
|
|
719
|
+
});
|
|
720
|
+
assert.equal(res.status, 200);
|
|
721
|
+
const body = await res.json();
|
|
722
|
+
assert.equal(body.ok, true);
|
|
723
|
+
assert.equal(body.sessionOnly, true);
|
|
724
|
+
assert.equal(body.persistedToEnv, false);
|
|
725
|
+
assert.equal(body.persistedEnvPath, null);
|
|
726
|
+
assert.equal(body.persistWarning, null);
|
|
727
|
+
assert.equal(body.baseUrl, "http://localhost:1234/v1");
|
|
728
|
+
assert.equal(body.needsSetup, false);
|
|
729
|
+
assert.deepEqual(body.missing, []);
|
|
730
|
+
assert.equal(body.message, "The OpenAI-compatible provider connected for this serve session.");
|
|
731
|
+
assert.equal(bridge.getConfig().modelProvider, "openai-compatible");
|
|
732
|
+
assert.equal(bridge.getConfig().openAiBaseUrl, "http://localhost:1234/v1");
|
|
733
|
+
assert.equal(bridge.getConfig().openAiApiKey, "local-key");
|
|
734
|
+
assert.equal(bridge.isOpenAiCompatibleSessionConnected(), true);
|
|
735
|
+
});
|
|
736
|
+
test("POST /api/openai-compatible/connect can persist the endpoint to ~/.minicode/.env", async () => {
|
|
737
|
+
const bridge = new MockBridge();
|
|
738
|
+
const minicodeHome = mkdtempSync(path.join(os.tmpdir(), "minicode-openai-compatible-home-"));
|
|
739
|
+
const base = await startTestServer(bridge, { minicodeHome });
|
|
740
|
+
try {
|
|
741
|
+
const res = await fetch(`${base}/api/openai-compatible/connect`, {
|
|
742
|
+
method: "POST",
|
|
743
|
+
headers: { "Content-Type": "application/json" },
|
|
744
|
+
body: JSON.stringify({
|
|
745
|
+
baseUrl: "http://127.0.0.1:1234/v1",
|
|
746
|
+
apiKey: "",
|
|
747
|
+
persistToEnv: true,
|
|
748
|
+
}),
|
|
749
|
+
});
|
|
750
|
+
assert.equal(res.status, 200);
|
|
751
|
+
const body = await res.json();
|
|
752
|
+
assert.equal(body.persistedToEnv, true);
|
|
753
|
+
assert.equal(body.persistWarning, null);
|
|
754
|
+
assert.equal(body.persistedEnvPath, path.join(minicodeHome, ".env"));
|
|
755
|
+
assert.match(body.message, /saved to ~\/\.minicode\/\.env/);
|
|
756
|
+
const envContents = readFileSync(path.join(minicodeHome, ".env"), "utf8");
|
|
757
|
+
assert.match(envContents, /^MODEL_PROVIDER=openai-compatible$/m);
|
|
758
|
+
assert.match(envContents, /^OPENAI_BASE_URL=http:\/\/127\.0\.0\.1:1234\/v1$/m);
|
|
759
|
+
assert.doesNotMatch(envContents, /^OPENAI_API_KEY=/m);
|
|
760
|
+
}
|
|
761
|
+
finally {
|
|
762
|
+
rmSync(minicodeHome, { recursive: true, force: true });
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
test("POST /api/openai-compatible/disconnect removes the session-only endpoint", async () => {
|
|
766
|
+
const bridge = new MockBridge();
|
|
767
|
+
bridge.connectOpenAiCompatible("http://localhost:1234/v1", "local-key");
|
|
768
|
+
const base = await startTestServer(bridge);
|
|
769
|
+
const res = await fetch(`${base}/api/openai-compatible/disconnect`, {
|
|
770
|
+
method: "POST",
|
|
771
|
+
headers: { "Content-Type": "application/json" },
|
|
772
|
+
});
|
|
773
|
+
assert.equal(res.status, 200);
|
|
774
|
+
const body = await res.json();
|
|
775
|
+
assert.equal(body.ok, true);
|
|
776
|
+
assert.equal(body.disconnected, true);
|
|
777
|
+
assert.equal(body.sessionOnly, true);
|
|
778
|
+
assert.equal(body.provider, "anthropic");
|
|
779
|
+
assert.equal(body.baseUrl, "http://localhost:1234/v1");
|
|
780
|
+
assert.equal(body.message, "Removed the session-only OpenAI-compatible connection and restored your original provider settings.");
|
|
781
|
+
assert.equal(bridge.isOpenAiCompatibleSessionConnected(), false);
|
|
782
|
+
assert.equal(bridge.getConfig().modelProvider, "anthropic");
|
|
783
|
+
assert.equal(bridge.getConfig().openAiApiKey, undefined);
|
|
784
|
+
});
|
|
785
|
+
test("GET /api/status exposes OpenAI-compatible session state and base URL", async () => {
|
|
786
|
+
const bridge = new MockBridge();
|
|
787
|
+
bridge.connectOpenAiCompatible("http://localhost:1234/v1");
|
|
788
|
+
const base = await startTestServer(bridge);
|
|
789
|
+
const res = await fetch(`${base}/api/status`);
|
|
790
|
+
assert.equal(res.status, 200);
|
|
791
|
+
const body = await res.json();
|
|
792
|
+
assert.equal(body.provider, "openai-compatible");
|
|
793
|
+
assert.equal(body.baseUrl, "http://localhost:1234/v1");
|
|
794
|
+
assert.equal(body.sessionOpenAiCompatibleConnected, true);
|
|
795
|
+
});
|
|
656
796
|
test("POST /api/openrouter/connect returns 400 when code is missing", async () => {
|
|
657
797
|
const bridge = new MockBridge();
|
|
658
798
|
const base = await startTestServer(bridge);
|
|
@@ -688,6 +828,18 @@ test("POST /api/openrouter/connect surfaces exchange failures", async () => {
|
|
|
688
828
|
globalThis.fetch = originalFetch;
|
|
689
829
|
}
|
|
690
830
|
});
|
|
831
|
+
test("POST /api/openai-compatible/connect rejects an invalid endpoint", async () => {
|
|
832
|
+
const bridge = new MockBridge();
|
|
833
|
+
const base = await startTestServer(bridge);
|
|
834
|
+
const res = await fetch(`${base}/api/openai-compatible/connect`, {
|
|
835
|
+
method: "POST",
|
|
836
|
+
headers: { "Content-Type": "application/json" },
|
|
837
|
+
body: JSON.stringify({ baseUrl: "localhost:1234/v1" }),
|
|
838
|
+
});
|
|
839
|
+
assert.equal(res.status, 400);
|
|
840
|
+
const body = await res.json();
|
|
841
|
+
assert.match(body.error, /valid absolute http\(s\) URL/);
|
|
842
|
+
});
|
|
691
843
|
// ── OpenAI-compatible API tests ──
|
|
692
844
|
test("GET /v1/models returns minicode-agent model", async () => {
|
|
693
845
|
const bridge = new MockBridge();
|
|
@@ -928,6 +1080,17 @@ test("GET /api/graph returns nodes and edges", async () => {
|
|
|
928
1080
|
assert.equal(body.edges[0].to, "Bar");
|
|
929
1081
|
assert.equal(body.edges[0].kind, "calls");
|
|
930
1082
|
});
|
|
1083
|
+
test("POST /api/index/refresh rebuilds the project index", async () => {
|
|
1084
|
+
const bridge = new MockBridge();
|
|
1085
|
+
const base = await startTestServer(bridge);
|
|
1086
|
+
const res = await fetch(`${base}/api/index/refresh`, { method: "POST" });
|
|
1087
|
+
assert.equal(res.status, 200);
|
|
1088
|
+
const body = (await res.json());
|
|
1089
|
+
assert.equal(body.ok, true);
|
|
1090
|
+
assert.equal(body.symbolCount, 2);
|
|
1091
|
+
assert.equal(body.edgeCount, 1);
|
|
1092
|
+
assert.equal(bridge.refreshIndexCount, 1);
|
|
1093
|
+
});
|
|
931
1094
|
test("GET /api/analysis returns deterministic structural findings", async () => {
|
|
932
1095
|
const bridge = new MockBridge();
|
|
933
1096
|
const base = await startTestServer(bridge);
|
|
@@ -1079,6 +1242,32 @@ test("GET /api/symbols/:name/source returns 500 when file is missing", async ()
|
|
|
1079
1242
|
const body = (await res.json());
|
|
1080
1243
|
assert.ok(body.error.includes("src/foo.ts"));
|
|
1081
1244
|
});
|
|
1245
|
+
test("GET /api/file-source returns full file contents for a workspace file", async () => {
|
|
1246
|
+
const bridge = new MockBridge();
|
|
1247
|
+
const base = await startTestServer(bridge);
|
|
1248
|
+
const wsRoot = "/tmp/test-workspace";
|
|
1249
|
+
mkdirSync(`${wsRoot}/src`, { recursive: true });
|
|
1250
|
+
writeFileSync(`${wsRoot}/src/foo.ts`, "line1\nfunction foo(): void {\n return;\n}\nline5\n");
|
|
1251
|
+
try {
|
|
1252
|
+
const res = await fetch(`${base}/api/file-source?path=${encodeURIComponent("src/foo.ts")}`);
|
|
1253
|
+
assert.equal(res.status, 200);
|
|
1254
|
+
const body = (await res.json());
|
|
1255
|
+
assert.equal(body.filePath, "src/foo.ts");
|
|
1256
|
+
assert.ok(body.source.includes("function foo(): void"));
|
|
1257
|
+
assert.ok(body.source.includes("line5"));
|
|
1258
|
+
}
|
|
1259
|
+
finally {
|
|
1260
|
+
rmSync(wsRoot, { recursive: true, force: true });
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
test("GET /api/file-source rejects path traversal", async () => {
|
|
1264
|
+
const bridge = new MockBridge();
|
|
1265
|
+
const base = await startTestServer(bridge);
|
|
1266
|
+
const res = await fetch(`${base}/api/file-source?path=${encodeURIComponent("../secret.txt")}`);
|
|
1267
|
+
assert.equal(res.status, 403);
|
|
1268
|
+
const body = (await res.json());
|
|
1269
|
+
assert.ok(body.error.includes("Invalid workspace file path"));
|
|
1270
|
+
});
|
|
1082
1271
|
// ── Annotations API tests ──
|
|
1083
1272
|
test("GET /api/annotations returns empty annotations initially", async () => {
|
|
1084
1273
|
const bridge = new MockBridge();
|
|
@@ -4,7 +4,7 @@ import { mkdtemp, readdir, rm } from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { Session } from "@minicode/agent-sdk";
|
|
7
|
-
import { listSessions, loadSession, loadSessionByLabel, saveSession, setSessionsDir, } from "../src/session/session-store.js";
|
|
7
|
+
import { DuplicateSessionLabelError, listSessions, loadSession, loadSessionByLabel, saveSession, setSessionsDir, } from "../src/session/session-store.js";
|
|
8
8
|
async function withTmpDir(fn) {
|
|
9
9
|
const dir = await mkdtemp(path.join(os.tmpdir(), "minicode-test-"));
|
|
10
10
|
setSessionsDir(dir);
|
|
@@ -98,6 +98,20 @@ test("loadSessionByLabel returns undefined for no match", async () => {
|
|
|
98
98
|
assert.equal(result, undefined);
|
|
99
99
|
});
|
|
100
100
|
});
|
|
101
|
+
test("saveSession rejects duplicate labels for different sessions", async () => {
|
|
102
|
+
await withTmpDir(async (dir) => {
|
|
103
|
+
const firstSession = new Session("first-id");
|
|
104
|
+
firstSession.addMessage({ role: "user", content: "first" });
|
|
105
|
+
await saveSession(firstSession, "My Label");
|
|
106
|
+
const secondSession = new Session("second-id");
|
|
107
|
+
secondSession.addMessage({ role: "user", content: "second" });
|
|
108
|
+
await assert.rejects(() => saveSession(secondSession, " my label "), (error) => error instanceof DuplicateSessionLabelError &&
|
|
109
|
+
error.label === "my label" &&
|
|
110
|
+
error.existingSessionId === "first-id");
|
|
111
|
+
const files = await readdir(dir);
|
|
112
|
+
assert.deepEqual(files, ["first-id.json"]);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
101
115
|
test("saving same session twice overwrites the file", async () => {
|
|
102
116
|
await withTmpDir(async (dir) => {
|
|
103
117
|
const session = new Session("test-id");
|
|
@@ -8,11 +8,15 @@ test("built HTML contains settings entry point and modal shell", () => {
|
|
|
8
8
|
assert.ok(html.includes('id="settings-btn"'), "HTML should contain the settings button");
|
|
9
9
|
assert.ok(html.includes('id="settings-modal"'), "HTML should contain the settings modal");
|
|
10
10
|
assert.ok(html.includes('id="connect-openrouter-btn"'), "HTML should contain the OpenRouter connect button");
|
|
11
|
+
assert.ok(html.includes('id="connect-openai-compatible-btn"'), "HTML should contain the OpenAI-compatible connect button");
|
|
11
12
|
assert.ok(html.includes('id="config-overlay-intro"'), "HTML should contain the setup overlay intro copy");
|
|
12
13
|
assert.ok(html.includes("Try minicode for free with OpenRouter"), "HTML should promote the free OpenRouter quick start");
|
|
13
14
|
assert.ok(html.includes('id="openrouter-connect-modal"'), "HTML should contain the OpenRouter consent modal");
|
|
15
|
+
assert.ok(html.includes('id="openai-compatible-connect-modal"'), "HTML should contain the OpenAI-compatible connect modal");
|
|
16
|
+
assert.ok(html.includes('id="openai-compatible-preset"'), "HTML should contain the OpenAI-compatible preset selector");
|
|
14
17
|
assert.ok(html.includes('id="openrouter-persist-checkbox"'), "HTML should contain the OpenRouter persistence checkbox");
|
|
15
18
|
assert.ok(html.includes('id="disconnect-openrouter-btn"'), "HTML should contain the OpenRouter disconnect button");
|
|
19
|
+
assert.ok(html.includes('id="disconnect-openai-compatible-btn"'), "HTML should contain the OpenAI-compatible disconnect button");
|
|
16
20
|
assert.ok(!html.includes('id="settings-scope"'), "HTML should no longer contain the settings scope selector");
|
|
17
21
|
assert.ok(html.includes('id="settings-save"'), "HTML should contain the settings save action");
|
|
18
22
|
});
|
|
@@ -24,9 +28,12 @@ test("built CSS contains modal and settings layout styles", () => {
|
|
|
24
28
|
assert.ok(css.includes(".settings-help-warning"), "CSS should contain warning styling for env overrides");
|
|
25
29
|
assert.ok(css.includes("align-items: flex-start;"), "CSS should top-align scrollable setup overlay content");
|
|
26
30
|
assert.ok(css.includes(".config-overlay-spotlight"), "CSS should style the OpenRouter quick-start spotlight");
|
|
31
|
+
assert.ok(css.includes(".config-overlay-shortcuts"), "CSS should lay out the setup quick-connect cards");
|
|
27
32
|
assert.ok(css.includes(".openrouter-connect-body"), "CSS should style the OpenRouter consent modal body");
|
|
33
|
+
assert.ok(css.includes(".provider-input"), "CSS should style provider connection form inputs");
|
|
28
34
|
assert.ok(css.includes(".config-connect-status.success"), "CSS should style OpenRouter connect success state");
|
|
29
35
|
assert.ok(css.includes(".settings-session-banner"), "CSS should style the OpenRouter session banner");
|
|
36
|
+
assert.ok(css.includes(".settings-provider-shortcuts"), "CSS should style provider shortcuts inside settings");
|
|
30
37
|
assert.ok(css.includes("body.modal-open"), "CSS should lock scroll while the settings modal is open");
|
|
31
38
|
});
|
|
32
39
|
test("built JS contains config loading and saving logic for settings", () => {
|
|
@@ -34,11 +41,15 @@ test("built JS contains config loading and saving logic for settings", () => {
|
|
|
34
41
|
assert.ok(js.includes("/api/config"), "JS should fetch the config API");
|
|
35
42
|
assert.ok(js.includes("/api/openrouter/connect"), "JS should call the OpenRouter connect API");
|
|
36
43
|
assert.ok(js.includes("/api/openrouter/disconnect"), "JS should call the OpenRouter disconnect API");
|
|
44
|
+
assert.ok(js.includes("/api/openai-compatible/connect"), "JS should call the OpenAI-compatible connect API");
|
|
45
|
+
assert.ok(js.includes("/api/openai-compatible/disconnect"), "JS should call the OpenAI-compatible disconnect API");
|
|
37
46
|
assert.ok(js.includes("persistToHomeEnv"), "JS should support persisting the selected model after OpenRouter setup");
|
|
38
47
|
assert.ok(js.includes("code_challenge_method"), "JS should generate an OpenRouter PKCE auth request");
|
|
39
48
|
assert.ok(js.includes("sessionStorage"), "JS should persist the PKCE verifier for the OAuth callback");
|
|
40
49
|
assert.ok(js.includes("minicode:openrouter:persist-to-env"), "JS should persist the optional OpenRouter env-write choice across OAuth");
|
|
41
50
|
assert.ok(js.includes("sessionOpenRouterConnected"), "JS should track session-only OpenRouter state");
|
|
51
|
+
assert.ok(js.includes("sessionOpenAiCompatibleConnected"), "JS should track session-only OpenAI-compatible state");
|
|
52
|
+
assert.ok(js.includes("OPENAI_COMPATIBLE_PRESETS"), "JS should include OpenAI-compatible endpoint presets");
|
|
42
53
|
assert.ok(js.includes("Save settings"), "JS should contain the settings save action text");
|
|
43
54
|
assert.ok(js.includes("settingsPayload"), "JS should track settings payload state");
|
|
44
55
|
assert.ok(js.includes("persistedValue"), "JS should wire persisted settings behavior");
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { DEFAULT_SETUP_INTRO, deriveSetupOverlayState } from "../src/web/setup-overlay-state.js";
|
|
4
|
+
test("fresh install hides MODEL missing copy until a provider is configured", () => {
|
|
5
|
+
const state = deriveSetupOverlayState({
|
|
6
|
+
configuredProvider: null,
|
|
7
|
+
missing: ["MODEL is not set"],
|
|
8
|
+
});
|
|
9
|
+
assert.equal(state.introText, DEFAULT_SETUP_INTRO);
|
|
10
|
+
assert.equal(state.hideQuickConnects, false);
|
|
11
|
+
assert.equal(state.hideOpenRouterSpotlight, false);
|
|
12
|
+
assert.deepEqual(state.missingItems, []);
|
|
13
|
+
assert.equal(state.showModelSelectionHint, false);
|
|
14
|
+
assert.equal(state.modelSelectionNote, null);
|
|
15
|
+
});
|
|
16
|
+
test("configured OpenAI-compatible provider keeps model guidance visible", () => {
|
|
17
|
+
const state = deriveSetupOverlayState({
|
|
18
|
+
configuredProvider: "openai-compatible",
|
|
19
|
+
missing: ["MODEL is not set"],
|
|
20
|
+
});
|
|
21
|
+
assert.equal(state.introText, "An OpenAI-compatible provider is already configured. Select a model to continue:");
|
|
22
|
+
assert.equal(state.hideQuickConnects, true);
|
|
23
|
+
assert.equal(state.hideOpenRouterSpotlight, false);
|
|
24
|
+
assert.deepEqual(state.missingItems, ["MODEL is not set"]);
|
|
25
|
+
assert.equal(state.showModelSelectionHint, true);
|
|
26
|
+
assert.equal(state.modelSelectionNote, null);
|
|
27
|
+
});
|
|
28
|
+
test("configured OpenRouter provider keeps model guidance and hides spotlight", () => {
|
|
29
|
+
const state = deriveSetupOverlayState({
|
|
30
|
+
configuredProvider: "openrouter",
|
|
31
|
+
missing: ["MODEL is not set"],
|
|
32
|
+
});
|
|
33
|
+
assert.equal(state.introText, "OpenRouter is already configured. Select a model to continue:");
|
|
34
|
+
assert.equal(state.hideQuickConnects, true);
|
|
35
|
+
assert.equal(state.hideOpenRouterSpotlight, true);
|
|
36
|
+
assert.deepEqual(state.missingItems, ["MODEL is not set"]);
|
|
37
|
+
assert.equal(state.showModelSelectionHint, true);
|
|
38
|
+
assert.equal(state.modelSelectionNote, 'If you are on the OpenRouter free tier, search "free" in the model dropdown to find supported free models.');
|
|
39
|
+
});
|
|
40
|
+
test("non-model missing items still surface before provider selection", () => {
|
|
41
|
+
const state = deriveSetupOverlayState({
|
|
42
|
+
configuredProvider: null,
|
|
43
|
+
missing: ["SOME_OTHER_SETTING is not set"],
|
|
44
|
+
});
|
|
45
|
+
assert.equal(state.introText, DEFAULT_SETUP_INTRO);
|
|
46
|
+
assert.deepEqual(state.missingItems, ["SOME_OTHER_SETTING is not set"]);
|
|
47
|
+
assert.equal(state.showModelSelectionHint, false);
|
|
48
|
+
assert.equal(state.modelSelectionNote, null);
|
|
49
|
+
});
|
package/dist/tests/test-utils.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../src/agent/agent.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../src/agent/agent.ts"],"names":[],"mappings":"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"}
|
|
@@ -343,7 +343,8 @@ export class CodingAgent {
|
|
|
343
343
|
content: thinkingContent,
|
|
344
344
|
toolCalls: response.toolCalls,
|
|
345
345
|
});
|
|
346
|
-
for (
|
|
346
|
+
for (let toolCallIndex = 0; toolCallIndex < response.toolCalls.length; toolCallIndex += 1) {
|
|
347
|
+
const toolCall = response.toolCalls[toolCallIndex];
|
|
347
348
|
const fingerprint = signatureForToolCall(toolCall);
|
|
348
349
|
recentToolCallFingerprints.push(fingerprint);
|
|
349
350
|
if (recentToolCallFingerprints.length >
|
|
@@ -353,6 +354,14 @@ export class CodingAgent {
|
|
|
353
354
|
const repeatedCalls = recentToolCallFingerprints.filter((value) => value === fingerprint).length;
|
|
354
355
|
if (repeatedCalls >= 3) {
|
|
355
356
|
const loopMessage = "Stopped due to repeated identical tool calls. Please refine the prompt or provide additional constraints.";
|
|
357
|
+
for (const skippedToolCall of response.toolCalls.slice(toolCallIndex)) {
|
|
358
|
+
this.session.addMessage({
|
|
359
|
+
role: "tool",
|
|
360
|
+
toolCallId: skippedToolCall.id,
|
|
361
|
+
toolName: skippedToolCall.name,
|
|
362
|
+
content: `Tool skipped: ${loopMessage}`,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
356
365
|
this.session.addMessage({
|
|
357
366
|
role: "assistant",
|
|
358
367
|
content: loopMessage,
|