pi-system-one 1.2.4 → 1.3.1
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 +43 -23
- package/package.json +4 -1
- package/src/commands.ts +166 -78
- package/src/config.ts +64 -12
- package/src/extension.ts +23 -13
- package/src/persistence.ts +111 -0
- package/src/provider.ts +184 -0
- package/src/tool.ts +63 -7
- package/tests/commands.test.ts +252 -17
- package/tests/config.test.ts +59 -0
- package/tests/extension.test.ts +160 -1
- package/tests/persistence.test.ts +88 -0
- package/tests/provider.test.ts +329 -0
- package/tests/tool.test.ts +246 -0
package/tests/commands.test.ts
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
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[];
|
|
13
17
|
handler: ((args: string, ctx: never) => Promise<void>) | undefined;
|
|
14
18
|
completions: ((prefix: string) => { value: string }[]) | undefined;
|
|
15
19
|
notices: string[];
|
|
20
|
+
selections: { title: string; options: string[] }[];
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
function harness() {
|
|
@@ -21,6 +26,7 @@ function harness() {
|
|
|
21
26
|
handler: undefined,
|
|
22
27
|
completions: undefined,
|
|
23
28
|
notices: [],
|
|
29
|
+
selections: [],
|
|
24
30
|
};
|
|
25
31
|
const pi = {
|
|
26
32
|
registerTool: (t: { name: string }) => {
|
|
@@ -41,10 +47,23 @@ function harness() {
|
|
|
41
47
|
return { pi: pi as never, cap };
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
function ctxFor(
|
|
50
|
+
function ctxFor(
|
|
51
|
+
answers: (string | undefined)[],
|
|
52
|
+
cap: Captured,
|
|
53
|
+
defaults?: string[],
|
|
54
|
+
) {
|
|
45
55
|
return {
|
|
56
|
+
modelRegistry: { getApiKeyForProvider: async () => undefined },
|
|
46
57
|
ui: {
|
|
47
|
-
|
|
58
|
+
select: async (title: string, options: string[]) => {
|
|
59
|
+
cap.selections.push({ title, options });
|
|
60
|
+
const choice = answers.shift();
|
|
61
|
+
return options.find((option) => option.startsWith(choice ?? "\0"));
|
|
62
|
+
},
|
|
63
|
+
input: async (_prompt: string, initial?: string) => {
|
|
64
|
+
defaults?.push(initial ?? "");
|
|
65
|
+
return answers.shift();
|
|
66
|
+
},
|
|
48
67
|
notify: (msg: string) => {
|
|
49
68
|
cap.notices.push(msg);
|
|
50
69
|
},
|
|
@@ -53,6 +72,202 @@ function ctxFor(answers: (string | undefined)[], cap: Captured) {
|
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
describe("so command", () => {
|
|
75
|
+
it("shows selectable provider modes for /so and /so config", async () => {
|
|
76
|
+
const { pi, cap } = harness();
|
|
77
|
+
const store: SessionStore = { session: createSessionConfig({}) };
|
|
78
|
+
registerSystemOneCommands(pi, store);
|
|
79
|
+
await cap.handler?.("", ctxFor(["native"], cap));
|
|
80
|
+
assert.equal(store.session?.mode, "native");
|
|
81
|
+
await cap.handler?.("config", ctxFor(["typesafe"], cap));
|
|
82
|
+
assert.equal(store.session?.mode, "typesafe");
|
|
83
|
+
assert.equal(cap.selections.length, 2);
|
|
84
|
+
assert.equal(cap.selections[0].options.length, 3);
|
|
85
|
+
assert.match(cap.selections[0].options[0], /typesafe.*current/i);
|
|
86
|
+
assert.match(cap.selections[1].options[0], /native.*current/i);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("cancels selection without changing mode or saved settings", async () => {
|
|
90
|
+
const dir = mkdtempSync(join(tmpdir(), "so-select-"));
|
|
91
|
+
try {
|
|
92
|
+
const path = join(dir, "pi-system-one.json");
|
|
93
|
+
const { pi, cap } = harness();
|
|
94
|
+
const store: SessionStore = { session: createSessionConfig({}) };
|
|
95
|
+
registerSystemOneCommands(pi, store, path);
|
|
96
|
+
await cap.handler?.("config", ctxFor([undefined], cap));
|
|
97
|
+
assert.equal(store.session?.mode, "typesafe");
|
|
98
|
+
assert.throws(() => readFileSync(path, "utf8"), /ENOENT/);
|
|
99
|
+
assert.match(cap.notices.at(-1) ?? "", /Cancelled/);
|
|
100
|
+
} finally {
|
|
101
|
+
rmSync(dir, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("direct mode command bypasses selection", async () => {
|
|
106
|
+
const { pi, cap } = harness();
|
|
107
|
+
const store: SessionStore = { session: createSessionConfig({}) };
|
|
108
|
+
registerSystemOneCommands(pi, store);
|
|
109
|
+
await cap.handler?.("config native", ctxFor([], cap));
|
|
110
|
+
assert.equal(store.session?.mode, "native");
|
|
111
|
+
assert.equal(cap.selections.length, 0);
|
|
112
|
+
});
|
|
113
|
+
it("clears prior model on custom endpoint change with blank model", async () => {
|
|
114
|
+
const dir = mkdtempSync(join(tmpdir(), "so-model-change-"));
|
|
115
|
+
const originalFetch = globalThis.fetch;
|
|
116
|
+
try {
|
|
117
|
+
const path = join(dir, "pi-system-one.json");
|
|
118
|
+
const { pi, cap } = harness();
|
|
119
|
+
const store: SessionStore = {
|
|
120
|
+
session: createSessionConfig({
|
|
121
|
+
SYSTEM_ONE_BASE_URL: "http://localhost:8008",
|
|
122
|
+
SYSTEM_ONE_MODEL: "reflex",
|
|
123
|
+
}),
|
|
124
|
+
};
|
|
125
|
+
registerSystemOneCommands(pi, store, path);
|
|
126
|
+
const defaults: string[] = [];
|
|
127
|
+
await cap.handler?.(
|
|
128
|
+
"config custom",
|
|
129
|
+
ctxFor(["http://localhost:8009", "", ""], cap, defaults),
|
|
130
|
+
);
|
|
131
|
+
assert.equal(defaults[1], "");
|
|
132
|
+
assert.equal(store.session?.current.model, undefined);
|
|
133
|
+
assert.equal(JSON.parse(readFileSync(path, "utf8")).model, undefined);
|
|
134
|
+
globalThis.fetch = (async (url, options) => {
|
|
135
|
+
assert.equal(url, "http://localhost:8009/v1/systemone");
|
|
136
|
+
assert.equal(JSON.parse(String(options?.body)).model, undefined);
|
|
137
|
+
return new Response(
|
|
138
|
+
JSON.stringify({ answers: { q: { type: "noul", noul: 0.6 } } }),
|
|
139
|
+
{ status: 200 },
|
|
140
|
+
);
|
|
141
|
+
}) as typeof fetch;
|
|
142
|
+
const session = store.session;
|
|
143
|
+
assert.ok(session);
|
|
144
|
+
const provider = await resolveSessionProvider(session, {} as never);
|
|
145
|
+
await provider.evaluate({
|
|
146
|
+
state: "x",
|
|
147
|
+
questions: { q: { type: "noul", instructions: "Yes?" } },
|
|
148
|
+
});
|
|
149
|
+
} finally {
|
|
150
|
+
globalThis.fetch = originalFetch;
|
|
151
|
+
rmSync(dir, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("keeps model for same custom endpoint and accepts explicit replacement", async () => {
|
|
156
|
+
const { pi, cap } = harness();
|
|
157
|
+
const store: SessionStore = {
|
|
158
|
+
session: createSessionConfig({
|
|
159
|
+
SYSTEM_ONE_BASE_URL: "http://localhost:8008",
|
|
160
|
+
SYSTEM_ONE_MODEL: "reflex",
|
|
161
|
+
}),
|
|
162
|
+
};
|
|
163
|
+
registerSystemOneCommands(pi, store);
|
|
164
|
+
await cap.handler?.("config custom", ctxFor(["", "", ""], cap));
|
|
165
|
+
assert.equal(store.session?.current.model, "reflex");
|
|
166
|
+
await cap.handler?.(
|
|
167
|
+
"config custom",
|
|
168
|
+
ctxFor(["http://localhost:8009", "replacement", ""], cap),
|
|
169
|
+
);
|
|
170
|
+
assert.equal(store.session?.current.model, "replacement");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("keeps explicit TypeSafe environment key after selecting TypeSafe", async () => {
|
|
174
|
+
const previousKey = process.env.SYSTEM_ONE_API_KEY;
|
|
175
|
+
const previousUrl = process.env.SYSTEM_ONE_BASE_URL;
|
|
176
|
+
const originalFetch = globalThis.fetch;
|
|
177
|
+
try {
|
|
178
|
+
process.env.SYSTEM_ONE_API_KEY = "test-key";
|
|
179
|
+
delete process.env.SYSTEM_ONE_BASE_URL;
|
|
180
|
+
const { pi, cap } = harness();
|
|
181
|
+
const store: SessionStore = { session: createSessionConfig(process.env) };
|
|
182
|
+
registerSystemOneCommands(pi, store);
|
|
183
|
+
await cap.handler?.("config typesafe", ctxFor([], cap));
|
|
184
|
+
globalThis.fetch = (async (_url, options) => {
|
|
185
|
+
assert.equal(
|
|
186
|
+
new Headers(options?.headers).get("authorization"),
|
|
187
|
+
"Bearer test-key",
|
|
188
|
+
);
|
|
189
|
+
return new Response(
|
|
190
|
+
JSON.stringify({ answers: { q: { type: "noul", noul: 0.7 } } }),
|
|
191
|
+
{ status: 200 },
|
|
192
|
+
);
|
|
193
|
+
}) as typeof fetch;
|
|
194
|
+
const session = store.session;
|
|
195
|
+
assert.ok(session);
|
|
196
|
+
const provider = await resolveSessionProvider(session, {
|
|
197
|
+
modelRegistry: { getApiKeyForProvider: async () => undefined },
|
|
198
|
+
} as never);
|
|
199
|
+
await provider.evaluate({
|
|
200
|
+
state: "x",
|
|
201
|
+
questions: { q: { type: "noul", instructions: "Yes?" } },
|
|
202
|
+
});
|
|
203
|
+
} finally {
|
|
204
|
+
if (previousKey === undefined) delete process.env.SYSTEM_ONE_API_KEY;
|
|
205
|
+
else process.env.SYSTEM_ONE_API_KEY = previousKey;
|
|
206
|
+
if (previousUrl === undefined) delete process.env.SYSTEM_ONE_BASE_URL;
|
|
207
|
+
else process.env.SYSTEM_ONE_BASE_URL = previousUrl;
|
|
208
|
+
globalThis.fetch = originalFetch;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
it("does not carry a custom endpoint environment key to TypeSafe", async () => {
|
|
212
|
+
const previousKey = process.env.SYSTEM_ONE_API_KEY;
|
|
213
|
+
const previousUrl = process.env.SYSTEM_ONE_BASE_URL;
|
|
214
|
+
try {
|
|
215
|
+
process.env.SYSTEM_ONE_API_KEY = "custom-key";
|
|
216
|
+
process.env.SYSTEM_ONE_BASE_URL = "https://custom.example";
|
|
217
|
+
const { pi, cap } = harness();
|
|
218
|
+
const store: SessionStore = { session: createSessionConfig(process.env) };
|
|
219
|
+
registerSystemOneCommands(pi, store);
|
|
220
|
+
await cap.handler?.("config typesafe", ctxFor([], cap));
|
|
221
|
+
assert.equal(store.session?.current.apiKey, undefined);
|
|
222
|
+
} finally {
|
|
223
|
+
if (previousKey === undefined) delete process.env.SYSTEM_ONE_API_KEY;
|
|
224
|
+
else process.env.SYSTEM_ONE_API_KEY = previousKey;
|
|
225
|
+
if (previousUrl === undefined) delete process.env.SYSTEM_ONE_BASE_URL;
|
|
226
|
+
else process.env.SYSTEM_ONE_BASE_URL = previousUrl;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
it("does not save a URL containing a token or change the active session", async () => {
|
|
230
|
+
const dir = mkdtempSync(join(tmpdir(), "so-command-"));
|
|
231
|
+
try {
|
|
232
|
+
const path = join(dir, "pi-system-one.json");
|
|
233
|
+
const { pi, cap } = harness();
|
|
234
|
+
const store: SessionStore = { session: createSessionConfig({}) };
|
|
235
|
+
registerSystemOneCommands(pi, store, path);
|
|
236
|
+
await cap.handler?.(
|
|
237
|
+
"config custom",
|
|
238
|
+
ctxFor(["https://example.com/?token=secret", "", ""], cap),
|
|
239
|
+
);
|
|
240
|
+
assert.equal(store.session?.mode, "typesafe");
|
|
241
|
+
assert.match(cap.notices.at(-1) ?? "", /Could not save settings/);
|
|
242
|
+
assert.doesNotMatch(cap.notices.join(" "), /secret/);
|
|
243
|
+
assert.throws(() => readFileSync(path, "utf8"), /ENOENT/);
|
|
244
|
+
} finally {
|
|
245
|
+
rmSync(dir, { recursive: true, force: true });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
it("persists custom settings without session key and switches back to native", async () => {
|
|
249
|
+
const dir = mkdtempSync(join(tmpdir(), "so-command-"));
|
|
250
|
+
try {
|
|
251
|
+
const path = join(dir, "pi-system-one.json");
|
|
252
|
+
const { pi, cap } = harness();
|
|
253
|
+
const store: SessionStore = {};
|
|
254
|
+
registerSystemOneCommands(pi, store, path);
|
|
255
|
+
await cap.handler?.(
|
|
256
|
+
"config custom",
|
|
257
|
+
ctxFor(["http://localhost:8008", "reflex", "secret"], cap),
|
|
258
|
+
);
|
|
259
|
+
assert.equal(store.session?.current.apiKey, "secret");
|
|
260
|
+
const saved = readFileSync(path, "utf8");
|
|
261
|
+
assert.doesNotMatch(saved, /secret|apiKey/);
|
|
262
|
+
assert.match(saved, /localhost:8008/);
|
|
263
|
+
await cap.handler?.("config native", ctxFor([], cap));
|
|
264
|
+
assert.equal(store.session?.mode, "native");
|
|
265
|
+
assert.equal(store.session?.current.apiKey, undefined);
|
|
266
|
+
assert.match(readFileSync(path, "utf8"), /native/);
|
|
267
|
+
} finally {
|
|
268
|
+
rmSync(dir, { recursive: true, force: true });
|
|
269
|
+
}
|
|
270
|
+
});
|
|
56
271
|
it("completes config + status, case-insensitively", () => {
|
|
57
272
|
const { pi, cap } = harness();
|
|
58
273
|
registerSystemOneCommands(pi, {});
|
|
@@ -66,13 +281,14 @@ describe("so command", () => {
|
|
|
66
281
|
);
|
|
67
282
|
assert.deepEqual(cap.completions?.("x"), []);
|
|
68
283
|
});
|
|
69
|
-
it("
|
|
284
|
+
it("registers the tool before config and switches to custom on request", async () => {
|
|
70
285
|
const { pi, cap } = harness();
|
|
71
286
|
const store: SessionStore = {};
|
|
72
|
-
registerSystemOneCommands(pi, store);
|
|
73
|
-
await cap.handler?.("", ctxFor(["http://x/", "", ""], cap));
|
|
287
|
+
registerSystemOneCommands(pi, store).applyProvider();
|
|
74
288
|
assert.deepEqual(cap.tools, ["system_one"]);
|
|
289
|
+
await cap.handler?.("", ctxFor(["custom", "http://x/", "", ""], cap));
|
|
75
290
|
assert.equal(store.session?.current.baseUrl, "http://x/");
|
|
291
|
+
assert.equal(store.session?.mode, "custom");
|
|
76
292
|
});
|
|
77
293
|
it("cancel (undefined) keeps everything and registers nothing", async () => {
|
|
78
294
|
const { pi, cap } = harness();
|
|
@@ -89,15 +305,15 @@ describe("so command", () => {
|
|
|
89
305
|
session: createSessionConfig({ SYSTEM_ONE_BASE_URL: "http://keep/" }),
|
|
90
306
|
};
|
|
91
307
|
registerSystemOneCommands(pi, store);
|
|
92
|
-
await cap.handler?.("", ctxFor(["", "", ""], cap));
|
|
308
|
+
await cap.handler?.("", ctxFor(["custom", "", "", ""], cap));
|
|
93
309
|
assert.equal(store.session?.current.baseUrl, "http://keep/");
|
|
94
310
|
});
|
|
95
311
|
it("whitespace-only base URL is rejected", async () => {
|
|
96
312
|
const { pi, cap } = harness();
|
|
97
|
-
const store: SessionStore = {};
|
|
313
|
+
const store: SessionStore = { session: createSessionConfig({}) };
|
|
98
314
|
registerSystemOneCommands(pi, store);
|
|
99
|
-
await cap.handler?.("", ctxFor([" ", "", ""], cap));
|
|
100
|
-
assert.equal(store.session,
|
|
315
|
+
await cap.handler?.("", ctxFor(["custom", " ", "", ""], cap));
|
|
316
|
+
assert.equal(store.session?.mode, "typesafe");
|
|
101
317
|
assert.match(cap.notices[0], /Cancelled/);
|
|
102
318
|
});
|
|
103
319
|
it("'-' forgets a memory key back to env", async () => {
|
|
@@ -106,15 +322,33 @@ describe("so command", () => {
|
|
|
106
322
|
session: createSessionConfig({ SYSTEM_ONE_BASE_URL: "http://x/" }),
|
|
107
323
|
};
|
|
108
324
|
registerSystemOneCommands(pi, store);
|
|
109
|
-
await cap.handler?.("", ctxFor(["", "", "
|
|
325
|
+
await cap.handler?.("", ctxFor(["custom", "", "", "sk-mem"], cap));
|
|
110
326
|
assert.equal(store.session?.keyInMemory, true);
|
|
111
|
-
await cap.handler?.("", ctxFor(["", "", "
|
|
112
|
-
//
|
|
113
|
-
// In the CI environment, SYSTEM_ONE_API_KEY may be set; otherwise it will be undefined.
|
|
327
|
+
await cap.handler?.("", ctxFor(["custom", "", "", "-"], cap));
|
|
328
|
+
// A key from another endpoint must never follow this configured URL.
|
|
114
329
|
assert.equal(store.session?.keyInMemory, false);
|
|
115
330
|
assert.equal(
|
|
116
331
|
store.session?.current.apiKey,
|
|
117
|
-
process.env.
|
|
332
|
+
process.env.SYSTEM_ONE_BASE_URL === "http://x/"
|
|
333
|
+
? process.env.SYSTEM_ONE_API_KEY || undefined
|
|
334
|
+
: undefined,
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
it("native status reports Pi-managed credentials, not unused HTTP environment key", async () => {
|
|
338
|
+
const { pi, cap } = harness();
|
|
339
|
+
const store: SessionStore = {
|
|
340
|
+
session: createSessionConfig(
|
|
341
|
+
{ SYSTEM_ONE_API_KEY: "test-env-key", SYSTEM_ONE_MODEL: "other-model" },
|
|
342
|
+
{ mode: "native" },
|
|
343
|
+
),
|
|
344
|
+
};
|
|
345
|
+
registerSystemOneCommands(pi, store);
|
|
346
|
+
await cap.handler?.("status", ctxFor([], cap));
|
|
347
|
+
assert.match(cap.notices[0], /api key: managed by Pi/);
|
|
348
|
+
assert.match(cap.notices[0], /model: jev-latest/);
|
|
349
|
+
assert.doesNotMatch(
|
|
350
|
+
cap.notices[0],
|
|
351
|
+
/from environment|test-env-key|other-model/,
|
|
118
352
|
);
|
|
119
353
|
});
|
|
120
354
|
it("status shows current config; STATUS routes case-insensitively", async () => {
|
|
@@ -126,10 +360,11 @@ describe("so command", () => {
|
|
|
126
360
|
await cap.handler?.("STATUS", ctxFor([], cap));
|
|
127
361
|
assert.match(cap.notices[0], /http:\/\/x\//);
|
|
128
362
|
});
|
|
129
|
-
it("status
|
|
363
|
+
it("status shows TypeSafe defaults and login guidance without auth", async () => {
|
|
130
364
|
const { pi, cap } = harness();
|
|
131
|
-
registerSystemOneCommands(pi, {});
|
|
365
|
+
registerSystemOneCommands(pi, { session: createSessionConfig({}) });
|
|
132
366
|
await cap.handler?.("status", ctxFor([], cap));
|
|
133
|
-
assert.match(cap.notices[0], /
|
|
367
|
+
assert.match(cap.notices[0], /typesafe/);
|
|
368
|
+
assert.match(cap.notices[0], /login typesafe/);
|
|
134
369
|
});
|
|
135
370
|
});
|
package/tests/config.test.ts
CHANGED
|
@@ -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",
|
package/tests/extension.test.ts
CHANGED
|
@@ -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 {
|
|
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
|
+
});
|