@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,143 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Mock the storage and handlers to avoid touching real auth.json
|
|
4
|
+
vi.mock("../provider-auth-storage.js", () => ({
|
|
5
|
+
getOAuthProvidersMeta: () => [
|
|
6
|
+
{ id: "anthropic", name: "Anthropic", flowType: "auth_code" },
|
|
7
|
+
{ id: "github-copilot", name: "GitHub Copilot", flowType: "device_code" },
|
|
8
|
+
],
|
|
9
|
+
getAuthStatus: () => [
|
|
10
|
+
{ id: "anthropic", name: "Anthropic", flowType: "auth_code", authenticated: false },
|
|
11
|
+
{ id: "github-copilot", name: "GitHub Copilot", flowType: "device_code", authenticated: true, expires: Date.now() + 86400000 },
|
|
12
|
+
],
|
|
13
|
+
writeCredential: vi.fn(),
|
|
14
|
+
removeCredential: vi.fn(),
|
|
15
|
+
resolveAuthJsonKey: (id: string) => id,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// Mock the callback server to avoid opening real ports
|
|
19
|
+
vi.mock("../oauth-callback-server.js", () => ({
|
|
20
|
+
startCallbackServer: vi.fn().mockResolvedValue({
|
|
21
|
+
closed: new Promise(() => {}),
|
|
22
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
}),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
// Mock child_process.exec to avoid opening a browser
|
|
27
|
+
vi.mock("node:child_process", () => ({
|
|
28
|
+
exec: vi.fn(),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
import Fastify from "fastify";
|
|
32
|
+
import { registerProviderAuthRoutes } from "../routes/provider-auth-routes.js";
|
|
33
|
+
|
|
34
|
+
function createMockPiGateway() {
|
|
35
|
+
return {
|
|
36
|
+
broadcast: vi.fn(),
|
|
37
|
+
start: vi.fn(),
|
|
38
|
+
stop: vi.fn(),
|
|
39
|
+
sendToSession: vi.fn(),
|
|
40
|
+
connectionCount: () => 0,
|
|
41
|
+
findSessionByCwd: () => undefined,
|
|
42
|
+
getConnectedSessionIds: () => [],
|
|
43
|
+
isSessionConnected: () => false,
|
|
44
|
+
} as any;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("provider-auth-routes", () => {
|
|
48
|
+
let app: ReturnType<typeof Fastify>;
|
|
49
|
+
let piGateway: ReturnType<typeof createMockPiGateway>;
|
|
50
|
+
|
|
51
|
+
beforeEach(async () => {
|
|
52
|
+
app = Fastify();
|
|
53
|
+
piGateway = createMockPiGateway();
|
|
54
|
+
registerProviderAuthRoutes(app, { piGateway });
|
|
55
|
+
await app.ready();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("GET /api/provider-auth/providers returns OAuth provider list", async () => {
|
|
59
|
+
const res = await app.inject({ method: "GET", url: "/api/provider-auth/providers" });
|
|
60
|
+
expect(res.statusCode).toBe(200);
|
|
61
|
+
const data = JSON.parse(res.payload);
|
|
62
|
+
expect(data).toHaveLength(2);
|
|
63
|
+
expect(data[0].id).toBe("anthropic");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("GET /api/provider-auth/status returns all provider statuses", async () => {
|
|
67
|
+
const res = await app.inject({ method: "GET", url: "/api/provider-auth/status" });
|
|
68
|
+
expect(res.statusCode).toBe(200);
|
|
69
|
+
const data = JSON.parse(res.payload);
|
|
70
|
+
expect(data).toHaveLength(2);
|
|
71
|
+
expect(data[0].authenticated).toBe(false);
|
|
72
|
+
expect(data[1].authenticated).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("POST /api/provider-auth/authorize returns authUrl with correct redirect URI", async () => {
|
|
76
|
+
const { startCallbackServer } = await import("../oauth-callback-server.js");
|
|
77
|
+
const res = await app.inject({
|
|
78
|
+
method: "POST",
|
|
79
|
+
url: "/api/provider-auth/authorize",
|
|
80
|
+
payload: { provider: "anthropic" },
|
|
81
|
+
});
|
|
82
|
+
expect(res.statusCode).toBe(200);
|
|
83
|
+
const data = JSON.parse(res.payload);
|
|
84
|
+
expect(data.flowId).toBeTruthy();
|
|
85
|
+
expect(data.authUrl).toContain("claude.ai/oauth/authorize");
|
|
86
|
+
// Verify redirect URI uses the registered callback port/path
|
|
87
|
+
expect(data.authUrl).toContain(encodeURIComponent("http://localhost:53692/callback"));
|
|
88
|
+
// Verify callback server was started
|
|
89
|
+
expect(startCallbackServer).toHaveBeenCalledWith(
|
|
90
|
+
expect.objectContaining({
|
|
91
|
+
providerId: "anthropic",
|
|
92
|
+
port: 53692,
|
|
93
|
+
path: "/callback",
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("POST /api/provider-auth/authorize rejects unknown provider", async () => {
|
|
99
|
+
const res = await app.inject({
|
|
100
|
+
method: "POST",
|
|
101
|
+
url: "/api/provider-auth/authorize",
|
|
102
|
+
payload: { provider: "nope" },
|
|
103
|
+
});
|
|
104
|
+
expect(res.statusCode).toBe(400);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// /exchange endpoint removed — token exchange happens in the callback server's onCode
|
|
108
|
+
|
|
109
|
+
it("PUT /api/provider-auth/api-key saves and notifies", async () => {
|
|
110
|
+
const { writeCredential } = await import("../provider-auth-storage.js");
|
|
111
|
+
const res = await app.inject({
|
|
112
|
+
method: "PUT",
|
|
113
|
+
url: "/api/provider-auth/api-key",
|
|
114
|
+
payload: { provider: "openai", key: "sk-test" },
|
|
115
|
+
});
|
|
116
|
+
expect(res.statusCode).toBe(200);
|
|
117
|
+
expect(JSON.parse(res.payload).ok).toBe(true);
|
|
118
|
+
expect(writeCredential).toHaveBeenCalledWith("openai", { type: "api_key", key: "sk-test" });
|
|
119
|
+
expect(piGateway.broadcast).toHaveBeenCalledWith({ type: "credentials_updated" });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("DELETE /api/provider-auth/:provider removes and notifies", async () => {
|
|
123
|
+
const { removeCredential } = await import("../provider-auth-storage.js");
|
|
124
|
+
const res = await app.inject({
|
|
125
|
+
method: "DELETE",
|
|
126
|
+
url: "/api/provider-auth/anthropic",
|
|
127
|
+
});
|
|
128
|
+
expect(res.statusCode).toBe(200);
|
|
129
|
+
expect(removeCredential).toHaveBeenCalledWith("anthropic");
|
|
130
|
+
expect(piGateway.broadcast).toHaveBeenCalledWith({ type: "credentials_updated" });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// /callback/:provider route removed — temp callback server handles this directly
|
|
134
|
+
|
|
135
|
+
it("POST /api/provider-auth/device-code rejects auth_code provider", async () => {
|
|
136
|
+
const res = await app.inject({
|
|
137
|
+
method: "POST",
|
|
138
|
+
url: "/api/provider-auth/device-code",
|
|
139
|
+
payload: { provider: "anthropic" },
|
|
140
|
+
});
|
|
141
|
+
expect(res.statusCode).toBe(400);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
|
|
6
|
+
// We test by importing the module and using a temp directory
|
|
7
|
+
// Since the module uses hardcoded paths, we mock fs operations
|
|
8
|
+
|
|
9
|
+
describe("provider-auth-storage", () => {
|
|
10
|
+
const authDir = path.join(os.homedir(), ".pi", "agent");
|
|
11
|
+
const authPath = path.join(authDir, "auth.json");
|
|
12
|
+
let originalContent: string | null = null;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
// Backup existing auth.json
|
|
16
|
+
try {
|
|
17
|
+
originalContent = fs.readFileSync(authPath, "utf-8");
|
|
18
|
+
} catch {
|
|
19
|
+
originalContent = null;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
// Restore original auth.json
|
|
25
|
+
if (originalContent !== null) {
|
|
26
|
+
fs.writeFileSync(authPath, originalContent);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("readAuthJson returns empty object when file does not exist", async () => {
|
|
31
|
+
// Use dynamic import to get fresh module
|
|
32
|
+
const { readAuthJson } = await import("../provider-auth-storage.js");
|
|
33
|
+
// readAuthJson handles ENOENT gracefully
|
|
34
|
+
const result = readAuthJson();
|
|
35
|
+
expect(typeof result).toBe("object");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("writeCredential and readAuthJson roundtrip", async () => {
|
|
39
|
+
const { writeCredential, readAuthJson } = await import("../provider-auth-storage.js");
|
|
40
|
+
const cred = { type: "api_key" as const, key: "test-key-123" };
|
|
41
|
+
writeCredential("test-provider", cred);
|
|
42
|
+
const data = readAuthJson();
|
|
43
|
+
expect(data["test-provider"]).toEqual(cred);
|
|
44
|
+
// Cleanup
|
|
45
|
+
const { removeCredential } = await import("../provider-auth-storage.js");
|
|
46
|
+
removeCredential("test-provider");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("removeCredential removes the entry", async () => {
|
|
50
|
+
const { writeCredential, removeCredential, readAuthJson } = await import("../provider-auth-storage.js");
|
|
51
|
+
writeCredential("test-remove", { type: "api_key" as const, key: "x" });
|
|
52
|
+
removeCredential("test-remove");
|
|
53
|
+
const data = readAuthJson();
|
|
54
|
+
expect(data["test-remove"]).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("getAuthStatus returns all providers", async () => {
|
|
58
|
+
const { getAuthStatus } = await import("../provider-auth-storage.js");
|
|
59
|
+
const statuses = getAuthStatus();
|
|
60
|
+
// Should have at least the 5 OAuth providers
|
|
61
|
+
const oauthIds = statuses.filter((s) => s.flowType !== "api_key").map((s) => s.id);
|
|
62
|
+
expect(oauthIds).toContain("anthropic");
|
|
63
|
+
expect(oauthIds).toContain("openai-codex");
|
|
64
|
+
expect(oauthIds).toContain("github-copilot");
|
|
65
|
+
expect(oauthIds).toContain("google-gemini-cli");
|
|
66
|
+
expect(oauthIds).toContain("google-antigravity");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("getAuthStatus includes zai provider with flowType api_key", async () => {
|
|
70
|
+
const { getAuthStatus } = await import("../provider-auth-storage.js");
|
|
71
|
+
const statuses = getAuthStatus();
|
|
72
|
+
const zai = statuses.find((s) => s.id === "zai");
|
|
73
|
+
expect(zai).toBeDefined();
|
|
74
|
+
expect(zai!.name).toBe("Z.ai");
|
|
75
|
+
expect(zai!.flowType).toBe("api_key");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("masking shows first 5 + ... + last 3 for keys >= 12 chars", async () => {
|
|
79
|
+
const { writeCredential, getAuthStatus, removeCredential } = await import("../provider-auth-storage.js");
|
|
80
|
+
writeCredential("openai", { type: "api_key", key: "sk-abc123xyz789" });
|
|
81
|
+
try {
|
|
82
|
+
const statuses = getAuthStatus();
|
|
83
|
+
const openai = statuses.find((s) => s.id === "openai");
|
|
84
|
+
expect(openai!.maskedKey).toBe("sk-ab...789");
|
|
85
|
+
} finally {
|
|
86
|
+
removeCredential("openai");
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("masking returns **** for keys < 12 chars", async () => {
|
|
91
|
+
const { writeCredential, getAuthStatus, removeCredential } = await import("../provider-auth-storage.js");
|
|
92
|
+
writeCredential("openai", { type: "api_key", key: "shortkey" });
|
|
93
|
+
try {
|
|
94
|
+
const statuses = getAuthStatus();
|
|
95
|
+
const openai = statuses.find((s) => s.id === "openai");
|
|
96
|
+
expect(openai!.maskedKey).toBe("****");
|
|
97
|
+
} finally {
|
|
98
|
+
removeCredential("openai");
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("empty key string results in authenticated false with no maskedKey", async () => {
|
|
103
|
+
const { writeCredential, getAuthStatus, removeCredential } = await import("../provider-auth-storage.js");
|
|
104
|
+
writeCredential("openai", { type: "api_key", key: "" });
|
|
105
|
+
try {
|
|
106
|
+
const statuses = getAuthStatus();
|
|
107
|
+
const openai = statuses.find((s) => s.id === "openai");
|
|
108
|
+
expect(openai!.authenticated).toBe(false);
|
|
109
|
+
expect(openai!.maskedKey).toBeUndefined();
|
|
110
|
+
} finally {
|
|
111
|
+
removeCredential("openai");
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { safeRealpathSync } from "../resolve-path.js";
|
|
4
|
+
|
|
5
|
+
describe("safeRealpathSync", () => {
|
|
6
|
+
it("resolves a real path", () => {
|
|
7
|
+
// process.cwd() is always a real path
|
|
8
|
+
const result = safeRealpathSync(process.cwd());
|
|
9
|
+
expect(result).toBe(fs.realpathSync(process.cwd()));
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("falls back to original when path does not exist", () => {
|
|
13
|
+
const fakePath = "/nonexistent/path/that/does/not/exist";
|
|
14
|
+
const result = safeRealpathSync(fakePath);
|
|
15
|
+
expect(result).toBe(fakePath);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("resolves symlinks", () => {
|
|
19
|
+
const os = require("node:os");
|
|
20
|
+
const path = require("node:path");
|
|
21
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "resolve-test-"));
|
|
22
|
+
const realDir = path.join(tmpDir, "real");
|
|
23
|
+
const linkDir = path.join(tmpDir, "link");
|
|
24
|
+
|
|
25
|
+
const realTmpDir = fs.realpathSync(tmpDir);
|
|
26
|
+
const target = path.join(realTmpDir, "real");
|
|
27
|
+
const link = path.join(realTmpDir, "link");
|
|
28
|
+
|
|
29
|
+
fs.mkdirSync(target);
|
|
30
|
+
fs.symlinkSync(target, link);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
expect(safeRealpathSync(link)).toBe(target);
|
|
34
|
+
} finally {
|
|
35
|
+
fs.rmSync(realTmpDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { RingBuffer } from "../terminal-manager.js";
|
|
3
|
+
|
|
4
|
+
describe("RingBuffer", () => {
|
|
5
|
+
it("stores and returns written data", () => {
|
|
6
|
+
const buf = new RingBuffer(64);
|
|
7
|
+
buf.write(Buffer.from("hello"));
|
|
8
|
+
expect(buf.contents().toString()).toBe("hello");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("appends multiple writes", () => {
|
|
12
|
+
const buf = new RingBuffer(64);
|
|
13
|
+
buf.write(Buffer.from("hello "));
|
|
14
|
+
buf.write(Buffer.from("world"));
|
|
15
|
+
expect(buf.contents().toString()).toBe("hello world");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("returns empty buffer when nothing written", () => {
|
|
19
|
+
const buf = new RingBuffer(64);
|
|
20
|
+
expect(buf.contents().length).toBe(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("overwrites oldest data when capacity exceeded", () => {
|
|
24
|
+
const buf = new RingBuffer(8);
|
|
25
|
+
buf.write(Buffer.from("12345678")); // fills exactly
|
|
26
|
+
buf.write(Buffer.from("AB")); // overwrites first 2
|
|
27
|
+
expect(buf.contents().toString()).toBe("345678AB");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("handles write larger than capacity", () => {
|
|
31
|
+
const buf = new RingBuffer(4);
|
|
32
|
+
buf.write(Buffer.from("ABCDEFGH"));
|
|
33
|
+
// Only last 4 bytes should remain
|
|
34
|
+
expect(buf.contents().toString()).toBe("EFGH");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("handles many small writes wrapping around", () => {
|
|
38
|
+
const buf = new RingBuffer(6);
|
|
39
|
+
buf.write(Buffer.from("AAA"));
|
|
40
|
+
buf.write(Buffer.from("BBB"));
|
|
41
|
+
buf.write(Buffer.from("CC"));
|
|
42
|
+
// Should have BBBCC... last 6 = "BBBCC" wait: AAA + BBB = 6 bytes exactly, then CC overwrites first 2
|
|
43
|
+
expect(buf.contents().toString()).toBe("ABBBCC");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
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 { writePid, readPid, removePid, isProcessAlive } from "../server-pid.js";
|
|
6
|
+
|
|
7
|
+
describe("server-pid", () => {
|
|
8
|
+
const tmpDir = path.join(os.tmpdir(), "pi-dashboard-test-pid-" + process.pid);
|
|
9
|
+
const pidPath = path.join(tmpDir, "server.pid");
|
|
10
|
+
const opts = { pidPath };
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("writePid", () => {
|
|
21
|
+
it("writes PID to file", () => {
|
|
22
|
+
writePid(12345, opts);
|
|
23
|
+
const content = fs.readFileSync(pidPath, "utf-8").trim();
|
|
24
|
+
expect(content).toBe("12345");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("creates parent directories", () => {
|
|
28
|
+
const nestedPath = path.join(tmpDir, "nested", "dir", "server.pid");
|
|
29
|
+
writePid(99, { pidPath: nestedPath });
|
|
30
|
+
expect(fs.existsSync(nestedPath)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("readPid", () => {
|
|
35
|
+
it("reads PID from file", () => {
|
|
36
|
+
fs.writeFileSync(pidPath, "42\n");
|
|
37
|
+
expect(readPid(opts)).toBe(42);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns null for missing file", () => {
|
|
41
|
+
expect(readPid(opts)).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns null for invalid content", () => {
|
|
45
|
+
fs.writeFileSync(pidPath, "not-a-number\n");
|
|
46
|
+
expect(readPid(opts)).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns null for zero", () => {
|
|
50
|
+
fs.writeFileSync(pidPath, "0\n");
|
|
51
|
+
expect(readPid(opts)).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns null for negative number", () => {
|
|
55
|
+
fs.writeFileSync(pidPath, "-1\n");
|
|
56
|
+
expect(readPid(opts)).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("removePid", () => {
|
|
61
|
+
it("removes existing PID file", () => {
|
|
62
|
+
fs.writeFileSync(pidPath, "42\n");
|
|
63
|
+
removePid(opts);
|
|
64
|
+
expect(fs.existsSync(pidPath)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("does not throw for missing file", () => {
|
|
68
|
+
expect(() => removePid(opts)).not.toThrow();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("isProcessAlive", () => {
|
|
73
|
+
it("returns true for current process", () => {
|
|
74
|
+
expect(isProcessAlive(process.pid)).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns false for non-existent PID", () => {
|
|
78
|
+
// Use a very high PID that's unlikely to exist
|
|
79
|
+
expect(isProcessAlive(999999999)).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("writePid + readPid roundtrip", () => {
|
|
84
|
+
it("can write and read back", () => {
|
|
85
|
+
writePid(55555, opts);
|
|
86
|
+
expect(readPid(opts)).toBe(55555);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for session control REST API endpoints (session-api.ts).
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, afterAll, beforeAll, vi } from "vitest";
|
|
5
|
+
import { createServer, type DashboardServer } from "../server.js";
|
|
6
|
+
|
|
7
|
+
const httpPort = 19200;
|
|
8
|
+
const piPort = 19201;
|
|
9
|
+
let server: DashboardServer;
|
|
10
|
+
|
|
11
|
+
// Mock spawnPiSession to avoid actually spawning processes
|
|
12
|
+
vi.mock("../process-manager.js", async (importOriginal) => {
|
|
13
|
+
const orig: any = await importOriginal();
|
|
14
|
+
return {
|
|
15
|
+
...orig,
|
|
16
|
+
spawnPiSession: vi.fn().mockResolvedValue({ success: true, message: "spawned" }),
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function url(path: string) {
|
|
21
|
+
return `http://localhost:${httpPort}${path}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function postJson(path: string, body?: Record<string, unknown>) {
|
|
25
|
+
return fetch(url(path), {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "Content-Type": "application/json" },
|
|
28
|
+
body: JSON.stringify(body ?? {}),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Register a fresh session, returning its id */
|
|
33
|
+
function registerSession(id: string, overrides?: Record<string, unknown>) {
|
|
34
|
+
server.sessionManager.register({
|
|
35
|
+
id,
|
|
36
|
+
cwd: "/tmp/test",
|
|
37
|
+
source: "tui" as const,
|
|
38
|
+
startedAt: Date.now(),
|
|
39
|
+
...overrides,
|
|
40
|
+
});
|
|
41
|
+
return id;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("Session Control REST API", () => {
|
|
45
|
+
beforeAll(async () => {
|
|
46
|
+
server = await createServer({
|
|
47
|
+
port: httpPort,
|
|
48
|
+
piPort,
|
|
49
|
+
dev: true,
|
|
50
|
+
autoShutdown: false,
|
|
51
|
+
shutdownIdleSeconds: 999,
|
|
52
|
+
tunnel: false,
|
|
53
|
+
editor: { idleTimeoutMinutes: 10, maxInstances: 3 },
|
|
54
|
+
});
|
|
55
|
+
await server.start();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterAll(async () => {
|
|
59
|
+
if (server) {
|
|
60
|
+
try { await server.stop(); } catch { /* */ }
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ── prompt ──────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
it("POST /api/session/:id/prompt — 404 for unknown session", async () => {
|
|
67
|
+
const res = await postJson("/api/session/unknown-id/prompt", { text: "hello" });
|
|
68
|
+
expect(res.status).toBe(404);
|
|
69
|
+
const body = await res.json();
|
|
70
|
+
expect(body.success).toBe(false);
|
|
71
|
+
expect(body.error).toBe("session not found");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("POST /api/session/:id/prompt — 400 when text missing", async () => {
|
|
75
|
+
const res = await postJson("/api/session/any-id/prompt", {});
|
|
76
|
+
expect(res.status).toBe(400);
|
|
77
|
+
expect((await res.json()).error).toBe("text is required");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("POST /api/session/:id/prompt — 502 when no bridge connection", async () => {
|
|
81
|
+
registerSession("prompt-no-bridge");
|
|
82
|
+
const res = await postJson("/api/session/prompt-no-bridge/prompt", { text: "hello" });
|
|
83
|
+
expect(res.status).toBe(502);
|
|
84
|
+
expect((await res.json()).error).toBe("no bridge connection for session");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ── abort ───────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
it("POST /api/session/:id/abort — 404 for unknown", async () => {
|
|
90
|
+
const res = await postJson("/api/session/unknown/abort");
|
|
91
|
+
expect(res.status).toBe(404);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("POST /api/session/:id/abort — success for known session", async () => {
|
|
95
|
+
registerSession("abort-ok");
|
|
96
|
+
const res = await postJson("/api/session/abort-ok/abort");
|
|
97
|
+
expect(res.status).toBe(200);
|
|
98
|
+
expect((await res.json()).success).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ── shutdown ────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
it("POST /api/session/:id/shutdown — 404 for unknown", async () => {
|
|
104
|
+
const res = await postJson("/api/session/unknown/shutdown");
|
|
105
|
+
expect(res.status).toBe(404);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("POST /api/session/:id/shutdown — unregisters session", async () => {
|
|
109
|
+
registerSession("shutdown-me");
|
|
110
|
+
const res = await postJson("/api/session/shutdown-me/shutdown");
|
|
111
|
+
expect(res.status).toBe(200);
|
|
112
|
+
expect((await res.json()).success).toBe(true);
|
|
113
|
+
expect(server.sessionManager.get("shutdown-me")?.status).toBe("ended");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ── rename ──────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
it("POST /api/session/:id/rename — 400 when name missing", async () => {
|
|
119
|
+
const res = await postJson("/api/session/any/rename", {});
|
|
120
|
+
expect(res.status).toBe(400);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("POST /api/session/:id/rename — renames session", async () => {
|
|
124
|
+
registerSession("rename-me");
|
|
125
|
+
const res = await postJson("/api/session/rename-me/rename", { name: "new-name" });
|
|
126
|
+
expect(res.status).toBe(200);
|
|
127
|
+
expect(server.sessionManager.get("rename-me")?.name).toBe("new-name");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ── hide/unhide ─────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
it("POST /api/session/:id/hide — hides session", async () => {
|
|
133
|
+
registerSession("hide-me");
|
|
134
|
+
const res = await postJson("/api/session/hide-me/hide");
|
|
135
|
+
expect(res.status).toBe(200);
|
|
136
|
+
expect(server.sessionManager.get("hide-me")?.hidden).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("POST /api/session/:id/unhide — unhides session", async () => {
|
|
140
|
+
registerSession("unhide-me");
|
|
141
|
+
server.sessionManager.update("unhide-me", { hidden: true });
|
|
142
|
+
const res = await postJson("/api/session/unhide-me/unhide");
|
|
143
|
+
expect(res.status).toBe(200);
|
|
144
|
+
expect(server.sessionManager.get("unhide-me")?.hidden).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ── spawn ───────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
it("POST /api/session/spawn — 400 when cwd missing", async () => {
|
|
150
|
+
const res = await postJson("/api/session/spawn", {});
|
|
151
|
+
expect(res.status).toBe(400);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("POST /api/session/spawn — success with valid cwd", async () => {
|
|
155
|
+
const res = await postJson("/api/session/spawn", { cwd: "/tmp/project" });
|
|
156
|
+
expect(res.status).toBe(200);
|
|
157
|
+
expect((await res.json()).success).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ── resume ──────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
it("POST /api/session/:id/resume — 400 for invalid mode", async () => {
|
|
163
|
+
const res = await postJson("/api/session/any/resume", { mode: "invalid" });
|
|
164
|
+
expect(res.status).toBe(400);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("POST /api/session/:id/resume — 404 for unknown session", async () => {
|
|
168
|
+
const res = await postJson("/api/session/unknown/resume", { mode: "continue" });
|
|
169
|
+
expect(res.status).toBe(404);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("POST /api/session/:id/resume — 409 if session still active", async () => {
|
|
173
|
+
registerSession("resume-active", { sessionFile: "/path/session.jsonl" });
|
|
174
|
+
const res = await postJson("/api/session/resume-active/resume", { mode: "continue" });
|
|
175
|
+
expect(res.status).toBe(409);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ── flow-control ────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
it("POST /api/session/:id/flow-control — 400 for invalid action", async () => {
|
|
181
|
+
const res = await postJson("/api/session/any/flow-control", { action: "invalid" });
|
|
182
|
+
expect(res.status).toBe(400);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("POST /api/session/:id/flow-control — success", async () => {
|
|
186
|
+
registerSession("flow-ctrl");
|
|
187
|
+
const res = await postJson("/api/session/flow-ctrl/flow-control", { action: "abort" });
|
|
188
|
+
expect(res.status).toBe(200);
|
|
189
|
+
expect((await res.json()).success).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ── model ───────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
it("POST /api/session/:id/model — 400 when missing fields", async () => {
|
|
195
|
+
const res = await postJson("/api/session/any/model", { provider: "anthropic" });
|
|
196
|
+
expect(res.status).toBe(400);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("POST /api/session/:id/model — success", async () => {
|
|
200
|
+
registerSession("model-set");
|
|
201
|
+
const res = await postJson("/api/session/model-set/model", {
|
|
202
|
+
provider: "anthropic",
|
|
203
|
+
modelId: "claude-sonnet-4-20250514",
|
|
204
|
+
});
|
|
205
|
+
expect(res.status).toBe(200);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ── thinking-level ──────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
it("POST /api/session/:id/thinking-level — 400 when missing", async () => {
|
|
211
|
+
const res = await postJson("/api/session/any/thinking-level", {});
|
|
212
|
+
expect(res.status).toBe(400);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("POST /api/session/:id/thinking-level — success", async () => {
|
|
216
|
+
registerSession("think-set");
|
|
217
|
+
const res = await postJson("/api/session/think-set/thinking-level", { level: "high" });
|
|
218
|
+
expect(res.status).toBe(200);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ── attach/detach proposal ──────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
it("POST /api/session/:id/attach-proposal — 400 when changeName missing", async () => {
|
|
224
|
+
const res = await postJson("/api/session/any/attach-proposal", {});
|
|
225
|
+
expect(res.status).toBe(400);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("POST /api/session/:id/attach-proposal — attaches and auto-names", async () => {
|
|
229
|
+
registerSession("attach-me");
|
|
230
|
+
const res = await postJson("/api/session/attach-me/attach-proposal", { changeName: "add-feature" });
|
|
231
|
+
expect(res.status).toBe(200);
|
|
232
|
+
const session = server.sessionManager.get("attach-me");
|
|
233
|
+
expect(session?.attachedProposal).toBe("add-feature");
|
|
234
|
+
expect(session?.name).toBe("add-feature"); // auto-named
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("POST /api/session/:id/detach-proposal — detaches", async () => {
|
|
238
|
+
registerSession("detach-me");
|
|
239
|
+
server.sessionManager.update("detach-me", { attachedProposal: "some-change" });
|
|
240
|
+
const res = await postJson("/api/session/detach-me/detach-proposal");
|
|
241
|
+
expect(res.status).toBe(200);
|
|
242
|
+
expect(server.sessionManager.get("detach-me")?.attachedProposal).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
});
|