@polderlabs/bizar 3.23.0 → 4.0.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 +72 -2
- package/bizar-dash/.bizar/activity.log +3 -0
- package/bizar-dash/.bizar/graph/.graphify_analysis.json +1540 -0
- package/bizar-dash/.bizar/graph/.graphify_labels.json +1 -0
- package/bizar-dash/.bizar/graph/GRAPH_REPORT.md +404 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/0661b816358db84dfdb7f10443db8a61152fd20b3fee950caf8fbf32920a1790.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/16da06d774142a6e5d74829ad982531286a6193ef798de8f593db029e123ec32.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/27e5697c01254c69c64c5a33896669073b40afb5ba88375102ae77fa5404136d.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/532997898027254aa4c0932243dc71d2f55888181232b65974961934997dc85b.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/84e3451a64c7f99f0cd6a88b0748d7426780486f53ab4935b7476b28c08f8293.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/88e5ea5f3e78fa69353fe9e6a8d1a05391d54605e25600cb125531eb5a6432ff.json +1 -0
- package/bizar-dash/.bizar/graph/cache/stat-index.json +1 -0
- package/bizar-dash/.bizar/graph/graph.html +307 -0
- package/bizar-dash/.bizar/graph/graph.json +40267 -0
- package/bizar-dash/.bizar/graph/manifest.json +762 -0
- package/bizar-dash/.graphifyignore +40 -0
- package/bizar-dash/.obsidian/README.md +14 -0
- package/bizar-dash/.obsidian/daily/2026-06-25.md +9 -0
- package/bizar-dash/.obsidian/vault.json +7 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/SKILL.md +103 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/assets/template.html +338 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/example.html +81 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/open-design.json +172 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/checklist.md +44 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/layouts.md +247 -0
- package/bizar-dash/CHANGELOG.md +474 -0
- package/bizar-dash/DESIGN.md +136 -0
- package/bizar-dash/dist/assets/main-B3RgW6FS.js +331 -0
- package/bizar-dash/dist/assets/main-B3RgW6FS.js.map +1 -0
- package/bizar-dash/dist/assets/main-DlJ8wgLR.css +1 -0
- package/bizar-dash/dist/assets/mobile-CsZQAswA.css +1 -0
- package/bizar-dash/dist/assets/mobile-DbxIw8BG.js +2 -0
- package/bizar-dash/dist/assets/mobile-DbxIw8BG.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js +354 -0
- package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js.map +1 -0
- package/bizar-dash/dist/index.html +40 -0
- package/bizar-dash/dist/mobile.html +22 -0
- package/bizar-dash/plans/debug-plan/comments.json +1 -0
- package/bizar-dash/plans/debug-plan/meta.json +10 -0
- package/bizar-dash/plans/debug-plan/plan.json +174 -0
- package/bizar-dash/scripts/smoke-bg-retry.mjs +246 -0
- package/bizar-dash/src/cli/dashboard-ports.mjs +488 -0
- package/bizar-dash/src/cli.mjs +522 -0
- package/bizar-dash/src/server/activity-log.mjs +174 -0
- package/bizar-dash/src/server/agents-store.mjs +465 -0
- package/bizar-dash/src/server/api.mjs +138 -0
- package/bizar-dash/src/server/artifact-template.html +810 -0
- package/bizar-dash/src/server/artifacts-store.mjs +733 -0
- package/bizar-dash/src/server/auth.mjs +381 -0
- package/bizar-dash/src/server/background-store.mjs +447 -0
- package/bizar-dash/src/server/bg-poller.mjs +305 -0
- package/bizar-dash/src/server/bg-retry.mjs +574 -0
- package/bizar-dash/src/server/browser.mjs +40 -0
- package/bizar-dash/src/server/diagnostics-store.mjs +148 -0
- package/bizar-dash/src/server/dialog-poller.mjs +105 -0
- package/bizar-dash/src/server/dialog-store.mjs +177 -0
- package/bizar-dash/src/server/glyphs/mdx-compiler.mjs +583 -0
- package/bizar-dash/src/server/lib/path-safe.mjs +283 -0
- package/bizar-dash/src/server/memory-git.mjs +249 -0
- package/bizar-dash/src/server/memory-schema.mjs +145 -0
- package/bizar-dash/src/server/memory-secrets.mjs +84 -0
- package/bizar-dash/src/server/memory-store.mjs +424 -0
- package/bizar-dash/src/server/mod-security.mjs +398 -0
- package/bizar-dash/src/server/mods-loader.mjs +1067 -0
- package/bizar-dash/src/server/notifications-store.mjs +243 -0
- package/bizar-dash/src/server/obsidian-store.mjs +318 -0
- package/bizar-dash/src/server/opencode-runner.mjs +279 -0
- package/bizar-dash/src/server/pair-store.mjs +138 -0
- package/bizar-dash/src/server/projects-store.mjs +364 -0
- package/bizar-dash/src/server/providers-store.mjs +789 -0
- package/bizar-dash/src/server/routes/_shared.mjs +415 -0
- package/bizar-dash/src/server/routes/activity.mjs +110 -0
- package/bizar-dash/src/server/routes/agents.mjs +134 -0
- package/bizar-dash/src/server/routes/artifacts.mjs +279 -0
- package/bizar-dash/src/server/routes/auth.mjs +69 -0
- package/bizar-dash/src/server/routes/background.mjs +165 -0
- package/bizar-dash/src/server/routes/chat.mjs +485 -0
- package/bizar-dash/src/server/routes/config.mjs +129 -0
- package/bizar-dash/src/server/routes/diagnostics.mjs +50 -0
- package/bizar-dash/src/server/routes/dialogs.mjs +86 -0
- package/bizar-dash/src/server/routes/fs.mjs +429 -0
- package/bizar-dash/src/server/routes/history.mjs +78 -0
- package/bizar-dash/src/server/routes/memory.mjs +499 -0
- package/bizar-dash/src/server/routes/misc.mjs +89 -0
- package/bizar-dash/src/server/routes/mods.mjs +359 -0
- package/bizar-dash/src/server/routes/notifications.mjs +54 -0
- package/bizar-dash/src/server/routes/obsidian.mjs +239 -0
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +55 -0
- package/bizar-dash/src/server/routes/overview.mjs +91 -0
- package/bizar-dash/src/server/routes/pair.mjs +67 -0
- package/bizar-dash/src/server/routes/projects.mjs +175 -0
- package/bizar-dash/src/server/routes/providers.mjs +47 -0
- package/bizar-dash/src/server/routes/schedules.mjs +104 -0
- package/bizar-dash/src/server/routes/settings.mjs +51 -0
- package/bizar-dash/src/server/routes/skills.mjs +76 -0
- package/bizar-dash/src/server/routes/tasks.mjs +468 -0
- package/bizar-dash/src/server/routes/themes.mjs +51 -0
- package/bizar-dash/src/server/routes-v2/auth.mjs +59 -0
- package/bizar-dash/src/server/routes-v2/events.mjs +107 -0
- package/bizar-dash/src/server/routes-v2/health.mjs +21 -0
- package/bizar-dash/src/server/routes-v2/index.mjs +90 -0
- package/bizar-dash/src/server/routes-v2/sessions.mjs +111 -0
- package/bizar-dash/src/server/schedules-runner.mjs +369 -0
- package/bizar-dash/src/server/schedules-store.mjs +268 -0
- package/bizar-dash/src/server/search-store.mjs +220 -0
- package/bizar-dash/src/server/serve-info.mjs +684 -0
- package/bizar-dash/src/server/server.mjs +796 -0
- package/bizar-dash/src/server/settings-store.mjs +45 -0
- package/bizar-dash/src/server/skills-store.mjs +300 -0
- package/bizar-dash/src/server/state.mjs +437 -0
- package/bizar-dash/src/server/tailscale-store.mjs +113 -0
- package/bizar-dash/src/server/task-delegator.mjs +824 -0
- package/bizar-dash/src/server/tasks-store.mjs +508 -0
- package/bizar-dash/src/server/tui.mjs +923 -0
- package/bizar-dash/src/server/update-store.mjs +168 -0
- package/bizar-dash/src/server/v2-auth-file.mjs +99 -0
- package/bizar-dash/src/server/v2-event-bus.mjs +128 -0
- package/bizar-dash/src/server/watcher.mjs +81 -0
- package/bizar-dash/src/server/yaml.mjs +238 -0
- package/bizar-dash/src/web/App.tsx +718 -0
- package/bizar-dash/src/web/MobileApp.tsx +408 -0
- package/bizar-dash/src/web/components/ArtifactCreateDialog.tsx +95 -0
- package/bizar-dash/src/web/components/ArtifactListDialog.tsx +55 -0
- package/bizar-dash/src/web/components/ArtifactViewer.tsx +143 -0
- package/bizar-dash/src/web/components/AuditDialog.tsx +79 -0
- package/bizar-dash/src/web/components/BgStatusBadge.tsx +32 -0
- package/bizar-dash/src/web/components/Button.tsx +56 -0
- package/bizar-dash/src/web/components/CanvasContextMenu.tsx +68 -0
- package/bizar-dash/src/web/components/Card.tsx +40 -0
- package/bizar-dash/src/web/components/CollapsibleSection.tsx +72 -0
- package/bizar-dash/src/web/components/CommandDialog.tsx +66 -0
- package/bizar-dash/src/web/components/EmptyState.tsx +30 -0
- package/bizar-dash/src/web/components/FileBrowser.tsx +633 -0
- package/bizar-dash/src/web/components/HelpDialog.tsx +55 -0
- package/bizar-dash/src/web/components/KillConfirmDialog.tsx +88 -0
- package/bizar-dash/src/web/components/Modal.tsx +215 -0
- package/bizar-dash/src/web/components/Notifications.tsx +241 -0
- package/bizar-dash/src/web/components/SearchModal.tsx +224 -0
- package/bizar-dash/src/web/components/Sidebar.tsx +66 -0
- package/bizar-dash/src/web/components/Spinner.tsx +19 -0
- package/bizar-dash/src/web/components/StatusBadge.tsx +25 -0
- package/bizar-dash/src/web/components/Tag.tsx +28 -0
- package/bizar-dash/src/web/components/Toast.tsx +150 -0
- package/bizar-dash/src/web/components/Topbar.tsx +368 -0
- package/bizar-dash/src/web/components/VisualPlanDialog.tsx +58 -0
- package/bizar-dash/src/web/components/chat/AgentChip.tsx +68 -0
- package/bizar-dash/src/web/components/chat/ChatBubble.tsx +60 -0
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +88 -0
- package/bizar-dash/src/web/components/chat/ChatTopBar.tsx +50 -0
- package/bizar-dash/src/web/components/chat/Composer.tsx +164 -0
- package/bizar-dash/src/web/components/chat/ConfirmModal.tsx +54 -0
- package/bizar-dash/src/web/components/chat/EmptyState.tsx +21 -0
- package/bizar-dash/src/web/components/chat/FirstRunGreeting.tsx +18 -0
- package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +32 -0
- package/bizar-dash/src/web/components/chat/InfoPanel.tsx +119 -0
- package/bizar-dash/src/web/components/chat/LoadingSkeleton.tsx +19 -0
- package/bizar-dash/src/web/components/chat/MessageBubble.tsx +60 -0
- package/bizar-dash/src/web/components/chat/SessionList.tsx +153 -0
- package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +9 -0
- package/bizar-dash/src/web/components/chat/SuggestionCards.tsx +55 -0
- package/bizar-dash/src/web/components/chat/WelcomeScreen.tsx +36 -0
- package/bizar-dash/src/web/components/chat/index.ts +20 -0
- package/bizar-dash/src/web/components/chat/useAutoGrowTextarea.ts +18 -0
- package/bizar-dash/src/web/components/chat/useChat.ts +219 -0
- package/bizar-dash/src/web/components/chat/useSlashCommands.ts +47 -0
- package/bizar-dash/src/web/index.html +37 -0
- package/bizar-dash/src/web/lib/api.ts +193 -0
- package/bizar-dash/src/web/lib/markdown.tsx +59 -0
- package/bizar-dash/src/web/lib/types.ts +668 -0
- package/bizar-dash/src/web/lib/utils.ts +114 -0
- package/bizar-dash/src/web/lib/ws.ts +176 -0
- package/bizar-dash/src/web/main.tsx +34 -0
- package/bizar-dash/src/web/mobile/MobileApp.tsx +278 -0
- package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +38 -0
- package/bizar-dash/src/web/mobile/MobileTopbar.tsx +115 -0
- package/bizar-dash/src/web/mobile/components/MobileBottomSheet.tsx +182 -0
- package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +64 -0
- package/bizar-dash/src/web/mobile/components/MobileModal.tsx +123 -0
- package/bizar-dash/src/web/mobile/views/MobileActivity.tsx +273 -0
- package/bizar-dash/src/web/mobile/views/MobileAgents.tsx +374 -0
- package/bizar-dash/src/web/mobile/views/MobileArtifactCanvas.tsx +439 -0
- package/bizar-dash/src/web/mobile/views/MobileArtifacts.tsx +143 -0
- package/bizar-dash/src/web/mobile/views/MobileChat.tsx +180 -0
- package/bizar-dash/src/web/mobile/views/MobileConfig.tsx +175 -0
- package/bizar-dash/src/web/mobile/views/MobileHistory.tsx +191 -0
- package/bizar-dash/src/web/mobile/views/MobileMods.tsx +149 -0
- package/bizar-dash/src/web/mobile/views/MobileMore.tsx +121 -0
- package/bizar-dash/src/web/mobile/views/MobileNotifications.tsx +137 -0
- package/bizar-dash/src/web/mobile/views/MobilePlans.tsx +26 -0
- package/bizar-dash/src/web/mobile/views/MobileSchedules.tsx +685 -0
- package/bizar-dash/src/web/mobile/views/MobileSearchModal.tsx +172 -0
- package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +388 -0
- package/bizar-dash/src/web/mobile/views/MobileSkills.tsx +153 -0
- package/bizar-dash/src/web/mobile/views/MobileTasks.tsx +564 -0
- package/bizar-dash/src/web/mobile.html +20 -0
- package/bizar-dash/src/web/mobile.tsx +13 -0
- package/bizar-dash/src/web/styles/chat.css +198 -0
- package/bizar-dash/src/web/styles/glyphs.css +960 -0
- package/bizar-dash/src/web/styles/main.css +8855 -0
- package/bizar-dash/src/web/styles/mobile-chat.css +5 -0
- package/bizar-dash/src/web/styles/mobile.css +1960 -0
- package/bizar-dash/src/web/views/Activity.tsx +1203 -0
- package/bizar-dash/src/web/views/Agents.tsx +701 -0
- package/bizar-dash/src/web/views/Artifacts.tsx +1480 -0
- package/bizar-dash/src/web/views/BackgroundAgents.tsx +450 -0
- package/bizar-dash/src/web/views/Chat.tsx +190 -0
- package/bizar-dash/src/web/views/Config.tsx +1603 -0
- package/bizar-dash/src/web/views/History.tsx +335 -0
- package/bizar-dash/src/web/views/ModView.tsx +207 -0
- package/bizar-dash/src/web/views/Mods.tsx +693 -0
- package/bizar-dash/src/web/views/Overview.tsx +812 -0
- package/bizar-dash/src/web/views/Providers.tsx +302 -0
- package/bizar-dash/src/web/views/Schedules.tsx +802 -0
- package/bizar-dash/src/web/views/Settings.tsx +1787 -0
- package/bizar-dash/src/web/views/Skills.tsx +431 -0
- package/bizar-dash/src/web/views/Tasks.tsx +1523 -0
- package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +613 -0
- package/bizar-dash/src/web/views/glyphs/components.tsx +1098 -0
- package/bizar-dash/templates/mod/FORMAT.md +76 -0
- package/bizar-dash/templates/mod/hello-mod/README.md +19 -0
- package/bizar-dash/templates/mod/hello-mod/agents/greeter.md +8 -0
- package/bizar-dash/templates/mod/hello-mod/commands/hello.md +6 -0
- package/bizar-dash/templates/mod/hello-mod/mod.json +20 -0
- package/bizar-dash/templates/mod/hello-mod/routes/ping.mjs +9 -0
- package/bizar-dash/templates/mod/hello-mod/views/HelloView.tsx +10 -0
- package/bizar-dash/tests/dashboard-ports.test.mjs +138 -0
- package/bizar-dash/tests/graphify-mod-spawn.node.test.mjs +90 -0
- package/bizar-dash/tests/memory-git.test.mjs +138 -0
- package/bizar-dash/tests/memory-schema.test.mjs +198 -0
- package/bizar-dash/tests/memory-secrets.test.mjs +121 -0
- package/bizar-dash/tests/memory-store.test.mjs +390 -0
- package/bizar-dash/tests/memory-sync.test.mjs +141 -0
- package/bizar-dash/tests/mod-instructions.node.test.mjs +188 -0
- package/bizar-dash/tests/mod-security.test.mjs +273 -0
- package/bizar-dash/tests/mod-upgrade.node.test.mjs +357 -0
- package/bizar-dash/tests/no-agent-browser.node.test.mjs +98 -0
- package/bizar-dash/tests/obsidian-back-compat.test.mjs +199 -0
- package/bizar-dash/tests/opencode-runner.test.mjs +172 -0
- package/bizar-dash/tests/path-safe.test.mjs +108 -0
- package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +333 -0
- package/bizar-dash/tests/smoke-v2.mjs +212 -0
- package/bizar-dash/tests/strip-thinking.test.mjs +175 -0
- package/bizar-dash/tests/submit-feedback.test.mjs +353 -0
- package/bizar-dash/tests/tmux-wrap.test.mjs +145 -0
- package/bizar-dash/tests/yaml.test.mjs +128 -0
- package/bizar-dash/tsconfig.json +23 -0
- package/bizar-dash/vite.config.ts +27 -0
- package/cli/atomic.mjs +73 -0
- package/cli/bin.mjs +43 -46
- package/cli/copy.mjs +3 -0
- package/cli/dev-link.mjs +17 -1
- package/cli/init.mjs +135 -1
- package/cli/install.mjs +10 -23
- package/cli/memory-constants.mjs +59 -0
- package/cli/memory.mjs +757 -0
- package/cli/service.mjs +5 -6
- package/config/skills/obsidian/SKILL.md +213 -65
- package/install.sh +48 -18
- package/package.json +49 -14
- package/packages/sdk/LICENSE +21 -0
- package/packages/sdk/README.md +80 -0
- package/packages/sdk/src/client.ts +183 -0
- package/packages/sdk/src/errors.ts +120 -0
- package/packages/sdk/src/events.ts +153 -0
- package/packages/sdk/src/index.ts +63 -0
- package/packages/sdk/src/types.ts +176 -0
- package/packages/sdk/src/version.ts +4 -0
- package/packages/sdk/tests/client.test.ts +217 -0
- package/packages/sdk/tests/errors.test.ts +108 -0
- package/packages/sdk/tests/events.test.ts +139 -0
- package/packages/sdk/tests/fixtures/fetch-mock.ts +152 -0
- package/packages/sdk/tests/fixtures/sse-mock.ts +30 -0
- package/packages/sdk/tsconfig.json +25 -0
- package/packages/sdk/vitest.config.ts +9 -0
- package/plugins/bizar/LICENSE +21 -0
- package/plugins/bizar/README.md +448 -0
- package/plugins/bizar/dist/index.js +29901 -0
- package/plugins/bizar/index.ts +1484 -0
- package/plugins/bizar/scripts/check-forbidden-imports.sh +33 -0
- package/plugins/bizar/src/background-state.ts +551 -0
- package/plugins/bizar/src/background.ts +1250 -0
- package/plugins/bizar/src/commands-impl.ts +464 -0
- package/plugins/bizar/src/commands.ts +1127 -0
- package/plugins/bizar/src/dashboard-client.ts +233 -0
- package/plugins/bizar/src/event-stream.ts +606 -0
- package/plugins/bizar/src/fingerprint.ts +120 -0
- package/plugins/bizar/src/handoff.ts +79 -0
- package/plugins/bizar/src/http-client.ts +467 -0
- package/plugins/bizar/src/key-rotation.ts +218 -0
- package/plugins/bizar/src/logger.ts +144 -0
- package/plugins/bizar/src/loop.ts +176 -0
- package/plugins/bizar/src/opencode-runner.ts +390 -0
- package/plugins/bizar/src/options.ts +421 -0
- package/plugins/bizar/src/plan-fs.ts +323 -0
- package/plugins/bizar/src/reasoning-clean.ts +454 -0
- package/plugins/bizar/src/report.ts +178 -0
- package/plugins/bizar/src/research-prompt.ts +35 -0
- package/plugins/bizar/src/serve-info.ts +228 -0
- package/plugins/bizar/src/serve.ts +496 -0
- package/plugins/bizar/src/settings.ts +349 -0
- package/plugins/bizar/src/state.ts +298 -0
- package/plugins/bizar/src/tools/bg-collect.ts +104 -0
- package/plugins/bizar/src/tools/bg-get-comments.ts +239 -0
- package/plugins/bizar/src/tools/bg-kill.ts +87 -0
- package/plugins/bizar/src/tools/bg-spawn.ts +401 -0
- package/plugins/bizar/src/tools/bg-status.ts +99 -0
- package/plugins/bizar/src/tools/plan-action.ts +767 -0
- package/plugins/bizar/src/tools/read-glyph-feedback.ts +170 -0
- package/plugins/bizar/src/tools/wait-for-feedback.ts +402 -0
- package/plugins/bizar/tests/attach-handler-bug.test.ts +169 -0
- package/plugins/bizar/tests/background-state.test.ts +277 -0
- package/plugins/bizar/tests/background.test.ts +402 -0
- package/plugins/bizar/tests/block.test.ts +195 -0
- package/plugins/bizar/tests/canonical-key-order.test.ts +75 -0
- package/plugins/bizar/tests/commands-impl.test.ts +442 -0
- package/plugins/bizar/tests/commands.test.ts +548 -0
- package/plugins/bizar/tests/config.test.ts +122 -0
- package/plugins/bizar/tests/dashboard-client.test.ts +159 -0
- package/plugins/bizar/tests/dispose.test.ts +336 -0
- package/plugins/bizar/tests/event-stream.test.ts +409 -0
- package/plugins/bizar/tests/event.test.ts +262 -0
- package/plugins/bizar/tests/fingerprint.test.ts +162 -0
- package/plugins/bizar/tests/http-client.test.ts +404 -0
- package/plugins/bizar/tests/init-helpers.test.ts +203 -0
- package/plugins/bizar/tests/integration/slash-command.test.ts +348 -0
- package/plugins/bizar/tests/integration/tool-routing.test.ts +314 -0
- package/plugins/bizar/tests/key-rotation.test.ts +396 -0
- package/plugins/bizar/tests/loop.test.ts +397 -0
- package/plugins/bizar/tests/options.test.ts +276 -0
- package/plugins/bizar/tests/reasoning-clean.test.ts +422 -0
- package/plugins/bizar/tests/serve.test.ts +339 -0
- package/plugins/bizar/tests/settings.test.ts +351 -0
- package/plugins/bizar/tests/stall-think.test.ts +750 -0
- package/plugins/bizar/tests/state.test.ts +276 -0
- package/plugins/bizar/tests/tools/bg-collect.test.ts +337 -0
- package/plugins/bizar/tests/tools/bg-get-comments.test.ts +485 -0
- package/plugins/bizar/tests/tools/bg-kill.test.ts +235 -0
- package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +147 -0
- package/plugins/bizar/tests/tools/bg-spawn.test.ts +311 -0
- package/plugins/bizar/tests/tools/bg-status.test.ts +217 -0
- package/plugins/bizar/tests/tools/opencode-runner.test.ts +115 -0
- package/plugins/bizar/tests/tools/plan-action.test.ts +599 -0
- package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +253 -0
- package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +390 -0
- package/plugins/bizar/tests/update-deadlock.test.ts +145 -0
- package/plugins/bizar/tsconfig.json +29 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/opencode-runner.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for the dashboard's opencode-runner.mjs. The
|
|
5
|
+
* companion plugin runner (plugins/bizar/src/opencode-runner.ts) is
|
|
6
|
+
* tested via bun in plugins/bizar/tests/.
|
|
7
|
+
*
|
|
8
|
+
* Run with: node --test tests/opencode-runner.test.mjs
|
|
9
|
+
*/
|
|
10
|
+
import { test } from 'node:test';
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import { mkdtempSync, writeFileSync, existsSync, readFileSync, statSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
|
|
17
|
+
import { spawnAgent, getStatus, onExit, killAgent, list, _resetForTests } from '../src/server/opencode-runner.mjs';
|
|
18
|
+
|
|
19
|
+
const HAS_OPENCODE = (() => {
|
|
20
|
+
try {
|
|
21
|
+
const r = spawnSync('which', ['opencode'], { encoding: 'utf8' });
|
|
22
|
+
return r.status === 0 && r.stdout.trim().length > 0;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
27
|
+
|
|
28
|
+
test('_resetForTests clears the registry', () => {
|
|
29
|
+
list(); // sanity check no throw
|
|
30
|
+
_resetForTests();
|
|
31
|
+
assert.equal(list().length, 0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('getStatus returns null for unknown processId', () => {
|
|
35
|
+
_resetForTests();
|
|
36
|
+
assert.equal(getStatus(99999), null);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('killAgent on unknown processId is a no-op success', () => {
|
|
40
|
+
_resetForTests();
|
|
41
|
+
const res = killAgent(99999);
|
|
42
|
+
assert.equal(res.ok, true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('spawnAgent with non-existent worktree still returns a processId', { skip: !HAS_OPENCODE }, async () => {
|
|
46
|
+
_resetForTests();
|
|
47
|
+
const dir = mkdtempSync(join(tmpdir(), 'oc-runner-'));
|
|
48
|
+
const logPath = join(dir, 'test.log');
|
|
49
|
+
// Worktree doesn't exist, but spawn should still succeed and
|
|
50
|
+
// opencode will exit early with an error. We just verify the API
|
|
51
|
+
// contract: we get back a processId immediately, and the spawn
|
|
52
|
+
// resolves once the sessionId appears or the process exits.
|
|
53
|
+
const result = await Promise.race([
|
|
54
|
+
spawnAgent({
|
|
55
|
+
prompt: 'just say hi',
|
|
56
|
+
agent: 'mimir',
|
|
57
|
+
worktree: '/this/does/not/exist/anywhere',
|
|
58
|
+
logPath,
|
|
59
|
+
sessionIdTimeoutMs: 3000,
|
|
60
|
+
}),
|
|
61
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('spawnAgent timeout')), 5000)),
|
|
62
|
+
]);
|
|
63
|
+
// Either ok:false (process exited before reporting session) or
|
|
64
|
+
// ok:true (rare if opencode somehow succeeded) is acceptable.
|
|
65
|
+
assert.ok(typeof result.ok === 'boolean', 'expected ok to be boolean');
|
|
66
|
+
// The log dir should exist (spawnAgent creates it).
|
|
67
|
+
assert.ok(existsSync(dir), 'log dir should exist');
|
|
68
|
+
// No process should still be tracked if the spawn failed.
|
|
69
|
+
if (!result.ok) {
|
|
70
|
+
assert.ok(!result.sessionId, 'no sessionId on failure');
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('spawnAgent creates log dir if missing', { skip: !HAS_OPENCODE }, async () => {
|
|
75
|
+
_resetForTests();
|
|
76
|
+
const tmp = mkdtempSync(join(tmpdir(), 'oc-runner-logdir-'));
|
|
77
|
+
const nested = join(tmp, 'a', 'b', 'c');
|
|
78
|
+
const logPath = join(nested, 'test.log');
|
|
79
|
+
// The dir nested/a/b/c does not exist yet. spawnAgent should
|
|
80
|
+
// create it. The actual opencode process will likely fail (no
|
|
81
|
+
// worktree, etc.) but that's irrelevant to this test.
|
|
82
|
+
await Promise.race([
|
|
83
|
+
spawnAgent({
|
|
84
|
+
prompt: 'hi',
|
|
85
|
+
agent: 'mimir',
|
|
86
|
+
worktree: tmp,
|
|
87
|
+
logPath,
|
|
88
|
+
sessionIdTimeoutMs: 1000,
|
|
89
|
+
}),
|
|
90
|
+
new Promise((resolve) => setTimeout(resolve, 5000)),
|
|
91
|
+
]).catch(() => undefined);
|
|
92
|
+
assert.ok(existsSync(nested), `expected ${nested} to exist`);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('spawnAgent captures stdout+stderr to logPath', { skip: !HAS_OPENCODE }, async () => {
|
|
96
|
+
_resetForTests();
|
|
97
|
+
const tmp = mkdtempSync(join(tmpdir(), 'oc-runner-cap-'));
|
|
98
|
+
const logPath = join(tmp, 'capture.log');
|
|
99
|
+
// Use /tmp as worktree — it exists. opencode will run there.
|
|
100
|
+
const res = await Promise.race([
|
|
101
|
+
spawnAgent({
|
|
102
|
+
prompt: 'just say hi',
|
|
103
|
+
agent: 'mimir',
|
|
104
|
+
worktree: tmp,
|
|
105
|
+
logPath,
|
|
106
|
+
sessionIdTimeoutMs: 5000,
|
|
107
|
+
}),
|
|
108
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 8000)),
|
|
109
|
+
]);
|
|
110
|
+
// Resolved (either ok or with error). Wait briefly for the log
|
|
111
|
+
// stream to flush.
|
|
112
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
113
|
+
if (existsSync(logPath)) {
|
|
114
|
+
const stat = statSync(logPath);
|
|
115
|
+
assert.ok(stat.size >= 0, 'log file has size');
|
|
116
|
+
// If we wrote anything, it should be either [stdout] or [stderr]
|
|
117
|
+
// prefixed. (If the file is empty, the test passes trivially.)
|
|
118
|
+
if (stat.size > 0) {
|
|
119
|
+
const content = readFileSync(logPath, 'utf8');
|
|
120
|
+
// Either content is empty (process didn't output anything)
|
|
121
|
+
// or contains our prefix tags. We don't assert specific
|
|
122
|
+
// content because the opencode run is environment-dependent.
|
|
123
|
+
assert.ok(
|
|
124
|
+
content.includes('[stderr]') || content.includes('[stdout]') || content.length === 0,
|
|
125
|
+
'log file should be empty or contain [stderr]/[stdout] prefixes',
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// If the process is still running, kill it to clean up.
|
|
130
|
+
if (res.processId && getStatus(res.processId) && getStatus(res.processId).state === 'running') {
|
|
131
|
+
killAgent(res.processId);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('onExit fires when process exits', { skip: !HAS_OPENCODE }, async () => {
|
|
136
|
+
_resetForTests();
|
|
137
|
+
const tmp = mkdtempSync(join(tmpdir(), 'oc-runner-exit-'));
|
|
138
|
+
const logPath = join(tmp, 'exit.log');
|
|
139
|
+
const res = await Promise.race([
|
|
140
|
+
spawnAgent({
|
|
141
|
+
prompt: 'hi',
|
|
142
|
+
agent: 'mimir',
|
|
143
|
+
worktree: tmp,
|
|
144
|
+
logPath,
|
|
145
|
+
sessionIdTimeoutMs: 3000,
|
|
146
|
+
}),
|
|
147
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 5000)),
|
|
148
|
+
]).catch(() => ({ ok: false, processId: undefined }));
|
|
149
|
+
|
|
150
|
+
if (!res.processId) {
|
|
151
|
+
// Process never started; test is moot.
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// onExit should fire within a reasonable time.
|
|
156
|
+
const status = await Promise.race([
|
|
157
|
+
new Promise((resolve) => {
|
|
158
|
+
onExit(res.processId, (s) => resolve(s));
|
|
159
|
+
}),
|
|
160
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('onExit timeout')), 10_000)),
|
|
161
|
+
]).catch(() => null);
|
|
162
|
+
|
|
163
|
+
// status may be null if the test timed out; either way the test
|
|
164
|
+
// is best-effort in this environment.
|
|
165
|
+
if (status) {
|
|
166
|
+
assert.ok(
|
|
167
|
+
['done', 'failed', 'killed'].includes(status.state),
|
|
168
|
+
`state should be terminal, got ${status.state}`,
|
|
169
|
+
);
|
|
170
|
+
assert.ok(typeof status.endedAt === 'number', 'endedAt set');
|
|
171
|
+
}
|
|
172
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/path-safe.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for the bg-log path helpers. Run with:
|
|
5
|
+
* node --test tests/path-safe.test.mjs
|
|
6
|
+
*
|
|
7
|
+
* Covers:
|
|
8
|
+
* - getActualBgLogPath returns the path the plugin's LogWriter
|
|
9
|
+
* actually writes to (per plugins/bizar/src/options.ts:88 default
|
|
10
|
+
* logDir of ~/.cache/bizar/logs).
|
|
11
|
+
* - BIZAR_LOG_DIR env override is honored.
|
|
12
|
+
* - Path sanitizer strips unsafe characters from the session id.
|
|
13
|
+
* - The old phantom path (`<worktree>/.bizar/opencode.log`) is NOT
|
|
14
|
+
* returned — regression guard for the bug fixed in v3.11.1.
|
|
15
|
+
* - deriveAbsoluteBgLogPath still returns the historical worktree-
|
|
16
|
+
* based path (so we don't accidentally break callers depending on
|
|
17
|
+
* the old field).
|
|
18
|
+
*/
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import assert from 'node:assert/strict';
|
|
21
|
+
import { resolve as pathResolve, sep } from 'node:path';
|
|
22
|
+
import { homedir } from 'node:os';
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
getBgLogDir,
|
|
26
|
+
getActualBgLogPath,
|
|
27
|
+
deriveAbsoluteBgLogPath,
|
|
28
|
+
} from '../src/server/lib/path-safe.mjs';
|
|
29
|
+
|
|
30
|
+
const HOME = homedir();
|
|
31
|
+
const FALLBACK = pathResolve(HOME, '.cache', 'bizar', 'logs');
|
|
32
|
+
|
|
33
|
+
test('getBgLogDir returns the fallback when no env override', () => {
|
|
34
|
+
const dir = getBgLogDir({ env: {} });
|
|
35
|
+
assert.equal(dir, FALLBACK);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('getBgLogDir honors BIZAR_LOG_DIR env override', () => {
|
|
39
|
+
const dir = getBgLogDir({ env: { BIZAR_LOG_DIR: '/tmp/bizar-test' } });
|
|
40
|
+
assert.equal(dir, pathResolve('/tmp/bizar-test'));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('getActualBgLogPath returns path the LogWriter writes to', () => {
|
|
44
|
+
const p = getActualBgLogPath({ sessionId: 'ses_abc123' });
|
|
45
|
+
// The plugin's default logDir is ~/.cache/bizar/logs; the file is
|
|
46
|
+
// named <sessionId>.log. This must match the LogWriter's actual
|
|
47
|
+
// output path (plugins/bizar/src/report.ts:147).
|
|
48
|
+
assert.equal(p, pathResolve(FALLBACK, 'ses_abc123.log'));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('getActualBgLogPath sanitizes unsafe characters in sessionId', () => {
|
|
52
|
+
const p = getActualBgLogPath({ sessionId: '../../etc/passwd' });
|
|
53
|
+
// The `..` and `/` get replaced with `_`. The result must be a
|
|
54
|
+
// path under the log dir, never escape it via path traversal.
|
|
55
|
+
// Note: literal `..` can appear inside a single filename segment
|
|
56
|
+
// (e.g. `.._.._etc_passwd.log`) without escaping — only the
|
|
57
|
+
// directory boundaries matter. We assert the resolved path stays
|
|
58
|
+
// inside FALLBACK_LOG_DIR.
|
|
59
|
+
assert.ok(p.startsWith(FALLBACK + sep), `expected path to start with ${FALLBACK}, got ${p}`);
|
|
60
|
+
const rel = p.slice(FALLBACK.length + 1);
|
|
61
|
+
// The single filename segment must not itself contain `/` (which
|
|
62
|
+
// would mean an attempt to escape via the basename).
|
|
63
|
+
assert.ok(!rel.includes('/'), `expected no '/' in basename, got ${rel}`);
|
|
64
|
+
assert.ok(p.endsWith('.log'));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('getActualBgLogPath uses BIZAR_LOG_DIR env override', () => {
|
|
68
|
+
const p = getActualBgLogPath({
|
|
69
|
+
sessionId: 'ses_xyz',
|
|
70
|
+
env: { BIZAR_LOG_DIR: '/tmp/bizar-test' },
|
|
71
|
+
});
|
|
72
|
+
assert.equal(p, pathResolve('/tmp/bizar-test', 'ses_xyz.log'));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('getActualBgLogPath does NOT return the phantom .bizar/opencode.log path (regression guard)', () => {
|
|
76
|
+
// The old code at task-delegator.mjs:605 tailed
|
|
77
|
+
// `<worktree>/.bizar/opencode.log` — a path nothing writes to.
|
|
78
|
+
// The new function must NOT return anything under `.bizar/` or
|
|
79
|
+
// `.opencode/log/`.
|
|
80
|
+
const p = getActualBgLogPath({ sessionId: 'ses_regression' });
|
|
81
|
+
assert.ok(!p.includes('/.bizar/'), `phantom '.bizar/' path leaked: ${p}`);
|
|
82
|
+
assert.ok(!p.includes('/.opencode/log/'), `phantom '.opencode/log/' path leaked: ${p}`);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('deriveAbsoluteBgLogPath still returns the historical worktree-based path', () => {
|
|
86
|
+
// The bg-retry loop still uses this to repair stale logPath fields.
|
|
87
|
+
// Make sure the behavior didn't change as part of the v3.11.1 fix.
|
|
88
|
+
const p = deriveAbsoluteBgLogPath('/tmp/worktree', 'bgr_abc');
|
|
89
|
+
assert.equal(p, pathResolve('/tmp/worktree', '.opencode', 'log', 'bgr_abc.log'));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('deriveAbsoluteBgLogPath falls back to FALLBACK_LOG_DIR when worktree missing', () => {
|
|
93
|
+
const p = deriveAbsoluteBgLogPath('', 'bgr_xyz');
|
|
94
|
+
assert.equal(p, pathResolve(FALLBACK, '.opencode', 'log', 'bgr_xyz.log'));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('getActualBgLogPath is preferred over deriveAbsoluteBgLogPath for operator-facing tail targets', () => {
|
|
98
|
+
// Document the contract: when you want to show the operator a real
|
|
99
|
+
// log, use getActualBgLogPath. The historical worktree-based path
|
|
100
|
+
// exists only for repairing stale state files.
|
|
101
|
+
const sessionId = 'ses_operator_view';
|
|
102
|
+
const realPath = getActualBgLogPath({ sessionId });
|
|
103
|
+
const worktreePath = deriveAbsoluteBgLogPath('/tmp/proj', 'bg_' + sessionId.slice(3, 19));
|
|
104
|
+
assert.notEqual(realPath, worktreePath);
|
|
105
|
+
// Real path is in the LogWriter's directory; worktree path is not.
|
|
106
|
+
assert.ok(realPath.startsWith(FALLBACK));
|
|
107
|
+
assert.ok(worktreePath.startsWith('/tmp/proj'));
|
|
108
|
+
});
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* providers-store-backup-keys.node.test.mjs — regression tests for the
|
|
3
|
+
* v3.20.10 backup-keys feature.
|
|
4
|
+
*
|
|
5
|
+
* The dashboard's Providers card + the API provider config now support
|
|
6
|
+
* a second `backupApiKey` slot per provider. Operators can:
|
|
7
|
+
* - Set primary in opencode.json (`apiKey`)
|
|
8
|
+
* - Set backup in opencode.json (`backupApiKey`)
|
|
9
|
+
* - Set primary via env (`MINIMAX_API_KEY`, etc.)
|
|
10
|
+
* - Set backup via env (`MINIMAX_API_KEY_BACKUP`, etc.)
|
|
11
|
+
* - Have a backup without a primary (rare; valid)
|
|
12
|
+
*
|
|
13
|
+
* Detection order (highest priority first):
|
|
14
|
+
* 1. config.provider.<id>.apiKey
|
|
15
|
+
* 2. config.provider.<id>.backupApiKey
|
|
16
|
+
* 3. process.env[envKey] (primary)
|
|
17
|
+
* 4. process.env[backupEnvKey] (backup)
|
|
18
|
+
*
|
|
19
|
+
* Run with: node --test tests/providers-store-backup-keys.node.test.mjs
|
|
20
|
+
*/
|
|
21
|
+
import { describe, it, before, after, beforeEach } from 'node:test';
|
|
22
|
+
import assert from 'node:assert/strict';
|
|
23
|
+
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
|
24
|
+
import { join, resolve } from 'node:path';
|
|
25
|
+
import { tmpdir } from 'node:os';
|
|
26
|
+
|
|
27
|
+
const REPO = resolve(import.meta.dirname, '..', '..');
|
|
28
|
+
|
|
29
|
+
let SANDBOX_HOME;
|
|
30
|
+
let ORIGINAL_HOME;
|
|
31
|
+
let ORIGINAL_ENV_KEYS;
|
|
32
|
+
|
|
33
|
+
before(() => {
|
|
34
|
+
SANDBOX_HOME = mkdtempSync(join(tmpdir(), `bizar-providers-test-${Date.now()}-`));
|
|
35
|
+
ORIGINAL_HOME = process.env.HOME;
|
|
36
|
+
ORIGINAL_ENV_KEYS = {
|
|
37
|
+
MINIMAX_API_KEY: process.env.MINIMAX_API_KEY,
|
|
38
|
+
MINIMAX_API_KEY_BACKUP: process.env.MINIMAX_API_KEY_BACKUP,
|
|
39
|
+
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
|
40
|
+
OPENAI_API_KEY_BACKUP: process.env.OPENAI_API_KEY_BACKUP,
|
|
41
|
+
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
|
42
|
+
};
|
|
43
|
+
process.env.HOME = SANDBOX_HOME;
|
|
44
|
+
// Wipe known env vars so tests are deterministic.
|
|
45
|
+
delete process.env.MINIMAX_API_KEY;
|
|
46
|
+
delete process.env.MINIMAX_API_KEY_BACKUP;
|
|
47
|
+
delete process.env.OPENAI_API_KEY;
|
|
48
|
+
delete process.env.OPENAI_API_KEY_BACKUP;
|
|
49
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
after(() => {
|
|
53
|
+
process.env.HOME = ORIGINAL_HOME;
|
|
54
|
+
for (const [k, v] of Object.entries(ORIGINAL_ENV_KEYS)) {
|
|
55
|
+
if (v === undefined) delete process.env[k];
|
|
56
|
+
else process.env[k] = v;
|
|
57
|
+
}
|
|
58
|
+
try { rmSync(SANDBOX_HOME, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
async function writeOpencodeJson(obj) {
|
|
62
|
+
const { mkdirSync } = await import('node:fs');
|
|
63
|
+
const cfgDir = join(SANDBOX_HOME, '.config', 'opencode');
|
|
64
|
+
mkdirSync(cfgDir, { recursive: true });
|
|
65
|
+
writeFileSync(join(cfgDir, 'opencode.json'), JSON.stringify(obj, null, 2));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function resetOpencodeJson() {
|
|
69
|
+
const { mkdirSync } = await import('node:fs');
|
|
70
|
+
const cfgDir = join(SANDBOX_HOME, '.config', 'opencode');
|
|
71
|
+
mkdirSync(cfgDir, { recursive: true });
|
|
72
|
+
writeFileSync(join(cfgDir, 'opencode.json'), '{}');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('providers-store — backup keys (v3.20.10)', () => {
|
|
76
|
+
// We re-import the module under each test so HOME/env changes take
|
|
77
|
+
// effect. node:import-cache can bite us otherwise.
|
|
78
|
+
async function loadStore() {
|
|
79
|
+
// Bust the import cache so the module re-evaluates homedir().
|
|
80
|
+
const cacheBust = '?cb=' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
|
81
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + cacheBust);
|
|
82
|
+
return mod.providersStore;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe('KNOWN_PROVIDERS — backupEnvKeys', () => {
|
|
86
|
+
it('declares backupEnvKeys for every known provider', async () => {
|
|
87
|
+
const store = await loadStore();
|
|
88
|
+
assert.ok(Array.isArray(store.KNOWN_PROVIDERS));
|
|
89
|
+
assert.ok(store.KNOWN_PROVIDERS.length >= 9, 'expected at least 9 known providers');
|
|
90
|
+
for (const spec of store.KNOWN_PROVIDERS) {
|
|
91
|
+
assert.ok(Array.isArray(spec.envKeys), `${spec.id} missing envKeys`);
|
|
92
|
+
assert.ok(spec.envKeys.length >= 1, `${spec.id} has empty envKeys`);
|
|
93
|
+
assert.ok(Array.isArray(spec.backupEnvKeys), `${spec.id} missing backupEnvKeys`);
|
|
94
|
+
assert.ok(spec.backupEnvKeys.length >= 1, `${spec.id} has empty backupEnvKeys`);
|
|
95
|
+
// Backup envKeys must NOT overlap with primary envKeys.
|
|
96
|
+
const overlap = spec.envKeys.filter((k) => spec.backupEnvKeys.includes(k));
|
|
97
|
+
assert.equal(overlap.length, 0, `${spec.id} backup envKey overlaps primary: ${overlap.join(',')}`);
|
|
98
|
+
// All backup envKeys should follow one of the two naming
|
|
99
|
+
// conventions:
|
|
100
|
+
// - `<NAME>_API_KEY_BACKUP` (api-key-then-backup)
|
|
101
|
+
// - `<NAME>_BACKUP_API_KEY` (backup-then-api-key)
|
|
102
|
+
// We accept either; both are conventional. We reject anything
|
|
103
|
+
// else to catch typos like `MINIMAX_BACKUP` (missing `_API_KEY`).
|
|
104
|
+
for (const bk of spec.backupEnvKeys) {
|
|
105
|
+
const ok = /_(?:API_KEY_BACKUP|BACKUP_API_KEY)$/.test(bk);
|
|
106
|
+
assert.ok(ok, `${spec.id} backup envKey ${bk} doesn't end with _API_KEY_BACKUP or _BACKUP_API_KEY`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('MiniMax has 3 backup envKeys including Anthropic fallback', async () => {
|
|
112
|
+
const store = await loadStore();
|
|
113
|
+
const minimax = store.KNOWN_PROVIDERS.find((p) => p.id === 'minimax');
|
|
114
|
+
assert.ok(minimax);
|
|
115
|
+
assert.deepEqual(minimax.envKeys, ['MINIMAX_API_KEY', 'ANTHROPIC_API_KEY']);
|
|
116
|
+
assert.ok(minimax.backupEnvKeys.includes('MINIMAX_API_KEY_BACKUP'));
|
|
117
|
+
assert.ok(minimax.backupEnvKeys.includes('MINIMAX_BACKUP_API_KEY'));
|
|
118
|
+
assert.ok(minimax.backupEnvKeys.includes('ANTHROPIC_API_KEY_BACKUP'));
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('autoDetect — backup key discovery', () => {
|
|
123
|
+
beforeEach(async () => {
|
|
124
|
+
// Reset env + on-disk config between tests so they're independent.
|
|
125
|
+
delete process.env.MINIMAX_API_KEY;
|
|
126
|
+
delete process.env.MINIMAX_API_KEY_BACKUP;
|
|
127
|
+
delete process.env.OPENAI_API_KEY;
|
|
128
|
+
delete process.env.OPENAI_API_KEY_BACKUP;
|
|
129
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
130
|
+
await resetOpencodeJson();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('reports no backup when no env keys and no config', async () => {
|
|
134
|
+
const store = await loadStore();
|
|
135
|
+
const results = await store.autoDetect({ probe: false });
|
|
136
|
+
const minimax = results.find((p) => p.id === 'minimax');
|
|
137
|
+
assert.ok(minimax);
|
|
138
|
+
assert.equal(minimax.status, 'no-key');
|
|
139
|
+
assert.equal(minimax.hasKey, false);
|
|
140
|
+
assert.equal(minimax.backup.status, 'no-key');
|
|
141
|
+
assert.equal(minimax.backup.hasKey, false);
|
|
142
|
+
assert.equal(minimax.backup.source, '');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('detects primary from env and backup from config', async () => {
|
|
146
|
+
writeOpencodeJson({
|
|
147
|
+
provider: {
|
|
148
|
+
minimax: {
|
|
149
|
+
apiKey: '',
|
|
150
|
+
backupApiKey: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
process.env.MINIMAX_API_KEY = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
|
|
155
|
+
|
|
156
|
+
const store = await loadStore();
|
|
157
|
+
const results = await store.autoDetect({ probe: false });
|
|
158
|
+
const minimax = results.find((p) => p.id === 'minimax');
|
|
159
|
+
assert.equal(minimax.hasKey, true);
|
|
160
|
+
assert.equal(minimax.status, 'configured');
|
|
161
|
+
assert.match(minimax.keySource, /^env:MINIMAX_API_KEY$/);
|
|
162
|
+
assert.equal(minimax.backup.hasKey, true);
|
|
163
|
+
assert.equal(minimax.backup.status, 'configured');
|
|
164
|
+
assert.equal(minimax.backup.source, 'config');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('detects backup from *_BACKUP_API_KEY env var', async () => {
|
|
168
|
+
process.env.MINIMAX_API_KEY = 'cccccccccccccccccccccccccccccccccccccc';
|
|
169
|
+
process.env.MINIMAX_API_KEY_BACKUP = 'dddddddddddddddddddddddddddddddddddd';
|
|
170
|
+
|
|
171
|
+
const store = await loadStore();
|
|
172
|
+
const results = await store.autoDetect({ probe: false });
|
|
173
|
+
const minimax = results.find((p) => p.id === 'minimax');
|
|
174
|
+
assert.equal(minimax.hasKey, true);
|
|
175
|
+
assert.equal(minimax.backup.hasKey, true);
|
|
176
|
+
assert.equal(minimax.backup.source, 'env:MINIMAX_API_KEY_BACKUP');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('prefers config over env for both primary and backup', async () => {
|
|
180
|
+
writeOpencodeJson({
|
|
181
|
+
provider: {
|
|
182
|
+
minimax: {
|
|
183
|
+
apiKey: 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
|
|
184
|
+
backupApiKey: 'ffffffffffffffffffffffffffffffffffffffffff',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
// Env should be ignored when config has values.
|
|
189
|
+
process.env.MINIMAX_API_KEY = 'gggggggggggggggggggggggggggggggggggggg';
|
|
190
|
+
process.env.MINIMAX_API_KEY_BACKUP = 'hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh';
|
|
191
|
+
|
|
192
|
+
const store = await loadStore();
|
|
193
|
+
const results = await store.autoDetect({ probe: false });
|
|
194
|
+
const minimax = results.find((p) => p.id === 'minimax');
|
|
195
|
+
assert.equal(minimax.keySource, 'config');
|
|
196
|
+
assert.equal(minimax.backup.source, 'config');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('allows backup without primary (rotation in progress)', async () => {
|
|
200
|
+
// No primary key — only backup env var.
|
|
201
|
+
process.env.MINIMAX_API_KEY_BACKUP = 'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii';
|
|
202
|
+
|
|
203
|
+
const store = await loadStore();
|
|
204
|
+
const results = await store.autoDetect({ probe: false });
|
|
205
|
+
const minimax = results.find((p) => p.id === 'minimax');
|
|
206
|
+
assert.equal(minimax.status, 'no-key');
|
|
207
|
+
assert.equal(minimax.backup.status, 'configured');
|
|
208
|
+
assert.equal(minimax.backup.hasKey, true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('reports backup status as unknown when format does not match', async () => {
|
|
212
|
+
// OpenAI primary: must start with `sk-` for the configured status.
|
|
213
|
+
process.env.OPENAI_API_KEY = 'sk-jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj';
|
|
214
|
+
// Backup is intentionally invalid format (no `sk-` prefix) — must
|
|
215
|
+
// downgrade to `unknown` so the UI shows the operator a warning.
|
|
216
|
+
process.env.OPENAI_API_KEY_BACKUP = 'notvalidformat';
|
|
217
|
+
|
|
218
|
+
const store = await loadStore();
|
|
219
|
+
const results = await store.autoDetect({ probe: false });
|
|
220
|
+
const openai = results.find((p) => p.id === 'openai');
|
|
221
|
+
assert.equal(openai.status, 'configured');
|
|
222
|
+
assert.equal(openai.backup.status, 'unknown');
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('add / update — backup key persistence', () => {
|
|
227
|
+
beforeEach(async () => {
|
|
228
|
+
delete process.env.MINIMAX_API_KEY;
|
|
229
|
+
delete process.env.MINIMAX_API_KEY_BACKUP;
|
|
230
|
+
delete process.env.OPENAI_API_KEY;
|
|
231
|
+
delete process.env.OPENAI_API_KEY_BACKUP;
|
|
232
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
233
|
+
await resetOpencodeJson();
|
|
234
|
+
});
|
|
235
|
+
it('persists backupApiKey in opencode.json', async () => {
|
|
236
|
+
const store = await loadStore();
|
|
237
|
+
await store.add({
|
|
238
|
+
id: 'minimax',
|
|
239
|
+
name: 'MiniMax',
|
|
240
|
+
apiKey: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
241
|
+
backupApiKey: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
242
|
+
});
|
|
243
|
+
// Re-read from disk to confirm persistence.
|
|
244
|
+
const raw = JSON.parse(readFileSync(join(SANDBOX_HOME, '.config/opencode/opencode.json'), 'utf8'));
|
|
245
|
+
assert.equal(raw.provider.minimax.apiKey, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
|
|
246
|
+
assert.equal(raw.provider.minimax.backupApiKey, 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('list() masks both apiKey and backupApiKey', async () => {
|
|
250
|
+
const store = await loadStore();
|
|
251
|
+
await store.add({
|
|
252
|
+
id: 'minimax',
|
|
253
|
+
name: 'MiniMax',
|
|
254
|
+
apiKey: 'ccccccccccccccccccccccccccccccccccccccccc',
|
|
255
|
+
backupApiKey: 'dddddddddddddddddddddddddddddddddddddd',
|
|
256
|
+
});
|
|
257
|
+
const list = store.list();
|
|
258
|
+
const minimax = list.find((p) => p.id === 'minimax');
|
|
259
|
+
assert.match(minimax.apiKey, /^cc\.\.\.cc$/); // masked
|
|
260
|
+
assert.match(minimax.backupApiKey, /^dd\.\.\.dd$/);
|
|
261
|
+
assert.notEqual(minimax.apiKey, 'ccccccccccccccccccccccccccccccccccccccccc');
|
|
262
|
+
assert.notEqual(minimax.backupApiKey, 'dddddddddddddddddddddddddddddddddddddd');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('update() preserves stored backupApiKey when form submits masked placeholder', async () => {
|
|
266
|
+
const store = await loadStore();
|
|
267
|
+
await store.add({
|
|
268
|
+
id: 'minimax',
|
|
269
|
+
apiKey: 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
|
|
270
|
+
backupApiKey: 'ffffffffffffffffffffffffffffffffffffffffff',
|
|
271
|
+
});
|
|
272
|
+
// Operator re-submits the form without touching the keys (the
|
|
273
|
+
// masked placeholders come back from list()).
|
|
274
|
+
const updated = await store.update('minimax', {
|
|
275
|
+
name: 'MiniMax (renamed)',
|
|
276
|
+
apiKey: '***short***',
|
|
277
|
+
backupApiKey: '***short***',
|
|
278
|
+
});
|
|
279
|
+
assert.equal(updated.name, 'MiniMax (renamed)');
|
|
280
|
+
// Stored values must be preserved — mask-keep semantics.
|
|
281
|
+
const raw = JSON.parse(readFileSync(join(SANDBOX_HOME, '.config/opencode/opencode.json'), 'utf8'));
|
|
282
|
+
assert.equal(raw.provider.minimax.apiKey, 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee');
|
|
283
|
+
assert.equal(raw.provider.minimax.backupApiKey, 'ffffffffffffffffffffffffffffffffffffffffff');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('update() replaces backupApiKey when a new value is supplied', async () => {
|
|
287
|
+
const store = await loadStore();
|
|
288
|
+
await store.add({
|
|
289
|
+
id: 'minimax',
|
|
290
|
+
apiKey: 'gggggggggggggggggggggggggggggggggggggg',
|
|
291
|
+
backupApiKey: 'hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh',
|
|
292
|
+
});
|
|
293
|
+
await store.update('minimax', {
|
|
294
|
+
backupApiKey: 'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii',
|
|
295
|
+
});
|
|
296
|
+
const raw = JSON.parse(readFileSync(join(SANDBOX_HOME, '.config/opencode/opencode.json'), 'utf8'));
|
|
297
|
+
assert.equal(raw.provider.minimax.backupApiKey, 'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii');
|
|
298
|
+
assert.equal(raw.provider.minimax.apiKey, 'gggggggggggggggggggggggggggggggggggggg');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('update() supports clearing backupApiKey with empty string', async () => {
|
|
302
|
+
const store = await loadStore();
|
|
303
|
+
await store.add({
|
|
304
|
+
id: 'minimax',
|
|
305
|
+
apiKey: 'jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj',
|
|
306
|
+
backupApiKey: 'kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk',
|
|
307
|
+
});
|
|
308
|
+
// Operator clears the backup field.
|
|
309
|
+
await store.update('minimax', { backupApiKey: '' });
|
|
310
|
+
const raw = JSON.parse(readFileSync(join(SANDBOX_HOME, '.config/opencode/opencode.json'), 'utf8'));
|
|
311
|
+
assert.equal(raw.provider.minimax.backupApiKey, '');
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe('listAll — backup key surfaced', () => {
|
|
316
|
+
it('reports backupApiKey (masked) and source', async () => {
|
|
317
|
+
writeOpencodeJson({
|
|
318
|
+
provider: {
|
|
319
|
+
minimax: {
|
|
320
|
+
apiKey: 'llllllllllllllllllllllllllllllllllllllll',
|
|
321
|
+
backupApiKey: 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
const store = await loadStore();
|
|
326
|
+
const all = await store.listAll();
|
|
327
|
+
const minimax = all.find((p) => p.id === 'minimax');
|
|
328
|
+
assert.ok(minimax);
|
|
329
|
+
assert.match(minimax.apiKey, /^ll\.\.\.ll$/);
|
|
330
|
+
assert.match(minimax.backupApiKey, /^mm\.\.\.mm$/);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
});
|