kcg-code 0.0.0-stage → 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.
- package/README.md +230 -2
- package/bin/kcgcode.ts +13 -0
- package/bun-env.d.ts +19 -0
- package/kcg-code.config.example.json +3 -0
- package/package.json +84 -4
- package/public/logo.svg +1 -0
- package/public/manifest.json +18 -0
- package/public/sw.js +49 -0
- package/src/App.tsx +39 -0
- package/src/__tests__/manifest.test.ts +61 -0
- package/src/build-config.ts +9 -0
- package/src/cli/__tests__/daemon.test.ts +84 -0
- package/src/cli/__tests__/dashboard.test.ts +224 -0
- package/src/cli/__tests__/keep-awake.test.ts +44 -0
- package/src/cli/__tests__/port.test.ts +86 -0
- package/src/cli/args.ts +186 -0
- package/src/cli/attach.ts +154 -0
- package/src/cli/commands.ts +327 -0
- package/src/cli/daemon.ts +275 -0
- package/src/cli/dashboard.tsx +1060 -0
- package/src/cli/index.ts +47 -0
- package/src/cli/keep-awake.ts +148 -0
- package/src/cli/port.ts +92 -0
- package/src/cli/theme.ts +100 -0
- package/src/components/auth/AuthGate.tsx +182 -0
- package/src/components/auth/AvatarEditor.tsx +236 -0
- package/src/components/auth/LockScreen.tsx +714 -0
- package/src/components/auth/ProfileAvatar.tsx +60 -0
- package/src/components/auth/WelcomeScreen.tsx +672 -0
- package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
- package/src/components/auth/avatar-icons.tsx +114 -0
- package/src/components/layout/AppShell.tsx +147 -0
- package/src/components/layout/AppSidebar.tsx +419 -0
- package/src/components/layout/SearchDialog.tsx +189 -0
- package/src/components/layout/SidebarAccount.tsx +111 -0
- package/src/components/layout/SidebarSessionMenu.tsx +163 -0
- package/src/components/layout/ThemeToggle.tsx +67 -0
- package/src/components/projects/FolderBrowser.tsx +142 -0
- package/src/components/projects/HomeChat.tsx +287 -0
- package/src/components/projects/HomeRecentSessions.tsx +90 -0
- package/src/components/projects/NewProjectDialog.tsx +249 -0
- package/src/components/projects/ProjectInstructions.tsx +218 -0
- package/src/components/projects/ProjectRow.tsx +58 -0
- package/src/components/projects/ProjectSidePanel.tsx +514 -0
- package/src/components/sessions/AgentLogo.tsx +42 -0
- package/src/components/sessions/AgentModeList.tsx +265 -0
- package/src/components/sessions/AgentPicker.tsx +132 -0
- package/src/components/sessions/AssistantTurn.tsx +145 -0
- package/src/components/sessions/AttachmentTile.tsx +161 -0
- package/src/components/sessions/CodeBlock.tsx +107 -0
- package/src/components/sessions/ComposerActionSheet.tsx +96 -0
- package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
- package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +92 -0
- package/src/components/sessions/ContextPanel.tsx +661 -0
- package/src/components/sessions/DropPlate.tsx +165 -0
- package/src/components/sessions/FileDropOverlay.tsx +232 -0
- package/src/components/sessions/FileMentionDropdown.tsx +47 -0
- package/src/components/sessions/ImageLightbox.tsx +173 -0
- package/src/components/sessions/MarkdownContent.tsx +187 -0
- package/src/components/sessions/Mascot.tsx +47 -0
- package/src/components/sessions/MessageActions.tsx +60 -0
- package/src/components/sessions/MessageRail.tsx +121 -0
- package/src/components/sessions/ModelPicker.tsx +247 -0
- package/src/components/sessions/ModelSearchList.tsx +231 -0
- package/src/components/sessions/NewSessionDialog.tsx +172 -0
- package/src/components/sessions/PendingAttachments.tsx +127 -0
- package/src/components/sessions/PromptCard.tsx +1085 -0
- package/src/components/sessions/PromptPanel.tsx +112 -0
- package/src/components/sessions/SessionComposer.tsx +641 -0
- package/src/components/sessions/SessionHeader.tsx +176 -0
- package/src/components/sessions/SessionList.tsx +503 -0
- package/src/components/sessions/SessionRow.tsx +237 -0
- package/src/components/sessions/SessionTimeline.tsx +275 -0
- package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
- package/src/components/sessions/SessionView.tsx +240 -0
- package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
- package/src/components/sessions/UserMessage.tsx +177 -0
- package/src/components/sessions/__tests__/ContextPanel.test.tsx +256 -0
- package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
- package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
- package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
- package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
- package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
- package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
- package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
- package/src/components/settings/AboutGroup.tsx +170 -0
- package/src/components/settings/AccountGroup.tsx +122 -0
- package/src/components/settings/DevicesGroup.tsx +151 -0
- package/src/components/settings/LhrRemoteAccess.tsx +257 -0
- package/src/components/settings/PasskeysGroup.tsx +551 -0
- package/src/components/settings/RemoteAccessGroup.tsx +1201 -0
- package/src/components/settings/SecurityGroup.tsx +566 -0
- package/src/components/settings/WorkspaceGroup.tsx +370 -0
- package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
- package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
- package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
- package/src/components/settings/prefs.tsx +333 -0
- package/src/components/skills/InstallNotifications.tsx +326 -0
- package/src/components/skills/InstallSkillDialog.tsx +390 -0
- package/src/components/skills/SkillCards.tsx +254 -0
- package/src/components/skills/SkillsProjectPicker.tsx +178 -0
- package/src/components/ui/adw-dialog.tsx +114 -0
- package/src/components/ui/adw-toggle-group.tsx +75 -0
- package/src/components/ui/alert-dialog.tsx +179 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/bubble.tsx +125 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/dialog.tsx +142 -0
- package/src/components/ui/dropdown-menu.tsx +225 -0
- package/src/components/ui/empty.tsx +94 -0
- package/src/components/ui/field.tsx +234 -0
- package/src/components/ui/input-group.tsx +161 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/message-scroller.tsx +131 -0
- package/src/components/ui/message.tsx +85 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/search-entry.tsx +70 -0
- package/src/components/ui/select.tsx +172 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +699 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/config.ts +114 -0
- package/src/db.ts +146 -0
- package/src/frontend.tsx +51 -0
- package/src/hooks/__tests__/useMention.test.ts +79 -0
- package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
- package/src/hooks/__tests__/useRouter.test.ts +17 -0
- package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
- package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
- package/src/hooks/__tests__/useTheme.test.ts +118 -0
- package/src/hooks/use-mobile.ts +16 -0
- package/src/hooks/useAgentPicker.ts +101 -0
- package/src/hooks/useMediaQuery.ts +49 -0
- package/src/hooks/useMention.ts +236 -0
- package/src/hooks/useProjects.ts +88 -0
- package/src/hooks/useResyncOnReconnect.ts +28 -0
- package/src/hooks/useRouter.ts +92 -0
- package/src/hooks/useSessionChat.ts +427 -0
- package/src/hooks/useSessionContext.ts +106 -0
- package/src/hooks/useSessionList.ts +327 -0
- package/src/hooks/useSidebarData.ts +108 -0
- package/src/hooks/useSmoothText.ts +122 -0
- package/src/hooks/useTheme.ts +164 -0
- package/src/hooks/useWebSocket.ts +196 -0
- package/src/index.css +27 -0
- package/src/index.html +14 -0
- package/src/index.ts +49 -0
- package/src/lib/__tests__/attachments.test.ts +58 -0
- package/src/lib/__tests__/avatar-presets.test.ts +25 -0
- package/src/lib/__tests__/collapsible.test.ts +101 -0
- package/src/lib/__tests__/composer.test.ts +118 -0
- package/src/lib/__tests__/highlight.test.ts +42 -0
- package/src/lib/__tests__/home-chat.test.ts +64 -0
- package/src/lib/__tests__/lock-screen.test.ts +64 -0
- package/src/lib/__tests__/model-picker.test.ts +205 -0
- package/src/lib/__tests__/onboarding.test.ts +41 -0
- package/src/lib/__tests__/permission-info.test.ts +61 -0
- package/src/lib/__tests__/project-extensions.test.ts +83 -0
- package/src/lib/__tests__/project-overview.test.ts +188 -0
- package/src/lib/__tests__/references.test.ts +121 -0
- package/src/lib/__tests__/routes.test.ts +134 -0
- package/src/lib/__tests__/session-selection.test.ts +41 -0
- package/src/lib/__tests__/session-summary.test.ts +115 -0
- package/src/lib/__tests__/session-title.test.ts +58 -0
- package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
- package/src/lib/__tests__/skill-installs.test.ts +135 -0
- package/src/lib/__tests__/skills-catalog.test.ts +124 -0
- package/src/lib/__tests__/tone.test.ts +37 -0
- package/src/lib/__tests__/turns-delta.test.ts +55 -0
- package/src/lib/agent-types.ts +49 -0
- package/src/lib/api.ts +306 -0
- package/src/lib/attachments.ts +203 -0
- package/src/lib/auth.ts +92 -0
- package/src/lib/avatar-presets.ts +31 -0
- package/src/lib/collapsible.ts +75 -0
- package/src/lib/composer.ts +89 -0
- package/src/lib/data-events.ts +17 -0
- package/src/lib/headlines.ts +30 -0
- package/src/lib/highlight.ts +186 -0
- package/src/lib/layout.ts +20 -0
- package/src/lib/lock-screen.ts +54 -0
- package/src/lib/model-picker.ts +133 -0
- package/src/lib/onboarding.ts +38 -0
- package/src/lib/passkeys.ts +113 -0
- package/src/lib/pending-prompt.ts +53 -0
- package/src/lib/permission-info.ts +101 -0
- package/src/lib/project-extensions.ts +136 -0
- package/src/lib/project-overview.ts +123 -0
- package/src/lib/prompts.ts +87 -0
- package/src/lib/references.ts +106 -0
- package/src/lib/routes.ts +124 -0
- package/src/lib/session-handoff.ts +25 -0
- package/src/lib/session-selection.ts +40 -0
- package/src/lib/session-status.ts +45 -0
- package/src/lib/session-summary.ts +68 -0
- package/src/lib/session-title.ts +63 -0
- package/src/lib/sidebar-groups.ts +78 -0
- package/src/lib/skill-installs.ts +362 -0
- package/src/lib/skills-catalog.ts +138 -0
- package/src/lib/time.ts +22 -0
- package/src/lib/tone.ts +80 -0
- package/src/lib/turns.ts +320 -0
- package/src/lib/utils.ts +6 -0
- package/src/logo.svg +31 -0
- package/src/pages/projects/ProjectDetailPage.tsx +131 -0
- package/src/pages/projects/ProjectsPage.tsx +97 -0
- package/src/pages/sessions/SessionPage.tsx +122 -0
- package/src/pages/settings/SettingsPage.tsx +366 -0
- package/src/pages/settings/__tests__/SettingsPage.test.ts +16 -0
- package/src/pages/skills/SkillsPage.tsx +745 -0
- package/src/paths.ts +121 -0
- package/src/runtime.ts +24 -0
- package/src/server/__tests__/app.test.ts +1044 -0
- package/src/server/__tests__/auth.test.ts +370 -0
- package/src/server/__tests__/config.test.ts +101 -0
- package/src/server/__tests__/db.test.ts +485 -0
- package/src/server/__tests__/host.test.ts +14 -0
- package/src/server/__tests__/tunnel-config.test.ts +38 -0
- package/src/server/app.ts +427 -0
- package/src/server/db/auth.ts +311 -0
- package/src/server/db/messages.ts +50 -0
- package/src/server/db/projects.ts +106 -0
- package/src/server/db/prompts.ts +78 -0
- package/src/server/db/rows.ts +150 -0
- package/src/server/db/schema.ts +221 -0
- package/src/server/db/sessions.ts +239 -0
- package/src/server/db/tunnel.ts +124 -0
- package/src/server/host.ts +12 -0
- package/src/server/result.ts +15 -0
- package/src/server/routes/auth-guard.ts +156 -0
- package/src/server/routes/auth.routes.ts +287 -0
- package/src/server/routes/helpers.ts +165 -0
- package/src/server/routes/meta.routes.ts +24 -0
- package/src/server/routes/passkeys.routes.ts +161 -0
- package/src/server/routes/projects.routes.ts +167 -0
- package/src/server/routes/sessions.routes.ts +197 -0
- package/src/server/routes/skills.routes.ts +136 -0
- package/src/server/routes/tunnel.routes.ts +132 -0
- package/src/server/routes/types.ts +35 -0
- package/src/server/routes/uploads.routes.ts +54 -0
- package/src/server/routes/workspace.routes.ts +127 -0
- package/src/server/services/__tests__/app-info.test.ts +18 -0
- package/src/server/services/__tests__/attachments.test.ts +159 -0
- package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
- package/src/server/services/__tests__/auth.test.ts +341 -0
- package/src/server/services/__tests__/claude-code.test.ts +609 -0
- package/src/server/services/__tests__/claude-mcp.test.ts +90 -0
- package/src/server/services/__tests__/claude-skills.test.ts +63 -0
- package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
- package/src/server/services/__tests__/git-info.test.ts +74 -0
- package/src/server/services/__tests__/opencode.test.ts +320 -0
- package/src/server/services/__tests__/project-manager.test.ts +190 -0
- package/src/server/services/__tests__/sandbox.test.ts +141 -0
- package/src/server/services/__tests__/session-manager.test.ts +3509 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +245 -0
- package/src/server/services/__tests__/skills-registry.test.ts +365 -0
- package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
- package/src/server/services/__tests__/tunnel.test.ts +367 -0
- package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
- package/src/server/services/__tests__/workspace.test.ts +249 -0
- package/src/server/services/app-info.ts +29 -0
- package/src/server/services/attachments.ts +193 -0
- package/src/server/services/auth-passkeys.ts +459 -0
- package/src/server/services/auth.ts +633 -0
- package/src/server/services/claude-code-client.ts +1093 -0
- package/src/server/services/claude-code-translate.ts +370 -0
- package/src/server/services/claude-mcp.ts +122 -0
- package/src/server/services/claude-skills.ts +102 -0
- package/src/server/services/frpc-installer.ts +183 -0
- package/src/server/services/git-info.ts +87 -0
- package/src/server/services/opencode-client.ts +714 -0
- package/src/server/services/opencode-server.ts +319 -0
- package/src/server/services/opencode-sse.ts +82 -0
- package/src/server/services/project-manager.ts +147 -0
- package/src/server/services/sandbox.ts +83 -0
- package/src/server/services/session-events.ts +256 -0
- package/src/server/services/session-manager.ts +1439 -0
- package/src/server/services/skill-install-jobs.ts +275 -0
- package/src/server/services/skills-registry.ts +480 -0
- package/src/server/services/tunnel-api.ts +207 -0
- package/src/server/services/tunnel-config.ts +45 -0
- package/src/server/services/tunnel-lhr.ts +387 -0
- package/src/server/services/tunnel.ts +633 -0
- package/src/server/services/turn-stream.ts +339 -0
- package/src/server/services/websocket-gateway.ts +374 -0
- package/src/server/services/workspace.ts +223 -0
- package/src/server/services/ws-transport.ts +121 -0
- package/src/types/context.ts +69 -0
- package/src/types/index.ts +13 -0
- package/src/types/message.ts +58 -0
- package/src/types/project.ts +16 -0
- package/src/types/prompt.ts +118 -0
- package/src/types/session.ts +52 -0
- package/src/types/turns.ts +47 -0
- package/src/ws-protocol.ts +93 -0
- package/styles/globals.css +311 -0
- package/tsconfig.json +37 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit test placeholder composer (src/lib/composer.ts).
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, test } from "bun:test";
|
|
5
|
+
import { composerPlaceholder, extractMentionedFiles, splitComposerMentions } from "../composer";
|
|
6
|
+
|
|
7
|
+
describe("composerPlaceholder", () => {
|
|
8
|
+
test("ada lampiran: ajakan singkat (tidak wrap di ruang yang menyempit)", () => {
|
|
9
|
+
for (const compact of [false, true]) {
|
|
10
|
+
expect(
|
|
11
|
+
composerPlaceholder({ busy: false, canInput: true, compact, hasAttachments: true }),
|
|
12
|
+
).toBe("Add a message…");
|
|
13
|
+
}
|
|
14
|
+
// Status sibuk / Session mati tetap lebih penting.
|
|
15
|
+
expect(
|
|
16
|
+
composerPlaceholder({ busy: true, canInput: false, compact: false, hasAttachments: true }),
|
|
17
|
+
).toBe("Model is responding…");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("wide screen: full hint when input is ready", () => {
|
|
21
|
+
expect(composerPlaceholder({ busy: false, canInput: true, compact: false })).toBe(
|
|
22
|
+
"Type a message… type @ for file references, or drop files here",
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("narrow screen: short text to avoid truncation", () => {
|
|
27
|
+
expect(composerPlaceholder({ busy: false, canInput: true, compact: true })).toBe(
|
|
28
|
+
"Type a message…",
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("model responding: status outranks the typing hint", () => {
|
|
33
|
+
expect(composerPlaceholder({ busy: true, canInput: false, compact: false })).toBe(
|
|
34
|
+
"Model is responding…",
|
|
35
|
+
);
|
|
36
|
+
expect(composerPlaceholder({ busy: true, canInput: false, compact: true })).toBe("Responding…");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("session down: same text at both sizes (already short)", () => {
|
|
40
|
+
expect(composerPlaceholder({ busy: false, canInput: false, compact: false })).toBe(
|
|
41
|
+
"Session is not active",
|
|
42
|
+
);
|
|
43
|
+
expect(composerPlaceholder({ busy: false, canInput: false, compact: true })).toBe(
|
|
44
|
+
"Session is not active",
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("busy is checked before canInput", () => {
|
|
49
|
+
// Unusual combination (busy + canInput) still reports the responding status.
|
|
50
|
+
expect(composerPlaceholder({ busy: true, canInput: true, compact: false })).toBe(
|
|
51
|
+
"Model is responding…",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("extractMentionedFiles", () => {
|
|
57
|
+
test("path yang pernah disarankan autocomplete ikut dikirim", () => {
|
|
58
|
+
const known = new Set(["src/index.ts"]);
|
|
59
|
+
expect(extractMentionedFiles("fix @src/index.ts", known)).toEqual(["src/index.ts"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("path memuat / ikut dikirim walau tidak pernah disarankan", () => {
|
|
63
|
+
expect(extractMentionedFiles("lihat @a/b/c.ts dulu", new Set())).toEqual(["a/b/c.ts"]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("kata biasa dengan @ (mis. @user) TIDAK dikirim tanpa slash & tak dikenal", () => {
|
|
67
|
+
expect(extractMentionedFiles("halo @user apa kabar", new Set())).toEqual([]);
|
|
68
|
+
// Kecuali pernah disarankan autocomplete — baru dianggap file sungguhan.
|
|
69
|
+
expect(extractMentionedFiles("halo @user", new Set(["user"]))).toEqual(["user"]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("urutan & duplikat kemunculan dipertahankan", () => {
|
|
73
|
+
const known = new Set(["a.ts"]);
|
|
74
|
+
expect(extractMentionedFiles("@a.ts lalu @a.ts lagi", known)).toEqual(["a.ts", "a.ts"]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("tanpa teks atau tanpa @ -> kosong", () => {
|
|
78
|
+
expect(extractMentionedFiles("", new Set())).toEqual([]);
|
|
79
|
+
expect(extractMentionedFiles("tidak ada mention", new Set(["x.ts"]))).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("splitComposerMentions", () => {
|
|
84
|
+
const known = new Set(["skills-lock.json"]);
|
|
85
|
+
const join = (segs: { text: string }[]) => segs.map((x) => x.text).join("");
|
|
86
|
+
|
|
87
|
+
test("memecah teks + mention yang dikenal", () => {
|
|
88
|
+
const text = "lalu apa yang ada pada file @skills-lock.json ya";
|
|
89
|
+
const segs = splitComposerMentions(text, known);
|
|
90
|
+
expect(segs).toEqual([
|
|
91
|
+
{ kind: "text", text: "lalu apa yang ada pada file " },
|
|
92
|
+
{ kind: "mention", text: "@skills-lock.json", path: "skills-lock.json" },
|
|
93
|
+
{ kind: "text", text: " ya" },
|
|
94
|
+
]);
|
|
95
|
+
expect(join(segs)).toBe(text);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("path dengan / dianggap file walau belum disarankan", () => {
|
|
99
|
+
const segs = splitComposerMentions("@src/App.tsx dan @src/lib/x.ts", new Set());
|
|
100
|
+
expect(segs.filter((x) => x.kind === "mention").map((x) => x.text)).toEqual([
|
|
101
|
+
"@src/App.tsx",
|
|
102
|
+
"@src/lib/x.ts",
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("@kata biasa & email tidak disorot", () => {
|
|
107
|
+
const text = "halo @budi, email saya a@b.com";
|
|
108
|
+
const segs = splitComposerMentions(text, known);
|
|
109
|
+
expect(segs.every((x) => x.kind === "text")).toBe(true);
|
|
110
|
+
expect(join(segs)).toBe(text);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("gabungan potongan selalu identik dengan input", () => {
|
|
114
|
+
for (const text of ["", "@", "@skills-lock.json", "\n@src/a.ts\n\nteks", " @x/y "]) {
|
|
115
|
+
expect(join(splitComposerMentions(text, known))).toBe(text);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { highlightCode, resolveLang } from "../highlight";
|
|
3
|
+
|
|
4
|
+
describe("resolveLang", () => {
|
|
5
|
+
test("nama kanonis, alias, prefix language-, huruf besar", () => {
|
|
6
|
+
expect(resolveLang("typescript")).toBe("typescript");
|
|
7
|
+
expect(resolveLang("ts")).toBe("typescript");
|
|
8
|
+
expect(resolveLang("language-tsx")).toBe("tsx");
|
|
9
|
+
expect(resolveLang("Bash")).toBe("shellscript");
|
|
10
|
+
expect(resolveLang("yml")).toBe("yaml");
|
|
11
|
+
expect(resolveLang("Dockerfile")).toBe("docker");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("tak dikenal / kosong -> null (teks polos)", () => {
|
|
15
|
+
expect(resolveLang("brainfuck")).toBeNull();
|
|
16
|
+
expect(resolveLang("")).toBeNull();
|
|
17
|
+
expect(resolveLang(null)).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("highlightCode", () => {
|
|
22
|
+
test("token menyatu kembali persis jadi kode asli", async () => {
|
|
23
|
+
for (const code of ["const a = 1;\nlet b = 2", "x\n", "a\n\n b\t c\n", ""]) {
|
|
24
|
+
const lines = await highlightCode(code, "typescript");
|
|
25
|
+
expect(lines).not.toBeNull();
|
|
26
|
+
const joined = (lines ?? []).map((l) => l.map((t) => t.content).join("")).join("\n");
|
|
27
|
+
expect(joined).toBe(code);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("token membawa warna light & dark sebagai CSS variable", async () => {
|
|
32
|
+
const lines = await highlightCode("const x = 1", "typescript");
|
|
33
|
+
const kw = lines?.[0]?.find((t) => t.content === "const");
|
|
34
|
+
expect(kw?.style["--shiki-light"]).toMatch(/^#[0-9a-f]{6,8}$/i);
|
|
35
|
+
expect(kw?.style["--shiki-dark"]).toMatch(/^#[0-9a-f]{6,8}$/i);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("bahasa lain dimuat lazy (python, bash)", async () => {
|
|
39
|
+
expect(await highlightCode("def f():\n return 1", "python")).not.toBeNull();
|
|
40
|
+
expect(await highlightCode("echo $HOME | wc -l", "shellscript")).not.toBeNull();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit test headline acak homepage (lib/headlines.ts) dan prompt tertunda
|
|
3
|
+
* (lib/pending-prompt.ts) — logika murni.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { HOME_HEADLINES, pickHeadline } from "../headlines";
|
|
7
|
+
import {
|
|
8
|
+
type PendingStore,
|
|
9
|
+
peekPendingPrompt,
|
|
10
|
+
setPendingPrompt,
|
|
11
|
+
takePendingPrompt,
|
|
12
|
+
} from "../pending-prompt";
|
|
13
|
+
|
|
14
|
+
describe("pickHeadline", () => {
|
|
15
|
+
test("memilih sesuai nilai random (batas bawah & atas)", () => {
|
|
16
|
+
const list = ["a", "b", "c"];
|
|
17
|
+
expect(pickHeadline(list, () => 0)).toBe("a");
|
|
18
|
+
expect(pickHeadline(list, () => 0.5)).toBe("b");
|
|
19
|
+
expect(pickHeadline(list, () => 0.9999)).toBe("c");
|
|
20
|
+
// random() === 1 tidak boleh keluar dari indeks.
|
|
21
|
+
expect(pickHeadline(list, () => 1)).toBe("c");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("default memakai HOME_HEADLINES; list kosong -> string kosong", () => {
|
|
25
|
+
expect(HOME_HEADLINES).toContain(pickHeadline());
|
|
26
|
+
expect(pickHeadline([])).toBe("");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function memoryStore(): PendingStore {
|
|
31
|
+
const map = new Map<string, string>();
|
|
32
|
+
return {
|
|
33
|
+
getItem: (k) => map.get(k) ?? null,
|
|
34
|
+
setItem: (k, v) => void map.set(k, v),
|
|
35
|
+
removeItem: (k) => void map.delete(k),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("pending prompt", () => {
|
|
40
|
+
test("disimpan per Session dan hanya bisa diambil sekali", () => {
|
|
41
|
+
const store = memoryStore();
|
|
42
|
+
setPendingPrompt("s1", " hello ", store);
|
|
43
|
+
expect(takePendingPrompt("s2", store)).toBeNull();
|
|
44
|
+
expect(takePendingPrompt("s1", store)).toBe("hello");
|
|
45
|
+
expect(takePendingPrompt("s1", store)).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("peek membaca tanpa menghapus", () => {
|
|
49
|
+
const store = memoryStore();
|
|
50
|
+
setPendingPrompt("s1", "hi", store);
|
|
51
|
+
expect(peekPendingPrompt("s1", store)).toBe("hi");
|
|
52
|
+
expect(peekPendingPrompt("s1", store)).toBe("hi");
|
|
53
|
+
expect(takePendingPrompt("s1", store)).toBe("hi");
|
|
54
|
+
expect(peekPendingPrompt("s1", store)).toBeNull();
|
|
55
|
+
expect(peekPendingPrompt("s1", null)).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("teks kosong tidak disimpan; tanpa storage aman", () => {
|
|
59
|
+
const store = memoryStore();
|
|
60
|
+
setPendingPrompt("s1", " ", store);
|
|
61
|
+
expect(takePendingPrompt("s1", store)).toBeNull();
|
|
62
|
+
expect(takePendingPrompt("s1", null)).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit test logika keyboard lock screen (src/lib/lock-screen.ts).
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, test } from "bun:test";
|
|
5
|
+
import { applyPinAction, digitsFromPaste, PIN_MAX, pinKeyAction } from "../lock-screen";
|
|
6
|
+
|
|
7
|
+
describe("pinKeyAction", () => {
|
|
8
|
+
test("angka baris atas & numpad -> digit", () => {
|
|
9
|
+
for (const k of ["0", "5", "9"]) {
|
|
10
|
+
expect(pinKeyAction({ key: k })).toEqual({ type: "digit", digit: k });
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("numpad dibaca dari code, termasuk saat NumLock mati", () => {
|
|
15
|
+
expect(pinKeyAction({ key: "4", code: "Numpad4" })).toEqual({ type: "digit", digit: "4" });
|
|
16
|
+
expect(pinKeyAction({ key: "ArrowLeft", code: "Numpad4" })).toEqual({
|
|
17
|
+
type: "digit",
|
|
18
|
+
digit: "4",
|
|
19
|
+
});
|
|
20
|
+
expect(pinKeyAction({ key: "Insert", code: "Numpad0" })).toEqual({ type: "digit", digit: "0" });
|
|
21
|
+
expect(pinKeyAction({ key: "Enter", code: "NumpadEnter" })).toEqual({ type: "submit" });
|
|
22
|
+
// Panah biasa (bukan numpad) tetap diabaikan.
|
|
23
|
+
expect(pinKeyAction({ key: "ArrowLeft", code: "ArrowLeft" })).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("Backspace/Delete, Enter, Escape", () => {
|
|
27
|
+
expect(pinKeyAction({ key: "Backspace" })).toEqual({ type: "backspace" });
|
|
28
|
+
expect(pinKeyAction({ key: "Delete" })).toEqual({ type: "backspace" });
|
|
29
|
+
expect(pinKeyAction({ key: "Enter" })).toEqual({ type: "submit" });
|
|
30
|
+
expect(pinKeyAction({ key: "Escape" })).toEqual({ type: "clear" });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("huruf & pintasan browser diabaikan", () => {
|
|
34
|
+
expect(pinKeyAction({ key: "a" })).toBeNull();
|
|
35
|
+
expect(pinKeyAction({ key: "Tab" })).toBeNull();
|
|
36
|
+
expect(pinKeyAction({ key: "5", ctrlKey: true })).toBeNull();
|
|
37
|
+
expect(pinKeyAction({ key: "r", metaKey: true })).toBeNull();
|
|
38
|
+
expect(pinKeyAction({ key: "1", altKey: true })).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("applyPinAction", () => {
|
|
43
|
+
test("tambah, hapus, kosongkan; submit tidak mengubah", () => {
|
|
44
|
+
expect(applyPinAction("12", { type: "digit", digit: "3" })).toBe("123");
|
|
45
|
+
expect(applyPinAction("123", { type: "backspace" })).toBe("12");
|
|
46
|
+
expect(applyPinAction("", { type: "backspace" })).toBe("");
|
|
47
|
+
expect(applyPinAction("123", { type: "clear" })).toBe("");
|
|
48
|
+
expect(applyPinAction("123", { type: "submit" })).toBe("123");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("tidak melebihi PIN_MAX", () => {
|
|
52
|
+
const full = "4".repeat(PIN_MAX);
|
|
53
|
+
expect(applyPinAction(full, { type: "digit", digit: "1" })).toBe(full);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("digitsFromPaste", () => {
|
|
58
|
+
test("ambil angka saja, digabung, dipotong PIN_MAX", () => {
|
|
59
|
+
expect(digitsFromPaste("48 29-15", "")).toBe("482915");
|
|
60
|
+
expect(digitsFromPaste("15", "4829")).toBe("482915");
|
|
61
|
+
expect(digitsFromPaste("1".repeat(20), "")).toBe("1".repeat(PIN_MAX));
|
|
62
|
+
expect(digitsFromPaste("abc", "12")).toBe("12");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit test helper pemilihan model (src/lib/model-picker.ts) — logika murni,
|
|
3
|
+
* pola sama dengan `session-summary.test.ts`.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import type { ModelOption } from "@/server/services/opencode-client";
|
|
7
|
+
import {
|
|
8
|
+
activeModelLabel,
|
|
9
|
+
DEFAULT_MODEL_KEY,
|
|
10
|
+
describeModelCount,
|
|
11
|
+
filterModels,
|
|
12
|
+
groupModels,
|
|
13
|
+
keyOfSessionModel,
|
|
14
|
+
MODEL_NAME_MAX_LENGTH,
|
|
15
|
+
modelKey,
|
|
16
|
+
optionKey,
|
|
17
|
+
parseModelKey,
|
|
18
|
+
shortModelName,
|
|
19
|
+
} from "../model-picker";
|
|
20
|
+
|
|
21
|
+
function option(
|
|
22
|
+
providerID: string,
|
|
23
|
+
providerName: string,
|
|
24
|
+
modelID: string,
|
|
25
|
+
name: string,
|
|
26
|
+
): ModelOption {
|
|
27
|
+
return { providerID, providerName, modelID, name };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const OPUS = option("kcgcode", "KCG Code", "kiro/claude-opus-5", "Claude Opus 5");
|
|
31
|
+
const SONNET = option("kcgcode", "KCG Code", "kiro/claude-sonnet-4", "Claude Sonnet 4");
|
|
32
|
+
const GPT = option("openai", "OpenAI", "gpt-5", "GPT-5");
|
|
33
|
+
|
|
34
|
+
describe("kunci model", () => {
|
|
35
|
+
test("modelKey memakai pemisah non-slash (modelID boleh memuat '/')", () => {
|
|
36
|
+
const key = modelKey("kcgcode", "kiro/claude-opus-5");
|
|
37
|
+
expect(key).toContain("kiro/claude-opus-5");
|
|
38
|
+
expect(parseModelKey(key)).toEqual({
|
|
39
|
+
providerID: "kcgcode",
|
|
40
|
+
modelID: "kiro/claude-opus-5",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("optionKey konsisten dengan modelKey", () => {
|
|
45
|
+
expect(optionKey(OPUS)).toBe(modelKey(OPUS.providerID, OPUS.modelID));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("keyOfSessionModel: null -> kunci default", () => {
|
|
49
|
+
expect(keyOfSessionModel(null)).toBe(DEFAULT_MODEL_KEY);
|
|
50
|
+
expect(keyOfSessionModel({ providerID: "openai", modelID: "gpt-5" })).toBe(
|
|
51
|
+
modelKey("openai", "gpt-5"),
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("parseModelKey: kunci default & kunci tak lengkap -> null", () => {
|
|
56
|
+
expect(parseModelKey(DEFAULT_MODEL_KEY)).toBeNull();
|
|
57
|
+
expect(parseModelKey("")).toBeNull();
|
|
58
|
+
expect(parseModelKey("hanya-provider")).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("round-trip kunci untuk sembarang provider/model", () => {
|
|
62
|
+
for (const m of [OPUS, SONNET, GPT]) {
|
|
63
|
+
expect(parseModelKey(optionKey(m))).toEqual({
|
|
64
|
+
providerID: m.providerID,
|
|
65
|
+
modelID: m.modelID,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("groupModels", () => {
|
|
72
|
+
test("mengelompokkan per provider, urutan kemunculan dipertahankan", () => {
|
|
73
|
+
const groups = groupModels([OPUS, GPT, SONNET]);
|
|
74
|
+
expect(groups.map((g) => g.providerID)).toEqual(["kcgcode", "openai"]);
|
|
75
|
+
expect(groups[0]?.models.map((m) => m.modelID)).toEqual([
|
|
76
|
+
"kiro/claude-opus-5",
|
|
77
|
+
"kiro/claude-sonnet-4",
|
|
78
|
+
]);
|
|
79
|
+
expect(groups[0]?.providerName).toBe("KCG Code");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("daftar kosong -> kosong", () => {
|
|
83
|
+
expect(groupModels([])).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("filterModels", () => {
|
|
88
|
+
const all = [OPUS, SONNET, GPT];
|
|
89
|
+
|
|
90
|
+
test("query kosong / whitespace -> semua model", () => {
|
|
91
|
+
expect(filterModels(all, "")).toHaveLength(3);
|
|
92
|
+
expect(filterModels(all, " ")).toHaveLength(3);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("cocok pada nama tanpa membedakan huruf besar/kecil", () => {
|
|
96
|
+
expect(filterModels(all, "OPUS").map((m) => m.modelID)).toEqual(["kiro/claude-opus-5"]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("cocok pada modelID", () => {
|
|
100
|
+
expect(filterModels(all, "gpt-5").map((m) => m.name)).toEqual(["GPT-5"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("cocok pada nama provider", () => {
|
|
104
|
+
expect(filterModels(all, "openai").map((m) => m.name)).toEqual(["GPT-5"]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("beberapa kata: semua harus cocok, urutan bebas", () => {
|
|
108
|
+
expect(filterModels(all, "opus 5").map((m) => m.modelID)).toEqual(["kiro/claude-opus-5"]);
|
|
109
|
+
expect(filterModels(all, "5 opus").map((m) => m.modelID)).toEqual(["kiro/claude-opus-5"]);
|
|
110
|
+
expect(filterModels(all, "claude code").map((m) => m.modelID)).toEqual([
|
|
111
|
+
"kiro/claude-opus-5",
|
|
112
|
+
"kiro/claude-sonnet-4",
|
|
113
|
+
]);
|
|
114
|
+
expect(filterModels(all, "code claude").map((m) => m.modelID)).toEqual([
|
|
115
|
+
"kiro/claude-opus-5",
|
|
116
|
+
"kiro/claude-sonnet-4",
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("tidak ada yang cocok -> kosong", () => {
|
|
121
|
+
expect(filterModels(all, "gemini")).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("input tidak dimutasi", () => {
|
|
125
|
+
const input = [OPUS, GPT];
|
|
126
|
+
filterModels(input, "opus");
|
|
127
|
+
expect(input).toHaveLength(2);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("activeModelLabel", () => {
|
|
132
|
+
test("model null -> label default", () => {
|
|
133
|
+
expect(activeModelLabel(null, [OPUS])).toBe("Default model");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("model dikenal -> nama ramah dari daftar", () => {
|
|
137
|
+
expect(activeModelLabel({ providerID: "kcgcode", modelID: "kiro/claude-opus-5" }, [OPUS])).toBe(
|
|
138
|
+
"Claude Opus 5",
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("daftar belum termuat -> jatuh ke modelID (trigger tidak kosong)", () => {
|
|
143
|
+
expect(activeModelLabel({ providerID: "kcgcode", modelID: "kiro/claude-opus-5" }, [])).toBe(
|
|
144
|
+
"kiro/claude-opus-5",
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("describeModelCount", () => {
|
|
150
|
+
test("nol, satu, dan banyak", () => {
|
|
151
|
+
expect(describeModelCount(0)).toBe("No matching models");
|
|
152
|
+
expect(describeModelCount(1)).toBe("1 model");
|
|
153
|
+
expect(describeModelCount(7)).toBe("7 models");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("shortModelName", () => {
|
|
158
|
+
test("prefiks vendor dibuang, segmen terakhir dipakai", () => {
|
|
159
|
+
expect(shortModelName("kiro/claude-opus-5")).toBe("claude-opus-5");
|
|
160
|
+
expect(shortModelName("anthropic/claude-sonnet-4")).toBe("claude-sonnet-4");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("prefiks berlapis: tetap segmen terakhir", () => {
|
|
164
|
+
expect(shortModelName("vendor/family/gpt-5")).toBe("gpt-5");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("tanpa slash dibiarkan apa adanya", () => {
|
|
168
|
+
expect(shortModelName("Claude Opus 5")).toBe("Claude Opus 5");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("trailing slash diabaikan", () => {
|
|
172
|
+
expect(shortModelName("kiro/claude-opus-5/")).toBe("claude-opus-5");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("nama panjang dipotong dengan elipsis, total tepat maxLength", () => {
|
|
176
|
+
const long = "a".repeat(40);
|
|
177
|
+
const out = shortModelName(long, 10);
|
|
178
|
+
expect(out).toHaveLength(10);
|
|
179
|
+
expect(out.endsWith("…")).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("nama tepat sepanjang batas tidak dipotong", () => {
|
|
183
|
+
const exact = "b".repeat(MODEL_NAME_MAX_LENGTH);
|
|
184
|
+
expect(shortModelName(exact)).toBe(exact);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("spasi sebelum elipsis dirapikan", () => {
|
|
188
|
+
expect(shortModelName("Claude Opus Model Panjang", 12)).toBe("Claude Opus…");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("string kosong / hanya slash aman", () => {
|
|
192
|
+
expect(shortModelName("")).toBe("");
|
|
193
|
+
expect(shortModelName("/")).toBe("");
|
|
194
|
+
expect(shortModelName("///")).toBe("");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("maxLength nol atau negatif -> string kosong (tanpa crash)", () => {
|
|
198
|
+
expect(shortModelName("apa pun", 0)).toBe("");
|
|
199
|
+
expect(shortModelName("apa pun", -5)).toBe("");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("label default tetap terbaca", () => {
|
|
203
|
+
expect(shortModelName("Model default")).toBe("Model default");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
greeting,
|
|
4
|
+
nextStep,
|
|
5
|
+
ONBOARDING_STEPS,
|
|
6
|
+
PROGRESS_STEPS,
|
|
7
|
+
prevStep,
|
|
8
|
+
stepDirection,
|
|
9
|
+
} from "../onboarding";
|
|
10
|
+
|
|
11
|
+
describe("alur onboarding", () => {
|
|
12
|
+
test("urutan langkah welcome -> profile -> lock -> done", () => {
|
|
13
|
+
expect([...ONBOARDING_STEPS]).toEqual(["welcome", "profile", "lock", "done"]);
|
|
14
|
+
expect(nextStep("welcome")).toBe("profile");
|
|
15
|
+
expect(nextStep("profile")).toBe("lock");
|
|
16
|
+
expect(nextStep("lock")).toBe("done");
|
|
17
|
+
expect(nextStep("done")).toBe("done");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("mundur berhenti di welcome", () => {
|
|
21
|
+
expect(prevStep("lock")).toBe("profile");
|
|
22
|
+
expect(prevStep("profile")).toBe("welcome");
|
|
23
|
+
expect(prevStep("welcome")).toBe("welcome");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("arah animasi mengikuti navigasi", () => {
|
|
27
|
+
expect(stepDirection("welcome", "profile")).toBe(1);
|
|
28
|
+
expect(stepDirection("lock", "profile")).toBe(-1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("titik progres hanya untuk langkah pengaturan", () => {
|
|
32
|
+
expect([...PROGRESS_STEPS]).toEqual(["profile", "lock"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("sapaan sesuai jam", () => {
|
|
36
|
+
expect(greeting(8)).toBe("Good morning");
|
|
37
|
+
expect(greeting(14)).toBe("Good afternoon");
|
|
38
|
+
expect(greeting(20)).toBe("Good evening");
|
|
39
|
+
expect(greeting(2)).toBe("Good evening");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { parsePermissionTitle } from "../permission-info";
|
|
3
|
+
|
|
4
|
+
describe("parsePermissionTitle", () => {
|
|
5
|
+
test("judul server: permission + pola ber-backtick", () => {
|
|
6
|
+
const info = parsePermissionTitle(
|
|
7
|
+
"external_directory — `/home/irsyadulibad/.config/opencode/*`",
|
|
8
|
+
);
|
|
9
|
+
expect(info.permission).toBe("external_directory");
|
|
10
|
+
expect(info.label).toBe("Access a folder outside the project");
|
|
11
|
+
expect(info.targets).toEqual(["/home/irsyadulibad/.config/opencode/*"]);
|
|
12
|
+
expect(info.risk).toBe("high");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("beberapa pola dipisah", () => {
|
|
16
|
+
const info = parsePermissionTitle("edit — `src/a.ts` — `src/b.ts`");
|
|
17
|
+
expect(info.label).toBe("Edit files");
|
|
18
|
+
expect(info.targets).toEqual(["src/a.ts", "src/b.ts"]);
|
|
19
|
+
expect(info.risk).toBe("medium");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("perintah bash yang memuat ' — ' di dalam backtick tidak dipecah sebagai pola baru", () => {
|
|
23
|
+
// Server membungkus tiap pola dengan backtick; pemisah ' — ' hanya di
|
|
24
|
+
// antara pola. Perintah tanpa ' — ' di dalamnya tetap utuh.
|
|
25
|
+
const info = parsePermissionTitle("bash — `mkdir -p /tmp/perm-test`");
|
|
26
|
+
expect(info.label).toBe("Run a shell command");
|
|
27
|
+
expect(info.targets).toEqual(["mkdir -p /tmp/perm-test"]);
|
|
28
|
+
expect(info.risk).toBe("high");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("permission tanpa pola", () => {
|
|
32
|
+
const info = parsePermissionTitle("webfetch");
|
|
33
|
+
expect(info.label).toBe("Fetch a web page");
|
|
34
|
+
expect(info.targets).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("baca-saja = risiko rendah", () => {
|
|
38
|
+
expect(parsePermissionTitle("read — `README.md`").risk).toBe("low");
|
|
39
|
+
expect(parsePermissionTitle("grep — `TODO`").risk).toBe("low");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("permission tak dikenal: nama ditampilkan (bukan disembunyikan), risiko sedang", () => {
|
|
43
|
+
const info = parsePermissionTitle("future_tool — `x`");
|
|
44
|
+
expect(info.permission).toBe("future_tool");
|
|
45
|
+
expect(info.label).toBe("Future tool");
|
|
46
|
+
expect(info.risk).toBe("medium");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("judul kosong / fallback server -> label umum", () => {
|
|
50
|
+
for (const t of [null, "", " ", "Izin tool"]) {
|
|
51
|
+
const info = parsePermissionTitle(t);
|
|
52
|
+
expect(info.permission).toBeNull();
|
|
53
|
+
expect(info.label).toBe("Use a tool");
|
|
54
|
+
expect(info.targets).toEqual([]);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("nama permission tidak peka huruf besar", () => {
|
|
59
|
+
expect(parsePermissionTitle("BASH — `ls`").label).toBe("Run a shell command");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit test helper panel samping Project (src/lib/project-extensions.ts).
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, test } from "bun:test";
|
|
5
|
+
import type { SkillInfo } from "@/server/services/opencode-client";
|
|
6
|
+
import {
|
|
7
|
+
describeMcpEntries,
|
|
8
|
+
describeMcpSummary,
|
|
9
|
+
filterSkills,
|
|
10
|
+
groupSkills,
|
|
11
|
+
mergeMcpServers,
|
|
12
|
+
} from "../project-extensions";
|
|
13
|
+
|
|
14
|
+
function skill(name: string, source: SkillInfo["source"], description: string | null = null) {
|
|
15
|
+
return { name, description, location: null, source } satisfies SkillInfo;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("describeMcpSummary", () => {
|
|
19
|
+
test("kosong -> None configured", () => {
|
|
20
|
+
expect(describeMcpSummary([])).toBe("None configured");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("hitung yang connected", () => {
|
|
24
|
+
expect(
|
|
25
|
+
describeMcpSummary([
|
|
26
|
+
{ name: "a", status: "connected", error: null },
|
|
27
|
+
{ name: "b", status: "failed", error: "x" },
|
|
28
|
+
{ name: "c", status: "connected", error: null },
|
|
29
|
+
]),
|
|
30
|
+
).toBe("2 of 3 connected");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("mergeMcpServers", () => {
|
|
35
|
+
test("gabung per nama, catat agent, urut yang bermasalah dulu", () => {
|
|
36
|
+
const entries = mergeMcpServers({
|
|
37
|
+
opencode: [
|
|
38
|
+
{ name: "web", status: "connected", error: null },
|
|
39
|
+
{ name: "db", status: "connected", error: null },
|
|
40
|
+
],
|
|
41
|
+
"claude-code": [
|
|
42
|
+
{ name: "web", status: "unknown", error: null },
|
|
43
|
+
{ name: "db", status: "failed", error: "boom" },
|
|
44
|
+
{ name: "cc-only", status: "disabled", error: null },
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
expect(entries.map((e) => [e.name, e.status, e.agents])).toEqual([
|
|
48
|
+
["db", "failed", ["opencode", "claude-code"]],
|
|
49
|
+
["web", "connected", ["opencode", "claude-code"]],
|
|
50
|
+
["cc-only", "disabled", ["claude-code"]],
|
|
51
|
+
]);
|
|
52
|
+
expect(entries[0]?.byAgent["claude-code"]?.error).toBe("boom");
|
|
53
|
+
// Tersambung di salah satu agent sudah dihitung "connected".
|
|
54
|
+
expect(describeMcpEntries(entries)).toBe("2 of 3 connected");
|
|
55
|
+
expect(describeMcpEntries([])).toBe("None configured");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("groupSkills", () => {
|
|
60
|
+
test("urutan project -> global -> builtin, kelompok kosong dibuang", () => {
|
|
61
|
+
const groups = groupSkills([
|
|
62
|
+
skill("b1", "builtin"),
|
|
63
|
+
skill("p1", "project"),
|
|
64
|
+
skill("p2", "project"),
|
|
65
|
+
]);
|
|
66
|
+
expect(groups.map((g) => g.source)).toEqual(["project", "builtin"]);
|
|
67
|
+
expect(groups[0]?.skills.map((s) => s.name)).toEqual(["p1", "p2"]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("filterSkills", () => {
|
|
72
|
+
const all = [skill("shadcn", "project", "UI components"), skill("firecrawl", "global", "Web")];
|
|
73
|
+
|
|
74
|
+
test("query kosong -> semua", () => {
|
|
75
|
+
expect(filterSkills(all, " ")).toHaveLength(2);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("cocok nama atau deskripsi, case-insensitive", () => {
|
|
79
|
+
expect(filterSkills(all, "SHAD").map((s) => s.name)).toEqual(["shadcn"]);
|
|
80
|
+
expect(filterSkills(all, "web").map((s) => s.name)).toEqual(["firecrawl"]);
|
|
81
|
+
expect(filterSkills(all, "zzz")).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
});
|