@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,138 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { extractFileChanges } from "../session-diff.js";
|
|
3
|
+
import type { DashboardEvent } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
4
|
+
|
|
5
|
+
function makeEvent(eventType: string, timestamp: number, data: Record<string, unknown> = {}): DashboardEvent {
|
|
6
|
+
return { eventType, timestamp, data: { type: eventType, ...data } };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function makeToolStart(toolName: string, args: Record<string, unknown>, timestamp = 1000): DashboardEvent {
|
|
10
|
+
return makeEvent("tool_execution_start", timestamp, { toolName, toolCallId: `tc-${timestamp}`, args });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function makeMessageEnd(text: string, timestamp = 900): DashboardEvent {
|
|
14
|
+
return makeEvent("message_end", timestamp, {
|
|
15
|
+
message: { role: "assistant", content: [{ type: "text", text }] },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("extractFileChanges", () => {
|
|
20
|
+
const cwd = "/project";
|
|
21
|
+
|
|
22
|
+
it("should extract Write tool events", () => {
|
|
23
|
+
const events = [
|
|
24
|
+
makeToolStart("Write", { path: "src/foo.ts", content: "hello world" }, 1000),
|
|
25
|
+
];
|
|
26
|
+
const result = extractFileChanges(events, cwd);
|
|
27
|
+
expect(result).toHaveLength(1);
|
|
28
|
+
expect(result[0].path).toBe("src/foo.ts");
|
|
29
|
+
expect(result[0].changes).toHaveLength(1);
|
|
30
|
+
expect(result[0].changes[0].type).toBe("write");
|
|
31
|
+
expect(result[0].changes[0].content).toBe("hello world");
|
|
32
|
+
expect(result[0].changes[0].timestamp).toBe(1000);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should extract Edit tool events", () => {
|
|
36
|
+
const edits = [{ oldText: "foo", newText: "bar" }];
|
|
37
|
+
const events = [
|
|
38
|
+
makeToolStart("Edit", { path: "src/bar.ts", edits }, 2000),
|
|
39
|
+
];
|
|
40
|
+
const result = extractFileChanges(events, cwd);
|
|
41
|
+
expect(result).toHaveLength(1);
|
|
42
|
+
expect(result[0].path).toBe("src/bar.ts");
|
|
43
|
+
expect(result[0].changes[0].type).toBe("edit");
|
|
44
|
+
expect(result[0].changes[0].edits).toEqual(edits);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should be case-insensitive for tool names", () => {
|
|
48
|
+
const events = [
|
|
49
|
+
makeToolStart("write", { path: "a.ts", content: "x" }, 1000),
|
|
50
|
+
makeToolStart("EDIT", { path: "b.ts", edits: [{ oldText: "a", newText: "b" }] }, 2000),
|
|
51
|
+
];
|
|
52
|
+
const result = extractFileChanges(events, cwd);
|
|
53
|
+
expect(result).toHaveLength(2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should support file_path arg alias", () => {
|
|
57
|
+
const events = [
|
|
58
|
+
makeToolStart("Write", { file_path: "src/alt.ts", content: "x" }, 1000),
|
|
59
|
+
];
|
|
60
|
+
const result = extractFileChanges(events, cwd);
|
|
61
|
+
expect(result).toHaveLength(1);
|
|
62
|
+
expect(result[0].path).toBe("src/alt.ts");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should group multiple changes to same file", () => {
|
|
66
|
+
const events = [
|
|
67
|
+
makeToolStart("Write", { path: "src/foo.ts", content: "v1" }, 1000),
|
|
68
|
+
makeToolStart("Edit", { path: "src/foo.ts", edits: [{ oldText: "v1", newText: "v2" }] }, 2000),
|
|
69
|
+
];
|
|
70
|
+
const result = extractFileChanges(events, cwd);
|
|
71
|
+
expect(result).toHaveLength(1);
|
|
72
|
+
expect(result[0].changes).toHaveLength(2);
|
|
73
|
+
expect(result[0].changes[0].type).toBe("write");
|
|
74
|
+
expect(result[0].changes[1].type).toBe("edit");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should order changes by timestamp", () => {
|
|
78
|
+
const events = [
|
|
79
|
+
makeToolStart("Edit", { path: "src/foo.ts", edits: [{ oldText: "a", newText: "b" }] }, 3000),
|
|
80
|
+
makeToolStart("Write", { path: "src/foo.ts", content: "init" }, 1000),
|
|
81
|
+
];
|
|
82
|
+
const result = extractFileChanges(events, cwd);
|
|
83
|
+
expect(result[0].changes[0].timestamp).toBe(1000);
|
|
84
|
+
expect(result[0].changes[1].timestamp).toBe(3000);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should filter out absolute paths outside cwd", () => {
|
|
88
|
+
const events = [
|
|
89
|
+
makeToolStart("Write", { path: "/tmp/scratch.ts", content: "x" }, 1000),
|
|
90
|
+
makeToolStart("Write", { path: "/project/src/inside.ts", content: "y" }, 2000),
|
|
91
|
+
makeToolStart("Write", { path: "src/relative.ts", content: "z" }, 3000),
|
|
92
|
+
];
|
|
93
|
+
const result = extractFileChanges(events, cwd);
|
|
94
|
+
expect(result).toHaveLength(2);
|
|
95
|
+
expect(result.map((f) => f.path).sort()).toEqual(["src/inside.ts", "src/relative.ts"]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should extract preceding assistant message as context", () => {
|
|
99
|
+
const events = [
|
|
100
|
+
makeMessageEnd("I'll create the file now", 900),
|
|
101
|
+
makeToolStart("Write", { path: "src/foo.ts", content: "hello" }, 1000),
|
|
102
|
+
];
|
|
103
|
+
const result = extractFileChanges(events, cwd);
|
|
104
|
+
expect(result[0].changes[0].message).toBe("I'll create the file now");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should truncate long context messages to 120 chars", () => {
|
|
108
|
+
const longMsg = "A".repeat(200);
|
|
109
|
+
const events = [
|
|
110
|
+
makeMessageEnd(longMsg, 900),
|
|
111
|
+
makeToolStart("Write", { path: "src/foo.ts", content: "hello" }, 1000),
|
|
112
|
+
];
|
|
113
|
+
const result = extractFileChanges(events, cwd);
|
|
114
|
+
expect(result[0].changes[0].message!.length).toBeLessThanOrEqual(123); // 120 + "..."
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should ignore non-Write/Edit tool events", () => {
|
|
118
|
+
const events = [
|
|
119
|
+
makeToolStart("Read", { path: "src/foo.ts" }, 1000),
|
|
120
|
+
makeToolStart("Bash", { command: "ls" }, 2000),
|
|
121
|
+
];
|
|
122
|
+
const result = extractFileChanges(events, cwd);
|
|
123
|
+
expect(result).toHaveLength(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should skip events with no path", () => {
|
|
127
|
+
const events = [
|
|
128
|
+
makeToolStart("Write", { content: "no path" }, 1000),
|
|
129
|
+
];
|
|
130
|
+
const result = extractFileChanges(events, cwd);
|
|
131
|
+
expect(result).toHaveLength(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should return empty array for empty events", () => {
|
|
135
|
+
const result = extractFileChanges([], cwd);
|
|
136
|
+
expect(result).toHaveLength(0);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests that when a new session registers with a sessionFile already used by
|
|
3
|
+
* another session, the old session's sessionFile is cleared.
|
|
4
|
+
* This prevents resuming a stale session from loading the wrong conversation.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, afterAll } from "vitest";
|
|
7
|
+
import { createServer, type DashboardServer } from "../server.js";
|
|
8
|
+
import { WebSocket } from "ws";
|
|
9
|
+
|
|
10
|
+
function waitForOpen(ws: WebSocket): Promise<void> {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
if (ws.readyState === WebSocket.OPEN) return resolve();
|
|
13
|
+
ws.on("open", resolve);
|
|
14
|
+
ws.on("error", reject);
|
|
15
|
+
setTimeout(() => reject(new Error("open timeout")), 3000);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function collectMsgs(ws: WebSocket, ms: number): Promise<any[]> {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const arr: any[] = [];
|
|
22
|
+
const h = (raw: any) => arr.push(JSON.parse(raw.toString()));
|
|
23
|
+
ws.on("message", h);
|
|
24
|
+
setTimeout(() => { ws.off("message", h); resolve(arr); }, ms);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
29
|
+
const httpPort = 19090;
|
|
30
|
+
const piPort = 19091;
|
|
31
|
+
let server: DashboardServer;
|
|
32
|
+
|
|
33
|
+
describe("session file deduplication", () => {
|
|
34
|
+
afterAll(async () => {
|
|
35
|
+
if (server) await server.stop();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("clears sessionFile from old session when new session registers with same file", async () => {
|
|
39
|
+
server = await createServer({
|
|
40
|
+
port: httpPort, piPort, dev: true,
|
|
41
|
+
autoShutdown: false, shutdownIdleSeconds: 999, tunnel: false,
|
|
42
|
+
editor: { idleTimeoutMinutes: 10, maxInstances: 3 },
|
|
43
|
+
});
|
|
44
|
+
await server.start();
|
|
45
|
+
|
|
46
|
+
const sharedFile = "/tmp/sessions/test.jsonl";
|
|
47
|
+
|
|
48
|
+
// Bridge registers session A with a sessionFile
|
|
49
|
+
const bridge = new WebSocket(`ws://localhost:${piPort}`);
|
|
50
|
+
await waitForOpen(bridge);
|
|
51
|
+
bridge.send(JSON.stringify({
|
|
52
|
+
type: "session_register",
|
|
53
|
+
sessionId: "session-a",
|
|
54
|
+
cwd: "/tmp/project",
|
|
55
|
+
source: "tui",
|
|
56
|
+
name: "filesystem-browser",
|
|
57
|
+
sessionFile: sharedFile,
|
|
58
|
+
}));
|
|
59
|
+
await delay(100);
|
|
60
|
+
|
|
61
|
+
// Session A ends
|
|
62
|
+
bridge.send(JSON.stringify({
|
|
63
|
+
type: "session_unregister",
|
|
64
|
+
sessionId: "session-a",
|
|
65
|
+
}));
|
|
66
|
+
await delay(100);
|
|
67
|
+
|
|
68
|
+
// Browser connects to observe updates
|
|
69
|
+
const browser = new WebSocket(`ws://localhost:${httpPort}/ws`);
|
|
70
|
+
await waitForOpen(browser);
|
|
71
|
+
const msgs = collectMsgs(browser, 500);
|
|
72
|
+
await delay(50);
|
|
73
|
+
|
|
74
|
+
// Bridge registers session B (continued from same file) with same sessionFile
|
|
75
|
+
bridge.send(JSON.stringify({
|
|
76
|
+
type: "session_register",
|
|
77
|
+
sessionId: "session-b",
|
|
78
|
+
cwd: "/tmp/project",
|
|
79
|
+
source: "tui",
|
|
80
|
+
name: "fix-concurrent-server-launch",
|
|
81
|
+
sessionFile: sharedFile,
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
const collected = await msgs;
|
|
85
|
+
|
|
86
|
+
// Should see session_updated for session-a clearing its sessionFile
|
|
87
|
+
const clearUpdate = collected.find(
|
|
88
|
+
(m: any) => m.type === "session_updated" && m.sessionId === "session-a" && "sessionFile" in (m.updates ?? {}) && m.updates.sessionFile === null,
|
|
89
|
+
);
|
|
90
|
+
expect(clearUpdate).toBeDefined();
|
|
91
|
+
|
|
92
|
+
// Session B should have the sessionFile
|
|
93
|
+
const addedB = collected.find(
|
|
94
|
+
(m: any) => m.type === "session_added" && m.session?.id === "session-b",
|
|
95
|
+
);
|
|
96
|
+
expect(addedB?.session?.sessionFile).toBe(sharedFile);
|
|
97
|
+
|
|
98
|
+
bridge.close();
|
|
99
|
+
browser.close();
|
|
100
|
+
await delay(50);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { createBranchedSessionFile } from "../session-file-reader.js";
|
|
6
|
+
|
|
7
|
+
describe("createBranchedSessionFile", () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpDir = mkdtempSync(join(tmpdir(), "session-test-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function writeSession(entries: any[]): string {
|
|
19
|
+
const path = join(tmpDir, "test-session.jsonl");
|
|
20
|
+
writeFileSync(path, entries.map(e => JSON.stringify(e)).join("\n") + "\n");
|
|
21
|
+
return path;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
it("should create a branched session file with root-to-target path", () => {
|
|
25
|
+
const sessionFile = writeSession([
|
|
26
|
+
{ type: "session", id: "sess-1", timestamp: "2025-01-01T00:00:00Z", cwd: "/tmp" },
|
|
27
|
+
{ type: "message", id: "e1", parentId: null, message: { role: "user", content: "Hello" } },
|
|
28
|
+
{ type: "message", id: "e2", parentId: "e1", message: { role: "assistant", content: "Hi" } },
|
|
29
|
+
{ type: "message", id: "e3", parentId: "e2", message: { role: "user", content: "More" } },
|
|
30
|
+
{ type: "message", id: "e4", parentId: "e3", message: { role: "assistant", content: "Done" } },
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const newPath = createBranchedSessionFile(sessionFile, "e2");
|
|
34
|
+
const lines = readFileSync(newPath, "utf-8").trim().split("\n").map(l => JSON.parse(l));
|
|
35
|
+
|
|
36
|
+
// Header + 2 entries (e1, e2)
|
|
37
|
+
expect(lines).toHaveLength(3);
|
|
38
|
+
expect(lines[0].type).toBe("session");
|
|
39
|
+
expect(lines[0].parentSession).toBe(sessionFile);
|
|
40
|
+
expect(lines[1].id).toBe("e1");
|
|
41
|
+
expect(lines[1].parentId).toBeNull();
|
|
42
|
+
expect(lines[2].id).toBe("e2");
|
|
43
|
+
expect(lines[2].parentId).toBe("e1");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should throw for non-existent entry ID", () => {
|
|
47
|
+
const sessionFile = writeSession([
|
|
48
|
+
{ type: "session", id: "sess-1", timestamp: "2025-01-01T00:00:00Z", cwd: "/tmp" },
|
|
49
|
+
{ type: "message", id: "e1", parentId: null, message: { role: "user", content: "Hello" } },
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
expect(() => createBranchedSessionFile(sessionFile, "nonexistent")).toThrow("Entry ID not found");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should throw for non-existent session file", () => {
|
|
56
|
+
expect(() => createBranchedSessionFile("/no/such/file.jsonl", "e1")).toThrow("Session file not found");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should handle single-entry branch (root entry)", () => {
|
|
60
|
+
const sessionFile = writeSession([
|
|
61
|
+
{ type: "session", id: "sess-1", timestamp: "2025-01-01T00:00:00Z", cwd: "/tmp" },
|
|
62
|
+
{ type: "message", id: "e1", parentId: null, message: { role: "user", content: "Hello" } },
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const newPath = createBranchedSessionFile(sessionFile, "e1");
|
|
66
|
+
const lines = readFileSync(newPath, "utf-8").trim().split("\n").map(l => JSON.parse(l));
|
|
67
|
+
|
|
68
|
+
expect(lines).toHaveLength(2); // header + 1 entry
|
|
69
|
+
expect(lines[1].id).toBe("e1");
|
|
70
|
+
expect(lines[1].parentId).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should generate a new session ID for the branched file", () => {
|
|
74
|
+
const sessionFile = writeSession([
|
|
75
|
+
{ type: "session", id: "original-id", timestamp: "2025-01-01T00:00:00Z", cwd: "/tmp" },
|
|
76
|
+
{ type: "message", id: "e1", parentId: null, message: { role: "user", content: "Hello" } },
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
const newPath = createBranchedSessionFile(sessionFile, "e1");
|
|
80
|
+
const header = JSON.parse(readFileSync(newPath, "utf-8").split("\n")[0]);
|
|
81
|
+
|
|
82
|
+
expect(header.id).not.toBe("original-id");
|
|
83
|
+
expect(header.id).toMatch(/^[0-9a-f-]+$/); // UUID format
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for session lifecycle logging in pi-gateway.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
5
|
+
import { createPiGateway } from "../pi-gateway.js";
|
|
6
|
+
import { createMemorySessionManager } from "../memory-session-manager.js";
|
|
7
|
+
import { WebSocket } from "ws";
|
|
8
|
+
|
|
9
|
+
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
10
|
+
|
|
11
|
+
function waitForOpen(ws: WebSocket): Promise<void> {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
if (ws.readyState === WebSocket.OPEN) return resolve();
|
|
14
|
+
ws.on("open", resolve);
|
|
15
|
+
ws.on("error", reject);
|
|
16
|
+
setTimeout(() => reject(new Error("open timeout")), 3000);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SHORT_HB = 300;
|
|
21
|
+
let portCounter = 19600;
|
|
22
|
+
|
|
23
|
+
describe("Session lifecycle logging", () => {
|
|
24
|
+
let gateway: ReturnType<typeof createPiGateway>;
|
|
25
|
+
let errorSpy: ReturnType<typeof vi.spyOn>;
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
gateway?.stop();
|
|
29
|
+
errorSpy?.mockRestore();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should log on session_register", async () => {
|
|
33
|
+
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
34
|
+
const sessionManager = createMemorySessionManager();
|
|
35
|
+
gateway = createPiGateway(sessionManager, { heartbeatTimeout: 5000 });
|
|
36
|
+
const port = portCounter++;
|
|
37
|
+
gateway.start(port);
|
|
38
|
+
|
|
39
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
40
|
+
await waitForOpen(ws);
|
|
41
|
+
ws.send(JSON.stringify({
|
|
42
|
+
type: "session_register", sessionId: "log-reg", cwd: "/tmp/test", source: "tui",
|
|
43
|
+
}));
|
|
44
|
+
await delay(100);
|
|
45
|
+
|
|
46
|
+
const logs = errorSpy.mock.calls.map((c) => c[0]);
|
|
47
|
+
expect(logs).toContainEqual(expect.stringContaining("[gateway] session registered: log-reg cwd=/tmp/test"));
|
|
48
|
+
ws.close();
|
|
49
|
+
}, 10000);
|
|
50
|
+
|
|
51
|
+
it("should log on explicit session_unregister", async () => {
|
|
52
|
+
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
53
|
+
const sessionManager = createMemorySessionManager();
|
|
54
|
+
gateway = createPiGateway(sessionManager, { heartbeatTimeout: 5000 });
|
|
55
|
+
const port = portCounter++;
|
|
56
|
+
gateway.start(port);
|
|
57
|
+
|
|
58
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
59
|
+
await waitForOpen(ws);
|
|
60
|
+
ws.send(JSON.stringify({
|
|
61
|
+
type: "session_register", sessionId: "log-unreg", cwd: "/tmp", source: "tui",
|
|
62
|
+
}));
|
|
63
|
+
await delay(100);
|
|
64
|
+
ws.send(JSON.stringify({ type: "session_unregister", sessionId: "log-unreg" }));
|
|
65
|
+
await delay(100);
|
|
66
|
+
|
|
67
|
+
const logs = errorSpy.mock.calls.map((c) => c[0]);
|
|
68
|
+
expect(logs).toContainEqual(expect.stringContaining("[gateway] session unregistered: log-unreg (explicit)"));
|
|
69
|
+
ws.close();
|
|
70
|
+
}, 10000);
|
|
71
|
+
|
|
72
|
+
it("should log on heartbeat timeout", async () => {
|
|
73
|
+
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
74
|
+
const sessionManager = createMemorySessionManager();
|
|
75
|
+
gateway = createPiGateway(sessionManager, { heartbeatTimeout: SHORT_HB, pingInterval: 60000 });
|
|
76
|
+
const port = portCounter++;
|
|
77
|
+
gateway.start(port);
|
|
78
|
+
|
|
79
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
80
|
+
await waitForOpen(ws);
|
|
81
|
+
ws.send(JSON.stringify({
|
|
82
|
+
type: "session_register", sessionId: "log-timeout", cwd: "/tmp", source: "tui",
|
|
83
|
+
}));
|
|
84
|
+
await delay(100);
|
|
85
|
+
|
|
86
|
+
// Close without unregister — triggers heartbeat timeout
|
|
87
|
+
ws.close();
|
|
88
|
+
await delay(SHORT_HB + 300);
|
|
89
|
+
|
|
90
|
+
const logs = errorSpy.mock.calls.map((c) => c[0]);
|
|
91
|
+
expect(logs).toContainEqual(expect.stringContaining("[gateway] session timed out: log-timeout (no heartbeat for"));
|
|
92
|
+
}, 10000);
|
|
93
|
+
|
|
94
|
+
it("should log on connection close", async () => {
|
|
95
|
+
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
96
|
+
const sessionManager = createMemorySessionManager();
|
|
97
|
+
gateway = createPiGateway(sessionManager, { heartbeatTimeout: 5000 });
|
|
98
|
+
const port = portCounter++;
|
|
99
|
+
gateway.start(port);
|
|
100
|
+
|
|
101
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
102
|
+
await waitForOpen(ws);
|
|
103
|
+
ws.send(JSON.stringify({
|
|
104
|
+
type: "session_register", sessionId: "log-close", cwd: "/tmp", source: "tui",
|
|
105
|
+
}));
|
|
106
|
+
await delay(100);
|
|
107
|
+
ws.close();
|
|
108
|
+
await delay(200);
|
|
109
|
+
|
|
110
|
+
const logs = errorSpy.mock.calls.map((c) => c[0]);
|
|
111
|
+
expect(logs).toContainEqual(expect.stringContaining("[gateway] connection closed: log-close"));
|
|
112
|
+
}, 10000);
|
|
113
|
+
|
|
114
|
+
it("should log on ping timeout", async () => {
|
|
115
|
+
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
116
|
+
const sessionManager = createMemorySessionManager();
|
|
117
|
+
gateway = createPiGateway(sessionManager, {
|
|
118
|
+
heartbeatTimeout: 60000,
|
|
119
|
+
pingInterval: 200,
|
|
120
|
+
});
|
|
121
|
+
const port = portCounter++;
|
|
122
|
+
gateway.start(port);
|
|
123
|
+
|
|
124
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
125
|
+
await waitForOpen(ws);
|
|
126
|
+
ws.send(JSON.stringify({
|
|
127
|
+
type: "session_register", sessionId: "log-ping", cwd: "/tmp", source: "tui",
|
|
128
|
+
}));
|
|
129
|
+
await delay(100);
|
|
130
|
+
|
|
131
|
+
// Pause socket to prevent pong — need 2 missed pings before kill
|
|
132
|
+
(ws as any)._socket?.pause();
|
|
133
|
+
await delay(200 * 4);
|
|
134
|
+
|
|
135
|
+
const logs = errorSpy.mock.calls.map((c) => c[0]);
|
|
136
|
+
expect(logs).toContainEqual(expect.stringContaining("[gateway] connection dead (ping timeout, 2 misses): log-ping"));
|
|
137
|
+
}, 10000);
|
|
138
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { createSessionOrderManager } from "../session-order-manager.js";
|
|
3
|
+
import type { PreferencesStore } from "../preferences-store.js";
|
|
4
|
+
|
|
5
|
+
function createMockPreferencesStore(initialOrder: Record<string, string[]> = {}): PreferencesStore {
|
|
6
|
+
let order = { ...initialOrder };
|
|
7
|
+
return {
|
|
8
|
+
getSessionOrder: vi.fn(() => order),
|
|
9
|
+
setSessionOrder: vi.fn((o: Record<string, string[]>) => { order = o; }),
|
|
10
|
+
getPinnedDirectories: vi.fn(() => []),
|
|
11
|
+
setPinnedDirectories: vi.fn(),
|
|
12
|
+
pinDirectory: vi.fn(),
|
|
13
|
+
unpinDirectory: vi.fn(),
|
|
14
|
+
reorderPinnedDirs: vi.fn(),
|
|
15
|
+
flush: vi.fn(),
|
|
16
|
+
dispose: vi.fn(),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("SessionOrderManager", () => {
|
|
21
|
+
let stateStore: PreferencesStore;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
stateStore = createMockPreferencesStore();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("insert", () => {
|
|
28
|
+
it("prepends session to empty order", () => {
|
|
29
|
+
const mgr = createSessionOrderManager(stateStore);
|
|
30
|
+
mgr.insert("/project", "s1");
|
|
31
|
+
expect(mgr.getOrder("/project")).toEqual(["s1"]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("prepends session to existing order", () => {
|
|
35
|
+
stateStore = createMockPreferencesStore({ "/project": ["s1", "s2"] });
|
|
36
|
+
const mgr = createSessionOrderManager(stateStore);
|
|
37
|
+
mgr.insert("/project", "s3");
|
|
38
|
+
expect(mgr.getOrder("/project")).toEqual(["s3", "s1", "s2"]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("inserts after parent when afterSessionId provided", () => {
|
|
42
|
+
stateStore = createMockPreferencesStore({ "/project": ["s1", "s3"] });
|
|
43
|
+
const mgr = createSessionOrderManager(stateStore);
|
|
44
|
+
mgr.insert("/project", "s2", "s1");
|
|
45
|
+
expect(mgr.getOrder("/project")).toEqual(["s1", "s2", "s3"]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("prepends when afterSessionId not found in order", () => {
|
|
49
|
+
stateStore = createMockPreferencesStore({ "/project": ["s1", "s2"] });
|
|
50
|
+
const mgr = createSessionOrderManager(stateStore);
|
|
51
|
+
mgr.insert("/project", "s3", "nonexistent");
|
|
52
|
+
expect(mgr.getOrder("/project")).toEqual(["s3", "s1", "s2"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("does not duplicate if session already in order", () => {
|
|
56
|
+
stateStore = createMockPreferencesStore({ "/project": ["s1", "s2"] });
|
|
57
|
+
const mgr = createSessionOrderManager(stateStore);
|
|
58
|
+
mgr.insert("/project", "s1");
|
|
59
|
+
expect(mgr.getOrder("/project")).toEqual(["s1", "s2"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("persists after insert", () => {
|
|
63
|
+
const mgr = createSessionOrderManager(stateStore);
|
|
64
|
+
mgr.insert("/project", "s1");
|
|
65
|
+
expect(stateStore.setSessionOrder).toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("reorder", () => {
|
|
70
|
+
it("replaces order for a cwd", () => {
|
|
71
|
+
stateStore = createMockPreferencesStore({ "/project": ["s1", "s2", "s3"] });
|
|
72
|
+
const mgr = createSessionOrderManager(stateStore);
|
|
73
|
+
mgr.reorder("/project", ["s3", "s1", "s2"]);
|
|
74
|
+
expect(mgr.getOrder("/project")).toEqual(["s3", "s1", "s2"]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("persists after reorder", () => {
|
|
78
|
+
const mgr = createSessionOrderManager(stateStore);
|
|
79
|
+
mgr.reorder("/project", ["s1", "s2"]);
|
|
80
|
+
expect(stateStore.setSessionOrder).toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("remove", () => {
|
|
85
|
+
it("removes session from order", () => {
|
|
86
|
+
stateStore = createMockPreferencesStore({ "/project": ["s1", "s2", "s3"] });
|
|
87
|
+
const mgr = createSessionOrderManager(stateStore);
|
|
88
|
+
mgr.remove("/project", "s2");
|
|
89
|
+
expect(mgr.getOrder("/project")).toEqual(["s1", "s3"]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("no-op when session not in order", () => {
|
|
93
|
+
stateStore = createMockPreferencesStore({ "/project": ["s1", "s2"] });
|
|
94
|
+
const mgr = createSessionOrderManager(stateStore);
|
|
95
|
+
mgr.remove("/project", "s99");
|
|
96
|
+
expect(mgr.getOrder("/project")).toEqual(["s1", "s2"]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("removes empty cwd entry", () => {
|
|
100
|
+
stateStore = createMockPreferencesStore({ "/project": ["s1"] });
|
|
101
|
+
const mgr = createSessionOrderManager(stateStore);
|
|
102
|
+
mgr.remove("/project", "s1");
|
|
103
|
+
expect(mgr.getOrder("/project")).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("getOrder with validIds filtering", () => {
|
|
108
|
+
it("filters out stale IDs", () => {
|
|
109
|
+
stateStore = createMockPreferencesStore({ "/project": ["s1", "s2", "s3"] });
|
|
110
|
+
const mgr = createSessionOrderManager(stateStore);
|
|
111
|
+
const validIds = new Set(["s1", "s3"]);
|
|
112
|
+
expect(mgr.getOrder("/project", validIds)).toEqual(["s1", "s3"]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("returns empty for unknown cwd", () => {
|
|
116
|
+
const mgr = createSessionOrderManager(stateStore);
|
|
117
|
+
expect(mgr.getOrder("/unknown")).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("returns all if no validIds filter", () => {
|
|
121
|
+
stateStore = createMockPreferencesStore({ "/project": ["s1", "s2"] });
|
|
122
|
+
const mgr = createSessionOrderManager(stateStore);
|
|
123
|
+
expect(mgr.getOrder("/project")).toEqual(["s1", "s2"]);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("getAllOrders", () => {
|
|
128
|
+
it("returns all cwd orders", () => {
|
|
129
|
+
stateStore = createMockPreferencesStore({ "/a": ["s1"], "/b": ["s2", "s3"] });
|
|
130
|
+
const mgr = createSessionOrderManager(stateStore);
|
|
131
|
+
const orders = mgr.getAllOrders();
|
|
132
|
+
expect(orders).toEqual({ "/a": ["s1"], "/b": ["s2", "s3"] });
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|