code-coordinator 0.5.46__py3-none-any.whl

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 (295) hide show
  1. code_coordinator-0.5.46.dist-info/METADATA +625 -0
  2. code_coordinator-0.5.46.dist-info/RECORD +295 -0
  3. code_coordinator-0.5.46.dist-info/WHEEL +5 -0
  4. code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
  5. code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
  6. code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
  7. coord/__init__.py +176 -0
  8. coord/_board_mapping.py +229 -0
  9. coord/acceptance.py +468 -0
  10. coord/acceptance_drivers.py +632 -0
  11. coord/agent.py +7517 -0
  12. coord/agent_app.py +1555 -0
  13. coord/agent_update.py +417 -0
  14. coord/agents/opencode/.gitignore +13 -0
  15. coord/agents/opencode/agents/work.md +129 -0
  16. coord/agents/opencode/routing.jsonc +49 -0
  17. coord/audit.py +301 -0
  18. coord/auto_loop.py +1440 -0
  19. coord/board_bool_guard.py +72 -0
  20. coord/board_service.py +141 -0
  21. coord/board_wire.py +309 -0
  22. coord/brain.py +581 -0
  23. coord/branch_model.py +214 -0
  24. coord/cargo_cache.py +258 -0
  25. coord/ci_github.py +386 -0
  26. coord/ci_store.py +560 -0
  27. coord/claim.py +353 -0
  28. coord/cli.py +454 -0
  29. coord/client.py +610 -0
  30. coord/commands/__init__.py +1 -0
  31. coord/commands/_common.py +329 -0
  32. coord/commands/acceptance.py +916 -0
  33. coord/commands/agent_ops.py +1339 -0
  34. coord/commands/audit.py +131 -0
  35. coord/commands/chat.py +320 -0
  36. coord/commands/dispatch.py +1780 -0
  37. coord/commands/dispatch_workers.py +4894 -0
  38. coord/commands/drive.py +616 -0
  39. coord/commands/drive_queue.py +1203 -0
  40. coord/commands/gate_a.py +217 -0
  41. coord/commands/gates.py +89 -0
  42. coord/commands/issues.py +681 -0
  43. coord/commands/lifecycle.py +513 -0
  44. coord/commands/merge.py +1900 -0
  45. coord/commands/milestone.py +2081 -0
  46. coord/commands/plan_followup.py +1243 -0
  47. coord/commands/plans.py +156 -0
  48. coord/commands/release.py +2232 -0
  49. coord/commands/report.py +341 -0
  50. coord/commands/review.py +1523 -0
  51. coord/commands/scorecard.py +252 -0
  52. coord/commands/sessions.py +1930 -0
  53. coord/commands/setup.py +576 -0
  54. coord/commands/status.py +2089 -0
  55. coord/commands/terminal.py +385 -0
  56. coord/commands/test_gate.py +775 -0
  57. coord/commands/tui.py +288 -0
  58. coord/comments.py +718 -0
  59. coord/config.py +3032 -0
  60. coord/conflict_fix.py +633 -0
  61. coord/dao.py +483 -0
  62. coord/dashboard/__init__.py +0 -0
  63. coord/dashboard/fixture.py +376 -0
  64. coord/dashboard/index.html +658 -0
  65. coord/dashboard/server.py +1894 -0
  66. coord/dashboard/terminal.py +382 -0
  67. coord/dashboard/webapp/.gitignore +9 -0
  68. coord/dashboard/webapp/components.json +17 -0
  69. coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
  70. coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
  71. coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
  72. coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
  73. coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
  74. coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
  75. coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
  76. coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
  77. coord/dashboard/webapp/dist/icons/icon.svg +5 -0
  78. coord/dashboard/webapp/dist/index.html +38 -0
  79. coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
  80. coord/dashboard/webapp/dist/sw.js +1 -0
  81. coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
  82. coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
  83. coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
  84. coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
  85. coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
  86. coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
  87. coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
  88. coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
  89. coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
  90. coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
  91. coord/dashboard/webapp/eslint.config.js +20 -0
  92. coord/dashboard/webapp/index.html +37 -0
  93. coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
  94. coord/dashboard/webapp/package-lock.json +10584 -0
  95. coord/dashboard/webapp/package.json +63 -0
  96. coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
  97. coord/dashboard/webapp/playwright.config.ts +93 -0
  98. coord/dashboard/webapp/postcss.config.js +6 -0
  99. coord/dashboard/webapp/public/icons/icon-192.png +0 -0
  100. coord/dashboard/webapp/public/icons/icon-512.png +0 -0
  101. coord/dashboard/webapp/public/icons/icon.svg +5 -0
  102. coord/dashboard/webapp/src/App.tsx +140 -0
  103. coord/dashboard/webapp/src/api/client.ts +199 -0
  104. coord/dashboard/webapp/src/api/generated.ts +176 -0
  105. coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
  106. coord/dashboard/webapp/src/components/Detail.tsx +800 -0
  107. coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
  108. coord/dashboard/webapp/src/components/Home.tsx +435 -0
  109. coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
  110. coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
  111. coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
  112. coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
  113. coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
  114. coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
  115. coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
  116. coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
  117. coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
  118. coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
  119. coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
  120. coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
  121. coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
  122. coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
  123. coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
  124. coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
  125. coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
  126. coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
  127. coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
  128. coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
  129. coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
  130. coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
  131. coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
  132. coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
  133. coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
  134. coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
  135. coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
  136. coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
  137. coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
  138. coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
  139. coord/dashboard/webapp/src/index.css +210 -0
  140. coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
  141. coord/dashboard/webapp/src/lib/utils.ts +6 -0
  142. coord/dashboard/webapp/src/main.tsx +46 -0
  143. coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
  144. coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
  145. coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
  146. coord/dashboard/webapp/src/realtime/connection.ts +227 -0
  147. coord/dashboard/webapp/src/realtime/events.ts +100 -0
  148. coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
  149. coord/dashboard/webapp/src/routes/paths.ts +92 -0
  150. coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
  151. coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
  152. coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
  153. coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
  154. coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
  155. coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
  156. coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
  157. coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
  158. coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
  159. coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
  160. coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
  161. coord/dashboard/webapp/src/shell/railItems.ts +105 -0
  162. coord/dashboard/webapp/src/shell/shellState.ts +174 -0
  163. coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
  164. coord/dashboard/webapp/src/test-setup.ts +41 -0
  165. coord/dashboard/webapp/src/vite-env.d.ts +2 -0
  166. coord/dashboard/webapp/tailwind.config.js +140 -0
  167. coord/dashboard/webapp/tsconfig.json +25 -0
  168. coord/dashboard/webapp/tsconfig.node.json +11 -0
  169. coord/dashboard/webapp/vite.config.ts +71 -0
  170. coord/db.py +1076 -0
  171. coord/dead_end.py +332 -0
  172. coord/deploy/README.md +33 -0
  173. coord/deploy/coord-agent.service +89 -0
  174. coord/deploy/coord-db-backup.service +60 -0
  175. coord/deploy/coord-db-backup.sh +74 -0
  176. coord/deploy/coord-db-backup.timer +18 -0
  177. coord/deploy/coord-drive-queue.service +117 -0
  178. coord/deploy/coord-drive-queue.timer +39 -0
  179. coord/deploy/coord-notify.service +48 -0
  180. coord/deploy/coord-notify.timer +24 -0
  181. coord/deploy/coord-release-propagate.service +83 -0
  182. coord/deploy/coord-release-propagate.timer +38 -0
  183. coord/deploy/coord-release-window.service +119 -0
  184. coord/deploy/coord-release-window.timer +36 -0
  185. coord/deploy/coord-serve.service +82 -0
  186. coord/deploy/coord-web-dist-build.service +43 -0
  187. coord/deploy/coord-web-dist-build.timer +36 -0
  188. coord/deploy/coord-web.service +125 -0
  189. coord/deploy_manifest.py +80 -0
  190. coord/deploy_units.py +384 -0
  191. coord/deps.py +115 -0
  192. coord/diagnose.py +1623 -0
  193. coord/dispatch.py +1009 -0
  194. coord/dist_name.py +123 -0
  195. coord/drive.py +3101 -0
  196. coord/drive_queue.py +2298 -0
  197. coord/drive_state.py +870 -0
  198. coord/events.py +381 -0
  199. coord/failure_class.py +914 -0
  200. coord/filelock.py +168 -0
  201. coord/fleet_config_health.py +300 -0
  202. coord/freshness.py +206 -0
  203. coord/gate_a.py +469 -0
  204. coord/gate_b.py +411 -0
  205. coord/gate_snapshot.py +385 -0
  206. coord/gates.py +582 -0
  207. coord/github_ops.py +1954 -0
  208. coord/goal.py +125 -0
  209. coord/graph_health.py +348 -0
  210. coord/health/__init__.py +69 -0
  211. coord/health/aggregate.py +129 -0
  212. coord/health/checks/__init__.py +13 -0
  213. coord/health/checks/agent_install.py +280 -0
  214. coord/health/checks/cargo_targets.py +171 -0
  215. coord/health/checks/claude_binary.py +65 -0
  216. coord/health/checks/deploy_lane_facts.py +458 -0
  217. coord/health/checks/disk.py +99 -0
  218. coord/health/checks/fleet_board.py +89 -0
  219. coord/health/checks/fleet_deploy_lanes.py +469 -0
  220. coord/health/checks/fleet_phantom.py +69 -0
  221. coord/health/checks/fleet_unit_drift.py +151 -0
  222. coord/health/checks/graph.py +192 -0
  223. coord/health/checks/plan_usage.py +88 -0
  224. coord/health/checks/repo_state.py +161 -0
  225. coord/health/checks/spawned_coord.py +465 -0
  226. coord/health/checks/timer_active.py +254 -0
  227. coord/health/checks/toolchain.py +547 -0
  228. coord/health/checks/unit_drift.py +648 -0
  229. coord/health/checks/unit_enablement.py +171 -0
  230. coord/health/checks/worktrees.py +96 -0
  231. coord/health/cli.py +121 -0
  232. coord/health/context.py +106 -0
  233. coord/health/fleet_snapshot.py +477 -0
  234. coord/health/models.py +250 -0
  235. coord/health/pypi.py +231 -0
  236. coord/health/registry.py +240 -0
  237. coord/health/render.py +82 -0
  238. coord/health/units.py +60 -0
  239. coord/hooks.py +106 -0
  240. coord/housekeeping.py +204 -0
  241. coord/interactive.py +4286 -0
  242. coord/issue_store.py +1496 -0
  243. coord/liveness_auditor.py +293 -0
  244. coord/machine_pause.py +755 -0
  245. coord/merge_queue.py +4681 -0
  246. coord/milestone_chat.py +600 -0
  247. coord/milestone_dispatch.py +943 -0
  248. coord/milestone_gate.py +709 -0
  249. coord/milestone_order.py +840 -0
  250. coord/mock_author.py +334 -0
  251. coord/models.py +891 -0
  252. coord/network.py +269 -0
  253. coord/new_issue_chat.py +229 -0
  254. coord/notify.py +3226 -0
  255. coord/openapi.py +404 -0
  256. coord/overlap_fence.py +133 -0
  257. coord/parentage.py +200 -0
  258. coord/parentage_github.py +58 -0
  259. coord/pipeline.py +481 -0
  260. coord/plan_parser.py +266 -0
  261. coord/plans.py +543 -0
  262. coord/platform_paths.py +43 -0
  263. coord/pr_body_lint.py +67 -0
  264. coord/prereqs.py +533 -0
  265. coord/progress.py +425 -0
  266. coord/providers/__init__.py +683 -0
  267. coord/providers/base.py +218 -0
  268. coord/providers/claude.py +284 -0
  269. coord/providers/claude_pty.py +610 -0
  270. coord/providers/opencode.py +896 -0
  271. coord/reconcile.py +2233 -0
  272. coord/refine_chat.py +485 -0
  273. coord/release_cordon.py +525 -0
  274. coord/release_propagate.py +1176 -0
  275. coord/release_verify.py +777 -0
  276. coord/release_window.py +322 -0
  277. coord/reports.py +1643 -0
  278. coord/revalidate.py +1101 -0
  279. coord/review.py +3317 -0
  280. coord/scorecard.py +484 -0
  281. coord/serve_app.py +7192 -0
  282. coord/skills/update-issue/SKILL.md +93 -0
  283. coord/smoke.py +1030 -0
  284. coord/split_work.py +210 -0
  285. coord/stage_projection.py +650 -0
  286. coord/state.py +5720 -0
  287. coord/test_author.py +1064 -0
  288. coord/test_chat.py +352 -0
  289. coord/test_orchestrator.py +494 -0
  290. coord/test_report.py +178 -0
  291. coord/tui_release.py +271 -0
  292. coord/usage.py +753 -0
  293. coord/usage_limits.py +358 -0
  294. coord/usage_rollup.py +709 -0
  295. coord/worker_events.py +954 -0
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Component tests for PipelineCard and the Home-screen filter functions.
3
+ *
4
+ * These use vitest + @testing-library/react. Run with `npm test` (or
5
+ * `npx vitest run`) after `npm install`.
6
+ */
7
+ import { describe, it, expect, vi } from 'vitest'
8
+ import { render, screen } from '@testing-library/react'
9
+ import userEvent from '@testing-library/user-event'
10
+ import { PipelineCard } from '@/components/PipelineCard'
11
+ import { type PipelineView } from '@/api/client'
12
+
13
+ // ── Test helpers ──────────────────────────────────────────────────────────────
14
+
15
+ function makeView(overrides: Partial<PipelineView> = {}): PipelineView {
16
+ return {
17
+ assignment_id: 'work-1',
18
+ issue_number: 42,
19
+ issue_title: 'Fix the thing',
20
+ repo_name: 'myrepo',
21
+ machine_name: 'laptop',
22
+ current_stage: 'coding',
23
+ stages: [
24
+ { name: 'coding', status: 'active', is_current: true },
25
+ { name: 'review', status: 'waiting', is_current: false },
26
+ { name: 'smoke', status: 'waiting', is_current: false },
27
+ { name: 'merge', status: 'waiting', is_current: false },
28
+ ],
29
+ available_gates: [],
30
+ progress_pct: 10,
31
+ review_findings_pending: false,
32
+ review_verdict: null,
33
+ // #1456: null = the reviewer's own verdict stands (the normal case).
34
+ review_verdict_original: null,
35
+ review_verdict_override_reason: null,
36
+ review_findings_body: null,
37
+ test_verdict: null,
38
+ needs_attention: false,
39
+ needs_attention_reason: null,
40
+ needs_attention_detail: null,
41
+ finished_at: null,
42
+ ...overrides,
43
+ }
44
+ }
45
+
46
+ // ── PipelineCard rendering ────────────────────────────────────────────────────
47
+
48
+ describe('PipelineCard', () => {
49
+ it('renders issue title, repo, issue number, and machine', () => {
50
+ render(<PipelineCard view={makeView()} onClick={() => undefined} />)
51
+
52
+ // The card is a button — scope assertions to it to avoid ambiguity
53
+ const card = screen.getByRole('button')
54
+ expect(card).toHaveTextContent('Fix the thing')
55
+ expect(card).toHaveTextContent('#42')
56
+ expect(card).toHaveTextContent('myrepo')
57
+ expect(card).toHaveTextContent('laptop')
58
+ })
59
+
60
+ it('renders stage chips with display names (coding→work, smoke→test)', () => {
61
+ render(<PipelineCard view={makeView()} onClick={() => undefined} />)
62
+
63
+ expect(screen.getByText('work')).toBeInTheDocument()
64
+ expect(screen.getByText('review')).toBeInTheDocument()
65
+ expect(screen.getByText('test')).toBeInTheDocument()
66
+ expect(screen.getByText('merge')).toBeInTheDocument()
67
+ })
68
+
69
+ it('shows "running" status badge when current_stage is coding', () => {
70
+ render(<PipelineCard view={makeView({ current_stage: 'coding' })} onClick={() => undefined} />)
71
+ expect(screen.getByText('running')).toBeInTheDocument()
72
+ })
73
+
74
+ it('shows "failed" status badge for failed stage', () => {
75
+ const view = makeView({
76
+ current_stage: 'failed',
77
+ stages: [
78
+ { name: 'coding', status: 'active', is_current: true },
79
+ { name: 'review', status: 'waiting', is_current: false },
80
+ { name: 'smoke', status: 'waiting', is_current: false },
81
+ { name: 'merge', status: 'waiting', is_current: false },
82
+ ],
83
+ })
84
+ render(<PipelineCard view={view} onClick={() => undefined} />)
85
+ expect(screen.getByText('failed')).toBeInTheDocument()
86
+ })
87
+
88
+ it('shows "mergeable" badge when current_stage is merge_ready', () => {
89
+ const view = makeView({
90
+ current_stage: 'merge_ready',
91
+ stages: [
92
+ { name: 'coding', status: 'completed', is_current: false },
93
+ { name: 'review', status: 'completed', is_current: false },
94
+ { name: 'smoke', status: 'completed', is_current: false },
95
+ { name: 'merge', status: 'active', is_current: true },
96
+ ],
97
+ available_gates: [{ action: 'merge', label: 'Merge', endpoint: '/api/pipeline/action' }],
98
+ })
99
+ render(<PipelineCard view={view} onClick={() => undefined} />)
100
+ expect(screen.getByText('mergeable')).toBeInTheDocument()
101
+ })
102
+
103
+ it('shows review findings pending warning', () => {
104
+ render(
105
+ <PipelineCard
106
+ view={makeView({ review_findings_pending: true })}
107
+ onClick={() => undefined}
108
+ />,
109
+ )
110
+ expect(screen.getByText(/Review findings not yet posted/)).toBeInTheDocument()
111
+ })
112
+
113
+ it('calls onClick when the card is pressed', async () => {
114
+ const onClick = vi.fn()
115
+ render(<PipelineCard view={makeView()} onClick={onClick} />)
116
+ await userEvent.click(screen.getByRole('button'))
117
+ expect(onClick).toHaveBeenCalledOnce()
118
+ })
119
+ })
120
+
121
+ // ── Filter logic (unit-tested here since filterFns are co-located in Home) ────
122
+
123
+ describe('filter helpers', () => {
124
+ it('isActive excludes merged items', () => {
125
+ const active = makeView({ current_stage: 'coding' })
126
+ const merged = makeView({ current_stage: 'merged' })
127
+ // Re-implement the predicate inline to keep the test independent of
128
+ // Home's internal state. Tests document the contract.
129
+ const isActive = (v: PipelineView) => v.current_stage !== 'merged'
130
+ expect(isActive(active)).toBe(true)
131
+ expect(isActive(merged)).toBe(false)
132
+ })
133
+
134
+ it('needsMe returns true when available_gates is non-empty', () => {
135
+ const noAction = makeView({ available_gates: [] })
136
+ const hasAction = makeView({
137
+ available_gates: [{ action: 'merge', label: 'Merge', endpoint: '/api/pipeline/action' }],
138
+ })
139
+ const needsMe = (v: PipelineView) => v.available_gates.length > 0
140
+ expect(needsMe(noAction)).toBe(false)
141
+ expect(needsMe(hasAction)).toBe(true)
142
+ })
143
+ })
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Component tests for SessionCard.
3
+ */
4
+ import { describe, it, expect, vi } from 'vitest'
5
+ import { render, screen } from '@testing-library/react'
6
+ import userEvent from '@testing-library/user-event'
7
+ import { SessionCard } from '@/components/SessionCard'
8
+ import { type SessionInfo } from '@/api/client'
9
+
10
+ // ── Test helpers ──────────────────────────────────────────────────────────────
11
+
12
+ function makeSession(overrides: Partial<SessionInfo> = {}): SessionInfo {
13
+ return {
14
+ session_id: 'work-1',
15
+ session_name: 'coord-work-1',
16
+ machine: 'laptop',
17
+ host: 'laptop.tailnet.ts.net',
18
+ repo: 'myrepo',
19
+ issue: 42,
20
+ issue_title: 'Fix the thing',
21
+ stage: 'work',
22
+ status: 'running',
23
+ attached: false,
24
+ pane_dead: false,
25
+ ...overrides,
26
+ }
27
+ }
28
+
29
+ describe('SessionCard', () => {
30
+ it('renders issue title, repo, issue number, and machine', () => {
31
+ render(<SessionCard session={makeSession()} onClick={() => undefined} />)
32
+
33
+ const card = screen.getByRole('button')
34
+ expect(card).toHaveTextContent('Fix the thing')
35
+ expect(card).toHaveTextContent('#42')
36
+ expect(card).toHaveTextContent('myrepo')
37
+ expect(card).toHaveTextContent('laptop')
38
+ })
39
+
40
+ it('falls back to the tmux session name when no assignment is tracked', () => {
41
+ render(
42
+ <SessionCard
43
+ session={makeSession({ issue_title: null, repo: null, issue: null, machine: null })}
44
+ onClick={() => undefined}
45
+ />,
46
+ )
47
+ expect(screen.getByRole('button')).toHaveTextContent('coord-work-1')
48
+ })
49
+
50
+ it('shows "live" badge for an untouched running session', () => {
51
+ render(<SessionCard session={makeSession()} onClick={() => undefined} />)
52
+ expect(screen.getByText('live')).toBeInTheDocument()
53
+ })
54
+
55
+ it('shows "attached" badge when a client is already attached', () => {
56
+ render(<SessionCard session={makeSession({ attached: true })} onClick={() => undefined} />)
57
+ expect(screen.getByText('attached')).toBeInTheDocument()
58
+ })
59
+
60
+ it('shows "ended" badge when the pane is dead', () => {
61
+ render(<SessionCard session={makeSession({ pane_dead: true })} onClick={() => undefined} />)
62
+ expect(screen.getByText('ended')).toBeInTheDocument()
63
+ })
64
+
65
+ it('calls onClick when the card is pressed', async () => {
66
+ const onClick = vi.fn()
67
+ render(<SessionCard session={makeSession()} onClick={onClick} />)
68
+ await userEvent.click(screen.getByRole('button'))
69
+ expect(onClick).toHaveBeenCalledOnce()
70
+ })
71
+
72
+ // ── Assignment-type chip (#1276) ──────────────────────────────────────────
73
+
74
+ it('shows a "work" chip for a work session', () => {
75
+ render(<SessionCard session={makeSession({ stage: 'work' })} onClick={() => undefined} />)
76
+ expect(screen.getByText('work')).toBeInTheDocument()
77
+ })
78
+
79
+ it('shows a "test" chip for a smoke session', () => {
80
+ render(<SessionCard session={makeSession({ stage: 'smoke' })} onClick={() => undefined} />)
81
+ expect(screen.getByText('test')).toBeInTheDocument()
82
+ })
83
+
84
+ it('shows a "review" chip for a review session', () => {
85
+ render(<SessionCard session={makeSession({ stage: 'review' })} onClick={() => undefined} />)
86
+ expect(screen.getByText('review')).toBeInTheDocument()
87
+ })
88
+
89
+ it('shows a "merge" chip for a real merge session (assignment.type is "conflict-fix", never "merge")', () => {
90
+ render(<SessionCard session={makeSession({ stage: 'conflict-fix' })} onClick={() => undefined} />)
91
+ expect(screen.getByText('merge')).toBeInTheDocument()
92
+ })
93
+
94
+ it('falls back to the raw assignment type when it has no ASSIGNMENT_TYPE_LABEL entry', () => {
95
+ render(<SessionCard session={makeSession({ stage: 'fix' })} onClick={() => undefined} />)
96
+ expect(screen.getByText('fix')).toBeInTheDocument()
97
+ })
98
+
99
+ it('renders no stage chip when stage is null', () => {
100
+ render(<SessionCard session={makeSession({ stage: null })} onClick={() => undefined} />)
101
+ // Only the status badge ("live") should render — no orphan chip for a
102
+ // session without a resolvable assignment.
103
+ expect(screen.queryByText('work')).not.toBeInTheDocument()
104
+ expect(screen.getByText('live')).toBeInTheDocument()
105
+ })
106
+ })