kcg-code 0.0.0-stage → 0.1.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 (286) hide show
  1. package/README.md +228 -2
  2. package/bin/kcgcode.ts +13 -0
  3. package/bun-env.d.ts +19 -0
  4. package/kcg-code.config.example.json +3 -0
  5. package/package.json +83 -4
  6. package/public/logo.svg +1 -0
  7. package/public/manifest.json +18 -0
  8. package/public/sw.js +49 -0
  9. package/src/App.tsx +39 -0
  10. package/src/__tests__/manifest.test.ts +61 -0
  11. package/src/build-config.ts +9 -0
  12. package/src/cli/__tests__/dashboard.test.ts +62 -0
  13. package/src/cli/args.ts +123 -0
  14. package/src/cli/commands.ts +174 -0
  15. package/src/cli/dashboard.tsx +343 -0
  16. package/src/cli/index.ts +37 -0
  17. package/src/cli/theme.ts +100 -0
  18. package/src/components/auth/AuthGate.tsx +182 -0
  19. package/src/components/auth/AvatarEditor.tsx +236 -0
  20. package/src/components/auth/LockScreen.tsx +714 -0
  21. package/src/components/auth/ProfileAvatar.tsx +60 -0
  22. package/src/components/auth/WelcomeScreen.tsx +672 -0
  23. package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
  24. package/src/components/auth/avatar-icons.tsx +114 -0
  25. package/src/components/layout/AppShell.tsx +142 -0
  26. package/src/components/layout/AppSidebar.tsx +419 -0
  27. package/src/components/layout/SearchDialog.tsx +189 -0
  28. package/src/components/layout/SidebarAccount.tsx +111 -0
  29. package/src/components/layout/SidebarSessionMenu.tsx +163 -0
  30. package/src/components/layout/ThemeToggle.tsx +67 -0
  31. package/src/components/projects/FolderBrowser.tsx +142 -0
  32. package/src/components/projects/HomeChat.tsx +250 -0
  33. package/src/components/projects/NewProjectDialog.tsx +249 -0
  34. package/src/components/projects/PanelSection.tsx +102 -0
  35. package/src/components/projects/ProjectInstructions.tsx +229 -0
  36. package/src/components/projects/ProjectRow.tsx +58 -0
  37. package/src/components/projects/ProjectSidePanel.tsx +448 -0
  38. package/src/components/sessions/AgentModeList.tsx +265 -0
  39. package/src/components/sessions/AgentPicker.tsx +123 -0
  40. package/src/components/sessions/AssistantTurn.tsx +145 -0
  41. package/src/components/sessions/AttachmentTile.tsx +161 -0
  42. package/src/components/sessions/CodeBlock.tsx +107 -0
  43. package/src/components/sessions/ComposerActionSheet.tsx +96 -0
  44. package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
  45. package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +72 -0
  46. package/src/components/sessions/ContextPanel.tsx +561 -0
  47. package/src/components/sessions/DropPlate.tsx +165 -0
  48. package/src/components/sessions/FileDropOverlay.tsx +232 -0
  49. package/src/components/sessions/FileMentionDropdown.tsx +47 -0
  50. package/src/components/sessions/ImageLightbox.tsx +173 -0
  51. package/src/components/sessions/MarkdownContent.tsx +187 -0
  52. package/src/components/sessions/Mascot.tsx +47 -0
  53. package/src/components/sessions/MessageActions.tsx +60 -0
  54. package/src/components/sessions/MessageRail.tsx +121 -0
  55. package/src/components/sessions/ModelPicker.tsx +209 -0
  56. package/src/components/sessions/ModelSearchList.tsx +151 -0
  57. package/src/components/sessions/NewSessionDialog.tsx +189 -0
  58. package/src/components/sessions/PendingAttachments.tsx +127 -0
  59. package/src/components/sessions/PromptCard.tsx +1085 -0
  60. package/src/components/sessions/PromptPanel.tsx +112 -0
  61. package/src/components/sessions/SessionComposer.tsx +640 -0
  62. package/src/components/sessions/SessionHeader.tsx +175 -0
  63. package/src/components/sessions/SessionList.tsx +396 -0
  64. package/src/components/sessions/SessionRow.tsx +137 -0
  65. package/src/components/sessions/SessionTimeline.tsx +275 -0
  66. package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
  67. package/src/components/sessions/SessionView.tsx +236 -0
  68. package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
  69. package/src/components/sessions/UserMessage.tsx +177 -0
  70. package/src/components/sessions/__tests__/ContextPanel.test.tsx +220 -0
  71. package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
  72. package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
  73. package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
  74. package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
  75. package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
  76. package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
  77. package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
  78. package/src/components/settings/AboutGroup.tsx +170 -0
  79. package/src/components/settings/AccountGroup.tsx +122 -0
  80. package/src/components/settings/DevicesGroup.tsx +151 -0
  81. package/src/components/settings/LhrRemoteAccess.tsx +262 -0
  82. package/src/components/settings/PasskeysGroup.tsx +551 -0
  83. package/src/components/settings/RemoteAccessGroup.tsx +1226 -0
  84. package/src/components/settings/SecurityGroup.tsx +566 -0
  85. package/src/components/settings/WorkspaceGroup.tsx +370 -0
  86. package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
  87. package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
  88. package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
  89. package/src/components/settings/prefs.tsx +265 -0
  90. package/src/components/skills/InstallNotifications.tsx +325 -0
  91. package/src/components/skills/InstallSkillDialog.tsx +233 -0
  92. package/src/components/skills/SkillCards.tsx +210 -0
  93. package/src/components/skills/SkillsProjectPicker.tsx +178 -0
  94. package/src/components/ui/alert-dialog.tsx +179 -0
  95. package/src/components/ui/alert.tsx +60 -0
  96. package/src/components/ui/badge.tsx +46 -0
  97. package/src/components/ui/bubble.tsx +125 -0
  98. package/src/components/ui/button.tsx +62 -0
  99. package/src/components/ui/card.tsx +75 -0
  100. package/src/components/ui/collapsible.tsx +19 -0
  101. package/src/components/ui/dialog.tsx +142 -0
  102. package/src/components/ui/dropdown-menu.tsx +225 -0
  103. package/src/components/ui/empty.tsx +94 -0
  104. package/src/components/ui/field.tsx +234 -0
  105. package/src/components/ui/input-group.tsx +161 -0
  106. package/src/components/ui/input.tsx +21 -0
  107. package/src/components/ui/label.tsx +19 -0
  108. package/src/components/ui/message-scroller.tsx +131 -0
  109. package/src/components/ui/message.tsx +85 -0
  110. package/src/components/ui/popover.tsx +46 -0
  111. package/src/components/ui/select.tsx +172 -0
  112. package/src/components/ui/separator.tsx +28 -0
  113. package/src/components/ui/sheet.tsx +138 -0
  114. package/src/components/ui/sidebar.tsx +699 -0
  115. package/src/components/ui/skeleton.tsx +15 -0
  116. package/src/components/ui/sonner.tsx +45 -0
  117. package/src/components/ui/spinner.tsx +16 -0
  118. package/src/components/ui/switch.tsx +32 -0
  119. package/src/components/ui/textarea.tsx +18 -0
  120. package/src/components/ui/tooltip.tsx +51 -0
  121. package/src/config.ts +85 -0
  122. package/src/db.ts +146 -0
  123. package/src/frontend.tsx +51 -0
  124. package/src/hooks/__tests__/useMention.test.ts +79 -0
  125. package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
  126. package/src/hooks/__tests__/useRouter.test.ts +17 -0
  127. package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
  128. package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
  129. package/src/hooks/__tests__/useTheme.test.ts +118 -0
  130. package/src/hooks/use-mobile.ts +16 -0
  131. package/src/hooks/useAgentPicker.ts +97 -0
  132. package/src/hooks/useMediaQuery.ts +49 -0
  133. package/src/hooks/useMention.ts +236 -0
  134. package/src/hooks/useProjects.ts +88 -0
  135. package/src/hooks/useResyncOnReconnect.ts +28 -0
  136. package/src/hooks/useRouter.ts +92 -0
  137. package/src/hooks/useSessionChat.ts +427 -0
  138. package/src/hooks/useSessionContext.ts +106 -0
  139. package/src/hooks/useSessionList.ts +234 -0
  140. package/src/hooks/useSidebarData.ts +108 -0
  141. package/src/hooks/useSmoothText.ts +122 -0
  142. package/src/hooks/useTheme.ts +164 -0
  143. package/src/hooks/useWebSocket.ts +196 -0
  144. package/src/index.css +27 -0
  145. package/src/index.html +14 -0
  146. package/src/index.ts +49 -0
  147. package/src/lib/__tests__/attachments.test.ts +58 -0
  148. package/src/lib/__tests__/avatar-presets.test.ts +25 -0
  149. package/src/lib/__tests__/collapsible.test.ts +101 -0
  150. package/src/lib/__tests__/composer.test.ts +118 -0
  151. package/src/lib/__tests__/highlight.test.ts +42 -0
  152. package/src/lib/__tests__/home-chat.test.ts +64 -0
  153. package/src/lib/__tests__/lock-screen.test.ts +64 -0
  154. package/src/lib/__tests__/model-picker.test.ts +205 -0
  155. package/src/lib/__tests__/onboarding.test.ts +41 -0
  156. package/src/lib/__tests__/permission-info.test.ts +61 -0
  157. package/src/lib/__tests__/project-extensions.test.ts +52 -0
  158. package/src/lib/__tests__/project-overview.test.ts +173 -0
  159. package/src/lib/__tests__/references.test.ts +121 -0
  160. package/src/lib/__tests__/routes.test.ts +134 -0
  161. package/src/lib/__tests__/session-summary.test.ts +115 -0
  162. package/src/lib/__tests__/session-title.test.ts +58 -0
  163. package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
  164. package/src/lib/__tests__/skill-installs.test.ts +134 -0
  165. package/src/lib/__tests__/skills-catalog.test.ts +96 -0
  166. package/src/lib/__tests__/tone.test.ts +37 -0
  167. package/src/lib/__tests__/turns-delta.test.ts +55 -0
  168. package/src/lib/api.ts +306 -0
  169. package/src/lib/attachments.ts +203 -0
  170. package/src/lib/auth.ts +92 -0
  171. package/src/lib/avatar-presets.ts +31 -0
  172. package/src/lib/collapsible.ts +75 -0
  173. package/src/lib/composer.ts +89 -0
  174. package/src/lib/data-events.ts +17 -0
  175. package/src/lib/headlines.ts +30 -0
  176. package/src/lib/highlight.ts +186 -0
  177. package/src/lib/layout.ts +20 -0
  178. package/src/lib/lock-screen.ts +54 -0
  179. package/src/lib/model-picker.ts +133 -0
  180. package/src/lib/onboarding.ts +38 -0
  181. package/src/lib/passkeys.ts +113 -0
  182. package/src/lib/pending-prompt.ts +53 -0
  183. package/src/lib/permission-info.ts +101 -0
  184. package/src/lib/project-extensions.ts +74 -0
  185. package/src/lib/project-overview.ts +117 -0
  186. package/src/lib/prompts.ts +87 -0
  187. package/src/lib/references.ts +106 -0
  188. package/src/lib/routes.ts +115 -0
  189. package/src/lib/session-handoff.ts +25 -0
  190. package/src/lib/session-status.ts +45 -0
  191. package/src/lib/session-summary.ts +68 -0
  192. package/src/lib/session-title.ts +63 -0
  193. package/src/lib/sidebar-groups.ts +78 -0
  194. package/src/lib/skill-installs.ts +360 -0
  195. package/src/lib/skills-catalog.ts +99 -0
  196. package/src/lib/time.ts +22 -0
  197. package/src/lib/tone.ts +80 -0
  198. package/src/lib/turns.ts +320 -0
  199. package/src/lib/utils.ts +6 -0
  200. package/src/logo.svg +31 -0
  201. package/src/pages/projects/ProjectDetailPage.tsx +130 -0
  202. package/src/pages/projects/ProjectsPage.tsx +97 -0
  203. package/src/pages/sessions/SessionPage.tsx +122 -0
  204. package/src/pages/settings/SettingsPage.tsx +299 -0
  205. package/src/pages/settings/__tests__/SettingsPage.test.ts +9 -0
  206. package/src/pages/skills/SkillsPage.tsx +701 -0
  207. package/src/paths.ts +121 -0
  208. package/src/runtime.ts +24 -0
  209. package/src/server/__tests__/app.test.ts +1031 -0
  210. package/src/server/__tests__/auth.test.ts +370 -0
  211. package/src/server/__tests__/config.test.ts +101 -0
  212. package/src/server/__tests__/db.test.ts +485 -0
  213. package/src/server/__tests__/host.test.ts +14 -0
  214. package/src/server/__tests__/tunnel-config.test.ts +38 -0
  215. package/src/server/app.ts +415 -0
  216. package/src/server/db/auth.ts +311 -0
  217. package/src/server/db/messages.ts +50 -0
  218. package/src/server/db/projects.ts +106 -0
  219. package/src/server/db/prompts.ts +78 -0
  220. package/src/server/db/rows.ts +150 -0
  221. package/src/server/db/schema.ts +221 -0
  222. package/src/server/db/sessions.ts +239 -0
  223. package/src/server/db/tunnel.ts +124 -0
  224. package/src/server/host.ts +12 -0
  225. package/src/server/result.ts +15 -0
  226. package/src/server/routes/auth-guard.ts +156 -0
  227. package/src/server/routes/auth.routes.ts +287 -0
  228. package/src/server/routes/helpers.ts +165 -0
  229. package/src/server/routes/meta.routes.ts +24 -0
  230. package/src/server/routes/passkeys.routes.ts +161 -0
  231. package/src/server/routes/projects.routes.ts +157 -0
  232. package/src/server/routes/sessions.routes.ts +197 -0
  233. package/src/server/routes/skills.routes.ts +117 -0
  234. package/src/server/routes/tunnel.routes.ts +132 -0
  235. package/src/server/routes/types.ts +35 -0
  236. package/src/server/routes/uploads.routes.ts +54 -0
  237. package/src/server/routes/workspace.routes.ts +127 -0
  238. package/src/server/services/__tests__/app-info.test.ts +18 -0
  239. package/src/server/services/__tests__/attachments.test.ts +159 -0
  240. package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
  241. package/src/server/services/__tests__/auth.test.ts +341 -0
  242. package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
  243. package/src/server/services/__tests__/git-info.test.ts +74 -0
  244. package/src/server/services/__tests__/opencode.test.ts +320 -0
  245. package/src/server/services/__tests__/project-manager.test.ts +190 -0
  246. package/src/server/services/__tests__/sandbox.test.ts +141 -0
  247. package/src/server/services/__tests__/session-manager.test.ts +3469 -0
  248. package/src/server/services/__tests__/skill-install-jobs.test.ts +226 -0
  249. package/src/server/services/__tests__/skills-registry.test.ts +359 -0
  250. package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
  251. package/src/server/services/__tests__/tunnel.test.ts +367 -0
  252. package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
  253. package/src/server/services/__tests__/workspace.test.ts +249 -0
  254. package/src/server/services/app-info.ts +29 -0
  255. package/src/server/services/attachments.ts +193 -0
  256. package/src/server/services/auth-passkeys.ts +459 -0
  257. package/src/server/services/auth.ts +633 -0
  258. package/src/server/services/frpc-installer.ts +183 -0
  259. package/src/server/services/git-info.ts +87 -0
  260. package/src/server/services/opencode-client.ts +714 -0
  261. package/src/server/services/opencode-server.ts +319 -0
  262. package/src/server/services/opencode-sse.ts +82 -0
  263. package/src/server/services/project-manager.ts +147 -0
  264. package/src/server/services/sandbox.ts +83 -0
  265. package/src/server/services/session-events.ts +256 -0
  266. package/src/server/services/session-manager.ts +1374 -0
  267. package/src/server/services/skill-install-jobs.ts +262 -0
  268. package/src/server/services/skills-registry.ts +468 -0
  269. package/src/server/services/tunnel-api.ts +207 -0
  270. package/src/server/services/tunnel-config.ts +45 -0
  271. package/src/server/services/tunnel-lhr.ts +387 -0
  272. package/src/server/services/tunnel.ts +633 -0
  273. package/src/server/services/turn-stream.ts +339 -0
  274. package/src/server/services/websocket-gateway.ts +374 -0
  275. package/src/server/services/workspace.ts +223 -0
  276. package/src/server/services/ws-transport.ts +121 -0
  277. package/src/types/context.ts +69 -0
  278. package/src/types/index.ts +13 -0
  279. package/src/types/message.ts +58 -0
  280. package/src/types/project.ts +16 -0
  281. package/src/types/prompt.ts +118 -0
  282. package/src/types/session.ts +52 -0
  283. package/src/types/turns.ts +47 -0
  284. package/src/ws-protocol.ts +93 -0
  285. package/styles/globals.css +311 -0
  286. package/tsconfig.json +37 -0
@@ -0,0 +1,485 @@
1
+ /**
2
+ * Property & integration test `db.ts` (versi headless).
3
+ * - Property 8: Round-trip pesan terstruktur (Requirements 3.1, 3.4)
4
+ * - Property 9: Riwayat status append-only (Requirement 3.3)
5
+ * - Property 10: Query Session tidak ditemukan (Requirement 3.5)
6
+ * - Integration: kegagalan tulis & persistensi setelah restart (3.2, 3.6)
7
+ * - Migrasi DB lama (kolom oc_session_id / kind / title + tabel messages)
8
+ */
9
+ import { expect, test } from "bun:test";
10
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import path from "node:path";
13
+ import fc from "fast-check";
14
+ import { openSessionStore } from "../../db";
15
+ import type { InteractivePrompt, Session, SessionMessage, SessionStatus } from "../../types";
16
+
17
+ function makeSession(id = "s1", status: SessionStatus = "running"): Session {
18
+ return {
19
+ id,
20
+ projectId: "p1",
21
+ agentType: "opencode",
22
+ cwd: "/sandbox/proj",
23
+ status,
24
+ ocSessionId: id === "s1" ? "ses_1" : null,
25
+ model: id === "s1" ? { providerID: "kcgcode", modelID: "kiro/claude-opus-5" } : null,
26
+ agent: null,
27
+ title: null,
28
+ createdAt: 1,
29
+ updatedAt: 1,
30
+ };
31
+ }
32
+
33
+ function makeMessage(id: string, role: "user" | "assistant" = "user"): SessionMessage {
34
+ return {
35
+ id,
36
+ sessionId: "s1",
37
+ role,
38
+ parts: [{ type: "text", text: `isi-${id}` }],
39
+ createdAt: 1,
40
+ };
41
+ }
42
+
43
+ function seedProject(store: ReturnType<typeof openSessionStore>) {
44
+ const res = store.insertProject({ id: "p1", name: "proj", path: "/sandbox/proj", createdAt: 1 });
45
+ if (!res.ok) throw new Error(`seed project gagal: ${res.error}`);
46
+ }
47
+
48
+ function makeMemoryStore() {
49
+ const store = openSessionStore(":memory:");
50
+ seedProject(store);
51
+ return store;
52
+ }
53
+
54
+ // Feature: kcg-code, Property 8: Round-trip penyimpanan pesan terstruktur
55
+ test("Property 8: round-trip messages — urutan identik dengan penyimpanan", () => {
56
+ fc.assert(
57
+ fc.property(
58
+ fc.array(fc.constantFrom("user", "assistant" as const), { maxLength: 40 }),
59
+ (roles) => {
60
+ const store = makeMemoryStore();
61
+ try {
62
+ expect(store.insertSession(makeSession()).ok).toBe(true);
63
+ roles.forEach((role, i) => {
64
+ const r = store.insertMessage(makeMessage(`m${i}`, role));
65
+ expect(r.ok).toBe(true);
66
+ });
67
+
68
+ const res = store.getMessages("s1");
69
+ expect(res.ok).toBe(true);
70
+ if (res.ok) {
71
+ expect(res.data.map((m) => m.id)).toEqual(roles.map((_, i) => `m${i}`));
72
+ expect(res.data.map((m) => m.role)).toEqual(roles);
73
+ }
74
+ } finally {
75
+ store.close();
76
+ }
77
+ },
78
+ ),
79
+ { numRuns: 100 },
80
+ );
81
+ });
82
+
83
+ // Feature: kcg-code, Property 9: Riwayat status bersifat append-only
84
+ test("Property 9: riwayat status append-only — jumlah baris tidak pernah berkurang", () => {
85
+ fc.assert(
86
+ fc.property(
87
+ fc.array(fc.constantFrom("running", "stopped", "crashed" as const), { maxLength: 40 }),
88
+ (statuses) => {
89
+ const store = makeMemoryStore();
90
+ try {
91
+ expect(store.insertSession(makeSession()).ok).toBe(true);
92
+
93
+ const seen: { status: SessionStatus; changedAt: number }[] = [];
94
+ statuses.forEach((status, i) => {
95
+ const changedAt = i + 1;
96
+ expect(store.insertStatusHistory("s1", status, changedAt).ok).toBe(true);
97
+ seen.push({ status, changedAt });
98
+
99
+ const res = store.getStatusHistory("s1");
100
+ expect(res.ok).toBe(true);
101
+ if (res.ok) {
102
+ expect(res.data.length).toBeGreaterThanOrEqual(seen.length);
103
+ for (const prev of seen) {
104
+ expect(res.data).toContainEqual({ sessionId: "s1", ...prev });
105
+ }
106
+ }
107
+ });
108
+ } finally {
109
+ store.close();
110
+ }
111
+ },
112
+ ),
113
+ { numRuns: 100 },
114
+ );
115
+ });
116
+
117
+ // Feature: kcg-code, Property 10: Query terhadap Session tidak ditemukan
118
+ test("Property 10: query Session tidak ditemukan -> not-found tanpa data", () => {
119
+ fc.assert(
120
+ fc.property(fc.string(), fc.string(), (sid1, sid2) => {
121
+ const store = openSessionStore(":memory:");
122
+ try {
123
+ const out = store.getMessages(sid1);
124
+ expect(out.ok).toBe(false);
125
+ if (!out.ok) expect(out.error).toBe("SESSION_NOT_FOUND");
126
+
127
+ const hist = store.getStatusHistory(sid2);
128
+ expect(hist.ok).toBe(false);
129
+ if (!hist.ok) expect(hist.error).toBe("SESSION_NOT_FOUND");
130
+ } finally {
131
+ store.close();
132
+ }
133
+ }),
134
+ { numRuns: 100 },
135
+ );
136
+ });
137
+
138
+ test("5.1: CRUD sessions — insert, update status, get, getByOcId, list", () => {
139
+ const store = makeMemoryStore();
140
+ expect(store.insertSession(makeSession()).ok).toBe(true);
141
+
142
+ const listed = store.listSessions();
143
+ expect(listed).toHaveLength(1);
144
+ expect(listed[0]?.id).toBe("s1");
145
+ expect(listed[0]?.ocSessionId).toBe("ses_1");
146
+
147
+ const byOc = store.getSessionByOcId("ses_1");
148
+ expect(byOc.ok).toBe(true);
149
+ if (byOc.ok) expect(byOc.data.id).toBe("s1");
150
+ expect(store.getSessionByOcId("nope").ok).toBe(false);
151
+
152
+ const upd = store.updateSessionStatus("s1", "stopped", 99);
153
+ expect(upd.ok).toBe(true);
154
+ if (upd.ok) expect(upd.data.status).toBe("stopped");
155
+
156
+ const got = store.getSession("s1");
157
+ expect(got.ok).toBe(true);
158
+ if (got.ok) expect(got.data.status).toBe("stopped");
159
+
160
+ // updateSessionStatus juga mencatat ke riwayat (Requirement 3.3)
161
+ const hist = store.getStatusHistory("s1");
162
+ expect(hist.ok).toBe(true);
163
+ if (hist.ok) expect(hist.data.map((h) => h.status)).toEqual(["stopped"]);
164
+ store.close();
165
+ });
166
+
167
+ test("5.1: CRUD messages — insert, get, duplikat ditolak, not found session", () => {
168
+ const store = makeMemoryStore();
169
+ expect(store.insertSession(makeSession()).ok).toBe(true);
170
+
171
+ expect(store.insertMessage(makeMessage("m1")).ok).toBe(true);
172
+ // Duplikat message_id -> gagal (tidak menimpa).
173
+ expect(store.insertMessage(makeMessage("m1")).ok).toBe(false);
174
+
175
+ // Insert ke session tak dikenal -> ditolak.
176
+ expect(
177
+ store.insertMessage({ id: "mx", sessionId: "nope", role: "user", parts: [], createdAt: 1 }).ok,
178
+ ).toBe(false);
179
+
180
+ const res = store.getMessages("s1");
181
+ expect(res.ok).toBe(true);
182
+ if (res.ok) expect(res.data.map((m) => m.id)).toEqual(["m1"]);
183
+ store.close();
184
+ });
185
+
186
+ test("5.1: CRUD prompts — kind/title ikut tersimpan; update status", () => {
187
+ const store = makeMemoryStore();
188
+ expect(store.insertSession(makeSession()).ok).toBe(true);
189
+
190
+ const permission = {
191
+ id: "pr1",
192
+ sessionId: "s1",
193
+ kind: "permission" as const,
194
+ type: "confirmation" as const,
195
+ title: "bash:ls",
196
+ options: null,
197
+ status: "pending" as const,
198
+ createdAt: 1,
199
+ resolvedAt: null,
200
+ };
201
+ expect(store.insertPrompt(permission).ok).toBe(true);
202
+
203
+ const question = {
204
+ id: "que1",
205
+ sessionId: "s1",
206
+ kind: "question" as const,
207
+ type: "menu" as const,
208
+ custom: true,
209
+ title: "Pilih mode",
210
+ // String polos = bentuk lama di DB — sengaja dipertahankan untuk
211
+ // memverifikasi normalisasi saat dibaca kembali (mapPrompt).
212
+ options: ["Build", "Plan"] as unknown as InteractivePrompt["options"],
213
+ status: "pending" as const,
214
+ createdAt: 2,
215
+ resolvedAt: null,
216
+ };
217
+ expect(store.insertPrompt(question).ok).toBe(true);
218
+
219
+ // Question multi-select (skema tool `question` opencode: multiple: true)
220
+ // beserta description per opsi — ikut roundtrip lewat kolom prompts.
221
+ const multiQuestion = {
222
+ id: "quemulti1",
223
+ sessionId: "s1",
224
+ kind: "question" as const,
225
+ type: "menu" as const,
226
+ custom: true,
227
+ multiple: true,
228
+ title: "Pilih area",
229
+ options: [
230
+ { label: "Debug", description: "investigasi bug" },
231
+ { label: "Config", description: null },
232
+ ],
233
+ // Multi-question: dua pertanyaan dalam satu request.
234
+ questions: [
235
+ {
236
+ question: "Pilih area",
237
+ header: null,
238
+ options: [
239
+ { label: "Debug", description: "investigasi bug" },
240
+ { label: "Config", description: null },
241
+ ],
242
+ multiple: true,
243
+ custom: true,
244
+ },
245
+ {
246
+ question: "Kapan mulai?",
247
+ header: null,
248
+ options: [{ label: "Sekarang", description: null }],
249
+ custom: true,
250
+ },
251
+ ],
252
+ status: "pending" as const,
253
+ createdAt: 3,
254
+ resolvedAt: null,
255
+ };
256
+ expect(store.insertPrompt(multiQuestion).ok).toBe(true);
257
+
258
+ const pending = store.listPendingPrompts("s1");
259
+ expect(pending).toHaveLength(3);
260
+
261
+ const upd = store.updatePromptStatus("pr1", "resolved", 5);
262
+ expect(upd.ok).toBe(true);
263
+ if (upd.ok) expect(upd.data.status).toBe("resolved");
264
+ expect(store.listPendingPrompts("s1")).toHaveLength(2);
265
+
266
+ const got = store.getPrompt("que1");
267
+ expect(got.ok).toBe(true);
268
+ if (got.ok) {
269
+ expect(got.data.kind).toBe("question");
270
+ expect(got.data.title).toBe("Pilih mode");
271
+ // String polos dari DB lama dinormalisasi jadi PromptOption.
272
+ expect(got.data.options).toEqual([
273
+ { label: "Build", description: null },
274
+ { label: "Plan", description: null },
275
+ ]);
276
+ // Flag custom ikut roundtrip lewat kolom `prompts.custom`.
277
+ expect(got.data.custom).toBe(true);
278
+ }
279
+ const gotMulti = store.getPrompt("quemulti1");
280
+ expect(gotMulti.ok).toBe(true);
281
+ if (gotMulti.ok) {
282
+ // Opsi berobjek + flag multiple ikut tersimpan & terbaca kembali.
283
+ expect(gotMulti.data.options).toEqual([
284
+ { label: "Debug", description: "investigasi bug" },
285
+ { label: "Config", description: null },
286
+ ]);
287
+ expect(gotMulti.data.multiple).toBe(true);
288
+ // Seluruh pertanyaan ikut roundtrip lewat kolom `questions_json`.
289
+ expect(gotMulti.data.questions).toHaveLength(2);
290
+ expect(gotMulti.data.questions?.[1]?.question).toBe("Kapan mulai?");
291
+ }
292
+ expect(store.getPrompt("nope").ok).toBe(false);
293
+ store.close();
294
+ });
295
+
296
+ test("5.1: CRUD projects — insert, get by name/path, list", () => {
297
+ const store = makeMemoryStore();
298
+ const proj = { id: "p2", name: "proj2", path: "/sandbox/proj2", createdAt: 2 };
299
+ expect(store.insertProject(proj).ok).toBe(true);
300
+
301
+ const byName = store.getProjectByName("proj2");
302
+ expect(byName.ok).toBe(true);
303
+ if (byName.ok) expect(byName.data.path).toBe("/sandbox/proj2");
304
+
305
+ expect(store.getProjectByPath("/sandbox/proj2").ok).toBe(true);
306
+ expect(store.listProjects()).toHaveLength(2);
307
+ store.close();
308
+ });
309
+
310
+ test("updateProjectInstructions: simpan, hapus (null), id tak dikenal -> PROJECT_NOT_FOUND", () => {
311
+ const store = makeMemoryStore();
312
+ const proj = { id: "p3", name: "proj3", path: "/sandbox/proj3", createdAt: 3 };
313
+ expect(store.insertProject(proj).ok).toBe(true);
314
+
315
+ const fresh = store.getProjectById("p3");
316
+ expect(fresh.ok && fresh.data.instructions).toBeNull();
317
+
318
+ const saved = store.updateProjectInstructions("p3", "Use tabs.");
319
+ expect(saved.ok && saved.data.instructions).toBe("Use tabs.");
320
+
321
+ const cleared = store.updateProjectInstructions("p3", null);
322
+ expect(cleared.ok && cleared.data.instructions).toBeNull();
323
+
324
+ expect(store.updateProjectInstructions("nope", "x").ok).toBe(false);
325
+ store.close();
326
+ });
327
+
328
+ test("deleteProject: menolak selama masih ada Session, sukses setelah Session dihapus", () => {
329
+ const store = makeMemoryStore();
330
+ expect(store.insertSession(makeSession("s1")).ok).toBe(true);
331
+
332
+ expect(store.listProjectSessions("p1").map((s) => s.id)).toEqual(["s1"]);
333
+
334
+ // Session harus dibersihkan lewat SessionManager lebih dulu — jalur ini
335
+ // menolak agar sesi remote opencode tidak jadi yatim.
336
+ const blocked = store.deleteProject("p1");
337
+ expect(blocked.ok).toBe(false);
338
+ if (!blocked.ok) expect(blocked.error).toBe("PROJECT_HAS_SESSIONS");
339
+ expect(store.getProjectById("p1").ok).toBe(true);
340
+
341
+ expect(store.deleteSession("s1").ok).toBe(true);
342
+ expect(store.deleteProject("p1").ok).toBe(true);
343
+ expect(store.getProjectById("p1").ok).toBe(false);
344
+ expect(store.listProjects()).toHaveLength(0);
345
+ store.close();
346
+ });
347
+
348
+ test("deleteProject: id tak dikenal -> PROJECT_NOT_FOUND", () => {
349
+ const store = makeMemoryStore();
350
+ const res = store.deleteProject("tidak-ada");
351
+ expect(res.ok).toBe(false);
352
+ if (!res.ok) expect(res.error).toBe("PROJECT_NOT_FOUND");
353
+ expect(store.listProjectSessions("tidak-ada")).toEqual([]);
354
+ store.close();
355
+ });
356
+
357
+ test("migrasi: DB lama (skema PTY) dibuka dengan store baru -> kolom & tabel ditambah", () => {
358
+ const dir = mkdtempSync(path.join(tmpdir(), "kcg-mig-"));
359
+ const dbPath = path.join(dir, "legacy.sqlite");
360
+
361
+ // Simulasikan DB lama: skema versi PTY (tanpa oc_session_id, kind, title, messages).
362
+ writeFileSync(dbPath, "");
363
+ const { Database } = require("bun:sqlite") as typeof import("bun:sqlite");
364
+ const db = new Database(dbPath);
365
+ db.exec(`
366
+ CREATE TABLE projects (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, path TEXT NOT NULL UNIQUE, created_at INTEGER NOT NULL);
367
+ CREATE TABLE sessions (id TEXT PRIMARY KEY, project_id TEXT NOT NULL, agent_type TEXT NOT NULL, cwd TEXT NOT NULL, status TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);
368
+ CREATE TABLE session_status_history (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, status TEXT NOT NULL, changed_at INTEGER NOT NULL);
369
+ CREATE TABLE output_stream (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, seq INTEGER NOT NULL, chunk TEXT NOT NULL, created_at INTEGER NOT NULL);
370
+ CREATE TABLE prompts (id TEXT PRIMARY KEY, session_id TEXT NOT NULL, type TEXT NOT NULL, options_json TEXT, status TEXT NOT NULL, created_at INTEGER NOT NULL, resolved_at INTEGER);
371
+ INSERT INTO projects (id, name, path, created_at) VALUES ('p1', 'old', '/sandbox/old', 1);
372
+ INSERT INTO sessions (id, project_id, agent_type, cwd, status, created_at, updated_at) VALUES ('s1', 'p1', 'opencode', '/sandbox/old', 'running', 1, 1);
373
+ INSERT INTO output_stream (session_id, seq, chunk, created_at) VALUES ('s1', 1, 'old chunk', 1);
374
+ `);
375
+ db.close();
376
+
377
+ // Buka dengan store baru: migrasi menambah kolom + tabel messages.
378
+ const store = openSessionStore(dbPath);
379
+ const sess = store.getSession("s1");
380
+ expect(sess.ok).toBe(true);
381
+ if (sess.ok) expect(sess.data.ocSessionId).toBeNull();
382
+
383
+ // Data lama output_stream tetap utuh (tidak dihapus migrasi).
384
+ const legacy = dbExec(dbPath, "SELECT chunk FROM output_stream WHERE session_id='s1'");
385
+ expect(legacy).toEqual([{ chunk: "old chunk" }]);
386
+
387
+ // Tabel & kolom baru bisa dipakai.
388
+ expect(store.insertSession(makeSession("s2")).ok).toBe(true);
389
+ expect(store.insertMessage(makeMessage("m1")).ok).toBe(true);
390
+ // Kolom projects.instructions ditambahkan; Project lama tanpa instruksi.
391
+ const oldProj = store.getProjectById("p1");
392
+ expect(oldProj.ok && oldProj.data.instructions).toBeNull();
393
+ expect(store.updateProjectInstructions("p1", "hi").ok).toBe(true);
394
+ store.close();
395
+ rmSync(dir, { recursive: true, force: true });
396
+ });
397
+
398
+ function dbExec(dbPath: string, sql: string): Record<string, unknown>[] {
399
+ const { Database } = require("bun:sqlite") as typeof import("bun:sqlite");
400
+ const db = new Database(dbPath);
401
+ try {
402
+ return db.query(sql).all() as Record<string, unknown>[];
403
+ } finally {
404
+ db.close();
405
+ }
406
+ }
407
+
408
+ // ---- Integration: kegagalan tulis & persistensi setelah restart ----
409
+ test("4.4: kegagalan tulis tidak menghapus data lama (simulasi db tertutup)", () => {
410
+ const dir = mkdtempSync(path.join(tmpdir(), "kcg-db2-"));
411
+ const dbPath = path.join(dir, "kcg.sqlite");
412
+
413
+ const store = openSessionStore(dbPath);
414
+ seedProject(store);
415
+ expect(store.insertSession(makeSession()).ok).toBe(true);
416
+ expect(store.insertMessage(makeMessage("m1")).ok).toBe(true);
417
+ expect(store.insertMessage(makeMessage("m2", "assistant")).ok).toBe(true);
418
+ expect(store.insertStatusHistory("s1", "running", 1).ok).toBe(true);
419
+
420
+ // Simulasi kegagalan tulis: tutup koneksi lalu coba tulis lagi.
421
+ store.close();
422
+ const failed = store.insertMessage(makeMessage("m3"));
423
+ expect(failed.ok).toBe(false);
424
+
425
+ // Data lama tetap utuh setelah dibuka ulang (Requirement 3.6).
426
+ const store2 = openSessionStore(dbPath);
427
+ const messages = store2.getMessages("s1");
428
+ expect(messages.ok).toBe(true);
429
+ if (messages.ok) expect(messages.data.map((m) => m.id)).toEqual(["m1", "m2"]);
430
+ const hist = store2.getStatusHistory("s1");
431
+ expect(hist.ok).toBe(true);
432
+ if (hist.ok) expect(hist.data.map((h) => h.status)).toEqual(["running"]);
433
+ store2.close();
434
+ rmSync(dir, { recursive: true, force: true });
435
+ });
436
+
437
+ test("4.4: persistensi setelah restart server (file sqlite yang sama)", () => {
438
+ const dir = mkdtempSync(path.join(tmpdir(), "kcg-db2-"));
439
+ const dbPath = path.join(dir, "kcg.sqlite");
440
+
441
+ const store1 = openSessionStore(dbPath);
442
+ seedProject(store1);
443
+ expect(store1.insertSession(makeSession()).ok).toBe(true);
444
+ store1.insertMessage(makeMessage("m1"));
445
+ store1.insertMessage(makeMessage("m2", "assistant"));
446
+ store1.insertStatusHistory("s1", "running", 1);
447
+ store1.insertStatusHistory("s1", "crashed", 2);
448
+ store1.close();
449
+
450
+ const store2 = openSessionStore(dbPath);
451
+ const messages = store2.getMessages("s1");
452
+ expect(messages.ok).toBe(true);
453
+ if (messages.ok) expect(messages.data.map((m) => m.id)).toEqual(["m1", "m2"]);
454
+ const hist = store2.getStatusHistory("s1");
455
+ expect(hist.ok).toBe(true);
456
+ if (hist.ok) expect(hist.data.map((h) => h.status)).toEqual(["running", "crashed"]);
457
+ store2.close();
458
+ rmSync(dir, { recursive: true, force: true });
459
+ });
460
+
461
+ test("migrasi onboarded_at: instalasi lama (sudah punya project/kunci) ditandai selesai", () => {
462
+ const { Database } = require("bun:sqlite") as typeof import("bun:sqlite");
463
+ const { runMigrations } = require("../db/schema") as typeof import("../db/schema");
464
+ // DB lama: skema tanpa kolom onboarded_at + sudah ada nickname.
465
+ const old = new Database(":memory:");
466
+ old.exec(`CREATE TABLE auth_settings (
467
+ id INTEGER PRIMARY KEY CHECK (id = 1), lock_kind TEXT, lock_hash TEXT, nickname TEXT,
468
+ avatar_mime TEXT, avatar BLOB, avatar_version INTEGER NOT NULL DEFAULT 0,
469
+ auto_lock_minutes INTEGER NOT NULL DEFAULT 15, updated_at INTEGER NOT NULL)`);
470
+ old.exec("INSERT INTO auth_settings (id, nickname, updated_at) VALUES (1, 'Irsyad', 42)");
471
+ runMigrations(old);
472
+ const row = old.query("SELECT onboarded_at FROM auth_settings WHERE id = 1").get() as {
473
+ onboarded_at: number | null;
474
+ };
475
+ expect(row.onboarded_at).toBe(42);
476
+
477
+ // DB baru (belum ada apa-apa) -> tetap butuh onboarding.
478
+ const fresh = new Database(":memory:");
479
+ runMigrations(fresh);
480
+ fresh.exec("INSERT INTO auth_settings (id, updated_at) VALUES (1, 1)");
481
+ const r2 = fresh.query("SELECT onboarded_at FROM auth_settings WHERE id = 1").get() as {
482
+ onboarded_at: number | null;
483
+ };
484
+ expect(r2.onboarded_at).toBeNull();
485
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Alamat bind server (src/server/host.ts): default loopback, bisa di-override.
3
+ */
4
+ import { expect, test } from "bun:test";
5
+ import { DEFAULT_HOSTNAME, resolveHostname } from "../host";
6
+
7
+ test("default hostname loopback 127.0.0.1", () => {
8
+ expect(DEFAULT_HOSTNAME).toBe("127.0.0.1");
9
+ expect(resolveHostname({})).toBe("127.0.0.1");
10
+ });
11
+
12
+ test("KCG_HOST meng-override hostname", () => {
13
+ expect(resolveHostname({ KCG_HOST: "0.0.0.0" })).toBe("0.0.0.0");
14
+ });
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { parseTunnelApiUrl, resolveTunnelConfig } from "../services/tunnel-config";
3
+
4
+ describe("tunnel API URL", () => {
5
+ test("parseTunnelApiUrl normalises and rejects unsafe values", () => {
6
+ expect(parseTunnelApiUrl(undefined)).toEqual({ ok: true, url: null });
7
+ expect(parseTunnelApiUrl("https://rp.example.com/")).toEqual({
8
+ ok: true,
9
+ url: "https://rp.example.com",
10
+ });
11
+ expect(parseTunnelApiUrl("https://rp.example.com/base/")).toEqual({
12
+ ok: true,
13
+ url: "https://rp.example.com/base",
14
+ });
15
+ for (const bad of ["ftp://x", "not a url", "https://u:p@x.com", "https://x.com/?a=1", 42]) {
16
+ expect(parseTunnelApiUrl(bad).ok).toBe(false);
17
+ }
18
+ });
19
+
20
+ test("env wins over the baked-in URL; neither -> not configured", () => {
21
+ expect(resolveTunnelConfig("", "")).toEqual({ apiUrl: null });
22
+ expect(resolveTunnelConfig("", "https://baked.example.com")).toEqual({
23
+ apiUrl: "https://baked.example.com",
24
+ });
25
+ expect(resolveTunnelConfig("https://env.example.com/", "https://baked.example.com")).toEqual({
26
+ apiUrl: "https://env.example.com",
27
+ });
28
+ // Invalid env falls back to the baked-in value instead of breaking.
29
+ expect(resolveTunnelConfig("ftp://nope", "https://baked.example.com")).toEqual({
30
+ apiUrl: "https://baked.example.com",
31
+ });
32
+ });
33
+
34
+ test("the repo never ships a baked-in URL (stamped only at pack time)", async () => {
35
+ const { BUILD_TUNNEL_API_URL } = await import("../../build-config");
36
+ expect(BUILD_TUNNEL_API_URL).toBe("");
37
+ });
38
+ });