@opengsd/gsd-pi 1.0.2-dev.50223bc → 1.0.2-dev.5961fbf
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/dist/resource-loader.d.ts +5 -0
- package/dist/resource-loader.js +24 -8
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/loop.js +19 -0
- package/dist/resources/extensions/gsd/auto/phases.js +1 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +2 -54
- package/dist/resources/extensions/gsd/worktree-post-create-hook.js +117 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
- package/dist/web/standalone/.next/server/chunks/1834.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/dist/web/standalone/package.json +0 -1
- package/dist/worktree-cli.d.ts +0 -2
- package/dist/worktree-cli.js +21 -9
- package/package.json +9 -4
- package/packages/cloud-mcp-gateway/bin/gsd-cloud-mcp-gateway.js +14 -0
- package/packages/cloud-mcp-gateway/package.json +5 -4
- package/packages/contracts/package.json +2 -2
- package/packages/daemon/bin/gsd-daemon.js +14 -0
- package/packages/daemon/bin/gsd-mcp-runtime.js +14 -0
- package/packages/daemon/bin/gsd-mcp.js +14 -0
- package/packages/daemon/dist/channel-manager.d.ts +53 -0
- package/packages/daemon/dist/channel-manager.d.ts.map +1 -0
- package/packages/daemon/dist/channel-manager.js +167 -0
- package/packages/daemon/dist/channel-manager.js.map +1 -0
- package/packages/daemon/dist/cli.d.ts +3 -0
- package/packages/daemon/dist/cli.d.ts.map +1 -0
- package/packages/daemon/dist/cli.js +94 -0
- package/packages/daemon/dist/cli.js.map +1 -0
- package/packages/daemon/dist/cloud-cli.d.ts +7 -0
- package/packages/daemon/dist/cloud-cli.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-cli.js +96 -0
- package/packages/daemon/dist/cloud-cli.js.map +1 -0
- package/packages/daemon/dist/cloud-config.d.ts +18 -0
- package/packages/daemon/dist/cloud-config.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-config.js +209 -0
- package/packages/daemon/dist/cloud-config.js.map +1 -0
- package/packages/daemon/dist/cloud-config.test.d.ts +2 -0
- package/packages/daemon/dist/cloud-config.test.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-config.test.js +132 -0
- package/packages/daemon/dist/cloud-config.test.js.map +1 -0
- package/packages/daemon/dist/cloud-runtime.d.ts +26 -0
- package/packages/daemon/dist/cloud-runtime.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-runtime.js +180 -0
- package/packages/daemon/dist/cloud-runtime.js.map +1 -0
- package/packages/daemon/dist/cloud-runtime.test.d.ts +2 -0
- package/packages/daemon/dist/cloud-runtime.test.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-runtime.test.js +28 -0
- package/packages/daemon/dist/cloud-runtime.test.js.map +1 -0
- package/packages/daemon/dist/cloud-token.d.ts +3 -0
- package/packages/daemon/dist/cloud-token.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-token.js +37 -0
- package/packages/daemon/dist/cloud-token.js.map +1 -0
- package/packages/daemon/dist/commands.d.ts +25 -0
- package/packages/daemon/dist/commands.d.ts.map +1 -0
- package/packages/daemon/dist/commands.js +81 -0
- package/packages/daemon/dist/commands.js.map +1 -0
- package/packages/daemon/dist/config.d.ts +17 -0
- package/packages/daemon/dist/config.d.ts.map +1 -0
- package/packages/daemon/dist/config.js +146 -0
- package/packages/daemon/dist/config.js.map +1 -0
- package/packages/daemon/dist/daemon.d.ts +38 -0
- package/packages/daemon/dist/daemon.d.ts.map +1 -0
- package/packages/daemon/dist/daemon.js +194 -0
- package/packages/daemon/dist/daemon.js.map +1 -0
- package/packages/daemon/dist/daemon.test.d.ts +2 -0
- package/packages/daemon/dist/daemon.test.d.ts.map +1 -0
- package/packages/daemon/dist/daemon.test.js +692 -0
- package/packages/daemon/dist/daemon.test.js.map +1 -0
- package/packages/daemon/dist/discord-bot.d.ts +70 -0
- package/packages/daemon/dist/discord-bot.d.ts.map +1 -0
- package/packages/daemon/dist/discord-bot.js +433 -0
- package/packages/daemon/dist/discord-bot.js.map +1 -0
- package/packages/daemon/dist/discord-bot.test.d.ts +2 -0
- package/packages/daemon/dist/discord-bot.test.d.ts.map +1 -0
- package/packages/daemon/dist/discord-bot.test.js +667 -0
- package/packages/daemon/dist/discord-bot.test.js.map +1 -0
- package/packages/daemon/dist/event-bridge.d.ts +72 -0
- package/packages/daemon/dist/event-bridge.d.ts.map +1 -0
- package/packages/daemon/dist/event-bridge.js +366 -0
- package/packages/daemon/dist/event-bridge.js.map +1 -0
- package/packages/daemon/dist/event-bridge.test.d.ts +9 -0
- package/packages/daemon/dist/event-bridge.test.d.ts.map +1 -0
- package/packages/daemon/dist/event-bridge.test.js +528 -0
- package/packages/daemon/dist/event-bridge.test.js.map +1 -0
- package/packages/daemon/dist/event-formatter.d.ts +34 -0
- package/packages/daemon/dist/event-formatter.d.ts.map +1 -0
- package/packages/daemon/dist/event-formatter.js +355 -0
- package/packages/daemon/dist/event-formatter.js.map +1 -0
- package/packages/daemon/dist/event-formatter.test.d.ts +2 -0
- package/packages/daemon/dist/event-formatter.test.d.ts.map +1 -0
- package/packages/daemon/dist/event-formatter.test.js +333 -0
- package/packages/daemon/dist/event-formatter.test.js.map +1 -0
- package/packages/daemon/dist/index.d.ts +25 -0
- package/packages/daemon/dist/index.d.ts.map +1 -0
- package/packages/daemon/dist/index.js +17 -0
- package/packages/daemon/dist/index.js.map +1 -0
- package/packages/daemon/dist/launchd.d.ts +49 -0
- package/packages/daemon/dist/launchd.d.ts.map +1 -0
- package/packages/daemon/dist/launchd.js +188 -0
- package/packages/daemon/dist/launchd.js.map +1 -0
- package/packages/daemon/dist/launchd.test.d.ts +2 -0
- package/packages/daemon/dist/launchd.test.d.ts.map +1 -0
- package/packages/daemon/dist/launchd.test.js +296 -0
- package/packages/daemon/dist/launchd.test.js.map +1 -0
- package/packages/daemon/dist/local-tool-executor.d.ts +22 -0
- package/packages/daemon/dist/local-tool-executor.d.ts.map +1 -0
- package/packages/daemon/dist/local-tool-executor.js +307 -0
- package/packages/daemon/dist/local-tool-executor.js.map +1 -0
- package/packages/daemon/dist/local-tool-executor.test.d.ts +2 -0
- package/packages/daemon/dist/local-tool-executor.test.d.ts.map +1 -0
- package/packages/daemon/dist/local-tool-executor.test.js +111 -0
- package/packages/daemon/dist/local-tool-executor.test.js.map +1 -0
- package/packages/daemon/dist/logger.d.ts +25 -0
- package/packages/daemon/dist/logger.d.ts.map +1 -0
- package/packages/daemon/dist/logger.js +72 -0
- package/packages/daemon/dist/logger.js.map +1 -0
- package/packages/daemon/dist/mcp-cli.d.ts +3 -0
- package/packages/daemon/dist/mcp-cli.d.ts.map +1 -0
- package/packages/daemon/dist/mcp-cli.js +8 -0
- package/packages/daemon/dist/mcp-cli.js.map +1 -0
- package/packages/daemon/dist/mcp-cli.test.d.ts +2 -0
- package/packages/daemon/dist/mcp-cli.test.d.ts.map +1 -0
- package/packages/daemon/dist/mcp-cli.test.js +13 -0
- package/packages/daemon/dist/mcp-cli.test.js.map +1 -0
- package/packages/daemon/dist/mcp-runtime-cli.d.ts +3 -0
- package/packages/daemon/dist/mcp-runtime-cli.d.ts.map +1 -0
- package/packages/daemon/dist/mcp-runtime-cli.js +8 -0
- package/packages/daemon/dist/mcp-runtime-cli.js.map +1 -0
- package/packages/daemon/dist/message-batcher.d.ts +78 -0
- package/packages/daemon/dist/message-batcher.d.ts.map +1 -0
- package/packages/daemon/dist/message-batcher.js +173 -0
- package/packages/daemon/dist/message-batcher.js.map +1 -0
- package/packages/daemon/dist/message-batcher.test.d.ts +2 -0
- package/packages/daemon/dist/message-batcher.test.d.ts.map +1 -0
- package/packages/daemon/dist/message-batcher.test.js +242 -0
- package/packages/daemon/dist/message-batcher.test.js.map +1 -0
- package/packages/daemon/dist/orchestrator.d.ts +98 -0
- package/packages/daemon/dist/orchestrator.d.ts.map +1 -0
- package/packages/daemon/dist/orchestrator.js +359 -0
- package/packages/daemon/dist/orchestrator.js.map +1 -0
- package/packages/daemon/dist/orchestrator.test.d.ts +8 -0
- package/packages/daemon/dist/orchestrator.test.d.ts.map +1 -0
- package/packages/daemon/dist/orchestrator.test.js +425 -0
- package/packages/daemon/dist/orchestrator.test.js.map +1 -0
- package/packages/daemon/dist/project-scanner.d.ts +18 -0
- package/packages/daemon/dist/project-scanner.d.ts.map +1 -0
- package/packages/daemon/dist/project-scanner.js +90 -0
- package/packages/daemon/dist/project-scanner.js.map +1 -0
- package/packages/daemon/dist/project-scanner.test.d.ts +5 -0
- package/packages/daemon/dist/project-scanner.test.d.ts.map +1 -0
- package/packages/daemon/dist/project-scanner.test.js +183 -0
- package/packages/daemon/dist/project-scanner.test.js.map +1 -0
- package/packages/daemon/dist/session-manager.d.ts +70 -0
- package/packages/daemon/dist/session-manager.d.ts.map +1 -0
- package/packages/daemon/dist/session-manager.js +358 -0
- package/packages/daemon/dist/session-manager.js.map +1 -0
- package/packages/daemon/dist/session-manager.test.d.ts +9 -0
- package/packages/daemon/dist/session-manager.test.d.ts.map +1 -0
- package/packages/daemon/dist/session-manager.test.js +616 -0
- package/packages/daemon/dist/session-manager.test.js.map +1 -0
- package/packages/daemon/dist/types.d.ts +133 -0
- package/packages/daemon/dist/types.d.ts.map +1 -0
- package/packages/daemon/dist/types.js +8 -0
- package/packages/daemon/dist/types.js.map +1 -0
- package/packages/daemon/dist/verbosity.d.ts +27 -0
- package/packages/daemon/dist/verbosity.d.ts.map +1 -0
- package/packages/daemon/dist/verbosity.js +86 -0
- package/packages/daemon/dist/verbosity.js.map +1 -0
- package/packages/daemon/dist/verbosity.test.d.ts +2 -0
- package/packages/daemon/dist/verbosity.test.d.ts.map +1 -0
- package/packages/daemon/dist/verbosity.test.js +136 -0
- package/packages/daemon/dist/verbosity.test.js.map +1 -0
- package/packages/daemon/package.json +9 -8
- package/packages/gsd-agent-core/package.json +6 -6
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +3 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +2 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +8 -8
- package/packages/mcp-server/bin/gsd-mcp-server.js +14 -0
- package/packages/mcp-server/package.json +6 -5
- package/packages/native/package.json +3 -3
- package/packages/pi-agent-core/package.json +4 -4
- package/packages/pi-ai/bin/pi-ai.js +14 -0
- package/packages/pi-ai/dist/models.generated.d.ts +0 -17
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +18 -35
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +5 -4
- package/packages/pi-coding-agent/dist/core/tools/read.d.ts +2 -2
- package/packages/pi-coding-agent/dist/core/tools/read.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/read.js +5 -3
- package/packages/pi-coding-agent/dist/core/tools/read.js.map +1 -1
- package/packages/pi-coding-agent/package.json +9 -9
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +3 -3
- package/pkg/package.json +1 -1
- package/scripts/ensure-workspace-builds.cjs +4 -4
- package/scripts/install/deps.js +10 -0
- package/src/resources/extensions/gsd/auto/loop.ts +22 -0
- package/src/resources/extensions/gsd/auto/phases.ts +1 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +2 -56
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +141 -1
- package/src/resources/extensions/gsd/worktree-post-create-hook.ts +127 -0
- package/dist/tsconfig.extensions.tsbuildinfo +0 -1
- /package/dist/web/standalone/.next/static/{JP7xjsa5zSaO76XhE-mFJ → spUYLkQXoHJyxYOMH9VQy}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{JP7xjsa5zSaO76XhE-mFJ → spUYLkQXoHJyxYOMH9VQy}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionManager unit tests.
|
|
3
|
+
*
|
|
4
|
+
* Uses the MockRpcClient + TestableSessionManager pattern (K008) to test
|
|
5
|
+
* session lifecycle, event handling, cost tracking, blocker detection,
|
|
6
|
+
* and cleanup without spawning real GSD processes.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, afterEach } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { resolve, basename } from 'node:path';
|
|
11
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { SessionManager } from './session-manager.js';
|
|
15
|
+
import { MAX_EVENTS } from './types.js';
|
|
16
|
+
import { Logger } from './logger.js';
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Mock RpcClient (duck-typed to match RpcClient interface)
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
class MockRpcClient {
|
|
21
|
+
started = false;
|
|
22
|
+
stopped = false;
|
|
23
|
+
aborted = false;
|
|
24
|
+
prompted = [];
|
|
25
|
+
eventListeners = [];
|
|
26
|
+
uiResponses = [];
|
|
27
|
+
/** Control — set to make start() reject */
|
|
28
|
+
startError = null;
|
|
29
|
+
/** Control — set to make init() reject */
|
|
30
|
+
initError = null;
|
|
31
|
+
/** Control — override sessionId from init */
|
|
32
|
+
initSessionId = 'mock-session-001';
|
|
33
|
+
cwd;
|
|
34
|
+
args;
|
|
35
|
+
constructor(options) {
|
|
36
|
+
this.cwd = options?.cwd ?? '';
|
|
37
|
+
this.args = options?.args ?? [];
|
|
38
|
+
}
|
|
39
|
+
async start() {
|
|
40
|
+
if (this.startError)
|
|
41
|
+
throw this.startError;
|
|
42
|
+
this.started = true;
|
|
43
|
+
}
|
|
44
|
+
async stop() {
|
|
45
|
+
this.stopped = true;
|
|
46
|
+
}
|
|
47
|
+
async init() {
|
|
48
|
+
if (this.initError)
|
|
49
|
+
throw this.initError;
|
|
50
|
+
return { sessionId: this.initSessionId, version: '2.51.0' };
|
|
51
|
+
}
|
|
52
|
+
onEvent(listener) {
|
|
53
|
+
this.eventListeners.push(listener);
|
|
54
|
+
return () => {
|
|
55
|
+
const idx = this.eventListeners.indexOf(listener);
|
|
56
|
+
if (idx >= 0)
|
|
57
|
+
this.eventListeners.splice(idx, 1);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async prompt(message) {
|
|
61
|
+
this.prompted.push(message);
|
|
62
|
+
}
|
|
63
|
+
async abort() {
|
|
64
|
+
this.aborted = true;
|
|
65
|
+
}
|
|
66
|
+
sendUIResponse(requestId, response) {
|
|
67
|
+
this.uiResponses.push({ requestId, response });
|
|
68
|
+
}
|
|
69
|
+
/** Test helper — emit an event to all listeners */
|
|
70
|
+
emitEvent(event) {
|
|
71
|
+
for (const listener of this.eventListeners) {
|
|
72
|
+
listener(event);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// TestableSessionManager — injects mock clients without module mocking (K008)
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
class TestableSessionManager extends SessionManager {
|
|
80
|
+
lastClient = null;
|
|
81
|
+
allClients = [];
|
|
82
|
+
sessionCounter = 0;
|
|
83
|
+
nextInitError = null;
|
|
84
|
+
nextStartError = null;
|
|
85
|
+
async startSession(options) {
|
|
86
|
+
const { projectDir } = options;
|
|
87
|
+
if (!projectDir || projectDir.trim() === '') {
|
|
88
|
+
throw new Error('projectDir is required and cannot be empty');
|
|
89
|
+
}
|
|
90
|
+
const resolvedDir = resolve(projectDir);
|
|
91
|
+
const projectName = basename(resolvedDir);
|
|
92
|
+
// Check duplicate via getSessionByDir
|
|
93
|
+
const existing = this.getSessionByDir(resolvedDir);
|
|
94
|
+
if (existing) {
|
|
95
|
+
throw new Error(`Session already active for ${resolvedDir} (sessionId: ${existing.sessionId}, status: ${existing.status})`);
|
|
96
|
+
}
|
|
97
|
+
const client = new MockRpcClient({ cwd: resolvedDir, args: [] });
|
|
98
|
+
if (this.nextStartError) {
|
|
99
|
+
client.startError = this.nextStartError;
|
|
100
|
+
this.nextStartError = null;
|
|
101
|
+
}
|
|
102
|
+
if (this.nextInitError) {
|
|
103
|
+
client.initError = this.nextInitError;
|
|
104
|
+
this.nextInitError = null;
|
|
105
|
+
}
|
|
106
|
+
this.sessionCounter++;
|
|
107
|
+
client.initSessionId = `mock-session-${String(this.sessionCounter).padStart(3, '0')}`;
|
|
108
|
+
this.lastClient = client;
|
|
109
|
+
this.allClients.push(client);
|
|
110
|
+
// Build session shell
|
|
111
|
+
const session = {
|
|
112
|
+
sessionId: '',
|
|
113
|
+
projectDir: resolvedDir,
|
|
114
|
+
projectName,
|
|
115
|
+
status: 'starting',
|
|
116
|
+
client: client, // duck-typed mock
|
|
117
|
+
events: [],
|
|
118
|
+
pendingBlocker: null,
|
|
119
|
+
cost: { totalCost: 0, tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } },
|
|
120
|
+
startTime: Date.now(),
|
|
121
|
+
};
|
|
122
|
+
// Insert into internal sessions map
|
|
123
|
+
this.sessions.set(resolvedDir, session);
|
|
124
|
+
try {
|
|
125
|
+
await client.start();
|
|
126
|
+
const initResult = await client.init();
|
|
127
|
+
session.sessionId = initResult.sessionId;
|
|
128
|
+
session.status = 'running';
|
|
129
|
+
// Wire event tracking using parent's handleEvent
|
|
130
|
+
session.unsubscribe = client.onEvent((event) => {
|
|
131
|
+
this.handleEvent(session, event);
|
|
132
|
+
});
|
|
133
|
+
// Kick off auto-mode
|
|
134
|
+
const command = options.command ?? '/gsd auto';
|
|
135
|
+
await client.prompt(command);
|
|
136
|
+
// Emit lifecycle events (matching parent behavior)
|
|
137
|
+
this.logger.info('session started', { sessionId: session.sessionId, projectDir: resolvedDir });
|
|
138
|
+
this.emit('session:started', { sessionId: session.sessionId, projectDir: resolvedDir, projectName });
|
|
139
|
+
return session.sessionId;
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
session.status = 'error';
|
|
143
|
+
session.error = err instanceof Error ? err.message : String(err);
|
|
144
|
+
try {
|
|
145
|
+
await client.stop();
|
|
146
|
+
}
|
|
147
|
+
catch { /* swallow */ }
|
|
148
|
+
this.logger.error('session error', { sessionId: session.sessionId, projectDir: resolvedDir, error: session.error });
|
|
149
|
+
this.emit('session:error', { sessionId: session.sessionId, projectDir: resolvedDir, projectName, error: session.error });
|
|
150
|
+
throw new Error(`Failed to start session for ${resolvedDir}: ${session.error}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
class SpyLogger {
|
|
155
|
+
calls = [];
|
|
156
|
+
tmpDir;
|
|
157
|
+
logger;
|
|
158
|
+
constructor() {
|
|
159
|
+
this.tmpDir = mkdtempSync(join(tmpdir(), 'sm-test-'));
|
|
160
|
+
this.logger = new Logger({
|
|
161
|
+
filePath: join(this.tmpDir, 'test.log'),
|
|
162
|
+
level: 'debug',
|
|
163
|
+
});
|
|
164
|
+
// Intercept write calls by wrapping the logger methods
|
|
165
|
+
const original = {
|
|
166
|
+
debug: this.logger.debug.bind(this.logger),
|
|
167
|
+
info: this.logger.info.bind(this.logger),
|
|
168
|
+
warn: this.logger.warn.bind(this.logger),
|
|
169
|
+
error: this.logger.error.bind(this.logger),
|
|
170
|
+
};
|
|
171
|
+
this.logger.debug = (msg, data) => {
|
|
172
|
+
this.calls.push({ level: 'debug', msg, data });
|
|
173
|
+
original.debug(msg, data);
|
|
174
|
+
};
|
|
175
|
+
this.logger.info = (msg, data) => {
|
|
176
|
+
this.calls.push({ level: 'info', msg, data });
|
|
177
|
+
original.info(msg, data);
|
|
178
|
+
};
|
|
179
|
+
this.logger.warn = (msg, data) => {
|
|
180
|
+
this.calls.push({ level: 'warn', msg, data });
|
|
181
|
+
original.warn(msg, data);
|
|
182
|
+
};
|
|
183
|
+
this.logger.error = (msg, data) => {
|
|
184
|
+
this.calls.push({ level: 'error', msg, data });
|
|
185
|
+
original.error(msg, data);
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async cleanup() {
|
|
189
|
+
await this.logger.close();
|
|
190
|
+
try {
|
|
191
|
+
rmSync(this.tmpDir, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
catch { /* ignore */ }
|
|
194
|
+
}
|
|
195
|
+
findCalls(level, msgSubstring) {
|
|
196
|
+
return this.calls.filter(c => c.level === level && c.msg.includes(msgSubstring));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Test Helpers
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
let allManagers = [];
|
|
203
|
+
let allSpyLoggers = [];
|
|
204
|
+
function createManager() {
|
|
205
|
+
const spy = new SpyLogger();
|
|
206
|
+
const manager = new TestableSessionManager(spy.logger);
|
|
207
|
+
allManagers.push(manager);
|
|
208
|
+
allSpyLoggers.push(spy);
|
|
209
|
+
return { manager, spy };
|
|
210
|
+
}
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Tests
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
describe('SessionManager', () => {
|
|
215
|
+
afterEach(async () => {
|
|
216
|
+
for (const m of allManagers) {
|
|
217
|
+
try {
|
|
218
|
+
await m.cleanup();
|
|
219
|
+
}
|
|
220
|
+
catch { /* swallow */ }
|
|
221
|
+
}
|
|
222
|
+
allManagers = [];
|
|
223
|
+
for (const s of allSpyLoggers) {
|
|
224
|
+
await s.cleanup();
|
|
225
|
+
}
|
|
226
|
+
allSpyLoggers = [];
|
|
227
|
+
});
|
|
228
|
+
// ---- Lifecycle: start → running → completed ----
|
|
229
|
+
it('start → running → completed lifecycle', async () => {
|
|
230
|
+
const { manager, spy } = createManager();
|
|
231
|
+
const sessionId = await manager.startSession({ projectDir: '/tmp/test-project' });
|
|
232
|
+
assert.ok(sessionId);
|
|
233
|
+
const session = manager.getSession(sessionId);
|
|
234
|
+
assert.ok(session);
|
|
235
|
+
assert.equal(session.status, 'running');
|
|
236
|
+
assert.equal(session.projectName, 'test-project');
|
|
237
|
+
// Simulate terminal notification
|
|
238
|
+
manager.lastClient.emitEvent({
|
|
239
|
+
type: 'extension_ui_request',
|
|
240
|
+
id: 'n1',
|
|
241
|
+
method: 'notify',
|
|
242
|
+
message: 'Auto-mode stopped: completed all tasks',
|
|
243
|
+
});
|
|
244
|
+
assert.equal(session.status, 'completed');
|
|
245
|
+
// Verify logger calls
|
|
246
|
+
const startedLogs = spy.findCalls('info', 'session started');
|
|
247
|
+
assert.equal(startedLogs.length, 1);
|
|
248
|
+
const completedLogs = spy.findCalls('info', 'session completed');
|
|
249
|
+
assert.equal(completedLogs.length, 1);
|
|
250
|
+
});
|
|
251
|
+
// ---- Lifecycle: start → running → blocked → resolve → running → completed ----
|
|
252
|
+
it('start → blocked → resolve → running → completed lifecycle', async () => {
|
|
253
|
+
const { manager } = createManager();
|
|
254
|
+
const sessionId = await manager.startSession({ projectDir: '/tmp/test-project-2' });
|
|
255
|
+
const session = manager.getSession(sessionId);
|
|
256
|
+
// Simulate blocking UI request (non-fire-and-forget method)
|
|
257
|
+
manager.lastClient.emitEvent({
|
|
258
|
+
type: 'extension_ui_request',
|
|
259
|
+
id: 'blocker-1',
|
|
260
|
+
method: 'confirm',
|
|
261
|
+
title: 'Merge PR?',
|
|
262
|
+
message: 'Should I merge this PR?',
|
|
263
|
+
});
|
|
264
|
+
assert.equal(session.status, 'blocked');
|
|
265
|
+
assert.ok(session.pendingBlocker);
|
|
266
|
+
assert.equal(session.pendingBlocker.id, 'blocker-1');
|
|
267
|
+
assert.equal(session.pendingBlocker.method, 'confirm');
|
|
268
|
+
// Resolve the blocker
|
|
269
|
+
await manager.resolveBlocker(sessionId, 'yes');
|
|
270
|
+
assert.equal(session.status, 'running');
|
|
271
|
+
assert.equal(session.pendingBlocker, null);
|
|
272
|
+
// Verify UI response was sent
|
|
273
|
+
const client = manager.lastClient;
|
|
274
|
+
assert.equal(client.uiResponses.length, 1);
|
|
275
|
+
assert.equal(client.uiResponses[0].requestId, 'blocker-1');
|
|
276
|
+
// Complete the session
|
|
277
|
+
manager.lastClient.emitEvent({
|
|
278
|
+
type: 'extension_ui_request',
|
|
279
|
+
id: 'n2',
|
|
280
|
+
method: 'notify',
|
|
281
|
+
message: 'Auto-mode stopped: all done',
|
|
282
|
+
});
|
|
283
|
+
assert.equal(session.status, 'completed');
|
|
284
|
+
});
|
|
285
|
+
// ---- Lifecycle: start → error (init failure) ----
|
|
286
|
+
it('start → error when init fails', async () => {
|
|
287
|
+
const { manager, spy } = createManager();
|
|
288
|
+
manager.nextInitError = new Error('Connection refused');
|
|
289
|
+
await assert.rejects(() => manager.startSession({ projectDir: '/tmp/test-error-project' }), (err) => {
|
|
290
|
+
assert.ok(err.message.includes('Connection refused'));
|
|
291
|
+
return true;
|
|
292
|
+
});
|
|
293
|
+
// Session should still exist in map with error status
|
|
294
|
+
const session = manager.getSessionByDir('/tmp/test-error-project');
|
|
295
|
+
assert.ok(session);
|
|
296
|
+
assert.equal(session.status, 'error');
|
|
297
|
+
assert.ok(session.error?.includes('Connection refused'));
|
|
298
|
+
// Logger should have error call
|
|
299
|
+
const errorLogs = spy.findCalls('error', 'session error');
|
|
300
|
+
assert.equal(errorLogs.length, 1);
|
|
301
|
+
});
|
|
302
|
+
// ---- Duplicate session prevention ----
|
|
303
|
+
it('rejects duplicate session for same projectDir', async () => {
|
|
304
|
+
const { manager } = createManager();
|
|
305
|
+
await manager.startSession({ projectDir: '/tmp/dup-test' });
|
|
306
|
+
await assert.rejects(() => manager.startSession({ projectDir: '/tmp/dup-test' }), (err) => {
|
|
307
|
+
assert.ok(err.message.includes('Session already active'));
|
|
308
|
+
return true;
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
// ---- Cancel session ----
|
|
312
|
+
it('cancels a running session', async () => {
|
|
313
|
+
const { manager, spy } = createManager();
|
|
314
|
+
const sessionId = await manager.startSession({ projectDir: '/tmp/cancel-test' });
|
|
315
|
+
const session = manager.getSession(sessionId);
|
|
316
|
+
const client = manager.lastClient;
|
|
317
|
+
await manager.cancelSession(sessionId);
|
|
318
|
+
assert.equal(session.status, 'cancelled');
|
|
319
|
+
assert.ok(client.aborted);
|
|
320
|
+
assert.ok(client.stopped);
|
|
321
|
+
const cancelLogs = spy.findCalls('info', 'session cancelled');
|
|
322
|
+
assert.equal(cancelLogs.length, 1);
|
|
323
|
+
});
|
|
324
|
+
// ---- Cost accumulation (K004 cumulative-max) ----
|
|
325
|
+
it('accumulates cost using cumulative-max pattern (K004)', async () => {
|
|
326
|
+
const { manager } = createManager();
|
|
327
|
+
const sessionId = await manager.startSession({ projectDir: '/tmp/cost-test' });
|
|
328
|
+
const session = manager.getSession(sessionId);
|
|
329
|
+
const client = manager.lastClient;
|
|
330
|
+
// First cost update
|
|
331
|
+
client.emitEvent({
|
|
332
|
+
type: 'cost_update',
|
|
333
|
+
runId: 'run-1',
|
|
334
|
+
turnCost: 0.01,
|
|
335
|
+
cumulativeCost: 0.01,
|
|
336
|
+
tokens: { input: 100, output: 50, cacheRead: 20, cacheWrite: 10 },
|
|
337
|
+
});
|
|
338
|
+
assert.equal(session.cost.totalCost, 0.01);
|
|
339
|
+
assert.equal(session.cost.tokens.input, 100);
|
|
340
|
+
// Second cost update — cumulative values should increase
|
|
341
|
+
client.emitEvent({
|
|
342
|
+
type: 'cost_update',
|
|
343
|
+
runId: 'run-1',
|
|
344
|
+
turnCost: 0.02,
|
|
345
|
+
cumulativeCost: 0.03,
|
|
346
|
+
tokens: { input: 250, output: 120, cacheRead: 40, cacheWrite: 20 },
|
|
347
|
+
});
|
|
348
|
+
assert.equal(session.cost.totalCost, 0.03);
|
|
349
|
+
assert.equal(session.cost.tokens.input, 250);
|
|
350
|
+
assert.equal(session.cost.tokens.output, 120);
|
|
351
|
+
// Third update with lower values — max should hold
|
|
352
|
+
client.emitEvent({
|
|
353
|
+
type: 'cost_update',
|
|
354
|
+
runId: 'run-2',
|
|
355
|
+
turnCost: 0.005,
|
|
356
|
+
cumulativeCost: 0.02, // lower than 0.03 — should NOT replace
|
|
357
|
+
tokens: { input: 50, output: 30, cacheRead: 5, cacheWrite: 2 },
|
|
358
|
+
});
|
|
359
|
+
assert.equal(session.cost.totalCost, 0.03); // max held
|
|
360
|
+
assert.equal(session.cost.tokens.input, 250); // max held
|
|
361
|
+
});
|
|
362
|
+
// ---- Ring buffer event trimming ----
|
|
363
|
+
it('trims events when exceeding MAX_EVENTS', async () => {
|
|
364
|
+
const { manager } = createManager();
|
|
365
|
+
const sessionId = await manager.startSession({ projectDir: '/tmp/ringbuf-test' });
|
|
366
|
+
const session = manager.getSession(sessionId);
|
|
367
|
+
const client = manager.lastClient;
|
|
368
|
+
// Push MAX_EVENTS + 20 events
|
|
369
|
+
for (let i = 0; i < MAX_EVENTS + 20; i++) {
|
|
370
|
+
client.emitEvent({
|
|
371
|
+
type: 'assistant_message',
|
|
372
|
+
id: `msg-${i}`,
|
|
373
|
+
content: `Event ${i}`,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
assert.equal(session.events.length, MAX_EVENTS);
|
|
377
|
+
// Oldest events should be trimmed — first event should be #20
|
|
378
|
+
const firstEvent = session.events[0];
|
|
379
|
+
assert.equal(firstEvent.id, 'msg-20');
|
|
380
|
+
});
|
|
381
|
+
// ---- Blocker detection (non-fire-and-forget extension_ui_request) ----
|
|
382
|
+
it('detects blocker from non-fire-and-forget extension_ui_request', async () => {
|
|
383
|
+
const { manager, spy } = createManager();
|
|
384
|
+
const sessionId = await manager.startSession({ projectDir: '/tmp/blocker-test' });
|
|
385
|
+
const session = manager.getSession(sessionId);
|
|
386
|
+
manager.lastClient.emitEvent({
|
|
387
|
+
type: 'extension_ui_request',
|
|
388
|
+
id: 'sel-1',
|
|
389
|
+
method: 'select',
|
|
390
|
+
title: 'Choose deployment target',
|
|
391
|
+
options: ['staging', 'production'],
|
|
392
|
+
});
|
|
393
|
+
assert.equal(session.status, 'blocked');
|
|
394
|
+
assert.ok(session.pendingBlocker);
|
|
395
|
+
assert.equal(session.pendingBlocker.method, 'select');
|
|
396
|
+
const blockedLogs = spy.findCalls('info', 'session blocked');
|
|
397
|
+
assert.equal(blockedLogs.length, 1);
|
|
398
|
+
});
|
|
399
|
+
// ---- Fire-and-forget methods do NOT block ----
|
|
400
|
+
it('fire-and-forget methods do not trigger blocker', async () => {
|
|
401
|
+
const { manager } = createManager();
|
|
402
|
+
const sessionId = await manager.startSession({ projectDir: '/tmp/faf-test' });
|
|
403
|
+
const session = manager.getSession(sessionId);
|
|
404
|
+
// setStatus is fire-and-forget
|
|
405
|
+
manager.lastClient.emitEvent({
|
|
406
|
+
type: 'extension_ui_request',
|
|
407
|
+
id: 'st-1',
|
|
408
|
+
method: 'setStatus',
|
|
409
|
+
statusKey: 'build',
|
|
410
|
+
statusText: 'Building...',
|
|
411
|
+
});
|
|
412
|
+
assert.equal(session.status, 'running');
|
|
413
|
+
assert.equal(session.pendingBlocker, null);
|
|
414
|
+
});
|
|
415
|
+
// ---- Terminal detection (auto-mode stopped notification) ----
|
|
416
|
+
it('detects terminal from auto-mode stopped notification', async () => {
|
|
417
|
+
const { manager } = createManager();
|
|
418
|
+
const sessionId = await manager.startSession({ projectDir: '/tmp/terminal-test' });
|
|
419
|
+
const session = manager.getSession(sessionId);
|
|
420
|
+
manager.lastClient.emitEvent({
|
|
421
|
+
type: 'extension_ui_request',
|
|
422
|
+
id: 'n1',
|
|
423
|
+
method: 'notify',
|
|
424
|
+
message: 'Step-mode stopped: user requested',
|
|
425
|
+
});
|
|
426
|
+
assert.equal(session.status, 'completed');
|
|
427
|
+
});
|
|
428
|
+
// ---- getAllSessions returns all tracked sessions ----
|
|
429
|
+
it('getAllSessions returns all tracked sessions', async () => {
|
|
430
|
+
const { manager } = createManager();
|
|
431
|
+
await manager.startSession({ projectDir: '/tmp/proj-a' });
|
|
432
|
+
await manager.startSession({ projectDir: '/tmp/proj-b' });
|
|
433
|
+
await manager.startSession({ projectDir: '/tmp/proj-c' });
|
|
434
|
+
const all = manager.getAllSessions();
|
|
435
|
+
assert.equal(all.length, 3);
|
|
436
|
+
const dirs = all.map(s => s.projectDir).sort();
|
|
437
|
+
assert.ok(dirs[0].endsWith('proj-a'));
|
|
438
|
+
assert.ok(dirs[1].endsWith('proj-b'));
|
|
439
|
+
assert.ok(dirs[2].endsWith('proj-c'));
|
|
440
|
+
});
|
|
441
|
+
// ---- cleanup stops all active sessions ----
|
|
442
|
+
it('cleanup stops all active sessions', async () => {
|
|
443
|
+
const { manager } = createManager();
|
|
444
|
+
await manager.startSession({ projectDir: '/tmp/cleanup-a' });
|
|
445
|
+
await manager.startSession({ projectDir: '/tmp/cleanup-b' });
|
|
446
|
+
const clients = [...manager.allClients];
|
|
447
|
+
assert.equal(clients.length, 2);
|
|
448
|
+
await manager.cleanup();
|
|
449
|
+
const all = manager.getAllSessions();
|
|
450
|
+
for (const s of all) {
|
|
451
|
+
assert.equal(s.status, 'cancelled');
|
|
452
|
+
}
|
|
453
|
+
// Both clients should have been stopped
|
|
454
|
+
for (const c of clients) {
|
|
455
|
+
assert.ok(c.stopped);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
// ---- EventEmitter: session:started ----
|
|
459
|
+
it('emits session:started event', async () => {
|
|
460
|
+
const { manager } = createManager();
|
|
461
|
+
let emittedData;
|
|
462
|
+
manager.on('session:started', (data) => { emittedData = data; });
|
|
463
|
+
const sessionId = await manager.startSession({ projectDir: '/tmp/emit-start' });
|
|
464
|
+
assert.ok(emittedData);
|
|
465
|
+
assert.equal(emittedData.sessionId, sessionId);
|
|
466
|
+
assert.equal(emittedData.projectName, 'emit-start');
|
|
467
|
+
});
|
|
468
|
+
// ---- EventEmitter: session:blocked ----
|
|
469
|
+
it('emits session:blocked event', async () => {
|
|
470
|
+
const { manager } = createManager();
|
|
471
|
+
let emittedData;
|
|
472
|
+
manager.on('session:blocked', (data) => { emittedData = data; });
|
|
473
|
+
await manager.startSession({ projectDir: '/tmp/emit-blocked' });
|
|
474
|
+
manager.lastClient.emitEvent({
|
|
475
|
+
type: 'extension_ui_request',
|
|
476
|
+
id: 'b-1',
|
|
477
|
+
method: 'input',
|
|
478
|
+
title: 'Enter API key',
|
|
479
|
+
});
|
|
480
|
+
assert.ok(emittedData);
|
|
481
|
+
assert.equal(emittedData.blocker.id, 'b-1');
|
|
482
|
+
});
|
|
483
|
+
// ---- EventEmitter: session:completed ----
|
|
484
|
+
it('emits session:completed event', async () => {
|
|
485
|
+
const { manager } = createManager();
|
|
486
|
+
let emittedData;
|
|
487
|
+
manager.on('session:completed', (data) => { emittedData = data; });
|
|
488
|
+
await manager.startSession({ projectDir: '/tmp/emit-completed' });
|
|
489
|
+
manager.lastClient.emitEvent({
|
|
490
|
+
type: 'extension_ui_request',
|
|
491
|
+
id: 'n1',
|
|
492
|
+
method: 'notify',
|
|
493
|
+
message: 'Auto-mode stopped: success',
|
|
494
|
+
});
|
|
495
|
+
assert.ok(emittedData);
|
|
496
|
+
assert.equal(emittedData.projectName, 'emit-completed');
|
|
497
|
+
});
|
|
498
|
+
// ---- EventEmitter: session:error ----
|
|
499
|
+
it('emits session:error event on init failure', async () => {
|
|
500
|
+
const { manager } = createManager();
|
|
501
|
+
let emittedData;
|
|
502
|
+
manager.on('session:error', (data) => { emittedData = data; });
|
|
503
|
+
manager.nextInitError = new Error('Process crashed');
|
|
504
|
+
try {
|
|
505
|
+
await manager.startSession({ projectDir: '/tmp/emit-error' });
|
|
506
|
+
}
|
|
507
|
+
catch { /* expected */ }
|
|
508
|
+
assert.ok(emittedData);
|
|
509
|
+
assert.ok(emittedData.error.includes('Process crashed'));
|
|
510
|
+
});
|
|
511
|
+
// ---- EventEmitter: session:event ----
|
|
512
|
+
it('emits session:event for every forwarded event', async () => {
|
|
513
|
+
const { manager } = createManager();
|
|
514
|
+
const events = [];
|
|
515
|
+
manager.on('session:event', (data) => { events.push(data); });
|
|
516
|
+
await manager.startSession({ projectDir: '/tmp/emit-event' });
|
|
517
|
+
manager.lastClient.emitEvent({ type: 'assistant_message', id: 'a1', content: 'Hello' });
|
|
518
|
+
manager.lastClient.emitEvent({ type: 'tool_use', id: 't1', name: 'read' });
|
|
519
|
+
assert.equal(events.length, 2);
|
|
520
|
+
});
|
|
521
|
+
// ---- Empty projectDir rejection ----
|
|
522
|
+
it('rejects empty projectDir', async () => {
|
|
523
|
+
const { manager } = createManager();
|
|
524
|
+
await assert.rejects(() => manager.startSession({ projectDir: '' }), (err) => {
|
|
525
|
+
assert.ok(err.message.includes('projectDir is required'));
|
|
526
|
+
return true;
|
|
527
|
+
});
|
|
528
|
+
await assert.rejects(() => manager.startSession({ projectDir: ' ' }), (err) => {
|
|
529
|
+
assert.ok(err.message.includes('projectDir is required'));
|
|
530
|
+
return true;
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
// ---- Logger receives structured calls ----
|
|
534
|
+
it('logger receives structured calls during lifecycle', async () => {
|
|
535
|
+
const { manager, spy } = createManager();
|
|
536
|
+
const sessionId = await manager.startSession({ projectDir: '/tmp/log-test' });
|
|
537
|
+
// Should have 'session started' info log
|
|
538
|
+
const started = spy.findCalls('info', 'session started');
|
|
539
|
+
assert.equal(started.length, 1);
|
|
540
|
+
assert.ok(started[0].data?.sessionId);
|
|
541
|
+
assert.ok(started[0].data?.projectDir);
|
|
542
|
+
// Emit an event — should produce debug log
|
|
543
|
+
manager.lastClient.emitEvent({ type: 'assistant_message', id: 'a1', content: 'hi' });
|
|
544
|
+
const debugLogs = spy.findCalls('debug', 'session event');
|
|
545
|
+
assert.ok(debugLogs.length >= 1);
|
|
546
|
+
assert.ok(debugLogs[0].data?.type);
|
|
547
|
+
});
|
|
548
|
+
// ---- getResult returns structured status ----
|
|
549
|
+
it('getResult returns structured status', async () => {
|
|
550
|
+
const { manager } = createManager();
|
|
551
|
+
const sessionId = await manager.startSession({ projectDir: '/tmp/result-test' });
|
|
552
|
+
const result = manager.getResult(sessionId);
|
|
553
|
+
assert.equal(result.sessionId, sessionId);
|
|
554
|
+
assert.equal(result.status, 'running');
|
|
555
|
+
assert.equal(result.projectName, 'result-test');
|
|
556
|
+
assert.equal(result.error, null);
|
|
557
|
+
assert.equal(result.pendingBlocker, null);
|
|
558
|
+
assert.ok(typeof result.durationMs === 'number');
|
|
559
|
+
assert.ok(result.cost);
|
|
560
|
+
assert.ok(Array.isArray(result.recentEvents));
|
|
561
|
+
});
|
|
562
|
+
// ---- getResult throws for unknown session ----
|
|
563
|
+
it('getResult throws for unknown sessionId', () => {
|
|
564
|
+
const { manager } = createManager();
|
|
565
|
+
assert.throws(() => manager.getResult('nonexistent'), (err) => err.message.includes('Session not found'));
|
|
566
|
+
});
|
|
567
|
+
// ---- resolveBlocker throws when no blocker pending ----
|
|
568
|
+
it('resolveBlocker throws when no blocker pending', async () => {
|
|
569
|
+
const { manager } = createManager();
|
|
570
|
+
const sessionId = await manager.startSession({ projectDir: '/tmp/no-blocker' });
|
|
571
|
+
await assert.rejects(() => manager.resolveBlocker(sessionId, 'yes'), (err) => err.message.includes('No pending blocker'));
|
|
572
|
+
});
|
|
573
|
+
// ---- cancelSession throws for unknown session ----
|
|
574
|
+
it('cancelSession throws for unknown sessionId', async () => {
|
|
575
|
+
const { manager } = createManager();
|
|
576
|
+
await assert.rejects(() => manager.cancelSession('nonexistent'), (err) => err.message.includes('Session not found'));
|
|
577
|
+
});
|
|
578
|
+
// ---- Blocked notification detected as blocker, not terminal ----
|
|
579
|
+
it('blocked notification sets status to blocked, not completed', async () => {
|
|
580
|
+
const { manager } = createManager();
|
|
581
|
+
const sessionId = await manager.startSession({ projectDir: '/tmp/blocked-notify' });
|
|
582
|
+
const session = manager.getSession(sessionId);
|
|
583
|
+
manager.lastClient.emitEvent({
|
|
584
|
+
type: 'extension_ui_request',
|
|
585
|
+
id: 'bn-1',
|
|
586
|
+
method: 'notify',
|
|
587
|
+
message: 'Auto-mode stopped: Blocked: waiting for approval',
|
|
588
|
+
});
|
|
589
|
+
assert.equal(session.status, 'blocked');
|
|
590
|
+
assert.ok(session.pendingBlocker);
|
|
591
|
+
});
|
|
592
|
+
// ---- projectName is basename of resolved projectDir ----
|
|
593
|
+
it('projectName is basename of projectDir', async () => {
|
|
594
|
+
const { manager } = createManager();
|
|
595
|
+
const sessionId = await manager.startSession({ projectDir: '/home/user/projects/my-app' });
|
|
596
|
+
const session = manager.getSession(sessionId);
|
|
597
|
+
assert.equal(session.projectName, 'my-app');
|
|
598
|
+
});
|
|
599
|
+
// ---- Custom command is sent instead of default ----
|
|
600
|
+
it('sends custom command when provided', async () => {
|
|
601
|
+
const { manager } = createManager();
|
|
602
|
+
await manager.startSession({ projectDir: '/tmp/custom-cmd', command: '/gsd quick fix-typo' });
|
|
603
|
+
const client = manager.lastClient;
|
|
604
|
+
assert.ok(client.prompted.includes('/gsd quick fix-typo'));
|
|
605
|
+
assert.ok(!client.prompted.includes('/gsd auto'));
|
|
606
|
+
});
|
|
607
|
+
// ---- getSessionByDir returns session by directory lookup ----
|
|
608
|
+
it('getSessionByDir returns session by directory', async () => {
|
|
609
|
+
const { manager } = createManager();
|
|
610
|
+
await manager.startSession({ projectDir: '/tmp/dir-lookup' });
|
|
611
|
+
const session = manager.getSessionByDir('/tmp/dir-lookup');
|
|
612
|
+
assert.ok(session);
|
|
613
|
+
assert.equal(session.projectName, 'dir-lookup');
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
//# sourceMappingURL=session-manager.test.js.map
|