@blackbelt-technology/pi-agent-dashboard 0.4.1 → 0.4.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/AGENTS.md +79 -32
- package/README.md +7 -3
- package/docs/architecture.md +361 -12
- package/package.json +7 -7
- package/packages/extension/package.json +7 -2
- package/packages/extension/src/__tests__/ask-user-schema-discriminator.test.ts +141 -0
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +51 -7
- package/packages/extension/src/__tests__/multiselect-dashboard-routing.test.ts +203 -0
- package/packages/extension/src/__tests__/multiselect-polyfill.test.ts +92 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +81 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +37 -0
- package/packages/extension/src/__tests__/ui-decorators.test.ts +309 -0
- package/packages/extension/src/__tests__/ui-modules.test.ts +293 -0
- package/packages/extension/src/ask-user-tool.ts +165 -57
- package/packages/extension/src/bridge.ts +97 -4
- package/packages/extension/src/multiselect-decode.ts +40 -0
- package/packages/extension/src/multiselect-polyfill.ts +38 -8
- package/packages/extension/src/ui-modules.ts +272 -0
- package/packages/server/package.json +9 -3
- package/packages/server/src/__tests__/auto-attach.test.ts +61 -8
- package/packages/server/src/__tests__/browse-endpoint.test.ts +295 -19
- package/packages/server/src/__tests__/cli-bootstrap.test.ts +36 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +163 -0
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +315 -0
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +303 -0
- package/packages/server/src/__tests__/directory-service.test.ts +174 -0
- package/packages/server/src/__tests__/installed-package-enricher.test.ts +225 -0
- package/packages/server/src/__tests__/package-manager-wrapper-move.test.ts +414 -0
- package/packages/server/src/__tests__/package-routes.test.ts +136 -3
- package/packages/server/src/__tests__/package-source-helpers.test.ts +101 -0
- package/packages/server/src/__tests__/pending-attach-registry.test.ts +123 -0
- package/packages/server/src/__tests__/pending-resume-intent-registry.test.ts +138 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +73 -30
- package/packages/server/src/__tests__/pi-gateway-consume-pending-attach.test.ts +112 -0
- package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +180 -0
- package/packages/server/src/__tests__/post-install-rescan.test.ts +134 -0
- package/packages/server/src/__tests__/proposal-attach-naming.test.ts +79 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-with-attach.test.ts +108 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +55 -0
- package/packages/server/src/__tests__/session-order-reboot.test.ts +242 -0
- package/packages/server/src/__tests__/session-scanner.test.ts +44 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +40 -0
- package/packages/server/src/__tests__/translate-path-source.test.ts +77 -0
- package/packages/server/src/__tests__/ui-decorators-replay.test.ts +209 -0
- package/packages/server/src/__tests__/ui-modules-replay.test.ts +221 -0
- package/packages/server/src/browse.ts +118 -13
- package/packages/server/src/browser-gateway.ts +19 -0
- package/packages/server/src/browser-handlers/__tests__/session-meta-handler.test.ts +183 -0
- package/packages/server/src/browser-handlers/directory-handler.ts +7 -1
- package/packages/server/src/browser-handlers/handler-context.ts +15 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +29 -3
- package/packages/server/src/browser-handlers/session-meta-handler.ts +46 -12
- package/packages/server/src/browser-handlers/subscription-handler.ts +46 -1
- package/packages/server/src/cli.ts +5 -6
- package/packages/server/src/directory-service.ts +156 -15
- package/packages/server/src/event-wiring.ts +111 -10
- package/packages/server/src/installed-package-enricher.ts +143 -0
- package/packages/server/src/package-manager-wrapper.ts +305 -8
- package/packages/server/src/package-source-helpers.ts +104 -0
- package/packages/server/src/pending-attach-registry.ts +112 -0
- package/packages/server/src/pending-resume-intent-registry.ts +107 -0
- package/packages/server/src/pi-core-checker.ts +9 -14
- package/packages/server/src/pi-gateway.ts +14 -0
- package/packages/server/src/proposal-attach-naming.ts +47 -0
- package/packages/server/src/routes/file-routes.ts +29 -3
- package/packages/server/src/routes/package-routes.ts +72 -3
- package/packages/server/src/routes/plugin-config-routes.ts +129 -0
- package/packages/server/src/routes/system-routes.ts +2 -0
- package/packages/server/src/server.ts +339 -10
- package/packages/server/src/session-api.ts +30 -5
- package/packages/server/src/session-order-manager.ts +22 -0
- package/packages/server/src/session-scanner.ts +10 -1
- package/packages/shared/package.json +9 -2
- package/packages/shared/src/__tests__/browser-protocol-types.test.ts +59 -0
- package/packages/shared/src/__tests__/config-plugins.test.ts +68 -0
- package/packages/shared/src/__tests__/extension-ui-module-shape.test.ts +265 -0
- package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +81 -0
- package/packages/shared/src/__tests__/openspec-design-evidence.test.ts +288 -0
- package/packages/shared/src/__tests__/openspec-effective-status-script.test.ts +174 -0
- package/packages/shared/src/__tests__/openspec-poller-design-override.test.ts +225 -0
- package/packages/shared/src/__tests__/openspec-poller-specs-override.test.ts +284 -0
- package/packages/shared/src/__tests__/openspec-specs-evidence.test.ts +144 -0
- package/packages/shared/src/__tests__/platform/is-appimage-self-hit.test.ts +164 -0
- package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +72 -0
- package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +113 -0
- package/packages/shared/src/__tests__/plugin-config-update-protocol.test.ts +41 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +5 -1
- package/packages/shared/src/__tests__/spawn-session-attach-proposal.test.ts +47 -0
- package/packages/shared/src/__tests__/tool-registry-strategies-appimage.test.ts +118 -0
- package/packages/shared/src/browser-protocol.ts +110 -4
- package/packages/shared/src/config.ts +45 -0
- package/packages/shared/src/dashboard-plugin/index.ts +11 -0
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +58 -0
- package/packages/shared/src/dashboard-plugin/plugin-status.ts +26 -0
- package/packages/shared/src/dashboard-plugin/slot-props.ts +92 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +151 -0
- package/packages/shared/src/openspec-activity-detector.ts +18 -22
- package/packages/shared/src/openspec-design-evidence.ts +109 -0
- package/packages/shared/src/openspec-poller.ts +117 -3
- package/packages/shared/src/openspec-specs-evidence.ts +79 -0
- package/packages/shared/src/platform/binary-lookup.ts +96 -1
- package/packages/shared/src/plugin-bridge-register.ts +139 -0
- package/packages/shared/src/protocol.ts +56 -2
- package/packages/shared/src/recommended-extensions.ts +7 -1
- package/packages/shared/src/rest-api.ts +68 -3
- package/packages/shared/src/state-replay.ts +11 -1
- package/packages/shared/src/tool-registry/strategies.ts +17 -3
- package/packages/shared/src/types.ts +160 -0
|
@@ -226,6 +226,75 @@ describe("DirectoryService", () => {
|
|
|
226
226
|
|
|
227
227
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
228
228
|
});
|
|
229
|
+
|
|
230
|
+
it("applies design override (R3): tasks.md with checkboxes promotes design→done", async () => {
|
|
231
|
+
// See change: fix-openspec-design-detection.
|
|
232
|
+
const fs = await import("node:fs");
|
|
233
|
+
const os = await import("node:os");
|
|
234
|
+
const path = await import("node:path");
|
|
235
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "ds-design-override-"));
|
|
236
|
+
const changeDir = path.join(tmp, "openspec", "changes", "change-x");
|
|
237
|
+
fs.mkdirSync(changeDir, { recursive: true });
|
|
238
|
+
fs.writeFileSync(path.join(changeDir, "tasks.md"), "## 1. Setup\n\n- [ ] 1.1 Do thing\n");
|
|
239
|
+
|
|
240
|
+
const { runOpenSpecList, runOpenSpecStatus } = await import("@blackbelt-technology/pi-dashboard-shared/openspec-poller.js");
|
|
241
|
+
(runOpenSpecList as any).mockResolvedValue({ changes: [
|
|
242
|
+
{ name: "change-x", status: "in-progress", completedTasks: 0, totalTasks: 1 },
|
|
243
|
+
] });
|
|
244
|
+
(runOpenSpecStatus as any).mockResolvedValue({
|
|
245
|
+
artifacts: [
|
|
246
|
+
{ id: "proposal", status: "done" },
|
|
247
|
+
{ id: "specs", status: "done" },
|
|
248
|
+
{ id: "design", status: "ready" },
|
|
249
|
+
{ id: "tasks", status: "ready" },
|
|
250
|
+
],
|
|
251
|
+
isComplete: false,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const stateStore = createMockPreferencesStore();
|
|
255
|
+
const sessionManager = createMockSessionManager();
|
|
256
|
+
service = createDirectoryService(stateStore, sessionManager);
|
|
257
|
+
|
|
258
|
+
const data = await service.refreshOpenSpec(tmp);
|
|
259
|
+
const change = data.changes.find((c) => c.name === "change-x")!;
|
|
260
|
+
const design = change.artifacts.find((a) => a.id === "design")!;
|
|
261
|
+
expect(design.status).toBe("done");
|
|
262
|
+
// tasks artifact should pass through unchanged (still ready)
|
|
263
|
+
expect(change.artifacts.find((a) => a.id === "tasks")!.status).toBe("ready");
|
|
264
|
+
|
|
265
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("design override leaves design=ready when no evidence", async () => {
|
|
269
|
+
const fs = await import("node:fs");
|
|
270
|
+
const os = await import("node:os");
|
|
271
|
+
const path = await import("node:path");
|
|
272
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "ds-design-no-override-"));
|
|
273
|
+
const changeDir = path.join(tmp, "openspec", "changes", "change-y");
|
|
274
|
+
fs.mkdirSync(changeDir, { recursive: true });
|
|
275
|
+
fs.writeFileSync(path.join(changeDir, "proposal.md"), "# proposal\n");
|
|
276
|
+
|
|
277
|
+
const { runOpenSpecList, runOpenSpecStatus } = await import("@blackbelt-technology/pi-dashboard-shared/openspec-poller.js");
|
|
278
|
+
(runOpenSpecList as any).mockResolvedValue({ changes: [
|
|
279
|
+
{ name: "change-y", status: "in-progress", completedTasks: 0, totalTasks: 0 },
|
|
280
|
+
] });
|
|
281
|
+
(runOpenSpecStatus as any).mockResolvedValue({
|
|
282
|
+
artifacts: [
|
|
283
|
+
{ id: "proposal", status: "done" },
|
|
284
|
+
{ id: "design", status: "ready" },
|
|
285
|
+
],
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const stateStore = createMockPreferencesStore();
|
|
289
|
+
const sessionManager = createMockSessionManager();
|
|
290
|
+
service = createDirectoryService(stateStore, sessionManager);
|
|
291
|
+
|
|
292
|
+
const data = await service.refreshOpenSpec(tmp);
|
|
293
|
+
const change = data.changes.find((c) => c.name === "change-y")!;
|
|
294
|
+
expect(change.artifacts.find((a) => a.id === "design")!.status).toBe("ready");
|
|
295
|
+
|
|
296
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
297
|
+
});
|
|
229
298
|
});
|
|
230
299
|
|
|
231
300
|
describe("onDirectoryAdded", () => {
|
|
@@ -375,6 +444,111 @@ describe("DirectoryService", () => {
|
|
|
375
444
|
expect(runOpenSpecStatus).toHaveBeenCalledTimes(1);
|
|
376
445
|
});
|
|
377
446
|
|
|
447
|
+
it("re-spawns list+status when tasks.md is edited in place (POSIX dir-mtime blind spot)", async () => {
|
|
448
|
+
// This test covers the bug fix in change `fix-openspec-mtime-gate-blind-spots`:
|
|
449
|
+
// POSIX directory mtime advances only on entry create/delete/rename, not on
|
|
450
|
+
// in-place file content edits. The previous gate used dir mtime alone and
|
|
451
|
+
// missed these edits, leaving `completedTasks` stuck at the cached value.
|
|
452
|
+
const { runOpenSpecList, runOpenSpecStatus } = await import("@blackbelt-technology/pi-dashboard-shared/openspec-poller.js");
|
|
453
|
+
(runOpenSpecList as any).mockResolvedValue({ changes: [
|
|
454
|
+
{ name: "change-a", status: "in-progress", completedTasks: 0, totalTasks: 3 },
|
|
455
|
+
{ name: "change-b", status: "in-progress", completedTasks: 0, totalTasks: 1 },
|
|
456
|
+
] });
|
|
457
|
+
(runOpenSpecStatus as any).mockResolvedValue({ artifacts: [], isComplete: false });
|
|
458
|
+
|
|
459
|
+
// Seed tasks.md inside change-a so the file is part of the gate signal.
|
|
460
|
+
const tasksMd = path.join(changesDir, "change-a", "tasks.md");
|
|
461
|
+
fs.writeFileSync(tasksMd, "- [ ] 1.1 a\n- [ ] 1.2 b\n- [ ] 1.3 c\n", "utf-8");
|
|
462
|
+
|
|
463
|
+
const stateStore = createMockPreferencesStore();
|
|
464
|
+
const sessionManager = createMockSessionManager();
|
|
465
|
+
service = createDirectoryService(stateStore, sessionManager);
|
|
466
|
+
await service.refreshOpenSpec(cwd);
|
|
467
|
+
(runOpenSpecList as any).mockClear();
|
|
468
|
+
(runOpenSpecStatus as any).mockClear();
|
|
469
|
+
|
|
470
|
+
// Simulate an in-place edit: rewrite tasks.md AND bump its file mtime.
|
|
471
|
+
// Crucially, do NOT touch the parent directory's mtime — that's the
|
|
472
|
+
// blind spot the fix is supposed to cover.
|
|
473
|
+
fs.writeFileSync(tasksMd, "- [x] 1.1 a\n- [x] 1.2 b\n- [ ] 1.3 c\n", "utf-8");
|
|
474
|
+
const future = new Date(Date.now() + 60_000);
|
|
475
|
+
fs.utimesSync(tasksMd, future, future);
|
|
476
|
+
|
|
477
|
+
(runOpenSpecList as any).mockResolvedValue({ changes: [
|
|
478
|
+
{ name: "change-a", status: "in-progress", completedTasks: 2, totalTasks: 3 },
|
|
479
|
+
{ name: "change-b", status: "in-progress", completedTasks: 0, totalTasks: 1 },
|
|
480
|
+
] });
|
|
481
|
+
|
|
482
|
+
await service.pollDirectoryGated(cwd);
|
|
483
|
+
|
|
484
|
+
// List re-runs because the tasks.md file mtime is part of the list-step signal.
|
|
485
|
+
expect(runOpenSpecList).toHaveBeenCalledTimes(1);
|
|
486
|
+
// Status re-runs only for change-a (its effective mtime advanced via tasks.md).
|
|
487
|
+
expect(runOpenSpecStatus).toHaveBeenCalledTimes(1);
|
|
488
|
+
expect((runOpenSpecStatus as any).mock.calls[0][1]).toBe("change-a");
|
|
489
|
+
// Cache reflects the new counter.
|
|
490
|
+
const data = service.getOpenSpecData(cwd);
|
|
491
|
+
const ca = data?.changes.find((c) => c.name === "change-a");
|
|
492
|
+
expect(ca?.completedTasks).toBe(2);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("post-archive path uses pollDirectoryGated (zero status spawns for unchanged changes)", async () => {
|
|
496
|
+
// Covers the post-archive contract: `fix-openspec-mtime-gate-blind-spots`
|
|
497
|
+
// made the gate file-aware so the bulk-archive path could safely skip
|
|
498
|
+
// status spawns for unchanged changes. `fix-openspec-mtime-gate-toctou`
|
|
499
|
+
// re-introduced `force=true` on the user-facing `refreshOpenSpec` (so a
|
|
500
|
+
// user clicking the refresh icon always sees authoritative data), and
|
|
501
|
+
// routed the bulk-archive handler through `pollDirectoryGated` instead
|
|
502
|
+
// — preserving the O(1) status-spawn property after archives.
|
|
503
|
+
// Pre-fix (pre-blind-spots) this test would see 4 status spawns because
|
|
504
|
+
// `force=true` disabled the gate.
|
|
505
|
+
const { runOpenSpecList, runOpenSpecStatus } = await import("@blackbelt-technology/pi-dashboard-shared/openspec-poller.js");
|
|
506
|
+
|
|
507
|
+
// Set up 5 changes in the directory (replacing the beforeEach default).
|
|
508
|
+
fs.rmSync(path.join(changesDir, "change-a"), { recursive: true });
|
|
509
|
+
fs.rmSync(path.join(changesDir, "change-b"), { recursive: true });
|
|
510
|
+
for (const n of ["c1", "c2", "c3", "c4", "c5"]) {
|
|
511
|
+
fs.mkdirSync(path.join(changesDir, n));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
(runOpenSpecList as any).mockResolvedValueOnce({ changes: [
|
|
515
|
+
{ name: "c1", status: "in-progress", completedTasks: 0, totalTasks: 1 },
|
|
516
|
+
{ name: "c2", status: "in-progress", completedTasks: 0, totalTasks: 1 },
|
|
517
|
+
{ name: "c3", status: "in-progress", completedTasks: 0, totalTasks: 1 },
|
|
518
|
+
{ name: "c4", status: "in-progress", completedTasks: 0, totalTasks: 1 },
|
|
519
|
+
{ name: "c5", status: "in-progress", completedTasks: 0, totalTasks: 1 },
|
|
520
|
+
] });
|
|
521
|
+
(runOpenSpecStatus as any).mockResolvedValue({ artifacts: [], isComplete: false });
|
|
522
|
+
|
|
523
|
+
const stateStore = createMockPreferencesStore();
|
|
524
|
+
const sessionManager = createMockSessionManager();
|
|
525
|
+
service = createDirectoryService(stateStore, sessionManager);
|
|
526
|
+
|
|
527
|
+
// First poll seeds the cache (5 list+status spawns; not what's under test).
|
|
528
|
+
await service.pollDirectoryGated(cwd);
|
|
529
|
+
(runOpenSpecList as any).mockClear();
|
|
530
|
+
(runOpenSpecStatus as any).mockClear();
|
|
531
|
+
|
|
532
|
+
// Simulate archive: remove one change directory and bump <changes>/ mtime.
|
|
533
|
+
fs.rmSync(path.join(changesDir, "c5"), { recursive: true });
|
|
534
|
+
const future = new Date(Date.now() + 60_000);
|
|
535
|
+
fs.utimesSync(changesDir, future, future);
|
|
536
|
+
(runOpenSpecList as any).mockResolvedValueOnce({ changes: [
|
|
537
|
+
{ name: "c1", status: "in-progress", completedTasks: 0, totalTasks: 1 },
|
|
538
|
+
{ name: "c2", status: "in-progress", completedTasks: 0, totalTasks: 1 },
|
|
539
|
+
{ name: "c3", status: "in-progress", completedTasks: 0, totalTasks: 1 },
|
|
540
|
+
{ name: "c4", status: "in-progress", completedTasks: 0, totalTasks: 1 },
|
|
541
|
+
] });
|
|
542
|
+
|
|
543
|
+
await service.pollDirectoryGated(cwd);
|
|
544
|
+
|
|
545
|
+
expect(runOpenSpecList).toHaveBeenCalledTimes(1);
|
|
546
|
+
// The gate skips every status because none of the surviving changes'
|
|
547
|
+
// artifact files moved. This is the path `handleOpenSpecBulkArchive`
|
|
548
|
+
// now takes (was `refreshOpenSpec` before fix-openspec-mtime-gate-toctou).
|
|
549
|
+
expect(runOpenSpecStatus).toHaveBeenCalledTimes(0);
|
|
550
|
+
});
|
|
551
|
+
|
|
378
552
|
it("removed change is pruned from cache", async () => {
|
|
379
553
|
const { runOpenSpecList, runOpenSpecStatus } = await import("@blackbelt-technology/pi-dashboard-shared/openspec-poller.js");
|
|
380
554
|
(runOpenSpecList as any).mockResolvedValueOnce({ changes: [
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the installed-package enricher (used by /api/packages/installed).
|
|
3
|
+
*
|
|
4
|
+
* The enricher is pure-with-injected-IO: every external dep
|
|
5
|
+
* (readMeta, existsFn, manifest, bundledIds) is replaceable. We
|
|
6
|
+
* exercise each branch without touching the real filesystem.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect } from "vitest";
|
|
9
|
+
import {
|
|
10
|
+
enrichInstalledRow,
|
|
11
|
+
extractBasenameFromSource,
|
|
12
|
+
matchRecommendedEntry,
|
|
13
|
+
computeIsBundled,
|
|
14
|
+
type RawInstalledRow,
|
|
15
|
+
} from "../installed-package-enricher.js";
|
|
16
|
+
import type { RecommendedExtension } from "@blackbelt-technology/pi-dashboard-shared/recommended-extensions.js";
|
|
17
|
+
|
|
18
|
+
const FAKE_MANIFEST: readonly RecommendedExtension[] = [
|
|
19
|
+
{
|
|
20
|
+
id: "pi-flows",
|
|
21
|
+
source: "https://github.com/BlackBeltTechnology/pi-flows.git",
|
|
22
|
+
displayName: "pi-flows",
|
|
23
|
+
fallbackDescription: "Flow engine for pi.",
|
|
24
|
+
status: "strongly-suggested",
|
|
25
|
+
unlocks: [],
|
|
26
|
+
toolsRegistered: [],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "pi-anthropic-messages",
|
|
30
|
+
source: "https://github.com/BlackBeltTechnology/pi-anthropic-messages.git",
|
|
31
|
+
displayName: "pi-anthropic-messages",
|
|
32
|
+
fallbackDescription: "Anthropic Messages provider.",
|
|
33
|
+
status: "required",
|
|
34
|
+
unlocks: [],
|
|
35
|
+
toolsRegistered: [],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "@tintinweb/pi-subagents",
|
|
39
|
+
source: "npm:@tintinweb/pi-subagents",
|
|
40
|
+
displayName: "@tintinweb/pi-subagents",
|
|
41
|
+
fallbackDescription: "Sub-agents for pi.",
|
|
42
|
+
status: "strongly-suggested",
|
|
43
|
+
unlocks: [],
|
|
44
|
+
toolsRegistered: [],
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
describe("extractBasenameFromSource", () => {
|
|
49
|
+
it("strips npm: prefix and version pin", () => {
|
|
50
|
+
expect(extractBasenameFromSource("npm:pi-agent-browser")).toBe("pi-agent-browser");
|
|
51
|
+
expect(extractBasenameFromSource("npm:pi-agent-browser@1.2.3")).toBe("pi-agent-browser");
|
|
52
|
+
expect(extractBasenameFromSource("npm:@tintinweb/pi-subagents")).toBe("@tintinweb/pi-subagents");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("strips .git suffix from git URLs", () => {
|
|
56
|
+
expect(extractBasenameFromSource("https://github.com/BlackBeltTechnology/pi-flows.git")).toBe("pi-flows");
|
|
57
|
+
expect(extractBasenameFromSource("git@github.com:BlackBeltTechnology/pi-flows.git")).toBe("pi-flows");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns last path segment for local paths", () => {
|
|
61
|
+
expect(extractBasenameFromSource("/home/user/pi-packages/pi-flows")).toBe("pi-flows");
|
|
62
|
+
expect(extractBasenameFromSource("../../BB/pi-packages/pi-flows")).toBe("pi-flows");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("falls back to the raw source when nothing else matches", () => {
|
|
66
|
+
expect(extractBasenameFromSource("weird-thing")).toBe("weird-thing");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("matchRecommendedEntry", () => {
|
|
71
|
+
it("matches by exact source", () => {
|
|
72
|
+
expect(
|
|
73
|
+
matchRecommendedEntry("npm:@tintinweb/pi-subagents", FAKE_MANIFEST)?.id,
|
|
74
|
+
).toBe("@tintinweb/pi-subagents");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("matches git source regardless of trailing slash / case", () => {
|
|
78
|
+
expect(
|
|
79
|
+
matchRecommendedEntry(
|
|
80
|
+
"https://github.com/BlackBeltTechnology/pi-flows.git",
|
|
81
|
+
FAKE_MANIFEST,
|
|
82
|
+
)?.id,
|
|
83
|
+
).toBe("pi-flows");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns undefined when no entry matches", () => {
|
|
87
|
+
expect(matchRecommendedEntry("npm:weird-other-pkg", FAKE_MANIFEST)).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("computeIsBundled", () => {
|
|
92
|
+
it("returns false when no resourcesPath", () => {
|
|
93
|
+
expect(computeIsBundled("pi-flows", undefined, () => true, ["pi-flows"])).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("returns false when id not in bundledIds", () => {
|
|
97
|
+
expect(computeIsBundled("pi-flows", "/res", () => true, ["pi-anthropic-messages"])).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("returns false when bundle dir missing on disk", () => {
|
|
101
|
+
expect(computeIsBundled("pi-flows", "/res", () => false, ["pi-flows"])).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns true when all conditions met", () => {
|
|
105
|
+
const exists = (p: string) => p === "/res/bundled-extensions/pi-flows";
|
|
106
|
+
expect(computeIsBundled("pi-flows", "/res", exists, ["pi-flows"])).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("enrichInstalledRow", () => {
|
|
111
|
+
const baseDeps = {
|
|
112
|
+
manifest: FAKE_MANIFEST,
|
|
113
|
+
bundledIds: ["pi-flows", "pi-anthropic-messages"],
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
it("enriches a recommended npm row with displayName and description from manifest", () => {
|
|
117
|
+
const row: RawInstalledRow = {
|
|
118
|
+
source: "npm:@tintinweb/pi-subagents",
|
|
119
|
+
scope: "user",
|
|
120
|
+
filtered: false,
|
|
121
|
+
installedPath: "/fake/path",
|
|
122
|
+
};
|
|
123
|
+
const out = enrichInstalledRow(row, {
|
|
124
|
+
...baseDeps,
|
|
125
|
+
readMeta: () => ({ version: "0.6.1", description: "Live npm desc" }),
|
|
126
|
+
existsFn: () => false,
|
|
127
|
+
resourcesPath: "/res",
|
|
128
|
+
});
|
|
129
|
+
expect(out.displayName).toBe("@tintinweb/pi-subagents");
|
|
130
|
+
// Recommended manifest description wins over package.json description.
|
|
131
|
+
expect(out.description).toBe("Sub-agents for pi.");
|
|
132
|
+
expect(out.version).toBe("0.6.1");
|
|
133
|
+
expect(out.isRecommended).toBe(true);
|
|
134
|
+
expect(out.isBundled).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("enriches a recommended git row that is bundled", () => {
|
|
138
|
+
const row: RawInstalledRow = {
|
|
139
|
+
source: "https://github.com/BlackBeltTechnology/pi-flows.git",
|
|
140
|
+
scope: "user",
|
|
141
|
+
filtered: false,
|
|
142
|
+
installedPath: "/cache/pi-flows",
|
|
143
|
+
};
|
|
144
|
+
const out = enrichInstalledRow(row, {
|
|
145
|
+
...baseDeps,
|
|
146
|
+
readMeta: () => ({ version: "0.1.0", description: "from package.json" }),
|
|
147
|
+
existsFn: (p) => p === "/res/bundled-extensions/pi-flows",
|
|
148
|
+
resourcesPath: "/res",
|
|
149
|
+
});
|
|
150
|
+
expect(out.isRecommended).toBe(true);
|
|
151
|
+
expect(out.isBundled).toBe(true);
|
|
152
|
+
expect(out.displayName).toBe("pi-flows");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("enriches a non-recommended row using basename + package.json", () => {
|
|
156
|
+
const row: RawInstalledRow = {
|
|
157
|
+
source: "/home/dev/pi-mystery",
|
|
158
|
+
scope: "user",
|
|
159
|
+
filtered: false,
|
|
160
|
+
installedPath: "/home/dev/pi-mystery",
|
|
161
|
+
};
|
|
162
|
+
const out = enrichInstalledRow(row, {
|
|
163
|
+
...baseDeps,
|
|
164
|
+
readMeta: () => ({ version: "9.9.9", description: "Mystery extension" }),
|
|
165
|
+
existsFn: () => false,
|
|
166
|
+
});
|
|
167
|
+
expect(out.isRecommended).toBe(false);
|
|
168
|
+
expect(out.isBundled).toBe(false);
|
|
169
|
+
expect(out.displayName).toBe("pi-mystery");
|
|
170
|
+
expect(out.description).toBe("Mystery extension");
|
|
171
|
+
expect(out.version).toBe("9.9.9");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("handles missing installedPath silently", () => {
|
|
175
|
+
const row: RawInstalledRow = {
|
|
176
|
+
source: "npm:@tintinweb/pi-subagents",
|
|
177
|
+
scope: "user",
|
|
178
|
+
filtered: false,
|
|
179
|
+
};
|
|
180
|
+
const out = enrichInstalledRow(row, {
|
|
181
|
+
...baseDeps,
|
|
182
|
+
readMeta: () => ({}),
|
|
183
|
+
existsFn: () => false,
|
|
184
|
+
});
|
|
185
|
+
expect(out.version).toBeUndefined();
|
|
186
|
+
// Manifest description still applies.
|
|
187
|
+
expect(out.description).toBe("Sub-agents for pi.");
|
|
188
|
+
expect(out.isRecommended).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("handles unreadable package.json silently", () => {
|
|
192
|
+
const row: RawInstalledRow = {
|
|
193
|
+
source: "/local/broken",
|
|
194
|
+
scope: "user",
|
|
195
|
+
filtered: false,
|
|
196
|
+
installedPath: "/local/broken",
|
|
197
|
+
};
|
|
198
|
+
const out = enrichInstalledRow(row, {
|
|
199
|
+
...baseDeps,
|
|
200
|
+
readMeta: () => ({}),
|
|
201
|
+
existsFn: () => false,
|
|
202
|
+
});
|
|
203
|
+
expect(out.version).toBeUndefined();
|
|
204
|
+
expect(out.description).toBeUndefined();
|
|
205
|
+
expect(out.isRecommended).toBe(false);
|
|
206
|
+
expect(out.displayName).toBe("broken");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("isBundled is always false outside Electron (no resourcesPath)", () => {
|
|
210
|
+
const row: RawInstalledRow = {
|
|
211
|
+
source: "https://github.com/BlackBeltTechnology/pi-flows.git",
|
|
212
|
+
scope: "user",
|
|
213
|
+
filtered: false,
|
|
214
|
+
installedPath: "/cache/pi-flows",
|
|
215
|
+
};
|
|
216
|
+
const out = enrichInstalledRow(row, {
|
|
217
|
+
...baseDeps,
|
|
218
|
+
readMeta: () => ({ version: "0.1.0" }),
|
|
219
|
+
existsFn: () => true,
|
|
220
|
+
resourcesPath: undefined,
|
|
221
|
+
});
|
|
222
|
+
expect(out.isRecommended).toBe(true);
|
|
223
|
+
expect(out.isBundled).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
});
|