@assistkick/create 1.7.0 → 1.9.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.
Files changed (206) hide show
  1. package/dist/bin/create.js +0 -0
  2. package/package.json +9 -7
  3. package/templates/assistkick-product-system/.env.example +1 -0
  4. package/templates/assistkick-product-system/local.db +0 -0
  5. package/templates/assistkick-product-system/package.json +4 -2
  6. package/templates/assistkick-product-system/packages/backend/package.json +2 -0
  7. package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +165 -0
  8. package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +358 -0
  9. package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +356 -0
  10. package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +96 -1
  11. package/templates/assistkick-product-system/packages/backend/src/routes/graph.ts +1 -0
  12. package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +61 -6
  13. package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +200 -84
  14. package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +6 -3
  15. package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +53 -17
  16. package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +218 -0
  17. package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +119 -0
  18. package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +158 -0
  19. package/templates/assistkick-product-system/packages/backend/src/server.ts +60 -9
  20. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +489 -0
  21. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +416 -0
  22. package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.test.ts +189 -0
  23. package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.ts +182 -0
  24. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +43 -77
  25. package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +16 -0
  26. package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +73 -2
  27. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +4 -4
  28. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +87 -11
  29. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +210 -69
  30. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +210 -215
  31. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +162 -0
  32. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +148 -0
  33. package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +11 -5
  34. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.test.ts +64 -0
  35. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +134 -0
  36. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.test.ts +256 -0
  37. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +258 -0
  38. package/templates/assistkick-product-system/packages/backend/src/services/workflow_group_service.ts +106 -0
  39. package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.test.ts +275 -0
  40. package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.ts +245 -0
  41. package/templates/assistkick-product-system/packages/frontend/package-lock.json +3455 -0
  42. package/templates/assistkick-product-system/packages/frontend/package.json +6 -0
  43. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +8 -0
  44. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +458 -18
  45. package/templates/assistkick-product-system/packages/frontend/src/api/client_files.test.ts +172 -0
  46. package/templates/assistkick-product-system/packages/frontend/src/api/client_video.test.ts +238 -0
  47. package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +307 -0
  48. package/templates/assistkick-product-system/packages/frontend/src/components/CoherenceView.tsx +82 -66
  49. package/templates/assistkick-product-system/packages/frontend/src/components/CompositionPlaceholder.tsx +97 -0
  50. package/templates/assistkick-product-system/packages/frontend/src/components/DesignSystemView.tsx +20 -0
  51. package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +57 -0
  52. package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +313 -0
  53. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeContextMenu.tsx +61 -0
  54. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +73 -0
  55. package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +404 -0
  56. package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +187 -56
  57. package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +71 -73
  58. package/templates/assistkick-product-system/packages/frontend/src/components/GraphSettings.tsx +8 -8
  59. package/templates/assistkick-product-system/packages/frontend/src/components/GraphView.tsx +1 -1
  60. package/templates/assistkick-product-system/packages/frontend/src/components/InviteUserDialog.tsx +15 -11
  61. package/templates/assistkick-product-system/packages/frontend/src/components/IterationCommentModal.tsx +80 -0
  62. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +263 -167
  63. package/templates/assistkick-product-system/packages/frontend/src/components/LoginPage.tsx +14 -14
  64. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +54 -33
  65. package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +32 -49
  66. package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +43 -48
  67. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +121 -52
  68. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +20 -14
  69. package/templates/assistkick-product-system/packages/frontend/src/components/UsersView.tsx +52 -52
  70. package/templates/assistkick-product-system/packages/frontend/src/components/VideoGallery.tsx +313 -0
  71. package/templates/assistkick-product-system/packages/frontend/src/components/VideographyView.tsx +250 -0
  72. package/templates/assistkick-product-system/packages/frontend/src/components/WorkflowsView.tsx +474 -0
  73. package/templates/assistkick-product-system/packages/frontend/src/components/ds/AccentBorderList.tsx +53 -0
  74. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Button.tsx +87 -0
  75. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonGroup.tsx +29 -0
  76. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonShowcase.tsx +221 -0
  77. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CardGlass.tsx +141 -0
  78. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CompletionRing.tsx +30 -0
  79. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ContentCard.tsx +34 -0
  80. package/templates/assistkick-product-system/packages/frontend/src/components/ds/IconButton.tsx +74 -0
  81. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +103 -87
  82. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +9 -188
  83. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Kbd.tsx +11 -0
  84. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KindBadge.tsx +21 -0
  85. package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +81 -37
  86. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SidePanelShowcase.tsx +370 -0
  87. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SideSheet.tsx +64 -0
  88. package/templates/assistkick-product-system/packages/frontend/src/components/ds/StatusDot.tsx +18 -0
  89. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCardPositionNode.tsx +36 -0
  90. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCycleCountNode.tsx +60 -0
  91. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/EndNode.tsx +42 -0
  92. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GenerateTTSNode.tsx +52 -0
  93. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +189 -0
  94. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +123 -0
  95. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RebuildBundleNode.tsx +20 -0
  96. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RenderVideoNode.tsx +72 -0
  97. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RunAgentNode.tsx +51 -0
  98. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/SetCardMetadataNode.tsx +53 -0
  99. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/StartNode.tsx +18 -0
  100. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/TransitionCardNode.tsx +59 -0
  101. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +341 -0
  102. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +643 -0
  103. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/autoLayout.ts +103 -0
  104. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/edgeColors.ts +35 -0
  105. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +246 -0
  106. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.test.ts +119 -0
  107. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +136 -0
  108. package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +13 -11
  109. package/templates/assistkick-product-system/packages/frontend/src/hooks/useAutoSave.ts +75 -0
  110. package/templates/assistkick-product-system/packages/frontend/src/hooks/useToast.tsx +16 -3
  111. package/templates/assistkick-product-system/packages/frontend/src/pages/accept_invitation_page.tsx +30 -27
  112. package/templates/assistkick-product-system/packages/frontend/src/pages/forgot_password_page.tsx +18 -15
  113. package/templates/assistkick-product-system/packages/frontend/src/pages/register_page.tsx +21 -18
  114. package/templates/assistkick-product-system/packages/frontend/src/pages/reset_password_page.tsx +28 -25
  115. package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +6 -0
  116. package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +1 -1
  117. package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +2 -2
  118. package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +13 -0
  119. package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -2
  120. package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +13 -0
  121. package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +6 -0
  122. package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +6 -3
  123. package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +4 -4
  124. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +275 -3535
  125. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.test.ts +167 -0
  126. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.ts +101 -0
  127. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.test.ts +42 -0
  128. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.ts +17 -0
  129. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.test.ts +145 -0
  130. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.ts +42 -0
  131. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.test.ts +4 -10
  132. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.ts +19 -1
  133. package/templates/assistkick-product-system/packages/frontend/vite.config.ts +5 -0
  134. package/templates/assistkick-product-system/packages/shared/db/local.db +0 -0
  135. package/templates/assistkick-product-system/packages/shared/db/migrations/0004_tidy_matthew_murdock.sql +9 -0
  136. package/templates/assistkick-product-system/packages/shared/db/migrations/0005_mysterious_falcon.sql +692 -0
  137. package/templates/assistkick-product-system/packages/shared/db/migrations/0006_next_venom.sql +9 -0
  138. package/templates/assistkick-product-system/packages/shared/db/migrations/0007_deep_barracuda.sql +39 -0
  139. package/templates/assistkick-product-system/packages/shared/db/migrations/0008_puzzling_hannibal_king.sql +1 -0
  140. package/templates/assistkick-product-system/packages/shared/db/migrations/0009_amused_beast.sql +8 -0
  141. package/templates/assistkick-product-system/packages/shared/db/migrations/0010_spotty_moira_mactaggert.sql +9 -0
  142. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_goofy_snowbird.sql +3 -0
  143. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_supreme_doctor_octopus.sql +3 -0
  144. package/templates/assistkick-product-system/packages/shared/db/migrations/0013_reflective_prowler.sql +15 -0
  145. package/templates/assistkick-product-system/packages/shared/db/migrations/0014_nifty_punisher.sql +15 -0
  146. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0004_snapshot.json +921 -0
  147. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0005_snapshot.json +1042 -0
  148. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0006_snapshot.json +1101 -0
  149. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0007_snapshot.json +1336 -0
  150. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0008_snapshot.json +1275 -0
  151. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0009_snapshot.json +1327 -0
  152. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0010_snapshot.json +1393 -0
  153. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0011_snapshot.json +1436 -0
  154. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0013_snapshot.json +1538 -0
  155. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0014_snapshot.json +1545 -0
  156. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +77 -0
  157. package/templates/assistkick-product-system/packages/shared/db/schema.ts +114 -0
  158. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +32 -7
  159. package/templates/assistkick-product-system/packages/shared/lib/constants.ts +9 -0
  160. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +12 -4
  161. package/templates/assistkick-product-system/packages/shared/lib/graph.ts +5 -0
  162. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +1999 -0
  163. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +1437 -0
  164. package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +211 -0
  165. package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +43 -0
  166. package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +13 -2
  167. package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +1 -1
  168. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.test.ts +226 -0
  169. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.ts +251 -0
  170. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
  171. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.test.ts +10 -0
  172. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.ts +6 -0
  173. package/templates/assistkick-product-system/packages/video/Root.tsx +85 -0
  174. package/templates/assistkick-product-system/packages/video/components/email_scene.tsx +231 -0
  175. package/templates/assistkick-product-system/packages/video/components/outro_scene.tsx +153 -0
  176. package/templates/assistkick-product-system/packages/video/components/part_divider.tsx +90 -0
  177. package/templates/assistkick-product-system/packages/video/components/scene.tsx +226 -0
  178. package/templates/assistkick-product-system/packages/video/components/theme.ts +22 -0
  179. package/templates/assistkick-product-system/packages/video/components/title_scene.tsx +169 -0
  180. package/templates/assistkick-product-system/packages/video/components/video_split_layout.tsx +84 -0
  181. package/templates/assistkick-product-system/packages/video/compositions/.gitkeep +0 -0
  182. package/templates/assistkick-product-system/packages/video/index.ts +4 -0
  183. package/templates/assistkick-product-system/packages/video/package.json +28 -0
  184. package/templates/assistkick-product-system/packages/video/remotion.config.ts +11 -0
  185. package/templates/assistkick-product-system/packages/video/scripts/process_script.test.ts +326 -0
  186. package/templates/assistkick-product-system/packages/video/scripts/process_script.ts +630 -0
  187. package/templates/assistkick-product-system/packages/video/style.css +1 -0
  188. package/templates/assistkick-product-system/packages/video/tsconfig.json +18 -0
  189. package/templates/assistkick-product-system/tests/graph_legend.test.ts +2 -1
  190. package/templates/assistkick-product-system/tests/video_render_service.test.ts +181 -0
  191. package/templates/assistkick-product-system/tests/web_terminal.test.ts +219 -455
  192. package/templates/assistkick-product-system/tests/workflow_integration.test.ts +341 -0
  193. package/templates/skills/assistkick-developer/SKILL.md +3 -0
  194. package/templates/skills/assistkick-developer/references/react_development_guidelines.md +225 -0
  195. package/templates/skills/product-system/graph.json +1890 -0
  196. package/templates/skills/product-system/kanban.json +304 -0
  197. package/templates/skills/product-system/nodes/comp_001.md +56 -0
  198. package/templates/skills/product-system/nodes/comp_002.md +57 -0
  199. package/templates/skills/product-system/nodes/data_001.md +51 -0
  200. package/templates/skills/product-system/nodes/data_002.md +40 -0
  201. package/templates/skills/product-system/nodes/data_004.md +38 -0
  202. package/templates/skills/product-system/nodes/dec_001.md +34 -0
  203. package/templates/skills/product-system/nodes/dec_016.md +32 -0
  204. package/templates/skills/product-system/nodes/feat_008.md +30 -0
  205. package/templates/skills/video-composition-agent/SKILL.md +232 -0
  206. 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, command whitelist validation (dec_029),
4
- * command execution model, and WebSocket handler auth checks.
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
- // --- Command Whitelist (dec_029) ---
10
+ // --- Mock helpers ---
11
11
 
12
- describe('Command Prefix Whitelist (dec_029)', () => {
13
- const mockLog = mock.fn();
14
- const mockSpawn = mock.fn(() => ({
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
- const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
22
-
23
- it('allows "claude" command', () => {
24
- const result = manager.validateCommand('claude');
25
- assert.equal(result.valid, true);
26
- assert.equal(result.error, undefined);
27
- });
28
-
29
- it('allows "claude login" command', () => {
30
- const result = manager.validateCommand('claude login');
31
- assert.equal(result.valid, true);
32
- });
33
-
34
- it('allows "claude --version" command', () => {
35
- const result = manager.validateCommand('claude --version');
36
- assert.equal(result.valid, true);
37
- });
38
-
39
- it('rejects "ls" command', () => {
40
- const result = manager.validateCommand('ls');
41
- assert.equal(result.valid, false);
42
- assert.ok(result.error?.includes('not allowed'));
43
- });
44
-
45
- it('rejects "rm -rf /" command', () => {
46
- const result = manager.validateCommand('rm -rf /');
47
- assert.equal(result.valid, false);
48
- assert.ok(result.error?.includes('not allowed'));
49
- });
50
-
51
- it('rejects "bash" command', () => {
52
- const result = manager.validateCommand('bash');
53
- assert.equal(result.valid, false);
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
- it('auto-launches claude on session creation', () => {
89
- const mockLog = mock.fn();
90
- const mockPty = { onData: mock.fn(), onExit: mock.fn(), write: mock.fn(), resize: mock.fn(), kill: mock.fn() };
91
- const mockSpawn = mock.fn(() => mockPty);
92
- const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
93
-
94
- const session = manager.createSession('proj-1', 'Project One', 80, 24);
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'); // claude auto-launches on creation
84
+ assert.equal(session.state, 'running');
99
85
  assert.ok(session.id.startsWith('term_'));
100
- // claude was spawned automatically with project context
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 (no uniqueness constraint)', () => {
110
- const mockLog = mock.fn();
111
- const mockSpawn = mock.fn(() => ({ onData: mock.fn(), onExit: mock.fn(), write: mock.fn(), resize: mock.fn(), kill: mock.fn() }));
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.equal(mockSpawn.mock.calls.length, 2); // auto-launch for each session
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 mockLog = mock.fn();
122
- const mockSpawn = mock.fn(() => ({ onData: mock.fn(), onExit: mock.fn(), write: mock.fn(), resize: mock.fn(), kill: mock.fn() }));
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('getSession returns undefined for unknown session id', () => {
131
- const mockLog = mock.fn();
132
- const mockSpawn = mock.fn();
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('destroyAll removes all sessions', () => {
160
- const mockLog = mock.fn();
161
- const mockPty1 = {
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
- const mockSpawn = mock.fn(() => ptys[idx++]);
178
- const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
179
-
180
- const s1 = manager.createSession('proj-1', 'Project One', 80, 24);
181
- const s2 = manager.createSession('proj-2', 'Project Two', 80, 24);
182
- // PTYs are already running via auto-launch
183
-
184
- manager.destroyAll();
185
- assert.equal(manager.getSession(s1.id), undefined);
186
- assert.equal(manager.getSession(s2.id), undefined);
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 stored dimensions and forwards to running PTY', () => {
190
- const mockLog = mock.fn();
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
- const session = manager.createSession('proj-1', 'Project', 80, 24);
289
- // Session is already running via auto-launch send input directly
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
- it('returns to idle state and shows prompt when command exits', () => {
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
- it('prevents arbitrary shell commands even after a valid command exits', () => {
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
- let exitHandler: (info: { exitCode: number; signal?: number }) => void = () => {};
330
- const mockPty = {
331
- onData: mock.fn(),
332
- onExit: mock.fn((handler: (info: { exitCode: number; signal?: number }) => void) => {
333
- exitHandler = handler;
334
- }),
335
- write: mock.fn(),
336
- resize: mock.fn(),
337
- kill: mock.fn(),
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
- // Type "rm" then Ctrl+C
411
- manager.writeToSession(session.id, 'rm');
412
- manager.writeToSession(session.id, '\x03');
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('handles spawn failure gracefully', () => {
182
+ it('ensureRunning returns false for unknown session', async () => {
183
+ const mockSpawn = mock.fn();
450
184
  const mockLog = mock.fn();
451
- const mockSpawn = mock.fn(() => { throw new Error('spawn failed'); });
452
- const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
453
- const output: string[] = [];
454
-
455
- const session = manager.createSession('proj-1', 'Project', 80, 24);
456
- manager.addListener(session.id, (data) => output.push(data));
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
- // Should show error and prompt
465
- const fullOutput = output.join('');
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 (dec_030 — session persistence) ---
198
+ // --- Output buffer ---
472
199
 
473
- describe('PTY output buffering (dec_030)', () => {
474
- it('buffers welcome message on session creation', () => {
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 mockPty = {
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(() => mockPty);
498
- const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
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 manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
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 in idle mode', () => {
519
- const mockLog = mock.fn();
520
- let exitHandler: (info: { exitCode: number }) => void = () => {};
521
- const mockPty = {
522
- onData: mock.fn(),
523
- onExit: mock.fn((cb: Function) => { exitHandler = cb as any; }),
524
- write: mock.fn(), resize: mock.fn(), kill: mock.fn(),
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(() => mockPty);
527
- const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
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
- // Typing in idle mode echoes characters to listeners
537
- manager.writeToSession(session.id, 'c');
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], 'c');
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 mockPty = { onData: mock.fn(), onExit: mock.fn(), write: mock.fn(), resize: mock.fn(), kill: mock.fn() };
545
- const mockSpawn = mock.fn(() => mockPty);
546
- const manager = new PtySessionManager({ spawn: mockSpawn as any, log: mockLog, projectRoot: '/test' });
547
-
548
- const session = manager.createSession('proj-1', 'Project', 80, 24);
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
- manager.writeToSession(session.id, 'x'); // forwarded to running PTY, no echo
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', () => {