@sean.holung/minicode 0.3.5 → 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.
@@ -0,0 +1,58 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { buildFileFocusedSelection, buildGraphEdgeId, buildGraphEdgeIndex, } from "../src/shared/graph-selection.js";
4
+ test("buildGraphEdgeIndex indexes edges by both source and target", () => {
5
+ const edges = [
6
+ { source: "Review#class", target: "Session.trim", kind: "references" },
7
+ { source: "renderReview", target: "Review#type", kind: "references" },
8
+ ];
9
+ const edgeIndex = buildGraphEdgeIndex(edges);
10
+ assert.deepEqual(edgeIndex.get("Review#class"), [edges[0]]);
11
+ assert.deepEqual(edgeIndex.get("Session.trim"), [edges[0]]);
12
+ assert.deepEqual(edgeIndex.get("renderReview"), [edges[1]]);
13
+ assert.deepEqual(edgeIndex.get("Review#type"), [edges[1]]);
14
+ });
15
+ test("buildFileFocusedSelection includes file symbols and touching neighbors", () => {
16
+ const fileIndex = new Map([
17
+ ["src/review.ts", ["Review#class", "Review#type"]],
18
+ ]);
19
+ const edges = [
20
+ { source: "Review#class", target: "Session.trim", kind: "references" },
21
+ { source: "renderReview", target: "Review#type", kind: "references" },
22
+ { source: "Review#class", target: "Review#type", kind: "references" },
23
+ ];
24
+ const selection = buildFileFocusedSelection({
25
+ filePath: "src/review.ts",
26
+ fileIndex,
27
+ edgeIndex: buildGraphEdgeIndex(edges),
28
+ });
29
+ assert.deepEqual(new Set(selection.nodeIds), new Set(["Review#class", "Review#type", "Session.trim", "renderReview"]));
30
+ assert.deepEqual(new Set(selection.edges.map((edge) => buildGraphEdgeId(edge))), new Set(edges.map((edge) => buildGraphEdgeId(edge))));
31
+ });
32
+ test("buildFileFocusedSelection excludes edges between external neighbors", () => {
33
+ const fileIndex = new Map([
34
+ ["src/review.ts", ["Review#class"]],
35
+ ]);
36
+ const reviewToSession = { source: "Review#class", target: "Session.trim", kind: "references" };
37
+ const reviewToRender = { source: "Review#class", target: "renderReview", kind: "references" };
38
+ const sessionToRender = { source: "Session.trim", target: "renderReview", kind: "references" };
39
+ const edges = [reviewToSession, reviewToRender, sessionToRender];
40
+ const selection = buildFileFocusedSelection({
41
+ filePath: "src/review.ts",
42
+ fileIndex,
43
+ edgeIndex: buildGraphEdgeIndex(edges),
44
+ });
45
+ assert.deepEqual(new Set(selection.nodeIds), new Set(["Review#class", "Session.trim", "renderReview"]));
46
+ assert.deepEqual(new Set(selection.edges.map((edge) => buildGraphEdgeId(edge))), new Set([
47
+ buildGraphEdgeId(reviewToSession),
48
+ buildGraphEdgeId(reviewToRender),
49
+ ]));
50
+ });
51
+ test("buildFileFocusedSelection returns an empty selection for unknown files", () => {
52
+ const selection = buildFileFocusedSelection({
53
+ filePath: "src/missing.ts",
54
+ fileIndex: new Map(),
55
+ edgeIndex: buildGraphEdgeIndex([]),
56
+ });
57
+ assert.deepEqual(selection, { nodeIds: [], edges: [] });
58
+ });
@@ -0,0 +1,56 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { afterEach, test } from "node:test";
6
+ import { getHomeEnvPath, upsertHomeEnvValues } from "../src/agent/home-env.js";
7
+ const tempDirs = [];
8
+ afterEach(async () => {
9
+ await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
10
+ });
11
+ test("upsertHomeEnvValues creates ~/.minicode/.env when missing", async () => {
12
+ const minicodeHome = await mkdtemp(path.join(os.tmpdir(), "minicode-home-env-"));
13
+ tempDirs.push(minicodeHome);
14
+ const result = await upsertHomeEnvValues({
15
+ minicodeHome,
16
+ values: {
17
+ MODEL_PROVIDER: "openai-compatible",
18
+ OPENAI_BASE_URL: "https://openrouter.ai/api/v1",
19
+ OPENROUTER_API_KEY: "sk-or-v1-test",
20
+ },
21
+ });
22
+ assert.equal(result.path, getHomeEnvPath(minicodeHome));
23
+ const contents = await readFile(result.path, "utf8");
24
+ assert.match(contents, /^MODEL_PROVIDER=openai-compatible/m);
25
+ assert.match(contents, /^OPENAI_BASE_URL=https:\/\/openrouter\.ai\/api\/v1/m);
26
+ assert.match(contents, /^OPENROUTER_API_KEY=sk-or-v1-test/m);
27
+ });
28
+ test("upsertHomeEnvValues replaces existing keys and preserves unrelated lines", async () => {
29
+ const minicodeHome = await mkdtemp(path.join(os.tmpdir(), "minicode-home-env-"));
30
+ tempDirs.push(minicodeHome);
31
+ const envPath = getHomeEnvPath(minicodeHome);
32
+ await writeFile(envPath, [
33
+ "# Existing config",
34
+ "MODEL_PROVIDER=anthropic",
35
+ "OPENAI_BASE_URL=https://example.invalid/v1",
36
+ "OPENROUTER_API_KEY=old-key",
37
+ "OPENROUTER_API_KEY=older-key",
38
+ "MODEL=existing-model",
39
+ "",
40
+ ].join("\n"), "utf8");
41
+ await upsertHomeEnvValues({
42
+ minicodeHome,
43
+ values: {
44
+ MODEL_PROVIDER: "openai-compatible",
45
+ OPENAI_BASE_URL: "https://openrouter.ai/api/v1",
46
+ OPENROUTER_API_KEY: "sk-or-v1-new",
47
+ },
48
+ });
49
+ const contents = await readFile(envPath, "utf8");
50
+ assert.match(contents, /^# Existing config/m);
51
+ assert.match(contents, /^MODEL_PROVIDER=openai-compatible$/m);
52
+ assert.match(contents, /^OPENAI_BASE_URL=https:\/\/openrouter\.ai\/api\/v1$/m);
53
+ assert.match(contents, /^OPENROUTER_API_KEY=sk-or-v1-new$/m);
54
+ assert.match(contents, /^MODEL=existing-model$/m);
55
+ assert.equal(contents.match(/^OPENROUTER_API_KEY=/gm)?.length, 1);
56
+ });
@@ -1,9 +1,12 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { test, afterEach } from "node:test";
3
3
  import { createServer } from "node:http";
4
- import { mkdirSync, writeFileSync, rmSync } from "node:fs";
4
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync, rmSync } from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
5
7
  import { createRequestHandler, shutdownServe } from "../src/serve/server.js";
6
8
  import { AgentBridge } from "../src/serve/agent-bridge.js";
9
+ import { Session } from "@minicode/agent-sdk";
7
10
  import { createTestAgentConfig } from "./test-utils.js";
8
11
  /**
9
12
  * Lightweight AgentBridge subclass for testing.
@@ -12,7 +15,11 @@ import { createTestAgentConfig } from "./test-utils.js";
12
15
  class MockBridge extends AgentBridge {
13
16
  _busy = false;
14
17
  _currentSessionId = "sess-1";
18
+ _baseConfig = createTestAgentConfig("/tmp/test-workspace");
19
+ _config = createTestAgentConfig("/tmp/test-workspace");
15
20
  turnHistory = [];
21
+ openRouterKey;
22
+ openRouterSessionActive = false;
16
23
  constructor() {
17
24
  super(() => { }, false);
18
25
  }
@@ -20,7 +27,7 @@ class MockBridge extends AgentBridge {
20
27
  return this._busy;
21
28
  }
22
29
  getConfig() {
23
- return createTestAgentConfig("/tmp/test-workspace");
30
+ return this._config;
24
31
  }
25
32
  async runTurn(message) {
26
33
  if (this._busy)
@@ -65,11 +72,47 @@ class MockBridge extends AgentBridge {
65
72
  async loadSess(label) {
66
73
  if (label === "nonexistent")
67
74
  return null;
68
- return { session: {}, label };
75
+ const session = new Session("sess-1");
76
+ session.addMessage({
77
+ role: "user",
78
+ content: "[Conversation Summary — earlier messages were compacted to save context]\nOlder summary",
79
+ });
80
+ for (let i = 1; i <= 11; i += 1) {
81
+ session.addMessage({
82
+ role: i % 2 === 0 ? "assistant" : "user",
83
+ content: `message-${i}`,
84
+ });
85
+ }
86
+ return { session, label };
69
87
  }
70
88
  getCurrentSessionId() {
71
89
  return this._currentSessionId;
72
90
  }
91
+ connectOpenRouter(apiKey) {
92
+ this.openRouterKey = apiKey;
93
+ this.openRouterSessionActive = true;
94
+ this._config.modelProvider = "openai-compatible";
95
+ this._config.openAiBaseUrl = "https://openrouter.ai/api/v1";
96
+ this._config.openAiApiKey = apiKey;
97
+ }
98
+ disconnectOpenRouter() {
99
+ if (!this.openRouterSessionActive) {
100
+ return false;
101
+ }
102
+ this.openRouterSessionActive = false;
103
+ this.openRouterKey = undefined;
104
+ this._config.modelProvider = this._baseConfig.modelProvider;
105
+ this._config.model = this._baseConfig.model;
106
+ this._config.openAiBaseUrl = this._baseConfig.openAiBaseUrl;
107
+ delete this._config.openAiApiKey;
108
+ return true;
109
+ }
110
+ isOpenRouterSessionConnected() {
111
+ return this.openRouterSessionActive;
112
+ }
113
+ switchModel(modelId) {
114
+ this._config.model = modelId;
115
+ }
73
116
  setBusy(busy) {
74
117
  this._busy = busy;
75
118
  }
@@ -310,8 +353,8 @@ class EmptySessionsBridge extends MockBridge {
310
353
  }
311
354
  // ── Test harness ──
312
355
  let activeServer;
313
- function startTestServer(bridge) {
314
- const handler = createRequestHandler(bridge);
356
+ function startTestServer(bridge, options = {}) {
357
+ const handler = createRequestHandler(bridge, undefined, options);
315
358
  const server = createServer(handler);
316
359
  activeServer = server;
317
360
  return new Promise((resolve) => {
@@ -367,7 +410,7 @@ test("GET /api/config returns formatted config plus structured settings", async
367
410
  assert.ok(body.config.includes("test-model"));
368
411
  assert.equal(body.restartRequired, true);
369
412
  assert.equal(body.secretsUiSupported, false);
370
- assert.ok(body.settings.configPath.endsWith("/.minicode/agent.config.json"));
413
+ assert.ok(body.settings.configPath.endsWith("/.minicode/.env"));
371
414
  assert.ok(body.settings.entries.some((entry) => entry.key === "maxSteps"));
372
415
  });
373
416
  test("GET /api/sessions returns session list", async () => {
@@ -432,6 +475,10 @@ test("POST /api/sessions/load returns success for known session", async () => {
432
475
  assert.equal(res.status, 200);
433
476
  const body = (await res.json());
434
477
  assert.equal(body.label, "test-session");
478
+ assert.equal(body.messages.length, 10);
479
+ assert.equal(body.messages[0]?.content, "message-2");
480
+ assert.equal(body.messages[9]?.content, "message-11");
481
+ assert.ok(body.messages.every((message) => !message.content.startsWith("[Conversation Summary")));
435
482
  });
436
483
  test("POST /api/chat returns agent response", async () => {
437
484
  const bridge = new MockBridge();
@@ -468,6 +515,179 @@ test("POST /api/chat returns 429 when agent is busy", async () => {
468
515
  });
469
516
  assert.equal(res.status, 429);
470
517
  });
518
+ test("POST /api/openrouter/connect exchanges code and stores a session-only key", async () => {
519
+ const bridge = new MockBridge();
520
+ const base = await startTestServer(bridge);
521
+ const originalFetch = globalThis.fetch;
522
+ globalThis.fetch = async (input, init) => {
523
+ if (String(input) !== "https://openrouter.ai/api/v1/auth/keys") {
524
+ return originalFetch(input, init);
525
+ }
526
+ assert.equal(init?.method, "POST");
527
+ assert.equal((init?.headers)["Content-Type"], "application/json");
528
+ const body = JSON.parse(String(init?.body));
529
+ assert.equal(body.code, "oauth-code");
530
+ assert.equal(body.code_verifier, "pkce-verifier");
531
+ assert.equal(body.code_challenge_method, "S256");
532
+ return new Response(JSON.stringify({ key: "sk-or-v1-session-key" }), { status: 200, headers: { "content-type": "application/json" } });
533
+ };
534
+ try {
535
+ const res = await originalFetch(`${base}/api/openrouter/connect`, {
536
+ method: "POST",
537
+ headers: { "Content-Type": "application/json" },
538
+ body: JSON.stringify({ code: "oauth-code", codeVerifier: "pkce-verifier" }),
539
+ });
540
+ assert.equal(res.status, 200);
541
+ const body = await res.json();
542
+ assert.equal(body.ok, true);
543
+ assert.equal(body.sessionOnly, true);
544
+ assert.equal(body.persistedToEnv, false);
545
+ assert.equal(body.persistedEnvPath, null);
546
+ assert.equal(body.persistWarning, null);
547
+ assert.equal(body.baseUrl, "https://openrouter.ai/api/v1");
548
+ assert.equal(body.needsSetup, false);
549
+ assert.deepEqual(body.missing, []);
550
+ assert.equal(body.message, "OpenRouter connected for this serve session.");
551
+ assert.equal(bridge.openRouterKey, "sk-or-v1-session-key");
552
+ assert.equal(bridge.getConfig().modelProvider, "openai-compatible");
553
+ assert.equal(bridge.getConfig().openAiBaseUrl, "https://openrouter.ai/api/v1");
554
+ assert.equal(bridge.getConfig().openAiApiKey, "sk-or-v1-session-key");
555
+ }
556
+ finally {
557
+ globalThis.fetch = originalFetch;
558
+ }
559
+ });
560
+ test("POST /api/openrouter/connect can persist OpenRouter setup to ~/.minicode/.env", async () => {
561
+ const bridge = new MockBridge();
562
+ const minicodeHome = mkdtempSync(path.join(os.tmpdir(), "minicode-openrouter-home-"));
563
+ const base = await startTestServer(bridge, { minicodeHome });
564
+ const originalFetch = globalThis.fetch;
565
+ globalThis.fetch = async (input, init) => {
566
+ if (String(input) !== "https://openrouter.ai/api/v1/auth/keys") {
567
+ return originalFetch(input, init);
568
+ }
569
+ return new Response(JSON.stringify({ key: "sk-or-v1-session-key" }), { status: 200, headers: { "content-type": "application/json" } });
570
+ };
571
+ try {
572
+ const res = await originalFetch(`${base}/api/openrouter/connect`, {
573
+ method: "POST",
574
+ headers: { "Content-Type": "application/json" },
575
+ body: JSON.stringify({ code: "oauth-code", codeVerifier: "pkce-verifier", persistToEnv: true }),
576
+ });
577
+ assert.equal(res.status, 200);
578
+ const body = await res.json();
579
+ assert.equal(body.persistedToEnv, true);
580
+ assert.equal(body.persistWarning, null);
581
+ assert.equal(body.persistedEnvPath, path.join(minicodeHome, ".env"));
582
+ assert.match(body.message, /saved to ~\/\.minicode\/\.env/);
583
+ const envContents = readFileSync(path.join(minicodeHome, ".env"), "utf8");
584
+ assert.match(envContents, /^MODEL_PROVIDER=openai-compatible$/m);
585
+ assert.match(envContents, /^OPENAI_BASE_URL=https:\/\/openrouter\.ai\/api\/v1$/m);
586
+ assert.match(envContents, /^OPENROUTER_API_KEY=sk-or-v1-session-key$/m);
587
+ }
588
+ finally {
589
+ globalThis.fetch = originalFetch;
590
+ rmSync(minicodeHome, { recursive: true, force: true });
591
+ }
592
+ });
593
+ test("POST /api/model persists the selected model to ~/.minicode/.env", async () => {
594
+ const bridge = new MockBridge();
595
+ const minicodeHome = mkdtempSync(path.join(os.tmpdir(), "minicode-model-home-"));
596
+ const envPath = path.join(minicodeHome, ".env");
597
+ writeFileSync(envPath, [
598
+ "MODEL_PROVIDER=openai-compatible",
599
+ "OPENAI_BASE_URL=https://openrouter.ai/api/v1",
600
+ "OPENROUTER_API_KEY=sk-or-v1-session-key",
601
+ "",
602
+ ].join("\n"), "utf8");
603
+ const base = await startTestServer(bridge, { minicodeHome });
604
+ try {
605
+ const res = await fetch(`${base}/api/model`, {
606
+ method: "POST",
607
+ headers: { "Content-Type": "application/json" },
608
+ body: JSON.stringify({ model: "openrouter/test-model" }),
609
+ });
610
+ assert.equal(res.status, 200);
611
+ const body = await res.json();
612
+ assert.equal(body.model, "openrouter/test-model");
613
+ assert.equal(body.persistedToEnv, true);
614
+ assert.equal(body.persistedEnvPath, envPath);
615
+ const envContents = readFileSync(envPath, "utf8");
616
+ assert.match(envContents, /^MODEL=openrouter\/test-model$/m);
617
+ assert.match(envContents, /^OPENROUTER_API_KEY=sk-or-v1-session-key$/m);
618
+ assert.equal(bridge.getConfig().model, "openrouter/test-model");
619
+ }
620
+ finally {
621
+ rmSync(minicodeHome, { recursive: true, force: true });
622
+ }
623
+ });
624
+ test("POST /api/openrouter/disconnect removes the session-only OpenRouter connection", async () => {
625
+ const bridge = new MockBridge();
626
+ bridge.connectOpenRouter("sk-or-v1-session-key");
627
+ const base = await startTestServer(bridge);
628
+ const res = await fetch(`${base}/api/openrouter/disconnect`, {
629
+ method: "POST",
630
+ headers: { "Content-Type": "application/json" },
631
+ });
632
+ assert.equal(res.status, 200);
633
+ const body = await res.json();
634
+ assert.equal(body.ok, true);
635
+ assert.equal(body.disconnected, true);
636
+ assert.equal(body.sessionOnly, true);
637
+ assert.equal(body.provider, "anthropic");
638
+ assert.equal(body.baseUrl, "http://localhost:1234/v1");
639
+ assert.equal(body.message, "Removed the session-only OpenRouter connection and restored your original provider settings.");
640
+ assert.equal(bridge.openRouterSessionActive, false);
641
+ assert.equal(bridge.getConfig().modelProvider, "anthropic");
642
+ assert.equal(bridge.getConfig().openAiBaseUrl, "http://localhost:1234/v1");
643
+ assert.equal(bridge.getConfig().openAiApiKey, undefined);
644
+ });
645
+ test("GET /api/status exposes OpenRouter session state and base URL", async () => {
646
+ const bridge = new MockBridge();
647
+ bridge.connectOpenRouter("sk-or-v1-session-key");
648
+ const base = await startTestServer(bridge);
649
+ const res = await fetch(`${base}/api/status`);
650
+ assert.equal(res.status, 200);
651
+ const body = await res.json();
652
+ assert.equal(body.provider, "openai-compatible");
653
+ assert.equal(body.baseUrl, "https://openrouter.ai/api/v1");
654
+ assert.equal(body.sessionOpenRouterConnected, true);
655
+ });
656
+ test("POST /api/openrouter/connect returns 400 when code is missing", async () => {
657
+ const bridge = new MockBridge();
658
+ const base = await startTestServer(bridge);
659
+ const res = await fetch(`${base}/api/openrouter/connect`, {
660
+ method: "POST",
661
+ headers: { "Content-Type": "application/json" },
662
+ body: JSON.stringify({ codeVerifier: "pkce-verifier" }),
663
+ });
664
+ assert.equal(res.status, 400);
665
+ });
666
+ test("POST /api/openrouter/connect surfaces exchange failures", async () => {
667
+ const bridge = new MockBridge();
668
+ const base = await startTestServer(bridge);
669
+ const originalFetch = globalThis.fetch;
670
+ globalThis.fetch = async (input, init) => {
671
+ if (String(input) !== "https://openrouter.ai/api/v1/auth/keys") {
672
+ return originalFetch(input, init);
673
+ }
674
+ return new Response("Invalid code", { status: 403 });
675
+ };
676
+ try {
677
+ const res = await originalFetch(`${base}/api/openrouter/connect`, {
678
+ method: "POST",
679
+ headers: { "Content-Type": "application/json" },
680
+ body: JSON.stringify({ code: "bad-code", codeVerifier: "pkce-verifier" }),
681
+ });
682
+ assert.equal(res.status, 403);
683
+ const body = await res.json();
684
+ assert.ok(body.error.includes("OpenRouter OAuth exchange failed"));
685
+ assert.ok(body.error.includes("Invalid code"));
686
+ }
687
+ finally {
688
+ globalThis.fetch = originalFetch;
689
+ }
690
+ });
471
691
  // ── OpenAI-compatible API tests ──
472
692
  test("GET /v1/models returns minicode-agent model", async () => {
473
693
  const bridge = new MockBridge();
@@ -576,6 +796,7 @@ test("GET / serves index.html", async () => {
576
796
  const res = await fetch(`${base}/`);
577
797
  assert.equal(res.status, 200);
578
798
  assert.equal(res.headers.get("content-type"), "text/html");
799
+ assert.equal(res.headers.get("cache-control"), "no-store");
579
800
  const html = await res.text();
580
801
  assert.ok(html.includes("minicode"));
581
802
  });
@@ -585,6 +806,7 @@ test("GET /style.css serves CSS file", async () => {
585
806
  const res = await fetch(`${base}/style.css`);
586
807
  assert.equal(res.status, 200);
587
808
  assert.equal(res.headers.get("content-type"), "text/css");
809
+ assert.equal(res.headers.get("cache-control"), "no-store");
588
810
  });
589
811
  test("GET /app.js serves JS file", async () => {
590
812
  const bridge = new MockBridge();
@@ -592,6 +814,7 @@ test("GET /app.js serves JS file", async () => {
592
814
  const res = await fetch(`${base}/app.js`);
593
815
  assert.equal(res.status, 200);
594
816
  assert.equal(res.headers.get("content-type"), "application/javascript");
817
+ assert.equal(res.headers.get("cache-control"), "no-store");
595
818
  });
596
819
  test("GET /nonexistent returns 404", async () => {
597
820
  const bridge = new MockBridge();
@@ -0,0 +1,56 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { buildSessionPreview, isCompactionSummaryMessage, } from "../src/session/session-preview.js";
4
+ test("isCompactionSummaryMessage detects compacted summary stubs", () => {
5
+ assert.equal(isCompactionSummaryMessage({
6
+ role: "user",
7
+ content: "[Conversation Summary — earlier messages were compacted to save context]\nSummary text",
8
+ }), true);
9
+ assert.equal(isCompactionSummaryMessage({
10
+ role: "assistant",
11
+ content: "[Conversation Summary — earlier messages were compacted to save context]\nSummary text",
12
+ }), false);
13
+ });
14
+ test("buildSessionPreview filters compaction summaries and keeps the last ten messages", () => {
15
+ const preview = buildSessionPreview([
16
+ {
17
+ role: "user",
18
+ content: "[Conversation Summary — earlier messages were compacted using LLM summarization]\nSummary text",
19
+ },
20
+ { role: "user", content: "message-1" },
21
+ { role: "assistant", content: "message-2" },
22
+ { role: "user", content: "message-3" },
23
+ { role: "assistant", content: "message-4" },
24
+ { role: "user", content: "message-5" },
25
+ { role: "assistant", content: "message-6" },
26
+ { role: "user", content: "message-7" },
27
+ { role: "assistant", content: "message-8" },
28
+ { role: "user", content: "message-9" },
29
+ { role: "assistant", content: "message-10" },
30
+ { role: "user", content: "message-11" },
31
+ ]);
32
+ assert.equal(preview.length, 10);
33
+ assert.equal(preview[0]?.content, "message-2");
34
+ assert.equal(preview[9]?.content, "message-11");
35
+ assert.ok(preview.every((message) => !message.content.startsWith("[Conversation Summary")));
36
+ });
37
+ test("buildSessionPreview preserves tool messages in order", () => {
38
+ const preview = buildSessionPreview([
39
+ {
40
+ role: "assistant",
41
+ content: "Let me check that",
42
+ toolCalls: [{ id: "tool-1", name: "search", input: { query: "foo" } }],
43
+ },
44
+ {
45
+ role: "tool",
46
+ toolCallId: "tool-1",
47
+ toolName: "search",
48
+ content: "search output",
49
+ },
50
+ {
51
+ role: "assistant",
52
+ content: "Found it",
53
+ },
54
+ ]);
55
+ assert.deepEqual(preview.map((message) => message.role), ["assistant", "tool", "assistant"]);
56
+ });
@@ -20,4 +20,6 @@ test("built JS contains active saved session update logic", () => {
20
20
  assert.ok(js.includes("Session updated:"), "JS should emit the update confirmation message");
21
21
  assert.ok(js.includes("sessionRefreshTracker"), "JS should guard session list refreshes against stale responses");
22
22
  assert.ok(js.includes('saveBtn.setAttribute("disabled", "true")'), "JS should disable saving while the first save is in flight");
23
+ assert.ok(js.includes("renderLoadedSessionMessages"), "JS should render session previews after load");
24
+ assert.ok(js.includes("body.messages"), "JS should read preview messages from the load session response");
23
25
  });
@@ -7,6 +7,12 @@ test("built HTML contains settings entry point and modal shell", () => {
7
7
  const html = readFileSync(join(distWeb, "index.html"), "utf8");
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
+ assert.ok(html.includes('id="connect-openrouter-btn"'), "HTML should contain the OpenRouter connect button");
11
+ assert.ok(html.includes('id="config-overlay-intro"'), "HTML should contain the setup overlay intro copy");
12
+ assert.ok(html.includes("Try minicode for free with OpenRouter"), "HTML should promote the free OpenRouter quick start");
13
+ assert.ok(html.includes('id="openrouter-connect-modal"'), "HTML should contain the OpenRouter consent modal");
14
+ assert.ok(html.includes('id="openrouter-persist-checkbox"'), "HTML should contain the OpenRouter persistence checkbox");
15
+ assert.ok(html.includes('id="disconnect-openrouter-btn"'), "HTML should contain the OpenRouter disconnect button");
10
16
  assert.ok(!html.includes('id="settings-scope"'), "HTML should no longer contain the settings scope selector");
11
17
  assert.ok(html.includes('id="settings-save"'), "HTML should contain the settings save action");
12
18
  });
@@ -16,11 +22,23 @@ test("built CSS contains modal and settings layout styles", () => {
16
22
  assert.ok(css.includes(".settings-list"), "CSS should contain settings list styles");
17
23
  assert.ok(css.includes(".settings-item-meta"), "CSS should contain settings metadata grid styles");
18
24
  assert.ok(css.includes(".settings-help-warning"), "CSS should contain warning styling for env overrides");
25
+ assert.ok(css.includes("align-items: flex-start;"), "CSS should top-align scrollable setup overlay content");
26
+ assert.ok(css.includes(".config-overlay-spotlight"), "CSS should style the OpenRouter quick-start spotlight");
27
+ assert.ok(css.includes(".openrouter-connect-body"), "CSS should style the OpenRouter consent modal body");
28
+ assert.ok(css.includes(".config-connect-status.success"), "CSS should style OpenRouter connect success state");
29
+ assert.ok(css.includes(".settings-session-banner"), "CSS should style the OpenRouter session banner");
19
30
  assert.ok(css.includes("body.modal-open"), "CSS should lock scroll while the settings modal is open");
20
31
  });
21
32
  test("built JS contains config loading and saving logic for settings", () => {
22
33
  const js = readFileSync(join(distWeb, "app.js"), "utf8");
23
34
  assert.ok(js.includes("/api/config"), "JS should fetch the config API");
35
+ assert.ok(js.includes("/api/openrouter/connect"), "JS should call the OpenRouter connect API");
36
+ assert.ok(js.includes("/api/openrouter/disconnect"), "JS should call the OpenRouter disconnect API");
37
+ assert.ok(js.includes("persistToHomeEnv"), "JS should support persisting the selected model after OpenRouter setup");
38
+ assert.ok(js.includes("code_challenge_method"), "JS should generate an OpenRouter PKCE auth request");
39
+ assert.ok(js.includes("sessionStorage"), "JS should persist the PKCE verifier for the OAuth callback");
40
+ assert.ok(js.includes("minicode:openrouter:persist-to-env"), "JS should persist the optional OpenRouter env-write choice across OAuth");
41
+ assert.ok(js.includes("sessionOpenRouterConnected"), "JS should track session-only OpenRouter state");
24
42
  assert.ok(js.includes("Save settings"), "JS should contain the settings save action text");
25
43
  assert.ok(js.includes("settingsPayload"), "JS should track settings payload state");
26
44
  assert.ok(js.includes("persistedValue"), "JS should wire persisted settings behavior");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sean.holung/minicode",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "workspaces": [
5
5
  "packages/*"
6
6
  ],