pi-system-one 1.2.3 → 1.3.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.
@@ -1,12 +1,16 @@
1
1
  // pi-system-one/tests/commands.test.ts
2
2
 
3
3
  import assert from "node:assert/strict";
4
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
4
7
  import { describe, it } from "node:test";
5
8
  import {
6
9
  registerSystemOneCommands,
7
10
  type SessionStore,
8
11
  } from "../src/commands.ts";
9
12
  import { createSessionConfig } from "../src/config.ts";
13
+ import { resolveSessionProvider } from "../src/provider.ts";
10
14
 
11
15
  interface Captured {
12
16
  tools: string[];
@@ -41,10 +45,18 @@ function harness() {
41
45
  return { pi: pi as never, cap };
42
46
  }
43
47
 
44
- function ctxFor(answers: (string | undefined)[], cap: Captured) {
48
+ function ctxFor(
49
+ answers: (string | undefined)[],
50
+ cap: Captured,
51
+ defaults?: string[],
52
+ ) {
45
53
  return {
54
+ modelRegistry: { getApiKeyForProvider: async () => undefined },
46
55
  ui: {
47
- input: async () => answers.shift(),
56
+ input: async (_prompt: string, initial?: string) => {
57
+ defaults?.push(initial ?? "");
58
+ return answers.shift();
59
+ },
48
60
  notify: (msg: string) => {
49
61
  cap.notices.push(msg);
50
62
  },
@@ -53,6 +65,164 @@ function ctxFor(answers: (string | undefined)[], cap: Captured) {
53
65
  }
54
66
 
55
67
  describe("so command", () => {
68
+ it("clears prior model on custom endpoint change with blank model", async () => {
69
+ const dir = mkdtempSync(join(tmpdir(), "so-model-change-"));
70
+ const originalFetch = globalThis.fetch;
71
+ try {
72
+ const path = join(dir, "pi-system-one.json");
73
+ const { pi, cap } = harness();
74
+ const store: SessionStore = {
75
+ session: createSessionConfig({
76
+ SYSTEM_ONE_BASE_URL: "http://localhost:8008",
77
+ SYSTEM_ONE_MODEL: "reflex",
78
+ }),
79
+ };
80
+ registerSystemOneCommands(pi, store, path);
81
+ const defaults: string[] = [];
82
+ await cap.handler?.(
83
+ "config custom",
84
+ ctxFor(["http://localhost:8009", "", ""], cap, defaults),
85
+ );
86
+ assert.equal(defaults[1], "");
87
+ assert.equal(store.session?.current.model, undefined);
88
+ assert.equal(JSON.parse(readFileSync(path, "utf8")).model, undefined);
89
+ globalThis.fetch = (async (url, options) => {
90
+ assert.equal(url, "http://localhost:8009/v1/systemone");
91
+ assert.equal(JSON.parse(String(options?.body)).model, undefined);
92
+ return new Response(
93
+ JSON.stringify({ answers: { q: { type: "noul", noul: 0.6 } } }),
94
+ { status: 200 },
95
+ );
96
+ }) as typeof fetch;
97
+ const session = store.session;
98
+ assert.ok(session);
99
+ const provider = await resolveSessionProvider(session, {} as never);
100
+ await provider.evaluate({
101
+ state: "x",
102
+ questions: { q: { type: "noul", instructions: "Yes?" } },
103
+ });
104
+ } finally {
105
+ globalThis.fetch = originalFetch;
106
+ rmSync(dir, { recursive: true, force: true });
107
+ }
108
+ });
109
+
110
+ it("keeps model for same custom endpoint and accepts explicit replacement", async () => {
111
+ const { pi, cap } = harness();
112
+ const store: SessionStore = {
113
+ session: createSessionConfig({
114
+ SYSTEM_ONE_BASE_URL: "http://localhost:8008",
115
+ SYSTEM_ONE_MODEL: "reflex",
116
+ }),
117
+ };
118
+ registerSystemOneCommands(pi, store);
119
+ await cap.handler?.("config custom", ctxFor(["", "", ""], cap));
120
+ assert.equal(store.session?.current.model, "reflex");
121
+ await cap.handler?.(
122
+ "config custom",
123
+ ctxFor(["http://localhost:8009", "replacement", ""], cap),
124
+ );
125
+ assert.equal(store.session?.current.model, "replacement");
126
+ });
127
+
128
+ it("keeps explicit TypeSafe environment key after selecting TypeSafe", async () => {
129
+ const previousKey = process.env.SYSTEM_ONE_API_KEY;
130
+ const previousUrl = process.env.SYSTEM_ONE_BASE_URL;
131
+ const originalFetch = globalThis.fetch;
132
+ try {
133
+ process.env.SYSTEM_ONE_API_KEY = "test-key";
134
+ delete process.env.SYSTEM_ONE_BASE_URL;
135
+ const { pi, cap } = harness();
136
+ const store: SessionStore = { session: createSessionConfig(process.env) };
137
+ registerSystemOneCommands(pi, store);
138
+ await cap.handler?.("config typesafe", ctxFor([], cap));
139
+ globalThis.fetch = (async (_url, options) => {
140
+ assert.equal(
141
+ new Headers(options?.headers).get("authorization"),
142
+ "Bearer test-key",
143
+ );
144
+ return new Response(
145
+ JSON.stringify({ answers: { q: { type: "noul", noul: 0.7 } } }),
146
+ { status: 200 },
147
+ );
148
+ }) as typeof fetch;
149
+ const session = store.session;
150
+ assert.ok(session);
151
+ const provider = await resolveSessionProvider(session, {
152
+ modelRegistry: { getApiKeyForProvider: async () => undefined },
153
+ } as never);
154
+ await provider.evaluate({
155
+ state: "x",
156
+ questions: { q: { type: "noul", instructions: "Yes?" } },
157
+ });
158
+ } finally {
159
+ if (previousKey === undefined) delete process.env.SYSTEM_ONE_API_KEY;
160
+ else process.env.SYSTEM_ONE_API_KEY = previousKey;
161
+ if (previousUrl === undefined) delete process.env.SYSTEM_ONE_BASE_URL;
162
+ else process.env.SYSTEM_ONE_BASE_URL = previousUrl;
163
+ globalThis.fetch = originalFetch;
164
+ }
165
+ });
166
+ it("does not carry a custom endpoint environment key to TypeSafe", async () => {
167
+ const previousKey = process.env.SYSTEM_ONE_API_KEY;
168
+ const previousUrl = process.env.SYSTEM_ONE_BASE_URL;
169
+ try {
170
+ process.env.SYSTEM_ONE_API_KEY = "custom-key";
171
+ process.env.SYSTEM_ONE_BASE_URL = "https://custom.example";
172
+ const { pi, cap } = harness();
173
+ const store: SessionStore = { session: createSessionConfig(process.env) };
174
+ registerSystemOneCommands(pi, store);
175
+ await cap.handler?.("config typesafe", ctxFor([], cap));
176
+ assert.equal(store.session?.current.apiKey, undefined);
177
+ } finally {
178
+ if (previousKey === undefined) delete process.env.SYSTEM_ONE_API_KEY;
179
+ else process.env.SYSTEM_ONE_API_KEY = previousKey;
180
+ if (previousUrl === undefined) delete process.env.SYSTEM_ONE_BASE_URL;
181
+ else process.env.SYSTEM_ONE_BASE_URL = previousUrl;
182
+ }
183
+ });
184
+ it("does not save a URL containing a token or change the active session", async () => {
185
+ const dir = mkdtempSync(join(tmpdir(), "so-command-"));
186
+ try {
187
+ const path = join(dir, "pi-system-one.json");
188
+ const { pi, cap } = harness();
189
+ const store: SessionStore = { session: createSessionConfig({}) };
190
+ registerSystemOneCommands(pi, store, path);
191
+ await cap.handler?.(
192
+ "config custom",
193
+ ctxFor(["https://example.com/?token=secret", "", ""], cap),
194
+ );
195
+ assert.equal(store.session?.mode, "typesafe");
196
+ assert.match(cap.notices.at(-1) ?? "", /Could not save settings/);
197
+ assert.doesNotMatch(cap.notices.join(" "), /secret/);
198
+ assert.throws(() => readFileSync(path, "utf8"), /ENOENT/);
199
+ } finally {
200
+ rmSync(dir, { recursive: true, force: true });
201
+ }
202
+ });
203
+ it("persists custom settings without session key and switches back to native", async () => {
204
+ const dir = mkdtempSync(join(tmpdir(), "so-command-"));
205
+ try {
206
+ const path = join(dir, "pi-system-one.json");
207
+ const { pi, cap } = harness();
208
+ const store: SessionStore = {};
209
+ registerSystemOneCommands(pi, store, path);
210
+ await cap.handler?.(
211
+ "config custom",
212
+ ctxFor(["http://localhost:8008", "reflex", "secret"], cap),
213
+ );
214
+ assert.equal(store.session?.current.apiKey, "secret");
215
+ const saved = readFileSync(path, "utf8");
216
+ assert.doesNotMatch(saved, /secret|apiKey/);
217
+ assert.match(saved, /localhost:8008/);
218
+ await cap.handler?.("config native", ctxFor([], cap));
219
+ assert.equal(store.session?.mode, "native");
220
+ assert.equal(store.session?.current.apiKey, undefined);
221
+ assert.match(readFileSync(path, "utf8"), /native/);
222
+ } finally {
223
+ rmSync(dir, { recursive: true, force: true });
224
+ }
225
+ });
56
226
  it("completes config + status, case-insensitively", () => {
57
227
  const { pi, cap } = harness();
58
228
  registerSystemOneCommands(pi, {});
@@ -66,13 +236,14 @@ describe("so command", () => {
66
236
  );
67
237
  assert.deepEqual(cap.completions?.("x"), []);
68
238
  });
69
- it("bare /so configures from blank session and registers the tool", async () => {
239
+ it("registers the tool before config and switches to custom on request", async () => {
70
240
  const { pi, cap } = harness();
71
241
  const store: SessionStore = {};
72
- registerSystemOneCommands(pi, store);
73
- await cap.handler?.("", ctxFor(["http://x/", "", ""], cap));
242
+ registerSystemOneCommands(pi, store).applyProvider();
74
243
  assert.deepEqual(cap.tools, ["system_one"]);
244
+ await cap.handler?.("", ctxFor(["custom", "http://x/", "", ""], cap));
75
245
  assert.equal(store.session?.current.baseUrl, "http://x/");
246
+ assert.equal(store.session?.mode, "custom");
76
247
  });
77
248
  it("cancel (undefined) keeps everything and registers nothing", async () => {
78
249
  const { pi, cap } = harness();
@@ -89,15 +260,15 @@ describe("so command", () => {
89
260
  session: createSessionConfig({ SYSTEM_ONE_BASE_URL: "http://keep/" }),
90
261
  };
91
262
  registerSystemOneCommands(pi, store);
92
- await cap.handler?.("", ctxFor(["", "", ""], cap));
263
+ await cap.handler?.("", ctxFor(["custom", "", "", ""], cap));
93
264
  assert.equal(store.session?.current.baseUrl, "http://keep/");
94
265
  });
95
266
  it("whitespace-only base URL is rejected", async () => {
96
267
  const { pi, cap } = harness();
97
- const store: SessionStore = {};
268
+ const store: SessionStore = { session: createSessionConfig({}) };
98
269
  registerSystemOneCommands(pi, store);
99
- await cap.handler?.("", ctxFor([" ", "", ""], cap));
100
- assert.equal(store.session, undefined);
270
+ await cap.handler?.("", ctxFor(["custom", " ", "", ""], cap));
271
+ assert.equal(store.session?.mode, "typesafe");
101
272
  assert.match(cap.notices[0], /Cancelled/);
102
273
  });
103
274
  it("'-' forgets a memory key back to env", async () => {
@@ -106,15 +277,33 @@ describe("so command", () => {
106
277
  session: createSessionConfig({ SYSTEM_ONE_BASE_URL: "http://x/" }),
107
278
  };
108
279
  registerSystemOneCommands(pi, store);
109
- await cap.handler?.("", ctxFor(["", "", "sk-mem", ""], cap));
280
+ await cap.handler?.("", ctxFor(["custom", "", "", "sk-mem"], cap));
110
281
  assert.equal(store.session?.keyInMemory, true);
111
- await cap.handler?.("", ctxFor(["", "", "-", ""], cap));
112
- // After forgetting the memory key, the API key should revert to the environment variable (if any).
113
- // In the CI environment, SYSTEM_ONE_API_KEY may be set; otherwise it will be undefined.
282
+ await cap.handler?.("", ctxFor(["custom", "", "", "-"], cap));
283
+ // A key from another endpoint must never follow this configured URL.
114
284
  assert.equal(store.session?.keyInMemory, false);
115
285
  assert.equal(
116
286
  store.session?.current.apiKey,
117
- process.env.SYSTEM_ONE_API_KEY || undefined,
287
+ process.env.SYSTEM_ONE_BASE_URL === "http://x/"
288
+ ? process.env.SYSTEM_ONE_API_KEY || undefined
289
+ : undefined,
290
+ );
291
+ });
292
+ it("native status reports Pi-managed credentials, not unused HTTP environment key", async () => {
293
+ const { pi, cap } = harness();
294
+ const store: SessionStore = {
295
+ session: createSessionConfig(
296
+ { SYSTEM_ONE_API_KEY: "test-env-key", SYSTEM_ONE_MODEL: "other-model" },
297
+ { mode: "native" },
298
+ ),
299
+ };
300
+ registerSystemOneCommands(pi, store);
301
+ await cap.handler?.("status", ctxFor([], cap));
302
+ assert.match(cap.notices[0], /api key: managed by Pi/);
303
+ assert.match(cap.notices[0], /model: jev-latest/);
304
+ assert.doesNotMatch(
305
+ cap.notices[0],
306
+ /from environment|test-env-key|other-model/,
118
307
  );
119
308
  });
120
309
  it("status shows current config; STATUS routes case-insensitively", async () => {
@@ -126,10 +315,11 @@ describe("so command", () => {
126
315
  await cap.handler?.("STATUS", ctxFor([], cap));
127
316
  assert.match(cap.notices[0], /http:\/\/x\//);
128
317
  });
129
- it("status with no session explains how to configure", async () => {
318
+ it("status shows TypeSafe defaults and login guidance without auth", async () => {
130
319
  const { pi, cap } = harness();
131
- registerSystemOneCommands(pi, {});
320
+ registerSystemOneCommands(pi, { session: createSessionConfig({}) });
132
321
  await cap.handler?.("status", ctxFor([], cap));
133
- assert.match(cap.notices[0], /not configured/);
322
+ assert.match(cap.notices[0], /typesafe/);
323
+ assert.match(cap.notices[0], /login typesafe/);
134
324
  });
135
325
  });
@@ -11,6 +11,65 @@ import {
11
11
  } from "../src/config.ts";
12
12
 
13
13
  describe("config", () => {
14
+ it("native mode ignores HTTP environment key and model", () => {
15
+ const s = createSessionConfig(
16
+ { SYSTEM_ONE_API_KEY: "test-env-key", SYSTEM_ONE_MODEL: "other-model" },
17
+ { mode: "native", model: "stored-model", baseUrl: "http://stale/" },
18
+ );
19
+ assert.equal(s.mode, "native");
20
+ assert.equal(s.current.baseUrl, "");
21
+ assert.equal(s.current.apiKey, undefined);
22
+ assert.equal(s.current.model, "jev-latest");
23
+ assert.match(describeConfig(s), /api key: managed by Pi/);
24
+ });
25
+ it("defaults to TypeSafe Jev without environment configuration", () => {
26
+ const s = createSessionConfig({});
27
+ assert.equal(s.mode, "typesafe");
28
+ assert.equal(s.current.baseUrl, "https://api.typesafe.ai");
29
+ assert.equal(s.current.model, "jev-latest");
30
+ assert.equal(s.current.apiKey, undefined);
31
+ });
32
+ it("keeps environment endpoint ahead of saved custom config", () => {
33
+ const s = createSessionConfig(
34
+ { SYSTEM_ONE_BASE_URL: "http://existing/", SYSTEM_ONE_MODEL: "old" },
35
+ { mode: "native", model: "new", timeoutMs: 5000 },
36
+ );
37
+ assert.equal(s.mode, "custom");
38
+ assert.equal(s.current.baseUrl, "http://existing/");
39
+ assert.equal(s.current.model, "old");
40
+ assert.equal(s.current.timeoutMs, 5000);
41
+ });
42
+ it("does not carry a saved model to a different environment endpoint", () => {
43
+ const s = createSessionConfig(
44
+ { SYSTEM_ONE_BASE_URL: "http://new/" },
45
+ { mode: "custom", baseUrl: "http://old/", model: "old-model" },
46
+ );
47
+ assert.equal(s.current.model, undefined);
48
+ assert.equal(s.current.baseUrl, "http://new/");
49
+ });
50
+ it("keeps saved model when environment endpoint matches saved endpoint", () => {
51
+ const s = createSessionConfig(
52
+ { SYSTEM_ONE_BASE_URL: "http://same/" },
53
+ { mode: "custom", baseUrl: "http://same/", model: "saved-model" },
54
+ );
55
+ assert.equal(s.current.model, "saved-model");
56
+ });
57
+ it("does not forward an env key to a saved custom URL without an env endpoint", () => {
58
+ const s = createSessionConfig(
59
+ { SYSTEM_ONE_API_KEY: "key-for-other-endpoint" },
60
+ { mode: "custom", baseUrl: "https://other.example" },
61
+ );
62
+ assert.equal(s.current.apiKey, undefined);
63
+ });
64
+ it("loads saved custom config without an environment variable", () => {
65
+ const s = createSessionConfig(
66
+ {},
67
+ { mode: "custom", baseUrl: "http://localhost:8008", model: "reflex" },
68
+ );
69
+ assert.equal(s.mode, "custom");
70
+ assert.equal(s.current.model, "reflex");
71
+ assert.equal(s.current.baseUrl, "http://localhost:8008");
72
+ });
14
73
  it("loads from env", () => {
15
74
  const s = createSessionConfig({
16
75
  SYSTEM_ONE_BASE_URL: "http://localhost:8008",
@@ -1,15 +1,174 @@
1
1
  // pi-system-one/tests/extension.test.ts
2
2
 
3
3
  import assert from "node:assert/strict";
4
- import { existsSync, readFileSync } from "node:fs";
4
+ import {
5
+ existsSync,
6
+ mkdtempSync,
7
+ readFileSync,
8
+ rmSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import { tmpdir } from "node:os";
5
12
  import { join } from "node:path";
6
13
  import { describe, it } from "node:test";
7
14
  import piSystemOneExtension, {
8
15
  systemOnePromptsDir,
9
16
  systemOneSkillsDir,
10
17
  } from "../src/extension.ts";
18
+ import type { buildSystemOneTool } from "../src/tool.ts";
11
19
 
12
20
  describe("extension resources", () => {
21
+ it("startup warning does not echo an untrusted settings field name", () => {
22
+ const dir = mkdtempSync(join(tmpdir(), "so-extension-warning-"));
23
+ const previous = process.env.PI_CODING_AGENT_DIR;
24
+ try {
25
+ process.env.PI_CODING_AGENT_DIR = dir;
26
+ writeFileSync(
27
+ join(dir, "pi-system-one.json"),
28
+ JSON.stringify({ "Bearer secret-token": "ignored" }),
29
+ );
30
+ const handlers: Record<string, (event: never, ctx: never) => void> = {};
31
+ piSystemOneExtension({
32
+ registerTool: () => {},
33
+ registerCommand: () => {},
34
+ on: (name: string, handler: (event: never, ctx: never) => void) => {
35
+ handlers[name] = handler;
36
+ },
37
+ } as never);
38
+ let warning = "";
39
+ handlers.session_start(
40
+ { reason: "startup" } as never,
41
+ {
42
+ ui: {
43
+ notify: (message: string) => {
44
+ warning = message;
45
+ },
46
+ },
47
+ } as never,
48
+ );
49
+ assert.match(warning, /Ignoring saved settings/);
50
+ assert.doesNotMatch(warning, /Bearer|secret-token/);
51
+ } finally {
52
+ if (previous === undefined) delete process.env.PI_CODING_AGENT_DIR;
53
+ else process.env.PI_CODING_AGENT_DIR = previous;
54
+ rmSync(dir, { recursive: true, force: true });
55
+ }
56
+ });
57
+ it("uses the registered tool after switching to native mode and rejects malformed success", async () => {
58
+ const dir = mkdtempSync(join(tmpdir(), "so-extension-native-"));
59
+ const previous = {
60
+ dir: process.env.PI_CODING_AGENT_DIR,
61
+ base: process.env.SYSTEM_ONE_BASE_URL,
62
+ };
63
+ try {
64
+ process.env.PI_CODING_AGENT_DIR = dir;
65
+ delete process.env.SYSTEM_ONE_BASE_URL;
66
+ let tool: ReturnType<typeof buildSystemOneTool> | undefined;
67
+ let command: ((args: string, ctx: never) => Promise<void>) | undefined;
68
+ piSystemOneExtension({
69
+ registerTool: (value: ReturnType<typeof buildSystemOneTool>) => {
70
+ tool = value;
71
+ },
72
+ registerCommand: (
73
+ _name: string,
74
+ options: { handler: typeof command },
75
+ ) => {
76
+ command = options.handler;
77
+ },
78
+ on: () => {},
79
+ } as never);
80
+ await command?.("config native", { ui: { notify: () => {} } } as never);
81
+ assert.ok(tool);
82
+ let probability = 0.7;
83
+ const ctx = {
84
+ modelRegistry: {
85
+ findOfType: () => ({ id: "jev-latest" }),
86
+ classify: async () => ({
87
+ stopReason: "stop",
88
+ model: "jev-latest",
89
+ provider: "typesafe",
90
+ answers: { q: { type: "bool", probability } },
91
+ }),
92
+ },
93
+ } as never;
94
+ const args = {
95
+ state: "x",
96
+ questions: { q: { type: "noul" as const, instructions: "Yes?" } },
97
+ };
98
+ const result = await tool.execute(
99
+ "native-valid",
100
+ args,
101
+ undefined,
102
+ undefined,
103
+ ctx,
104
+ );
105
+ assert.deepEqual(result.details.answers.q, { type: "noul", noul: 0.7 });
106
+ probability = 2;
107
+ await assert.rejects(
108
+ tool.execute("native-invalid", args, undefined, undefined, ctx),
109
+ /Pi native classifier failed/,
110
+ );
111
+ } finally {
112
+ if (previous.dir === undefined) delete process.env.PI_CODING_AGENT_DIR;
113
+ else process.env.PI_CODING_AGENT_DIR = previous.dir;
114
+ if (previous.base === undefined) delete process.env.SYSTEM_ONE_BASE_URL;
115
+ else process.env.SYSTEM_ONE_BASE_URL = previous.base;
116
+ rmSync(dir, { recursive: true, force: true });
117
+ }
118
+ });
119
+ it("reloads saved custom config across extension instances", async () => {
120
+ const dir = mkdtempSync(join(tmpdir(), "so-extension-"));
121
+ const prev = {
122
+ base: process.env.SYSTEM_ONE_BASE_URL,
123
+ model: process.env.SYSTEM_ONE_MODEL,
124
+ agentDir: process.env.PI_CODING_AGENT_DIR,
125
+ };
126
+ try {
127
+ delete process.env.SYSTEM_ONE_BASE_URL;
128
+ delete process.env.SYSTEM_ONE_MODEL;
129
+ process.env.PI_CODING_AGENT_DIR = dir;
130
+ let command: ((args: string, ctx: never) => Promise<void>) | undefined;
131
+ const pi = {
132
+ registerTool: () => {},
133
+ registerCommand: (
134
+ _name: string,
135
+ options: { handler: typeof command },
136
+ ) => {
137
+ command = options.handler;
138
+ },
139
+ on: () => {},
140
+ };
141
+ piSystemOneExtension(pi as never);
142
+ const values = ["http://localhost:8008", "reflex", "temporary-key"];
143
+ await command?.("config custom", {
144
+ ui: { input: async () => values.shift(), notify: () => {} },
145
+ } as never);
146
+ const stored = readFileSync(join(dir, "pi-system-one.json"), "utf8");
147
+ assert.doesNotMatch(stored, /temporary-key|apiKey/);
148
+ piSystemOneExtension(pi as never);
149
+ let status = "";
150
+ await command?.("status", {
151
+ ui: {
152
+ notify: (value: string) => {
153
+ status = value;
154
+ },
155
+ },
156
+ } as never);
157
+ assert.match(status, /mode: custom/);
158
+ assert.match(status, /localhost:8008/);
159
+ assert.match(status, /model: reflex/);
160
+ } finally {
161
+ for (const [key, value] of Object.entries({
162
+ SYSTEM_ONE_BASE_URL: prev.base,
163
+ SYSTEM_ONE_MODEL: prev.model,
164
+ PI_CODING_AGENT_DIR: prev.agentDir,
165
+ })) {
166
+ if (value === undefined) delete process.env[key];
167
+ else process.env[key] = value;
168
+ }
169
+ rmSync(dir, { recursive: true, force: true });
170
+ }
171
+ });
13
172
  it("contributes a valid system-one skill directory", async () => {
14
173
  const prev = process.env.SYSTEM_ONE_BASE_URL;
15
174
  process.env.SYSTEM_ONE_BASE_URL = "http://localhost:8008";
@@ -0,0 +1,88 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { it } from "node:test";
6
+ import { loadStoredConfig, saveStoredConfig } from "../src/persistence.ts";
7
+
8
+ it("round-trips persistent settings without writing a key", () => {
9
+ const dir = mkdtempSync(join(tmpdir(), "so-config-"));
10
+ try {
11
+ const path = join(dir, "pi-system-one.json");
12
+ saveStoredConfig(path, {
13
+ mode: "custom",
14
+ baseUrl: "http://localhost:8008",
15
+ model: "reflex",
16
+ timeoutMs: 9000,
17
+ });
18
+ assert.deepEqual(loadStoredConfig(path), {
19
+ mode: "custom",
20
+ baseUrl: "http://localhost:8008",
21
+ model: "reflex",
22
+ timeoutMs: 9000,
23
+ });
24
+ assert.doesNotMatch(readFileSync(path, "utf8"), /apiKey/);
25
+ } finally {
26
+ rmSync(dir, { recursive: true, force: true });
27
+ }
28
+ });
29
+
30
+ it("rejects credential-bearing endpoint URLs without persisting them", () => {
31
+ const dir = mkdtempSync(join(tmpdir(), "so-config-"));
32
+ try {
33
+ const path = join(dir, "pi-system-one.json");
34
+ for (const baseUrl of [
35
+ "https://user:secret@example.com",
36
+ "https://example.com/?token=secret",
37
+ "https://example.com/#secret",
38
+ ]) {
39
+ assert.throws(
40
+ () => saveStoredConfig(path, { mode: "custom", baseUrl }),
41
+ (error: Error) =>
42
+ !error.message.includes("secret") && /baseUrl/.test(error.message),
43
+ );
44
+ assert.throws(() => readFileSync(path, "utf8"), /ENOENT/);
45
+ }
46
+ writeFileSync(
47
+ path,
48
+ JSON.stringify({
49
+ mode: "custom",
50
+ baseUrl: "https://example.com/?token=secret",
51
+ }),
52
+ );
53
+ assert.throws(() => loadStoredConfig(path), /baseUrl/);
54
+ } finally {
55
+ rmSync(dir, { recursive: true, force: true });
56
+ }
57
+ });
58
+
59
+ it("does not echo unknown JSON field names in diagnostics", () => {
60
+ const dir = mkdtempSync(join(tmpdir(), "so-config-"));
61
+ try {
62
+ const path = join(dir, "pi-system-one.json");
63
+ writeFileSync(path, JSON.stringify({ "Bearer secret-token": "ignored" }));
64
+ assert.throws(
65
+ () => loadStoredConfig(path),
66
+ (error: Error) => {
67
+ assert.match(error.message, /unknown field/);
68
+ assert.doesNotMatch(error.message, /Bearer|secret-token/);
69
+ return true;
70
+ },
71
+ );
72
+ } finally {
73
+ rmSync(dir, { recursive: true, force: true });
74
+ }
75
+ });
76
+
77
+ it("rejects malformed settings and refuses stored secrets", () => {
78
+ const dir = mkdtempSync(join(tmpdir(), "so-config-"));
79
+ try {
80
+ const path = join(dir, "pi-system-one.json");
81
+ writeFileSync(path, JSON.stringify({ mode: "typesafe", apiKey: "secret" }));
82
+ assert.throws(() => loadStoredConfig(path), /apiKey/);
83
+ writeFileSync(path, "{");
84
+ assert.throws(() => loadStoredConfig(path), /pi-system-one.json/);
85
+ } finally {
86
+ rmSync(dir, { recursive: true, force: true });
87
+ }
88
+ });