@assistkick/create 1.7.0 → 1.8.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/dist/bin/create.js +0 -0
- package/package.json +9 -7
- package/templates/assistkick-product-system/.env.example +1 -0
- package/templates/assistkick-product-system/local.db +0 -0
- package/templates/assistkick-product-system/package.json +4 -2
- package/templates/assistkick-product-system/packages/backend/package.json +2 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +165 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +358 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +356 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +96 -1
- package/templates/assistkick-product-system/packages/backend/src/routes/graph.ts +1 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +43 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +200 -84
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +6 -3
- package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +53 -17
- package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +218 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +119 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +154 -0
- package/templates/assistkick-product-system/packages/backend/src/server.ts +81 -9
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +489 -0
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +416 -0
- package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.test.ts +189 -0
- package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.ts +182 -0
- package/templates/assistkick-product-system/packages/backend/src/services/init.ts +28 -78
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +16 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +73 -2
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +4 -4
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +87 -11
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +210 -69
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +210 -215
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +162 -0
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +148 -0
- package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +11 -5
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.test.ts +64 -0
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +134 -0
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.test.ts +256 -0
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +258 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_group_service.ts +106 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.test.ts +275 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.ts +222 -0
- package/templates/assistkick-product-system/packages/frontend/package-lock.json +3455 -0
- package/templates/assistkick-product-system/packages/frontend/package.json +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +8 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +456 -16
- package/templates/assistkick-product-system/packages/frontend/src/api/client_files.test.ts +172 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client_video.test.ts +238 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +307 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/CoherenceView.tsx +82 -66
- package/templates/assistkick-product-system/packages/frontend/src/components/CompositionPlaceholder.tsx +97 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/DesignSystemView.tsx +20 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +57 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +313 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeContextMenu.tsx +61 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +73 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +404 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +187 -56
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +71 -73
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphSettings.tsx +8 -8
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphView.tsx +1 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/InviteUserDialog.tsx +15 -11
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +202 -171
- package/templates/assistkick-product-system/packages/frontend/src/components/LoginPage.tsx +14 -14
- package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +54 -33
- package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +32 -49
- package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +43 -48
- package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +121 -52
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +20 -14
- package/templates/assistkick-product-system/packages/frontend/src/components/UsersView.tsx +52 -52
- package/templates/assistkick-product-system/packages/frontend/src/components/VideoGallery.tsx +313 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/VideographyView.tsx +250 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/WorkflowsView.tsx +474 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/AccentBorderList.tsx +53 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/Button.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonGroup.tsx +29 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonShowcase.tsx +221 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/CardGlass.tsx +141 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/CompletionRing.tsx +30 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ContentCard.tsx +34 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/IconButton.tsx +74 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +103 -87
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +9 -188
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/Kbd.tsx +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KindBadge.tsx +21 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +81 -37
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/SidePanelShowcase.tsx +370 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/SideSheet.tsx +64 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/StatusDot.tsx +18 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCardPositionNode.tsx +36 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCycleCountNode.tsx +60 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/EndNode.tsx +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +189 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +123 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RunAgentNode.tsx +51 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/SetCardMetadataNode.tsx +53 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/StartNode.tsx +18 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/TransitionCardNode.tsx +59 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +335 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +634 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/autoLayout.ts +103 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/edgeColors.ts +35 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +208 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.test.ts +119 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +107 -0
- package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +13 -11
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useAutoSave.ts +75 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useToast.tsx +16 -3
- package/templates/assistkick-product-system/packages/frontend/src/pages/accept_invitation_page.tsx +30 -27
- package/templates/assistkick-product-system/packages/frontend/src/pages/forgot_password_page.tsx +18 -15
- package/templates/assistkick-product-system/packages/frontend/src/pages/register_page.tsx +21 -18
- package/templates/assistkick-product-system/packages/frontend/src/pages/reset_password_page.tsx +28 -25
- package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +1 -1
- package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +13 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +13 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +6 -3
- package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +4 -4
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +275 -3535
- package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.test.ts +167 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.ts +101 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.test.ts +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.ts +17 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.test.ts +145 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.ts +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.test.ts +4 -10
- package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.ts +19 -1
- package/templates/assistkick-product-system/packages/frontend/vite.config.ts +5 -0
- package/templates/assistkick-product-system/packages/shared/db/local.db +0 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0004_tidy_matthew_murdock.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0005_mysterious_falcon.sql +692 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0006_next_venom.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0007_deep_barracuda.sql +39 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0008_puzzling_hannibal_king.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0009_amused_beast.sql +8 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0010_spotty_moira_mactaggert.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0011_goofy_snowbird.sql +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0011_supreme_doctor_octopus.sql +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0013_reflective_prowler.sql +15 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0004_snapshot.json +921 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0005_snapshot.json +1042 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0006_snapshot.json +1101 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0007_snapshot.json +1336 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0008_snapshot.json +1275 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0009_snapshot.json +1327 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0010_snapshot.json +1393 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0011_snapshot.json +1436 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0013_snapshot.json +1538 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +70 -0
- package/templates/assistkick-product-system/packages/shared/db/schema.ts +113 -0
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +32 -7
- package/templates/assistkick-product-system/packages/shared/lib/constants.ts +9 -0
- package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +12 -4
- package/templates/assistkick-product-system/packages/shared/lib/graph.ts +5 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +1753 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +1281 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +211 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +43 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +13 -2
- package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +1 -1
- package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.test.ts +226 -0
- package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.ts +251 -0
- package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
- package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.test.ts +10 -0
- package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.ts +6 -0
- package/templates/assistkick-product-system/packages/video/Root.tsx +85 -0
- package/templates/assistkick-product-system/packages/video/components/email_scene.tsx +231 -0
- package/templates/assistkick-product-system/packages/video/components/outro_scene.tsx +153 -0
- package/templates/assistkick-product-system/packages/video/components/part_divider.tsx +90 -0
- package/templates/assistkick-product-system/packages/video/components/scene.tsx +226 -0
- package/templates/assistkick-product-system/packages/video/components/theme.ts +22 -0
- package/templates/assistkick-product-system/packages/video/components/title_scene.tsx +169 -0
- package/templates/assistkick-product-system/packages/video/components/video_split_layout.tsx +84 -0
- package/templates/assistkick-product-system/packages/video/compositions/.gitkeep +0 -0
- package/templates/assistkick-product-system/packages/video/index.ts +4 -0
- package/templates/assistkick-product-system/packages/video/package.json +28 -0
- package/templates/assistkick-product-system/packages/video/remotion.config.ts +11 -0
- package/templates/assistkick-product-system/packages/video/scripts/process_script.test.ts +326 -0
- package/templates/assistkick-product-system/packages/video/scripts/process_script.ts +630 -0
- package/templates/assistkick-product-system/packages/video/style.css +1 -0
- package/templates/assistkick-product-system/packages/video/tsconfig.json +18 -0
- package/templates/assistkick-product-system/tests/graph_legend.test.ts +2 -1
- package/templates/assistkick-product-system/tests/video_render_service.test.ts +179 -0
- package/templates/assistkick-product-system/tests/web_terminal.test.ts +219 -455
- package/templates/assistkick-product-system/tests/workflow_integration.test.ts +341 -0
- package/templates/skills/assistkick-developer/SKILL.md +3 -0
- package/templates/skills/assistkick-developer/references/react_development_guidelines.md +225 -0
- package/templates/skills/product-system/graph.json +1890 -0
- package/templates/skills/product-system/kanban.json +304 -0
- package/templates/skills/product-system/nodes/comp_001.md +56 -0
- package/templates/skills/product-system/nodes/comp_002.md +57 -0
- package/templates/skills/product-system/nodes/data_001.md +51 -0
- package/templates/skills/product-system/nodes/data_002.md +40 -0
- package/templates/skills/product-system/nodes/data_004.md +38 -0
- package/templates/skills/product-system/nodes/dec_001.md +34 -0
- package/templates/skills/product-system/nodes/dec_016.md +32 -0
- package/templates/skills/product-system/nodes/feat_008.md +30 -0
- package/templates/skills/video-composition-agent/SKILL.md +232 -0
- package/templates/skills/video-script-writer/SKILL.md +136 -0
|
@@ -1,504 +1,223 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unit tests for Web Terminal (feat_073).
|
|
3
|
-
* Tests PTY session manager logic
|
|
4
|
-
*
|
|
3
|
+
* Tests PTY session manager logic with DB-backed sessions,
|
|
4
|
+
* claude session resumption, and WebSocket handler auth checks.
|
|
5
5
|
*/
|
|
6
|
-
import { describe, it, mock } from 'node:test';
|
|
6
|
+
import { describe, it, mock, beforeEach } from 'node:test';
|
|
7
7
|
import assert from 'node:assert/strict';
|
|
8
8
|
import { PtySessionManager } from '../packages/backend/src/services/pty_session_manager.js';
|
|
9
9
|
|
|
10
|
-
// ---
|
|
10
|
+
// --- Mock helpers ---
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
onData: mock.fn(),
|
|
16
|
-
onExit: mock.fn(),
|
|
12
|
+
const createMockPty = () => {
|
|
13
|
+
const listeners: Record<string, Function> = {};
|
|
14
|
+
return {
|
|
15
|
+
onData: mock.fn((cb: Function) => { listeners.data = cb; }),
|
|
16
|
+
onExit: mock.fn((cb: Function) => { listeners.exit = cb; }),
|
|
17
17
|
write: mock.fn(),
|
|
18
18
|
resize: mock.fn(),
|
|
19
19
|
kill: mock.fn(),
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
it('rejects empty command', () => {
|
|
57
|
-
const result = manager.validateCommand('');
|
|
58
|
-
assert.equal(result.valid, false);
|
|
59
|
-
assert.ok(result.error?.includes('Empty'));
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('rejects whitespace-only command', () => {
|
|
63
|
-
const result = manager.validateCommand(' ');
|
|
64
|
-
assert.equal(result.valid, false);
|
|
65
|
-
assert.ok(result.error?.includes('Empty'));
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('trims leading whitespace before validating', () => {
|
|
69
|
-
const result = manager.validateCommand(' claude login');
|
|
70
|
-
assert.equal(result.valid, true);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('rejects "git" command (not in whitelist)', () => {
|
|
74
|
-
const result = manager.validateCommand('git status');
|
|
75
|
-
assert.equal(result.valid, false);
|
|
76
|
-
assert.ok(result.error?.includes('not allowed'));
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('rejects "claude-imposter" command (must be exact prefix match)', () => {
|
|
80
|
-
const result = manager.validateCommand('claude-imposter');
|
|
81
|
-
assert.equal(result.valid, false);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
20
|
+
_emit: (event: string, data: unknown) => listeners[event]?.(data),
|
|
21
|
+
_listeners: listeners,
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** In-memory mock DB that mimics Drizzle's API surface for terminal_sessions. */
|
|
26
|
+
const createMockDb = () => {
|
|
27
|
+
const rows: any[] = [];
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
_rows: rows,
|
|
31
|
+
select: () => ({
|
|
32
|
+
from: () => ({
|
|
33
|
+
where: (_pred: any) => Promise.resolve(rows.filter(() => true)),
|
|
34
|
+
then: (resolve: Function) => resolve(rows),
|
|
35
|
+
}),
|
|
36
|
+
}),
|
|
37
|
+
insert: () => ({
|
|
38
|
+
values: (val: any) => {
|
|
39
|
+
rows.push(val);
|
|
40
|
+
return Promise.resolve();
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
update: () => ({
|
|
44
|
+
set: () => ({
|
|
45
|
+
where: () => Promise.resolve(),
|
|
46
|
+
}),
|
|
47
|
+
}),
|
|
48
|
+
delete: () => ({
|
|
49
|
+
where: () => {
|
|
50
|
+
rows.length = 0;
|
|
51
|
+
return Promise.resolve();
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
};
|
|
84
56
|
|
|
85
57
|
// --- PTY Session Manager — session lifecycle ---
|
|
86
58
|
|
|
87
59
|
describe('PtySessionManager session lifecycle', () => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
60
|
+
let manager: PtySessionManager;
|
|
61
|
+
let mockSpawn: ReturnType<typeof mock.fn>;
|
|
62
|
+
let mockLog: ReturnType<typeof mock.fn>;
|
|
63
|
+
let mockPty: ReturnType<typeof createMockPty>;
|
|
64
|
+
let mockDb: ReturnType<typeof createMockDb>;
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
mockPty = createMockPty();
|
|
68
|
+
mockSpawn = mock.fn(() => mockPty);
|
|
69
|
+
mockLog = mock.fn();
|
|
70
|
+
mockDb = createMockDb();
|
|
71
|
+
manager = new PtySessionManager({
|
|
72
|
+
spawn: mockSpawn as any,
|
|
73
|
+
log: mockLog,
|
|
74
|
+
projectRoot: '/test',
|
|
75
|
+
getDb: () => mockDb,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('auto-launches claude on session creation with --session-id', async () => {
|
|
80
|
+
const session = await manager.createSession('proj-1', 'Project One', 80, 24);
|
|
95
81
|
assert.ok(session);
|
|
96
82
|
assert.equal(session.projectId, 'proj-1');
|
|
97
83
|
assert.equal(session.projectName, 'Project One');
|
|
98
|
-
assert.equal(session.state, 'running');
|
|
84
|
+
assert.equal(session.state, 'running');
|
|
99
85
|
assert.ok(session.id.startsWith('term_'));
|
|
100
|
-
//
|
|
86
|
+
assert.ok(session.claudeSessionId); // UUID should be set
|
|
87
|
+
|
|
88
|
+
// claude was spawned automatically with project context and --session-id
|
|
101
89
|
assert.equal(mockSpawn.mock.calls.length, 1);
|
|
102
90
|
assert.equal(mockSpawn.mock.calls[0].arguments[0], 'claude');
|
|
103
91
|
const args = mockSpawn.mock.calls[0].arguments[1] as string[];
|
|
104
92
|
assert.ok(args.includes('--dangerously-skip-permissions'));
|
|
105
93
|
assert.ok(args.includes('--append-system-prompt'));
|
|
94
|
+
assert.ok(args.includes('--session-id'));
|
|
95
|
+
assert.ok(args.includes(session.claudeSessionId));
|
|
106
96
|
assert.ok(args.some((a: string) => a.includes('proj-1')));
|
|
107
97
|
});
|
|
108
98
|
|
|
109
|
-
it('creates separate sessions for the same project
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
|
|
113
|
-
|
|
114
|
-
const session1 = manager.createSession('proj-1', 'Project', 80, 24);
|
|
115
|
-
const session2 = manager.createSession('proj-1', 'Project', 100, 30);
|
|
99
|
+
it('creates separate sessions for the same project', async () => {
|
|
100
|
+
const session1 = await manager.createSession('proj-1', 'Project', 80, 24);
|
|
101
|
+
const session2 = await manager.createSession('proj-1', 'Project', 100, 30);
|
|
116
102
|
assert.notEqual(session1.id, session2.id);
|
|
117
|
-
assert.
|
|
103
|
+
assert.notEqual(session1.claudeSessionId, session2.claudeSessionId);
|
|
104
|
+
assert.equal(mockSpawn.mock.calls.length, 2);
|
|
118
105
|
});
|
|
119
106
|
|
|
120
|
-
it('creates separate sessions for different projects', () => {
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
|
|
124
|
-
|
|
125
|
-
const session1 = manager.createSession('proj-1', 'Project One', 80, 24);
|
|
126
|
-
const session2 = manager.createSession('proj-2', 'Project Two', 80, 24);
|
|
107
|
+
it('creates separate sessions for different projects', async () => {
|
|
108
|
+
const session1 = await manager.createSession('proj-1', 'Project One', 80, 24);
|
|
109
|
+
const session2 = await manager.createSession('proj-2', 'Project Two', 80, 24);
|
|
127
110
|
assert.notEqual(session1.id, session2.id);
|
|
128
111
|
});
|
|
129
112
|
|
|
130
|
-
it('
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
|
|
134
|
-
|
|
135
|
-
assert.equal(manager.getSession('term_unknown'), undefined);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('destroySession removes the session and kills running PTY', () => {
|
|
139
|
-
const mockLog = mock.fn();
|
|
140
|
-
const mockPty = {
|
|
141
|
-
onData: mock.fn(),
|
|
142
|
-
onExit: mock.fn(),
|
|
143
|
-
write: mock.fn(),
|
|
144
|
-
resize: mock.fn(),
|
|
145
|
-
kill: mock.fn(),
|
|
146
|
-
};
|
|
147
|
-
const mockSpawn = mock.fn(() => mockPty);
|
|
148
|
-
const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
|
|
149
|
-
|
|
150
|
-
const session = manager.createSession('proj-1', 'Project', 80, 24);
|
|
151
|
-
// session.pty is already set by auto-launch
|
|
152
|
-
assert.ok(manager.getSession(session.id));
|
|
153
|
-
|
|
154
|
-
manager.destroySession(session.id);
|
|
155
|
-
assert.equal(manager.getSession(session.id), undefined);
|
|
113
|
+
it('destroySession kills running PTY', async () => {
|
|
114
|
+
const session = await manager.createSession('proj-1', 'Project', 80, 24);
|
|
115
|
+
await manager.destroySession(session.id);
|
|
156
116
|
assert.equal(mockPty.kill.mock.calls.length, 1);
|
|
157
117
|
});
|
|
158
118
|
|
|
159
|
-
it('
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
onData: mock.fn(),
|
|
163
|
-
onExit: mock.fn(),
|
|
164
|
-
write: mock.fn(),
|
|
165
|
-
resize: mock.fn(),
|
|
166
|
-
kill: mock.fn(),
|
|
167
|
-
};
|
|
168
|
-
const mockPty2 = {
|
|
169
|
-
onData: mock.fn(),
|
|
170
|
-
onExit: mock.fn(),
|
|
171
|
-
write: mock.fn(),
|
|
172
|
-
resize: mock.fn(),
|
|
173
|
-
kill: mock.fn(),
|
|
174
|
-
};
|
|
175
|
-
const ptys = [mockPty1, mockPty2];
|
|
119
|
+
it('destroyAllPty kills all live PTYs without removing DB records', async () => {
|
|
120
|
+
const mockPty2 = createMockPty();
|
|
121
|
+
const ptys = [mockPty, mockPty2];
|
|
176
122
|
let idx = 0;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
123
|
+
mockSpawn = mock.fn(() => ptys[idx++]);
|
|
124
|
+
manager = new PtySessionManager({
|
|
125
|
+
spawn: mockSpawn as any,
|
|
126
|
+
log: mockLog,
|
|
127
|
+
projectRoot: '/test',
|
|
128
|
+
getDb: () => mockDb,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await manager.createSession('proj-1', 'Project One', 80, 24);
|
|
132
|
+
await manager.createSession('proj-2', 'Project Two', 80, 24);
|
|
133
|
+
|
|
134
|
+
manager.destroyAllPty();
|
|
135
|
+
assert.equal(mockPty.kill.mock.calls.length, 1);
|
|
136
|
+
assert.equal(mockPty2.kill.mock.calls.length, 1);
|
|
137
|
+
// DB rows should still exist
|
|
138
|
+
assert.equal(mockDb._rows.length, 2);
|
|
187
139
|
});
|
|
188
140
|
|
|
189
|
-
it('resizeSession updates
|
|
190
|
-
const
|
|
191
|
-
const mockPty = {
|
|
192
|
-
onData: mock.fn(),
|
|
193
|
-
onExit: mock.fn(),
|
|
194
|
-
write: mock.fn(),
|
|
195
|
-
resize: mock.fn(),
|
|
196
|
-
kill: mock.fn(),
|
|
197
|
-
};
|
|
198
|
-
const mockSpawn = mock.fn(() => mockPty);
|
|
199
|
-
const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
|
|
200
|
-
|
|
201
|
-
const session = manager.createSession('proj-1', 'Project', 80, 24);
|
|
202
|
-
// PTY already running via auto-launch
|
|
141
|
+
it('resizeSession updates dimensions and forwards to running PTY', async () => {
|
|
142
|
+
const session = await manager.createSession('proj-1', 'Project', 80, 24);
|
|
203
143
|
manager.resizeSession(session.id, 120, 40);
|
|
204
144
|
|
|
205
145
|
assert.equal(mockPty.resize.mock.calls.length, 1);
|
|
206
146
|
assert.deepEqual(mockPty.resize.mock.calls[0].arguments, [120, 40]);
|
|
207
|
-
|
|
208
|
-
const updated = manager.getSession(session.id);
|
|
209
|
-
assert.equal(updated?.cols, 120);
|
|
210
|
-
assert.equal(updated?.rows, 40);
|
|
211
147
|
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// --- Command execution model (dec_029 enforcement) ---
|
|
215
|
-
|
|
216
|
-
describe('Command execution — whitelist enforced on every command', () => {
|
|
217
|
-
it('rejects disallowed command typed character-by-character', () => {
|
|
218
|
-
const mockLog = mock.fn();
|
|
219
|
-
let exitHandler: (info: { exitCode: number }) => void = () => {};
|
|
220
|
-
const mockPty = {
|
|
221
|
-
onData: mock.fn(),
|
|
222
|
-
onExit: mock.fn((cb: Function) => { exitHandler = cb as any; }),
|
|
223
|
-
write: mock.fn(), resize: mock.fn(), kill: mock.fn(),
|
|
224
|
-
};
|
|
225
|
-
const mockSpawn = mock.fn(() => mockPty);
|
|
226
|
-
const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
|
|
227
|
-
const output: string[] = [];
|
|
228
|
-
|
|
229
|
-
const session = manager.createSession('proj-1', 'Project', 80, 24);
|
|
230
|
-
// Let auto-launch exit to enter idle mode
|
|
231
|
-
exitHandler({ exitCode: 0 });
|
|
232
|
-
manager.addListener(session.id, (data) => output.push(data));
|
|
233
|
-
|
|
234
|
-
// Type "ls" and press Enter (in idle mode)
|
|
235
|
-
manager.writeToSession(session.id, 'l');
|
|
236
|
-
manager.writeToSession(session.id, 's');
|
|
237
|
-
manager.writeToSession(session.id, '\r');
|
|
238
|
-
|
|
239
|
-
// Only auto-launch was spawned, no new PTY for disallowed command
|
|
240
|
-
assert.equal(mockSpawn.mock.calls.length, 1);
|
|
241
|
-
|
|
242
|
-
// Output should contain the error message
|
|
243
|
-
const fullOutput = output.join('');
|
|
244
|
-
assert.ok(fullOutput.includes('not allowed'));
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('allows whitelisted command and spawns PTY', () => {
|
|
248
|
-
const mockLog = mock.fn();
|
|
249
|
-
let exitHandler: (info: { exitCode: number }) => void = () => {};
|
|
250
|
-
const mockPty = {
|
|
251
|
-
onData: mock.fn(),
|
|
252
|
-
onExit: mock.fn((cb: Function) => { exitHandler = cb as any; }),
|
|
253
|
-
write: mock.fn(),
|
|
254
|
-
resize: mock.fn(),
|
|
255
|
-
kill: mock.fn(),
|
|
256
|
-
};
|
|
257
|
-
const mockSpawn = mock.fn(() => mockPty);
|
|
258
|
-
const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
|
|
259
|
-
|
|
260
|
-
const session = manager.createSession('proj-1', 'Project', 80, 24);
|
|
261
|
-
// Let auto-launch exit to enter idle mode
|
|
262
|
-
exitHandler({ exitCode: 0 });
|
|
263
|
-
|
|
264
|
-
manager.writeToSession(session.id, 'claude login\r');
|
|
265
|
-
|
|
266
|
-
// Auto-launch (call 0) + user command (call 1)
|
|
267
|
-
assert.equal(mockSpawn.mock.calls.length, 2);
|
|
268
|
-
assert.equal(mockSpawn.mock.calls[1].arguments[0], 'claude');
|
|
269
|
-
assert.deepEqual(mockSpawn.mock.calls[1].arguments[1], ['login']);
|
|
270
|
-
|
|
271
|
-
// Session should be in running state
|
|
272
|
-
const updated = manager.getSession(session.id);
|
|
273
|
-
assert.equal(updated?.state, 'running');
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it('forwards raw input to running PTY process', () => {
|
|
277
|
-
const mockLog = mock.fn();
|
|
278
|
-
const mockPty = {
|
|
279
|
-
onData: mock.fn(),
|
|
280
|
-
onExit: mock.fn(),
|
|
281
|
-
write: mock.fn(),
|
|
282
|
-
resize: mock.fn(),
|
|
283
|
-
kill: mock.fn(),
|
|
284
|
-
};
|
|
285
|
-
const mockSpawn = mock.fn(() => mockPty);
|
|
286
|
-
const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
|
|
287
148
|
|
|
288
|
-
|
|
289
|
-
|
|
149
|
+
it('forwards raw input to running PTY process', async () => {
|
|
150
|
+
await manager.createSession('proj-1', 'Project', 80, 24);
|
|
151
|
+
const session = await manager.createSession('proj-1', 'Project', 80, 24);
|
|
290
152
|
manager.writeToSession(session.id, 'hello');
|
|
291
153
|
const lastWrite = mockPty.write.mock.calls[mockPty.write.mock.calls.length - 1];
|
|
292
154
|
assert.equal(lastWrite.arguments[0], 'hello');
|
|
293
155
|
});
|
|
156
|
+
});
|
|
294
157
|
|
|
295
|
-
|
|
296
|
-
const mockLog = mock.fn();
|
|
297
|
-
let exitHandler: (info: { exitCode: number; signal?: number }) => void = () => {};
|
|
298
|
-
const mockPty = {
|
|
299
|
-
onData: mock.fn(),
|
|
300
|
-
onExit: mock.fn((handler: (info: { exitCode: number; signal?: number }) => void) => {
|
|
301
|
-
exitHandler = handler;
|
|
302
|
-
}),
|
|
303
|
-
write: mock.fn(),
|
|
304
|
-
resize: mock.fn(),
|
|
305
|
-
kill: mock.fn(),
|
|
306
|
-
};
|
|
307
|
-
const mockSpawn = mock.fn(() => mockPty);
|
|
308
|
-
const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
|
|
309
|
-
const output: string[] = [];
|
|
310
|
-
|
|
311
|
-
const session = manager.createSession('proj-1', 'Project', 80, 24);
|
|
312
|
-
manager.addListener(session.id, (data) => output.push(data));
|
|
313
|
-
manager.writeToSession(session.id, 'claude --version\r');
|
|
314
|
-
|
|
315
|
-
// Simulate command exit
|
|
316
|
-
exitHandler({ exitCode: 0 });
|
|
317
|
-
|
|
318
|
-
const updated = manager.getSession(session.id);
|
|
319
|
-
assert.equal(updated?.state, 'idle');
|
|
320
|
-
assert.equal(updated?.pty, null);
|
|
321
|
-
|
|
322
|
-
// Should show prompt again
|
|
323
|
-
const fullOutput = output.join('');
|
|
324
|
-
assert.ok(fullOutput.includes('$'));
|
|
325
|
-
});
|
|
158
|
+
// --- Session resumption ---
|
|
326
159
|
|
|
327
|
-
|
|
160
|
+
describe('Session resumption across restarts', () => {
|
|
161
|
+
it('ensureRunning does not re-spawn for already live session', async () => {
|
|
162
|
+
const mockPty1 = createMockPty();
|
|
163
|
+
const mockSpawn = mock.fn(() => mockPty1);
|
|
328
164
|
const mockLog = mock.fn();
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
};
|
|
339
|
-
const mockSpawn = mock.fn(() => mockPty);
|
|
340
|
-
const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
|
|
341
|
-
const output: string[] = [];
|
|
342
|
-
|
|
343
|
-
const session = manager.createSession('proj-1', 'Project', 80, 24);
|
|
344
|
-
manager.addListener(session.id, (data) => output.push(data));
|
|
345
|
-
|
|
346
|
-
// Let auto-launch exit to enter idle mode
|
|
347
|
-
exitHandler({ exitCode: 0 });
|
|
348
|
-
|
|
349
|
-
// Clear output to check next command
|
|
350
|
-
output.length = 0;
|
|
351
|
-
|
|
352
|
-
// Try to execute a disallowed command after returning to idle
|
|
353
|
-
manager.writeToSession(session.id, 'rm -rf /\r');
|
|
354
|
-
|
|
355
|
-
// Should NOT spawn a new PTY beyond the initial auto-launch
|
|
165
|
+
const mockDb = createMockDb();
|
|
166
|
+
const manager = new PtySessionManager({
|
|
167
|
+
spawn: mockSpawn as any,
|
|
168
|
+
log: mockLog,
|
|
169
|
+
projectRoot: '/test',
|
|
170
|
+
getDb: () => mockDb,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const session = await manager.createSession('proj-1', 'Project', 80, 24);
|
|
356
174
|
assert.equal(mockSpawn.mock.calls.length, 1);
|
|
357
|
-
const fullOutput = output.join('');
|
|
358
|
-
assert.ok(fullOutput.includes('not allowed'));
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('handles backspace in idle mode', () => {
|
|
362
|
-
const mockLog = mock.fn();
|
|
363
|
-
let exitHandler: (info: { exitCode: number }) => void = () => {};
|
|
364
|
-
const mockPty = {
|
|
365
|
-
onData: mock.fn(),
|
|
366
|
-
onExit: mock.fn((cb: Function) => { exitHandler = cb as any; }),
|
|
367
|
-
write: mock.fn(),
|
|
368
|
-
resize: mock.fn(),
|
|
369
|
-
kill: mock.fn(),
|
|
370
|
-
};
|
|
371
|
-
const mockSpawn = mock.fn(() => mockPty);
|
|
372
|
-
const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
|
|
373
|
-
const output: string[] = [];
|
|
374
|
-
|
|
375
|
-
const session = manager.createSession('proj-1', 'Project', 80, 24);
|
|
376
|
-
// Let auto-launch exit to enter idle mode
|
|
377
|
-
exitHandler({ exitCode: 0 });
|
|
378
|
-
manager.addListener(session.id, (data) => output.push(data));
|
|
379
|
-
|
|
380
|
-
// Type "ls", backspace twice, type "claude"
|
|
381
|
-
manager.writeToSession(session.id, 'ls');
|
|
382
|
-
manager.writeToSession(session.id, '\x7f\x7f'); // two backspaces
|
|
383
|
-
manager.writeToSession(session.id, 'claude\r');
|
|
384
|
-
|
|
385
|
-
// auto-launch (call 0) + user's claude command (call 1)
|
|
386
|
-
assert.equal(mockSpawn.mock.calls.length, 2);
|
|
387
|
-
assert.equal(mockSpawn.mock.calls[1].arguments[0], 'claude');
|
|
388
|
-
assert.deepEqual(mockSpawn.mock.calls[1].arguments[1], []);
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it('handles Ctrl+C in idle mode (clears input)', () => {
|
|
392
|
-
const mockLog = mock.fn();
|
|
393
|
-
let exitHandler: (info: { exitCode: number }) => void = () => {};
|
|
394
|
-
const mockPty = {
|
|
395
|
-
onData: mock.fn(),
|
|
396
|
-
onExit: mock.fn((cb: Function) => { exitHandler = cb as any; }),
|
|
397
|
-
write: mock.fn(),
|
|
398
|
-
resize: mock.fn(),
|
|
399
|
-
kill: mock.fn(),
|
|
400
|
-
};
|
|
401
|
-
const mockSpawn = mock.fn(() => mockPty);
|
|
402
|
-
const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
|
|
403
|
-
const output: string[] = [];
|
|
404
|
-
|
|
405
|
-
const session = manager.createSession('proj-1', 'Project', 80, 24);
|
|
406
|
-
// Let auto-launch exit to enter idle mode
|
|
407
|
-
exitHandler({ exitCode: 0 });
|
|
408
|
-
manager.addListener(session.id, (data) => output.push(data));
|
|
409
175
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
// Then type a valid command
|
|
415
|
-
manager.writeToSession(session.id, 'claude\r');
|
|
416
|
-
|
|
417
|
-
// auto-launch (call 0) + user's claude command (call 1), Ctrl+C cleared "rm"
|
|
418
|
-
assert.equal(mockSpawn.mock.calls.length, 2);
|
|
419
|
-
assert.equal(mockSpawn.mock.calls[1].arguments[0], 'claude');
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
it('handles empty Enter (shows prompt again)', () => {
|
|
423
|
-
const mockLog = mock.fn();
|
|
424
|
-
let exitHandler: (info: { exitCode: number }) => void = () => {};
|
|
425
|
-
const mockPty = {
|
|
426
|
-
onData: mock.fn(),
|
|
427
|
-
onExit: mock.fn((cb: Function) => { exitHandler = cb as any; }),
|
|
428
|
-
write: mock.fn(), resize: mock.fn(), kill: mock.fn(),
|
|
429
|
-
};
|
|
430
|
-
const mockSpawn = mock.fn(() => mockPty);
|
|
431
|
-
const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
|
|
432
|
-
const output: string[] = [];
|
|
433
|
-
|
|
434
|
-
const session = manager.createSession('proj-1', 'Project', 80, 24);
|
|
435
|
-
// Let auto-launch exit to enter idle mode
|
|
436
|
-
exitHandler({ exitCode: 0 });
|
|
437
|
-
manager.addListener(session.id, (data) => output.push(data));
|
|
438
|
-
|
|
439
|
-
manager.writeToSession(session.id, '\r');
|
|
440
|
-
|
|
441
|
-
// No additional spawn beyond auto-launch
|
|
176
|
+
const result = await manager.ensureRunning(session.id, 80, 24);
|
|
177
|
+
assert.equal(result, true);
|
|
178
|
+
// No extra spawn
|
|
442
179
|
assert.equal(mockSpawn.mock.calls.length, 1);
|
|
443
|
-
|
|
444
|
-
// Should show prompt
|
|
445
|
-
const fullOutput = output.join('');
|
|
446
|
-
assert.ok(fullOutput.includes('$'));
|
|
447
180
|
});
|
|
448
181
|
|
|
449
|
-
it('
|
|
182
|
+
it('ensureRunning returns false for unknown session', async () => {
|
|
183
|
+
const mockSpawn = mock.fn();
|
|
450
184
|
const mockLog = mock.fn();
|
|
451
|
-
const
|
|
452
|
-
const manager = new PtySessionManager({
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
manager.writeToSession(session.id, 'claude\r');
|
|
459
|
-
|
|
460
|
-
// Session should remain idle
|
|
461
|
-
const updated = manager.getSession(session.id);
|
|
462
|
-
assert.equal(updated?.state, 'idle');
|
|
185
|
+
const mockDb = createMockDb();
|
|
186
|
+
const manager = new PtySessionManager({
|
|
187
|
+
spawn: mockSpawn as any,
|
|
188
|
+
log: mockLog,
|
|
189
|
+
projectRoot: '/test',
|
|
190
|
+
getDb: () => mockDb,
|
|
191
|
+
});
|
|
463
192
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
assert.ok(fullOutput.includes('Failed to execute'));
|
|
467
|
-
assert.ok(fullOutput.includes('$'));
|
|
193
|
+
const result = await manager.ensureRunning('term_unknown', 80, 24);
|
|
194
|
+
assert.equal(result, false);
|
|
468
195
|
});
|
|
469
196
|
});
|
|
470
197
|
|
|
471
|
-
// --- Output buffer
|
|
198
|
+
// --- Output buffer ---
|
|
472
199
|
|
|
473
|
-
describe('PTY output buffering
|
|
474
|
-
it('buffers
|
|
475
|
-
const mockLog = mock.fn();
|
|
476
|
-
const mockPty = { onData: mock.fn(), onExit: mock.fn(), write: mock.fn(), resize: mock.fn(), kill: mock.fn() };
|
|
477
|
-
const mockSpawn = mock.fn(() => mockPty);
|
|
478
|
-
const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
|
|
479
|
-
|
|
480
|
-
const session = manager.createSession('proj-1', 'Project', 80, 24);
|
|
481
|
-
|
|
482
|
-
const buffered = manager.getBufferedOutput(session.id);
|
|
483
|
-
assert.ok(buffered.includes('Restricted terminal'));
|
|
484
|
-
// prompt appears after auto-launched claude exits, not before
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
it('buffers PTY command output for reconnection', () => {
|
|
488
|
-
const mockLog = mock.fn();
|
|
200
|
+
describe('PTY output buffering', () => {
|
|
201
|
+
it('buffers PTY command output for reconnection', async () => {
|
|
489
202
|
let dataHandler: ((data: string) => void) | null = null;
|
|
490
|
-
const
|
|
203
|
+
const mockPty1 = {
|
|
491
204
|
onData: mock.fn((handler: (data: string) => void) => { dataHandler = handler; }),
|
|
492
205
|
onExit: mock.fn(),
|
|
493
206
|
write: mock.fn(),
|
|
494
207
|
resize: mock.fn(),
|
|
495
208
|
kill: mock.fn(),
|
|
496
209
|
};
|
|
497
|
-
const mockSpawn = mock.fn(() =>
|
|
498
|
-
const
|
|
210
|
+
const mockSpawn = mock.fn(() => mockPty1);
|
|
211
|
+
const mockLog = mock.fn();
|
|
212
|
+
const mockDb = createMockDb();
|
|
213
|
+
const manager = new PtySessionManager({
|
|
214
|
+
spawn: mockSpawn as any,
|
|
215
|
+
log: mockLog,
|
|
216
|
+
projectRoot: '/test',
|
|
217
|
+
getDb: () => mockDb,
|
|
218
|
+
});
|
|
499
219
|
|
|
500
|
-
const session = manager.createSession('proj-1', 'Project', 80, 24);
|
|
501
|
-
// dataHandler is captured from the auto-launched claude PTY
|
|
220
|
+
const session = await manager.createSession('proj-1', 'Project', 80, 24);
|
|
502
221
|
|
|
503
222
|
assert.ok(dataHandler);
|
|
504
223
|
dataHandler!('Claude v1.0.0');
|
|
@@ -508,54 +227,99 @@ describe('PTY output buffering (dec_030)', () => {
|
|
|
508
227
|
});
|
|
509
228
|
|
|
510
229
|
it('returns empty string for unknown session id', () => {
|
|
511
|
-
const mockLog = mock.fn();
|
|
512
230
|
const mockSpawn = mock.fn();
|
|
513
|
-
const
|
|
231
|
+
const mockLog = mock.fn();
|
|
232
|
+
const mockDb = createMockDb();
|
|
233
|
+
const manager = new PtySessionManager({
|
|
234
|
+
spawn: mockSpawn as any,
|
|
235
|
+
log: mockLog,
|
|
236
|
+
projectRoot: '/test',
|
|
237
|
+
getDb: () => mockDb,
|
|
238
|
+
});
|
|
514
239
|
|
|
515
240
|
assert.equal(manager.getBufferedOutput('term_unknown'), '');
|
|
516
241
|
});
|
|
517
242
|
|
|
518
|
-
it('notifies listeners when output is produced
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
243
|
+
it('notifies listeners when output is produced', async () => {
|
|
244
|
+
let dataHandler: ((data: string) => void) | null = null;
|
|
245
|
+
const mockPty1 = {
|
|
246
|
+
onData: mock.fn((handler: (data: string) => void) => { dataHandler = handler; }),
|
|
247
|
+
onExit: mock.fn(),
|
|
248
|
+
write: mock.fn(),
|
|
249
|
+
resize: mock.fn(),
|
|
250
|
+
kill: mock.fn(),
|
|
525
251
|
};
|
|
526
|
-
const mockSpawn = mock.fn(() =>
|
|
527
|
-
const
|
|
252
|
+
const mockSpawn = mock.fn(() => mockPty1);
|
|
253
|
+
const mockLog = mock.fn();
|
|
254
|
+
const mockDb = createMockDb();
|
|
255
|
+
const manager = new PtySessionManager({
|
|
256
|
+
spawn: mockSpawn as any,
|
|
257
|
+
log: mockLog,
|
|
258
|
+
projectRoot: '/test',
|
|
259
|
+
getDb: () => mockDb,
|
|
260
|
+
});
|
|
528
261
|
|
|
529
|
-
const session = manager.createSession('proj-1', 'Project', 80, 24);
|
|
530
|
-
// Let auto-launch exit to enter idle mode
|
|
531
|
-
exitHandler({ exitCode: 0 });
|
|
262
|
+
const session = await manager.createSession('proj-1', 'Project', 80, 24);
|
|
532
263
|
|
|
533
264
|
const listener = mock.fn();
|
|
534
265
|
manager.addListener(session.id, listener);
|
|
535
266
|
|
|
536
|
-
//
|
|
537
|
-
|
|
267
|
+
// Simulate PTY output
|
|
268
|
+
dataHandler!('test output');
|
|
538
269
|
assert.ok(listener.mock.calls.length > 0);
|
|
539
|
-
assert.equal(listener.mock.calls[listener.mock.calls.length - 1].arguments[0], '
|
|
270
|
+
assert.equal(listener.mock.calls[listener.mock.calls.length - 1].arguments[0], 'test output');
|
|
540
271
|
});
|
|
541
272
|
|
|
542
|
-
it('removeListener stops notifications', () => {
|
|
273
|
+
it('removeListener stops notifications', async () => {
|
|
274
|
+
let dataHandler: ((data: string) => void) | null = null;
|
|
275
|
+
const mockPty1 = {
|
|
276
|
+
onData: mock.fn((handler: (data: string) => void) => { dataHandler = handler; }),
|
|
277
|
+
onExit: mock.fn(),
|
|
278
|
+
write: mock.fn(),
|
|
279
|
+
resize: mock.fn(),
|
|
280
|
+
kill: mock.fn(),
|
|
281
|
+
};
|
|
282
|
+
const mockSpawn = mock.fn(() => mockPty1);
|
|
543
283
|
const mockLog = mock.fn();
|
|
544
|
-
const
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
284
|
+
const mockDb = createMockDb();
|
|
285
|
+
const manager = new PtySessionManager({
|
|
286
|
+
spawn: mockSpawn as any,
|
|
287
|
+
log: mockLog,
|
|
288
|
+
projectRoot: '/test',
|
|
289
|
+
getDb: () => mockDb,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const session = await manager.createSession('proj-1', 'Project', 80, 24);
|
|
549
293
|
const listener = mock.fn();
|
|
550
294
|
manager.addListener(session.id, listener);
|
|
551
295
|
manager.removeListener(session.id, listener);
|
|
552
296
|
|
|
553
297
|
const callsBefore = listener.mock.calls.length;
|
|
554
|
-
|
|
298
|
+
dataHandler!('test output');
|
|
555
299
|
assert.equal(listener.mock.calls.length, callsBefore);
|
|
556
300
|
});
|
|
557
301
|
});
|
|
558
302
|
|
|
303
|
+
// --- Session name formatting ---
|
|
304
|
+
|
|
305
|
+
describe('Session name formatting', () => {
|
|
306
|
+
it('formats session name as ProjectName - DD/MM/YY - HH:MM', () => {
|
|
307
|
+
const mockSpawn = mock.fn();
|
|
308
|
+
const mockLog = mock.fn();
|
|
309
|
+
const mockDb = createMockDb();
|
|
310
|
+
const manager = new PtySessionManager({
|
|
311
|
+
spawn: mockSpawn as any,
|
|
312
|
+
log: mockLog,
|
|
313
|
+
projectRoot: '/test',
|
|
314
|
+
getDb: () => mockDb,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const date = new Date(2026, 2, 6, 14, 30);
|
|
318
|
+
const name = manager.formatSessionName('My Project', date);
|
|
319
|
+
assert.equal(name, 'My Project - 06/03/26 - 14:30');
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
559
323
|
// --- WebSocket auth checks ---
|
|
560
324
|
|
|
561
325
|
describe('Terminal WebSocket authentication', () => {
|