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,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit test pemindai skill Claude Code (src/server/services/claude-skills.ts)
|
|
3
|
+
* — frontmatter SKILL.md dan pemindaian `.claude/skills` di direktori temp.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { claudeGlobalSkillsDir, listClaudeSkills, parseSkillFrontmatter } from "../claude-skills";
|
|
10
|
+
|
|
11
|
+
function writeSkill(dir: string, body: string): void {
|
|
12
|
+
mkdirSync(dir, { recursive: true });
|
|
13
|
+
writeFileSync(join(dir, "SKILL.md"), body);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("parseSkillFrontmatter", () => {
|
|
17
|
+
test("nilai satu baris, berkutip maupun tidak", () => {
|
|
18
|
+
expect(
|
|
19
|
+
parseSkillFrontmatter('---\nname: pdf\ndescription: "Read PDFs: fast"\n---\n# PDF'),
|
|
20
|
+
).toEqual({ name: "pdf", description: "Read PDFs: fast" });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("blok `>` digabung menjadi satu baris", () => {
|
|
24
|
+
expect(
|
|
25
|
+
parseSkillFrontmatter("---\nname: x\ndescription: >\n first line\n second line\n---\n"),
|
|
26
|
+
).toEqual({ name: "x", description: "first line second line" });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("tanpa frontmatter -> null", () => {
|
|
30
|
+
expect(parseSkillFrontmatter("# Just markdown")).toEqual({ name: null, description: null });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("listClaudeSkills", () => {
|
|
35
|
+
test("Project + global, ikuti symlink, fallback nama folder", async () => {
|
|
36
|
+
const root = mkdtempSync(join(tmpdir(), "kcg-cc-skills-"));
|
|
37
|
+
const project = join(root, "proj");
|
|
38
|
+
const global = join(root, "home-skills");
|
|
39
|
+
// Pola CLI `skills --agent claude-code`: isi di .agents/skills, symlink di .claude/skills.
|
|
40
|
+
writeSkill(join(project, ".agents/skills/linked"), "---\nname: linked\ndescription: L\n---\n");
|
|
41
|
+
mkdirSync(join(project, ".claude/skills"), { recursive: true });
|
|
42
|
+
symlinkSync(join(project, ".agents/skills/linked"), join(project, ".claude/skills/linked"));
|
|
43
|
+
writeSkill(join(project, ".claude/skills/plain"), "no frontmatter");
|
|
44
|
+
writeSkill(join(global, "global-one"), "---\nname: global-one\n---\n");
|
|
45
|
+
|
|
46
|
+
const skills = await listClaudeSkills(project, global);
|
|
47
|
+
expect(skills.map((s) => [s.name, s.source, s.description])).toEqual([
|
|
48
|
+
["global-one", "global", null],
|
|
49
|
+
["linked", "project", "L"],
|
|
50
|
+
["plain", "project", null],
|
|
51
|
+
]);
|
|
52
|
+
expect(skills[1]?.location).toBe(join(project, ".claude/skills/linked/SKILL.md"));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("direktori tidak ada -> daftar kosong", async () => {
|
|
56
|
+
expect(await listClaudeSkills("/nonexistent-kcg", "/nonexistent-kcg-global")).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("claudeGlobalSkillsDir menghormati CLAUDE_CONFIG_DIR", () => {
|
|
60
|
+
expect(claudeGlobalSkillsDir({ CLAUDE_CONFIG_DIR: "/cfg" })).toBe("/cfg/skills");
|
|
61
|
+
expect(claudeGlobalSkillsDir({})).toEndWith("/.claude/skills");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { createFrpcInstaller, FRP_VERSION } from "../frpc-installer";
|
|
6
|
+
|
|
7
|
+
let dirs: string[] = [];
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
for (const d of dirs) rmSync(d, { recursive: true, force: true });
|
|
10
|
+
dirs = [];
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/** Build a real frp-shaped .tar.gz with a fake frpc script inside. */
|
|
14
|
+
async function fakeRelease(): Promise<{ bytes: ArrayBuffer; sha256: string }> {
|
|
15
|
+
const src = mkdtempSync(path.join(tmpdir(), "kcg-frp-src-"));
|
|
16
|
+
dirs.push(src);
|
|
17
|
+
const name = `frp_${FRP_VERSION}_linux_amd64`;
|
|
18
|
+
mkdirSync(path.join(src, name));
|
|
19
|
+
await Bun.write(path.join(src, name, "frpc"), "#!/bin/sh\necho fake\n");
|
|
20
|
+
const archive = path.join(src, `${name}.tar.gz`);
|
|
21
|
+
const proc = Bun.spawn(["tar", "-czf", archive, "-C", src, name]);
|
|
22
|
+
expect(await proc.exited).toBe(0);
|
|
23
|
+
const bytes = await Bun.file(archive).arrayBuffer();
|
|
24
|
+
return { bytes, sha256: new Bun.CryptoHasher("sha256").update(bytes).digest("hex") };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("frpc installer", () => {
|
|
28
|
+
test("rejects an archive whose checksum does not match the pinned one", async () => {
|
|
29
|
+
const { bytes } = await fakeRelease();
|
|
30
|
+
const binDir = mkdtempSync(path.join(tmpdir(), "kcg-frpc-test-"));
|
|
31
|
+
dirs.push(binDir);
|
|
32
|
+
const installer = createFrpcInstaller({
|
|
33
|
+
binDir,
|
|
34
|
+
platform: "linux",
|
|
35
|
+
arch: "x64",
|
|
36
|
+
which: () => null,
|
|
37
|
+
fetch: (async () => new Response(bytes)) as unknown as typeof fetch,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const r = await installer.ensure();
|
|
41
|
+
expect(r).toEqual({ ok: false, error: "FRPC_CHECKSUM_MISMATCH" });
|
|
42
|
+
expect(existsSync(path.join(binDir, "frpc"))).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("installs across filesystems (tmpdir -> home) without EXDEV, atomically", async () => {
|
|
46
|
+
const { bytes, sha256 } = await fakeRelease();
|
|
47
|
+
// binDir under $HOME while extraction happens in tmpdir(): on many systems
|
|
48
|
+
// /tmp is tmpfs, so a plain rename() would fail with EXDEV.
|
|
49
|
+
const binDir = mkdtempSync(path.join(homedir(), ".kcg-frpc-test-"));
|
|
50
|
+
dirs.push(binDir);
|
|
51
|
+
const installer = createFrpcInstaller({
|
|
52
|
+
binDir,
|
|
53
|
+
platform: "linux",
|
|
54
|
+
arch: "x64",
|
|
55
|
+
which: () => null,
|
|
56
|
+
fetch: (async () => new Response(bytes)) as unknown as typeof fetch,
|
|
57
|
+
sha256Override: sha256,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const r = await installer.ensure();
|
|
61
|
+
expect(r.ok).toBe(true);
|
|
62
|
+
const target = path.join(binDir, "frpc");
|
|
63
|
+
expect(r.ok && r.data).toBe(target);
|
|
64
|
+
expect(statSync(target).mode & 0o777).toBe(0o755);
|
|
65
|
+
expect(readdirSync(binDir)).toEqual(["frpc"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("prefers an existing binary and does not download", async () => {
|
|
69
|
+
const binDir = mkdtempSync(path.join(tmpdir(), "kcg-frpc-test-"));
|
|
70
|
+
dirs.push(binDir);
|
|
71
|
+
let fetched = false;
|
|
72
|
+
const installer = createFrpcInstaller({
|
|
73
|
+
binDir,
|
|
74
|
+
which: () => "/usr/local/bin/frpc",
|
|
75
|
+
fetch: (async () => {
|
|
76
|
+
fetched = true;
|
|
77
|
+
return new Response("");
|
|
78
|
+
}) as unknown as typeof fetch,
|
|
79
|
+
});
|
|
80
|
+
expect(await installer.ensure()).toEqual({ ok: true, data: "/usr/local/bin/frpc" });
|
|
81
|
+
expect(fetched).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit test `git-info.ts` — dijalankan pada repo git SUNGGUHAN di tmpdir
|
|
3
|
+
* (bukan mock `Bun.spawn`), karena yang rawan salah justru penafsiran output
|
|
4
|
+
* git: urutan `--left-right` pada `rev-list` mudah tertukar antara
|
|
5
|
+
* ahead & behind.
|
|
6
|
+
*/
|
|
7
|
+
import { expect, test } from "bun:test";
|
|
8
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { readGitInfo } from "../git-info";
|
|
12
|
+
|
|
13
|
+
async function run(cwd: string, args: string[]): Promise<void> {
|
|
14
|
+
const proc = Bun.spawn(args, { cwd, stdout: "ignore", stderr: "ignore", stdin: "ignore" });
|
|
15
|
+
await proc.exited;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function commit(cwd: string, name: string): Promise<void> {
|
|
19
|
+
writeFileSync(path.join(cwd, name), name);
|
|
20
|
+
await run(cwd, ["git", "add", "."]);
|
|
21
|
+
await run(cwd, ["git", "commit", "-m", name]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test("cabang, ahead/behind, dan remote dibaca dari repo sungguhan", async () => {
|
|
25
|
+
const root = mkdtempSync(path.join(tmpdir(), "kcg-git-"));
|
|
26
|
+
const remote = path.join(root, "remote.git");
|
|
27
|
+
const work = path.join(root, "work");
|
|
28
|
+
try {
|
|
29
|
+
await run(root, ["git", "init", "--bare", "-b", "main", remote]);
|
|
30
|
+
await run(root, ["git", "clone", remote, work]);
|
|
31
|
+
await run(work, ["git", "config", "user.email", "t@example.com"]);
|
|
32
|
+
await run(work, ["git", "config", "user.name", "Tester"]);
|
|
33
|
+
await commit(work, "a.txt");
|
|
34
|
+
await run(work, ["git", "push", "-u", "origin", "main"]);
|
|
35
|
+
|
|
36
|
+
const synced = await readGitInfo(work);
|
|
37
|
+
expect(synced.branch).toBe("main");
|
|
38
|
+
expect(synced.hasRemote).toBe(true);
|
|
39
|
+
expect(synced.ahead).toBe(0);
|
|
40
|
+
expect(synced.behind).toBe(0);
|
|
41
|
+
|
|
42
|
+
// Dua commit lokal yang belum dipush = ahead 2 (BUKAN behind).
|
|
43
|
+
await commit(work, "b.txt");
|
|
44
|
+
await commit(work, "c.txt");
|
|
45
|
+
const ahead = await readGitInfo(work);
|
|
46
|
+
expect(ahead.ahead).toBe(2);
|
|
47
|
+
expect(ahead.behind).toBe(0);
|
|
48
|
+
|
|
49
|
+
// Cabang baru tanpa upstream -> selisih tidak diketahui, bukan 0.
|
|
50
|
+
await run(work, ["git", "checkout", "-b", "feature"]);
|
|
51
|
+
const noUpstream = await readGitInfo(work);
|
|
52
|
+
expect(noUpstream.branch).toBe("feature");
|
|
53
|
+
expect(noUpstream.ahead).toBeNull();
|
|
54
|
+
expect(noUpstream.behind).toBeNull();
|
|
55
|
+
} finally {
|
|
56
|
+
rmSync(root, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("direktori biasa (bukan repo git) -> info kosong, bukan error", async () => {
|
|
61
|
+
const dir = mkdtempSync(path.join(tmpdir(), "kcg-nogit-"));
|
|
62
|
+
try {
|
|
63
|
+
const info = await readGitInfo(dir);
|
|
64
|
+
expect(info).toEqual({ branch: null, ahead: null, behind: null, hasRemote: false });
|
|
65
|
+
} finally {
|
|
66
|
+
rmSync(dir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("path yang tidak ada -> info kosong (tidak melempar)", async () => {
|
|
71
|
+
const info = await readGitInfo("/tmp/kcg-does-not-exist-xyz");
|
|
72
|
+
expect(info.branch).toBeNull();
|
|
73
|
+
expect(info.hasRemote).toBe(false);
|
|
74
|
+
});
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit test helper murni `opencode-client.ts` & `opencode-server.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Yang diuji (tanpa jaringan/proses sungguhan):
|
|
5
|
+
* - `parseSseFrame` / `normalizeEvent`: parsing & normalisasi event SSE.
|
|
6
|
+
* - `parseListeningPort`: ekstraksi port dari baris log server.
|
|
7
|
+
* - `flattenProviders`: respons GET /config/providers -> daftar model UI.
|
|
8
|
+
* - `flattenAgents`: respons GET /agent -> daftar mode (primary/all saja).
|
|
9
|
+
* - `configFingerprint`: deteksi perubahan file config opencode.
|
|
10
|
+
* - `ensureFreshServer`: restart saat fingerprint config berubah.
|
|
11
|
+
*/
|
|
12
|
+
import { expect, test } from "bun:test";
|
|
13
|
+
import { mkdtempSync, utimesSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import fc from "fast-check";
|
|
17
|
+
import type { OpenCodeClient } from "../opencode-client";
|
|
18
|
+
import { flattenAgents, flattenMcp, flattenProviders, flattenSkills } from "../opencode-client";
|
|
19
|
+
import {
|
|
20
|
+
configFingerprint,
|
|
21
|
+
createOpenCodeServerManager,
|
|
22
|
+
parseListeningPort,
|
|
23
|
+
type ServerProcessLike,
|
|
24
|
+
} from "../opencode-server";
|
|
25
|
+
import { normalizeEvent, parseSseFrame } from "../opencode-sse";
|
|
26
|
+
|
|
27
|
+
test("parseSseFrame: mengambil baris data; frame tanpa data -> null", () => {
|
|
28
|
+
expect(parseSseFrame('data: {"a":1}\n\n')).toBe('{"a":1}');
|
|
29
|
+
expect(parseSseFrame("event: x\ndata: hello\n")).toBe("hello");
|
|
30
|
+
expect(parseSseFrame("comment saja\n\n")).toBeNull();
|
|
31
|
+
expect(parseSseFrame("")).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("normalizeEvent: {id,type,properties} -> event datar (type + properties)", () => {
|
|
35
|
+
const ev = normalizeEvent({
|
|
36
|
+
id: "evt_1",
|
|
37
|
+
type: "permission.asked",
|
|
38
|
+
properties: { requestID: "per_1", sessionID: "ses_1", permission: "bash:ls" },
|
|
39
|
+
});
|
|
40
|
+
expect(ev).toEqual({
|
|
41
|
+
id: "evt_1",
|
|
42
|
+
type: "permission.asked",
|
|
43
|
+
requestID: "per_1",
|
|
44
|
+
sessionID: "ses_1",
|
|
45
|
+
permission: "bash:ls",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Payload tanpa properties -> tetap aman.
|
|
49
|
+
expect(normalizeEvent({ type: "server.connected" })).toEqual({ type: "server.connected" });
|
|
50
|
+
|
|
51
|
+
// Payload invalid -> null.
|
|
52
|
+
expect(normalizeEvent(null)).toBeNull();
|
|
53
|
+
expect(normalizeEvent("x")).toBeNull();
|
|
54
|
+
expect(normalizeEvent({})).toBeNull();
|
|
55
|
+
expect(normalizeEvent({ id: 1 })).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("flattenAgents: hanya primary/all; subagent murni disaring", () => {
|
|
59
|
+
const agents = flattenAgents([
|
|
60
|
+
{ name: "build", mode: "primary", description: "Full tool access" },
|
|
61
|
+
{ name: "plan", mode: "primary", description: "Planning only" },
|
|
62
|
+
{ name: "general", mode: "subagent", description: "Sub-agent" },
|
|
63
|
+
{ name: "explore", mode: "all", description: null },
|
|
64
|
+
{ name: "", mode: "primary" },
|
|
65
|
+
"rusak",
|
|
66
|
+
]);
|
|
67
|
+
expect(agents.map((a) => a.name)).toEqual(["build", "plan", "explore"]);
|
|
68
|
+
expect(agents[2]).toEqual({ name: "explore", mode: "all", description: null });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("flattenAgents: payload bukan array / kosong -> []", () => {
|
|
72
|
+
expect(flattenAgents(null)).toEqual([]);
|
|
73
|
+
expect(flattenAgents({})).toEqual([]);
|
|
74
|
+
expect(flattenAgents([])).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("parseListeningPort: mengekstrak port dari baris log opencode", () => {
|
|
78
|
+
expect(parseListeningPort("opencode server listening on http://127.0.0.1:4096")).toBe(4096);
|
|
79
|
+
expect(parseListeningPort("listening on http://0.0.0.0:4599")).toBe(4599);
|
|
80
|
+
expect(parseListeningPort("beberapa teks tanpa port")).toBeNull();
|
|
81
|
+
expect(parseListeningPort("port 80 di tengah teks")).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("parseListeningPort: properti — port valid apa pun selalu diekstrak", () => {
|
|
85
|
+
fc.assert(
|
|
86
|
+
fc.property(fc.integer({ min: 1024, max: 65535 }), (p) => {
|
|
87
|
+
expect(parseListeningPort(`opencode server listening on http://127.0.0.1:${p}`)).toBe(p);
|
|
88
|
+
}),
|
|
89
|
+
{ numRuns: 50 },
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// flattenProviders (GET /config/providers -> daftar model untuk UI)
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
test("flattenProviders: flatten provider + model dengan nama tampilan", () => {
|
|
98
|
+
const payload = {
|
|
99
|
+
providers: [
|
|
100
|
+
{
|
|
101
|
+
id: "kcgcode",
|
|
102
|
+
name: "kcgcode",
|
|
103
|
+
models: {
|
|
104
|
+
"kiro/claude-opus-5": { id: "kiro/claude-opus-5", name: "Claude Opus 5" },
|
|
105
|
+
"mimo/mimo-v2.5": { id: "mimo/mimo-v2.5" }, // tanpa name -> fallback id
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
expect(flattenProviders(payload)).toEqual([
|
|
111
|
+
{
|
|
112
|
+
providerID: "kcgcode",
|
|
113
|
+
providerName: "kcgcode",
|
|
114
|
+
modelID: "kiro/claude-opus-5",
|
|
115
|
+
name: "Claude Opus 5",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
providerID: "kcgcode",
|
|
119
|
+
providerName: "kcgcode",
|
|
120
|
+
modelID: "mimo/mimo-v2.5",
|
|
121
|
+
name: "mimo/mimo-v2.5",
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("flattenProviders: model deprecated disaring", () => {
|
|
127
|
+
const payload = {
|
|
128
|
+
providers: [
|
|
129
|
+
{
|
|
130
|
+
id: "p",
|
|
131
|
+
name: "P",
|
|
132
|
+
models: {
|
|
133
|
+
lama: { id: "lama", status: "deprecated" },
|
|
134
|
+
baru: { id: "baru", status: "active" },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
const out = flattenProviders(payload);
|
|
140
|
+
expect(out.map((m) => m.modelID)).toEqual(["baru"]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("flattenProviders: payload tidak valid / kosong -> []", () => {
|
|
144
|
+
expect(flattenProviders(null)).toEqual([]);
|
|
145
|
+
expect(flattenProviders({})).toEqual([]);
|
|
146
|
+
expect(flattenProviders({ providers: "bukan array" })).toEqual([]);
|
|
147
|
+
// provider tanpa id / tanpa models diabaikan
|
|
148
|
+
expect(flattenProviders({ providers: [{ name: "tanpa-id" }, { id: "kosong" }] })).toEqual([]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("flattenProviders: provider name kosong fallback ke id", () => {
|
|
152
|
+
const out = flattenProviders({
|
|
153
|
+
providers: [{ id: "opencode", name: "", models: { m1: { id: "m1", name: "M1" } } }],
|
|
154
|
+
});
|
|
155
|
+
expect(out[0]).toMatchObject({ providerID: "opencode", providerName: "opencode" });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// configFingerprint / ensureFreshServer (live reread config opencode)
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
function makeFakeProc(port: number): ServerProcessLike & { kills: string[] } {
|
|
163
|
+
const kills: string[] = [];
|
|
164
|
+
let resolveExited: (code: number | null) => void = () => {};
|
|
165
|
+
const exited = new Promise<number | null>((res) => {
|
|
166
|
+
resolveExited = res;
|
|
167
|
+
});
|
|
168
|
+
const stdout = new ReadableStream<Uint8Array>({
|
|
169
|
+
start(controller) {
|
|
170
|
+
controller.close();
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
// stderr sengaja tidak di-close: ensureServer menunggu baris "listening".
|
|
174
|
+
const stderr = new ReadableStream<Uint8Array>({
|
|
175
|
+
start(controller) {
|
|
176
|
+
const enc = new TextEncoder();
|
|
177
|
+
controller.enqueue(enc.encode(`opencode server listening on http://127.0.0.1:${port}\n`));
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
kills,
|
|
182
|
+
kill(signal) {
|
|
183
|
+
kills.push(signal);
|
|
184
|
+
resolveExited(0);
|
|
185
|
+
},
|
|
186
|
+
exited,
|
|
187
|
+
stderr,
|
|
188
|
+
stdout,
|
|
189
|
+
pid: undefined,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function makeFakeClientFactory() {
|
|
194
|
+
const clients: OpenCodeClient[] = [];
|
|
195
|
+
const factory = (): OpenCodeClient => {
|
|
196
|
+
const client = {
|
|
197
|
+
health: async () => true,
|
|
198
|
+
subscribeEvents: () => () => {},
|
|
199
|
+
} as unknown as OpenCodeClient;
|
|
200
|
+
clients.push(client);
|
|
201
|
+
return client;
|
|
202
|
+
};
|
|
203
|
+
return { clients, factory };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function makeFastManager() {
|
|
207
|
+
const procs: ReturnType<typeof makeFakeProc>[] = [];
|
|
208
|
+
const { clients, factory } = makeFakeClientFactory();
|
|
209
|
+
let nextPort = 9000;
|
|
210
|
+
const manager = createOpenCodeServerManager({
|
|
211
|
+
portTimeoutMs: 2000,
|
|
212
|
+
readyTimeoutMs: 2000,
|
|
213
|
+
spawn: () => {
|
|
214
|
+
const proc = makeFakeProc(nextPort++);
|
|
215
|
+
procs.push(proc);
|
|
216
|
+
return proc;
|
|
217
|
+
},
|
|
218
|
+
clientFactory: factory,
|
|
219
|
+
});
|
|
220
|
+
return { manager, procs, clients };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
test("configFingerprint: file project ikut masuk dan berubah saat disentuh", () => {
|
|
224
|
+
const dir = mkdtempSync(join(tmpdir(), "kcg-fp-"));
|
|
225
|
+
const base = configFingerprint(dir);
|
|
226
|
+
const p = join(dir, "opencode.json");
|
|
227
|
+
writeFileSync(p, "{}");
|
|
228
|
+
const t = new Date("2026-01-01T00:00:00Z");
|
|
229
|
+
utimesSync(p, t, t);
|
|
230
|
+
const fp1 = configFingerprint(dir);
|
|
231
|
+
expect(fp1).toContain("opencode.json");
|
|
232
|
+
expect(fp1).not.toBe(base);
|
|
233
|
+
// Stabil bila file tidak disentuh.
|
|
234
|
+
expect(configFingerprint(dir)).toBe(fp1);
|
|
235
|
+
// Sentuh file -> fingerprint berubah.
|
|
236
|
+
utimesSync(p, new Date("2026-01-02T00:00:00Z"), new Date("2026-01-02T00:00:00Z"));
|
|
237
|
+
expect(configFingerprint(dir)).not.toBe(fp1);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("ensureFreshServer: config tak berubah -> instance sama; berubah -> restart", async () => {
|
|
241
|
+
const dir = mkdtempSync(join(tmpdir(), "kcg-fresh-"));
|
|
242
|
+
const { manager, procs } = makeFastManager();
|
|
243
|
+
|
|
244
|
+
const first = await manager.ensureFreshServer("p1", dir);
|
|
245
|
+
expect(first.ok).toBe(true);
|
|
246
|
+
if (!first.ok) return;
|
|
247
|
+
expect(procs).toHaveLength(1);
|
|
248
|
+
|
|
249
|
+
// Config tidak berubah: tidak spawn ulang.
|
|
250
|
+
const second = await manager.ensureFreshServer("p1", dir);
|
|
251
|
+
expect(second.ok).toBe(true);
|
|
252
|
+
expect(procs).toHaveLength(1);
|
|
253
|
+
if (second.ok) expect(second.data.baseUrl).toBe(first.data.baseUrl);
|
|
254
|
+
|
|
255
|
+
// Ubah config project -> fingerprint beda -> stop + spawn baru.
|
|
256
|
+
writeFileSync(join(dir, "opencode.json"), "{}");
|
|
257
|
+
const third = await manager.ensureFreshServer("p1", dir);
|
|
258
|
+
expect(third.ok).toBe(true);
|
|
259
|
+
if (!third.ok) return;
|
|
260
|
+
expect(procs).toHaveLength(2);
|
|
261
|
+
expect(procs[0]?.kills).toContain("SIGTERM");
|
|
262
|
+
expect(third.data.baseUrl).not.toBe(first.data.baseUrl);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("flattenMcp: map nama -> status, terurut; status tak dikenal -> unknown", () => {
|
|
266
|
+
expect(
|
|
267
|
+
flattenMcp({
|
|
268
|
+
zeta: { status: "connected" },
|
|
269
|
+
alpha: { status: "failed", error: "spawn ENOENT" },
|
|
270
|
+
mid: { status: "weird" },
|
|
271
|
+
"": { status: "connected" },
|
|
272
|
+
}),
|
|
273
|
+
).toEqual([
|
|
274
|
+
{ name: "alpha", status: "failed", error: "spawn ENOENT" },
|
|
275
|
+
{ name: "mid", status: "unknown", error: null },
|
|
276
|
+
{ name: "zeta", status: "connected", error: null },
|
|
277
|
+
]);
|
|
278
|
+
expect(flattenMcp(null)).toEqual([]);
|
|
279
|
+
expect(flattenMcp([])).toEqual([]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("flattenSkills: klasifikasi asal (builtin/project/global), content dibuang", () => {
|
|
283
|
+
const out = flattenSkills(
|
|
284
|
+
[
|
|
285
|
+
{
|
|
286
|
+
name: "shadcn",
|
|
287
|
+
description: "UI",
|
|
288
|
+
location: "/work/app/.agents/skills/shadcn/SKILL.md",
|
|
289
|
+
content: "x",
|
|
290
|
+
},
|
|
291
|
+
{ name: "customize", description: "", location: "<built-in>", content: "x" },
|
|
292
|
+
{
|
|
293
|
+
name: "firecrawl",
|
|
294
|
+
description: "Web",
|
|
295
|
+
location: "/home/u/.config/opencode/skills/f/SKILL.md",
|
|
296
|
+
},
|
|
297
|
+
// Prefix path mirip tapi bukan di dalam Project -> global.
|
|
298
|
+
{ name: "other", location: "/work/app-2/SKILL.md" },
|
|
299
|
+
{ description: "tanpa nama" },
|
|
300
|
+
],
|
|
301
|
+
"/work/app/",
|
|
302
|
+
);
|
|
303
|
+
expect(out).toEqual([
|
|
304
|
+
{ name: "customize", description: null, location: null, source: "builtin" },
|
|
305
|
+
{
|
|
306
|
+
name: "firecrawl",
|
|
307
|
+
description: "Web",
|
|
308
|
+
location: "/home/u/.config/opencode/skills/f/SKILL.md",
|
|
309
|
+
source: "global",
|
|
310
|
+
},
|
|
311
|
+
{ name: "other", description: null, location: "/work/app-2/SKILL.md", source: "global" },
|
|
312
|
+
{
|
|
313
|
+
name: "shadcn",
|
|
314
|
+
description: "UI",
|
|
315
|
+
location: "/work/app/.agents/skills/shadcn/SKILL.md",
|
|
316
|
+
source: "project",
|
|
317
|
+
},
|
|
318
|
+
]);
|
|
319
|
+
expect(flattenSkills({})).toEqual([]);
|
|
320
|
+
});
|