pi-system-one 1.2.4 → 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.
- package/README.md +43 -23
- package/package.json +4 -1
- package/src/commands.ts +152 -79
- 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 +207 -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,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(
|
|
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 () =>
|
|
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("
|
|
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,
|
|
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(["", "", "
|
|
280
|
+
await cap.handler?.("", ctxFor(["custom", "", "", "sk-mem"], cap));
|
|
110
281
|
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.
|
|
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.
|
|
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
|
|
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], /
|
|
322
|
+
assert.match(cap.notices[0], /typesafe/);
|
|
323
|
+
assert.match(cap.notices[0], /login typesafe/);
|
|
134
324
|
});
|
|
135
325
|
});
|
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
|
+
});
|