@blackbelt-technology/pi-agent-dashboard 0.2.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/AGENTS.md +342 -0
- package/README.md +619 -0
- package/docs/architecture.md +646 -0
- package/package.json +92 -0
- package/packages/extension/package.json +33 -0
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +85 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +712 -0
- package/packages/extension/src/__tests__/connection.test.ts +344 -0
- package/packages/extension/src/__tests__/credentials-updated.test.ts +26 -0
- package/packages/extension/src/__tests__/dev-build.test.ts +79 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +89 -0
- package/packages/extension/src/__tests__/git-info.test.ts +112 -0
- package/packages/extension/src/__tests__/git-link-builder.test.ts +102 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +232 -0
- package/packages/extension/src/__tests__/openspec-poller.test.ts +119 -0
- package/packages/extension/src/__tests__/process-metrics.test.ts +47 -0
- package/packages/extension/src/__tests__/process-scanner.test.ts +202 -0
- package/packages/extension/src/__tests__/prompt-expander.test.ts +54 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +167 -0
- package/packages/extension/src/__tests__/server-launcher.test.ts +44 -0
- package/packages/extension/src/__tests__/server-probe.test.ts +25 -0
- package/packages/extension/src/__tests__/session-switch.test.ts +139 -0
- package/packages/extension/src/__tests__/session-sync.test.ts +55 -0
- package/packages/extension/src/__tests__/source-detector.test.ts +73 -0
- package/packages/extension/src/__tests__/stats-extractor.test.ts +92 -0
- package/packages/extension/src/__tests__/ui-proxy.test.ts +583 -0
- package/packages/extension/src/__tests__/watchdog.test.ts +161 -0
- package/packages/extension/src/ask-user-tool.ts +63 -0
- package/packages/extension/src/bridge-context.ts +64 -0
- package/packages/extension/src/bridge.ts +926 -0
- package/packages/extension/src/command-handler.ts +538 -0
- package/packages/extension/src/connection.ts +204 -0
- package/packages/extension/src/dev-build.ts +39 -0
- package/packages/extension/src/event-forwarder.ts +40 -0
- package/packages/extension/src/flow-event-wiring.ts +102 -0
- package/packages/extension/src/git-info.ts +65 -0
- package/packages/extension/src/git-link-builder.ts +112 -0
- package/packages/extension/src/model-tracker.ts +56 -0
- package/packages/extension/src/pi-env.d.ts +23 -0
- package/packages/extension/src/process-metrics.ts +70 -0
- package/packages/extension/src/process-scanner.ts +396 -0
- package/packages/extension/src/prompt-expander.ts +87 -0
- package/packages/extension/src/provider-register.ts +276 -0
- package/packages/extension/src/server-auto-start.ts +87 -0
- package/packages/extension/src/server-launcher.ts +82 -0
- package/packages/extension/src/server-probe.ts +33 -0
- package/packages/extension/src/session-sync.ts +154 -0
- package/packages/extension/src/source-detector.ts +26 -0
- package/packages/extension/src/ui-proxy.ts +269 -0
- package/packages/extension/tsconfig.json +11 -0
- package/packages/server/package.json +37 -0
- package/packages/server/src/__tests__/auth-plugin.test.ts +117 -0
- package/packages/server/src/__tests__/auth.test.ts +224 -0
- package/packages/server/src/__tests__/auto-attach.test.ts +246 -0
- package/packages/server/src/__tests__/auto-resume.test.ts +135 -0
- package/packages/server/src/__tests__/auto-shutdown.test.ts +136 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +104 -0
- package/packages/server/src/__tests__/bulk-archive-handler.test.ts +15 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +73 -0
- package/packages/server/src/__tests__/client-discovery.test.ts +39 -0
- package/packages/server/src/__tests__/config-api.test.ts +104 -0
- package/packages/server/src/__tests__/cors.test.ts +48 -0
- package/packages/server/src/__tests__/directory-service.test.ts +240 -0
- package/packages/server/src/__tests__/editor-detection.test.ts +60 -0
- package/packages/server/src/__tests__/editor-endpoints.test.ts +26 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +73 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +151 -0
- package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +55 -0
- package/packages/server/src/__tests__/event-status-extraction.test.ts +58 -0
- package/packages/server/src/__tests__/extension-register.test.ts +61 -0
- package/packages/server/src/__tests__/file-endpoint.test.ts +49 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +109 -0
- package/packages/server/src/__tests__/git-operations.test.ts +251 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +233 -0
- package/packages/server/src/__tests__/headless-shutdown-fallback.test.ts +109 -0
- package/packages/server/src/__tests__/health-endpoint.test.ts +35 -0
- package/packages/server/src/__tests__/heartbeat-ack.test.ts +63 -0
- package/packages/server/src/__tests__/json-store.test.ts +70 -0
- package/packages/server/src/__tests__/localhost-guard.test.ts +149 -0
- package/packages/server/src/__tests__/memory-event-store.test.ts +260 -0
- package/packages/server/src/__tests__/memory-session-manager.test.ts +80 -0
- package/packages/server/src/__tests__/meta-persistence.test.ts +107 -0
- package/packages/server/src/__tests__/migrate-persistence.test.ts +180 -0
- package/packages/server/src/__tests__/npm-search-proxy.test.ts +153 -0
- package/packages/server/src/__tests__/oauth-callback-server.test.ts +165 -0
- package/packages/server/src/__tests__/openspec-archive.test.ts +87 -0
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +163 -0
- package/packages/server/src/__tests__/package-routes.test.ts +172 -0
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +69 -0
- package/packages/server/src/__tests__/pending-load-manager.test.ts +144 -0
- package/packages/server/src/__tests__/pending-resume-registry.test.ts +130 -0
- package/packages/server/src/__tests__/pi-resource-scanner.test.ts +235 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +108 -0
- package/packages/server/src/__tests__/process-manager.test.ts +184 -0
- package/packages/server/src/__tests__/provider-auth-handlers.test.ts +93 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +143 -0
- package/packages/server/src/__tests__/provider-auth-storage.test.ts +114 -0
- package/packages/server/src/__tests__/resolve-path.test.ts +38 -0
- package/packages/server/src/__tests__/ring-buffer.test.ts +45 -0
- package/packages/server/src/__tests__/server-pid.test.ts +89 -0
- package/packages/server/src/__tests__/session-api.test.ts +244 -0
- package/packages/server/src/__tests__/session-diff.test.ts +138 -0
- package/packages/server/src/__tests__/session-file-dedup.test.ts +102 -0
- package/packages/server/src/__tests__/session-file-reader.test.ts +85 -0
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +138 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +135 -0
- package/packages/server/src/__tests__/session-ordering-integration.test.ts +102 -0
- package/packages/server/src/__tests__/session-scanner.test.ts +199 -0
- package/packages/server/src/__tests__/shutdown-endpoint.test.ts +42 -0
- package/packages/server/src/__tests__/skip-wipe.test.ts +123 -0
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +126 -0
- package/packages/server/src/__tests__/smoke-integration.test.ts +175 -0
- package/packages/server/src/__tests__/spa-fallback.test.ts +68 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +155 -0
- package/packages/server/src/__tests__/terminal-gateway.test.ts +61 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +257 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +84 -0
- package/packages/server/src/__tests__/tunnel.test.ts +206 -0
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +112 -0
- package/packages/server/src/auth-plugin.ts +302 -0
- package/packages/server/src/auth.ts +323 -0
- package/packages/server/src/browse.ts +55 -0
- package/packages/server/src/browser-gateway.ts +495 -0
- package/packages/server/src/browser-handlers/directory-handler.ts +137 -0
- package/packages/server/src/browser-handlers/handler-context.ts +45 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +271 -0
- package/packages/server/src/browser-handlers/session-meta-handler.ts +95 -0
- package/packages/server/src/browser-handlers/subscription-handler.ts +154 -0
- package/packages/server/src/browser-handlers/terminal-handler.ts +37 -0
- package/packages/server/src/cli.ts +347 -0
- package/packages/server/src/config-api.ts +130 -0
- package/packages/server/src/directory-service.ts +162 -0
- package/packages/server/src/editor-detection.ts +60 -0
- package/packages/server/src/editor-manager.ts +352 -0
- package/packages/server/src/editor-proxy.ts +134 -0
- package/packages/server/src/editor-registry.ts +108 -0
- package/packages/server/src/event-status-extraction.ts +131 -0
- package/packages/server/src/event-wiring.ts +589 -0
- package/packages/server/src/extension-register.ts +92 -0
- package/packages/server/src/git-operations.ts +200 -0
- package/packages/server/src/headless-pid-registry.ts +207 -0
- package/packages/server/src/idle-timer.ts +61 -0
- package/packages/server/src/json-store.ts +32 -0
- package/packages/server/src/localhost-guard.ts +117 -0
- package/packages/server/src/memory-event-store.ts +193 -0
- package/packages/server/src/memory-session-manager.ts +123 -0
- package/packages/server/src/meta-persistence.ts +64 -0
- package/packages/server/src/migrate-persistence.ts +195 -0
- package/packages/server/src/npm-search-proxy.ts +143 -0
- package/packages/server/src/oauth-callback-server.ts +177 -0
- package/packages/server/src/openspec-archive.ts +60 -0
- package/packages/server/src/package-manager-wrapper.ts +200 -0
- package/packages/server/src/pending-fork-registry.ts +53 -0
- package/packages/server/src/pending-load-manager.ts +110 -0
- package/packages/server/src/pending-resume-registry.ts +69 -0
- package/packages/server/src/pi-gateway.ts +419 -0
- package/packages/server/src/pi-resource-scanner.ts +369 -0
- package/packages/server/src/preferences-store.ts +116 -0
- package/packages/server/src/process-manager.ts +311 -0
- package/packages/server/src/provider-auth-handlers.ts +438 -0
- package/packages/server/src/provider-auth-storage.ts +200 -0
- package/packages/server/src/resolve-path.ts +12 -0
- package/packages/server/src/routes/editor-routes.ts +86 -0
- package/packages/server/src/routes/file-routes.ts +116 -0
- package/packages/server/src/routes/git-routes.ts +89 -0
- package/packages/server/src/routes/openspec-routes.ts +99 -0
- package/packages/server/src/routes/package-routes.ts +172 -0
- package/packages/server/src/routes/provider-auth-routes.ts +244 -0
- package/packages/server/src/routes/provider-routes.ts +101 -0
- package/packages/server/src/routes/route-deps.ts +23 -0
- package/packages/server/src/routes/session-routes.ts +91 -0
- package/packages/server/src/routes/system-routes.ts +271 -0
- package/packages/server/src/server-pid.ts +84 -0
- package/packages/server/src/server.ts +554 -0
- package/packages/server/src/session-api.ts +330 -0
- package/packages/server/src/session-bootstrap.ts +80 -0
- package/packages/server/src/session-diff.ts +178 -0
- package/packages/server/src/session-discovery.ts +134 -0
- package/packages/server/src/session-file-reader.ts +135 -0
- package/packages/server/src/session-order-manager.ts +73 -0
- package/packages/server/src/session-scanner.ts +233 -0
- package/packages/server/src/session-stats-reader.ts +99 -0
- package/packages/server/src/terminal-gateway.ts +51 -0
- package/packages/server/src/terminal-manager.ts +241 -0
- package/packages/server/src/tunnel.ts +329 -0
- package/packages/server/tsconfig.json +11 -0
- package/packages/shared/package.json +15 -0
- package/packages/shared/src/__tests__/config.test.ts +358 -0
- package/packages/shared/src/__tests__/deriveChangeState.test.ts +95 -0
- package/packages/shared/src/__tests__/mdns-discovery.test.ts +80 -0
- package/packages/shared/src/__tests__/protocol.test.ts +243 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +17 -0
- package/packages/shared/src/__tests__/server-identity.test.ts +73 -0
- package/packages/shared/src/__tests__/session-meta.test.ts +125 -0
- package/packages/shared/src/archive-types.ts +11 -0
- package/packages/shared/src/browser-protocol.ts +534 -0
- package/packages/shared/src/config.ts +245 -0
- package/packages/shared/src/diff-types.ts +41 -0
- package/packages/shared/src/editor-types.ts +18 -0
- package/packages/shared/src/mdns-discovery.ts +248 -0
- package/packages/shared/src/openspec-activity-detector.ts +109 -0
- package/packages/shared/src/openspec-poller.ts +96 -0
- package/packages/shared/src/protocol.ts +369 -0
- package/packages/shared/src/resolve-jiti.ts +43 -0
- package/packages/shared/src/rest-api.ts +255 -0
- package/packages/shared/src/server-identity.ts +51 -0
- package/packages/shared/src/session-meta.ts +86 -0
- package/packages/shared/src/state-replay.ts +174 -0
- package/packages/shared/src/stats-extractor.ts +54 -0
- package/packages/shared/src/terminal-types.ts +18 -0
- package/packages/shared/src/types.ts +351 -0
- package/packages/shared/tsconfig.json +8 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CLI argument parsing.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from "vitest";
|
|
5
|
+
import { parseArgs } from "../cli.js";
|
|
6
|
+
|
|
7
|
+
describe("parseArgs", () => {
|
|
8
|
+
it("returns null subcommand with no args", () => {
|
|
9
|
+
const result = parseArgs([]);
|
|
10
|
+
expect(result.subcommand).toBeNull();
|
|
11
|
+
expect(result.flags).toEqual({});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("parses start subcommand", () => {
|
|
15
|
+
const result = parseArgs(["start"]);
|
|
16
|
+
expect(result.subcommand).toBe("start");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("parses stop subcommand", () => {
|
|
20
|
+
const result = parseArgs(["stop"]);
|
|
21
|
+
expect(result.subcommand).toBe("stop");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("parses restart subcommand", () => {
|
|
25
|
+
const result = parseArgs(["restart"]);
|
|
26
|
+
expect(result.subcommand).toBe("restart");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("parses status subcommand", () => {
|
|
30
|
+
const result = parseArgs(["status"]);
|
|
31
|
+
expect(result.subcommand).toBe("status");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("parses subcommand with flags", () => {
|
|
35
|
+
const result = parseArgs(["start", "--port", "3000", "--pi-port", "4000"]);
|
|
36
|
+
expect(result.subcommand).toBe("start");
|
|
37
|
+
expect(result.flags.port).toBe(3000);
|
|
38
|
+
expect(result.flags.piPort).toBe(4000);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("parses flags without subcommand (foreground mode)", () => {
|
|
42
|
+
const result = parseArgs(["--port", "3000", "--dev"]);
|
|
43
|
+
expect(result.subcommand).toBeNull();
|
|
44
|
+
expect(result.flags.port).toBe(3000);
|
|
45
|
+
expect(result.flags.dev).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("parses --no-tunnel flag", () => {
|
|
49
|
+
const result = parseArgs(["start", "--no-tunnel"]);
|
|
50
|
+
expect(result.subcommand).toBe("start");
|
|
51
|
+
expect(result.flags.tunnel).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("ignores unknown args", () => {
|
|
55
|
+
const result = parseArgs(["start", "--unknown", "value"]);
|
|
56
|
+
expect(result.subcommand).toBe("start");
|
|
57
|
+
expect(result.flags).toEqual({});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("does not treat flag values as subcommands", () => {
|
|
61
|
+
const result = parseArgs(["--port", "3000"]);
|
|
62
|
+
expect(result.subcommand).toBeNull();
|
|
63
|
+
expect(result.flags.port).toBe(3000);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("daemon spawn jiti resolution", () => {
|
|
68
|
+
it("resolveJitiImport throws outside of pi context", async () => {
|
|
69
|
+
// In test context (no pi/jiti loader), peer deps aren't resolvable
|
|
70
|
+
const { resolveJitiImport } = await import("@blackbelt-technology/pi-dashboard-shared/resolve-jiti.js");
|
|
71
|
+
expect(() => resolveJitiImport()).toThrow("Cannot find pi's TypeScript loader");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests the client static file discovery order.
|
|
7
|
+
* Replicates the search logic from server.ts.
|
|
8
|
+
*/
|
|
9
|
+
function findClientDir(serverDir: string): string {
|
|
10
|
+
const searchPaths = [
|
|
11
|
+
path.join(serverDir, "../../node_modules/@blackbelt-technology/pi-dashboard-web/dist"),
|
|
12
|
+
path.join(serverDir, "../../client/dist"),
|
|
13
|
+
path.join(serverDir, "../../dist/client"),
|
|
14
|
+
];
|
|
15
|
+
return searchPaths.find(p => existsSync(path.join(p, "index.html"))) ?? "";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("client static file discovery", () => {
|
|
19
|
+
it("returns empty string when no client build exists", () => {
|
|
20
|
+
// Use a path that definitely doesn't have client builds
|
|
21
|
+
expect(findClientDir("/tmp/nonexistent-server-dir")).toBe("");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("searches npm package path first", () => {
|
|
25
|
+
// This is a structural test — verifies search order
|
|
26
|
+
const serverDir = "/fake/packages/server/src";
|
|
27
|
+
const searchPaths = [
|
|
28
|
+
path.join(serverDir, "../../node_modules/@blackbelt-technology/pi-dashboard-web/dist"),
|
|
29
|
+
path.join(serverDir, "../../client/dist"),
|
|
30
|
+
path.join(serverDir, "../../dist/client"),
|
|
31
|
+
];
|
|
32
|
+
// npm package path should be first
|
|
33
|
+
expect(searchPaths[0]).toContain("pi-dashboard-web/dist");
|
|
34
|
+
// workspace sibling second
|
|
35
|
+
expect(searchPaths[1]).toContain("client/dist");
|
|
36
|
+
// legacy third
|
|
37
|
+
expect(searchPaths[2]).toContain("dist/client");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { readConfigRedacted, writeConfigPartial } from "../config-api.js";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
|
|
7
|
+
describe("config-api", () => {
|
|
8
|
+
let testDir: string;
|
|
9
|
+
let configFile: string;
|
|
10
|
+
let origHome: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
testDir = path.join(os.tmpdir(), `test-config-api-${Date.now()}`);
|
|
14
|
+
fs.mkdirSync(path.join(testDir, ".pi", "dashboard"), { recursive: true });
|
|
15
|
+
configFile = path.join(testDir, ".pi", "dashboard", "config.json");
|
|
16
|
+
origHome = process.env.HOME!;
|
|
17
|
+
process.env.HOME = testDir;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
process.env.HOME = origHome;
|
|
22
|
+
if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("readConfigRedacted", () => {
|
|
26
|
+
it("should redact auth.secret and provider clientSecrets", () => {
|
|
27
|
+
fs.writeFileSync(configFile, JSON.stringify({
|
|
28
|
+
port: 8000,
|
|
29
|
+
auth: {
|
|
30
|
+
secret: "real-secret",
|
|
31
|
+
providers: {
|
|
32
|
+
github: { clientId: "id1", clientSecret: "real-client-secret" },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
const config = readConfigRedacted();
|
|
37
|
+
expect(config.auth!.secret).toBe("***");
|
|
38
|
+
expect(config.auth!.providers.github.clientSecret).toBe("***");
|
|
39
|
+
expect(config.auth!.providers.github.clientId).toBe("id1");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should return config without auth when not configured", () => {
|
|
43
|
+
fs.writeFileSync(configFile, JSON.stringify({ port: 3000 }));
|
|
44
|
+
const config = readConfigRedacted();
|
|
45
|
+
expect(config.auth).toBeUndefined();
|
|
46
|
+
expect(config.port).toBe(3000);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("writeConfigPartial", () => {
|
|
51
|
+
it("should merge partial config and write to disk", () => {
|
|
52
|
+
fs.writeFileSync(configFile, JSON.stringify({ port: 8000, autoShutdown: true }));
|
|
53
|
+
const result = writeConfigPartial({ autoShutdown: false });
|
|
54
|
+
expect(result.success).toBe(true);
|
|
55
|
+
expect(result.restartRequired).toBe(false);
|
|
56
|
+
const written = JSON.parse(fs.readFileSync(configFile, "utf-8"));
|
|
57
|
+
expect(written.autoShutdown).toBe(false);
|
|
58
|
+
expect(written.port).toBe(8000); // preserved
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should flag restartRequired when port changes", () => {
|
|
62
|
+
fs.writeFileSync(configFile, JSON.stringify({ port: 8000 }));
|
|
63
|
+
const result = writeConfigPartial({ port: 9000 });
|
|
64
|
+
expect(result.success).toBe(true);
|
|
65
|
+
expect(result.restartRequired).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should flag restartRequired when piPort changes", () => {
|
|
69
|
+
fs.writeFileSync(configFile, JSON.stringify({ piPort: 9999 }));
|
|
70
|
+
const result = writeConfigPartial({ piPort: 8888 });
|
|
71
|
+
expect(result.restartRequired).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should not flag restartRequired for non-port changes", () => {
|
|
75
|
+
fs.writeFileSync(configFile, JSON.stringify({ port: 8000, autoShutdown: true }));
|
|
76
|
+
const result = writeConfigPartial({ autoShutdown: false, shutdownIdleSeconds: 60 });
|
|
77
|
+
expect(result.restartRequired).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should preserve redacted auth.secret", () => {
|
|
81
|
+
fs.writeFileSync(configFile, JSON.stringify({
|
|
82
|
+
auth: { secret: "real-secret", providers: { github: { clientId: "id", clientSecret: "real-cs" } } },
|
|
83
|
+
}));
|
|
84
|
+
const result = writeConfigPartial({
|
|
85
|
+
auth: { secret: "***", providers: { github: { clientId: "new-id", clientSecret: "***" } } },
|
|
86
|
+
});
|
|
87
|
+
expect(result.success).toBe(true);
|
|
88
|
+
const written = JSON.parse(fs.readFileSync(configFile, "utf-8"));
|
|
89
|
+
expect(written.auth.secret).toBe("real-secret");
|
|
90
|
+
expect(written.auth.providers.github.clientSecret).toBe("real-cs");
|
|
91
|
+
expect(written.auth.providers.github.clientId).toBe("new-id");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should update allowedUsers", () => {
|
|
95
|
+
fs.writeFileSync(configFile, JSON.stringify({ auth: { providers: { github: { clientId: "x", clientSecret: "y" } } } }));
|
|
96
|
+
const result = writeConfigPartial({ auth: { allowedUsers: ["octocat", "*@company.com"] } });
|
|
97
|
+
expect(result.success).toBe(true);
|
|
98
|
+
const written = JSON.parse(fs.readFileSync(configFile, "utf-8"));
|
|
99
|
+
expect(written.auth.allowedUsers).toEqual(["octocat", "*@company.com"]);
|
|
100
|
+
// providers preserved
|
|
101
|
+
expect(written.auth.providers.github.clientId).toBe("x");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unit tests for CORS origin validation logic.
|
|
5
|
+
* Tests the same logic used in server.ts CORS callback.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function isAllowedOrigin(origin: string | undefined, configuredOrigins: string[]): boolean {
|
|
9
|
+
if (!origin) return true;
|
|
10
|
+
try {
|
|
11
|
+
const u = new URL(origin);
|
|
12
|
+
const host = u.hostname;
|
|
13
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1") {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
} catch { /* ignore */ }
|
|
17
|
+
return configuredOrigins.includes(origin);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("CORS origin validation", () => {
|
|
21
|
+
it("allows requests with no origin (same-origin)", () => {
|
|
22
|
+
expect(isAllowedOrigin(undefined, [])).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("allows localhost on any port", () => {
|
|
26
|
+
expect(isAllowedOrigin("http://localhost:3000", [])).toBe(true);
|
|
27
|
+
expect(isAllowedOrigin("http://localhost:5173", [])).toBe(true);
|
|
28
|
+
expect(isAllowedOrigin("https://localhost:8443", [])).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("allows 127.0.0.1 on any port", () => {
|
|
32
|
+
expect(isAllowedOrigin("http://127.0.0.1:3000", [])).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("allows configured origins", () => {
|
|
36
|
+
const configured = ["https://dashboard.example.com"];
|
|
37
|
+
expect(isAllowedOrigin("https://dashboard.example.com", configured)).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("rejects unknown origins", () => {
|
|
41
|
+
expect(isAllowedOrigin("https://evil.example.com", [])).toBe(false);
|
|
42
|
+
expect(isAllowedOrigin("https://evil.example.com", ["https://good.example.com"])).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("rejects non-localhost remote origins without config", () => {
|
|
46
|
+
expect(isAllowedOrigin("http://192.168.1.100:3000", [])).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for DirectoryService - server-side directory-scoped operations.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
5
|
+
import { createDirectoryService, type DirectoryService } from "../directory-service.js";
|
|
6
|
+
import type { PreferencesStore } from "../preferences-store.js";
|
|
7
|
+
import type { SessionManager } from "../memory-session-manager.js";
|
|
8
|
+
import type { DashboardSession } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
9
|
+
|
|
10
|
+
// Mock the shared openspec poller
|
|
11
|
+
vi.mock("@blackbelt-technology/pi-dashboard-shared/openspec-poller.js", () => ({
|
|
12
|
+
pollOpenSpecAsync: vi.fn(async () => ({ initialized: false, changes: [] })),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// Mock the shared state replay
|
|
16
|
+
vi.mock("@blackbelt-technology/pi-dashboard-shared/state-replay.js", () => ({
|
|
17
|
+
replayEntriesAsEvents: vi.fn(() => []),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
// Mock session-discovery
|
|
21
|
+
vi.mock("../session-discovery.js", () => ({
|
|
22
|
+
discoverSessionsForCwd: vi.fn(() => []),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
// Mock session-file-reader
|
|
26
|
+
vi.mock("../session-file-reader.js", () => ({
|
|
27
|
+
loadSessionEntries: vi.fn(() => []),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// Mock the pi-coding-agent SessionManager (legacy, kept for compatibility)
|
|
31
|
+
vi.mock("@mariozechner/pi-coding-agent", () => ({
|
|
32
|
+
SessionManager: {
|
|
33
|
+
list: vi.fn(async () => []),
|
|
34
|
+
open: vi.fn(() => ({
|
|
35
|
+
getBranch: vi.fn(() => []),
|
|
36
|
+
})),
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
function createMockPreferencesStore(pinnedDirs: string[] = []): PreferencesStore {
|
|
41
|
+
return {
|
|
42
|
+
getPinnedDirectories: () => pinnedDirs,
|
|
43
|
+
getSessionOrder: () => ({}),
|
|
44
|
+
setSessionOrder: vi.fn(),
|
|
45
|
+
setPinnedDirectories: vi.fn(),
|
|
46
|
+
pinDirectory: vi.fn(),
|
|
47
|
+
unpinDirectory: vi.fn(),
|
|
48
|
+
reorderPinnedDirs: vi.fn(),
|
|
49
|
+
flush: vi.fn(),
|
|
50
|
+
dispose: vi.fn(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createMockSessionManager(sessions: DashboardSession[] = []): SessionManager {
|
|
55
|
+
const map = new Map<string, DashboardSession>();
|
|
56
|
+
for (const s of sessions) map.set(s.id, s);
|
|
57
|
+
return {
|
|
58
|
+
register: vi.fn((params) => {
|
|
59
|
+
const session = { ...params, status: "active", startedAt: Date.now() } as DashboardSession;
|
|
60
|
+
map.set(params.id, session);
|
|
61
|
+
return session;
|
|
62
|
+
}),
|
|
63
|
+
restore: vi.fn(),
|
|
64
|
+
unregister: vi.fn((id) => {
|
|
65
|
+
const s = map.get(id);
|
|
66
|
+
if (s) { s.status = "ended"; s.endedAt = Date.now(); }
|
|
67
|
+
}),
|
|
68
|
+
update: vi.fn(),
|
|
69
|
+
get: (id) => map.get(id),
|
|
70
|
+
listActive: () => Array.from(map.values()).filter((s) => s.status !== "ended"),
|
|
71
|
+
listAll: () => Array.from(map.values()),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("DirectoryService", () => {
|
|
76
|
+
let service: DirectoryService;
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
service?.stopPolling();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("knownDirectories", () => {
|
|
83
|
+
it("returns union of pinned dirs and session cwds", () => {
|
|
84
|
+
const stateStore = createMockPreferencesStore(["/pinned/a", "/pinned/b"]);
|
|
85
|
+
const sessionManager = createMockSessionManager([
|
|
86
|
+
{ id: "s1", cwd: "/pinned/a", source: "tui", status: "active", startedAt: 1 } as DashboardSession,
|
|
87
|
+
{ id: "s2", cwd: "/project/c", source: "tui", status: "active", startedAt: 2 } as DashboardSession,
|
|
88
|
+
]);
|
|
89
|
+
service = createDirectoryService(stateStore, sessionManager);
|
|
90
|
+
|
|
91
|
+
const dirs = service.knownDirectories();
|
|
92
|
+
expect(dirs).toContain("/pinned/a");
|
|
93
|
+
expect(dirs).toContain("/pinned/b");
|
|
94
|
+
expect(dirs).toContain("/project/c");
|
|
95
|
+
expect(dirs.length).toBe(3);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("deduplicates directories", () => {
|
|
99
|
+
const stateStore = createMockPreferencesStore(["/same/dir"]);
|
|
100
|
+
const sessionManager = createMockSessionManager([
|
|
101
|
+
{ id: "s1", cwd: "/same/dir", source: "tui", status: "active", startedAt: 1 } as DashboardSession,
|
|
102
|
+
]);
|
|
103
|
+
service = createDirectoryService(stateStore, sessionManager);
|
|
104
|
+
|
|
105
|
+
const dirs = service.knownDirectories();
|
|
106
|
+
expect(dirs.filter((d) => d === "/same/dir").length).toBe(1);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("discoverSessions", () => {
|
|
111
|
+
it("calls discoverSessionsForCwd and returns metadata", async () => {
|
|
112
|
+
const { discoverSessionsForCwd } = await import("../session-discovery.js");
|
|
113
|
+
(discoverSessionsForCwd as any).mockReturnValueOnce([
|
|
114
|
+
{
|
|
115
|
+
id: "hist-1",
|
|
116
|
+
cwd: "/project",
|
|
117
|
+
name: "old session",
|
|
118
|
+
startedAt: Date.now(),
|
|
119
|
+
modifiedAt: Date.now(),
|
|
120
|
+
firstMessage: "hello",
|
|
121
|
+
sessionFile: "/project/.pi/sessions/hist-1.jsonl",
|
|
122
|
+
sessionDir: "/project/.pi/sessions",
|
|
123
|
+
},
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
const stateStore = createMockPreferencesStore();
|
|
127
|
+
const sessionManager = createMockSessionManager();
|
|
128
|
+
service = createDirectoryService(stateStore, sessionManager);
|
|
129
|
+
|
|
130
|
+
const sessions = service.discoverSessions("/project");
|
|
131
|
+
expect(sessions).toHaveLength(1);
|
|
132
|
+
expect(sessions[0].id).toBe("hist-1");
|
|
133
|
+
expect(sessions[0].cwd).toBe("/project");
|
|
134
|
+
expect(sessions[0].sessionFile).toBe("/project/.pi/sessions/hist-1.jsonl");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("returns empty array when no sessions found", async () => {
|
|
138
|
+
const { discoverSessionsForCwd } = await import("../session-discovery.js");
|
|
139
|
+
(discoverSessionsForCwd as any).mockReturnValueOnce([]);
|
|
140
|
+
|
|
141
|
+
const stateStore = createMockPreferencesStore();
|
|
142
|
+
const sessionManager = createMockSessionManager();
|
|
143
|
+
service = createDirectoryService(stateStore, sessionManager);
|
|
144
|
+
|
|
145
|
+
const sessions = service.discoverSessions("/nonexistent");
|
|
146
|
+
expect(sessions).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("loadSessionEvents", () => {
|
|
151
|
+
it("loads and converts session entries", async () => {
|
|
152
|
+
const { loadSessionEntries } = await import("../session-file-reader.js");
|
|
153
|
+
const { replayEntriesAsEvents } = await import("@blackbelt-technology/pi-dashboard-shared/state-replay.js");
|
|
154
|
+
|
|
155
|
+
const mockEntries = [{ type: "message", message: { role: "user", content: "hi" } }];
|
|
156
|
+
(loadSessionEntries as any).mockReturnValueOnce(mockEntries);
|
|
157
|
+
(replayEntriesAsEvents as any).mockReturnValueOnce([
|
|
158
|
+
{ type: "event_forward", sessionId: "s1", event: { eventType: "message_start", timestamp: 1, data: {} } },
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
const stateStore = createMockPreferencesStore();
|
|
162
|
+
const sessionManager = createMockSessionManager();
|
|
163
|
+
service = createDirectoryService(stateStore, sessionManager);
|
|
164
|
+
|
|
165
|
+
const result = await service.loadSessionEvents("s1", "/path/to/session.jsonl");
|
|
166
|
+
expect(result.success).toBe(true);
|
|
167
|
+
expect(result.events).toHaveLength(1);
|
|
168
|
+
expect(loadSessionEntries).toHaveBeenCalledWith("/path/to/session.jsonl");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("returns error on missing file", async () => {
|
|
172
|
+
const { loadSessionEntries } = await import("../session-file-reader.js");
|
|
173
|
+
(loadSessionEntries as any).mockImplementationOnce(() => { throw Object.assign(new Error("not found"), { code: "ENOENT" }); });
|
|
174
|
+
|
|
175
|
+
const stateStore = createMockPreferencesStore();
|
|
176
|
+
const sessionManager = createMockSessionManager();
|
|
177
|
+
service = createDirectoryService(stateStore, sessionManager);
|
|
178
|
+
|
|
179
|
+
const result = await service.loadSessionEvents("s1", "/missing.jsonl");
|
|
180
|
+
expect(result.success).toBe(false);
|
|
181
|
+
expect(result.error).toBe("file_not_found");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("getOpenSpecData / refreshOpenSpec", () => {
|
|
186
|
+
it("returns cached data after polling", async () => {
|
|
187
|
+
const { pollOpenSpecAsync } = await import("@blackbelt-technology/pi-dashboard-shared/openspec-poller.js");
|
|
188
|
+
(pollOpenSpecAsync as any).mockResolvedValue({ initialized: true, changes: [{ name: "change-1" }] });
|
|
189
|
+
|
|
190
|
+
const stateStore = createMockPreferencesStore();
|
|
191
|
+
const sessionManager = createMockSessionManager();
|
|
192
|
+
service = createDirectoryService(stateStore, sessionManager);
|
|
193
|
+
|
|
194
|
+
const data = await service.refreshOpenSpec("/project");
|
|
195
|
+
expect(data.initialized).toBe(true);
|
|
196
|
+
expect(data.changes[0].name).toBe("change-1");
|
|
197
|
+
|
|
198
|
+
// getOpenSpecData returns cached value
|
|
199
|
+
const cached = service.getOpenSpecData("/project");
|
|
200
|
+
expect(cached).toEqual(data);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("onDirectoryAdded", () => {
|
|
205
|
+
it("discovers sessions and polls openspec immediately", async () => {
|
|
206
|
+
const { discoverSessionsForCwd } = await import("../session-discovery.js");
|
|
207
|
+
const { pollOpenSpecAsync } = await import("@blackbelt-technology/pi-dashboard-shared/openspec-poller.js");
|
|
208
|
+
|
|
209
|
+
(discoverSessionsForCwd as any).mockReturnValueOnce([]);
|
|
210
|
+
(pollOpenSpecAsync as any).mockResolvedValue({ initialized: false, changes: [] });
|
|
211
|
+
|
|
212
|
+
const stateStore = createMockPreferencesStore();
|
|
213
|
+
const sessionManager = createMockSessionManager();
|
|
214
|
+
service = createDirectoryService(stateStore, sessionManager);
|
|
215
|
+
|
|
216
|
+
const result = await service.onDirectoryAdded("/new/dir");
|
|
217
|
+
expect(discoverSessionsForCwd).toHaveBeenCalledWith("/new/dir");
|
|
218
|
+
expect(result.sessions).toEqual([]);
|
|
219
|
+
expect(result.openspecData).toBeDefined();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("polling", () => {
|
|
224
|
+
it("startPolling and stopPolling control the timer", () => {
|
|
225
|
+
vi.useFakeTimers();
|
|
226
|
+
const stateStore = createMockPreferencesStore(["/project"]);
|
|
227
|
+
const sessionManager = createMockSessionManager();
|
|
228
|
+
service = createDirectoryService(stateStore, sessionManager);
|
|
229
|
+
|
|
230
|
+
const onChange = vi.fn();
|
|
231
|
+
service.startPolling(onChange);
|
|
232
|
+
|
|
233
|
+
// Should not have fired yet
|
|
234
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
235
|
+
|
|
236
|
+
service.stopPolling();
|
|
237
|
+
vi.useRealTimers();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { detectCodeServerBinary, resetDetectionCache } from "../editor-detection.js";
|
|
3
|
+
import type { EditorConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CONFIG: EditorConfig = { idleTimeoutMinutes: 10, maxInstances: 3 };
|
|
6
|
+
|
|
7
|
+
describe("detectCodeServerBinary", () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
resetDetectionCache();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("returns config override when binary is set", () => {
|
|
13
|
+
const whichFn = vi.fn();
|
|
14
|
+
const result = detectCodeServerBinary({ ...DEFAULT_CONFIG, binary: "/opt/code-server" }, whichFn);
|
|
15
|
+
expect(result).toEqual({ available: true, binary: "/opt/code-server" });
|
|
16
|
+
expect(whichFn).not.toHaveBeenCalled();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("detects code-server on PATH", () => {
|
|
20
|
+
const whichFn = vi.fn().mockReturnValueOnce("/usr/local/bin/code-server");
|
|
21
|
+
const result = detectCodeServerBinary(DEFAULT_CONFIG, whichFn);
|
|
22
|
+
expect(result).toEqual({ available: true, binary: "/usr/local/bin/code-server" });
|
|
23
|
+
expect(whichFn).toHaveBeenCalledWith("code-server");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("falls back to openvscode-server", () => {
|
|
27
|
+
const whichFn = vi.fn()
|
|
28
|
+
.mockReturnValueOnce(null)
|
|
29
|
+
.mockReturnValueOnce("/usr/bin/openvscode-server");
|
|
30
|
+
const result = detectCodeServerBinary(DEFAULT_CONFIG, whichFn);
|
|
31
|
+
expect(result).toEqual({ available: true, binary: "/usr/bin/openvscode-server" });
|
|
32
|
+
expect(whichFn).toHaveBeenCalledTimes(2);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("returns not available when nothing found", () => {
|
|
36
|
+
const whichFn = vi.fn().mockReturnValue(null);
|
|
37
|
+
const result = detectCodeServerBinary(DEFAULT_CONFIG, whichFn);
|
|
38
|
+
expect(result).toEqual({ available: false });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("caches result across calls", () => {
|
|
42
|
+
const whichFn = vi.fn().mockReturnValue("/usr/local/bin/code-server");
|
|
43
|
+
detectCodeServerBinary(DEFAULT_CONFIG, whichFn);
|
|
44
|
+
detectCodeServerBinary(DEFAULT_CONFIG, whichFn);
|
|
45
|
+
// Only called once due to caching
|
|
46
|
+
expect(whichFn).toHaveBeenCalledTimes(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("re-detects after cache reset", () => {
|
|
50
|
+
const whichFn1 = vi.fn().mockReturnValue("/usr/local/bin/code-server");
|
|
51
|
+
const first = detectCodeServerBinary(DEFAULT_CONFIG, whichFn1);
|
|
52
|
+
expect(first.available).toBe(true);
|
|
53
|
+
|
|
54
|
+
resetDetectionCache();
|
|
55
|
+
|
|
56
|
+
const whichFn2 = vi.fn().mockReturnValue(null);
|
|
57
|
+
const second = detectCodeServerBinary(DEFAULT_CONFIG, whichFn2);
|
|
58
|
+
expect(second).toEqual({ available: false });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { isLoopback } from "../localhost-guard.js";
|
|
3
|
+
|
|
4
|
+
// Unit tests for localhost guard
|
|
5
|
+
describe("isLoopback", () => {
|
|
6
|
+
it("should accept 127.0.0.1", () => {
|
|
7
|
+
expect(isLoopback("127.0.0.1")).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("should accept ::1", () => {
|
|
11
|
+
expect(isLoopback("::1")).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should accept ::ffff:127.0.0.1", () => {
|
|
15
|
+
expect(isLoopback("::ffff:127.0.0.1")).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should reject remote IPs", () => {
|
|
19
|
+
expect(isLoopback("192.168.1.1")).toBe(false);
|
|
20
|
+
expect(isLoopback("10.0.0.1")).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Unit tests for detectEditors are in editor-registry.test.ts
|
|
25
|
+
// Integration tests for the full endpoints would require spinning up the server,
|
|
26
|
+
// which is covered by manual testing. Here we test the core logic units.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { createEditorManager, allocatePort } from "../editor-manager.js";
|
|
3
|
+
import type { EditorConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
4
|
+
import type { EditorDetectionResult } from "@blackbelt-technology/pi-dashboard-shared/editor-types.js";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CONFIG: EditorConfig = { idleTimeoutMinutes: 10, maxInstances: 3 };
|
|
7
|
+
const DETECTED: EditorDetectionResult = { available: true, binary: "/usr/local/bin/code-server" };
|
|
8
|
+
const NOT_DETECTED: EditorDetectionResult = { available: false };
|
|
9
|
+
|
|
10
|
+
describe("allocatePort", () => {
|
|
11
|
+
it("returns a positive port number", async () => {
|
|
12
|
+
const port = await allocatePort();
|
|
13
|
+
expect(port).toBeGreaterThan(0);
|
|
14
|
+
expect(port).toBeLessThan(65536);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("createEditorManager", () => {
|
|
19
|
+
it("creates a manager with empty instance list", () => {
|
|
20
|
+
const mgr = createEditorManager({ config: DEFAULT_CONFIG, detection: DETECTED });
|
|
21
|
+
expect(mgr.list()).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("start throws when binary not found", async () => {
|
|
25
|
+
const mgr = createEditorManager({ config: DEFAULT_CONFIG, detection: NOT_DETECTED, allowRedetection: false });
|
|
26
|
+
await expect(mgr.start("/tmp/test")).rejects.toThrow("binary_not_found");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("get returns undefined for unknown id", () => {
|
|
30
|
+
const mgr = createEditorManager({ config: DEFAULT_CONFIG, detection: DETECTED });
|
|
31
|
+
expect(mgr.get("nonexistent")).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("getByFolder returns undefined for unknown folder", () => {
|
|
35
|
+
const mgr = createEditorManager({ config: DEFAULT_CONFIG, detection: DETECTED });
|
|
36
|
+
expect(mgr.getByFolder("/unknown")).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("stop on unknown id does not throw", () => {
|
|
40
|
+
const mgr = createEditorManager({ config: DEFAULT_CONFIG, detection: DETECTED });
|
|
41
|
+
expect(() => mgr.stop("nonexistent")).not.toThrow();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("stopAll on empty manager does not throw", () => {
|
|
45
|
+
const mgr = createEditorManager({ config: DEFAULT_CONFIG, detection: DETECTED });
|
|
46
|
+
expect(() => mgr.stopAll()).not.toThrow();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("heartbeat on unknown id does not throw", () => {
|
|
50
|
+
const mgr = createEditorManager({ config: DEFAULT_CONFIG, detection: DETECTED });
|
|
51
|
+
expect(() => mgr.heartbeat("nonexistent")).not.toThrow();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("enforces max instances when all would exceed cap", async () => {
|
|
55
|
+
const config: EditorConfig = { idleTimeoutMinutes: 10, maxInstances: 0 };
|
|
56
|
+
const mgr = createEditorManager({ config, detection: DETECTED });
|
|
57
|
+
// maxInstances=0 means no instances allowed, but eviction has nothing to evict
|
|
58
|
+
await expect(mgr.start("/tmp/test")).rejects.toThrow("max_instances_reached");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("calls onStatusChange callback", async () => {
|
|
62
|
+
const statusChanges: Array<{ cwd: string; id: string; status: string }> = [];
|
|
63
|
+
const mgr = createEditorManager({
|
|
64
|
+
config: DEFAULT_CONFIG,
|
|
65
|
+
detection: NOT_DETECTED,
|
|
66
|
+
allowRedetection: false,
|
|
67
|
+
onStatusChange: (cwd, id, status) => statusChanges.push({ cwd, id, status }),
|
|
68
|
+
});
|
|
69
|
+
// binary_not_found throws before any status change
|
|
70
|
+
await expect(mgr.start("/tmp")).rejects.toThrow("binary_not_found");
|
|
71
|
+
expect(statusChanges).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
});
|