@butlerw/vellum 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +411 -0
- package/__fixtures__/responses/code-generation.json +42 -0
- package/__fixtures__/responses/error-response.json +20 -0
- package/__fixtures__/responses/hello-world.json +32 -0
- package/dist/auth-6MCXESOH.js +26 -0
- package/dist/chunk-SECXJGWA.js +597 -0
- package/dist/index.js +34023 -0
- package/package.json +67 -0
- package/src/__tests__/commands.e2e.test.ts +728 -0
- package/src/__tests__/credentials.test.ts +713 -0
- package/src/__tests__/mode-e2e.test.ts +391 -0
- package/src/__tests__/tui-integration.test.tsx +1271 -0
- package/src/agents/__tests__/task-persistence.test.ts +235 -0
- package/src/agents/commands/delegate.ts +240 -0
- package/src/agents/commands/index.ts +10 -0
- package/src/agents/commands/resume.ts +335 -0
- package/src/agents/index.ts +29 -0
- package/src/agents/task-persistence.ts +272 -0
- package/src/agents/task-resumption.ts +242 -0
- package/src/app.tsx +4737 -0
- package/src/commands/__tests__/.gitkeep +1 -0
- package/src/commands/__tests__/agents.test.ts +606 -0
- package/src/commands/__tests__/auth.test.ts +626 -0
- package/src/commands/__tests__/autocomplete.test.ts +683 -0
- package/src/commands/__tests__/batch.test.ts +287 -0
- package/src/commands/__tests__/chain-pipe-parser.test.ts +654 -0
- package/src/commands/__tests__/completion.test.ts +238 -0
- package/src/commands/__tests__/core.test.ts +363 -0
- package/src/commands/__tests__/executor.test.ts +496 -0
- package/src/commands/__tests__/exit-codes.test.ts +220 -0
- package/src/commands/__tests__/init.test.ts +243 -0
- package/src/commands/__tests__/language.test.ts +353 -0
- package/src/commands/__tests__/mode-cli.test.ts +667 -0
- package/src/commands/__tests__/model.test.ts +277 -0
- package/src/commands/__tests__/parser.test.ts +493 -0
- package/src/commands/__tests__/performance.bench.ts +380 -0
- package/src/commands/__tests__/registry.test.ts +534 -0
- package/src/commands/__tests__/resume.test.ts +449 -0
- package/src/commands/__tests__/security.test.ts +845 -0
- package/src/commands/__tests__/stream-json.test.ts +372 -0
- package/src/commands/__tests__/user-commands.test.ts +597 -0
- package/src/commands/adapters.ts +267 -0
- package/src/commands/agent.ts +395 -0
- package/src/commands/agents/generate.ts +506 -0
- package/src/commands/agents/index.ts +272 -0
- package/src/commands/agents/show.ts +271 -0
- package/src/commands/agents/validate.ts +387 -0
- package/src/commands/auth.ts +883 -0
- package/src/commands/autocomplete.ts +480 -0
- package/src/commands/batch/command.ts +388 -0
- package/src/commands/batch/executor.ts +361 -0
- package/src/commands/batch/index.ts +12 -0
- package/src/commands/commit.ts +235 -0
- package/src/commands/completion/index.ts +371 -0
- package/src/commands/condense.ts +191 -0
- package/src/commands/config.ts +344 -0
- package/src/commands/context-provider.ts +173 -0
- package/src/commands/copy.ts +329 -0
- package/src/commands/core/clear.ts +38 -0
- package/src/commands/core/exit.ts +43 -0
- package/src/commands/core/help.ts +354 -0
- package/src/commands/core/index.ts +15 -0
- package/src/commands/cost.ts +179 -0
- package/src/commands/credentials.tsx +618 -0
- package/src/commands/custom-agents/__tests__/custom-agents.test.ts +709 -0
- package/src/commands/custom-agents/create.ts +377 -0
- package/src/commands/custom-agents/export.ts +135 -0
- package/src/commands/custom-agents/import.ts +199 -0
- package/src/commands/custom-agents/index.ts +372 -0
- package/src/commands/custom-agents/info.ts +318 -0
- package/src/commands/custom-agents/list.ts +267 -0
- package/src/commands/custom-agents/validate.ts +388 -0
- package/src/commands/diff-mode.ts +241 -0
- package/src/commands/env.ts +53 -0
- package/src/commands/executor.ts +579 -0
- package/src/commands/exit-codes.ts +202 -0
- package/src/commands/index.ts +701 -0
- package/src/commands/init/index.ts +15 -0
- package/src/commands/init/prompts.ts +366 -0
- package/src/commands/init/templates/commands-readme.md +80 -0
- package/src/commands/init/templates/example-command.md +79 -0
- package/src/commands/init/templates/example-skill.md +168 -0
- package/src/commands/init/templates/example-workflow.md +101 -0
- package/src/commands/init/templates/prompts-readme.md +52 -0
- package/src/commands/init/templates/rules-readme.md +63 -0
- package/src/commands/init/templates/skills-readme.md +83 -0
- package/src/commands/init/templates/workflows-readme.md +94 -0
- package/src/commands/init.ts +391 -0
- package/src/commands/install.ts +90 -0
- package/src/commands/language.ts +191 -0
- package/src/commands/loaders/.gitkeep +1 -0
- package/src/commands/lsp.ts +199 -0
- package/src/commands/markdown-commands.ts +253 -0
- package/src/commands/mcp.ts +588 -0
- package/src/commands/memory/export.ts +341 -0
- package/src/commands/memory/index.ts +148 -0
- package/src/commands/memory/list.ts +261 -0
- package/src/commands/memory/search.ts +346 -0
- package/src/commands/memory/utils.ts +15 -0
- package/src/commands/metrics.ts +75 -0
- package/src/commands/migrate/index.ts +16 -0
- package/src/commands/migrate/prompts.ts +477 -0
- package/src/commands/mode.ts +331 -0
- package/src/commands/model.ts +298 -0
- package/src/commands/onboard.ts +205 -0
- package/src/commands/open.ts +169 -0
- package/src/commands/output/stream-json.ts +373 -0
- package/src/commands/parser/chain-parser.ts +370 -0
- package/src/commands/parser/index.ts +29 -0
- package/src/commands/parser/pipe-parser.ts +480 -0
- package/src/commands/parser.ts +588 -0
- package/src/commands/persistence.ts +355 -0
- package/src/commands/progress.ts +18 -0
- package/src/commands/prompt/index.ts +17 -0
- package/src/commands/prompt/validate.ts +621 -0
- package/src/commands/prompt-priority.ts +401 -0
- package/src/commands/registry.ts +374 -0
- package/src/commands/sandbox/index.ts +131 -0
- package/src/commands/security/index.ts +21 -0
- package/src/commands/security/input-sanitizer.ts +168 -0
- package/src/commands/security/permission-checker.ts +456 -0
- package/src/commands/security/sensitive-data.ts +350 -0
- package/src/commands/session/delete.ts +38 -0
- package/src/commands/session/export.ts +39 -0
- package/src/commands/session/index.ts +26 -0
- package/src/commands/session/list.ts +26 -0
- package/src/commands/session/resume.ts +562 -0
- package/src/commands/session/search.ts +434 -0
- package/src/commands/session/show.ts +26 -0
- package/src/commands/settings.ts +368 -0
- package/src/commands/setup.ts +23 -0
- package/src/commands/shell/index.ts +16 -0
- package/src/commands/shell/setup.ts +422 -0
- package/src/commands/shell-init.ts +50 -0
- package/src/commands/shell-integration/index.ts +194 -0
- package/src/commands/skill.ts +1220 -0
- package/src/commands/spec.ts +558 -0
- package/src/commands/status.ts +246 -0
- package/src/commands/theme.ts +211 -0
- package/src/commands/think.ts +551 -0
- package/src/commands/trust.ts +211 -0
- package/src/commands/tutorial.ts +522 -0
- package/src/commands/types.ts +512 -0
- package/src/commands/update.ts +274 -0
- package/src/commands/usage.ts +213 -0
- package/src/commands/user-commands.ts +630 -0
- package/src/commands/utils.ts +142 -0
- package/src/commands/vim.ts +152 -0
- package/src/commands/workflow.ts +257 -0
- package/src/components/header.tsx +25 -0
- package/src/components/input.tsx +25 -0
- package/src/components/message-list.tsx +32 -0
- package/src/components/status-bar.tsx +23 -0
- package/src/index.tsx +614 -0
- package/src/onboarding/__tests__/tutorial.test.ts +740 -0
- package/src/onboarding/index.ts +69 -0
- package/src/onboarding/tips/index.ts +9 -0
- package/src/onboarding/tips/tip-engine.ts +459 -0
- package/src/onboarding/tutorial/index.ts +88 -0
- package/src/onboarding/tutorial/lessons/basics.ts +151 -0
- package/src/onboarding/tutorial/lessons/index.ts +151 -0
- package/src/onboarding/tutorial/lessons/modes.ts +230 -0
- package/src/onboarding/tutorial/lessons/tools.ts +172 -0
- package/src/onboarding/tutorial/progress-tracker.ts +350 -0
- package/src/onboarding/tutorial/storage.ts +249 -0
- package/src/onboarding/tutorial/tutorial-system.ts +462 -0
- package/src/onboarding/tutorial/types.ts +310 -0
- package/src/orchestrator-singleton.ts +129 -0
- package/src/shutdown.ts +33 -0
- package/src/test/e2e/assertions.ts +267 -0
- package/src/test/e2e/fixtures.ts +204 -0
- package/src/test/e2e/harness.ts +575 -0
- package/src/test/e2e/index.ts +57 -0
- package/src/test/e2e/types.ts +228 -0
- package/src/test/fixtures/__tests__/fake-response-loader.test.ts +314 -0
- package/src/test/fixtures/fake-response-loader.ts +314 -0
- package/src/test/fixtures/index.ts +20 -0
- package/src/tui/__tests__/mcp-panel.test.tsx +82 -0
- package/src/tui/__tests__/mcp-wiring.test.tsx +78 -0
- package/src/tui/__tests__/mode-components.test.tsx +395 -0
- package/src/tui/__tests__/permission-ask-flow.test.tsx +138 -0
- package/src/tui/__tests__/sidebar-panel-data.test.tsx +148 -0
- package/src/tui/__tests__/tools-panel-hotkeys.test.tsx +41 -0
- package/src/tui/adapters/agent-adapter.ts +1008 -0
- package/src/tui/adapters/index.ts +48 -0
- package/src/tui/adapters/message-adapter.ts +315 -0
- package/src/tui/adapters/persistence-bridge.ts +331 -0
- package/src/tui/adapters/session-adapter.ts +419 -0
- package/src/tui/buffered-stdout.ts +223 -0
- package/src/tui/components/AgentProgress.tsx +424 -0
- package/src/tui/components/Banner/AsciiArt.ts +160 -0
- package/src/tui/components/Banner/Banner.tsx +355 -0
- package/src/tui/components/Banner/ShimmerContext.tsx +131 -0
- package/src/tui/components/Banner/ShimmerText.tsx +193 -0
- package/src/tui/components/Banner/TypeWriterGradient.tsx +321 -0
- package/src/tui/components/Banner/index.ts +61 -0
- package/src/tui/components/Banner/useShimmer.ts +241 -0
- package/src/tui/components/ChatView.tsx +11 -0
- package/src/tui/components/Checkpoint/CheckpointDiffView.tsx +371 -0
- package/src/tui/components/Checkpoint/SnapshotCheckpointPanel.tsx +440 -0
- package/src/tui/components/Checkpoint/index.ts +19 -0
- package/src/tui/components/CostDisplay.tsx +226 -0
- package/src/tui/components/InitErrorBanner.tsx +122 -0
- package/src/tui/components/Input/Autocomplete.tsx +603 -0
- package/src/tui/components/Input/EnhancedCommandInput.tsx +471 -0
- package/src/tui/components/Input/HighlightedText.tsx +236 -0
- package/src/tui/components/Input/MentionAutocomplete.tsx +375 -0
- package/src/tui/components/Input/TextInput.tsx +1002 -0
- package/src/tui/components/Input/__tests__/Autocomplete.test.tsx +374 -0
- package/src/tui/components/Input/__tests__/TextInput.test.tsx +241 -0
- package/src/tui/components/Input/__tests__/highlight.test.ts +219 -0
- package/src/tui/components/Input/__tests__/slash-command-utils.test.ts +104 -0
- package/src/tui/components/Input/highlight.ts +362 -0
- package/src/tui/components/Input/index.ts +36 -0
- package/src/tui/components/Input/slash-command-utils.ts +135 -0
- package/src/tui/components/Layout.tsx +432 -0
- package/src/tui/components/McpPanel.tsx +137 -0
- package/src/tui/components/MemoryPanel.tsx +448 -0
- package/src/tui/components/Messages/CodeBlock.tsx +527 -0
- package/src/tui/components/Messages/DiffView.tsx +679 -0
- package/src/tui/components/Messages/ImageReference.tsx +89 -0
- package/src/tui/components/Messages/MarkdownBlock.tsx +228 -0
- package/src/tui/components/Messages/MarkdownRenderer.tsx +498 -0
- package/src/tui/components/Messages/MessageBubble.tsx +270 -0
- package/src/tui/components/Messages/MessageList.tsx +1719 -0
- package/src/tui/components/Messages/StreamingText.tsx +216 -0
- package/src/tui/components/Messages/ThinkingBlock.tsx +408 -0
- package/src/tui/components/Messages/ToolResultPreview.tsx +243 -0
- package/src/tui/components/Messages/__tests__/CodeBlock.test.tsx +296 -0
- package/src/tui/components/Messages/__tests__/DiffView.test.tsx +239 -0
- package/src/tui/components/Messages/__tests__/MarkdownRenderer.test.tsx +303 -0
- package/src/tui/components/Messages/__tests__/MessageBubble.test.tsx +268 -0
- package/src/tui/components/Messages/__tests__/MessageList.test.tsx +324 -0
- package/src/tui/components/Messages/__tests__/StreamingText.test.tsx +215 -0
- package/src/tui/components/Messages/index.ts +25 -0
- package/src/tui/components/ModeIndicator.tsx +177 -0
- package/src/tui/components/ModeSelector.tsx +216 -0
- package/src/tui/components/ModelSelector.tsx +339 -0
- package/src/tui/components/OnboardingWizard.tsx +670 -0
- package/src/tui/components/PhaseProgressIndicator.tsx +270 -0
- package/src/tui/components/RateLimitIndicator.tsx +82 -0
- package/src/tui/components/ScreenReaderLayout.tsx +295 -0
- package/src/tui/components/SettingsPanel.tsx +643 -0
- package/src/tui/components/Sidebar/SystemStatusPanel.tsx +284 -0
- package/src/tui/components/Sidebar/index.ts +9 -0
- package/src/tui/components/Status/ModelStatusBar.tsx +270 -0
- package/src/tui/components/Status/index.ts +12 -0
- package/src/tui/components/StatusBar/AgentModeIndicator.tsx +257 -0
- package/src/tui/components/StatusBar/ContextProgress.tsx +167 -0
- package/src/tui/components/StatusBar/FileChangesIndicator.tsx +62 -0
- package/src/tui/components/StatusBar/GitIndicator.tsx +89 -0
- package/src/tui/components/StatusBar/HeaderBar.tsx +126 -0
- package/src/tui/components/StatusBar/ModelIndicator.tsx +157 -0
- package/src/tui/components/StatusBar/PersistenceStatusIndicator.tsx +210 -0
- package/src/tui/components/StatusBar/ResilienceIndicator.tsx +106 -0
- package/src/tui/components/StatusBar/SandboxIndicator.tsx +167 -0
- package/src/tui/components/StatusBar/StatusBar.tsx +368 -0
- package/src/tui/components/StatusBar/ThinkingModeIndicator.tsx +170 -0
- package/src/tui/components/StatusBar/TokenBreakdown.tsx +246 -0
- package/src/tui/components/StatusBar/TokenCounter.tsx +135 -0
- package/src/tui/components/StatusBar/TrustModeIndicator.tsx +130 -0
- package/src/tui/components/StatusBar/WorkspaceIndicator.tsx +86 -0
- package/src/tui/components/StatusBar/__tests__/AgentModeIndicator.test.tsx +193 -0
- package/src/tui/components/StatusBar/__tests__/StatusBar.test.tsx +729 -0
- package/src/tui/components/StatusBar/index.ts +60 -0
- package/src/tui/components/TipBanner.tsx +115 -0
- package/src/tui/components/TodoItem.tsx +208 -0
- package/src/tui/components/TodoPanel.tsx +455 -0
- package/src/tui/components/Tools/ApprovalQueue.tsx +407 -0
- package/src/tui/components/Tools/OptionSelector.tsx +160 -0
- package/src/tui/components/Tools/PermissionDialog.tsx +286 -0
- package/src/tui/components/Tools/ToolParams.tsx +483 -0
- package/src/tui/components/Tools/ToolsPanel.tsx +178 -0
- package/src/tui/components/Tools/__tests__/PermissionDialog.test.tsx +510 -0
- package/src/tui/components/Tools/__tests__/ToolParams.test.tsx +432 -0
- package/src/tui/components/Tools/index.ts +21 -0
- package/src/tui/components/TrustPrompt.tsx +279 -0
- package/src/tui/components/UpdateBanner.tsx +166 -0
- package/src/tui/components/VimModeIndicator.tsx +112 -0
- package/src/tui/components/backtrack/BacktrackControls.tsx +402 -0
- package/src/tui/components/backtrack/index.ts +13 -0
- package/src/tui/components/common/AutoApprovalStatus.tsx +251 -0
- package/src/tui/components/common/CostWarning.tsx +294 -0
- package/src/tui/components/common/DynamicShortcutHints.tsx +209 -0
- package/src/tui/components/common/EnhancedLoadingIndicator.tsx +305 -0
- package/src/tui/components/common/ErrorBoundary.tsx +140 -0
- package/src/tui/components/common/GradientText.tsx +224 -0
- package/src/tui/components/common/HotkeyHelpModal.tsx +193 -0
- package/src/tui/components/common/HotkeyHints.tsx +70 -0
- package/src/tui/components/common/MaxSizedBox.tsx +354 -0
- package/src/tui/components/common/NewMessagesBadge.tsx +65 -0
- package/src/tui/components/common/ProtectedFileLegend.tsx +89 -0
- package/src/tui/components/common/ScrollIndicator.tsx +160 -0
- package/src/tui/components/common/Spinner.tsx +342 -0
- package/src/tui/components/common/StreamingIndicator.tsx +316 -0
- package/src/tui/components/common/VirtualizedList/VirtualizedList.tsx +428 -0
- package/src/tui/components/common/VirtualizedList/hooks/index.ts +19 -0
- package/src/tui/components/common/VirtualizedList/hooks/useBatchedScroll.ts +64 -0
- package/src/tui/components/common/VirtualizedList/hooks/useScrollAnchor.ts +290 -0
- package/src/tui/components/common/VirtualizedList/hooks/useVirtualization.ts +340 -0
- package/src/tui/components/common/VirtualizedList/index.ts +30 -0
- package/src/tui/components/common/VirtualizedList/types.ts +107 -0
- package/src/tui/components/common/__tests__/NewMessagesBadge.test.tsx +74 -0
- package/src/tui/components/common/__tests__/ScrollIndicator.test.tsx +193 -0
- package/src/tui/components/common/index.ts +110 -0
- package/src/tui/components/index.ts +79 -0
- package/src/tui/components/session/CheckpointPanel.tsx +323 -0
- package/src/tui/components/session/RollbackDialog.tsx +169 -0
- package/src/tui/components/session/SessionItem.tsx +136 -0
- package/src/tui/components/session/SessionListPanel.tsx +252 -0
- package/src/tui/components/session/SessionPicker.tsx +449 -0
- package/src/tui/components/session/SessionPreview.tsx +240 -0
- package/src/tui/components/session/__tests__/session.test.tsx +408 -0
- package/src/tui/components/session/index.ts +28 -0
- package/src/tui/components/session/types.ts +116 -0
- package/src/tui/components/theme/__tests__/tokens.test.ts +471 -0
- package/src/tui/components/theme/index.ts +227 -0
- package/src/tui/components/theme/tokens.ts +484 -0
- package/src/tui/config/defaults.ts +134 -0
- package/src/tui/config/index.ts +17 -0
- package/src/tui/context/AnimationContext.tsx +284 -0
- package/src/tui/context/AppContext.tsx +349 -0
- package/src/tui/context/BracketedPasteContext.tsx +372 -0
- package/src/tui/context/LspContext.tsx +192 -0
- package/src/tui/context/McpContext.tsx +325 -0
- package/src/tui/context/MessagesContext.tsx +870 -0
- package/src/tui/context/OverflowContext.tsx +213 -0
- package/src/tui/context/RateLimitContext.tsx +108 -0
- package/src/tui/context/ResilienceContext.tsx +275 -0
- package/src/tui/context/RootProvider.tsx +136 -0
- package/src/tui/context/ScrollContext.tsx +331 -0
- package/src/tui/context/ToolsContext.tsx +702 -0
- package/src/tui/context/__tests__/BracketedPasteContext.test.tsx +416 -0
- package/src/tui/context/index.ts +140 -0
- package/src/tui/enterprise-integration.ts +282 -0
- package/src/tui/hooks/__tests__/useBacktrack.test.tsx +138 -0
- package/src/tui/hooks/__tests__/useBracketedPaste.test.tsx +222 -0
- package/src/tui/hooks/__tests__/useCopyMode.test.tsx +336 -0
- package/src/tui/hooks/__tests__/useHotkeys.ctrl-input.test.tsx +96 -0
- package/src/tui/hooks/__tests__/useHotkeys.test.tsx +454 -0
- package/src/tui/hooks/__tests__/useInputHistory.test.tsx +660 -0
- package/src/tui/hooks/__tests__/useLineBuffer.test.ts +295 -0
- package/src/tui/hooks/__tests__/useModeController.test.ts +137 -0
- package/src/tui/hooks/__tests__/useModeShortcuts.test.tsx +142 -0
- package/src/tui/hooks/__tests__/useScrollController.test.ts +464 -0
- package/src/tui/hooks/__tests__/useVim.test.tsx +531 -0
- package/src/tui/hooks/index.ts +252 -0
- package/src/tui/hooks/useAgentLoop.ts +712 -0
- package/src/tui/hooks/useAlternateBuffer.ts +398 -0
- package/src/tui/hooks/useAnimatedScrollbar.ts +241 -0
- package/src/tui/hooks/useBacktrack.ts +443 -0
- package/src/tui/hooks/useBracketedPaste.ts +104 -0
- package/src/tui/hooks/useCollapsible.ts +240 -0
- package/src/tui/hooks/useCopyMode.ts +382 -0
- package/src/tui/hooks/useCostSummary.ts +75 -0
- package/src/tui/hooks/useDesktopNotification.ts +414 -0
- package/src/tui/hooks/useDiffMode.ts +44 -0
- package/src/tui/hooks/useFileChangeStats.ts +110 -0
- package/src/tui/hooks/useFileSuggestions.ts +284 -0
- package/src/tui/hooks/useFlickerDetector.ts +250 -0
- package/src/tui/hooks/useGitStatus.ts +200 -0
- package/src/tui/hooks/useHotkeys.ts +579 -0
- package/src/tui/hooks/useImagePaste.ts +114 -0
- package/src/tui/hooks/useInputHighlight.ts +145 -0
- package/src/tui/hooks/useInputHistory.ts +246 -0
- package/src/tui/hooks/useKeyboardScroll.ts +209 -0
- package/src/tui/hooks/useLineBuffer.ts +356 -0
- package/src/tui/hooks/useMentionAutocomplete.ts +235 -0
- package/src/tui/hooks/useModeController.ts +167 -0
- package/src/tui/hooks/useModeShortcuts.ts +196 -0
- package/src/tui/hooks/usePermissionHandler.ts +146 -0
- package/src/tui/hooks/usePersistence.ts +480 -0
- package/src/tui/hooks/usePersistenceShortcuts.ts +225 -0
- package/src/tui/hooks/usePlaceholderRotation.ts +143 -0
- package/src/tui/hooks/useProviderStatus.ts +270 -0
- package/src/tui/hooks/useRateLimitStatus.ts +90 -0
- package/src/tui/hooks/useScreenReader.ts +315 -0
- package/src/tui/hooks/useScrollController.ts +450 -0
- package/src/tui/hooks/useScrollEventBatcher.ts +185 -0
- package/src/tui/hooks/useSidebarPanelData.ts +115 -0
- package/src/tui/hooks/useSmoothScroll.ts +202 -0
- package/src/tui/hooks/useSnapshots.ts +300 -0
- package/src/tui/hooks/useStateAndRef.ts +50 -0
- package/src/tui/hooks/useTerminalSize.ts +206 -0
- package/src/tui/hooks/useToolApprovalController.ts +91 -0
- package/src/tui/hooks/useVim.ts +334 -0
- package/src/tui/hooks/useWorkspace.ts +56 -0
- package/src/tui/i18n/__tests__/init.test.ts +278 -0
- package/src/tui/i18n/__tests__/language-config.test.ts +199 -0
- package/src/tui/i18n/__tests__/locale-detection.test.ts +250 -0
- package/src/tui/i18n/__tests__/settings-integration.test.ts +262 -0
- package/src/tui/i18n/index.ts +72 -0
- package/src/tui/i18n/init.ts +131 -0
- package/src/tui/i18n/language-config.ts +106 -0
- package/src/tui/i18n/locale-detection.ts +173 -0
- package/src/tui/i18n/settings-integration.ts +557 -0
- package/src/tui/i18n/tui-namespace.ts +538 -0
- package/src/tui/i18n/types.ts +312 -0
- package/src/tui/index.ts +43 -0
- package/src/tui/lsp-integration.ts +409 -0
- package/src/tui/metrics-integration.ts +366 -0
- package/src/tui/plugins.ts +383 -0
- package/src/tui/resilience.ts +342 -0
- package/src/tui/sandbox-integration.ts +317 -0
- package/src/tui/services/clipboard.ts +348 -0
- package/src/tui/services/fuzzy-search.ts +441 -0
- package/src/tui/services/index.ts +72 -0
- package/src/tui/services/markdown-renderer.ts +565 -0
- package/src/tui/services/open-external.ts +247 -0
- package/src/tui/services/syntax-highlighter.ts +483 -0
- package/src/tui/slash-commands.ts +12 -0
- package/src/tui/theme/index.ts +15 -0
- package/src/tui/theme/provider.tsx +206 -0
- package/src/tui/tip-integration.ts +300 -0
- package/src/tui/types/__tests__/ink-extended.test.ts +121 -0
- package/src/tui/types/ink-extended.ts +87 -0
- package/src/tui/utils/__tests__/bracketedPaste.test.ts +231 -0
- package/src/tui/utils/__tests__/heightEstimator.test.ts +157 -0
- package/src/tui/utils/__tests__/text-width.test.ts +158 -0
- package/src/tui/utils/__tests__/textSanitizer.test.ts +266 -0
- package/src/tui/utils/__tests__/ui-sizing.test.ts +169 -0
- package/src/tui/utils/bracketedPaste.ts +107 -0
- package/src/tui/utils/cursor-manager.ts +131 -0
- package/src/tui/utils/detectTerminal.ts +596 -0
- package/src/tui/utils/findLastSafeSplitPoint.ts +92 -0
- package/src/tui/utils/heightEstimator.ts +198 -0
- package/src/tui/utils/index.ts +91 -0
- package/src/tui/utils/isNarrowWidth.ts +52 -0
- package/src/tui/utils/stdoutGuard.ts +90 -0
- package/src/tui/utils/synchronized-update.ts +70 -0
- package/src/tui/utils/text-width.ts +225 -0
- package/src/tui/utils/textSanitizer.ts +225 -0
- package/src/tui/utils/textUtils.ts +114 -0
- package/src/tui/utils/ui-sizing.ts +192 -0
- package/src/tui-blessed/app.ts +160 -0
- package/src/tui-blessed/index.ts +2 -0
- package/src/tui-blessed/neo-blessed.d.ts +6 -0
- package/src/tui-blessed/test.ts +21 -0
- package/src/tui-blessed/types.ts +14 -0
- package/src/utils/icons.ts +130 -0
- package/src/utils/index.ts +33 -0
- package/src/utils/resume-hint.ts +86 -0
- package/src/version.ts +1 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +35 -0
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tutorial System Tests
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for the tutorial system including:
|
|
5
|
+
* - TutorialSystem lifecycle and navigation
|
|
6
|
+
* - ProgressTracker step/lesson tracking
|
|
7
|
+
* - TipEngine contextual tip matching
|
|
8
|
+
* - Lesson content validation
|
|
9
|
+
* - Storage operations
|
|
10
|
+
*
|
|
11
|
+
* @module cli/onboarding/__tests__/tutorial
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
ALL_LESSONS,
|
|
18
|
+
BUILTIN_TIPS,
|
|
19
|
+
basicsLesson,
|
|
20
|
+
createProgressTracker,
|
|
21
|
+
createTipEngine,
|
|
22
|
+
createTutorialSystem,
|
|
23
|
+
getLessonById,
|
|
24
|
+
getLessonsByCategory,
|
|
25
|
+
getNextLesson,
|
|
26
|
+
getRecommendedOrder,
|
|
27
|
+
getTotalDuration,
|
|
28
|
+
getTotalStepCount,
|
|
29
|
+
INITIAL_TUTORIAL_PROGRESS,
|
|
30
|
+
LESSONS_BY_CATEGORY,
|
|
31
|
+
LESSONS_BY_ID,
|
|
32
|
+
MemoryTutorialStorage,
|
|
33
|
+
modesLesson,
|
|
34
|
+
type TipContext,
|
|
35
|
+
TipEngine,
|
|
36
|
+
type TutorialProgress,
|
|
37
|
+
type TutorialSystem,
|
|
38
|
+
toolsLesson,
|
|
39
|
+
} from "../index.js";
|
|
40
|
+
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Test Fixtures
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a fresh memory storage for testing
|
|
47
|
+
*/
|
|
48
|
+
function createTestStorage(): MemoryTutorialStorage {
|
|
49
|
+
return new MemoryTutorialStorage();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a tutorial system with memory storage
|
|
54
|
+
*/
|
|
55
|
+
function createTestTutorialSystem(): { system: TutorialSystem; storage: MemoryTutorialStorage } {
|
|
56
|
+
const storage = createTestStorage();
|
|
57
|
+
const system = createTutorialSystem(storage);
|
|
58
|
+
return { system, storage };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// =============================================================================
|
|
62
|
+
// T001: Lesson Registry Tests
|
|
63
|
+
// =============================================================================
|
|
64
|
+
|
|
65
|
+
describe("Lesson Registry", () => {
|
|
66
|
+
describe("ALL_LESSONS", () => {
|
|
67
|
+
it("should contain at least 3 lessons", () => {
|
|
68
|
+
expect(ALL_LESSONS.length).toBeGreaterThanOrEqual(3);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should include basics, tools, and modes lessons", () => {
|
|
72
|
+
const ids = ALL_LESSONS.map((l) => l.id);
|
|
73
|
+
expect(ids).toContain("basics");
|
|
74
|
+
expect(ids).toContain("tools");
|
|
75
|
+
expect(ids).toContain("modes");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should have unique lesson IDs", () => {
|
|
79
|
+
const ids = ALL_LESSONS.map((l) => l.id);
|
|
80
|
+
const uniqueIds = new Set(ids);
|
|
81
|
+
expect(uniqueIds.size).toBe(ids.length);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("getLessonById", () => {
|
|
86
|
+
it("should return lesson for valid ID", () => {
|
|
87
|
+
const lesson = getLessonById("basics");
|
|
88
|
+
expect(lesson).toBeDefined();
|
|
89
|
+
expect(lesson?.id).toBe("basics");
|
|
90
|
+
expect(lesson?.title).toBe("Getting Started with Vellum");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should return undefined for invalid ID", () => {
|
|
94
|
+
const lesson = getLessonById("nonexistent");
|
|
95
|
+
expect(lesson).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("getLessonsByCategory", () => {
|
|
100
|
+
it("should return lessons filtered by category", () => {
|
|
101
|
+
const basics = getLessonsByCategory("basics");
|
|
102
|
+
expect(basics.length).toBeGreaterThan(0);
|
|
103
|
+
expect(basics.every((l) => l.category === "basics")).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should return empty array for category with no lessons", () => {
|
|
107
|
+
const workflow = getLessonsByCategory("workflow");
|
|
108
|
+
expect(Array.isArray(workflow)).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("LESSONS_BY_ID", () => {
|
|
113
|
+
it("should index all lessons by ID", () => {
|
|
114
|
+
expect(LESSONS_BY_ID.basics).toBe(basicsLesson);
|
|
115
|
+
expect(LESSONS_BY_ID.tools).toBe(toolsLesson);
|
|
116
|
+
expect(LESSONS_BY_ID.modes).toBe(modesLesson);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("LESSONS_BY_CATEGORY", () => {
|
|
121
|
+
it("should group lessons by category", () => {
|
|
122
|
+
expect(LESSONS_BY_CATEGORY.basics).toContain(basicsLesson);
|
|
123
|
+
expect(LESSONS_BY_CATEGORY.tools).toContain(toolsLesson);
|
|
124
|
+
expect(LESSONS_BY_CATEGORY.modes).toContain(modesLesson);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// =============================================================================
|
|
130
|
+
// T002: Lesson Content Tests
|
|
131
|
+
// =============================================================================
|
|
132
|
+
|
|
133
|
+
describe("Lesson Content", () => {
|
|
134
|
+
describe("basicsLesson", () => {
|
|
135
|
+
it("should have valid structure", () => {
|
|
136
|
+
expect(basicsLesson.id).toBe("basics");
|
|
137
|
+
expect(basicsLesson.title).toBeDefined();
|
|
138
|
+
expect(basicsLesson.description).toBeDefined();
|
|
139
|
+
expect(basicsLesson.category).toBe("basics");
|
|
140
|
+
expect(basicsLesson.difficulty).toBe("beginner");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should have at least 5 steps", () => {
|
|
144
|
+
expect(basicsLesson.steps.length).toBeGreaterThanOrEqual(5);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should have unique step IDs", () => {
|
|
148
|
+
const ids = basicsLesson.steps.map((s: { id: string }) => s.id);
|
|
149
|
+
const uniqueIds = new Set(ids);
|
|
150
|
+
expect(uniqueIds.size).toBe(ids.length);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should have no prerequisites", () => {
|
|
154
|
+
expect(basicsLesson.prerequisites).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should have valid steps with required fields", () => {
|
|
158
|
+
for (const step of basicsLesson.steps) {
|
|
159
|
+
expect(step.id).toBeDefined();
|
|
160
|
+
expect(step.title).toBeDefined();
|
|
161
|
+
expect(step.content).toBeDefined();
|
|
162
|
+
expect(step.action).toBeDefined();
|
|
163
|
+
expect(["read", "command", "navigate", "interact", "complete"]).toContain(step.action);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("toolsLesson", () => {
|
|
169
|
+
it("should require basics as prerequisite", () => {
|
|
170
|
+
expect(toolsLesson.prerequisites).toContain("basics");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should be in tools category", () => {
|
|
174
|
+
expect(toolsLesson.category).toBe("tools");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("modesLesson", () => {
|
|
179
|
+
it("should require basics as prerequisite", () => {
|
|
180
|
+
expect(modesLesson.prerequisites).toContain("basics");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should be in modes category", () => {
|
|
184
|
+
expect(modesLesson.category).toBe("modes");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// =============================================================================
|
|
190
|
+
// T003: Tutorial System Tests
|
|
191
|
+
// =============================================================================
|
|
192
|
+
|
|
193
|
+
describe("TutorialSystem", () => {
|
|
194
|
+
let system: TutorialSystem;
|
|
195
|
+
|
|
196
|
+
beforeEach(async () => {
|
|
197
|
+
const result = createTestTutorialSystem();
|
|
198
|
+
system = result.system;
|
|
199
|
+
await system.initialize();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("initialization", () => {
|
|
203
|
+
it("should initialize without errors", async () => {
|
|
204
|
+
const newSystem = createTutorialSystem(createTestStorage());
|
|
205
|
+
await expect(newSystem.initialize()).resolves.not.toThrow();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("should have no active lesson after initialization", () => {
|
|
209
|
+
expect(system.currentStep()).toBeNull();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("start", () => {
|
|
214
|
+
it("should start a lesson successfully", async () => {
|
|
215
|
+
await system.start("basics");
|
|
216
|
+
expect(system.currentStep()).not.toBeNull();
|
|
217
|
+
expect(system.currentStep()?.id).toBe("basics-welcome");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("should throw for non-existent lesson", async () => {
|
|
221
|
+
await expect(system.start("nonexistent")).rejects.toThrow("Lesson not found");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should throw if prerequisites not met", async () => {
|
|
225
|
+
// tools requires basics
|
|
226
|
+
await expect(system.start("tools")).rejects.toThrow("Prerequisites not met");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("should allow starting lesson after prerequisites completed", async () => {
|
|
230
|
+
// Complete basics first
|
|
231
|
+
await system.start("basics");
|
|
232
|
+
let step = system.currentStep();
|
|
233
|
+
while (step) {
|
|
234
|
+
step = await system.completeStep(step.id);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Now tools should work
|
|
238
|
+
await expect(system.start("tools")).resolves.not.toThrow();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("completeStep", () => {
|
|
243
|
+
it("should advance to next step", async () => {
|
|
244
|
+
await system.start("basics");
|
|
245
|
+
const firstStep = system.currentStep();
|
|
246
|
+
expect(firstStep?.id).toBe("basics-welcome");
|
|
247
|
+
|
|
248
|
+
if (firstStep) {
|
|
249
|
+
await system.completeStep(firstStep.id);
|
|
250
|
+
}
|
|
251
|
+
const secondStep = system.currentStep();
|
|
252
|
+
expect(secondStep?.id).toBe("basics-chat");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should complete lesson when all steps done", async () => {
|
|
256
|
+
await system.start("basics");
|
|
257
|
+
const stepCount = basicsLesson.steps.length;
|
|
258
|
+
|
|
259
|
+
let step = system.currentStep();
|
|
260
|
+
for (let i = 0; i < stepCount && step; i++) {
|
|
261
|
+
step = await system.completeStep(step.id);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
expect(system.currentStep()).toBeNull();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should return null when no active lesson", async () => {
|
|
268
|
+
const result = await system.completeStep("test");
|
|
269
|
+
expect(result).toBeNull();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("skipStep", () => {
|
|
274
|
+
it("should skip current step without completing", async () => {
|
|
275
|
+
await system.start("basics");
|
|
276
|
+
await system.skipStep();
|
|
277
|
+
expect(system.currentStep()?.id).toBe("basics-chat");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("should return null when no active lesson", async () => {
|
|
281
|
+
const result = await system.skipStep();
|
|
282
|
+
expect(result).toBeNull();
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe("stop", () => {
|
|
287
|
+
it("should stop the current lesson", async () => {
|
|
288
|
+
await system.start("basics");
|
|
289
|
+
expect(system.currentStep()).not.toBeNull();
|
|
290
|
+
|
|
291
|
+
system.stop();
|
|
292
|
+
expect(system.currentStep()).toBeNull();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe("startNext", () => {
|
|
297
|
+
it("should start the first lesson when none completed", async () => {
|
|
298
|
+
const started = await system.startNext();
|
|
299
|
+
expect(started).toBe(true);
|
|
300
|
+
expect(system.currentStep()).not.toBeNull();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("should start next recommended lesson", async () => {
|
|
304
|
+
// Complete basics
|
|
305
|
+
await system.start("basics");
|
|
306
|
+
let step = system.currentStep();
|
|
307
|
+
while (step) {
|
|
308
|
+
step = await system.completeStep(step.id);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Start next should get tools or modes
|
|
312
|
+
const started = await system.startNext();
|
|
313
|
+
expect(started).toBe(true);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("resume", () => {
|
|
318
|
+
it("should resume method should not throw for started lesson", async () => {
|
|
319
|
+
// Create fresh system to avoid state pollution
|
|
320
|
+
const freshStorage = new MemoryTutorialStorage();
|
|
321
|
+
const freshSystem = createTutorialSystem(freshStorage);
|
|
322
|
+
await freshSystem.initialize();
|
|
323
|
+
|
|
324
|
+
// Start basics
|
|
325
|
+
await freshSystem.start("basics");
|
|
326
|
+
freshSystem.stop();
|
|
327
|
+
|
|
328
|
+
// Resume should not throw
|
|
329
|
+
await expect(freshSystem.resume("basics")).resolves.not.toThrow();
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// =============================================================================
|
|
335
|
+
// T004: Progress Tracker Tests
|
|
336
|
+
// =============================================================================
|
|
337
|
+
|
|
338
|
+
describe("ProgressTracker", () => {
|
|
339
|
+
let storage: MemoryTutorialStorage;
|
|
340
|
+
let tracker: ReturnType<typeof createProgressTracker>;
|
|
341
|
+
|
|
342
|
+
beforeEach(async () => {
|
|
343
|
+
// Create fresh storage for each test
|
|
344
|
+
storage = new MemoryTutorialStorage();
|
|
345
|
+
tracker = createProgressTracker(storage, ALL_LESSONS);
|
|
346
|
+
await tracker.loadProgress();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe("startLesson", () => {
|
|
350
|
+
it("should mark lesson as started", async () => {
|
|
351
|
+
const progress = await tracker.startLesson("basics");
|
|
352
|
+
expect(progress.started).toBe(true);
|
|
353
|
+
expect(progress.startedAt).toBeDefined();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("should not reset progress if already started", async () => {
|
|
357
|
+
const first = await tracker.startLesson("basics");
|
|
358
|
+
const second = await tracker.startLesson("basics");
|
|
359
|
+
expect(first.startedAt).toBe(second.startedAt);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe("completeStep", () => {
|
|
364
|
+
it("should add step to completed list", async () => {
|
|
365
|
+
// Use fresh tracker
|
|
366
|
+
const freshStorage = new MemoryTutorialStorage();
|
|
367
|
+
const freshTracker = createProgressTracker(freshStorage, ALL_LESSONS);
|
|
368
|
+
await freshTracker.loadProgress();
|
|
369
|
+
|
|
370
|
+
await freshTracker.startLesson("basics");
|
|
371
|
+
await freshTracker.completeStep("basics", "basics-welcome", 0);
|
|
372
|
+
|
|
373
|
+
const progress = await freshTracker.getLessonProgress("basics");
|
|
374
|
+
expect(progress?.completedSteps).toContain("basics-welcome");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("should increment step index after completing step", async () => {
|
|
378
|
+
// Use fresh tracker
|
|
379
|
+
const freshStorage = new MemoryTutorialStorage();
|
|
380
|
+
const freshTracker = createProgressTracker(freshStorage, ALL_LESSONS);
|
|
381
|
+
await freshTracker.loadProgress();
|
|
382
|
+
|
|
383
|
+
await freshTracker.startLesson("basics");
|
|
384
|
+
await freshTracker.completeStep("basics", "basics-welcome", 0);
|
|
385
|
+
|
|
386
|
+
const progress = await freshTracker.getLessonProgress("basics");
|
|
387
|
+
// After completing one step, index should be > 0
|
|
388
|
+
expect(progress?.currentStepIndex).toBeGreaterThan(0);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
describe("completeLesson", () => {
|
|
393
|
+
it("should mark lesson as completed", async () => {
|
|
394
|
+
await tracker.startLesson("basics");
|
|
395
|
+
await tracker.completeLesson("basics");
|
|
396
|
+
|
|
397
|
+
const progress = await tracker.getLessonProgress("basics");
|
|
398
|
+
expect(progress?.completed).toBe(true);
|
|
399
|
+
expect(progress?.completedAt).toBeDefined();
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe("getStats", () => {
|
|
404
|
+
it("should return correct stats structure", async () => {
|
|
405
|
+
const stats = await tracker.getStats();
|
|
406
|
+
expect(stats.totalLessons).toBe(ALL_LESSONS.length);
|
|
407
|
+
expect(typeof stats.completedLessons).toBe("number");
|
|
408
|
+
expect(typeof stats.completionPercent).toBe("number");
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("should update stats after completion", async () => {
|
|
412
|
+
await tracker.startLesson("basics");
|
|
413
|
+
await tracker.completeLesson("basics");
|
|
414
|
+
|
|
415
|
+
const stats = await tracker.getStats();
|
|
416
|
+
expect(stats.completedLessons).toBeGreaterThanOrEqual(1);
|
|
417
|
+
expect(stats.completionPercent).toBeGreaterThan(0);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// =============================================================================
|
|
423
|
+
// T005: Tip Engine Tests
|
|
424
|
+
// =============================================================================
|
|
425
|
+
|
|
426
|
+
describe("TipEngine", () => {
|
|
427
|
+
let engine: TipEngine;
|
|
428
|
+
|
|
429
|
+
beforeEach(() => {
|
|
430
|
+
engine = createTipEngine();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
describe("constructor", () => {
|
|
434
|
+
it("should register builtin tips", () => {
|
|
435
|
+
const tips = engine.getAllTips();
|
|
436
|
+
expect(tips.length).toBeGreaterThanOrEqual(BUILTIN_TIPS.length);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
describe("registerTip", () => {
|
|
441
|
+
it("should add custom tip", () => {
|
|
442
|
+
const customTip = {
|
|
443
|
+
id: "custom-tip",
|
|
444
|
+
title: "Custom",
|
|
445
|
+
content: "Custom tip content",
|
|
446
|
+
category: "shortcuts" as const,
|
|
447
|
+
trigger: { maxShows: 1 },
|
|
448
|
+
priority: 5,
|
|
449
|
+
dismissable: true,
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
engine.registerTip(customTip);
|
|
453
|
+
const tips = engine.getAllTips();
|
|
454
|
+
expect(tips.find((t) => t.id === "custom-tip")).toBeDefined();
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
describe("unregisterTip", () => {
|
|
459
|
+
it("should remove tip", () => {
|
|
460
|
+
engine.unregisterTip("tip-shortcuts-help");
|
|
461
|
+
const tips = engine.getAllTips();
|
|
462
|
+
expect(tips.find((t) => t.id === "tip-shortcuts-help")).toBeUndefined();
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe("getTip", () => {
|
|
467
|
+
it("should return null when no tips match context", () => {
|
|
468
|
+
// Reset all tips first
|
|
469
|
+
engine = new TipEngine();
|
|
470
|
+
for (const tip of BUILTIN_TIPS) {
|
|
471
|
+
engine.unregisterTip(tip.id);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const tip = engine.getTip({ screen: "nonexistent" });
|
|
475
|
+
expect(tip).toBeNull();
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("should return highest priority matching tip", () => {
|
|
479
|
+
const context: TipContext = { screen: "main" };
|
|
480
|
+
const tip = engine.getTip(context);
|
|
481
|
+
|
|
482
|
+
if (tip) {
|
|
483
|
+
// Should be one of the tips triggered by main screen
|
|
484
|
+
const matchingTips = BUILTIN_TIPS.filter((t) => t.trigger.screens?.includes("main"));
|
|
485
|
+
expect(matchingTips.some((t) => t.id === tip.id)).toBe(true);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it("should respect maxShows limit", () => {
|
|
490
|
+
const context: TipContext = { screen: "main" };
|
|
491
|
+
|
|
492
|
+
// Show tip max times
|
|
493
|
+
const tip = engine.getTip(context);
|
|
494
|
+
if (tip?.trigger.maxShows) {
|
|
495
|
+
for (let i = 1; i < tip.trigger.maxShows; i++) {
|
|
496
|
+
engine.getTip(context);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Should not return same tip after max shows
|
|
500
|
+
const allMatching = engine.getMatchingTips(context);
|
|
501
|
+
expect(allMatching.find((t) => t.id === tip.id)).toBeUndefined();
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
describe("dismissTip", () => {
|
|
507
|
+
it("should prevent tip from showing again", () => {
|
|
508
|
+
const context: TipContext = { screen: "main" };
|
|
509
|
+
const tip = engine.getTip(context);
|
|
510
|
+
|
|
511
|
+
if (tip) {
|
|
512
|
+
engine.dismissTip(tip.id);
|
|
513
|
+
expect(engine.isTipDismissed(tip.id)).toBe(true);
|
|
514
|
+
|
|
515
|
+
const matchingAfter = engine.getMatchingTips(context);
|
|
516
|
+
expect(matchingAfter.find((t) => t.id === tip.id)).toBeUndefined();
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
describe("getTipsByCategory", () => {
|
|
522
|
+
it("should return tips filtered by category", () => {
|
|
523
|
+
const shortcuts = engine.getTipsByCategory("shortcuts");
|
|
524
|
+
expect(shortcuts.length).toBeGreaterThan(0);
|
|
525
|
+
expect(shortcuts.every((t) => t.category === "shortcuts")).toBe(true);
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
describe("resetAllStates", () => {
|
|
530
|
+
it("should clear all tip states", () => {
|
|
531
|
+
const context: TipContext = { screen: "main" };
|
|
532
|
+
const tip = engine.getTip(context);
|
|
533
|
+
|
|
534
|
+
if (tip) {
|
|
535
|
+
engine.dismissTip(tip.id);
|
|
536
|
+
expect(engine.isTipDismissed(tip.id)).toBe(true);
|
|
537
|
+
|
|
538
|
+
engine.resetAllStates();
|
|
539
|
+
expect(engine.isTipDismissed(tip.id)).toBe(false);
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
describe("exportStates/importStates", () => {
|
|
545
|
+
it("should serialize and restore states", () => {
|
|
546
|
+
const context: TipContext = { screen: "main" };
|
|
547
|
+
engine.getTip(context);
|
|
548
|
+
|
|
549
|
+
const exported = engine.exportStates();
|
|
550
|
+
expect(Object.keys(exported).length).toBeGreaterThan(0);
|
|
551
|
+
|
|
552
|
+
// Create new engine and import
|
|
553
|
+
const newEngine = new TipEngine();
|
|
554
|
+
newEngine.importStates(exported);
|
|
555
|
+
|
|
556
|
+
const importedStates = newEngine.exportStates();
|
|
557
|
+
expect(importedStates).toEqual(exported);
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// =============================================================================
|
|
563
|
+
// T006: Storage Tests
|
|
564
|
+
// =============================================================================
|
|
565
|
+
|
|
566
|
+
describe("MemoryTutorialStorage", () => {
|
|
567
|
+
let storage: MemoryTutorialStorage;
|
|
568
|
+
|
|
569
|
+
beforeEach(() => {
|
|
570
|
+
storage = new MemoryTutorialStorage();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
describe("loadProgress", () => {
|
|
574
|
+
it("should return initial progress", async () => {
|
|
575
|
+
const progress = await storage.loadProgress();
|
|
576
|
+
expect(progress.lessonsCompleted).toBe(0);
|
|
577
|
+
expect(progress.stepsCompleted).toBe(0);
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
describe("saveProgress", () => {
|
|
582
|
+
it("should persist progress", async () => {
|
|
583
|
+
const newProgress: TutorialProgress = {
|
|
584
|
+
...INITIAL_TUTORIAL_PROGRESS,
|
|
585
|
+
lessonsCompleted: 1,
|
|
586
|
+
stepsCompleted: 5,
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
await storage.saveProgress(newProgress);
|
|
590
|
+
const loaded = await storage.loadProgress();
|
|
591
|
+
|
|
592
|
+
expect(loaded.lessonsCompleted).toBe(1);
|
|
593
|
+
expect(loaded.stepsCompleted).toBe(5);
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
describe("resetProgress", () => {
|
|
598
|
+
it("should reset to initial state", async () => {
|
|
599
|
+
await storage.saveProgress({
|
|
600
|
+
...INITIAL_TUTORIAL_PROGRESS,
|
|
601
|
+
lessonsCompleted: 3,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
await storage.resetProgress();
|
|
605
|
+
const progress = await storage.loadProgress();
|
|
606
|
+
|
|
607
|
+
expect(progress.lessonsCompleted).toBe(0);
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
describe("getLessonProgress", () => {
|
|
612
|
+
it("should return undefined for unknown lesson", async () => {
|
|
613
|
+
const progress = await storage.getLessonProgress("unknown");
|
|
614
|
+
expect(progress).toBeUndefined();
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it("should return lesson progress after update", async () => {
|
|
618
|
+
await storage.updateLessonProgress("basics", {
|
|
619
|
+
started: true,
|
|
620
|
+
currentStepIndex: 2,
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
const progress = await storage.getLessonProgress("basics");
|
|
624
|
+
expect(progress?.started).toBe(true);
|
|
625
|
+
expect(progress?.currentStepIndex).toBe(2);
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
describe("updateLessonProgress", () => {
|
|
630
|
+
it("should create new lesson progress", async () => {
|
|
631
|
+
await storage.updateLessonProgress("basics", {
|
|
632
|
+
started: true,
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
const progress = await storage.getLessonProgress("basics");
|
|
636
|
+
expect(progress?.started).toBe(true);
|
|
637
|
+
expect(progress?.lessonId).toBe("basics");
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("should update existing lesson progress", async () => {
|
|
641
|
+
await storage.updateLessonProgress("basics", { started: true });
|
|
642
|
+
await storage.updateLessonProgress("basics", { currentStepIndex: 3 });
|
|
643
|
+
|
|
644
|
+
const progress = await storage.getLessonProgress("basics");
|
|
645
|
+
expect(progress?.started).toBe(true);
|
|
646
|
+
expect(progress?.currentStepIndex).toBe(3);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it("should update lastActivityAt and lastActiveLessonId", async () => {
|
|
650
|
+
await storage.updateLessonProgress("basics", { started: true });
|
|
651
|
+
|
|
652
|
+
const overall = await storage.loadProgress();
|
|
653
|
+
expect(overall.lastActiveLessonId).toBe("basics");
|
|
654
|
+
expect(overall.lastActivityAt).toBeDefined();
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// =============================================================================
|
|
660
|
+
// T007: Helper Function Tests
|
|
661
|
+
// =============================================================================
|
|
662
|
+
|
|
663
|
+
describe("Helper Functions", () => {
|
|
664
|
+
describe("getNextLesson", () => {
|
|
665
|
+
it("should return basics when no lessons completed", () => {
|
|
666
|
+
const next = getNextLesson([]);
|
|
667
|
+
expect(next?.id).toBe("basics");
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it("should return tools or modes after basics", () => {
|
|
671
|
+
const next = getNextLesson(["basics"]);
|
|
672
|
+
expect(["tools", "modes"]).toContain(next?.id);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it("should return undefined when all completed", () => {
|
|
676
|
+
const allIds = ALL_LESSONS.map((l) => l.id);
|
|
677
|
+
const next = getNextLesson(allIds);
|
|
678
|
+
expect(next).toBeUndefined();
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
describe("getRecommendedOrder", () => {
|
|
683
|
+
it("should start with basics", () => {
|
|
684
|
+
const order = getRecommendedOrder();
|
|
685
|
+
expect(order[0]?.id).toBe("basics");
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it("should include all lessons", () => {
|
|
689
|
+
const order = getRecommendedOrder();
|
|
690
|
+
expect(order.length).toBe(ALL_LESSONS.length);
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
describe("getTotalDuration", () => {
|
|
695
|
+
it("should sum all lesson durations", () => {
|
|
696
|
+
const total = getTotalDuration();
|
|
697
|
+
const expected = ALL_LESSONS.reduce((sum, l) => sum + l.estimatedMinutes, 0);
|
|
698
|
+
expect(total).toBe(expected);
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
describe("getTotalStepCount", () => {
|
|
703
|
+
it("should sum all lesson steps", () => {
|
|
704
|
+
const total = getTotalStepCount();
|
|
705
|
+
const expected = ALL_LESSONS.reduce((sum, l) => sum + l.steps.length, 0);
|
|
706
|
+
expect(total).toBe(expected);
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// =============================================================================
|
|
712
|
+
// T008: Initial Constants Tests
|
|
713
|
+
// =============================================================================
|
|
714
|
+
|
|
715
|
+
describe("Initial Constants", () => {
|
|
716
|
+
describe("default lesson progress", () => {
|
|
717
|
+
it("should have correct default values for new lessons", async () => {
|
|
718
|
+
const storage = new MemoryTutorialStorage();
|
|
719
|
+
// New storage should return undefined for unknown lesson
|
|
720
|
+
const progress = await storage.getLessonProgress("unknown");
|
|
721
|
+
expect(progress).toBeUndefined();
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
describe("storage initialization", () => {
|
|
726
|
+
it("should return fresh progress with zero counts", async () => {
|
|
727
|
+
const storage = new MemoryTutorialStorage();
|
|
728
|
+
const progress = await storage.loadProgress();
|
|
729
|
+
expect(progress.lessonsCompleted).toBe(0);
|
|
730
|
+
expect(progress.stepsCompleted).toBe(0);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it("should have undefined timestamps on fresh storage", async () => {
|
|
734
|
+
const storage = new MemoryTutorialStorage();
|
|
735
|
+
const progress = await storage.loadProgress();
|
|
736
|
+
expect(progress.startedAt).toBeUndefined();
|
|
737
|
+
expect(progress.lastActivityAt).toBeUndefined();
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
});
|