pi-system-one 1.3.1 → 1.3.2
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 +7 -5
- package/package.json +2 -1
- package/src/commands.ts +40 -7
- package/src/config.ts +71 -20
- package/src/debug-ui.ts +125 -0
- package/src/debug.ts +159 -0
- package/src/persistence.ts +2 -2
- package/src/provider.ts +149 -25
- package/tests/commands.test.ts +138 -4
- package/tests/config.test.ts +64 -8
- package/tests/debug.test.ts +345 -0
- package/tests/extension.test.ts +79 -11
- package/tests/persistence.test.ts +15 -0
- package/tests/provider.test.ts +209 -0
package/src/provider.ts
CHANGED
|
@@ -9,29 +9,36 @@ import {
|
|
|
9
9
|
validateResponse,
|
|
10
10
|
} from "system-one-core";
|
|
11
11
|
import {
|
|
12
|
+
isTypeSafeEndpoint,
|
|
12
13
|
type SessionConfig,
|
|
13
14
|
TYPESAFE_BASE_URL,
|
|
14
15
|
TYPESAFE_MODEL,
|
|
15
16
|
} from "./config.ts";
|
|
16
17
|
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
export type NativeFailureCategory =
|
|
19
|
+
| "native model unavailable"
|
|
20
|
+
| "native Score unsupported"
|
|
21
|
+
| "native input incompatible"
|
|
22
|
+
| "native classify threw"
|
|
23
|
+
| "native classifier returned error"
|
|
24
|
+
| "invalid native response"
|
|
25
|
+
| "native request aborted";
|
|
26
|
+
|
|
27
|
+
/** Fixed diagnostic category only; never include Pi/provider error text. */
|
|
28
|
+
export class PiNativeDiagnosticError extends Error {
|
|
29
|
+
readonly category: NativeFailureCategory;
|
|
30
|
+
constructor(category: NativeFailureCategory, message: string) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.category = category;
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
function nativeFailure(
|
|
34
|
-
|
|
36
|
+
function nativeFailure(
|
|
37
|
+
category: NativeFailureCategory,
|
|
38
|
+
aborted: boolean,
|
|
39
|
+
): PiNativeDiagnosticError {
|
|
40
|
+
return new PiNativeDiagnosticError(
|
|
41
|
+
aborted ? "native request aborted" : category,
|
|
35
42
|
aborted
|
|
36
43
|
? "Pi native classifier request aborted."
|
|
37
44
|
: "Pi native classifier failed. Check /login typesafe and Pi logs.",
|
|
@@ -42,9 +49,15 @@ class PiNativeProvider implements SystemOneProvider {
|
|
|
42
49
|
readonly id = "pi-native";
|
|
43
50
|
private readonly ctx: ExtensionToolContext;
|
|
44
51
|
private readonly timeoutMs: number;
|
|
45
|
-
|
|
52
|
+
private readonly onDispatch?: () => void;
|
|
53
|
+
constructor(
|
|
54
|
+
ctx: ExtensionToolContext,
|
|
55
|
+
timeoutMs: number,
|
|
56
|
+
onDispatch?: () => void,
|
|
57
|
+
) {
|
|
46
58
|
this.ctx = ctx;
|
|
47
59
|
this.timeoutMs = timeoutMs;
|
|
60
|
+
this.onDispatch = onDispatch;
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
async evaluate<Q extends QuestionMap>(
|
|
@@ -57,24 +70,28 @@ class PiNativeProvider implements SystemOneProvider {
|
|
|
57
70
|
"jev-latest",
|
|
58
71
|
);
|
|
59
72
|
if (!model)
|
|
60
|
-
throw new
|
|
73
|
+
throw new PiNativeDiagnosticError(
|
|
74
|
+
"native model unavailable",
|
|
61
75
|
"Pi classifier jev-latest unavailable. Use /so config to select TypeSafe HTTP instead.",
|
|
62
76
|
);
|
|
63
77
|
const questions: Record<string, unknown> = Object.create(null);
|
|
64
78
|
for (const [id, question] of Object.entries(request.questions)) {
|
|
65
79
|
if (question.type === "score")
|
|
66
|
-
throw new
|
|
80
|
+
throw new PiNativeDiagnosticError(
|
|
81
|
+
"native Score unsupported",
|
|
67
82
|
"Pi native classifier omits Score probabilities. Use /so config to select TypeSafe HTTP for Score questions.",
|
|
68
83
|
);
|
|
69
84
|
if (typeof question.instructions !== "string")
|
|
70
|
-
throw new
|
|
85
|
+
throw new PiNativeDiagnosticError(
|
|
86
|
+
"native input incompatible",
|
|
71
87
|
"Pi native classifier requires string instructions. Use TypeSafe HTTP for structured instructions.",
|
|
72
88
|
);
|
|
73
89
|
if (question.type === "choice") {
|
|
74
90
|
const criteria: Record<string, string> = Object.create(null);
|
|
75
91
|
for (const [key, meaning] of Object.entries(question.criteria)) {
|
|
76
92
|
if (meaning !== null && typeof meaning !== "string")
|
|
77
|
-
throw new
|
|
93
|
+
throw new PiNativeDiagnosticError(
|
|
94
|
+
"native input incompatible",
|
|
78
95
|
"Pi native classifier requires string Choice criteria. Use TypeSafe HTTP for structured criteria.",
|
|
79
96
|
);
|
|
80
97
|
criteria[key] = meaning ?? key;
|
|
@@ -90,7 +107,8 @@ class PiNativeProvider implements SystemOneProvider {
|
|
|
90
107
|
values &&
|
|
91
108
|
(typeof values.true !== "string" || typeof values.false !== "string")
|
|
92
109
|
)
|
|
93
|
-
throw new
|
|
110
|
+
throw new PiNativeDiagnosticError(
|
|
111
|
+
"native input incompatible",
|
|
94
112
|
"Pi native classifier requires string yes/no criteria. Use TypeSafe HTTP for structured criteria.",
|
|
95
113
|
);
|
|
96
114
|
questions[id] = {
|
|
@@ -113,6 +131,7 @@ class PiNativeProvider implements SystemOneProvider {
|
|
|
113
131
|
ReturnType<ExtensionToolContext["modelRegistry"]["classify"]>
|
|
114
132
|
>;
|
|
115
133
|
try {
|
|
134
|
+
this.onDispatch?.();
|
|
116
135
|
result = await this.ctx.modelRegistry.classify(
|
|
117
136
|
model,
|
|
118
137
|
{ state, questions } as never,
|
|
@@ -120,10 +139,14 @@ class PiNativeProvider implements SystemOneProvider {
|
|
|
120
139
|
);
|
|
121
140
|
} catch {
|
|
122
141
|
// Pi and its provider may include untrusted response bodies in errors.
|
|
123
|
-
throw nativeFailure(
|
|
142
|
+
throw nativeFailure(
|
|
143
|
+
"native classify threw",
|
|
144
|
+
options?.signal?.aborted === true,
|
|
145
|
+
);
|
|
124
146
|
}
|
|
125
147
|
if (result.stopReason !== "stop")
|
|
126
148
|
throw nativeFailure(
|
|
149
|
+
"native classifier returned error",
|
|
127
150
|
result.stopReason === "aborted" || options?.signal?.aborted === true,
|
|
128
151
|
);
|
|
129
152
|
try {
|
|
@@ -149,22 +172,123 @@ class PiNativeProvider implements SystemOneProvider {
|
|
|
149
172
|
);
|
|
150
173
|
} catch {
|
|
151
174
|
// Core validation can quote provider-controlled values; hide its detail.
|
|
152
|
-
throw nativeFailure(false);
|
|
175
|
+
throw nativeFailure("invalid native response", false);
|
|
153
176
|
}
|
|
154
177
|
}
|
|
155
178
|
}
|
|
156
179
|
|
|
180
|
+
function supportsNative(request: SystemOneRequest<QuestionMap>): boolean {
|
|
181
|
+
return Object.values(request.questions).every((question) => {
|
|
182
|
+
if (question.type === "score" || typeof question.instructions !== "string")
|
|
183
|
+
return false;
|
|
184
|
+
if (question.type === "choice")
|
|
185
|
+
return Object.values(question.criteria).every(
|
|
186
|
+
(value) => value === null || typeof value === "string",
|
|
187
|
+
);
|
|
188
|
+
return (
|
|
189
|
+
!question.criteria ||
|
|
190
|
+
(Object.keys(question.criteria).every(
|
|
191
|
+
(key) => key === "true" || key === "false",
|
|
192
|
+
) &&
|
|
193
|
+
typeof question.criteria.true === "string" &&
|
|
194
|
+
typeof question.criteria.false === "string")
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export type RouteObserver = (
|
|
200
|
+
provider: "pi-native" | "typesafe" | "configured",
|
|
201
|
+
reason:
|
|
202
|
+
| "compatible request"
|
|
203
|
+
| "contains Score"
|
|
204
|
+
| "native-incompatible input"
|
|
205
|
+
| "classifier unavailable"
|
|
206
|
+
| "selected mode",
|
|
207
|
+
) => void;
|
|
208
|
+
|
|
209
|
+
class AutoSystemOneProvider implements SystemOneProvider {
|
|
210
|
+
readonly id = "auto";
|
|
211
|
+
private readonly session: SessionConfig;
|
|
212
|
+
private readonly ctx: ExtensionToolContext;
|
|
213
|
+
private readonly onRoute?: RouteObserver;
|
|
214
|
+
private readonly onDispatch?: () => void;
|
|
215
|
+
constructor(
|
|
216
|
+
session: SessionConfig,
|
|
217
|
+
ctx: ExtensionToolContext,
|
|
218
|
+
onRoute?: RouteObserver,
|
|
219
|
+
onDispatch?: () => void,
|
|
220
|
+
) {
|
|
221
|
+
this.session = session;
|
|
222
|
+
this.ctx = ctx;
|
|
223
|
+
this.onRoute = onRoute;
|
|
224
|
+
this.onDispatch = onDispatch;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async evaluate<Q extends QuestionMap>(
|
|
228
|
+
request: SystemOneRequest<Q>,
|
|
229
|
+
options?: SystemOneCallOptions,
|
|
230
|
+
): Promise<SystemOneResponse<Q>> {
|
|
231
|
+
const compatible = supportsNative(request);
|
|
232
|
+
if (
|
|
233
|
+
compatible &&
|
|
234
|
+
this.ctx.modelRegistry?.findOfType?.(
|
|
235
|
+
"classifier",
|
|
236
|
+
"typesafe",
|
|
237
|
+
"jev-latest",
|
|
238
|
+
)
|
|
239
|
+
) {
|
|
240
|
+
this.onRoute?.("pi-native", "compatible request");
|
|
241
|
+
return new PiNativeProvider(
|
|
242
|
+
this.ctx,
|
|
243
|
+
this.session.current.timeoutMs,
|
|
244
|
+
this.onDispatch,
|
|
245
|
+
).evaluate(request, options);
|
|
246
|
+
}
|
|
247
|
+
this.onRoute?.(
|
|
248
|
+
"typesafe",
|
|
249
|
+
compatible
|
|
250
|
+
? "classifier unavailable"
|
|
251
|
+
: Object.values(request.questions).some(
|
|
252
|
+
(question) => question.type === "score",
|
|
253
|
+
)
|
|
254
|
+
? "contains Score"
|
|
255
|
+
: "native-incompatible input",
|
|
256
|
+
);
|
|
257
|
+
const http = await resolveSessionProvider(
|
|
258
|
+
{
|
|
259
|
+
...this.session,
|
|
260
|
+
mode: "typesafe",
|
|
261
|
+
current: {
|
|
262
|
+
...this.session.current,
|
|
263
|
+
baseUrl: TYPESAFE_BASE_URL,
|
|
264
|
+
model: TYPESAFE_MODEL,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
this.ctx,
|
|
268
|
+
);
|
|
269
|
+
this.onDispatch?.();
|
|
270
|
+
return http.evaluate(request, options);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
157
274
|
/** Resolve credentials at call time. Never forward Pi's TypeSafe key to a custom origin. */
|
|
158
275
|
export async function resolveSessionProvider(
|
|
159
276
|
session: SessionConfig,
|
|
160
277
|
ctx: ExtensionToolContext,
|
|
278
|
+
onRoute?: RouteObserver,
|
|
279
|
+
onDispatch?: () => void,
|
|
161
280
|
): Promise<SystemOneProvider> {
|
|
162
|
-
if (session.mode === "native")
|
|
163
|
-
|
|
281
|
+
if (session.mode === "native") {
|
|
282
|
+
onRoute?.("pi-native", "selected mode");
|
|
283
|
+
return new PiNativeProvider(ctx, session.current.timeoutMs, onDispatch);
|
|
284
|
+
}
|
|
285
|
+
if (session.mode === "auto")
|
|
286
|
+
return new AutoSystemOneProvider(session, ctx, onRoute, onDispatch);
|
|
164
287
|
const config = session.current;
|
|
165
288
|
if (!config.baseUrl)
|
|
166
289
|
throw new Error("Custom endpoint missing. Run /so config.");
|
|
167
290
|
const typeSafe = isTypeSafeEndpoint(config.baseUrl);
|
|
291
|
+
onRoute?.(typeSafe ? "typesafe" : "configured", "selected mode");
|
|
168
292
|
const key =
|
|
169
293
|
config.apiKey ??
|
|
170
294
|
(typeSafe
|
package/tests/commands.test.ts
CHANGED
|
@@ -11,9 +11,21 @@ import {
|
|
|
11
11
|
} from "../src/commands.ts";
|
|
12
12
|
import { createSessionConfig } from "../src/config.ts";
|
|
13
13
|
import { resolveSessionProvider } from "../src/provider.ts";
|
|
14
|
+
import type { buildSystemOneTool } from "../src/tool.ts";
|
|
15
|
+
|
|
16
|
+
type OverlayFactory = (
|
|
17
|
+
tui: { requestRender: () => void },
|
|
18
|
+
theme: { fg: (key: string, text: string) => string },
|
|
19
|
+
keys: unknown,
|
|
20
|
+
done: () => void,
|
|
21
|
+
) => {
|
|
22
|
+
render: (width: number) => string[];
|
|
23
|
+
handleInput: (data: string) => void;
|
|
24
|
+
};
|
|
14
25
|
|
|
15
26
|
interface Captured {
|
|
16
27
|
tools: string[];
|
|
28
|
+
tool?: ReturnType<typeof buildSystemOneTool>;
|
|
17
29
|
handler: ((args: string, ctx: never) => Promise<void>) | undefined;
|
|
18
30
|
completions: ((prefix: string) => { value: string }[]) | undefined;
|
|
19
31
|
notices: string[];
|
|
@@ -29,8 +41,9 @@ function harness() {
|
|
|
29
41
|
selections: [],
|
|
30
42
|
};
|
|
31
43
|
const pi = {
|
|
32
|
-
registerTool: (t:
|
|
44
|
+
registerTool: (t: ReturnType<typeof buildSystemOneTool>) => {
|
|
33
45
|
cap.tools.push(t.name);
|
|
46
|
+
cap.tool = t;
|
|
34
47
|
},
|
|
35
48
|
registerCommand: (
|
|
36
49
|
_n: string,
|
|
@@ -72,6 +85,93 @@ function ctxFor(
|
|
|
72
85
|
}
|
|
73
86
|
|
|
74
87
|
describe("so command", () => {
|
|
88
|
+
it("opens /so debug as a dismissible TUI overlay without changing settings", async () => {
|
|
89
|
+
const { pi, cap } = harness();
|
|
90
|
+
const store: SessionStore = { session: createSessionConfig({}) };
|
|
91
|
+
registerSystemOneCommands(pi, store);
|
|
92
|
+
let overlay = false;
|
|
93
|
+
let closed = false;
|
|
94
|
+
await cap.handler?.("debug", {
|
|
95
|
+
mode: "tui",
|
|
96
|
+
ui: {
|
|
97
|
+
custom: async (
|
|
98
|
+
factory: OverlayFactory,
|
|
99
|
+
options: { overlay: boolean },
|
|
100
|
+
) => {
|
|
101
|
+
overlay = options.overlay;
|
|
102
|
+
const component = factory(
|
|
103
|
+
{ requestRender: () => {} },
|
|
104
|
+
{ fg: (_color: string, text: string) => text },
|
|
105
|
+
{},
|
|
106
|
+
() => {
|
|
107
|
+
closed = true;
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
assert.match(
|
|
111
|
+
component.render(72).join("\n"),
|
|
112
|
+
/No system_one calls yet/,
|
|
113
|
+
);
|
|
114
|
+
component.handleInput("\u001b");
|
|
115
|
+
},
|
|
116
|
+
notify: () => {},
|
|
117
|
+
},
|
|
118
|
+
} as never);
|
|
119
|
+
assert.ok(overlay);
|
|
120
|
+
assert.ok(closed);
|
|
121
|
+
assert.equal(store.session?.mode, "typesafe");
|
|
122
|
+
assert.ok(cap.completions?.("de").some((item) => item.value === "debug"));
|
|
123
|
+
});
|
|
124
|
+
it("shows actual provider for a completed tool call in /so debug overlay", async () => {
|
|
125
|
+
const { pi, cap } = harness();
|
|
126
|
+
registerSystemOneCommands(pi, {
|
|
127
|
+
session: createSessionConfig({}, { mode: "native" }),
|
|
128
|
+
}).applyProvider();
|
|
129
|
+
assert.ok(cap.tool);
|
|
130
|
+
const result = await cap.tool.execute(
|
|
131
|
+
"native-debug",
|
|
132
|
+
{
|
|
133
|
+
state: "private-state",
|
|
134
|
+
questions: { q: { type: "noul", instructions: "private-instruction" } },
|
|
135
|
+
},
|
|
136
|
+
undefined,
|
|
137
|
+
undefined,
|
|
138
|
+
{
|
|
139
|
+
modelRegistry: {
|
|
140
|
+
findOfType: () => ({ id: "jev-latest" }),
|
|
141
|
+
classify: async () => ({
|
|
142
|
+
stopReason: "stop",
|
|
143
|
+
answers: { q: { type: "bool", probability: 0.7 } },
|
|
144
|
+
}),
|
|
145
|
+
},
|
|
146
|
+
} as never,
|
|
147
|
+
);
|
|
148
|
+
assert.equal(result.details.metadata.provider, "pi-native");
|
|
149
|
+
await cap.handler?.("debug", {
|
|
150
|
+
mode: "tui",
|
|
151
|
+
ui: {
|
|
152
|
+
custom: async (factory: OverlayFactory) => {
|
|
153
|
+
const component = factory(
|
|
154
|
+
{ requestRender: () => {} },
|
|
155
|
+
{ fg: (_key: string, text: string) => text },
|
|
156
|
+
{},
|
|
157
|
+
() => {},
|
|
158
|
+
);
|
|
159
|
+
const text = component.render(76).join("\n");
|
|
160
|
+
assert.match(text, /Provider: pi-native/);
|
|
161
|
+
assert.doesNotMatch(text, /private-state|private-instruction/);
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
} as never);
|
|
165
|
+
});
|
|
166
|
+
it("summarizes debug calls without custom UI in RPC mode", async () => {
|
|
167
|
+
const { pi, cap } = harness();
|
|
168
|
+
registerSystemOneCommands(pi, { session: createSessionConfig({}) });
|
|
169
|
+
await cap.handler?.("debug", {
|
|
170
|
+
mode: "rpc",
|
|
171
|
+
ui: { notify: (message: string) => cap.notices.push(message) },
|
|
172
|
+
} as never);
|
|
173
|
+
assert.match(cap.notices.at(-1) ?? "", /No system_one calls yet/);
|
|
174
|
+
});
|
|
75
175
|
it("shows selectable provider modes for /so and /so config", async () => {
|
|
76
176
|
const { pi, cap } = harness();
|
|
77
177
|
const store: SessionStore = { session: createSessionConfig({}) };
|
|
@@ -81,11 +181,45 @@ describe("so command", () => {
|
|
|
81
181
|
await cap.handler?.("config", ctxFor(["typesafe"], cap));
|
|
82
182
|
assert.equal(store.session?.mode, "typesafe");
|
|
83
183
|
assert.equal(cap.selections.length, 2);
|
|
84
|
-
assert.equal(cap.selections[0].options.length,
|
|
184
|
+
assert.equal(cap.selections[0].options.length, 4);
|
|
85
185
|
assert.match(cap.selections[0].options[0], /typesafe.*current/i);
|
|
86
186
|
assert.match(cap.selections[1].options[0], /native.*current/i);
|
|
87
187
|
});
|
|
88
188
|
|
|
189
|
+
it("selects auto and persists policy without an inherited custom endpoint", async () => {
|
|
190
|
+
const dir = mkdtempSync(join(tmpdir(), "so-auto-command-"));
|
|
191
|
+
const previousUrl = process.env.SYSTEM_ONE_BASE_URL;
|
|
192
|
+
const previousKey = process.env.SYSTEM_ONE_API_KEY;
|
|
193
|
+
try {
|
|
194
|
+
process.env.SYSTEM_ONE_BASE_URL = "https://custom.example";
|
|
195
|
+
process.env.SYSTEM_ONE_API_KEY = "custom-secret";
|
|
196
|
+
const path = join(dir, "pi-system-one.json");
|
|
197
|
+
const { pi, cap } = harness();
|
|
198
|
+
const store: SessionStore = {
|
|
199
|
+
session: createSessionConfig({
|
|
200
|
+
SYSTEM_ONE_BASE_URL: "https://custom.example",
|
|
201
|
+
SYSTEM_ONE_API_KEY: "custom-secret",
|
|
202
|
+
}),
|
|
203
|
+
};
|
|
204
|
+
registerSystemOneCommands(pi, store, path);
|
|
205
|
+
await cap.handler?.("config", ctxFor(["auto"], cap));
|
|
206
|
+
assert.equal(store.session?.mode, "auto");
|
|
207
|
+
assert.equal(store.session?.current.baseUrl, "https://api.typesafe.ai");
|
|
208
|
+
assert.equal(store.session?.current.apiKey, undefined);
|
|
209
|
+
assert.equal(JSON.parse(readFileSync(path, "utf8")).mode, "auto");
|
|
210
|
+
assert.doesNotMatch(readFileSync(path, "utf8"), /custom.example|secret/);
|
|
211
|
+
assert.ok(
|
|
212
|
+
cap.selections[0].options.some((option) => option.startsWith("auto")),
|
|
213
|
+
);
|
|
214
|
+
} finally {
|
|
215
|
+
rmSync(dir, { recursive: true, force: true });
|
|
216
|
+
if (previousUrl === undefined) delete process.env.SYSTEM_ONE_BASE_URL;
|
|
217
|
+
else process.env.SYSTEM_ONE_BASE_URL = previousUrl;
|
|
218
|
+
if (previousKey === undefined) delete process.env.SYSTEM_ONE_API_KEY;
|
|
219
|
+
else process.env.SYSTEM_ONE_API_KEY = previousKey;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
89
223
|
it("cancels selection without changing mode or saved settings", async () => {
|
|
90
224
|
const dir = mkdtempSync(join(tmpdir(), "so-select-"));
|
|
91
225
|
try {
|
|
@@ -268,12 +402,12 @@ describe("so command", () => {
|
|
|
268
402
|
rmSync(dir, { recursive: true, force: true });
|
|
269
403
|
}
|
|
270
404
|
});
|
|
271
|
-
it("completes config + status, case-insensitively", () => {
|
|
405
|
+
it("completes config + status + debug, case-insensitively", () => {
|
|
272
406
|
const { pi, cap } = harness();
|
|
273
407
|
registerSystemOneCommands(pi, {});
|
|
274
408
|
assert.deepEqual(
|
|
275
409
|
cap.completions?.("").map((c) => c.value),
|
|
276
|
-
["config", "status"],
|
|
410
|
+
["config", "status", "debug"],
|
|
277
411
|
);
|
|
278
412
|
assert.deepEqual(
|
|
279
413
|
cap.completions?.("S").map((c) => c.value),
|
package/tests/config.test.ts
CHANGED
|
@@ -22,6 +22,28 @@ describe("config", () => {
|
|
|
22
22
|
assert.equal(s.current.model, "jev-latest");
|
|
23
23
|
assert.match(describeConfig(s), /api key: managed by Pi/);
|
|
24
24
|
});
|
|
25
|
+
it("status identifies saved native mode and ignored endpoint without exposing keys", () => {
|
|
26
|
+
const s = createSessionConfig(
|
|
27
|
+
{
|
|
28
|
+
SYSTEM_ONE_BASE_URL: "https://custom.example",
|
|
29
|
+
SYSTEM_ONE_API_KEY: "secret",
|
|
30
|
+
},
|
|
31
|
+
{ mode: "native" },
|
|
32
|
+
);
|
|
33
|
+
const status = describeConfig(s);
|
|
34
|
+
assert.match(status, /mode: native \(saved\)/);
|
|
35
|
+
assert.match(status, /environment endpoint ignored/i);
|
|
36
|
+
assert.doesNotMatch(status, /custom.example|secret/);
|
|
37
|
+
});
|
|
38
|
+
it("status explains auto policy rather than claiming a fixed provider", () => {
|
|
39
|
+
const s = createSessionConfig({}, { mode: "auto" });
|
|
40
|
+
assert.match(describeConfig(s), /mode: auto \(saved\)/);
|
|
41
|
+
assert.match(describeConfig(s), /per request/i);
|
|
42
|
+
assert.match(
|
|
43
|
+
describeConfig(s),
|
|
44
|
+
/HTTP fallback endpoint: https:\/\/api\.typesafe\.ai/,
|
|
45
|
+
);
|
|
46
|
+
});
|
|
25
47
|
it("defaults to TypeSafe Jev without environment configuration", () => {
|
|
26
48
|
const s = createSessionConfig({});
|
|
27
49
|
assert.equal(s.mode, "typesafe");
|
|
@@ -29,23 +51,57 @@ describe("config", () => {
|
|
|
29
51
|
assert.equal(s.current.model, "jev-latest");
|
|
30
52
|
assert.equal(s.current.apiKey, undefined);
|
|
31
53
|
});
|
|
32
|
-
it("keeps environment
|
|
54
|
+
it("keeps a TypeSafe environment model bound to the canonical endpoint", () => {
|
|
55
|
+
for (const stored of [{}, { mode: "typesafe" as const }]) {
|
|
56
|
+
const s = createSessionConfig(
|
|
57
|
+
{ SYSTEM_ONE_MODEL: "jev-custom", SYSTEM_ONE_API_KEY: "typesafe-key" },
|
|
58
|
+
stored,
|
|
59
|
+
);
|
|
60
|
+
assert.equal(s.mode, "typesafe");
|
|
61
|
+
assert.equal(s.current.model, "jev-custom");
|
|
62
|
+
assert.equal(s.current.apiKey, "typesafe-key");
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
it("keeps saved native mode despite inherited HTTP environment", () => {
|
|
33
66
|
const s = createSessionConfig(
|
|
34
67
|
{ SYSTEM_ONE_BASE_URL: "http://existing/", SYSTEM_ONE_MODEL: "old" },
|
|
35
68
|
{ mode: "native", model: "new", timeoutMs: 5000 },
|
|
36
69
|
);
|
|
37
|
-
assert.equal(s.mode, "
|
|
38
|
-
assert.equal(s.current.baseUrl, "
|
|
39
|
-
assert.equal(s.current.model, "
|
|
70
|
+
assert.equal(s.mode, "native");
|
|
71
|
+
assert.equal(s.current.baseUrl, "");
|
|
72
|
+
assert.equal(s.current.model, "jev-latest");
|
|
40
73
|
assert.equal(s.current.timeoutMs, 5000);
|
|
41
74
|
});
|
|
42
|
-
it("
|
|
75
|
+
it("saved TypeSafe and auto ignore custom endpoint keys and models", () => {
|
|
76
|
+
for (const mode of ["typesafe", "auto"] as const) {
|
|
77
|
+
const s = createSessionConfig(
|
|
78
|
+
{
|
|
79
|
+
SYSTEM_ONE_BASE_URL: "http://custom/",
|
|
80
|
+
SYSTEM_ONE_API_KEY: "custom-secret",
|
|
81
|
+
SYSTEM_ONE_MODEL: "custom-model",
|
|
82
|
+
},
|
|
83
|
+
{ mode },
|
|
84
|
+
);
|
|
85
|
+
assert.equal(s.mode, mode);
|
|
86
|
+
assert.equal(s.current.baseUrl, "https://api.typesafe.ai");
|
|
87
|
+
assert.equal(s.current.model, "jev-latest");
|
|
88
|
+
assert.equal(s.current.apiKey, undefined);
|
|
89
|
+
assert.equal(s.ignoredEnvironmentEndpoint, true);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
it("keeps saved custom endpoint and model, not unrelated environment credentials", () => {
|
|
43
93
|
const s = createSessionConfig(
|
|
44
|
-
{
|
|
94
|
+
{
|
|
95
|
+
SYSTEM_ONE_BASE_URL: "http://new/",
|
|
96
|
+
SYSTEM_ONE_MODEL: "new-model",
|
|
97
|
+
SYSTEM_ONE_API_KEY: "key-for-new",
|
|
98
|
+
},
|
|
45
99
|
{ mode: "custom", baseUrl: "http://old/", model: "old-model" },
|
|
46
100
|
);
|
|
47
|
-
assert.equal(s.
|
|
48
|
-
assert.equal(s.current.baseUrl, "http://
|
|
101
|
+
assert.equal(s.mode, "custom");
|
|
102
|
+
assert.equal(s.current.baseUrl, "http://old/");
|
|
103
|
+
assert.equal(s.current.model, "old-model");
|
|
104
|
+
assert.equal(s.current.apiKey, undefined);
|
|
49
105
|
});
|
|
50
106
|
it("keeps saved model when environment endpoint matches saved endpoint", () => {
|
|
51
107
|
const s = createSessionConfig(
|