@jcjeon/integration-cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/.gitignore +23 -0
  2. package/.npmignore +21 -0
  3. package/.prettierignore +6 -0
  4. package/.prettierrc +26 -0
  5. package/AGENTS.md +10 -0
  6. package/CLAUDE.md +10 -0
  7. package/README.md +384 -0
  8. package/apps/server/README.md +294 -0
  9. package/apps/server/eslint.config.mjs +20 -0
  10. package/apps/server/nest-cli.json +8 -0
  11. package/apps/server/package.json +89 -0
  12. package/apps/server/scripts/postinstall.js +53 -0
  13. package/apps/server/src/__mocks__/glob.js +6 -0
  14. package/apps/server/src/__mocks__/uuid.js +5 -0
  15. package/apps/server/src/app.controller.spec.ts +24 -0
  16. package/apps/server/src/app.controller.ts +13 -0
  17. package/apps/server/src/app.module.ts +18 -0
  18. package/apps/server/src/app.service.ts +8 -0
  19. package/apps/server/src/common/ji-paths.ts +41 -0
  20. package/apps/server/src/database/database.module.ts +27 -0
  21. package/apps/server/src/database/entities/agent-changelog.entity.ts +39 -0
  22. package/apps/server/src/database/entities/agent-session.entity.ts +29 -0
  23. package/apps/server/src/database/entities/conversation.entity.ts +41 -0
  24. package/apps/server/src/database/entities/session.entity.ts +16 -0
  25. package/apps/server/src/database/entities/task-agent-run.entity.ts +40 -0
  26. package/apps/server/src/database/entities/task-agent.entity.ts +42 -0
  27. package/apps/server/src/database/entities/task-requirement.entity.ts +27 -0
  28. package/apps/server/src/database/entities/task-run.entity.ts +41 -0
  29. package/apps/server/src/database/entities/task.entity.ts +44 -0
  30. package/apps/server/src/main.ts +65 -0
  31. package/apps/server/src/modules/agents/agent-model-settings.spec.ts +80 -0
  32. package/apps/server/src/modules/agents/agents.module.ts +11 -0
  33. package/apps/server/src/modules/agents/claude/claude-auth.manager.ts +83 -0
  34. package/apps/server/src/modules/agents/claude/claude-pty.manager.ts +380 -0
  35. package/apps/server/src/modules/agents/claude/claude.controller.ts +85 -0
  36. package/apps/server/src/modules/agents/claude/claude.gateway.ts +158 -0
  37. package/apps/server/src/modules/agents/claude/claude.module.ts +18 -0
  38. package/apps/server/src/modules/agents/claude/claude.service.ts +67 -0
  39. package/apps/server/src/modules/agents/claude/dto/create-session.dto.ts +24 -0
  40. package/apps/server/src/modules/agents/claude/dto/resize-session.dto.ts +13 -0
  41. package/apps/server/src/modules/agents/claude/dto/send-input.dto.ts +9 -0
  42. package/apps/server/src/modules/agents/claude/interfaces/claude-session.interface.ts +26 -0
  43. package/apps/server/src/modules/agents/claude/interfaces/pty-event.interface.ts +10 -0
  44. package/apps/server/src/modules/agents/claude/interfaces/stream-event.interface.ts +61 -0
  45. package/apps/server/src/modules/agents/codex/codex-auth.manager.ts +107 -0
  46. package/apps/server/src/modules/agents/codex/codex-session.manager.ts +357 -0
  47. package/apps/server/src/modules/agents/codex/codex.controller.ts +64 -0
  48. package/apps/server/src/modules/agents/codex/codex.gateway.ts +97 -0
  49. package/apps/server/src/modules/agents/codex/codex.module.ts +17 -0
  50. package/apps/server/src/modules/agents/codex/dto/configure-auth.dto.ts +7 -0
  51. package/apps/server/src/modules/agents/gemini/dto/configure-auth.dto.ts +15 -0
  52. package/apps/server/src/modules/agents/gemini/dto/create-session.dto.ts +9 -0
  53. package/apps/server/src/modules/agents/gemini/dto/send-input.dto.ts +9 -0
  54. package/apps/server/src/modules/agents/gemini/gemini-auth.manager.ts +157 -0
  55. package/apps/server/src/modules/agents/gemini/gemini-session.manager.ts +287 -0
  56. package/apps/server/src/modules/agents/gemini/gemini.controller.ts +93 -0
  57. package/apps/server/src/modules/agents/gemini/gemini.gateway.ts +149 -0
  58. package/apps/server/src/modules/agents/gemini/gemini.module.ts +17 -0
  59. package/apps/server/src/modules/agents/gemini/interfaces/gemini-session.interface.ts +18 -0
  60. package/apps/server/src/modules/agents/gemini/interfaces/stream-event.interface.ts +14 -0
  61. package/apps/server/src/modules/agents/session-termination.spec.ts +103 -0
  62. package/apps/server/src/modules/changelog/changelog.controller.ts +20 -0
  63. package/apps/server/src/modules/changelog/changelog.module.ts +14 -0
  64. package/apps/server/src/modules/changelog/changelog.service.spec.ts +531 -0
  65. package/apps/server/src/modules/changelog/changelog.service.ts +690 -0
  66. package/apps/server/src/modules/conversations/conversation.controller.spec.ts +106 -0
  67. package/apps/server/src/modules/conversations/conversation.controller.ts +60 -0
  68. package/apps/server/src/modules/conversations/conversation.module.ts +14 -0
  69. package/apps/server/src/modules/conversations/conversation.service.spec.ts +176 -0
  70. package/apps/server/src/modules/conversations/conversation.service.ts +54 -0
  71. package/apps/server/src/modules/conversations/dto/create-conversation.dto.ts +37 -0
  72. package/apps/server/src/modules/conversations/enums/conversation.enum.ts +13 -0
  73. package/apps/server/src/modules/fs/fs.controller.ts +29 -0
  74. package/apps/server/src/modules/fs/fs.module.ts +8 -0
  75. package/apps/server/src/modules/harness/dto/save-harness.dto.ts +9 -0
  76. package/apps/server/src/modules/harness/harness.controller.spec.ts +95 -0
  77. package/apps/server/src/modules/harness/harness.controller.ts +35 -0
  78. package/apps/server/src/modules/harness/harness.module.ts +11 -0
  79. package/apps/server/src/modules/harness/harness.service.spec.ts +217 -0
  80. package/apps/server/src/modules/harness/harness.service.ts +112 -0
  81. package/apps/server/src/modules/sessions/session.controller.spec.ts +68 -0
  82. package/apps/server/src/modules/sessions/session.controller.ts +43 -0
  83. package/apps/server/src/modules/sessions/session.module.ts +14 -0
  84. package/apps/server/src/modules/sessions/session.service.spec.ts +106 -0
  85. package/apps/server/src/modules/sessions/session.service.ts +35 -0
  86. package/apps/server/src/modules/tasks/dto/create-task.dto.ts +54 -0
  87. package/apps/server/src/modules/tasks/dto/execute-task.dto.ts +22 -0
  88. package/apps/server/src/modules/tasks/dto/merge-file.dto.ts +7 -0
  89. package/apps/server/src/modules/tasks/dto/rerun-task.dto.ts +14 -0
  90. package/apps/server/src/modules/tasks/dto/update-task.dto.ts +55 -0
  91. package/apps/server/src/modules/tasks/task-execution.service.ts +978 -0
  92. package/apps/server/src/modules/tasks/task.gateway.ts +140 -0
  93. package/apps/server/src/modules/tasks/tasks.controller.spec.ts +210 -0
  94. package/apps/server/src/modules/tasks/tasks.controller.ts +139 -0
  95. package/apps/server/src/modules/tasks/tasks.module.ts +30 -0
  96. package/apps/server/src/modules/tasks/tasks.service.spec.ts +552 -0
  97. package/apps/server/src/modules/tasks/tasks.service.ts +333 -0
  98. package/apps/server/test/app.e2e-spec.ts +28 -0
  99. package/apps/server/test/jest-e2e.json +9 -0
  100. package/apps/server/tsconfig.build.json +4 -0
  101. package/apps/server/tsconfig.json +13 -0
  102. package/apps/web/AGENTS.md +7 -0
  103. package/apps/web/CLAUDE.md +1 -0
  104. package/apps/web/README.md +36 -0
  105. package/apps/web/eslint.config.mjs +21 -0
  106. package/apps/web/next-env.d.ts +6 -0
  107. package/apps/web/next.config.ts +7 -0
  108. package/apps/web/package.json +49 -0
  109. package/apps/web/postcss.config.mjs +7 -0
  110. package/apps/web/public/file.svg +1 -0
  111. package/apps/web/public/globe.svg +1 -0
  112. package/apps/web/public/next.svg +1 -0
  113. package/apps/web/public/vercel.svg +1 -0
  114. package/apps/web/public/window.svg +1 -0
  115. package/apps/web/src/app/claude/page.tsx +5 -0
  116. package/apps/web/src/app/codex/page.tsx +126 -0
  117. package/apps/web/src/app/favicon.ico +0 -0
  118. package/apps/web/src/app/gemini/page.tsx +130 -0
  119. package/apps/web/src/app/globals.css +149 -0
  120. package/apps/web/src/app/layout.tsx +40 -0
  121. package/apps/web/src/app/login/page.tsx +67 -0
  122. package/apps/web/src/app/page.tsx +497 -0
  123. package/apps/web/src/app/task/[id]/page.tsx +11 -0
  124. package/apps/web/src/app/test/page.tsx +298 -0
  125. package/apps/web/src/components/ui/Modal.tsx +78 -0
  126. package/apps/web/src/components/ui/WorkingDirPicker.tsx +195 -0
  127. package/apps/web/src/components/ui/__tests__/Modal.test.tsx +68 -0
  128. package/apps/web/src/features/auth/api/__tests__/auth.api.test.ts +83 -0
  129. package/apps/web/src/features/auth/api/auth.api.ts +81 -0
  130. package/apps/web/src/features/auth/hooks/__tests__/useClaudeAuth.test.ts +166 -0
  131. package/apps/web/src/features/auth/hooks/__tests__/useCodexAuth.test.ts +127 -0
  132. package/apps/web/src/features/auth/hooks/__tests__/useGeminiAuth.test.ts +120 -0
  133. package/apps/web/src/features/auth/hooks/useClaudeAuth.ts +88 -0
  134. package/apps/web/src/features/auth/hooks/useCodexAuth.ts +149 -0
  135. package/apps/web/src/features/auth/hooks/useGeminiAuth.ts +125 -0
  136. package/apps/web/src/features/auth/ui/CodexLoginPanel.tsx +302 -0
  137. package/apps/web/src/features/auth/ui/GeminiLoginPanel.tsx +316 -0
  138. package/apps/web/src/features/auth/ui/LoginForm.tsx +190 -0
  139. package/apps/web/src/features/auth/ui/LoginPanel.tsx +114 -0
  140. package/apps/web/src/features/auth/ui/__tests__/LoginPanel.test.tsx +105 -0
  141. package/apps/web/src/features/chat/api/__tests__/sessions.api.test.ts +187 -0
  142. package/apps/web/src/features/chat/api/sessions.api.ts +161 -0
  143. package/apps/web/src/features/chat/container/ClaudePageContainer.tsx +152 -0
  144. package/apps/web/src/features/chat/hooks/__tests__/useCodexSessions.test.ts +131 -0
  145. package/apps/web/src/features/chat/hooks/__tests__/useGeminiSessions.test.ts +130 -0
  146. package/apps/web/src/features/chat/hooks/useAgentModelSettings.ts +54 -0
  147. package/apps/web/src/features/chat/hooks/useClaudeSessions.ts +323 -0
  148. package/apps/web/src/features/chat/hooks/useCodexSessions.ts +275 -0
  149. package/apps/web/src/features/chat/hooks/useGeminiSessions.ts +255 -0
  150. package/apps/web/src/features/chat/hooks/useSessionCommand.ts +66 -0
  151. package/apps/web/src/features/chat/hooks/useSessionRename.ts +61 -0
  152. package/apps/web/src/features/chat/hooks/useSessionWorkingDirectories.ts +34 -0
  153. package/apps/web/src/features/chat/hooks/useUnifiedSessions.ts +156 -0
  154. package/apps/web/src/features/chat/lib/agentModelOptions.ts +72 -0
  155. package/apps/web/src/features/chat/ui/AgentModelPicker.tsx +134 -0
  156. package/apps/web/src/features/chat/ui/AgentSelectModal.tsx +236 -0
  157. package/apps/web/src/features/chat/ui/ChatInput.tsx +162 -0
  158. package/apps/web/src/features/chat/ui/ChatMessage.tsx +204 -0
  159. package/apps/web/src/features/chat/ui/ChatWorkspace.tsx +207 -0
  160. package/apps/web/src/features/chat/ui/CheckingSkeleton.tsx +44 -0
  161. package/apps/web/src/features/chat/ui/ClaudeLoginView.tsx +44 -0
  162. package/apps/web/src/features/chat/ui/PermissionCard.tsx +37 -0
  163. package/apps/web/src/features/chat/ui/SessionSidebar.tsx +280 -0
  164. package/apps/web/src/features/chat/ui/__tests__/AgentSelectModal.test.tsx +58 -0
  165. package/apps/web/src/features/chat/ui/__tests__/ChatInput.test.tsx +134 -0
  166. package/apps/web/src/features/chat/ui/__tests__/ChatMessage.test.tsx +106 -0
  167. package/apps/web/src/features/chat/ui/__tests__/ChatWorkspace.test.tsx +66 -0
  168. package/apps/web/src/features/diff/ui/DiffFileRow.tsx +73 -0
  169. package/apps/web/src/features/diff/ui/DiffHunk.tsx +61 -0
  170. package/apps/web/src/features/diff/ui/FileChangeBadge.tsx +23 -0
  171. package/apps/web/src/features/diff/ui/__tests__/DiffFileRow.test.tsx +40 -0
  172. package/apps/web/src/features/diff/ui/__tests__/DiffHunk.test.tsx +24 -0
  173. package/apps/web/src/features/diff/ui/__tests__/FileChangeBadge.test.tsx +16 -0
  174. package/apps/web/src/features/fs/api/fs.api.ts +14 -0
  175. package/apps/web/src/features/fs/hooks/useDirBrowser.ts +50 -0
  176. package/apps/web/src/features/harness/api/__tests__/harness.api.test.ts +73 -0
  177. package/apps/web/src/features/harness/api/harness.api.ts +46 -0
  178. package/apps/web/src/features/harness/hooks/__tests__/useHarness.test.ts +65 -0
  179. package/apps/web/src/features/harness/hooks/useHarness.ts +66 -0
  180. package/apps/web/src/features/harness/ui/HarnessModal.tsx +171 -0
  181. package/apps/web/src/features/harness/ui/__tests__/HarnessModal.test.tsx +46 -0
  182. package/apps/web/src/features/status/ui/AgentStatusModal.tsx +267 -0
  183. package/apps/web/src/features/status/ui/__tests__/AgentStatusModal.test.tsx +71 -0
  184. package/apps/web/src/features/tasks/api/__tests__/changelog.api.test.ts +89 -0
  185. package/apps/web/src/features/tasks/api/__tests__/tasks.api.test.ts +282 -0
  186. package/apps/web/src/features/tasks/api/changelog.api.ts +52 -0
  187. package/apps/web/src/features/tasks/api/tasks.api.ts +175 -0
  188. package/apps/web/src/features/tasks/container/TaskDetailPageContainer.tsx +69 -0
  189. package/apps/web/src/features/tasks/hooks/__tests__/useChangelogCodeCopy.test.ts +48 -0
  190. package/apps/web/src/features/tasks/hooks/__tests__/useTaskChangelog.test.ts +48 -0
  191. package/apps/web/src/features/tasks/hooks/__tests__/useTaskCreate.test.ts +217 -0
  192. package/apps/web/src/features/tasks/hooks/__tests__/useTaskEdit.test.ts +152 -0
  193. package/apps/web/src/features/tasks/hooks/__tests__/useTaskExecution.test.ts +143 -0
  194. package/apps/web/src/features/tasks/hooks/__tests__/useTaskList.test.ts +168 -0
  195. package/apps/web/src/features/tasks/hooks/__tests__/useTaskNotification.test.ts +125 -0
  196. package/apps/web/src/features/tasks/hooks/__tests__/useTaskRuns.test.ts +51 -0
  197. package/apps/web/src/features/tasks/hooks/useChangelogCodeCopy.ts +52 -0
  198. package/apps/web/src/features/tasks/hooks/useCopyToClipboard.ts +47 -0
  199. package/apps/web/src/features/tasks/hooks/useTaskChangelog.ts +32 -0
  200. package/apps/web/src/features/tasks/hooks/useTaskCreate.ts +137 -0
  201. package/apps/web/src/features/tasks/hooks/useTaskDetail.ts +217 -0
  202. package/apps/web/src/features/tasks/hooks/useTaskEdit.ts +130 -0
  203. package/apps/web/src/features/tasks/hooks/useTaskExecution.ts +137 -0
  204. package/apps/web/src/features/tasks/hooks/useTaskList.ts +159 -0
  205. package/apps/web/src/features/tasks/hooks/useTaskNotification.ts +80 -0
  206. package/apps/web/src/features/tasks/hooks/useTaskRuns.ts +32 -0
  207. package/apps/web/src/features/tasks/ui/AgentOutputPanel.tsx +203 -0
  208. package/apps/web/src/features/tasks/ui/AgentRoleSelect.tsx +97 -0
  209. package/apps/web/src/features/tasks/ui/ChangelogPanel.tsx +321 -0
  210. package/apps/web/src/features/tasks/ui/RunHistoryPanel.tsx +193 -0
  211. package/apps/web/src/features/tasks/ui/TaskCreateModal.tsx +205 -0
  212. package/apps/web/src/features/tasks/ui/TaskDetailView.tsx +413 -0
  213. package/apps/web/src/features/tasks/ui/TaskEditModal.tsx +165 -0
  214. package/apps/web/src/features/tasks/ui/TaskListModal.tsx +591 -0
  215. package/apps/web/src/features/tasks/ui/__tests__/AgentRoleSelect.test.tsx +91 -0
  216. package/apps/web/src/features/tasks/ui/__tests__/ChangelogPanel.test.tsx +94 -0
  217. package/apps/web/src/features/tasks/ui/__tests__/RunHistoryPanel.test.tsx +71 -0
  218. package/apps/web/src/features/tasks/ui/__tests__/TaskCreateModal.test.tsx +153 -0
  219. package/apps/web/src/features/tasks/ui/__tests__/TaskEditModal.test.tsx +75 -0
  220. package/apps/web/src/features/tasks/ui/__tests__/TaskListModal.test.tsx +243 -0
  221. package/apps/web/src/hooks/useWorkingDir.ts +28 -0
  222. package/apps/web/src/lib/__tests__/ansi.test.ts +88 -0
  223. package/apps/web/src/lib/ansi.ts +105 -0
  224. package/apps/web/src/lib/constants.ts +4 -0
  225. package/apps/web/src/lib/quota.ts +22 -0
  226. package/apps/web/src/lib/theme.tsx +78 -0
  227. package/apps/web/src/lib/toast.tsx +175 -0
  228. package/apps/web/src/store/agentStatusStore.ts +38 -0
  229. package/apps/web/tsconfig.json +18 -0
  230. package/apps/web/vitest.config.ts +25 -0
  231. package/apps/web/vitest.setup.ts +10 -0
  232. package/package.json +85 -0
  233. package/packages/cli/dist/commands/check.d.ts +1 -0
  234. package/packages/cli/dist/commands/check.js +89 -0
  235. package/packages/cli/dist/commands/init.d.ts +5 -0
  236. package/packages/cli/dist/commands/init.js +183 -0
  237. package/packages/cli/dist/commands/start.d.ts +4 -0
  238. package/packages/cli/dist/commands/start.js +188 -0
  239. package/packages/cli/dist/index.d.ts +2 -0
  240. package/packages/cli/dist/index.js +71 -0
  241. package/packages/cli/dist/utils/agent-tools.d.ts +28 -0
  242. package/packages/cli/dist/utils/agent-tools.js +193 -0
  243. package/packages/cli/dist/utils/project-init.d.ts +12 -0
  244. package/packages/cli/dist/utils/project-init.js +258 -0
  245. package/packages/cli/dist/utils/proxy.d.ts +8 -0
  246. package/packages/cli/dist/utils/proxy.js +138 -0
  247. package/packages/cli/package.json +30 -0
  248. package/packages/cli/src/commands/check.ts +77 -0
  249. package/packages/cli/src/commands/init.ts +209 -0
  250. package/packages/cli/src/commands/start.ts +183 -0
  251. package/packages/cli/src/index.ts +91 -0
  252. package/packages/cli/src/utils/agent-tools.ts +201 -0
  253. package/packages/cli/src/utils/project-init.ts +252 -0
  254. package/packages/cli/src/utils/proxy.ts +123 -0
  255. package/packages/cli/tsconfig.json +14 -0
  256. package/packages/eslint-config/base.mjs +31 -0
  257. package/packages/eslint-config/nest.mjs +55 -0
  258. package/packages/eslint-config/next.mjs +23 -0
  259. package/packages/eslint-config/package.json +20 -0
  260. package/packages/typescript-config/base.json +16 -0
  261. package/packages/typescript-config/nestjs.json +17 -0
  262. package/packages/typescript-config/nextjs.json +15 -0
  263. package/packages/typescript-config/package.json +11 -0
  264. package/turbo.json +28 -0
@@ -0,0 +1,91 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import { AgentRoleBadge, AgentRow } from "../AgentRoleSelect";
6
+ import type { AgentDraft } from "../../hooks/useTaskCreate";
7
+
8
+ function draft(overrides: Partial<AgentDraft> = {}): AgentDraft {
9
+ return { id: "d1", agentType: "claude", role: "frontend", customRole: "", ...overrides };
10
+ }
11
+
12
+ describe("AgentRow — role buttons", () => {
13
+ it("renders all 5 role buttons", () => {
14
+ render(<AgentRow agent={draft()} onChange={vi.fn()} onRemove={vi.fn()} />);
15
+ ["Frontend", "Backend", "Doc", "Operation", "Other"].forEach((label) => {
16
+ expect(screen.getByRole("button", { name: label })).toBeInTheDocument();
17
+ });
18
+ });
19
+
20
+ it("calls onChange with new role when button clicked", async () => {
21
+ const user = userEvent.setup();
22
+ const onChange = vi.fn();
23
+ render(<AgentRow agent={draft({ role: "frontend" })} onChange={onChange} onRemove={vi.fn()} />);
24
+
25
+ await user.click(screen.getByRole("button", { name: "Backend" }));
26
+
27
+ expect(onChange).toHaveBeenCalledWith({ role: "backend" });
28
+ });
29
+
30
+ it("calls onRemove when delete button clicked", async () => {
31
+ const user = userEvent.setup();
32
+ const onRemove = vi.fn();
33
+ render(<AgentRow agent={draft()} onChange={vi.fn()} onRemove={onRemove} />);
34
+
35
+ await user.click(screen.getByRole("button", { name: "에이전트 삭제" }));
36
+
37
+ expect(onRemove).toHaveBeenCalledTimes(1);
38
+ });
39
+ });
40
+
41
+ describe("AgentRow — role=other", () => {
42
+ it("shows custom role input when role is other", () => {
43
+ render(<AgentRow agent={draft({ role: "other" })} onChange={vi.fn()} onRemove={vi.fn()} />);
44
+ expect(screen.getByPlaceholderText("역할을 직접 입력하세요")).toBeInTheDocument();
45
+ });
46
+
47
+ it("hides custom input for non-other roles", () => {
48
+ render(<AgentRow agent={draft({ role: "frontend" })} onChange={vi.fn()} onRemove={vi.fn()} />);
49
+ expect(screen.queryByPlaceholderText("역할을 직접 입력하세요")).not.toBeInTheDocument();
50
+ });
51
+
52
+ it("calls onChange with customRole on every keystroke", async () => {
53
+ const user = userEvent.setup();
54
+ const onChange = vi.fn();
55
+ render(<AgentRow agent={draft({ role: "other" })} onChange={onChange} onRemove={vi.fn()} />);
56
+
57
+ // Since this is a controlled input, each keystroke calls onChange once.
58
+ await user.type(screen.getByPlaceholderText("역할을 직접 입력하세요"), "D");
59
+
60
+ expect(onChange).toHaveBeenCalledWith({ customRole: "D" });
61
+ });
62
+ });
63
+
64
+ describe("AgentRow — frontend description hint", () => {
65
+ it("shows description hint for frontend", () => {
66
+ render(<AgentRow agent={draft({ role: "frontend" })} onChange={vi.fn()} onRemove={vi.fn()} />);
67
+ expect(screen.getByText(/UI 구현/)).toBeInTheDocument();
68
+ });
69
+
70
+ it("does not show hint for backend", () => {
71
+ render(<AgentRow agent={draft({ role: "backend" })} onChange={vi.fn()} onRemove={vi.fn()} />);
72
+ expect(screen.queryByText(/UI 구현/)).not.toBeInTheDocument();
73
+ });
74
+ });
75
+
76
+ describe("AgentRoleBadge", () => {
77
+ it("renders role label", () => {
78
+ render(<AgentRoleBadge role="frontend" />);
79
+ expect(screen.getByText("Frontend")).toBeInTheDocument();
80
+ });
81
+
82
+ it("renders customRole for other when provided", () => {
83
+ render(<AgentRoleBadge role="other" customRole="DevOps" />);
84
+ expect(screen.getByText("DevOps")).toBeInTheDocument();
85
+ });
86
+
87
+ it('renders "Other" when customRole is null', () => {
88
+ render(<AgentRoleBadge role="other" customRole={null} />);
89
+ expect(screen.getByText("Other")).toBeInTheDocument();
90
+ });
91
+ });
@@ -0,0 +1,94 @@
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import * as changelogApi from "../../api/changelog.api";
6
+ import type { TaskAgent } from "../../api/tasks.api";
7
+ import { useTaskChangelog } from "../../hooks/useTaskChangelog";
8
+ import { ChangelogPanel } from "../ChangelogPanel";
9
+
10
+ vi.mock("../../hooks/useTaskChangelog", () => ({
11
+ useTaskChangelog: vi.fn(),
12
+ }));
13
+
14
+ vi.mock("../../api/changelog.api", async () => {
15
+ const actual = await vi.importActual<typeof import("../../api/changelog.api")>("../../api/changelog.api");
16
+ return {
17
+ ...actual,
18
+ mergeAgentAll: vi.fn(),
19
+ mergeAgentFile: vi.fn(),
20
+ };
21
+ });
22
+
23
+ const mockUseTaskChangelog = vi.mocked(useTaskChangelog);
24
+ const mockMergeAll = vi.mocked(changelogApi.mergeAgentAll);
25
+ const mockMergeFile = vi.mocked(changelogApi.mergeAgentFile);
26
+
27
+ const agents: TaskAgent[] = [
28
+ { id: 1, agentType: "claude", role: "frontend", customRole: null, status: "completed" },
29
+ ];
30
+
31
+ const changelogs = [
32
+ {
33
+ agentId: 1,
34
+ files: [
35
+ {
36
+ id: 1,
37
+ filePath: "src/App.tsx",
38
+ changeType: "modified" as const,
39
+ additions: 2,
40
+ deletions: 1,
41
+ patch: "diff --git\n@@ -1 +1 @@\n-old\n+new",
42
+ },
43
+ ],
44
+ },
45
+ ];
46
+
47
+ describe("ChangelogPanel", () => {
48
+ beforeEach(() => {
49
+ vi.clearAllMocks();
50
+ });
51
+
52
+ it("renders loading, error, and empty states", () => {
53
+ mockUseTaskChangelog.mockReturnValueOnce({ changelogs: [], loading: true, error: null });
54
+ const { container, rerender } = render(<ChangelogPanel taskId="task-1" agents={agents} />);
55
+ expect(container.querySelectorAll(".animate-pulse")).toHaveLength(3);
56
+
57
+ mockUseTaskChangelog.mockReturnValueOnce({ changelogs: [], loading: false, error: "diff failed" });
58
+ rerender(<ChangelogPanel taskId="task-1" agents={agents} />);
59
+ expect(screen.getByText("diff failed")).toBeInTheDocument();
60
+
61
+ mockUseTaskChangelog.mockReturnValueOnce({ changelogs: [], loading: false, error: null });
62
+ rerender(<ChangelogPanel taskId="task-1" agents={agents} />);
63
+ expect(screen.getByText(/변경사항이 없거나/)).toBeInTheDocument();
64
+ });
65
+
66
+ it("renders files, toggles patch, and merges all/file", async () => {
67
+ mockUseTaskChangelog.mockReturnValue({ changelogs, loading: false, error: null });
68
+ mockMergeAll.mockResolvedValue({ success: true, message: "ok" });
69
+ mockMergeFile.mockResolvedValue({ success: false, message: "conflict" });
70
+ const user = userEvent.setup();
71
+
72
+ render(<ChangelogPanel taskId="task-1" agents={agents} />);
73
+
74
+ expect(screen.getByText("Claude · frontend")).toBeInTheDocument();
75
+ expect(screen.getByText("src/")).toBeInTheDocument();
76
+ expect(screen.getByText("App.tsx")).toBeInTheDocument();
77
+ expect(screen.getAllByText("+2")).toHaveLength(2);
78
+ expect(screen.getAllByText("-1")).toHaveLength(2);
79
+
80
+ await user.click(screen.getByRole("button", { name: /수정.*App\.tsx/s }));
81
+ expect(screen.getByText("+new")).toBeInTheDocument();
82
+
83
+ await user.click(screen.getByRole("button", { name: "src/App.tsx 변경 코드 복사" }));
84
+ expect(screen.getByText("복사됨")).toBeInTheDocument();
85
+
86
+ await user.click(screen.getByRole("button", { name: "전체 병합" }));
87
+ await waitFor(() => expect(mockMergeAll).toHaveBeenCalledWith("task-1", 1));
88
+ expect(screen.getByText((text) => text.includes("병합 완료"))).toBeInTheDocument();
89
+
90
+ await user.click(screen.getByRole("button", { name: "병합" }));
91
+ await waitFor(() => expect(mockMergeFile).toHaveBeenCalledWith("task-1", 1, "src/App.tsx"));
92
+ expect(screen.getByText((text) => text.includes("conflict"))).toBeInTheDocument();
93
+ });
94
+ });
@@ -0,0 +1,71 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import type { TaskAgent, TaskRun } from "../../api/tasks.api";
5
+ import { useTaskRuns } from "../../hooks/useTaskRuns";
6
+ import { RunHistoryPanel } from "../RunHistoryPanel";
7
+
8
+ vi.mock("../../hooks/useTaskRuns", () => ({
9
+ useTaskRuns: vi.fn(),
10
+ }));
11
+
12
+ const mockUseTaskRuns = vi.mocked(useTaskRuns);
13
+
14
+ const agents: TaskAgent[] = [
15
+ { id: 1, agentType: "claude", role: "frontend", customRole: null, status: "completed" },
16
+ ];
17
+
18
+ const runs: TaskRun[] = [
19
+ {
20
+ id: 1,
21
+ version: 2,
22
+ supplementNote: "보완 요청",
23
+ status: "completed",
24
+ startedAt: "2024-01-01T00:00:00.000Z",
25
+ completedAt: "2024-01-01T00:00:03.000Z",
26
+ agentRuns: [
27
+ { id: 10, agentId: 1, status: "completed", worktreePath: null, startCommitHash: null, durationMs: 1500, costUsd: 0.01 },
28
+ ],
29
+ },
30
+ {
31
+ id: 2,
32
+ version: 1,
33
+ supplementNote: null,
34
+ status: "error",
35
+ startedAt: "2024-01-01T00:00:00.000Z",
36
+ completedAt: null,
37
+ agentRuns: [],
38
+ },
39
+ ];
40
+
41
+ describe("RunHistoryPanel", () => {
42
+ it("renders loading skeleton", () => {
43
+ mockUseTaskRuns.mockReturnValue({ runs: [], loading: true, error: null });
44
+ const { container } = render(<RunHistoryPanel taskId="task-1" agents={agents} />);
45
+ expect(container.querySelectorAll(".animate-pulse")).toHaveLength(2);
46
+ });
47
+
48
+ it("renders error state", () => {
49
+ mockUseTaskRuns.mockReturnValue({ runs: [], loading: false, error: "failed" });
50
+ render(<RunHistoryPanel taskId="task-1" agents={agents} />);
51
+ expect(screen.getByText("failed")).toBeInTheDocument();
52
+ });
53
+
54
+ it("renders empty state", () => {
55
+ mockUseTaskRuns.mockReturnValue({ runs: [], loading: false, error: null });
56
+ render(<RunHistoryPanel taskId="task-1" agents={agents} />);
57
+ expect(screen.getByText("실행 기록이 없습니다")).toBeInTheDocument();
58
+ });
59
+
60
+ it("renders run history, latest badge, supplement note, duration, and cost", () => {
61
+ mockUseTaskRuns.mockReturnValue({ runs, loading: false, error: null });
62
+ render(<RunHistoryPanel taskId="task-1" agents={agents} />);
63
+
64
+ expect(screen.getByText("v2")).toBeInTheDocument();
65
+ expect(screen.getByText("최신")).toBeInTheDocument();
66
+ expect(screen.getByText("보완 요청")).toBeInTheDocument();
67
+ expect(screen.getByText("3s")).toBeInTheDocument();
68
+ expect(screen.getByText("$0.0100")).toBeInTheDocument();
69
+ expect(screen.getByText("Frontend")).toBeInTheDocument();
70
+ });
71
+ });
@@ -0,0 +1,153 @@
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import type { Task } from "../../api/tasks.api";
6
+ import * as tasksApi from "../../api/tasks.api";
7
+ import { TaskCreateModal } from "../TaskCreateModal";
8
+
9
+ vi.mock("../../api/tasks.api", () => ({
10
+ createTask: vi.fn(),
11
+ }));
12
+
13
+ const mockCreateTask = vi.mocked(tasksApi.createTask);
14
+
15
+ const stubTask: Task = {
16
+ id: "t1",
17
+ title: "Test",
18
+ status: "pending",
19
+ workingDir: null,
20
+ requirements: [],
21
+ agents: [],
22
+ createdAt: "2024-01-01",
23
+ updatedAt: "2024-01-01",
24
+ };
25
+
26
+ afterEach(() => { vi.clearAllMocks(); });
27
+
28
+ describe("TaskCreateModal — open/close", () => {
29
+ it("renders modal when open=true", () => {
30
+ render(<TaskCreateModal open={true} onClose={vi.fn()} />);
31
+ expect(screen.getByText("새 작업 추가")).toBeInTheDocument();
32
+ });
33
+
34
+ it("does not render when open=false", () => {
35
+ render(<TaskCreateModal open={false} onClose={vi.fn()} />);
36
+ expect(screen.queryByText("새 작업 추가")).not.toBeInTheDocument();
37
+ });
38
+
39
+ it("calls onClose when 취소 button is clicked", async () => {
40
+ const user = userEvent.setup();
41
+ const onClose = vi.fn();
42
+ render(<TaskCreateModal open={true} onClose={onClose} />);
43
+ await user.click(screen.getByRole("button", { name: "취소" }));
44
+ expect(onClose).toHaveBeenCalledTimes(1);
45
+ });
46
+ });
47
+
48
+ describe("TaskCreateModal — form fields", () => {
49
+ it("renders goal textarea", () => {
50
+ render(<TaskCreateModal open={true} onClose={vi.fn()} />);
51
+ expect(screen.getByPlaceholderText(/예: 로그인 페이지/)).toBeInTheDocument();
52
+ });
53
+
54
+ it("renders workingDir input", () => {
55
+ render(<TaskCreateModal open={true} onClose={vi.fn()} />);
56
+ expect(screen.getByPlaceholderText("/path/to/project")).toBeInTheDocument();
57
+ });
58
+
59
+ it("submit button is disabled when title is empty", () => {
60
+ render(<TaskCreateModal open={true} onClose={vi.fn()} />);
61
+ expect(screen.getByRole("button", { name: "작업 생성" })).toBeDisabled();
62
+ });
63
+
64
+ it("submit button becomes enabled after title is entered", async () => {
65
+ const user = userEvent.setup();
66
+ render(<TaskCreateModal open={true} onClose={vi.fn()} />);
67
+ await user.type(screen.getByPlaceholderText(/예: 로그인 페이지/), "My Task");
68
+ expect(screen.getByRole("button", { name: "작업 생성" })).toBeEnabled();
69
+ });
70
+ });
71
+
72
+ describe("TaskCreateModal — requirements", () => {
73
+ it("adds requirement row when '항목 추가' is clicked", async () => {
74
+ const user = userEvent.setup();
75
+ render(<TaskCreateModal open={true} onClose={vi.fn()} />);
76
+ await user.click(screen.getByRole("button", { name: /항목 추가/ }));
77
+ expect(screen.getAllByPlaceholderText("요구사항 입력")).toHaveLength(1);
78
+ });
79
+
80
+ it("removes requirement row when X button clicked", async () => {
81
+ const user = userEvent.setup();
82
+ render(<TaskCreateModal open={true} onClose={vi.fn()} />);
83
+ await user.click(screen.getByRole("button", { name: /항목 추가/ }));
84
+ const removeBtn = screen.getAllByRole("button").find(
85
+ (b) => !["항목 추가", "에이전트 추가", "취소", "작업 생성", "닫기"].includes(b.textContent?.trim() ?? ""),
86
+ );
87
+ if (removeBtn) await user.click(removeBtn);
88
+ expect(screen.queryByPlaceholderText("요구사항 입력")).not.toBeInTheDocument();
89
+ });
90
+ });
91
+
92
+ describe("TaskCreateModal — agents", () => {
93
+ it("adds agent row when '에이전트 추가' is clicked", async () => {
94
+ const user = userEvent.setup();
95
+ render(<TaskCreateModal open={true} onClose={vi.fn()} />);
96
+ await user.click(screen.getByRole("button", { name: /에이전트 추가/ }));
97
+ await user.click(screen.getByRole("button", { name: /Claude Code/ }));
98
+
99
+ expect(screen.getByText("Claude Code")).toBeInTheDocument();
100
+ expect(screen.getByRole("button", { name: "Frontend" })).toBeInTheDocument();
101
+ });
102
+
103
+ it("shows frontend as default role for new agent", async () => {
104
+ const user = userEvent.setup();
105
+ render(<TaskCreateModal open={true} onClose={vi.fn()} />);
106
+ await user.click(screen.getByRole("button", { name: /에이전트 추가/ }));
107
+ await user.click(screen.getByRole("button", { name: /Claude Code/ }));
108
+
109
+ expect(screen.getByText(/UI 구현/)).toBeInTheDocument();
110
+ });
111
+ });
112
+
113
+ describe("TaskCreateModal — submit", () => {
114
+ it("calls createTask with correct payload", async () => {
115
+ mockCreateTask.mockResolvedValueOnce(stubTask);
116
+ const user = userEvent.setup();
117
+ render(<TaskCreateModal open={true} onClose={vi.fn()} />);
118
+
119
+ await user.type(screen.getByPlaceholderText(/예: 로그인 페이지/), "My Task");
120
+ await user.click(screen.getByRole("button", { name: "작업 생성" }));
121
+
122
+ await waitFor(() => {
123
+ expect(mockCreateTask).toHaveBeenCalledWith(
124
+ expect.objectContaining({ title: "My Task" }),
125
+ );
126
+ });
127
+ });
128
+
129
+ it("calls onCreated callback after successful creation", async () => {
130
+ mockCreateTask.mockResolvedValueOnce(stubTask);
131
+ const user = userEvent.setup();
132
+ const onCreated = vi.fn();
133
+ render(<TaskCreateModal open={true} onClose={vi.fn()} onCreated={onCreated} />);
134
+
135
+ await user.type(screen.getByPlaceholderText(/예: 로그인 페이지/), "My Task");
136
+ await user.click(screen.getByRole("button", { name: "작업 생성" }));
137
+
138
+ await waitFor(() => { expect(onCreated).toHaveBeenCalledWith(stubTask); });
139
+ });
140
+
141
+ it("shows error message when createTask fails", async () => {
142
+ mockCreateTask.mockRejectedValueOnce(new Error("Server error"));
143
+ const user = userEvent.setup();
144
+ render(<TaskCreateModal open={true} onClose={vi.fn()} />);
145
+
146
+ await user.type(screen.getByPlaceholderText(/예: 로그인 페이지/), "My Task");
147
+ await user.click(screen.getByRole("button", { name: "작업 생성" }));
148
+
149
+ await waitFor(() => {
150
+ expect(screen.getByText("Server error")).toBeInTheDocument();
151
+ });
152
+ });
153
+ });
@@ -0,0 +1,75 @@
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import type { Task } from "../../api/tasks.api";
6
+ import * as tasksApi from "../../api/tasks.api";
7
+ import { TaskEditModal } from "../TaskEditModal";
8
+
9
+ vi.mock("../../api/tasks.api", () => ({
10
+ updateTask: vi.fn(),
11
+ }));
12
+
13
+ const mockUpdateTask = vi.mocked(tasksApi.updateTask);
14
+
15
+ const task: Task = {
16
+ id: "task-1",
17
+ title: "Original title",
18
+ status: "pending",
19
+ workingDir: "/repo",
20
+ requirements: [{ id: 1, content: "First req", status: "pending", orderIndex: 0 }],
21
+ agents: [{ id: 1, agentType: "claude", role: "frontend", customRole: null, status: "pending" }],
22
+ createdAt: "2024-01-01",
23
+ updatedAt: "2024-01-01",
24
+ };
25
+
26
+ afterEach(() => { vi.clearAllMocks(); });
27
+
28
+ describe("TaskEditModal", () => {
29
+ it("renders existing task fields and closes on cancel", async () => {
30
+ const user = userEvent.setup();
31
+ const onClose = vi.fn();
32
+ render(<TaskEditModal task={task} onClose={onClose} onSaved={vi.fn()} />);
33
+
34
+ expect(screen.getByText("작업 수정")).toBeInTheDocument();
35
+ expect(screen.getByDisplayValue("Original title")).toBeInTheDocument();
36
+ expect(screen.getByDisplayValue("/repo")).toBeInTheDocument();
37
+ expect(screen.getByDisplayValue("First req")).toBeInTheDocument();
38
+ expect(screen.getByText("Claude Code")).toBeInTheDocument();
39
+
40
+ await user.click(screen.getByRole("button", { name: "취소" }));
41
+ expect(onClose).toHaveBeenCalledTimes(1);
42
+ });
43
+
44
+ it("adds requirement and agent, then saves", async () => {
45
+ const user = userEvent.setup();
46
+ const onClose = vi.fn();
47
+ const onSaved = vi.fn();
48
+ const updated = { ...task, title: "Changed" };
49
+ mockUpdateTask.mockResolvedValueOnce(updated);
50
+ render(<TaskEditModal task={{ ...task, requirements: [], agents: [] }} onClose={onClose} onSaved={onSaved} />);
51
+
52
+ const title = screen.getByPlaceholderText(/예: 로그인 페이지/);
53
+ await user.clear(title);
54
+ await user.type(title, "Changed");
55
+ await user.click(screen.getByRole("button", { name: /항목 추가/ }));
56
+ await user.type(screen.getByPlaceholderText("요구사항 입력"), "Req");
57
+ await user.click(screen.getByRole("button", { name: /에이전트 추가/ }));
58
+ await user.click(screen.getByRole("button", { name: /Codex CLI/ }));
59
+ await user.click(screen.getByRole("button", { name: "저장" }));
60
+
61
+ await waitFor(() => expect(mockUpdateTask).toHaveBeenCalled());
62
+ expect(onSaved).toHaveBeenCalledWith(updated);
63
+ expect(onClose).toHaveBeenCalledTimes(1);
64
+ });
65
+
66
+ it("shows API error", async () => {
67
+ const user = userEvent.setup();
68
+ mockUpdateTask.mockRejectedValueOnce(new Error("save failed"));
69
+ render(<TaskEditModal task={task} onClose={vi.fn()} onSaved={vi.fn()} />);
70
+
71
+ await user.click(screen.getByRole("button", { name: "저장" }));
72
+
73
+ expect(await screen.findByText("save failed")).toBeInTheDocument();
74
+ });
75
+ });