gsd-pi 2.45.0 → 2.46.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/dist/help-text.js +1 -1
- package/dist/loader.js +34 -0
- package/dist/resources/extensions/gsd/auto/phases.js +27 -42
- package/dist/resources/extensions/gsd/auto/run-unit.js +6 -3
- package/dist/resources/extensions/gsd/auto/session.js +0 -11
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +112 -0
- package/dist/resources/extensions/gsd/auto-post-unit.js +25 -96
- package/dist/resources/extensions/gsd/auto-start.js +2 -3
- package/dist/resources/extensions/gsd/auto-worktree.js +5 -4
- package/dist/resources/extensions/gsd/auto.js +12 -57
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +15 -12
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +18 -0
- package/dist/resources/extensions/gsd/commands/context.js +0 -4
- package/dist/resources/extensions/gsd/commands/handlers/parallel.js +1 -1
- package/dist/resources/extensions/gsd/crash-recovery.js +2 -4
- package/dist/resources/extensions/gsd/dashboard-overlay.js +0 -44
- package/dist/resources/extensions/gsd/db-writer.js +9 -9
- package/dist/resources/extensions/gsd/doctor-checks.js +167 -2
- package/dist/resources/extensions/gsd/doctor.js +5 -3
- package/dist/resources/extensions/gsd/gsd-db.js +16 -3
- package/dist/resources/extensions/gsd/guided-flow.js +1 -2
- package/dist/resources/extensions/gsd/parallel-merge.js +1 -1
- package/dist/resources/extensions/gsd/parallel-orchestrator.js +5 -18
- package/dist/resources/extensions/gsd/preferences-types.js +2 -2
- package/dist/resources/extensions/gsd/preferences.js +8 -4
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +21 -8
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +10 -23
- package/dist/resources/extensions/gsd/prompts/discuss.md +2 -2
- package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -15
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +4 -2
- package/dist/resources/extensions/gsd/prompts/queue.md +2 -2
- package/dist/resources/extensions/gsd/prompts/quick-task.md +2 -0
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -3
- package/dist/resources/extensions/gsd/prompts/rethink.md +7 -2
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/session-lock.js +1 -3
- package/dist/resources/extensions/gsd/state.js +7 -0
- package/dist/resources/extensions/gsd/sync-lock.js +89 -0
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +61 -11
- package/dist/resources/extensions/gsd/tools/complete-slice.js +56 -11
- package/dist/resources/extensions/gsd/tools/complete-task.js +50 -2
- package/dist/resources/extensions/gsd/tools/plan-milestone.js +37 -1
- package/dist/resources/extensions/gsd/tools/plan-slice.js +30 -1
- package/dist/resources/extensions/gsd/tools/plan-task.js +27 -1
- package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +32 -2
- package/dist/resources/extensions/gsd/tools/reopen-slice.js +86 -0
- package/dist/resources/extensions/gsd/tools/reopen-task.js +90 -0
- package/dist/resources/extensions/gsd/tools/replan-slice.js +32 -2
- package/dist/resources/extensions/gsd/unit-ownership.js +85 -0
- package/dist/resources/extensions/gsd/workflow-events.js +102 -0
- package/dist/resources/extensions/gsd/workflow-logger.js +193 -0
- package/dist/resources/extensions/gsd/workflow-manifest.js +244 -0
- package/dist/resources/extensions/gsd/workflow-migration.js +280 -0
- package/dist/resources/extensions/gsd/workflow-projections.js +373 -0
- package/dist/resources/extensions/gsd/workflow-reconcile.js +411 -0
- package/dist/resources/extensions/gsd/worktree-manager.js +4 -3
- package/dist/resources/extensions/gsd/worktree-resolver.js +37 -0
- package/dist/resources/extensions/gsd/write-intercept.js +84 -0
- package/dist/resources/extensions/voice/index.js +11 -16
- package/dist/resources/extensions/voice/linux-ready.js +67 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +2 -1
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.js +2 -0
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lifecycle-hooks.d.ts +4 -0
- package/packages/pi-coding-agent/dist/core/lifecycle-hooks.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lifecycle-hooks.js +10 -5
- package/packages/pi-coding-agent/dist/core/lifecycle-hooks.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lifecycle-hooks.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/lifecycle-hooks.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lifecycle-hooks.test.js +185 -0
- package/packages/pi-coding-agent/dist/core/lifecycle-hooks.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js +239 -10
- package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +2 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +20 -2
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-commands.test.js +206 -195
- package/packages/pi-coding-agent/dist/core/package-commands.test.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/compaction-orchestrator.ts +2 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -1
- package/packages/pi-coding-agent/src/core/lifecycle-hooks.test.ts +227 -0
- package/packages/pi-coding-agent/src/core/lifecycle-hooks.ts +11 -5
- package/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts +297 -11
- package/packages/pi-coding-agent/src/core/model-registry.ts +30 -3
- package/packages/pi-coding-agent/src/core/package-commands.test.ts +227 -205
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/loop-deps.ts +0 -19
- package/src/resources/extensions/gsd/auto/phases.ts +24 -44
- package/src/resources/extensions/gsd/auto/run-unit.ts +6 -3
- package/src/resources/extensions/gsd/auto/session.ts +0 -18
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +131 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +0 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +25 -106
- package/src/resources/extensions/gsd/auto-start.ts +1 -3
- package/src/resources/extensions/gsd/auto-worktree.ts +8 -5
- package/src/resources/extensions/gsd/auto.ts +7 -83
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +15 -12
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
- package/src/resources/extensions/gsd/commands/context.ts +0 -5
- package/src/resources/extensions/gsd/commands/handlers/parallel.ts +1 -1
- package/src/resources/extensions/gsd/crash-recovery.ts +1 -5
- package/src/resources/extensions/gsd/dashboard-overlay.ts +0 -50
- package/src/resources/extensions/gsd/db-writer.ts +9 -17
- package/src/resources/extensions/gsd/doctor-checks.ts +180 -2
- package/src/resources/extensions/gsd/doctor-types.ts +7 -1
- package/src/resources/extensions/gsd/doctor.ts +6 -3
- package/src/resources/extensions/gsd/gsd-db.ts +16 -3
- package/src/resources/extensions/gsd/guided-flow.ts +1 -2
- package/src/resources/extensions/gsd/journal.ts +6 -1
- package/src/resources/extensions/gsd/parallel-merge.ts +1 -1
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +5 -21
- package/src/resources/extensions/gsd/preferences-types.ts +2 -2
- package/src/resources/extensions/gsd/preferences.ts +7 -3
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +21 -8
- package/src/resources/extensions/gsd/prompts/complete-slice.md +10 -23
- package/src/resources/extensions/gsd/prompts/discuss.md +2 -2
- package/src/resources/extensions/gsd/prompts/execute-task.md +5 -15
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +4 -2
- package/src/resources/extensions/gsd/prompts/queue.md +2 -2
- package/src/resources/extensions/gsd/prompts/quick-task.md +2 -0
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +3 -3
- package/src/resources/extensions/gsd/prompts/rethink.md +7 -2
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/session-lock.ts +0 -4
- package/src/resources/extensions/gsd/state.ts +8 -0
- package/src/resources/extensions/gsd/sync-lock.ts +94 -0
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +5 -13
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +6 -10
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +96 -0
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +264 -228
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +317 -250
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +2 -8
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/integration-proof.test.ts +15 -24
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/milestone-transition-state-rebuild.test.ts +8 -9
- package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +42 -3
- package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +0 -7
- package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +7 -8
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +20 -24
- package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +0 -2
- package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +9 -6
- package/src/resources/extensions/gsd/tests/post-mutation-hook.test.ts +171 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +7 -9
- package/src/resources/extensions/gsd/tests/projection-regression.test.ts +174 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +15 -14
- package/src/resources/extensions/gsd/tests/reopen-slice.test.ts +155 -0
- package/src/resources/extensions/gsd/tests/reopen-task.test.ts +165 -0
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +1 -4
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +2 -3
- package/src/resources/extensions/gsd/tests/sync-lock.test.ts +122 -0
- package/src/resources/extensions/gsd/tests/unit-ownership.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/workflow-events.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +275 -0
- package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +171 -0
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +220 -0
- package/src/resources/extensions/gsd/tests/write-intercept.test.ts +76 -0
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +74 -11
- package/src/resources/extensions/gsd/tools/complete-slice.ts +68 -11
- package/src/resources/extensions/gsd/tools/complete-task.ts +63 -1
- package/src/resources/extensions/gsd/tools/plan-milestone.ts +45 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +38 -0
- package/src/resources/extensions/gsd/tools/plan-task.ts +35 -1
- package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +39 -1
- package/src/resources/extensions/gsd/tools/reopen-slice.ts +125 -0
- package/src/resources/extensions/gsd/tools/reopen-task.ts +129 -0
- package/src/resources/extensions/gsd/tools/replan-slice.ts +38 -1
- package/src/resources/extensions/gsd/types.ts +8 -0
- package/src/resources/extensions/gsd/unit-ownership.ts +104 -0
- package/src/resources/extensions/gsd/workflow-events.ts +154 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +243 -0
- package/src/resources/extensions/gsd/workflow-manifest.ts +334 -0
- package/src/resources/extensions/gsd/workflow-migration.ts +345 -0
- package/src/resources/extensions/gsd/workflow-projections.ts +425 -0
- package/src/resources/extensions/gsd/workflow-reconcile.ts +503 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +4 -9
- package/src/resources/extensions/gsd/worktree-resolver.ts +37 -0
- package/src/resources/extensions/gsd/write-intercept.ts +90 -0
- package/src/resources/extensions/voice/index.ts +11 -21
- package/src/resources/extensions/voice/linux-ready.ts +87 -0
- package/src/resources/extensions/voice/tests/linux-ready.test.ts +124 -0
- /package/dist/web/standalone/.next/static/{wUzEX1U3CmFcMry2SUDJn → 8zT99piZz8u3xAU3Omz2g}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{wUzEX1U3CmFcMry2SUDJn → 8zT99piZz8u3xAU3Omz2g}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import { readManifestRuntimeDeps, collectRuntimeDependencies, verifyRuntimeDependencies, resolveLocalSourcePath, } from "./lifecycle-hooks.js";
|
|
7
|
+
function tmpDir(prefix, t) {
|
|
8
|
+
const dir = mkdtempSync(join(tmpdir(), `pi-lh-${prefix}-`));
|
|
9
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
// ─── readManifestRuntimeDeps ──────────────────────────────────────────────────
|
|
13
|
+
describe("readManifestRuntimeDeps", () => {
|
|
14
|
+
it("returns empty array when manifest file is missing", (t) => {
|
|
15
|
+
const dir = tmpDir("no-manifest", t);
|
|
16
|
+
assert.deepEqual(readManifestRuntimeDeps(dir), []);
|
|
17
|
+
});
|
|
18
|
+
it("returns empty array for malformed JSON", (t) => {
|
|
19
|
+
const dir = tmpDir("bad-json", t);
|
|
20
|
+
writeFileSync(join(dir, "extension-manifest.json"), "not json{{{", "utf-8");
|
|
21
|
+
assert.deepEqual(readManifestRuntimeDeps(dir), []);
|
|
22
|
+
});
|
|
23
|
+
it("returns runtime deps from valid manifest", (t) => {
|
|
24
|
+
const dir = tmpDir("valid", t);
|
|
25
|
+
writeFileSync(join(dir, "extension-manifest.json"), JSON.stringify({
|
|
26
|
+
dependencies: { runtime: ["claude", "node"] },
|
|
27
|
+
}), "utf-8");
|
|
28
|
+
assert.deepEqual(readManifestRuntimeDeps(dir), ["claude", "node"]);
|
|
29
|
+
});
|
|
30
|
+
it("returns empty array when dependencies exists but runtime is missing", (t) => {
|
|
31
|
+
const dir = tmpDir("no-runtime", t);
|
|
32
|
+
writeFileSync(join(dir, "extension-manifest.json"), JSON.stringify({
|
|
33
|
+
dependencies: {},
|
|
34
|
+
}), "utf-8");
|
|
35
|
+
assert.deepEqual(readManifestRuntimeDeps(dir), []);
|
|
36
|
+
});
|
|
37
|
+
it("returns empty array when runtime is empty", (t) => {
|
|
38
|
+
const dir = tmpDir("empty-runtime", t);
|
|
39
|
+
writeFileSync(join(dir, "extension-manifest.json"), JSON.stringify({
|
|
40
|
+
dependencies: { runtime: [] },
|
|
41
|
+
}), "utf-8");
|
|
42
|
+
assert.deepEqual(readManifestRuntimeDeps(dir), []);
|
|
43
|
+
});
|
|
44
|
+
it("filters out non-string entries in runtime array", (t) => {
|
|
45
|
+
const dir = tmpDir("mixed-types", t);
|
|
46
|
+
writeFileSync(join(dir, "extension-manifest.json"), JSON.stringify({
|
|
47
|
+
dependencies: { runtime: [123, null, "node", false, "python"] },
|
|
48
|
+
}), "utf-8");
|
|
49
|
+
assert.deepEqual(readManifestRuntimeDeps(dir), ["node", "python"]);
|
|
50
|
+
});
|
|
51
|
+
it("returns empty array when no dependencies field at all", (t) => {
|
|
52
|
+
const dir = tmpDir("no-deps-field", t);
|
|
53
|
+
writeFileSync(join(dir, "extension-manifest.json"), JSON.stringify({
|
|
54
|
+
id: "test",
|
|
55
|
+
name: "Test",
|
|
56
|
+
}), "utf-8");
|
|
57
|
+
assert.deepEqual(readManifestRuntimeDeps(dir), []);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
// ─── collectRuntimeDependencies ───────────────────────────────────────────────
|
|
61
|
+
describe("collectRuntimeDependencies", () => {
|
|
62
|
+
it("aggregates deps from installedPath manifest", (t) => {
|
|
63
|
+
const dir = tmpDir("collect-installed", t);
|
|
64
|
+
writeFileSync(join(dir, "extension-manifest.json"), JSON.stringify({
|
|
65
|
+
dependencies: { runtime: ["claude"] },
|
|
66
|
+
}), "utf-8");
|
|
67
|
+
assert.deepEqual(collectRuntimeDependencies(dir, []), ["claude"]);
|
|
68
|
+
});
|
|
69
|
+
it("aggregates deps from entry path directory manifests", (t) => {
|
|
70
|
+
const root = tmpDir("collect-entry", t);
|
|
71
|
+
const installedDir = join(root, "installed");
|
|
72
|
+
const entryDir = join(root, "entry");
|
|
73
|
+
mkdirSync(installedDir, { recursive: true });
|
|
74
|
+
mkdirSync(entryDir, { recursive: true });
|
|
75
|
+
writeFileSync(join(entryDir, "extension-manifest.json"), JSON.stringify({
|
|
76
|
+
dependencies: { runtime: ["python"] },
|
|
77
|
+
}), "utf-8");
|
|
78
|
+
const deps = collectRuntimeDependencies(installedDir, [join(entryDir, "index.ts")]);
|
|
79
|
+
assert.deepEqual(deps, ["python"]);
|
|
80
|
+
});
|
|
81
|
+
it("deduplicates across multiple directories", (t) => {
|
|
82
|
+
const root = tmpDir("collect-dedup", t);
|
|
83
|
+
const dir1 = join(root, "dir1");
|
|
84
|
+
const dir2 = join(root, "dir2");
|
|
85
|
+
mkdirSync(dir1, { recursive: true });
|
|
86
|
+
mkdirSync(dir2, { recursive: true });
|
|
87
|
+
writeFileSync(join(dir1, "extension-manifest.json"), JSON.stringify({
|
|
88
|
+
dependencies: { runtime: ["node", "python"] },
|
|
89
|
+
}), "utf-8");
|
|
90
|
+
writeFileSync(join(dir2, "extension-manifest.json"), JSON.stringify({
|
|
91
|
+
dependencies: { runtime: ["python", "claude"] },
|
|
92
|
+
}), "utf-8");
|
|
93
|
+
const deps = collectRuntimeDependencies(dir1, [join(dir2, "index.ts")]);
|
|
94
|
+
assert.equal(deps.length, 3);
|
|
95
|
+
assert.ok(deps.includes("node"));
|
|
96
|
+
assert.ok(deps.includes("python"));
|
|
97
|
+
assert.ok(deps.includes("claude"));
|
|
98
|
+
});
|
|
99
|
+
it("returns empty when no directories have manifests", (t) => {
|
|
100
|
+
const dir = tmpDir("collect-empty", t);
|
|
101
|
+
assert.deepEqual(collectRuntimeDependencies(dir, []), []);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
// ─── verifyRuntimeDependencies ────────────────────────────────────────────────
|
|
105
|
+
describe("verifyRuntimeDependencies", () => {
|
|
106
|
+
it("does not throw for empty deps array", () => {
|
|
107
|
+
assert.doesNotThrow(() => verifyRuntimeDependencies([], "test-source", "pi"));
|
|
108
|
+
});
|
|
109
|
+
it("does not throw when all deps are present", () => {
|
|
110
|
+
assert.doesNotThrow(() => verifyRuntimeDependencies(["node"], "test-source", "pi"));
|
|
111
|
+
});
|
|
112
|
+
it("throws for missing dep with 'Missing runtime dependencies' message", () => {
|
|
113
|
+
assert.throws(() => verifyRuntimeDependencies(["__nonexistent_dep_for_test__"], "test-source", "pi"), (err) => {
|
|
114
|
+
assert.ok(err.message.includes("Missing runtime dependencies"));
|
|
115
|
+
assert.ok(err.message.includes("__nonexistent_dep_for_test__"));
|
|
116
|
+
return true;
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
it("lists all missing deps in error message", () => {
|
|
120
|
+
assert.throws(() => verifyRuntimeDependencies(["__missing_1__", "__missing_2__"], "test-source", "pi"), (err) => {
|
|
121
|
+
assert.ok(err.message.includes("__missing_1__"));
|
|
122
|
+
assert.ok(err.message.includes("__missing_2__"));
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
it("includes appName and source in error for retry hint", () => {
|
|
127
|
+
assert.throws(() => verifyRuntimeDependencies(["__missing__"], "github:user/repo", "gsd"), (err) => {
|
|
128
|
+
assert.ok(err.message.includes("gsd"));
|
|
129
|
+
assert.ok(err.message.includes("github:user/repo"));
|
|
130
|
+
return true;
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
// ─── resolveLocalSourcePath ───────────────────────────────────────────────────
|
|
135
|
+
describe("resolveLocalSourcePath", () => {
|
|
136
|
+
it("returns undefined for empty string", () => {
|
|
137
|
+
assert.equal(resolveLocalSourcePath("", "/tmp"), undefined);
|
|
138
|
+
});
|
|
139
|
+
it("returns undefined for npm: source", () => {
|
|
140
|
+
assert.equal(resolveLocalSourcePath("npm:@foo/bar", "/tmp"), undefined);
|
|
141
|
+
});
|
|
142
|
+
it("returns undefined for git URL", () => {
|
|
143
|
+
assert.equal(resolveLocalSourcePath("git:github.com/user/repo", "/tmp"), undefined);
|
|
144
|
+
});
|
|
145
|
+
it("returns undefined for https git URL", () => {
|
|
146
|
+
assert.equal(resolveLocalSourcePath("https://github.com/user/repo", "/tmp"), undefined);
|
|
147
|
+
});
|
|
148
|
+
it("resolves ~ to homedir", () => {
|
|
149
|
+
const result = resolveLocalSourcePath("~", "/tmp");
|
|
150
|
+
if (existsSync(homedir())) {
|
|
151
|
+
assert.equal(result, homedir());
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
assert.equal(result, undefined);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
it("resolves ~/path relative to homedir", () => {
|
|
158
|
+
const result = resolveLocalSourcePath("~/", "/tmp");
|
|
159
|
+
if (existsSync(homedir())) {
|
|
160
|
+
assert.equal(result, homedir());
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
assert.equal(result, undefined);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
it("resolves relative path that exists", (t) => {
|
|
167
|
+
const dir = tmpDir("resolve-rel", t);
|
|
168
|
+
const sub = join(dir, "myext");
|
|
169
|
+
mkdirSync(sub, { recursive: true });
|
|
170
|
+
const result = resolveLocalSourcePath("myext", dir);
|
|
171
|
+
assert.equal(result, resolve(dir, "myext"));
|
|
172
|
+
});
|
|
173
|
+
it("returns undefined for relative path that does not exist", (t) => {
|
|
174
|
+
const dir = tmpDir("resolve-noexist", t);
|
|
175
|
+
assert.equal(resolveLocalSourcePath("nonexistent", dir), undefined);
|
|
176
|
+
});
|
|
177
|
+
it("resolves absolute path that exists", (t) => {
|
|
178
|
+
const dir = tmpDir("resolve-abs", t);
|
|
179
|
+
assert.equal(resolveLocalSourcePath(dir, "/irrelevant"), dir);
|
|
180
|
+
});
|
|
181
|
+
it("returns undefined for absolute path that does not exist", () => {
|
|
182
|
+
assert.equal(resolveLocalSourcePath("/tmp/__nonexistent_path_for_test__", "/tmp"), undefined);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
//# sourceMappingURL=lifecycle-hooks.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle-hooks.test.js","sourceRoot":"","sources":["../../src/core/lifecycle-hooks.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\";\nimport { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from \"node:fs\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\nimport { describe, it } from \"node:test\";\nimport {\n\treadManifestRuntimeDeps,\n\tcollectRuntimeDependencies,\n\tverifyRuntimeDependencies,\n\tresolveLocalSourcePath,\n} from \"./lifecycle-hooks.js\";\n\nfunction tmpDir(prefix: string, t: { after: (fn: () => void) => void }): string {\n\tconst dir = mkdtempSync(join(tmpdir(), `pi-lh-${prefix}-`));\n\tt.after(() => rmSync(dir, { recursive: true, force: true }));\n\treturn dir;\n}\n\n// ─── readManifestRuntimeDeps ──────────────────────────────────────────────────\n\ndescribe(\"readManifestRuntimeDeps\", () => {\n\tit(\"returns empty array when manifest file is missing\", (t) => {\n\t\tconst dir = tmpDir(\"no-manifest\", t);\n\t\tassert.deepEqual(readManifestRuntimeDeps(dir), []);\n\t});\n\n\tit(\"returns empty array for malformed JSON\", (t) => {\n\t\tconst dir = tmpDir(\"bad-json\", t);\n\t\twriteFileSync(join(dir, \"extension-manifest.json\"), \"not json{{{\", \"utf-8\");\n\t\tassert.deepEqual(readManifestRuntimeDeps(dir), []);\n\t});\n\n\tit(\"returns runtime deps from valid manifest\", (t) => {\n\t\tconst dir = tmpDir(\"valid\", t);\n\t\twriteFileSync(join(dir, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tdependencies: { runtime: [\"claude\", \"node\"] },\n\t\t}), \"utf-8\");\n\t\tassert.deepEqual(readManifestRuntimeDeps(dir), [\"claude\", \"node\"]);\n\t});\n\n\tit(\"returns empty array when dependencies exists but runtime is missing\", (t) => {\n\t\tconst dir = tmpDir(\"no-runtime\", t);\n\t\twriteFileSync(join(dir, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tdependencies: {},\n\t\t}), \"utf-8\");\n\t\tassert.deepEqual(readManifestRuntimeDeps(dir), []);\n\t});\n\n\tit(\"returns empty array when runtime is empty\", (t) => {\n\t\tconst dir = tmpDir(\"empty-runtime\", t);\n\t\twriteFileSync(join(dir, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tdependencies: { runtime: [] },\n\t\t}), \"utf-8\");\n\t\tassert.deepEqual(readManifestRuntimeDeps(dir), []);\n\t});\n\n\tit(\"filters out non-string entries in runtime array\", (t) => {\n\t\tconst dir = tmpDir(\"mixed-types\", t);\n\t\twriteFileSync(join(dir, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tdependencies: { runtime: [123, null, \"node\", false, \"python\"] },\n\t\t}), \"utf-8\");\n\t\tassert.deepEqual(readManifestRuntimeDeps(dir), [\"node\", \"python\"]);\n\t});\n\n\tit(\"returns empty array when no dependencies field at all\", (t) => {\n\t\tconst dir = tmpDir(\"no-deps-field\", t);\n\t\twriteFileSync(join(dir, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tid: \"test\",\n\t\t\tname: \"Test\",\n\t\t}), \"utf-8\");\n\t\tassert.deepEqual(readManifestRuntimeDeps(dir), []);\n\t});\n});\n\n// ─── collectRuntimeDependencies ───────────────────────────────────────────────\n\ndescribe(\"collectRuntimeDependencies\", () => {\n\tit(\"aggregates deps from installedPath manifest\", (t) => {\n\t\tconst dir = tmpDir(\"collect-installed\", t);\n\t\twriteFileSync(join(dir, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tdependencies: { runtime: [\"claude\"] },\n\t\t}), \"utf-8\");\n\t\tassert.deepEqual(collectRuntimeDependencies(dir, []), [\"claude\"]);\n\t});\n\n\tit(\"aggregates deps from entry path directory manifests\", (t) => {\n\t\tconst root = tmpDir(\"collect-entry\", t);\n\t\tconst installedDir = join(root, \"installed\");\n\t\tconst entryDir = join(root, \"entry\");\n\t\tmkdirSync(installedDir, { recursive: true });\n\t\tmkdirSync(entryDir, { recursive: true });\n\t\twriteFileSync(join(entryDir, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tdependencies: { runtime: [\"python\"] },\n\t\t}), \"utf-8\");\n\t\tconst deps = collectRuntimeDependencies(installedDir, [join(entryDir, \"index.ts\")]);\n\t\tassert.deepEqual(deps, [\"python\"]);\n\t});\n\n\tit(\"deduplicates across multiple directories\", (t) => {\n\t\tconst root = tmpDir(\"collect-dedup\", t);\n\t\tconst dir1 = join(root, \"dir1\");\n\t\tconst dir2 = join(root, \"dir2\");\n\t\tmkdirSync(dir1, { recursive: true });\n\t\tmkdirSync(dir2, { recursive: true });\n\t\twriteFileSync(join(dir1, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tdependencies: { runtime: [\"node\", \"python\"] },\n\t\t}), \"utf-8\");\n\t\twriteFileSync(join(dir2, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tdependencies: { runtime: [\"python\", \"claude\"] },\n\t\t}), \"utf-8\");\n\t\tconst deps = collectRuntimeDependencies(dir1, [join(dir2, \"index.ts\")]);\n\t\tassert.equal(deps.length, 3);\n\t\tassert.ok(deps.includes(\"node\"));\n\t\tassert.ok(deps.includes(\"python\"));\n\t\tassert.ok(deps.includes(\"claude\"));\n\t});\n\n\tit(\"returns empty when no directories have manifests\", (t) => {\n\t\tconst dir = tmpDir(\"collect-empty\", t);\n\t\tassert.deepEqual(collectRuntimeDependencies(dir, []), []);\n\t});\n});\n\n// ─── verifyRuntimeDependencies ────────────────────────────────────────────────\n\ndescribe(\"verifyRuntimeDependencies\", () => {\n\tit(\"does not throw for empty deps array\", () => {\n\t\tassert.doesNotThrow(() => verifyRuntimeDependencies([], \"test-source\", \"pi\"));\n\t});\n\n\tit(\"does not throw when all deps are present\", () => {\n\t\tassert.doesNotThrow(() => verifyRuntimeDependencies([\"node\"], \"test-source\", \"pi\"));\n\t});\n\n\tit(\"throws for missing dep with 'Missing runtime dependencies' message\", () => {\n\t\tassert.throws(\n\t\t\t() => verifyRuntimeDependencies([\"__nonexistent_dep_for_test__\"], \"test-source\", \"pi\"),\n\t\t\t(err: Error) => {\n\t\t\t\tassert.ok(err.message.includes(\"Missing runtime dependencies\"));\n\t\t\t\tassert.ok(err.message.includes(\"__nonexistent_dep_for_test__\"));\n\t\t\t\treturn true;\n\t\t\t},\n\t\t);\n\t});\n\n\tit(\"lists all missing deps in error message\", () => {\n\t\tassert.throws(\n\t\t\t() => verifyRuntimeDependencies([\"__missing_1__\", \"__missing_2__\"], \"test-source\", \"pi\"),\n\t\t\t(err: Error) => {\n\t\t\t\tassert.ok(err.message.includes(\"__missing_1__\"));\n\t\t\t\tassert.ok(err.message.includes(\"__missing_2__\"));\n\t\t\t\treturn true;\n\t\t\t},\n\t\t);\n\t});\n\n\tit(\"includes appName and source in error for retry hint\", () => {\n\t\tassert.throws(\n\t\t\t() => verifyRuntimeDependencies([\"__missing__\"], \"github:user/repo\", \"gsd\"),\n\t\t\t(err: Error) => {\n\t\t\t\tassert.ok(err.message.includes(\"gsd\"));\n\t\t\t\tassert.ok(err.message.includes(\"github:user/repo\"));\n\t\t\t\treturn true;\n\t\t\t},\n\t\t);\n\t});\n});\n\n// ─── resolveLocalSourcePath ───────────────────────────────────────────────────\n\ndescribe(\"resolveLocalSourcePath\", () => {\n\tit(\"returns undefined for empty string\", () => {\n\t\tassert.equal(resolveLocalSourcePath(\"\", \"/tmp\"), undefined);\n\t});\n\n\tit(\"returns undefined for npm: source\", () => {\n\t\tassert.equal(resolveLocalSourcePath(\"npm:@foo/bar\", \"/tmp\"), undefined);\n\t});\n\n\tit(\"returns undefined for git URL\", () => {\n\t\tassert.equal(resolveLocalSourcePath(\"git:github.com/user/repo\", \"/tmp\"), undefined);\n\t});\n\n\tit(\"returns undefined for https git URL\", () => {\n\t\tassert.equal(resolveLocalSourcePath(\"https://github.com/user/repo\", \"/tmp\"), undefined);\n\t});\n\n\tit(\"resolves ~ to homedir\", () => {\n\t\tconst result = resolveLocalSourcePath(\"~\", \"/tmp\");\n\t\tif (existsSync(homedir())) {\n\t\t\tassert.equal(result, homedir());\n\t\t} else {\n\t\t\tassert.equal(result, undefined);\n\t\t}\n\t});\n\n\tit(\"resolves ~/path relative to homedir\", () => {\n\t\tconst result = resolveLocalSourcePath(\"~/\", \"/tmp\");\n\t\tif (existsSync(homedir())) {\n\t\t\tassert.equal(result, homedir());\n\t\t} else {\n\t\t\tassert.equal(result, undefined);\n\t\t}\n\t});\n\n\tit(\"resolves relative path that exists\", (t) => {\n\t\tconst dir = tmpDir(\"resolve-rel\", t);\n\t\tconst sub = join(dir, \"myext\");\n\t\tmkdirSync(sub, { recursive: true });\n\t\tconst result = resolveLocalSourcePath(\"myext\", dir);\n\t\tassert.equal(result, resolve(dir, \"myext\"));\n\t});\n\n\tit(\"returns undefined for relative path that does not exist\", (t) => {\n\t\tconst dir = tmpDir(\"resolve-noexist\", t);\n\t\tassert.equal(resolveLocalSourcePath(\"nonexistent\", dir), undefined);\n\t});\n\n\tit(\"resolves absolute path that exists\", (t) => {\n\t\tconst dir = tmpDir(\"resolve-abs\", t);\n\t\tassert.equal(resolveLocalSourcePath(dir, \"/irrelevant\"), dir);\n\t});\n\n\tit(\"returns undefined for absolute path that does not exist\", () => {\n\t\tassert.equal(resolveLocalSourcePath(\"/tmp/__nonexistent_path_for_test__\", \"/tmp\"), undefined);\n\t});\n});\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
|
+
import { getApiProvider } from "@gsd/pi-ai";
|
|
3
4
|
import { ModelRegistry } from "./model-registry.js";
|
|
4
5
|
function createRegistry(hasAuthFn) {
|
|
5
6
|
const authStorage = {
|
|
@@ -12,11 +13,11 @@ function createRegistry(hasAuthFn) {
|
|
|
12
13
|
};
|
|
13
14
|
return new ModelRegistry(authStorage, undefined);
|
|
14
15
|
}
|
|
15
|
-
function createProviderModel(id) {
|
|
16
|
+
function createProviderModel(id, api) {
|
|
16
17
|
return {
|
|
17
18
|
id,
|
|
18
19
|
name: id,
|
|
19
|
-
api: "openai-completions",
|
|
20
|
+
api: (api ?? "openai-completions"),
|
|
20
21
|
reasoning: false,
|
|
21
22
|
input: ["text"],
|
|
22
23
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
@@ -27,31 +28,79 @@ function createProviderModel(id) {
|
|
|
27
28
|
function findModel(registry, provider, id) {
|
|
28
29
|
return registry.getAvailable().find((m) => m.provider === provider && m.id === id);
|
|
29
30
|
}
|
|
31
|
+
function makeModel(provider, id, api) {
|
|
32
|
+
return {
|
|
33
|
+
id,
|
|
34
|
+
name: id,
|
|
35
|
+
api: api,
|
|
36
|
+
provider,
|
|
37
|
+
baseUrl: `${provider}:`,
|
|
38
|
+
reasoning: false,
|
|
39
|
+
input: ["text"],
|
|
40
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
41
|
+
contextWindow: 128000,
|
|
42
|
+
maxTokens: 16384,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function makeContext() {
|
|
46
|
+
return {
|
|
47
|
+
systemPrompt: "test",
|
|
48
|
+
messages: [{ role: "user", content: "hello", timestamp: Date.now() }],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** No-op streamSimple for tests that need one to pass validation but don't inspect it. */
|
|
52
|
+
const noopStreamSimple = (_model, _context, _options) => {
|
|
53
|
+
return {
|
|
54
|
+
[Symbol.asyncIterator]() { return { next: async () => ({ value: undefined, done: true }) }; },
|
|
55
|
+
result: () => Promise.resolve({ role: "assistant", content: [], api: "test", provider: "test", model: "test", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, stopReason: "stop", timestamp: Date.now() }),
|
|
56
|
+
push: () => { },
|
|
57
|
+
end: () => { },
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/** Create a spy streamSimple that captures the options it receives and returns a stub stream. */
|
|
61
|
+
function createStreamSpy() {
|
|
62
|
+
let capturedOptions;
|
|
63
|
+
const streamSimple = (_model, _context, options) => {
|
|
64
|
+
capturedOptions = options;
|
|
65
|
+
// Return a minimal stub that satisfies AssistantMessageEventStream
|
|
66
|
+
return {
|
|
67
|
+
[Symbol.asyncIterator]() { return { next: async () => ({ value: undefined, done: true }) }; },
|
|
68
|
+
result: () => Promise.resolve({ role: "assistant", content: [], api: "test", provider: "test", model: "test", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, stopReason: "stop", timestamp: Date.now() }),
|
|
69
|
+
push: () => { },
|
|
70
|
+
end: () => { },
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
return { streamSimple, getCapturedOptions: () => capturedOptions };
|
|
74
|
+
}
|
|
30
75
|
// ─── Registration ─────────────────────────────────────────────────────────────
|
|
31
76
|
describe("ModelRegistry authMode — registration", () => {
|
|
32
|
-
it("registers externalCli provider without apiKey/oauth", () => {
|
|
77
|
+
it("registers externalCli provider with streamSimple and without apiKey/oauth", () => {
|
|
33
78
|
const registry = createRegistry();
|
|
79
|
+
const spy = createStreamSpy();
|
|
34
80
|
assert.doesNotThrow(() => {
|
|
35
81
|
registry.registerProvider("cli-provider", {
|
|
36
82
|
authMode: "externalCli",
|
|
37
83
|
baseUrl: "https://cli.local",
|
|
38
84
|
api: "openai-completions",
|
|
85
|
+
streamSimple: spy.streamSimple,
|
|
39
86
|
models: [createProviderModel("cli-model")],
|
|
40
87
|
});
|
|
41
88
|
});
|
|
42
89
|
});
|
|
43
|
-
it("registers none provider without apiKey/oauth", () => {
|
|
90
|
+
it("registers none provider with streamSimple and without apiKey/oauth", () => {
|
|
44
91
|
const registry = createRegistry();
|
|
92
|
+
const spy = createStreamSpy();
|
|
45
93
|
assert.doesNotThrow(() => {
|
|
46
94
|
registry.registerProvider("none-provider", {
|
|
47
95
|
authMode: "none",
|
|
48
96
|
baseUrl: "http://localhost:11434",
|
|
49
97
|
api: "openai-completions",
|
|
98
|
+
streamSimple: spy.streamSimple,
|
|
50
99
|
models: [createProviderModel("local-model")],
|
|
51
100
|
});
|
|
52
101
|
});
|
|
53
102
|
});
|
|
54
|
-
it("rejects apiKey provider without apiKey or oauth", () => {
|
|
103
|
+
it("rejects apiKey provider without apiKey or oauth — message mentions authMode", () => {
|
|
55
104
|
const registry = createRegistry();
|
|
56
105
|
assert.throws(() => {
|
|
57
106
|
registry.registerProvider("apikey-provider", {
|
|
@@ -60,6 +109,10 @@ describe("ModelRegistry authMode — registration", () => {
|
|
|
60
109
|
api: "openai-completions",
|
|
61
110
|
models: [createProviderModel("model")],
|
|
62
111
|
});
|
|
112
|
+
}, (err) => {
|
|
113
|
+
assert.ok(err.message.includes("authMode"), "error message must mention authMode");
|
|
114
|
+
assert.ok(err.message.includes("externalCli"), "error message must suggest externalCli");
|
|
115
|
+
return true;
|
|
63
116
|
});
|
|
64
117
|
});
|
|
65
118
|
it("rejects provider with no authMode and no apiKey/oauth (defaults to apiKey)", () => {
|
|
@@ -70,6 +123,75 @@ describe("ModelRegistry authMode — registration", () => {
|
|
|
70
123
|
api: "openai-completions",
|
|
71
124
|
models: [createProviderModel("model")],
|
|
72
125
|
});
|
|
126
|
+
}, (err) => {
|
|
127
|
+
assert.ok(err.message.includes("authMode"), "error message must mention authMode");
|
|
128
|
+
return true;
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
it("rejects externalCli provider without streamSimple", () => {
|
|
132
|
+
const registry = createRegistry();
|
|
133
|
+
assert.throws(() => {
|
|
134
|
+
registry.registerProvider("cli-no-stream", {
|
|
135
|
+
authMode: "externalCli",
|
|
136
|
+
baseUrl: "https://cli.local",
|
|
137
|
+
api: "openai-completions",
|
|
138
|
+
models: [createProviderModel("model")],
|
|
139
|
+
});
|
|
140
|
+
}, (err) => {
|
|
141
|
+
assert.ok(err.message.includes("streamSimple"), "error message must mention streamSimple");
|
|
142
|
+
assert.ok(err.message.includes("externalCli"), "error message must mention authMode");
|
|
143
|
+
return true;
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
it("rejects none provider without streamSimple", () => {
|
|
147
|
+
const registry = createRegistry();
|
|
148
|
+
assert.throws(() => {
|
|
149
|
+
registry.registerProvider("none-no-stream", {
|
|
150
|
+
authMode: "none",
|
|
151
|
+
baseUrl: "http://localhost:11434",
|
|
152
|
+
api: "openai-completions",
|
|
153
|
+
models: [createProviderModel("model")],
|
|
154
|
+
});
|
|
155
|
+
}, (err) => {
|
|
156
|
+
assert.ok(err.message.includes("streamSimple"), "error message must mention streamSimple");
|
|
157
|
+
assert.ok(err.message.includes("none"), "error message must mention authMode");
|
|
158
|
+
return true;
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
it("rejects externalCli provider that also sets apiKey", () => {
|
|
162
|
+
const registry = createRegistry();
|
|
163
|
+
const spy = createStreamSpy();
|
|
164
|
+
assert.throws(() => {
|
|
165
|
+
registry.registerProvider("cli-with-key", {
|
|
166
|
+
authMode: "externalCli",
|
|
167
|
+
baseUrl: "https://cli.local",
|
|
168
|
+
api: "openai-completions",
|
|
169
|
+
apiKey: "SHOULD_NOT_EXIST",
|
|
170
|
+
streamSimple: spy.streamSimple,
|
|
171
|
+
models: [createProviderModel("model")],
|
|
172
|
+
});
|
|
173
|
+
}, (err) => {
|
|
174
|
+
assert.ok(err.message.includes("apiKey"), "error message must mention apiKey");
|
|
175
|
+
assert.ok(err.message.includes("externalCli"), "error message must mention authMode");
|
|
176
|
+
return true;
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
it("rejects none provider that also sets apiKey", () => {
|
|
180
|
+
const registry = createRegistry();
|
|
181
|
+
const spy = createStreamSpy();
|
|
182
|
+
assert.throws(() => {
|
|
183
|
+
registry.registerProvider("none-with-key", {
|
|
184
|
+
authMode: "none",
|
|
185
|
+
baseUrl: "http://localhost:11434",
|
|
186
|
+
api: "openai-completions",
|
|
187
|
+
apiKey: "SHOULD_NOT_EXIST",
|
|
188
|
+
streamSimple: spy.streamSimple,
|
|
189
|
+
models: [createProviderModel("model")],
|
|
190
|
+
});
|
|
191
|
+
}, (err) => {
|
|
192
|
+
assert.ok(err.message.includes("apiKey"), "error message must mention apiKey");
|
|
193
|
+
assert.ok(err.message.includes("none"), "error message must mention authMode");
|
|
194
|
+
return true;
|
|
73
195
|
});
|
|
74
196
|
});
|
|
75
197
|
});
|
|
@@ -85,6 +207,7 @@ describe("ModelRegistry authMode — getProviderAuthMode", () => {
|
|
|
85
207
|
authMode: "externalCli",
|
|
86
208
|
baseUrl: "https://cli.local",
|
|
87
209
|
api: "openai-completions",
|
|
210
|
+
streamSimple: noopStreamSimple,
|
|
88
211
|
models: [createProviderModel("m")],
|
|
89
212
|
});
|
|
90
213
|
assert.equal(registry.getProviderAuthMode("cli"), "externalCli");
|
|
@@ -95,6 +218,7 @@ describe("ModelRegistry authMode — getProviderAuthMode", () => {
|
|
|
95
218
|
authMode: "none",
|
|
96
219
|
baseUrl: "http://localhost:11434",
|
|
97
220
|
api: "openai-completions",
|
|
221
|
+
streamSimple: noopStreamSimple,
|
|
98
222
|
models: [createProviderModel("m")],
|
|
99
223
|
});
|
|
100
224
|
assert.equal(registry.getProviderAuthMode("local"), "none");
|
|
@@ -108,6 +232,7 @@ describe("ModelRegistry authMode — isProviderRequestReady", () => {
|
|
|
108
232
|
authMode: "externalCli",
|
|
109
233
|
baseUrl: "https://cli.local",
|
|
110
234
|
api: "openai-completions",
|
|
235
|
+
streamSimple: noopStreamSimple,
|
|
111
236
|
models: [createProviderModel("m")],
|
|
112
237
|
});
|
|
113
238
|
assert.equal(registry.isProviderRequestReady("cli"), true);
|
|
@@ -118,6 +243,7 @@ describe("ModelRegistry authMode — isProviderRequestReady", () => {
|
|
|
118
243
|
authMode: "none",
|
|
119
244
|
baseUrl: "http://localhost:11434",
|
|
120
245
|
api: "openai-completions",
|
|
246
|
+
streamSimple: noopStreamSimple,
|
|
121
247
|
models: [createProviderModel("m")],
|
|
122
248
|
});
|
|
123
249
|
assert.equal(registry.isProviderRequestReady("local"), true);
|
|
@@ -139,6 +265,7 @@ describe("ModelRegistry authMode — isReady callback", () => {
|
|
|
139
265
|
authMode: "externalCli",
|
|
140
266
|
baseUrl: "https://cli.local",
|
|
141
267
|
api: "openai-completions",
|
|
268
|
+
streamSimple: noopStreamSimple,
|
|
142
269
|
isReady: () => false,
|
|
143
270
|
models: [createProviderModel("m")],
|
|
144
271
|
});
|
|
@@ -161,6 +288,7 @@ describe("ModelRegistry authMode — isReady callback", () => {
|
|
|
161
288
|
authMode: "externalCli",
|
|
162
289
|
baseUrl: "https://cli.local",
|
|
163
290
|
api: "openai-completions",
|
|
291
|
+
streamSimple: noopStreamSimple,
|
|
164
292
|
isReady: () => true,
|
|
165
293
|
models: [createProviderModel("m")],
|
|
166
294
|
});
|
|
@@ -172,6 +300,7 @@ describe("ModelRegistry authMode — isReady callback", () => {
|
|
|
172
300
|
authMode: "externalCli",
|
|
173
301
|
baseUrl: "https://cli.local",
|
|
174
302
|
api: "openai-completions",
|
|
303
|
+
streamSimple: noopStreamSimple,
|
|
175
304
|
models: [createProviderModel("m")],
|
|
176
305
|
});
|
|
177
306
|
// externalCli without isReady → true (default)
|
|
@@ -186,6 +315,7 @@ describe("ModelRegistry authMode — getAvailable", () => {
|
|
|
186
315
|
authMode: "externalCli",
|
|
187
316
|
baseUrl: "https://cli.local",
|
|
188
317
|
api: "openai-completions",
|
|
318
|
+
streamSimple: noopStreamSimple,
|
|
189
319
|
models: [createProviderModel("cli-model")],
|
|
190
320
|
});
|
|
191
321
|
assert.ok(findModel(registry, "cli", "cli-model"));
|
|
@@ -196,6 +326,7 @@ describe("ModelRegistry authMode — getAvailable", () => {
|
|
|
196
326
|
authMode: "none",
|
|
197
327
|
baseUrl: "http://localhost:11434",
|
|
198
328
|
api: "openai-completions",
|
|
329
|
+
streamSimple: noopStreamSimple,
|
|
199
330
|
models: [createProviderModel("local-model")],
|
|
200
331
|
});
|
|
201
332
|
assert.ok(findModel(registry, "local", "local-model"));
|
|
@@ -206,6 +337,7 @@ describe("ModelRegistry authMode — getAvailable", () => {
|
|
|
206
337
|
authMode: "externalCli",
|
|
207
338
|
baseUrl: "https://cli.local",
|
|
208
339
|
api: "openai-completions",
|
|
340
|
+
streamSimple: noopStreamSimple,
|
|
209
341
|
isReady: () => false,
|
|
210
342
|
models: [createProviderModel("m")],
|
|
211
343
|
});
|
|
@@ -213,10 +345,7 @@ describe("ModelRegistry authMode — getAvailable", () => {
|
|
|
213
345
|
});
|
|
214
346
|
it("excludes apiKey models without stored auth", () => {
|
|
215
347
|
const registry = createRegistry(() => false);
|
|
216
|
-
// Built-in providers have no registeredProviders entry, so authMode defaults to apiKey
|
|
217
|
-
// getAvailable filters by isProviderRequestReady → hasAuth → false
|
|
218
348
|
const available = registry.getAvailable();
|
|
219
|
-
// No models should be available since hasAuth returns false for everything
|
|
220
349
|
assert.equal(available.length, 0);
|
|
221
350
|
});
|
|
222
351
|
});
|
|
@@ -228,6 +357,7 @@ describe("ModelRegistry authMode — getApiKey", () => {
|
|
|
228
357
|
authMode: "externalCli",
|
|
229
358
|
baseUrl: "https://cli.local",
|
|
230
359
|
api: "openai-completions",
|
|
360
|
+
streamSimple: noopStreamSimple,
|
|
231
361
|
models: [createProviderModel("m")],
|
|
232
362
|
});
|
|
233
363
|
const model = registry.getAll().find((m) => m.provider === "cli");
|
|
@@ -239,6 +369,7 @@ describe("ModelRegistry authMode — getApiKey", () => {
|
|
|
239
369
|
authMode: "none",
|
|
240
370
|
baseUrl: "http://localhost:11434",
|
|
241
371
|
api: "openai-completions",
|
|
372
|
+
streamSimple: noopStreamSimple,
|
|
242
373
|
models: [createProviderModel("m")],
|
|
243
374
|
});
|
|
244
375
|
const model = registry.getAll().find((m) => m.provider === "local");
|
|
@@ -246,10 +377,108 @@ describe("ModelRegistry authMode — getApiKey", () => {
|
|
|
246
377
|
});
|
|
247
378
|
it("delegates to authStorage for apiKey provider", async () => {
|
|
248
379
|
const registry = createRegistry();
|
|
249
|
-
// authStorage.getApiKey returns undefined (no key configured)
|
|
250
|
-
// For apiKey providers this is an expected "no key" response, not early exit
|
|
251
380
|
const key = await registry.getApiKeyForProvider("anthropic");
|
|
252
381
|
assert.equal(key, undefined);
|
|
253
382
|
});
|
|
254
383
|
});
|
|
384
|
+
// ─── streamSimple apiKey stripping ────────────────────────────────────────────
|
|
385
|
+
describe("ModelRegistry authMode — streamSimple apiKey boundary", () => {
|
|
386
|
+
it("strips apiKey from options for externalCli provider", () => {
|
|
387
|
+
const registry = createRegistry();
|
|
388
|
+
const spy = createStreamSpy();
|
|
389
|
+
const apiType = `ext-cli-strip-${Date.now()}`;
|
|
390
|
+
registry.registerProvider("cli-strip", {
|
|
391
|
+
authMode: "externalCli",
|
|
392
|
+
baseUrl: "https://cli.local",
|
|
393
|
+
api: apiType,
|
|
394
|
+
streamSimple: spy.streamSimple,
|
|
395
|
+
models: [createProviderModel("m", apiType)],
|
|
396
|
+
});
|
|
397
|
+
const provider = getApiProvider(apiType);
|
|
398
|
+
assert.ok(provider, "provider must be registered in api registry");
|
|
399
|
+
provider.streamSimple(makeModel("cli-strip", "m", apiType), makeContext(), { apiKey: "should-be-stripped", maxTokens: 1024 });
|
|
400
|
+
const captured = spy.getCapturedOptions();
|
|
401
|
+
assert.ok(captured, "streamSimple must have been called");
|
|
402
|
+
assert.equal("apiKey" in captured, false, "apiKey must not exist in options for externalCli provider");
|
|
403
|
+
assert.equal(captured.maxTokens, 1024, "other options must pass through");
|
|
404
|
+
});
|
|
405
|
+
it("strips apiKey from options for none provider", () => {
|
|
406
|
+
const registry = createRegistry();
|
|
407
|
+
const spy = createStreamSpy();
|
|
408
|
+
const apiType = `none-strip-${Date.now()}`;
|
|
409
|
+
registry.registerProvider("none-strip", {
|
|
410
|
+
authMode: "none",
|
|
411
|
+
baseUrl: "http://localhost:11434",
|
|
412
|
+
api: apiType,
|
|
413
|
+
streamSimple: spy.streamSimple,
|
|
414
|
+
models: [createProviderModel("m", apiType)],
|
|
415
|
+
});
|
|
416
|
+
const provider = getApiProvider(apiType);
|
|
417
|
+
assert.ok(provider, "provider must be registered in api registry");
|
|
418
|
+
provider.streamSimple(makeModel("none-strip", "m", apiType), makeContext(), { apiKey: "should-be-stripped", maxTokens: 2048 });
|
|
419
|
+
const captured = spy.getCapturedOptions();
|
|
420
|
+
assert.ok(captured, "streamSimple must have been called");
|
|
421
|
+
assert.equal("apiKey" in captured, false, "apiKey must not exist in options for none provider");
|
|
422
|
+
assert.equal(captured.maxTokens, 2048, "other options must pass through");
|
|
423
|
+
});
|
|
424
|
+
it("preserves apiKey in options for apiKey provider", () => {
|
|
425
|
+
const registry = createRegistry();
|
|
426
|
+
const spy = createStreamSpy();
|
|
427
|
+
const apiType = `apikey-preserve-${Date.now()}`;
|
|
428
|
+
registry.registerProvider("apikey-preserve", {
|
|
429
|
+
apiKey: "MY_KEY",
|
|
430
|
+
baseUrl: "https://api.local",
|
|
431
|
+
api: apiType,
|
|
432
|
+
streamSimple: spy.streamSimple,
|
|
433
|
+
models: [createProviderModel("m", apiType)],
|
|
434
|
+
});
|
|
435
|
+
const provider = getApiProvider(apiType);
|
|
436
|
+
assert.ok(provider, "provider must be registered in api registry");
|
|
437
|
+
provider.streamSimple(makeModel("apikey-preserve", "m", apiType), makeContext(), { apiKey: "sk-real-key", maxTokens: 4096 });
|
|
438
|
+
const captured = spy.getCapturedOptions();
|
|
439
|
+
assert.ok(captured, "streamSimple must have been called");
|
|
440
|
+
assert.equal(captured.apiKey, "sk-real-key", "apiKey must be preserved for apiKey provider");
|
|
441
|
+
assert.equal(captured.maxTokens, 4096, "other options must pass through");
|
|
442
|
+
});
|
|
443
|
+
it("handles undefined options for externalCli provider", () => {
|
|
444
|
+
const registry = createRegistry();
|
|
445
|
+
const spy = createStreamSpy();
|
|
446
|
+
const apiType = `ext-cli-undef-${Date.now()}`;
|
|
447
|
+
registry.registerProvider("cli-undef", {
|
|
448
|
+
authMode: "externalCli",
|
|
449
|
+
baseUrl: "https://cli.local",
|
|
450
|
+
api: apiType,
|
|
451
|
+
streamSimple: spy.streamSimple,
|
|
452
|
+
models: [createProviderModel("m", apiType)],
|
|
453
|
+
});
|
|
454
|
+
const provider = getApiProvider(apiType);
|
|
455
|
+
assert.ok(provider, "provider must be registered in api registry");
|
|
456
|
+
provider.streamSimple(makeModel("cli-undef", "m", apiType), makeContext(), undefined);
|
|
457
|
+
const captured = spy.getCapturedOptions();
|
|
458
|
+
assert.ok(captured !== undefined, "streamSimple must have been called");
|
|
459
|
+
assert.equal("apiKey" in captured, false, "apiKey must not exist even when options is undefined");
|
|
460
|
+
});
|
|
461
|
+
it("strips apiKey but preserves signal and other fields for externalCli", () => {
|
|
462
|
+
const registry = createRegistry();
|
|
463
|
+
const spy = createStreamSpy();
|
|
464
|
+
const apiType = `ext-cli-fields-${Date.now()}`;
|
|
465
|
+
const abortController = new AbortController();
|
|
466
|
+
registry.registerProvider("cli-fields", {
|
|
467
|
+
authMode: "externalCli",
|
|
468
|
+
baseUrl: "https://cli.local",
|
|
469
|
+
api: apiType,
|
|
470
|
+
streamSimple: spy.streamSimple,
|
|
471
|
+
models: [createProviderModel("m", apiType)],
|
|
472
|
+
});
|
|
473
|
+
const provider = getApiProvider(apiType);
|
|
474
|
+
assert.ok(provider, "provider must be registered in api registry");
|
|
475
|
+
provider.streamSimple(makeModel("cli-fields", "m", apiType), makeContext(), { apiKey: "strip-me", maxTokens: 8192, signal: abortController.signal, reasoning: "high" });
|
|
476
|
+
const captured = spy.getCapturedOptions();
|
|
477
|
+
assert.ok(captured, "streamSimple must have been called");
|
|
478
|
+
assert.equal("apiKey" in captured, false, "apiKey must be stripped");
|
|
479
|
+
assert.equal(captured.maxTokens, 8192, "maxTokens must pass through");
|
|
480
|
+
assert.equal(captured.signal, abortController.signal, "signal must pass through");
|
|
481
|
+
assert.equal(captured.reasoning, "high", "reasoning must pass through");
|
|
482
|
+
});
|
|
483
|
+
});
|
|
255
484
|
//# sourceMappingURL=model-registry-auth-mode.test.js.map
|