@sksoftofficial/mindroot 1.0.5 → 1.1.0

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,187 @@
1
+ import { test, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { parse as parseToml } from "@decimalturn/toml-patch";
7
+ import { configureClaude, configureCodex } from "../src/commands/install.js";
8
+
9
+ let dir;
10
+ const cfg = { port: 8765, apiKey: "test-only-key" };
11
+ const policy = (await fs.readFile(new URL("../INSTRUCTIONS.md", import.meta.url), "utf8")).trim();
12
+ const savedEnv = {};
13
+
14
+ beforeEach(async () => {
15
+ dir = await fs.mkdtemp(path.join(os.tmpdir(), "mindroot-install-clients-"));
16
+ for (const name of ["CLAUDE_CONFIG_DIR", "CODEX_HOME"]) {
17
+ savedEnv[name] = process.env[name];
18
+ delete process.env[name];
19
+ }
20
+ });
21
+
22
+ afterEach(async () => {
23
+ for (const [name, value] of Object.entries(savedEnv)) {
24
+ if (value === undefined) delete process.env[name];
25
+ else process.env[name] = value;
26
+ }
27
+ await fs.rm(dir, { recursive: true, force: true });
28
+ });
29
+
30
+ test("claude: defaults keep .claude.json outside ~/.claude; a rerun is byte-identical", async (t) => {
31
+ const home = path.join(dir, "home");
32
+ t.mock.method(os, "homedir", () => home);
33
+ const { configPath, agentsPath } = configureClaude(cfg);
34
+ assert.equal(configPath, path.join(home, ".claude.json"));
35
+ assert.equal(agentsPath, path.join(home, ".claude", "CLAUDE.md"));
36
+ const configText = await fs.readFile(configPath, "utf8");
37
+ assert.deepEqual(JSON.parse(configText), {
38
+ mcpServers: { mindroot: {
39
+ type: "http", url: "http://127.0.0.1:8765/mcp",
40
+ headers: { Authorization: "Bearer test-only-key" },
41
+ } },
42
+ });
43
+ assert.equal(await fs.readFile(agentsPath, "utf8"), policy + "\n");
44
+ configureClaude(cfg);
45
+ assert.equal(await fs.readFile(configPath, "utf8"), configText);
46
+ assert.equal(await fs.readFile(agentsPath, "utf8"), policy + "\n");
47
+ if (process.platform !== "win32") assert.equal((await fs.stat(configPath)).mode & 0o777, 0o600);
48
+ });
49
+
50
+ test("claude: explicit directories and CLAUDE_CONFIG_DIR keep both files inside", async () => {
51
+ const explicit = configureClaude(cfg, path.join(dir, "explicit"));
52
+ assert.equal(explicit.configPath, path.join(dir, "explicit", ".claude.json"));
53
+ assert.equal(explicit.agentsPath, path.join(dir, "explicit", "CLAUDE.md"));
54
+ process.env.CLAUDE_CONFIG_DIR = path.join(dir, "envdir");
55
+ const env = configureClaude(cfg);
56
+ assert.equal(env.configPath, path.join(dir, "envdir", ".claude.json"));
57
+ assert.equal(env.agentsPath, path.join(dir, "envdir", "CLAUDE.md"));
58
+ });
59
+
60
+ test("claude: preserves unrelated settings and refreshes the owned entry", async () => {
61
+ const configPath = path.join(dir, ".claude.json");
62
+ await fs.writeFile(configPath, JSON.stringify({
63
+ model: "claude-opus",
64
+ mcpServers: { other: { type: "stdio", command: ["x"] }, mindroot: { type: "sse", url: "stale" } },
65
+ projects: { "/work": { allowed: true } },
66
+ }));
67
+ configureClaude(cfg, dir);
68
+ const config = JSON.parse(await fs.readFile(configPath, "utf8"));
69
+ assert.equal(config.model, "claude-opus");
70
+ assert.deepEqual(config.projects, { "/work": { allowed: true } });
71
+ assert.deepEqual(config.mcpServers.other, { type: "stdio", command: ["x"] });
72
+ assert.deepEqual(config.mcpServers.mindroot, {
73
+ type: "http", url: "http://127.0.0.1:8765/mcp",
74
+ headers: { Authorization: "Bearer test-only-key" },
75
+ });
76
+ configureClaude({ port: 9876, apiKey: "replacement-test-key" }, dir);
77
+ const updated = JSON.parse(await fs.readFile(configPath, "utf8")).mcpServers.mindroot;
78
+ assert.equal(updated.url, "http://127.0.0.1:9876/mcp");
79
+ assert.equal(updated.headers.Authorization, "Bearer replacement-test-key");
80
+ });
81
+
82
+ test("claude: malformed config or markers leave both files untouched", async () => {
83
+ const configPath = path.join(dir, ".claude.json");
84
+ const agentsPath = path.join(dir, "CLAUDE.md");
85
+ const cases = [
86
+ ["{ broken", "Keep me", /invalid Claude configuration/],
87
+ ["[1]", "Keep me", /invalid Claude configuration/],
88
+ ['{"mcpServers": []}', "Keep me", /expected an mcpServers object/],
89
+ ["{}", "<!-- mindroot:start -->\nunfinished", /unbalanced mindroot markers/],
90
+ ];
91
+ for (const [config, agents, error] of cases) {
92
+ await fs.writeFile(configPath, config);
93
+ await fs.writeFile(agentsPath, agents);
94
+ assert.throws(() => configureClaude(cfg, dir), error);
95
+ assert.equal(await fs.readFile(configPath, "utf8"), config);
96
+ assert.equal(await fs.readFile(agentsPath, "utf8"), agents);
97
+ }
98
+ });
99
+
100
+ test("codex: creates config and policy, preserves TOML comments; a rerun is byte-identical", async () => {
101
+ const configPath = path.join(dir, "config.toml");
102
+ await fs.writeFile(configPath, `# Codex config
103
+ model = "gpt-5-codex"
104
+
105
+ [mcp_servers.other]
106
+ url = "https://example.com/mcp" # keep me
107
+ `);
108
+ const { configPath: returned, agentsPath } = configureCodex(cfg, dir);
109
+ assert.equal(returned, configPath);
110
+ assert.equal(agentsPath, path.join(dir, "AGENTS.md"));
111
+ const first = await fs.readFile(configPath, "utf8");
112
+ assert.ok(first.includes("# Codex config"));
113
+ assert.ok(first.includes('model = "gpt-5-codex"'));
114
+ assert.ok(first.includes('url = "https://example.com/mcp" # keep me'));
115
+ const config = parseToml(first);
116
+ assert.deepEqual(config.mcp_servers.mindroot, {
117
+ url: "http://127.0.0.1:8765/mcp",
118
+ http_headers: { Authorization: "Bearer test-only-key" },
119
+ });
120
+ assert.equal(config.model, "gpt-5-codex");
121
+ assert.equal(await fs.readFile(agentsPath, "utf8"), policy + "\n");
122
+ configureCodex(cfg, dir);
123
+ assert.equal(await fs.readFile(configPath, "utf8"), first);
124
+ assert.equal(await fs.readFile(agentsPath, "utf8"), policy + "\n");
125
+ if (process.platform !== "win32") assert.equal((await fs.stat(configPath)).mode & 0o777, 0o600);
126
+ });
127
+
128
+ test("codex: honors CODEX_HOME or ~/.codex by default and refreshes the owned entry", async (t) => {
129
+ const home = path.join(dir, "home");
130
+ t.mock.method(os, "homedir", () => home);
131
+ const defaulted = configureCodex(cfg);
132
+ assert.equal(defaulted.configPath, path.join(home, ".codex", "config.toml"));
133
+ assert.equal(defaulted.agentsPath, path.join(home, ".codex", "AGENTS.md"));
134
+ process.env.CODEX_HOME = path.join(dir, "custom");
135
+ const env = configureCodex({ port: 9876, apiKey: "replacement-test-key" });
136
+ assert.equal(env.configPath, path.join(dir, "custom", "config.toml"));
137
+ const config = parseToml(await fs.readFile(env.configPath, "utf8"));
138
+ assert.equal(config.mcp_servers.mindroot.url, "http://127.0.0.1:9876/mcp");
139
+ assert.equal(config.mcp_servers.mindroot.http_headers.Authorization, "Bearer replacement-test-key");
140
+ });
141
+
142
+ test("codex: syncs a non-empty AGENTS.override.md; an empty override falls back to AGENTS.md", async () => {
143
+ const basePath = path.join(dir, "AGENTS.md");
144
+ const overridePath = path.join(dir, "AGENTS.override.md");
145
+ await fs.writeFile(basePath, "# Base\nKeep me.\n");
146
+ await fs.writeFile(overridePath, "Override content.\n");
147
+ const active = configureCodex(cfg, dir);
148
+ assert.equal(active.agentsPath, overridePath);
149
+ assert.equal(await fs.readFile(overridePath, "utf8"), "Override content.\n\n" + policy + "\n");
150
+ assert.equal(await fs.readFile(basePath, "utf8"), "# Base\nKeep me.\n");
151
+
152
+ await fs.writeFile(overridePath, "\n");
153
+ const fallback = configureCodex(cfg, dir);
154
+ assert.equal(fallback.agentsPath, basePath);
155
+ assert.equal(await fs.readFile(overridePath, "utf8"), "\n");
156
+ assert.equal(await fs.readFile(basePath, "utf8"), "# Base\nKeep me.\n\n" + policy + "\n");
157
+ });
158
+
159
+ test("codex: malformed TOML, bad shapes, or markers leave both files untouched", async () => {
160
+ const configPath = path.join(dir, "config.toml");
161
+ const agentsPath = path.join(dir, "AGENTS.md");
162
+ const cases = [
163
+ ["model = ", "Keep me", /invalid Codex configuration/],
164
+ ["mcp_servers = 3", "Keep me", /expected an mcp_servers table/],
165
+ ["mcp_servers.mindroot = 5", "Keep me", /expected mcp_servers\.mindroot to be a table/],
166
+ ["", "<!-- mindroot:end -->", /unbalanced mindroot markers/],
167
+ ];
168
+ for (const [config, agents, error] of cases) {
169
+ await fs.writeFile(configPath, config);
170
+ await fs.writeFile(agentsPath, agents);
171
+ assert.throws(() => configureCodex(cfg, dir), error);
172
+ assert.equal(await fs.readFile(configPath, "utf8"), config);
173
+ assert.equal(await fs.readFile(agentsPath, "utf8"), agents);
174
+ }
175
+ });
176
+
177
+ test("both clients replace marked policies and preserve surrounding instructions", async () => {
178
+ const prefix = "# User instructions\nKeep these.\n\n";
179
+ const suffix = "\nCustom footer without a trailing newline";
180
+ const old = "<!-- mindroot:start -->\nOld policy\n<!-- mindroot:end -->";
181
+ for (const configure of [(d) => configureClaude(cfg, d), (d) => configureCodex(cfg, d)]) {
182
+ const agentsPath = path.join(dir, "AGENTS.md");
183
+ await fs.writeFile(agentsPath, prefix + old + suffix);
184
+ configure(dir);
185
+ assert.equal(await fs.readFile(agentsPath, "utf8"), prefix + policy + suffix);
186
+ }
187
+ });
@@ -0,0 +1,127 @@
1
+ import { test, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { parse } from "jsonc-parser";
7
+ import { configureOpencode, install } from "../src/commands/install.js";
8
+
9
+ let dir;
10
+ const cfg = { port: 8765, apiKey: "test-only-key" };
11
+ const policy = (await fs.readFile(new URL("../INSTRUCTIONS.md", import.meta.url), "utf8")).trim();
12
+ beforeEach(async () => { dir = await fs.mkdtemp(path.join(os.tmpdir(), "mindroot-install-")); });
13
+ afterEach(() => fs.rm(dir, { recursive: true, force: true }));
14
+
15
+ test("creates global config and policy; a rerun is byte-identical", async () => {
16
+ const configDir = path.join(dir, "opencode");
17
+ const { configPath, agentsPath } = configureOpencode(cfg, configDir);
18
+ const configText = await fs.readFile(configPath, "utf8");
19
+ assert.deepEqual(JSON.parse(configText), {
20
+ $schema: "https://opencode.ai/config.json",
21
+ mcp: { mindroot: {
22
+ type: "remote", url: "http://127.0.0.1:8765/mcp", enabled: true,
23
+ headers: { Authorization: "Bearer test-only-key" },
24
+ } },
25
+ });
26
+ assert.equal(await fs.readFile(agentsPath, "utf8"), policy + "\n");
27
+ configureOpencode(cfg, configDir);
28
+ assert.equal(await fs.readFile(configPath, "utf8"), configText);
29
+ assert.equal(await fs.readFile(agentsPath, "utf8"), policy + "\n");
30
+ if (process.platform !== "win32") assert.equal((await fs.stat(configPath)).mode & 0o777, 0o600);
31
+ });
32
+
33
+ test("refreshes the owned MCP entry while preserving JSONC and unrelated settings", async () => {
34
+ const jsonPath = path.join(dir, "opencode.json");
35
+ const jsoncPath = path.join(dir, "opencode.jsonc");
36
+ const json = '{"model":"keep/this"}\n';
37
+ await fs.writeFile(jsonPath, json);
38
+ await fs.writeFile(jsoncPath, `{
39
+ // Keep this comment and setting.
40
+ "model": "provider/model",
41
+ "mcp": {
42
+ // Another server stays untouched.
43
+ "other": { "type": "local", "command": ["other-mcp"] },
44
+ "mindroot": { "type": "local", "command": ["old"], "enabled": false },
45
+ },
46
+ }\n`);
47
+ const { configPath } = configureOpencode(cfg, dir);
48
+ assert.equal(configPath, jsoncPath);
49
+ assert.equal(await fs.readFile(jsonPath, "utf8"), json);
50
+ const text = await fs.readFile(jsoncPath, "utf8");
51
+ assert.ok(text.includes("// Keep this comment and setting."));
52
+ assert.ok(text.includes('// Another server stays untouched.\n "other": { "type": "local", "command": ["other-mcp"] },'));
53
+ const config = parse(text);
54
+ assert.equal(config.model, "provider/model");
55
+ assert.equal(config.mcp.mindroot.command, undefined);
56
+ assert.equal(config.mcp.mindroot.enabled, true);
57
+ configureOpencode({ port: 9876, apiKey: "replacement-test-key" }, dir);
58
+ const updated = parse(await fs.readFile(jsoncPath, "utf8")).mcp.mindroot;
59
+ assert.equal(updated.url, "http://127.0.0.1:9876/mcp");
60
+ assert.equal(updated.headers.Authorization, "Bearer replacement-test-key");
61
+ });
62
+
63
+ test("replaces marked policies, removes duplicates, and preserves surrounding instructions", async () => {
64
+ const agentsPath = path.join(dir, "AGENTS.md");
65
+ const old = "<!-- mindroot:start -->\nOld policy\n<!-- mindroot:end -->";
66
+ const prefix = "# User instructions\nKeep these.\n\n";
67
+ const middle = "\n\n<!-- another:start -->\nOther policy\n<!-- another:end -->\n";
68
+ const suffix = "\nCustom footer without a trailing newline";
69
+ await fs.writeFile(agentsPath, prefix + old + middle + old + suffix);
70
+ configureOpencode(cfg, dir);
71
+ const expected = prefix + policy + middle + suffix;
72
+ assert.equal(await fs.readFile(agentsPath, "utf8"), expected);
73
+ configureOpencode(cfg, dir);
74
+ assert.equal(await fs.readFile(agentsPath, "utf8"), expected);
75
+ });
76
+
77
+ test("appends a separated policy when no marked block exists", async () => {
78
+ const agentsPath = path.join(dir, "AGENTS.md");
79
+ for (const existing of ["", "Keep me", "Keep me\n", "Keep me\n\n"]) {
80
+ await fs.writeFile(agentsPath, existing);
81
+ configureOpencode(cfg, dir);
82
+ assert.equal(await fs.readFile(agentsPath, "utf8"), (existing ? "Keep me\n\n" : "") + policy + "\n");
83
+ }
84
+ });
85
+
86
+ test("malformed config or markers leave both files untouched", async () => {
87
+ const configPath = path.join(dir, "opencode.json");
88
+ const agentsPath = path.join(dir, "AGENTS.md");
89
+ const cases = [
90
+ ["{ broken", "Keep me", /invalid OpenCode configuration/],
91
+ ["[]", "Keep me", /invalid OpenCode configuration/],
92
+ ['{"mcp": []}', "Keep me", /expected an mcp object/],
93
+ ['{"mcp": null}', "Keep me", /expected an mcp object/],
94
+ ["{}", "<!-- mindroot:start -->\nunfinished", /unbalanced mindroot markers/],
95
+ ["{}", "<!-- mindroot:end -->", /unbalanced mindroot markers/],
96
+ ["{}", "<!-- mindroot:start --><!-- mindroot:start --><!-- mindroot:end --><!-- mindroot:end -->", /unbalanced mindroot markers/],
97
+ ];
98
+ for (const [config, agents, error] of cases) {
99
+ await fs.writeFile(configPath, config);
100
+ await fs.writeFile(agentsPath, agents);
101
+ assert.throws(() => configureOpencode(cfg, dir), error);
102
+ assert.equal(await fs.readFile(configPath, "utf8"), config);
103
+ assert.equal(await fs.readFile(agentsPath, "utf8"), agents);
104
+ }
105
+ });
106
+
107
+ test("respects XDG_CONFIG_HOME and the explicit OpenCode directory override", (t) => {
108
+ const previous = { XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME, OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR };
109
+ t.after(() => {
110
+ for (const [name, value] of Object.entries(previous)) {
111
+ if (value === undefined) delete process.env[name];
112
+ else process.env[name] = value;
113
+ }
114
+ });
115
+ process.env.XDG_CONFIG_HOME = dir;
116
+ delete process.env.OPENCODE_CONFIG_DIR;
117
+ assert.equal(configureOpencode(cfg).configPath, path.join(dir, "opencode", "opencode.json"));
118
+ const custom = path.join(dir, "custom");
119
+ process.env.OPENCODE_CONFIG_DIR = custom;
120
+ assert.equal(configureOpencode(cfg).agentsPath, path.join(custom, "AGENTS.md"));
121
+ });
122
+
123
+ test("rejects missing or unsupported install targets before initialization", async () => {
124
+ for (const args of [[], ["unknown"], ["opencode", "extra"], ["claude", "codex"]]) {
125
+ await assert.rejects(install(args), /usage: mindroot install <opencode\|claude\|codex>/);
126
+ }
127
+ });
package/test/mcp.test.js CHANGED
@@ -10,18 +10,22 @@ let db;
10
10
  let open;
11
11
 
12
12
  const EXPECTED_TOOLS = [
13
- "mindroot_list_projects",
14
- "mindroot_search_projects",
15
- "mindroot_rename_project",
16
- "mindroot_search_memories",
17
- "mindroot_search_notes",
18
- "mindroot_save_memory",
19
- "mindroot_delete_memory",
20
- "mindroot_list_notes",
21
- "mindroot_read_note",
22
- "mindroot_save_note",
23
- "mindroot_update_section",
24
- "mindroot_delete_note",
13
+ "list_projects",
14
+ "search_projects",
15
+ "rename_project",
16
+ "search_memories",
17
+ "search_notes",
18
+ "save_memory",
19
+ "delete_memory",
20
+ "list_notes",
21
+ "read_note",
22
+ "save_note",
23
+ "update_section",
24
+ "delete_note",
25
+ "search_skills",
26
+ "read_skill",
27
+ "save_skill",
28
+ "delete_skill",
25
29
  ];
26
30
 
27
31
  before(async () => {
@@ -58,19 +62,19 @@ test("unknown tool returns clean error", async () => {
58
62
  });
59
63
 
60
64
  test("search without project is a clean isError", async () => {
61
- const res = await call("mindroot_search_memories", { query: "x" });
65
+ const res = await call("search_memories", { query: "x" });
62
66
  assert.equal(res.body.result.isError, true);
63
67
  assert.match(res.body.result.content[0].text, /project is required/);
64
68
  });
65
69
 
66
70
  test("invalid slug is rejected before any lookup", async () => {
67
- const res = await call("mindroot_search_memories", { project: "../evil", query: "x" });
71
+ const res = await call("search_memories", { project: "../evil", query: "x" });
68
72
  assert.equal(res.body.result.isError, true);
69
73
  assert.match(res.body.result.content[0].text, /invalid project slug/);
70
74
  });
71
75
 
72
76
  test("unknown project is a clean error", async () => {
73
- const res = await call("mindroot_search_memories", { project: "ghost", query: "x" });
77
+ const res = await call("search_memories", { project: "ghost", query: "x" });
74
78
  assert.match(res.body.result.content[0].text, /unknown project: ghost/);
75
79
  });
76
80
 
@@ -89,35 +93,35 @@ test("rename_project moves slug, dir, and embeddings", async () => {
89
93
  fs.mkdirSync(path.join(NOTES_DIR, "old-name"), { recursive: true });
90
94
  fs.writeFileSync(path.join(NOTES_DIR, "old-name", "overview.md"), "# Old\n");
91
95
 
92
- const res = await call("mindroot_rename_project", { project: "old-name", new_slug: "new-name" });
96
+ const res = await call("rename_project", { project: "old-name", new_slug: "new-name" });
93
97
  const data = JSON.parse(res.body.result.content[0].text);
94
98
  assert.deepEqual(data.renamed, { from: "old-name", to: "new-name" });
95
99
  assert.equal(db.prepare("SELECT slug FROM projects WHERE id = ?").get(pid).slug, "new-name");
96
100
  assert.ok(fs.existsSync(path.join(NOTES_DIR, "new-name", "overview.md")));
97
101
  assert.ok(!fs.existsSync(path.join(NOTES_DIR, "old-name")));
98
102
  assert.equal(db.prepare("SELECT COUNT(*) n FROM project_embeddings WHERE project_id = ?").get(pid).n, 0);
99
- const listed = JSON.parse((await call("mindroot_list_projects", {})).body.result.content[0].text);
103
+ const listed = JSON.parse((await call("list_projects", {})).body.result.content[0].text);
100
104
  const row = listed.find((p) => p.slug === "new-name");
101
105
  assert.equal(row.memories, 1);
102
106
  assert.ok(!listed.some((p) => p.slug === "old-name"));
103
107
 
104
- const dup = await call("mindroot_rename_project", { project: "new-name", new_slug: "new-name" });
108
+ const dup = await call("rename_project", { project: "new-name", new_slug: "new-name" });
105
109
  assert.equal(dup.body.result.isError, true);
106
110
  assert.match(dup.body.result.content[0].text, /already named/);
107
111
 
108
- const bad = await call("mindroot_rename_project", { project: "new-name", new_slug: "../evil" });
112
+ const bad = await call("rename_project", { project: "new-name", new_slug: "../evil" });
109
113
  assert.equal(bad.body.result.isError, true);
110
114
  assert.match(bad.body.result.content[0].text, /invalid project slug/);
111
115
  });
112
116
 
113
117
  test("update_section appends a missing section", async () => {
114
- await call("mindroot_save_note", {
118
+ await call("save_note", {
115
119
  project: "section-upsert",
116
120
  path: "note.md",
117
121
  content: "# Note\n\n## Existing\n\nkeep me\n",
118
122
  });
119
123
 
120
- const res = await call("mindroot_update_section", {
124
+ const res = await call("update_section", {
121
125
  project: "section-upsert",
122
126
  path: "note.md",
123
127
  heading_path: "Added",
@@ -0,0 +1,53 @@
1
+ import { test, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ let dir;
8
+ let paths;
9
+ beforeEach(async () => {
10
+ dir = await fs.mkdtemp(path.join(os.tmpdir(), "mindroot-paths-"));
11
+ process.env.MINDROOT_DIR = dir;
12
+ paths = await import("../src/paths.js");
13
+ });
14
+ afterEach(async () => {
15
+ delete process.env.MINDROOT_DIR;
16
+ await fs.rm(dir, { recursive: true, force: true });
17
+ });
18
+
19
+ test("ensureConfig generates the key once and keeps it stable across runs", async () => {
20
+ const first = paths.ensureConfig();
21
+ assert.equal(first.created, true);
22
+ assert.ok(first.cfg.apiKey);
23
+ const second = paths.ensureConfig();
24
+ assert.equal(second.created, false);
25
+ assert.equal(second.cfg.apiKey, first.cfg.apiKey);
26
+ assert.equal(second.cfg.port, paths.DEFAULT_PORT);
27
+ if (process.platform !== "win32") {
28
+ assert.equal((await fs.stat(path.join(dir, "config.json"))).mode & 0o777, 0o600);
29
+ }
30
+ });
31
+
32
+ test("ensureConfig keeps a stored key and port", async () => {
33
+ await fs.writeFile(path.join(dir, "config.json"), JSON.stringify({ apiKey: "keep-me", port: 9999 }));
34
+ const { cfg, created } = paths.ensureConfig();
35
+ assert.equal(created, false);
36
+ assert.equal(cfg.apiKey, "keep-me");
37
+ assert.equal(cfg.port, 9999);
38
+ });
39
+
40
+ test("missing config reads as empty without creating one", () => {
41
+ const { cfg, created } = paths.ensureConfig();
42
+ assert.equal(created, true);
43
+ assert.ok(cfg.apiKey);
44
+ });
45
+
46
+ test("corrupt or non-object configs fail loudly instead of rotating the key", async () => {
47
+ const configPath = path.join(dir, "config.json");
48
+ for (const content of ['{ broken', "", "[]", '"x"']) {
49
+ await fs.writeFile(configPath, content);
50
+ assert.throws(() => paths.ensureConfig(), /invalid mindroot configuration|expected a JSON object/);
51
+ assert.equal(await fs.readFile(configPath, "utf8"), content);
52
+ }
53
+ });