@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,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for session ordering flows.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
5
|
+
import { createSessionOrderManager } from "../session-order-manager.js";
|
|
6
|
+
import { createPendingForkRegistry } from "../pending-fork-registry.js";
|
|
7
|
+
import type { PreferencesStore } from "../preferences-store.js";
|
|
8
|
+
|
|
9
|
+
function createMockPreferencesStore(): PreferencesStore {
|
|
10
|
+
let order: Record<string, string[]> = {};
|
|
11
|
+
return {
|
|
12
|
+
getSessionOrder: vi.fn(() => order),
|
|
13
|
+
setSessionOrder: vi.fn((o: Record<string, string[]>) => { order = o; }),
|
|
14
|
+
getPinnedDirectories: vi.fn(() => []),
|
|
15
|
+
setPinnedDirectories: vi.fn(),
|
|
16
|
+
pinDirectory: vi.fn(),
|
|
17
|
+
unpinDirectory: vi.fn(),
|
|
18
|
+
reorderPinnedDirs: vi.fn(),
|
|
19
|
+
flush: vi.fn(),
|
|
20
|
+
dispose: vi.fn(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("Session ordering integration", () => {
|
|
25
|
+
let stateStore: PreferencesStore;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
stateStore = createMockPreferencesStore();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("new session prepends to order", () => {
|
|
32
|
+
const orderMgr = createSessionOrderManager(stateStore);
|
|
33
|
+
orderMgr.insert("/project", "s1");
|
|
34
|
+
orderMgr.insert("/project", "s2");
|
|
35
|
+
orderMgr.insert("/project", "s3");
|
|
36
|
+
expect(orderMgr.getOrder("/project")).toEqual(["s3", "s2", "s1"]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("fork inserts after parent", () => {
|
|
40
|
+
const orderMgr = createSessionOrderManager(stateStore);
|
|
41
|
+
const forkRegistry = createPendingForkRegistry();
|
|
42
|
+
|
|
43
|
+
// Setup: two sessions exist
|
|
44
|
+
orderMgr.insert("/project", "s1");
|
|
45
|
+
orderMgr.insert("/project", "s2"); // s2 is at front: ["s2", "s1"]
|
|
46
|
+
|
|
47
|
+
// User forks s1
|
|
48
|
+
forkRegistry.recordFork("/project", "s1");
|
|
49
|
+
|
|
50
|
+
// New session registers — simulate server checking fork registry
|
|
51
|
+
const forkParent = forkRegistry.consumeFork("/project");
|
|
52
|
+
orderMgr.insert("/project", "s3", forkParent ?? undefined);
|
|
53
|
+
|
|
54
|
+
// s3 should be after s1: ["s2", "s1", "s3"]
|
|
55
|
+
expect(orderMgr.getOrder("/project")).toEqual(["s2", "s1", "s3"]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("reorder replaces order", () => {
|
|
59
|
+
const orderMgr = createSessionOrderManager(stateStore);
|
|
60
|
+
orderMgr.insert("/project", "s1");
|
|
61
|
+
orderMgr.insert("/project", "s2");
|
|
62
|
+
orderMgr.insert("/project", "s3"); // ["s3", "s2", "s1"]
|
|
63
|
+
|
|
64
|
+
orderMgr.reorder("/project", ["s1", "s3", "s2"]);
|
|
65
|
+
expect(orderMgr.getOrder("/project")).toEqual(["s1", "s3", "s2"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("continue preserves position (no re-insert for existing ID)", () => {
|
|
69
|
+
const orderMgr = createSessionOrderManager(stateStore);
|
|
70
|
+
orderMgr.insert("/project", "s1");
|
|
71
|
+
orderMgr.insert("/project", "s2"); // ["s2", "s1"]
|
|
72
|
+
|
|
73
|
+
// s1 re-registers (continue) — insert is a no-op because ID already exists
|
|
74
|
+
orderMgr.insert("/project", "s1");
|
|
75
|
+
expect(orderMgr.getOrder("/project")).toEqual(["s2", "s1"]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("getOrder prunes stale IDs", () => {
|
|
79
|
+
const orderMgr = createSessionOrderManager(stateStore);
|
|
80
|
+
orderMgr.insert("/project", "s1");
|
|
81
|
+
orderMgr.insert("/project", "s2");
|
|
82
|
+
orderMgr.insert("/project", "s3"); // ["s3", "s2", "s1"]
|
|
83
|
+
|
|
84
|
+
// s2 no longer exists
|
|
85
|
+
const validIds = new Set(["s1", "s3"]);
|
|
86
|
+
expect(orderMgr.getOrder("/project", validIds)).toEqual(["s3", "s1"]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("concurrent registrations in same cwd maintain correct order", () => {
|
|
90
|
+
const orderMgr = createSessionOrderManager(stateStore);
|
|
91
|
+
|
|
92
|
+
// Simulate rapid concurrent registrations (all synchronous in Node.js)
|
|
93
|
+
orderMgr.insert("/project", "s1");
|
|
94
|
+
orderMgr.insert("/project", "s2");
|
|
95
|
+
orderMgr.insert("/project", "s3");
|
|
96
|
+
orderMgr.insert("/project", "s4");
|
|
97
|
+
orderMgr.insert("/project", "s5");
|
|
98
|
+
|
|
99
|
+
// All should be in reverse order (most recent first)
|
|
100
|
+
expect(orderMgr.getOrder("/project")).toEqual(["s5", "s4", "s3", "s2", "s1"]);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
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 { scanAllSessions } from "../session-scanner.js";
|
|
6
|
+
import { metaPath, writeSessionMeta } from "@blackbelt-technology/pi-dashboard-shared/session-meta.js";
|
|
7
|
+
|
|
8
|
+
// Mock extractSessionStats to avoid needing real JSONL content with usage data
|
|
9
|
+
vi.mock("../session-stats-reader.js", () => ({
|
|
10
|
+
extractSessionStats: vi.fn(() => ({
|
|
11
|
+
tokensIn: 10,
|
|
12
|
+
tokensOut: 20,
|
|
13
|
+
cacheRead: 30,
|
|
14
|
+
cacheWrite: 40,
|
|
15
|
+
cost: 0.5,
|
|
16
|
+
lastTotalTokens: 1000,
|
|
17
|
+
contextWindow: 200000,
|
|
18
|
+
model: "anthropic/claude-sonnet-4-20250514",
|
|
19
|
+
thinkingLevel: "medium",
|
|
20
|
+
})),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
describe("session-scanner", () => {
|
|
24
|
+
let tmpDir: string;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "scanner-test-"));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function createSessionDir(cwdEncoded: string): string {
|
|
35
|
+
const dir = path.join(tmpDir, cwdEncoded);
|
|
36
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createJsonl(dir: string, filename: string, header?: { id: string; cwd: string }): string {
|
|
41
|
+
const filePath = path.join(dir, filename);
|
|
42
|
+
const h = header ?? { id: "test-id", cwd: "/test/cwd" };
|
|
43
|
+
const lines = [
|
|
44
|
+
JSON.stringify({ type: "session", id: h.id, cwd: h.cwd, timestamp: "2026-03-30T21:39:43.034Z" }),
|
|
45
|
+
JSON.stringify({ type: "message", message: { role: "user", content: "Hello world" } }),
|
|
46
|
+
];
|
|
47
|
+
fs.writeFileSync(filePath, lines.join("\n") + "\n");
|
|
48
|
+
return filePath;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
it("should return empty for non-existent directory", () => {
|
|
52
|
+
const result = scanAllSessions("/non/existent/path");
|
|
53
|
+
expect(result.sessions).toEqual([]);
|
|
54
|
+
expect(result.cacheUpdates).toBe(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should return empty for empty sessions directory", () => {
|
|
58
|
+
const result = scanAllSessions(tmpDir);
|
|
59
|
+
expect(result.sessions).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should discover session from .meta.json with cached data", () => {
|
|
63
|
+
const dir = createSessionDir("--test-cwd--");
|
|
64
|
+
const sf = createJsonl(dir, "2026-03-30T21-39-43-034Z_abc-123.jsonl", { id: "abc-123", cwd: "/test/cwd" });
|
|
65
|
+
writeSessionMeta(sf, {
|
|
66
|
+
cwd: "/test/cwd",
|
|
67
|
+
name: "My Session",
|
|
68
|
+
source: "dashboard",
|
|
69
|
+
status: "ended",
|
|
70
|
+
startedAt: 1000,
|
|
71
|
+
cost: 5.0,
|
|
72
|
+
tokensIn: 100,
|
|
73
|
+
tokensOut: 200,
|
|
74
|
+
cachedAt: Date.now() + 10000, // far future = fresh cache
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const result = scanAllSessions(tmpDir);
|
|
78
|
+
expect(result.sessions).toHaveLength(1);
|
|
79
|
+
expect(result.sessions[0].id).toBe("abc-123");
|
|
80
|
+
expect(result.sessions[0].cwd).toBe("/test/cwd");
|
|
81
|
+
expect(result.sessions[0].name).toBe("My Session");
|
|
82
|
+
expect(result.sessions[0].cost).toBe(5.0);
|
|
83
|
+
expect(result.cacheUpdates).toBe(0); // no re-extraction needed
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should fall back to .jsonl parsing when no .meta.json exists", () => {
|
|
87
|
+
const dir = createSessionDir("--test-cwd--");
|
|
88
|
+
createJsonl(dir, "2026-03-30T21-39-43-034Z_def-456.jsonl", { id: "def-456", cwd: "/fallback/cwd" });
|
|
89
|
+
|
|
90
|
+
const result = scanAllSessions(tmpDir);
|
|
91
|
+
expect(result.sessions).toHaveLength(1);
|
|
92
|
+
expect(result.sessions[0].id).toBe("def-456");
|
|
93
|
+
expect(result.sessions[0].cwd).toBe("/fallback/cwd");
|
|
94
|
+
expect(result.sessions[0].firstMessage).toBe("Hello world");
|
|
95
|
+
expect(result.cacheUpdates).toBe(1); // wrote new .meta.json
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should write .meta.json for uncached sessions", () => {
|
|
99
|
+
const dir = createSessionDir("--test-cwd--");
|
|
100
|
+
const sf = createJsonl(dir, "2026-03-30T21-39-43-034Z_ghi-789.jsonl", { id: "ghi-789", cwd: "/new/cwd" });
|
|
101
|
+
|
|
102
|
+
scanAllSessions(tmpDir);
|
|
103
|
+
|
|
104
|
+
// .meta.json should now exist
|
|
105
|
+
expect(fs.existsSync(metaPath(sf))).toBe(true);
|
|
106
|
+
const meta = JSON.parse(fs.readFileSync(metaPath(sf), "utf-8"));
|
|
107
|
+
expect(meta.cwd).toBe("/new/cwd");
|
|
108
|
+
expect(meta.cachedAt).toBeGreaterThan(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should ignore orphaned .meta.json without .jsonl", () => {
|
|
112
|
+
const dir = createSessionDir("--test-cwd--");
|
|
113
|
+
// Write .meta.json without a corresponding .jsonl
|
|
114
|
+
const orphanedMeta = path.join(dir, "2026-03-30T21-39-43-034Z_orphan-id.meta.json");
|
|
115
|
+
fs.writeFileSync(orphanedMeta, JSON.stringify({ cwd: "/ghost", source: "dashboard" }));
|
|
116
|
+
|
|
117
|
+
const result = scanAllSessions(tmpDir);
|
|
118
|
+
expect(result.sessions).toHaveLength(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should extract session ID from filename", () => {
|
|
122
|
+
const dir = createSessionDir("--test-cwd--");
|
|
123
|
+
createJsonl(dir, "2026-03-30T21-39-43-034Z_c7ab4be9-78d1-4764-8197-dbf74fea8bf4.jsonl", {
|
|
124
|
+
id: "c7ab4be9-78d1-4764-8197-dbf74fea8bf4",
|
|
125
|
+
cwd: "/test",
|
|
126
|
+
});
|
|
127
|
+
writeSessionMeta(
|
|
128
|
+
path.join(dir, "2026-03-30T21-39-43-034Z_c7ab4be9-78d1-4764-8197-dbf74fea8bf4.jsonl"),
|
|
129
|
+
{ cwd: "/test", cachedAt: Date.now() + 10000 },
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const result = scanAllSessions(tmpDir);
|
|
133
|
+
expect(result.sessions[0].id).toBe("c7ab4be9-78d1-4764-8197-dbf74fea8bf4");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should re-extract stats when .jsonl is newer than cachedAt", () => {
|
|
137
|
+
const dir = createSessionDir("--test-cwd--");
|
|
138
|
+
const sf = createJsonl(dir, "2026-03-30T21-39-43-034Z_stale-id.jsonl", { id: "stale-id", cwd: "/stale" });
|
|
139
|
+
|
|
140
|
+
// Write meta with old cachedAt
|
|
141
|
+
writeSessionMeta(sf, {
|
|
142
|
+
cwd: "/stale",
|
|
143
|
+
cost: 1.0,
|
|
144
|
+
cachedAt: 1000, // very old
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Touch the .jsonl to make it newer
|
|
148
|
+
const now = new Date();
|
|
149
|
+
fs.utimesSync(sf, now, now);
|
|
150
|
+
|
|
151
|
+
const result = scanAllSessions(tmpDir);
|
|
152
|
+
expect(result.sessions).toHaveLength(1);
|
|
153
|
+
// Stats should come from mock extractSessionStats (cost=0.5), not cached (cost=1.0)
|
|
154
|
+
expect(result.sessions[0].cost).toBe(0.5);
|
|
155
|
+
expect(result.cacheUpdates).toBe(1);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should scan multiple cwd directories", () => {
|
|
159
|
+
const dir1 = createSessionDir("--project-a--");
|
|
160
|
+
const dir2 = createSessionDir("--project-b--");
|
|
161
|
+
createJsonl(dir1, "2026-03-30T21-39-43-034Z_id-a.jsonl", { id: "id-a", cwd: "/project/a" });
|
|
162
|
+
createJsonl(dir2, "2026-03-30T21-39-43-034Z_id-b.jsonl", { id: "id-b", cwd: "/project/b" });
|
|
163
|
+
|
|
164
|
+
const result = scanAllSessions(tmpDir);
|
|
165
|
+
expect(result.sessions).toHaveLength(2);
|
|
166
|
+
const ids = result.sessions.map((s) => s.id).sort();
|
|
167
|
+
expect(ids).toEqual(["id-a", "id-b"]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should preserve existing meta fields when falling back to .jsonl", () => {
|
|
171
|
+
const dir = createSessionDir("--test-cwd--");
|
|
172
|
+
const sf = createJsonl(dir, "2026-03-30T21-39-43-034Z_preserve-id.jsonl", { id: "preserve-id", cwd: "/test" });
|
|
173
|
+
|
|
174
|
+
// Write partial meta (source only, no cwd — triggers fallback)
|
|
175
|
+
writeSessionMeta(sf, { source: "dashboard" });
|
|
176
|
+
|
|
177
|
+
const result = scanAllSessions(tmpDir);
|
|
178
|
+
expect(result.sessions).toHaveLength(1);
|
|
179
|
+
expect(result.sessions[0].source).toBe("dashboard");
|
|
180
|
+
|
|
181
|
+
// Check the written meta preserved source
|
|
182
|
+
const meta = JSON.parse(fs.readFileSync(metaPath(sf), "utf-8"));
|
|
183
|
+
expect(meta.source).toBe("dashboard");
|
|
184
|
+
expect(meta.cwd).toBe("/test");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should set hidden from meta", () => {
|
|
188
|
+
const dir = createSessionDir("--test-cwd--");
|
|
189
|
+
const sf = createJsonl(dir, "2026-03-30T21-39-43-034Z_hidden-id.jsonl", { id: "hidden-id", cwd: "/test" });
|
|
190
|
+
writeSessionMeta(sf, {
|
|
191
|
+
cwd: "/test",
|
|
192
|
+
hidden: true,
|
|
193
|
+
cachedAt: Date.now() + 10000,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const result = scanAllSessions(tmpDir);
|
|
197
|
+
expect(result.sessions[0].hidden).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for POST /api/shutdown endpoint.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
5
|
+
import { createServer, type DashboardServer } from "../server.js";
|
|
6
|
+
|
|
7
|
+
const httpPort = 19080;
|
|
8
|
+
const piPort = 19081;
|
|
9
|
+
let server: DashboardServer;
|
|
10
|
+
|
|
11
|
+
// Mock process.exit to prevent actually exiting
|
|
12
|
+
const mockExit = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
|
|
13
|
+
|
|
14
|
+
describe("POST /api/shutdown", () => {
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
mockExit.mockClear();
|
|
17
|
+
if (server) {
|
|
18
|
+
try { await server.stop(); } catch { /* already stopped */ }
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should respond with { ok: true }", async () => {
|
|
23
|
+
server = await createServer({
|
|
24
|
+
port: httpPort, piPort, dev: true,
|
|
25
|
+
autoShutdown: false, shutdownIdleSeconds: 999, tunnel: false,
|
|
26
|
+
editor: { idleTimeoutMinutes: 10, maxInstances: 3 },
|
|
27
|
+
});
|
|
28
|
+
await server.start();
|
|
29
|
+
|
|
30
|
+
const res = await fetch(`http://localhost:${httpPort}/api/shutdown`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(res.ok).toBe(true);
|
|
35
|
+
const body = await res.json();
|
|
36
|
+
expect(body).toEqual({ ok: true });
|
|
37
|
+
|
|
38
|
+
// Give the setTimeout a chance to fire
|
|
39
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
40
|
+
expect(mockExit).toHaveBeenCalledWith(0);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for bridge reconnect skip-wipe logic in event-wiring.
|
|
3
|
+
* When bridge sends eventCount matching server's stored count, skip the event wipe.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, vi } from "vitest";
|
|
6
|
+
import { createMemoryEventStore } from "../memory-event-store.js";
|
|
7
|
+
import { createMemorySessionManager } from "../memory-session-manager.js";
|
|
8
|
+
import type { DashboardEvent } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
9
|
+
|
|
10
|
+
function makeEvent(type: string = "test"): DashboardEvent {
|
|
11
|
+
return { eventType: type, timestamp: Date.now(), data: {} };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Minimal simulation of the session_register path in event-wiring.ts.
|
|
16
|
+
* We test the skip-wipe decision logic in isolation.
|
|
17
|
+
*/
|
|
18
|
+
function simulateSessionRegister(opts: {
|
|
19
|
+
eventStore: ReturnType<typeof createMemoryEventStore>;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
previousSessionId?: string;
|
|
22
|
+
eventCount?: number;
|
|
23
|
+
}) {
|
|
24
|
+
const { eventStore, sessionId, previousSessionId, eventCount } = opts;
|
|
25
|
+
const wiped = { value: false };
|
|
26
|
+
const resetSent = { value: false };
|
|
27
|
+
|
|
28
|
+
// Decision logic matching event-wiring.ts
|
|
29
|
+
const sameSession = !previousSessionId || previousSessionId === sessionId;
|
|
30
|
+
const serverEventCount = eventStore.getEvents(sessionId, 1).length;
|
|
31
|
+
const canSkipWipe = sameSession && eventCount !== undefined && eventCount === serverEventCount;
|
|
32
|
+
|
|
33
|
+
if (!canSkipWipe) {
|
|
34
|
+
eventStore.deleteEventsForSession(sessionId);
|
|
35
|
+
wiped.value = true;
|
|
36
|
+
resetSent.value = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { wiped: wiped.value, resetSent: resetSent.value };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("skip-wipe on bridge reconnect", () => {
|
|
43
|
+
it("skips wipe when eventCount matches server event count", () => {
|
|
44
|
+
const store = createMemoryEventStore(() => false);
|
|
45
|
+
store.insertEvent("s1", makeEvent("a"));
|
|
46
|
+
store.insertEvent("s1", makeEvent("b"));
|
|
47
|
+
store.insertEvent("s1", makeEvent("c"));
|
|
48
|
+
|
|
49
|
+
const result = simulateSessionRegister({
|
|
50
|
+
eventStore: store,
|
|
51
|
+
sessionId: "s1",
|
|
52
|
+
previousSessionId: "s1",
|
|
53
|
+
eventCount: 3,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result.wiped).toBe(false);
|
|
57
|
+
expect(result.resetSent).toBe(false);
|
|
58
|
+
// Events preserved
|
|
59
|
+
expect(store.getEvents("s1", 1)).toHaveLength(3);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("wipes when eventCount mismatches", () => {
|
|
63
|
+
const store = createMemoryEventStore(() => false);
|
|
64
|
+
store.insertEvent("s1", makeEvent("a"));
|
|
65
|
+
store.insertEvent("s1", makeEvent("b"));
|
|
66
|
+
|
|
67
|
+
const result = simulateSessionRegister({
|
|
68
|
+
eventStore: store,
|
|
69
|
+
sessionId: "s1",
|
|
70
|
+
previousSessionId: "s1",
|
|
71
|
+
eventCount: 5, // mismatch
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(result.wiped).toBe(true);
|
|
75
|
+
expect(result.resetSent).toBe(true);
|
|
76
|
+
expect(store.getEvents("s1", 1)).toHaveLength(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("wipes when eventCount is not provided (backward compat)", () => {
|
|
80
|
+
const store = createMemoryEventStore(() => false);
|
|
81
|
+
store.insertEvent("s1", makeEvent("a"));
|
|
82
|
+
|
|
83
|
+
const result = simulateSessionRegister({
|
|
84
|
+
eventStore: store,
|
|
85
|
+
sessionId: "s1",
|
|
86
|
+
previousSessionId: "s1",
|
|
87
|
+
eventCount: undefined,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(result.wiped).toBe(true);
|
|
91
|
+
expect(result.resetSent).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("wipes when session ID changed", () => {
|
|
95
|
+
const store = createMemoryEventStore(() => false);
|
|
96
|
+
store.insertEvent("s1", makeEvent("a"));
|
|
97
|
+
store.insertEvent("s1", makeEvent("b"));
|
|
98
|
+
|
|
99
|
+
const result = simulateSessionRegister({
|
|
100
|
+
eventStore: store,
|
|
101
|
+
sessionId: "s2", // different session
|
|
102
|
+
previousSessionId: "s1",
|
|
103
|
+
eventCount: 2,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(result.wiped).toBe(true);
|
|
107
|
+
expect(result.resetSent).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("wipes when no previous session (first connect)", () => {
|
|
111
|
+
const store = createMemoryEventStore(() => false);
|
|
112
|
+
|
|
113
|
+
const result = simulateSessionRegister({
|
|
114
|
+
eventStore: store,
|
|
115
|
+
sessionId: "s1",
|
|
116
|
+
previousSessionId: undefined,
|
|
117
|
+
eventCount: 5,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// No previous session = can't verify, so wipe
|
|
121
|
+
expect(result.wiped).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for sleep-aware heartbeat in pi-gateway.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, afterEach } 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
|
+
function makeTempSessionManager() {
|
|
21
|
+
return createMemorySessionManager();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Use a short heartbeat for fast tests
|
|
25
|
+
const SHORT_HB = 300; // 300ms
|
|
26
|
+
let portCounter = 19390;
|
|
27
|
+
|
|
28
|
+
describe("Sleep-aware heartbeat", () => {
|
|
29
|
+
let gateway: ReturnType<typeof createPiGateway>;
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
gateway?.stop();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should unregister session after normal heartbeat timeout", async () => {
|
|
36
|
+
const sessionManager = makeTempSessionManager();
|
|
37
|
+
gateway = createPiGateway(sessionManager, { heartbeatTimeout: SHORT_HB });
|
|
38
|
+
const port = portCounter++;
|
|
39
|
+
gateway.start(port);
|
|
40
|
+
|
|
41
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
42
|
+
await waitForOpen(ws);
|
|
43
|
+
ws.send(JSON.stringify({
|
|
44
|
+
type: "session_register", sessionId: "s1", cwd: "/tmp", source: "tui",
|
|
45
|
+
}));
|
|
46
|
+
await delay(100);
|
|
47
|
+
expect(sessionManager.get("s1")!.status).toBe("active");
|
|
48
|
+
|
|
49
|
+
// Close without unregister
|
|
50
|
+
ws.close();
|
|
51
|
+
await delay(100);
|
|
52
|
+
|
|
53
|
+
// Wait for heartbeat timeout
|
|
54
|
+
await delay(SHORT_HB + 200);
|
|
55
|
+
|
|
56
|
+
expect(sessionManager.get("s1")!.status).toBe("ended");
|
|
57
|
+
}, 10000);
|
|
58
|
+
|
|
59
|
+
it("should call onEmpty after heartbeat timeout", async () => {
|
|
60
|
+
const sessionManager = makeTempSessionManager();
|
|
61
|
+
gateway = createPiGateway(sessionManager, { heartbeatTimeout: SHORT_HB });
|
|
62
|
+
const port = portCounter++;
|
|
63
|
+
gateway.start(port);
|
|
64
|
+
|
|
65
|
+
let emptyCalled = false;
|
|
66
|
+
gateway.onEmpty = () => { emptyCalled = true; };
|
|
67
|
+
|
|
68
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
69
|
+
await waitForOpen(ws);
|
|
70
|
+
ws.send(JSON.stringify({
|
|
71
|
+
type: "session_register", sessionId: "s2", cwd: "/tmp", source: "tui",
|
|
72
|
+
}));
|
|
73
|
+
await delay(100);
|
|
74
|
+
|
|
75
|
+
ws.close();
|
|
76
|
+
await delay(SHORT_HB + 200);
|
|
77
|
+
|
|
78
|
+
expect(emptyCalled).toBe(true);
|
|
79
|
+
}, 10000);
|
|
80
|
+
|
|
81
|
+
it("should retry once when sleep is detected (simulated via Date.now override)", async () => {
|
|
82
|
+
const sessionManager = makeTempSessionManager();
|
|
83
|
+
// Use a longer timeout so we can manipulate Date.now between calls
|
|
84
|
+
const HB = 500;
|
|
85
|
+
gateway = createPiGateway(sessionManager, { heartbeatTimeout: HB });
|
|
86
|
+
const port = portCounter++;
|
|
87
|
+
gateway.start(port);
|
|
88
|
+
|
|
89
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
90
|
+
await waitForOpen(ws);
|
|
91
|
+
ws.send(JSON.stringify({
|
|
92
|
+
type: "session_register", sessionId: "s3", cwd: "/tmp", source: "tui",
|
|
93
|
+
}));
|
|
94
|
+
await delay(100);
|
|
95
|
+
expect(sessionManager.get("s3")!.status).toBe("active");
|
|
96
|
+
|
|
97
|
+
// Close connection
|
|
98
|
+
ws.close();
|
|
99
|
+
await delay(50);
|
|
100
|
+
|
|
101
|
+
// Simulate sleep: make Date.now() jump forward far beyond 2× timeout
|
|
102
|
+
const realNow = Date.now.bind(Date);
|
|
103
|
+
let offset = 0;
|
|
104
|
+
Date.now = () => realNow() + offset;
|
|
105
|
+
|
|
106
|
+
// Jump time forward to simulate sleep (10× timeout = clearly > 2×)
|
|
107
|
+
offset = HB * 10;
|
|
108
|
+
|
|
109
|
+
// Wait for the heartbeat timer to fire (it uses real setTimeout)
|
|
110
|
+
await delay(HB + 200);
|
|
111
|
+
|
|
112
|
+
// Session should still be active (sleep detected, one retry granted)
|
|
113
|
+
expect(sessionManager.get("s3")!.status).toBe("active");
|
|
114
|
+
|
|
115
|
+
// Reset time back to normal
|
|
116
|
+
offset = 0;
|
|
117
|
+
|
|
118
|
+
// Wait for the retry heartbeat to fire
|
|
119
|
+
await delay(HB + 200);
|
|
120
|
+
|
|
121
|
+
// Now it should be ended (second timeout, no sleep detected)
|
|
122
|
+
expect(sessionManager.get("s3")!.status).toBe("ended");
|
|
123
|
+
|
|
124
|
+
Date.now = realNow;
|
|
125
|
+
}, 10000);
|
|
126
|
+
});
|