@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,70 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { readJsonFile, writeJsonFile } from "../json-store.js";
|
|
6
|
+
|
|
7
|
+
describe("json-store", () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "json-store-test-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("readJsonFile", () => {
|
|
19
|
+
it("returns fallback when file does not exist", () => {
|
|
20
|
+
const result = readJsonFile(path.join(tmpDir, "missing.json"), { x: 1 });
|
|
21
|
+
expect(result).toEqual({ x: 1 });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns fallback for empty file", () => {
|
|
25
|
+
const fp = path.join(tmpDir, "empty.json");
|
|
26
|
+
fs.writeFileSync(fp, "");
|
|
27
|
+
expect(readJsonFile(fp, [])).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns fallback for invalid JSON", () => {
|
|
31
|
+
const fp = path.join(tmpDir, "bad.json");
|
|
32
|
+
fs.writeFileSync(fp, "{not valid json");
|
|
33
|
+
expect(readJsonFile(fp, "default")).toBe("default");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("parses valid JSON", () => {
|
|
37
|
+
const fp = path.join(tmpDir, "good.json");
|
|
38
|
+
fs.writeFileSync(fp, JSON.stringify({ a: 1, b: [2, 3] }));
|
|
39
|
+
expect(readJsonFile(fp, {})).toEqual({ a: 1, b: [2, 3] });
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("writeJsonFile", () => {
|
|
44
|
+
it("writes JSON atomically", () => {
|
|
45
|
+
const fp = path.join(tmpDir, "out.json");
|
|
46
|
+
writeJsonFile(fp, { hello: "world" });
|
|
47
|
+
const content = JSON.parse(fs.readFileSync(fp, "utf-8"));
|
|
48
|
+
expect(content).toEqual({ hello: "world" });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("creates parent directories", () => {
|
|
52
|
+
const fp = path.join(tmpDir, "a", "b", "out.json");
|
|
53
|
+
writeJsonFile(fp, [1, 2, 3]);
|
|
54
|
+
expect(JSON.parse(fs.readFileSync(fp, "utf-8"))).toEqual([1, 2, 3]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("overwrites existing file", () => {
|
|
58
|
+
const fp = path.join(tmpDir, "overwrite.json");
|
|
59
|
+
writeJsonFile(fp, { v: 1 });
|
|
60
|
+
writeJsonFile(fp, { v: 2 });
|
|
61
|
+
expect(JSON.parse(fs.readFileSync(fp, "utf-8"))).toEqual({ v: 2 });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("does not leave .tmp file", () => {
|
|
65
|
+
const fp = path.join(tmpDir, "clean.json");
|
|
66
|
+
writeJsonFile(fp, {});
|
|
67
|
+
expect(fs.existsSync(fp + ".tmp")).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { isLoopback, isBypassedHost, matchCidr, ipToNum, createNetworkGuard, netmaskToCidrBits, networkAddress } from "../localhost-guard.js";
|
|
3
|
+
|
|
4
|
+
describe("isLoopback", () => {
|
|
5
|
+
it("should match loopback addresses", () => {
|
|
6
|
+
expect(isLoopback("127.0.0.1")).toBe(true);
|
|
7
|
+
expect(isLoopback("::1")).toBe(true);
|
|
8
|
+
expect(isLoopback("::ffff:127.0.0.1")).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should reject non-loopback", () => {
|
|
12
|
+
expect(isLoopback("192.168.1.1")).toBe(false);
|
|
13
|
+
expect(isLoopback("10.0.0.1")).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("isBypassedHost", () => {
|
|
18
|
+
it("should match exact IP", () => {
|
|
19
|
+
expect(isBypassedHost("192.168.1.42", ["192.168.1.42"])).toBe(true);
|
|
20
|
+
expect(isBypassedHost("192.168.1.43", ["192.168.1.42"])).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should match wildcard", () => {
|
|
24
|
+
expect(isBypassedHost("10.0.0.5", ["10.0.0.*"])).toBe(true);
|
|
25
|
+
expect(isBypassedHost("10.0.1.5", ["10.0.0.*"])).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should match CIDR", () => {
|
|
29
|
+
expect(isBypassedHost("192.168.1.42", ["192.168.1.0/24"])).toBe(true);
|
|
30
|
+
expect(isBypassedHost("192.168.2.1", ["192.168.1.0/24"])).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should match wide CIDR", () => {
|
|
34
|
+
expect(isBypassedHost("10.255.0.1", ["10.0.0.0/8"])).toBe(true);
|
|
35
|
+
expect(isBypassedHost("11.0.0.1", ["10.0.0.0/8"])).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should return false for empty list", () => {
|
|
39
|
+
expect(isBypassedHost("192.168.1.1", [])).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should match any entry in the list", () => {
|
|
43
|
+
expect(isBypassedHost("10.0.0.5", ["192.168.1.0/24", "10.0.0.*"])).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should strip ::ffff: IPv4-mapped prefix", () => {
|
|
47
|
+
expect(isBypassedHost("::ffff:192.168.1.42", ["192.168.1.0/24"])).toBe(true);
|
|
48
|
+
expect(isBypassedHost("::ffff:10.0.0.5", ["10.0.0.*"])).toBe(true);
|
|
49
|
+
expect(isBypassedHost("::ffff:10.0.0.5", ["10.0.0.5"])).toBe(true);
|
|
50
|
+
expect(isBypassedHost("::ffff:192.168.2.1", ["192.168.1.0/24"])).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("matchCidr", () => {
|
|
55
|
+
it("should handle /32 (exact match)", () => {
|
|
56
|
+
expect(matchCidr("10.0.0.1", "10.0.0.1/32")).toBe(true);
|
|
57
|
+
expect(matchCidr("10.0.0.2", "10.0.0.1/32")).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should handle /0 (match all)", () => {
|
|
61
|
+
expect(matchCidr("1.2.3.4", "0.0.0.0/0")).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should reject invalid CIDR bits", () => {
|
|
65
|
+
expect(matchCidr("10.0.0.1", "10.0.0.0/33")).toBe(false);
|
|
66
|
+
expect(matchCidr("10.0.0.1", "10.0.0.0/-1")).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("ipToNum", () => {
|
|
71
|
+
it("should convert valid IPv4", () => {
|
|
72
|
+
expect(ipToNum("0.0.0.0")).toBe(0);
|
|
73
|
+
expect(ipToNum("255.255.255.255")).toBe(0xFFFFFFFF);
|
|
74
|
+
expect(ipToNum("192.168.1.1")).toBe((192 << 24 | 168 << 16 | 1 << 8 | 1) >>> 0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should return null for invalid input", () => {
|
|
78
|
+
expect(ipToNum("not-an-ip")).toBeNull();
|
|
79
|
+
expect(ipToNum("::1")).toBeNull();
|
|
80
|
+
expect(ipToNum("256.0.0.0")).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("netmaskToCidrBits", () => {
|
|
85
|
+
it("should convert common netmasks", () => {
|
|
86
|
+
expect(netmaskToCidrBits("255.255.255.0")).toBe(24);
|
|
87
|
+
expect(netmaskToCidrBits("255.255.0.0")).toBe(16);
|
|
88
|
+
expect(netmaskToCidrBits("255.0.0.0")).toBe(8);
|
|
89
|
+
expect(netmaskToCidrBits("255.255.255.255")).toBe(32);
|
|
90
|
+
expect(netmaskToCidrBits("0.0.0.0")).toBe(0);
|
|
91
|
+
expect(netmaskToCidrBits("255.255.255.128")).toBe(25);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("networkAddress", () => {
|
|
96
|
+
it("should compute network address", () => {
|
|
97
|
+
expect(networkAddress("192.168.1.42", "255.255.255.0")).toBe("192.168.1.0");
|
|
98
|
+
expect(networkAddress("10.0.5.100", "255.255.0.0")).toBe("10.0.0.0");
|
|
99
|
+
expect(networkAddress("172.16.3.1", "255.0.0.0")).toBe("172.0.0.0");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("createNetworkGuard", () => {
|
|
104
|
+
function mockRequest(ip: string, isAuthenticated = false) {
|
|
105
|
+
return { ip, isAuthenticated } as any;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function mockReply() {
|
|
109
|
+
const r: any = { statusCode: 0, body: null };
|
|
110
|
+
r.code = (c: number) => { r.statusCode = c; return r; };
|
|
111
|
+
r.send = (b: any) => { r.body = b; return r; };
|
|
112
|
+
return r;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
it("should allow loopback", async () => {
|
|
116
|
+
const guard = createNetworkGuard([]);
|
|
117
|
+
const reply = mockReply();
|
|
118
|
+
await guard(mockRequest("127.0.0.1"), reply);
|
|
119
|
+
expect(reply.statusCode).toBe(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should allow trusted network CIDR", async () => {
|
|
123
|
+
const guard = createNetworkGuard(["192.168.1.0/24"]);
|
|
124
|
+
const reply = mockReply();
|
|
125
|
+
await guard(mockRequest("192.168.1.42"), reply);
|
|
126
|
+
expect(reply.statusCode).toBe(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should allow authenticated request", async () => {
|
|
130
|
+
const guard = createNetworkGuard([]);
|
|
131
|
+
const reply = mockReply();
|
|
132
|
+
await guard(mockRequest("203.0.113.5", true), reply);
|
|
133
|
+
expect(reply.statusCode).toBe(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should block untrusted unauthenticated request", async () => {
|
|
137
|
+
const guard = createNetworkGuard(["192.168.1.0/24"]);
|
|
138
|
+
const reply = mockReply();
|
|
139
|
+
await guard(mockRequest("10.0.0.5", false), reply);
|
|
140
|
+
expect(reply.statusCode).toBe(403);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should block when no trusted networks and not authenticated", async () => {
|
|
144
|
+
const guard = createNetworkGuard([]);
|
|
145
|
+
const reply = mockReply();
|
|
146
|
+
await guard(mockRequest("192.168.1.5", false), reply);
|
|
147
|
+
expect(reply.statusCode).toBe(403);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createMemoryEventStore } from "../memory-event-store.js";
|
|
3
|
+
import type { DashboardEvent } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
4
|
+
|
|
5
|
+
function makeEvent(type: string = "test"): DashboardEvent {
|
|
6
|
+
return { eventType: type, timestamp: Date.now(), data: {} };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("memory-event-store", () => {
|
|
10
|
+
const neverPinned = () => false;
|
|
11
|
+
|
|
12
|
+
it("inserts and retrieves events", () => {
|
|
13
|
+
const store = createMemoryEventStore(neverPinned);
|
|
14
|
+
const seq1 = store.insertEvent("s1", makeEvent("a"));
|
|
15
|
+
const seq2 = store.insertEvent("s1", makeEvent("b"));
|
|
16
|
+
expect(seq1).toBe(1);
|
|
17
|
+
expect(seq2).toBe(2);
|
|
18
|
+
|
|
19
|
+
const events = store.getEvents("s1", 1);
|
|
20
|
+
expect(events).toHaveLength(2);
|
|
21
|
+
expect(events[0].seq).toBe(1);
|
|
22
|
+
expect(events[1].seq).toBe(2);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("getEvents with minSeq filters correctly", () => {
|
|
26
|
+
const store = createMemoryEventStore(neverPinned);
|
|
27
|
+
store.insertEvent("s1", makeEvent());
|
|
28
|
+
store.insertEvent("s1", makeEvent());
|
|
29
|
+
store.insertEvent("s1", makeEvent());
|
|
30
|
+
|
|
31
|
+
const events = store.getEvents("s1", 2);
|
|
32
|
+
expect(events).toHaveLength(2);
|
|
33
|
+
expect(events[0].seq).toBe(2);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("getEvents returns empty for unknown session", () => {
|
|
37
|
+
const store = createMemoryEventStore(neverPinned);
|
|
38
|
+
expect(store.getEvents("unknown", 1)).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("getEvent retrieves single event", () => {
|
|
42
|
+
const store = createMemoryEventStore(neverPinned);
|
|
43
|
+
const evt = makeEvent("special");
|
|
44
|
+
store.insertEvent("s1", evt);
|
|
45
|
+
const result = store.getEvent("s1", 1);
|
|
46
|
+
expect(result?.eventType).toBe("special");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("getEvent returns undefined for missing", () => {
|
|
50
|
+
const store = createMemoryEventStore(neverPinned);
|
|
51
|
+
expect(store.getEvent("s1", 1)).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("deleteEventsForSession clears buffer", () => {
|
|
55
|
+
const store = createMemoryEventStore(neverPinned);
|
|
56
|
+
store.insertEvent("s1", makeEvent());
|
|
57
|
+
store.insertEvent("s1", makeEvent());
|
|
58
|
+
const deleted = store.deleteEventsForSession("s1");
|
|
59
|
+
expect(deleted).toBe(2);
|
|
60
|
+
expect(store.getEvents("s1", 1)).toEqual([]);
|
|
61
|
+
expect(store.hasEvents("s1")).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("deleteEventsForSession returns 0 for unknown session", () => {
|
|
65
|
+
const store = createMemoryEventStore(neverPinned);
|
|
66
|
+
expect(store.deleteEventsForSession("unknown")).toBe(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("hasEvents checks correctly", () => {
|
|
70
|
+
const store = createMemoryEventStore(neverPinned);
|
|
71
|
+
expect(store.hasEvents("s1")).toBe(false);
|
|
72
|
+
store.insertEvent("s1", makeEvent());
|
|
73
|
+
expect(store.hasEvents("s1")).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("sessionCount tracks number of sessions", () => {
|
|
77
|
+
const store = createMemoryEventStore(neverPinned);
|
|
78
|
+
expect(store.sessionCount()).toBe(0);
|
|
79
|
+
store.insertEvent("s1", makeEvent());
|
|
80
|
+
store.insertEvent("s2", makeEvent());
|
|
81
|
+
expect(store.sessionCount()).toBe(2);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("assigns new seq numbers after deleteEventsForSession", () => {
|
|
85
|
+
const store = createMemoryEventStore(neverPinned);
|
|
86
|
+
store.insertEvent("s1", makeEvent());
|
|
87
|
+
store.insertEvent("s1", makeEvent());
|
|
88
|
+
store.deleteEventsForSession("s1");
|
|
89
|
+
const seq = store.insertEvent("s1", makeEvent());
|
|
90
|
+
expect(seq).toBe(1); // Resets after delete
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("LRU eviction", () => {
|
|
94
|
+
it("evicts least-recently-accessed when over limit", () => {
|
|
95
|
+
const store = createMemoryEventStore(neverPinned, 3);
|
|
96
|
+
store.insertEvent("s1", makeEvent());
|
|
97
|
+
store.insertEvent("s2", makeEvent());
|
|
98
|
+
store.insertEvent("s3", makeEvent());
|
|
99
|
+
expect(store.sessionCount()).toBe(3);
|
|
100
|
+
|
|
101
|
+
// s4 should cause eviction of s1 (oldest)
|
|
102
|
+
store.insertEvent("s4", makeEvent());
|
|
103
|
+
expect(store.sessionCount()).toBe(3);
|
|
104
|
+
expect(store.hasEvents("s1")).toBe(false);
|
|
105
|
+
expect(store.hasEvents("s4")).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("skips pinned sessions during eviction", () => {
|
|
109
|
+
const pinned = new Set(["s1"]);
|
|
110
|
+
const store = createMemoryEventStore((id) => pinned.has(id), 3);
|
|
111
|
+
store.insertEvent("s1", makeEvent());
|
|
112
|
+
store.insertEvent("s2", makeEvent());
|
|
113
|
+
store.insertEvent("s3", makeEvent());
|
|
114
|
+
|
|
115
|
+
// s4 should cause eviction of s2 (s1 is pinned)
|
|
116
|
+
store.insertEvent("s4", makeEvent());
|
|
117
|
+
expect(store.hasEvents("s1")).toBe(true); // pinned, not evicted
|
|
118
|
+
expect(store.hasEvents("s2")).toBe(false); // evicted
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("does not evict when all sessions are pinned", () => {
|
|
122
|
+
const store = createMemoryEventStore(() => true, 2);
|
|
123
|
+
store.insertEvent("s1", makeEvent());
|
|
124
|
+
store.insertEvent("s2", makeEvent());
|
|
125
|
+
store.insertEvent("s3", makeEvent());
|
|
126
|
+
// All pinned — can't evict, so size exceeds limit
|
|
127
|
+
expect(store.sessionCount()).toBe(3);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("accessing events updates lastAccess to prevent eviction", async () => {
|
|
131
|
+
const store = createMemoryEventStore(neverPinned, 3);
|
|
132
|
+
store.insertEvent("s1", makeEvent());
|
|
133
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
134
|
+
store.insertEvent("s2", makeEvent());
|
|
135
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
136
|
+
store.insertEvent("s3", makeEvent());
|
|
137
|
+
|
|
138
|
+
// Access s1 so it becomes most recent
|
|
139
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
140
|
+
store.getEvents("s1", 1);
|
|
141
|
+
|
|
142
|
+
// s4 should evict s2 (least recently accessed), not s1
|
|
143
|
+
store.insertEvent("s4", makeEvent());
|
|
144
|
+
expect(store.hasEvents("s1")).toBe(true);
|
|
145
|
+
expect(store.hasEvents("s2")).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("image data preservation", () => {
|
|
150
|
+
it("preserves base64 image data when sibling mimeType exists", () => {
|
|
151
|
+
// maxStringFieldSize = 100 so normal strings get truncated
|
|
152
|
+
const store = createMemoryEventStore(neverPinned, 100, 5000, 100);
|
|
153
|
+
const longBase64 = "A".repeat(500);
|
|
154
|
+
const event: DashboardEvent = {
|
|
155
|
+
eventType: "message_start",
|
|
156
|
+
timestamp: Date.now(),
|
|
157
|
+
data: {
|
|
158
|
+
message: {
|
|
159
|
+
role: "user",
|
|
160
|
+
content: [
|
|
161
|
+
{ type: "image", data: longBase64, mimeType: "image/png" },
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
store.insertEvent("s1", event);
|
|
167
|
+
const stored = store.getEvent("s1", 1);
|
|
168
|
+
const content = (stored as any).data.message.content[0];
|
|
169
|
+
expect(content.data).toBe(longBase64);
|
|
170
|
+
expect(content.data).toHaveLength(500);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("still truncates data field without mimeType sibling", () => {
|
|
174
|
+
const store = createMemoryEventStore(neverPinned, 100, 5000, 100);
|
|
175
|
+
const longString = "B".repeat(500);
|
|
176
|
+
const event: DashboardEvent = {
|
|
177
|
+
eventType: "test",
|
|
178
|
+
timestamp: Date.now(),
|
|
179
|
+
data: { payload: { data: longString } },
|
|
180
|
+
};
|
|
181
|
+
store.insertEvent("s1", event);
|
|
182
|
+
const stored = store.getEvent("s1", 1);
|
|
183
|
+
const val = (stored as any).data.payload.data as string;
|
|
184
|
+
expect(val.length).toBeLessThan(500);
|
|
185
|
+
expect(val).toContain("truncated");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("truncates other fields alongside preserved image data", () => {
|
|
189
|
+
const store = createMemoryEventStore(neverPinned, 100, 5000, 100);
|
|
190
|
+
const longBase64 = "C".repeat(500);
|
|
191
|
+
const longThinking = "D".repeat(5000);
|
|
192
|
+
const event: DashboardEvent = {
|
|
193
|
+
eventType: "message_start",
|
|
194
|
+
timestamp: Date.now(),
|
|
195
|
+
data: {
|
|
196
|
+
message: {
|
|
197
|
+
role: "user",
|
|
198
|
+
content: [
|
|
199
|
+
{ type: "image", data: longBase64, mimeType: "image/png" },
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
thinking: longThinking,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
store.insertEvent("s1", event);
|
|
206
|
+
const stored = store.getEvent("s1", 1);
|
|
207
|
+
const content = (stored as any).data.message.content[0];
|
|
208
|
+
expect(content.data).toBe(longBase64); // preserved
|
|
209
|
+
const thinking = (stored as any).data.thinking as string;
|
|
210
|
+
expect(thinking).toContain("truncated"); // truncated
|
|
211
|
+
expect(thinking.length).toBeLessThan(longThinking.length); // shorter than original
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("getMaxSeq", () => {
|
|
216
|
+
it("returns 0 for unknown session", () => {
|
|
217
|
+
const store = createMemoryEventStore(neverPinned);
|
|
218
|
+
expect(store.getMaxSeq("unknown")).toBe(0);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("returns highest seq for session with events", () => {
|
|
222
|
+
const store = createMemoryEventStore(neverPinned);
|
|
223
|
+
store.insertEvent("s1", makeEvent());
|
|
224
|
+
store.insertEvent("s1", makeEvent());
|
|
225
|
+
store.insertEvent("s1", makeEvent());
|
|
226
|
+
expect(store.getMaxSeq("s1")).toBe(3);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("returns 0 after deleteEventsForSession", () => {
|
|
230
|
+
const store = createMemoryEventStore(neverPinned);
|
|
231
|
+
store.insertEvent("s1", makeEvent());
|
|
232
|
+
store.insertEvent("s1", makeEvent());
|
|
233
|
+
store.deleteEventsForSession("s1");
|
|
234
|
+
expect(store.getMaxSeq("s1")).toBe(0);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("returns correct seq after oldest events trimmed", () => {
|
|
238
|
+
const store = createMemoryEventStore(neverPinned, 100, 3);
|
|
239
|
+
store.insertEvent("s1", makeEvent());
|
|
240
|
+
store.insertEvent("s1", makeEvent());
|
|
241
|
+
store.insertEvent("s1", makeEvent());
|
|
242
|
+
store.insertEvent("s1", makeEvent()); // seq 4, oldest (seq 1) trimmed
|
|
243
|
+
expect(store.getMaxSeq("s1")).toBe(4);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("trims oldest events when per-session limit exceeded", () => {
|
|
248
|
+
const store = createMemoryEventStore(neverPinned, 100, 3);
|
|
249
|
+
store.insertEvent("s1", makeEvent("a"));
|
|
250
|
+
store.insertEvent("s1", makeEvent("b"));
|
|
251
|
+
store.insertEvent("s1", makeEvent("c"));
|
|
252
|
+
store.insertEvent("s1", makeEvent("d"));
|
|
253
|
+
|
|
254
|
+
const events = store.getEvents("s1", 1);
|
|
255
|
+
expect(events).toHaveLength(3);
|
|
256
|
+
// Oldest event (seq 1) should be trimmed
|
|
257
|
+
expect(events[0].seq).toBe(2);
|
|
258
|
+
expect(events[2].seq).toBe(4);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createMemorySessionManager } from "../memory-session-manager.js";
|
|
3
|
+
|
|
4
|
+
describe("memory-session-manager", () => {
|
|
5
|
+
it("registers a session", () => {
|
|
6
|
+
const sm = createMemorySessionManager();
|
|
7
|
+
const session = sm.register({
|
|
8
|
+
id: "s1",
|
|
9
|
+
cwd: "/tmp",
|
|
10
|
+
source: "tui",
|
|
11
|
+
name: "Test",
|
|
12
|
+
});
|
|
13
|
+
expect(session.id).toBe("s1");
|
|
14
|
+
expect(session.status).toBe("active");
|
|
15
|
+
expect(session.name).toBe("Test");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("gets session by id", () => {
|
|
19
|
+
const sm = createMemorySessionManager();
|
|
20
|
+
sm.register({ id: "s1", cwd: "/tmp", source: "tui" });
|
|
21
|
+
expect(sm.get("s1")).toBeDefined();
|
|
22
|
+
expect(sm.get("nonexistent")).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("unregisters session", () => {
|
|
26
|
+
const sm = createMemorySessionManager();
|
|
27
|
+
sm.register({ id: "s1", cwd: "/tmp", source: "tui" });
|
|
28
|
+
sm.unregister("s1");
|
|
29
|
+
const s = sm.get("s1");
|
|
30
|
+
expect(s?.status).toBe("ended");
|
|
31
|
+
expect(s?.endedAt).toBeDefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("updates session", () => {
|
|
35
|
+
const sm = createMemorySessionManager();
|
|
36
|
+
sm.register({ id: "s1", cwd: "/tmp", source: "tui" });
|
|
37
|
+
sm.update("s1", { tokensIn: 100, model: "test/model" });
|
|
38
|
+
expect(sm.get("s1")?.tokensIn).toBe(100);
|
|
39
|
+
expect(sm.get("s1")?.model).toBe("test/model");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("updates hidden state on session object", () => {
|
|
43
|
+
const sm = createMemorySessionManager();
|
|
44
|
+
sm.register({ id: "s1", cwd: "/tmp", source: "tui" });
|
|
45
|
+
sm.update("s1", { hidden: true });
|
|
46
|
+
expect(sm.get("s1")?.hidden).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("listActive excludes ended sessions", () => {
|
|
50
|
+
const sm = createMemorySessionManager();
|
|
51
|
+
sm.register({ id: "s1", cwd: "/tmp", source: "tui" });
|
|
52
|
+
sm.register({ id: "s2", cwd: "/tmp", source: "tui" });
|
|
53
|
+
sm.unregister("s1");
|
|
54
|
+
expect(sm.listActive()).toHaveLength(1);
|
|
55
|
+
expect(sm.listActive()[0].id).toBe("s2");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("listAll includes all sessions", () => {
|
|
59
|
+
const sm = createMemorySessionManager();
|
|
60
|
+
sm.register({ id: "s1", cwd: "/tmp", source: "tui" });
|
|
61
|
+
sm.register({ id: "s2", cwd: "/tmp", source: "tui" });
|
|
62
|
+
sm.unregister("s1");
|
|
63
|
+
expect(sm.listAll()).toHaveLength(2);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("starts empty after creation", () => {
|
|
67
|
+
const sm = createMemorySessionManager();
|
|
68
|
+
expect(sm.listAll()).toHaveLength(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("onChange receives sessionId", () => {
|
|
72
|
+
const sm = createMemorySessionManager();
|
|
73
|
+
const ids: string[] = [];
|
|
74
|
+
sm.onChange = (sessionId) => ids.push(sessionId);
|
|
75
|
+
sm.register({ id: "s1", cwd: "/tmp", source: "tui" });
|
|
76
|
+
sm.update("s1", { tokensIn: 50 });
|
|
77
|
+
sm.unregister("s1");
|
|
78
|
+
expect(ids).toEqual(["s1", "s1", "s1"]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { createMetaPersistence } from "../meta-persistence.js";
|
|
6
|
+
import { metaPath, readSessionMeta } from "@blackbelt-technology/pi-dashboard-shared/session-meta.js";
|
|
7
|
+
|
|
8
|
+
describe("meta-persistence", () => {
|
|
9
|
+
let tmpDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.useFakeTimers();
|
|
13
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "meta-persist-test-"));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.useRealTimers();
|
|
18
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function sessionFile(name: string): string {
|
|
22
|
+
return path.join(tmpDir, `${name}.jsonl`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
it("should debounce writes (not write immediately)", () => {
|
|
26
|
+
const mp = createMetaPersistence();
|
|
27
|
+
const sf = sessionFile("a");
|
|
28
|
+
mp.save(sf, { source: "dashboard", cost: 1.0 });
|
|
29
|
+
// Not written yet
|
|
30
|
+
expect(fs.existsSync(metaPath(sf))).toBe(false);
|
|
31
|
+
mp.dispose();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should write after debounce period", () => {
|
|
35
|
+
const mp = createMetaPersistence();
|
|
36
|
+
const sf = sessionFile("a");
|
|
37
|
+
mp.save(sf, { source: "dashboard", cost: 1.0 });
|
|
38
|
+
vi.advanceTimersByTime(1000);
|
|
39
|
+
const meta = readSessionMeta(sf);
|
|
40
|
+
expect(meta?.cost).toBe(1.0);
|
|
41
|
+
mp.dispose();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should reset debounce on subsequent saves", () => {
|
|
45
|
+
const mp = createMetaPersistence();
|
|
46
|
+
const sf = sessionFile("a");
|
|
47
|
+
mp.save(sf, { source: "dashboard", cost: 1.0 });
|
|
48
|
+
vi.advanceTimersByTime(500);
|
|
49
|
+
// Update before debounce fires
|
|
50
|
+
mp.save(sf, { source: "dashboard", cost: 2.0 });
|
|
51
|
+
vi.advanceTimersByTime(500);
|
|
52
|
+
// First timer would have fired, but it was reset
|
|
53
|
+
expect(fs.existsSync(metaPath(sf))).toBe(false);
|
|
54
|
+
vi.advanceTimersByTime(500);
|
|
55
|
+
// Now the second timer fires
|
|
56
|
+
const meta = readSessionMeta(sf);
|
|
57
|
+
expect(meta?.cost).toBe(2.0);
|
|
58
|
+
mp.dispose();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should write sessions independently", () => {
|
|
62
|
+
const mp = createMetaPersistence();
|
|
63
|
+
const sfA = sessionFile("a");
|
|
64
|
+
const sfB = sessionFile("b");
|
|
65
|
+
mp.save(sfA, { source: "dashboard", name: "A" });
|
|
66
|
+
vi.advanceTimersByTime(500);
|
|
67
|
+
mp.save(sfB, { source: "dashboard", name: "B" });
|
|
68
|
+
vi.advanceTimersByTime(500);
|
|
69
|
+
// A's timer fired, B's hasn't
|
|
70
|
+
expect(readSessionMeta(sfA)?.name).toBe("A");
|
|
71
|
+
expect(fs.existsSync(metaPath(sfB))).toBe(false);
|
|
72
|
+
vi.advanceTimersByTime(500);
|
|
73
|
+
expect(readSessionMeta(sfB)?.name).toBe("B");
|
|
74
|
+
mp.dispose();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should flush all pending writes immediately", () => {
|
|
78
|
+
const mp = createMetaPersistence();
|
|
79
|
+
const sfA = sessionFile("a");
|
|
80
|
+
const sfB = sessionFile("b");
|
|
81
|
+
mp.save(sfA, { source: "dashboard", name: "A" });
|
|
82
|
+
mp.save(sfB, { source: "dashboard", name: "B" });
|
|
83
|
+
mp.flushAll();
|
|
84
|
+
expect(readSessionMeta(sfA)?.name).toBe("A");
|
|
85
|
+
expect(readSessionMeta(sfB)?.name).toBe("B");
|
|
86
|
+
mp.dispose();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should use atomic writes (no leftover .tmp files)", () => {
|
|
90
|
+
const mp = createMetaPersistence();
|
|
91
|
+
const sf = sessionFile("a");
|
|
92
|
+
mp.save(sf, { source: "dashboard" });
|
|
93
|
+
mp.flushAll();
|
|
94
|
+
expect(fs.existsSync(metaPath(sf) + ".tmp")).toBe(false);
|
|
95
|
+
expect(fs.existsSync(metaPath(sf))).toBe(true);
|
|
96
|
+
mp.dispose();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should dispose without writing", () => {
|
|
100
|
+
const mp = createMetaPersistence();
|
|
101
|
+
const sf = sessionFile("a");
|
|
102
|
+
mp.save(sf, { source: "dashboard" });
|
|
103
|
+
mp.dispose();
|
|
104
|
+
vi.advanceTimersByTime(2000);
|
|
105
|
+
expect(fs.existsSync(metaPath(sf))).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
});
|