@sriinnu/runic 1.2.0 → 2.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/CHANGELOG.md +11 -0
- package/README.md +8 -1
- package/RuniciOS/Core/RunicTypographyiOS.swift +24 -0
- package/RuniciOS/Views/ProviderDetailView.swift +9 -9
- package/RuniciOS/Views/ProviderListView.swift +10 -10
- package/RuniciOS/Widgets/LargeWidget.swift +14 -14
- package/RuniciOS/Widgets/LiveActivity.swift +19 -19
- package/RuniciOS/Widgets/LockScreenWidget.swift +15 -15
- package/RuniciOS/Widgets/MediumWidget.swift +10 -10
- package/RuniciOS/Widgets/RunicWidget.swift +5 -5
- package/RuniciOS/Widgets/SmallWidget.swift +9 -9
- package/Sources/Runic/About.swift +3 -3
- package/Sources/Runic/AppNotifications.swift +5 -3
- package/Sources/Runic/Controllers/StatusItemController+Actions.swift +7 -3
- package/Sources/Runic/Controllers/StatusItemController+Animation.swift +5 -4
- package/Sources/Runic/Controllers/StatusItemController+ChartSubmenus.swift +30 -16
- package/Sources/Runic/Controllers/StatusItemController+CustomProviders.swift +6 -3
- package/Sources/Runic/Controllers/StatusItemController+Menu.swift +18 -13
- package/Sources/Runic/Controllers/StatusItemController+MenuCards.swift +6 -4
- package/Sources/Runic/Controllers/StatusItemController+MenuHelpers.swift +40 -12
- package/Sources/Runic/Controllers/StatusItemController.swift +43 -43
- package/Sources/Runic/Core/BudgetNotificationManager.swift +2 -1
- package/Sources/Runic/Core/Login/ClaudeLoginRunner.swift +2 -2
- package/Sources/Runic/Core/Login/CodexLoginRunner.swift +1 -1
- package/Sources/Runic/Core/Login/CursorLoginRunner.swift +4 -4
- package/Sources/Runic/Core/Login/GeminiLoginRunner.swift +1 -1
- package/Sources/Runic/Core/Rendering/IconRenderer.swift +91 -85
- package/Sources/Runic/Core/Rendering/IconView.swift +3 -1
- package/Sources/Runic/Core/RunicColors.swift +6 -6
- package/Sources/Runic/Core/RunicSpacing.swift +7 -7
- package/Sources/Runic/Core/RunicTypography.swift +206 -0
- package/Sources/Runic/Core/Stores/AuggieTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/CohereTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/CustomProviderStore.swift +10 -10
- package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/GroqTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/KimiTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +1 -3
- package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +1 -3
- package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/MistralTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/ProviderToggleStore.swift +1 -1
- package/Sources/Runic/Core/Stores/QwenTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/SettingsStore.swift +89 -51
- package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/UsageStore+Accessors.swift +19 -6
- package/Sources/Runic/Core/Stores/UsageStore+TokenCost.swift +1 -1
- package/Sources/Runic/Core/Stores/UsageStore+WidgetSnapshot.swift +1 -1
- package/Sources/Runic/Core/Stores/UsageStore.swift +187 -161
- package/Sources/Runic/Core/Stores/XAITokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +1 -2
- package/Sources/Runic/Core/UsageExporter.swift +8 -4
- package/Sources/Runic/ProviderRegistry.swift +1 -1
- package/Sources/Runic/Providers/Antigravity/AntigravityProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Auggie/AuggieProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Azure/AzureProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Bedrock/BedrockProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Cerebras/CerebrasProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Codex/CodexProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Cohere/CohereProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Cursor/CursorProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/DeepSeek/DeepSeekProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Factory/FactoryProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Fireworks/FireworksProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Gemini/GeminiProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Groq/GroqProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Kimi/KimiProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/MiniMax/MiniMaxProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Mistral/MistralProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/OpenRouter/OpenRouterProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Perplexity/PerplexityProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/SambaNova/SambaNovaProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Shared/ProviderImplementation.swift +5 -2
- package/Sources/Runic/Providers/Shared/ProviderImplementationRegistry.swift +1 -1
- package/Sources/Runic/Providers/Shared/ProviderSettingsDescriptors.swift +1 -1
- package/Sources/Runic/Providers/Together/TogetherProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/XAI/XAIProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Zai/ZaiProviderImplementation.swift +1 -1
- package/Sources/Runic/Resources/Fonts/Caveat-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/Caveat-Regular.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/FiraCode-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/FiraCode-Light.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/FiraCode-Medium.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/FiraCode-Regular.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/FiraCode-SemiBold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/IBMPlexMono-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/IBMPlexMono-Medium.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/IBMPlexMono-Regular.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/IBMPlexMono-SemiBold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/Inconsolata-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/Inconsolata-Medium.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/Inconsolata-Regular.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/Inconsolata-SemiBold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/JetBrainsMono-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/JetBrainsMono-Medium.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/JetBrainsMono-Regular.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/JetBrainsMono-SemiBold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/SpaceMono-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/SpaceMono-Regular.ttf +0 -0
- package/Sources/Runic/RunicApp.swift +46 -36
- package/Sources/Runic/SessionQuotaNotifications.swift +2 -2
- package/Sources/Runic/UsagePaceText.swift +1 -1
- package/Sources/Runic/UsageProgressBar.swift +13 -9
- package/Sources/Runic/Utilities/LoadingPattern.swift +4 -2
- package/Sources/Runic/Utilities/PerformanceConstants.swift +28 -28
- package/Sources/Runic/Views/Menu/AlertsMenuView.swift +19 -21
- package/Sources/Runic/Views/Menu/CostComparativeChartMenuView.swift +19 -19
- package/Sources/Runic/Views/Menu/CostHistoryChartMenuView.swift +7 -7
- package/Sources/Runic/Views/Menu/CreditsHistoryChartMenuView.swift +5 -5
- package/Sources/Runic/Views/Menu/EfficiencyMetricsMenuView.swift +29 -30
- package/Sources/Runic/Views/Menu/HeroStatCardsMenuView.swift +20 -17
- package/Sources/Runic/Views/Menu/HourlyActivityChartMenuView.swift +14 -14
- package/Sources/Runic/Views/Menu/InlineUsageChartView.swift +12 -10
- package/Sources/Runic/Views/Menu/MenuCardEntranceModifier.swift +3 -4
- package/Sources/Runic/Views/Menu/MenuCardSidebarView.swift +5 -25
- package/Sources/Runic/Views/Menu/MenuCardSkeletonView.swift +1 -1
- package/Sources/Runic/Views/Menu/MenuCardView.swift +120 -131
- package/Sources/Runic/Views/Menu/MenuContent.swift +11 -8
- package/Sources/Runic/Views/Menu/MenuDescriptor.swift +4 -2
- package/Sources/Runic/Views/Menu/MenuHighlightStyle.swift +1 -8
- package/Sources/Runic/Views/Menu/ModelBreakdownMenuView.swift +20 -14
- package/Sources/Runic/Views/Menu/OverviewMenuView.swift +18 -15
- package/Sources/Runic/Views/Menu/ProjectBreakdownMenuView.swift +16 -11
- package/Sources/Runic/Views/Menu/ProjectBudgetMenuView.swift +27 -27
- package/Sources/Runic/Views/Menu/ProviderTabBarView.swift +4 -6
- package/Sources/Runic/Views/Menu/SubscriptionUtilizationChartMenuView.swift +24 -11
- package/Sources/Runic/Views/Menu/UsageBreakdownChartMenuView.swift +5 -5
- package/Sources/Runic/Views/Menu/UsageHeatmapMenuView.swift +7 -8
- package/Sources/Runic/Views/Menu/UsageTimelineChartMenuView.swift +22 -17
- package/Sources/Runic/Views/Menu/UsageWindowComparisonChartMenuView.swift +8 -7
- package/Sources/Runic/Views/Menu/WeeklyActivityChartMenuView.swift +7 -7
- package/Sources/Runic/Views/Performance/PerformanceDashboardView.swift +50 -47
- package/Sources/Runic/Views/Preferences/LiquidComponents.swift +15 -14
- package/Sources/Runic/Views/Preferences/PreferencesAboutPane.swift +7 -7
- package/Sources/Runic/Views/Preferences/PreferencesAdvancedPane.swift +27 -27
- package/Sources/Runic/Views/Preferences/PreferencesAlertsPane.swift +38 -42
- package/Sources/Runic/Views/Preferences/PreferencesAnalyticsPane.swift +39 -43
- package/Sources/Runic/Views/Preferences/PreferencesBudgetsPane.swift +21 -21
- package/Sources/Runic/Views/Preferences/PreferencesComponents.swift +7 -7
- package/Sources/Runic/Views/Preferences/PreferencesCustomProvidersPane.swift +49 -50
- package/Sources/Runic/Views/Preferences/PreferencesDebugPane.swift +10 -10
- package/Sources/Runic/Views/Preferences/PreferencesGeneralPane.swift +40 -21
- package/Sources/Runic/Views/Preferences/PreferencesHelpPane.swift +7 -7
- package/Sources/Runic/Views/Preferences/PreferencesIntegrationsPane.swift +44 -39
- package/Sources/Runic/Views/Preferences/PreferencesPerformancePane.swift +26 -23
- package/Sources/Runic/Views/Preferences/PreferencesProvidersPane.swift +271 -226
- package/Sources/Runic/Views/Preferences/PreferencesSelection.swift +6 -2
- package/Sources/Runic/Views/Preferences/PreferencesSyncPane.swift +7 -5
- package/Sources/Runic/Views/Preferences/PreferencesView.swift +38 -31
- package/Sources/Runic/Views/Rating/QualityRatingView.swift +11 -11
- package/Sources/Runic/Views/Team/TeamDashboardView.swift +29 -24
- package/Sources/Runic/Views/Team/TeamInviteSheet.swift +16 -16
- package/Sources/Runic/Views/Team/TeamManagementView.swift +50 -49
- package/Sources/Runic/Views/Team/TeamMemberRow.swift +8 -8
- package/Sources/RunicCLI/CLIEntry.swift +83 -42
- package/Sources/RunicCLI/CLIRenderer.swift +1 -1
- package/Sources/RunicCLI/Commands/EnhancedUsageCommand.swift +60 -60
- package/Sources/RunicCLI/Commands/ModelsCommand.swift +45 -44
- package/Sources/RunicCLI/Commands/ProjectsCommand.swift +83 -69
- package/Sources/RunicCLI/Commands/ResetCommand.swift +50 -49
- package/Sources/RunicCLI/Commands/UsageLedgerSourceFactory.swift +31 -32
- package/Sources/RunicClaudeWebProbe/main.swift +1 -1
- package/Sources/RunicCore/AlertRuleStore.swift +20 -20
- package/Sources/RunicCore/BrowserCookieImportOrder.swift +3 -3
- package/Sources/RunicCore/CostUsageModels.swift +1 -1
- package/Sources/RunicCore/CustomProviders/CustomProviderModels.swift +29 -30
- package/Sources/RunicCore/CustomProviders/GenericProviderFetcher.swift +73 -75
- package/Sources/RunicCore/CustomProviders/ProviderTemplates.swift +53 -109
- package/Sources/RunicCore/Host/PTY/TTYCommandRunner.swift +3 -3
- package/Sources/RunicCore/Integrations/GitHubIntegration.swift +3 -3
- package/Sources/RunicCore/Models/EnhancedUsageModels.swift +47 -48
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardBrowserCookieImporter.swift +1 -1
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardFetcher.swift +1 -1
- package/Sources/RunicCore/Performance/PerformanceModels.swift +4 -4
- package/Sources/RunicCore/Performance/PerformanceStorageImpl.swift +81 -64
- package/Sources/RunicCore/Policy/UsagePolicyEngine.swift +2 -2
- package/Sources/RunicCore/ProjectBudgetStore.swift +15 -12
- package/Sources/RunicCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Antigravity/AntigravityStatusProbe.swift +2 -2
- package/Sources/RunicCore/Providers/Auggie/AuggieProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Auggie/AuggieUsageFetcher.swift +3 -3
- package/Sources/RunicCore/Providers/Azure/AzureProviderDescriptor.swift +8 -8
- package/Sources/RunicCore/Providers/Azure/AzureUsageFetcher.swift +3 -3
- package/Sources/RunicCore/Providers/Bedrock/BedrockProviderDescriptor.swift +6 -6
- package/Sources/RunicCore/Providers/Bedrock/BedrockUsageFetcher.swift +3 -7
- package/Sources/RunicCore/Providers/Cerebras/CerebrasProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Cerebras/CerebrasUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift +3 -3
- package/Sources/RunicCore/Providers/Claude/ClaudeProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Claude/ClaudeStatusProbe.swift +4 -4
- package/Sources/RunicCore/Providers/Claude/ClaudeUsageDataSource.swift +3 -1
- package/Sources/RunicCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +1 -1
- package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +1 -1
- package/Sources/RunicCore/Providers/Codex/CodexProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Codex/CodexUsageDataSource.swift +3 -1
- package/Sources/RunicCore/Providers/Codex/CodexWebDashboardStrategy.swift +5 -3
- package/Sources/RunicCore/Providers/Cohere/CohereProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Cohere/CohereUsageFetcher.swift +3 -3
- package/Sources/RunicCore/Providers/Copilot/CopilotGitHubCLITokenReader.swift +7 -4
- package/Sources/RunicCore/Providers/Copilot/CopilotProviderDescriptor.swift +4 -4
- package/Sources/RunicCore/Providers/Copilot/CopilotUsageFetcher.swift +5 -3
- package/Sources/RunicCore/Providers/Copilot/CopilotVSCodeTokenReader.swift +9 -10
- package/Sources/RunicCore/Providers/Cursor/CursorProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Factory/FactoryLocalStorageImporter.swift +4 -4
- package/Sources/RunicCore/Providers/Factory/FactoryProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Factory/FactoryStatusProbe.swift +2 -2
- package/Sources/RunicCore/Providers/Fireworks/FireworksProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Fireworks/FireworksUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Gemini/GeminiProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Gemini/GeminiStatusProbe.swift +5 -2
- package/Sources/RunicCore/Providers/Groq/GroqProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Groq/GroqUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Kimi/KimiProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Kimi/KimiUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +10 -5
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +7 -7
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxWebUsageFetcher.swift +10 -10
- package/Sources/RunicCore/Providers/Mistral/MistralProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Mistral/MistralUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/OpenRouter/OpenRouterUsageFetcher.swift +23 -10
- package/Sources/RunicCore/Providers/Perplexity/PerplexityProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Perplexity/PerplexityUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/ProviderFetchPlan.swift +2 -2
- package/Sources/RunicCore/Providers/ProviderTokenResolver.swift +2 -2
- package/Sources/RunicCore/Providers/Qwen/QwenProviderDescriptor.swift +5 -3
- package/Sources/RunicCore/Providers/Qwen/QwenUsageStats.swift +26 -13
- package/Sources/RunicCore/Providers/SambaNova/SambaNovaProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/SambaNova/SambaNovaUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Together/TogetherProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Together/TogetherUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/VertexAI/VertexAIProviderDescriptor.swift +5 -5
- package/Sources/RunicCore/Providers/VertexAI/VertexAISettingsReader.swift +1 -1
- package/Sources/RunicCore/Providers/VertexAI/VertexAIUsageFetcher.swift +3 -3
- package/Sources/RunicCore/Providers/XAI/XAIProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/XAI/XAIUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Zai/ZaiProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Zai/ZaiUsageStats.swift +48 -21
- package/Sources/RunicCore/Sync/BackgroundSyncManager.swift +76 -77
- package/Sources/RunicCore/Sync/SyncConflictResolver.swift +55 -58
- package/Sources/RunicCore/Sync/SyncProtocol.swift +21 -22
- package/Sources/RunicCore/Sync/SyncRecord.swift +46 -49
- package/Sources/RunicCore/Sync/SyncUsageExample.swift +44 -50
- package/Sources/RunicCore/Sync/TeamSyncRecords.swift +94 -101
- package/Sources/RunicCore/Sync/iCloudSyncEngine.swift +51 -46
- package/Sources/RunicCore/Team/TeamModels.swift +111 -110
- package/Sources/RunicCore/Team/TeamStore.swift +126 -109
- package/Sources/RunicCore/UsageFetcher.swift +2 -2
- package/Sources/RunicCore/UsageFormatter.swift +25 -25
- package/Sources/RunicCore/UsageLedger/ClaudeUsageLogSource.swift +27 -13
- package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +11 -3
- package/Sources/RunicCore/UsageLedger/LedgerCache.swift +5 -2
- package/Sources/RunicCore/UsageLedger/OTelGenAILedgerAdapter.swift +1 -1
- package/Sources/RunicCore/UsageLedger/UsageLedgerAggregator.swift +40 -25
- package/Sources/RunicCore/UsageLedger/UsageLedgerInsightsAdvisor.swift +1 -1
- package/Sources/RunicCore/UsageLedger/UsageLedgerProjectIdentity.swift +55 -16
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageCache.swift +6 -6
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageJsonl.swift +1 -1
- package/Sources/RunicCore/Vendored/CostUsage/CostUsagePricing.swift +2 -2
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner.swift +5 -5
- package/Tests/RunicTests/AppDelegateTests.swift +8 -3
- package/Tests/RunicTests/BrowserCookieOrderLabelTests.swift +2 -3
- package/Tests/RunicTests/CLIArgumentParsingTests.swift +3 -4
- package/Tests/RunicTests/CLICostTests.swift +6 -7
- package/Tests/RunicTests/CLIProviderSelectionTests.swift +2 -3
- package/Tests/RunicTests/CLISnapshotTests.swift +3 -4
- package/Tests/RunicTests/CLIWebFallbackTests.swift +3 -4
- package/Tests/RunicTests/ClaudeOAuthTests.swift +11 -12
- package/Tests/RunicTests/ClaudeResilienceTests.swift +3 -4
- package/Tests/RunicTests/ClaudeUsageTests.swift +20 -21
- package/Tests/RunicTests/CodexOAuthTests.swift +7 -8
- package/Tests/RunicTests/CodexUsageLogSourceTests.swift +1 -3
- package/Tests/RunicTests/CopilotTokenResolverTests.swift +5 -6
- package/Tests/RunicTests/CopilotVSCodeTokenReaderTests.swift +4 -5
- package/Tests/RunicTests/CostUsageDecodingTests.swift +15 -17
- package/Tests/RunicTests/CostUsagePricingTests.swift +5 -7
- package/Tests/RunicTests/CostUsageScannerTests.swift +10 -12
- package/Tests/RunicTests/CursorStatusProbeTests.swift +15 -16
- package/Tests/RunicTests/FactoryStatusProbeTests.swift +3 -5
- package/Tests/RunicTests/GeminiLoginAlertTests.swift +3 -4
- package/Tests/RunicTests/GeminiStatusProbeAPITests.swift +11 -11
- package/Tests/RunicTests/GeminiStatusProbePlanTests.swift +8 -8
- package/Tests/RunicTests/GeminiStatusProbeTests.swift +20 -21
- package/Tests/RunicTests/GoogleWorkspaceStatusTests.swift +2 -3
- package/Tests/RunicTests/InstallOriginTests.swift +1 -2
- package/Tests/RunicTests/KeyboardShortcutsBundleTests.swift +2 -2
- package/Tests/RunicTests/LiveAccountTests.swift +3 -3
- package/Tests/RunicTests/LoadingPatternTests.swift +3 -4
- package/Tests/RunicTests/MenuCardModelTests.swift +33 -34
- package/Tests/RunicTests/MiniMaxCookieHeaderStoreTestSupport.swift +4 -1
- package/Tests/RunicTests/MiniMaxGroupIDStoreTestSupport.swift +4 -1
- package/Tests/RunicTests/MiniMaxParsingTests.swift +4 -5
- package/Tests/RunicTests/MiniMaxTokenStoreTestSupport.swift +4 -1
- package/Tests/RunicTests/OTelGenAILedgerAdapterTests.swift +4 -6
- package/Tests/RunicTests/OpenAIDashboardBrowserCookieImporterTests.swift +1 -2
- package/Tests/RunicTests/OpenAIDashboardFetcherCreditsWaitTests.swift +5 -6
- package/Tests/RunicTests/OpenAIDashboardOffscreenHostTests.swift +2 -3
- package/Tests/RunicTests/OpenAIDashboardParserTests.swift +12 -13
- package/Tests/RunicTests/OpenAIWebAccountSwitchTests.swift +17 -6
- package/Tests/RunicTests/OpenRouterTokenStoreTestSupport.swift +52 -13
- package/Tests/RunicTests/PathBuilderTests.swift +14 -15
- package/Tests/RunicTests/ProviderFetchPipelineTests.swift +3 -4
- package/Tests/RunicTests/ProviderIconResourcesTests.swift +1 -2
- package/Tests/RunicTests/ProviderMetadataStatusLinkTests.swift +1 -2
- package/Tests/RunicTests/ProviderSettingsDescriptorTests.swift +53 -19
- package/Tests/RunicTests/ProviderToggleStoreTests.swift +10 -11
- package/Tests/RunicTests/RunicTests.swift +6 -7
- package/Tests/RunicTests/SessionQuotaNotificationLogicTests.swift +5 -6
- package/Tests/RunicTests/SettingsStoreTests.swift +149 -45
- package/Tests/RunicTests/StatusItemAnimationTests.swift +8 -3
- package/Tests/RunicTests/StatusMenuTests.swift +63 -22
- package/Tests/RunicTests/StatusProbeTests.swift +28 -29
- package/Tests/RunicTests/SubprocessRunnerTests.swift +1 -3
- package/Tests/RunicTests/SubscriptionDetectionTests.swift +8 -9
- package/Tests/RunicTests/TTYCommandRunnerTests.swift +8 -9
- package/Tests/RunicTests/TeamShowbackExporterTests.swift +2 -4
- package/Tests/RunicTests/TextParsingTests.swift +1 -2
- package/Tests/RunicTests/UsageFormatterTests.swift +15 -16
- package/Tests/RunicTests/UsageLedgerAnomalyDetectionTests.swift +12 -14
- package/Tests/RunicTests/UsageLedgerForecastTests.swift +4 -6
- package/Tests/RunicTests/UsageLedgerInsightsAdvisorTests.swift +7 -9
- package/Tests/RunicTests/UsageLedgerProjectNameTests.swift +6 -8
- package/Tests/RunicTests/UsagePaceTests.swift +5 -6
- package/Tests/RunicTests/UsagePaceTextTests.swift +8 -9
- package/Tests/RunicTests/UsagePolicyEngineTests.swift +3 -5
- package/Tests/RunicTests/WidgetSnapshotTests.swift +1 -3
- package/Tests/RunicTests/ZaiProviderTests.swift +3 -5
- package/Tests/RunicTests/ZaiTokenStoreTestSupport.swift +4 -1
- package/appcast.xml +36 -76
- package/package.json +1 -1
- package/runic.png +0 -0
- package/runic.svg +15 -0
- package/version.env +2 -2
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import RunicCore
|
|
2
1
|
import Foundation
|
|
2
|
+
import RunicCore
|
|
3
3
|
import Testing
|
|
4
4
|
@testable import Runic
|
|
5
5
|
|
|
6
|
-
@Suite
|
|
7
6
|
struct StatusProbeTests {
|
|
8
7
|
@Test
|
|
9
|
-
func
|
|
8
|
+
func `parse codex status`() throws {
|
|
10
9
|
let sample = """
|
|
11
10
|
Model: gpt
|
|
12
11
|
Credits: 980 credits
|
|
@@ -20,7 +19,7 @@ struct StatusProbeTests {
|
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
@Test
|
|
23
|
-
func
|
|
22
|
+
func `parse codex status with ansi and resets`() throws {
|
|
24
23
|
let sample = """
|
|
25
24
|
\u{001B}[38;5;245mCredits:\u{001B}[0m 557 credits
|
|
26
25
|
5h limit: [█████ ] 50% left (resets 09:01)
|
|
@@ -33,7 +32,7 @@ struct StatusProbeTests {
|
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
@Test
|
|
36
|
-
func
|
|
35
|
+
func `parse claude status`() throws {
|
|
37
36
|
let sample = """
|
|
38
37
|
Settings: Status Config Usage (tab to cycle)
|
|
39
38
|
|
|
@@ -64,7 +63,7 @@ struct StatusProbeTests {
|
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
@Test
|
|
67
|
-
func
|
|
66
|
+
func `parse claude status with ANSI`() throws {
|
|
68
67
|
let sample = """
|
|
69
68
|
\u{001B}[35mCurrent session\u{001B}[0m
|
|
70
69
|
40% used (Resets 11am)
|
|
@@ -86,7 +85,7 @@ struct StatusProbeTests {
|
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
@Test
|
|
89
|
-
func
|
|
88
|
+
func `parse claude status legacy opus label`() throws {
|
|
90
89
|
let sample = """
|
|
91
90
|
Current session
|
|
92
91
|
12% used (Resets 11am)
|
|
@@ -107,7 +106,7 @@ struct StatusProbeTests {
|
|
|
107
106
|
}
|
|
108
107
|
|
|
109
108
|
@Test
|
|
110
|
-
func
|
|
109
|
+
func `parse claude status enterprise session only`() throws {
|
|
111
110
|
let sample = """
|
|
112
111
|
Current session
|
|
113
112
|
█ 2% used
|
|
@@ -121,7 +120,7 @@ struct StatusProbeTests {
|
|
|
121
120
|
}
|
|
122
121
|
|
|
123
122
|
@Test
|
|
124
|
-
func
|
|
123
|
+
func `parse claude status reset mappings with CR line endings`() throws {
|
|
125
124
|
let sample =
|
|
126
125
|
"Current session\r" +
|
|
127
126
|
"██████████████████████████████████████████████████ 17% used\r" +
|
|
@@ -143,7 +142,7 @@ struct StatusProbeTests {
|
|
|
143
142
|
}
|
|
144
143
|
|
|
145
144
|
@Test
|
|
146
|
-
func
|
|
145
|
+
func `parse claude status reset mappings does not promote weekly reset to session`() throws {
|
|
147
146
|
let sample = """
|
|
148
147
|
Current session
|
|
149
148
|
██████████████████████████████████████████████████ 17% used
|
|
@@ -159,7 +158,7 @@ struct StatusProbeTests {
|
|
|
159
158
|
}
|
|
160
159
|
|
|
161
160
|
@Test
|
|
162
|
-
func
|
|
161
|
+
func `parse claude status with plan and ansi noise`() throws {
|
|
163
162
|
let sample = """
|
|
164
163
|
Settings: Status Config Usage
|
|
165
164
|
|
|
@@ -184,7 +183,7 @@ struct StatusProbeTests {
|
|
|
184
183
|
}
|
|
185
184
|
|
|
186
185
|
@Test
|
|
187
|
-
func
|
|
186
|
+
func `parse claude status with bracket plan noise no esc`() throws {
|
|
188
187
|
let sample = """
|
|
189
188
|
Login method: [22m Claude Max Account
|
|
190
189
|
Account: user@example.com
|
|
@@ -203,7 +202,7 @@ struct StatusProbeTests {
|
|
|
203
202
|
}
|
|
204
203
|
|
|
205
204
|
@Test
|
|
206
|
-
func
|
|
205
|
+
func `surfaces claude token expired`() {
|
|
207
206
|
let sample = """
|
|
208
207
|
Settings: Status Config Usage
|
|
209
208
|
|
|
@@ -226,7 +225,7 @@ struct StatusProbeTests {
|
|
|
226
225
|
}
|
|
227
226
|
|
|
228
227
|
@Test
|
|
229
|
-
func
|
|
228
|
+
func `surfaces claude folder trust prompt`() {
|
|
230
229
|
let sample = """
|
|
231
230
|
Do you trust the files in this folder?
|
|
232
231
|
|
|
@@ -246,7 +245,7 @@ struct StatusProbeTests {
|
|
|
246
245
|
}
|
|
247
246
|
|
|
248
247
|
@Test
|
|
249
|
-
func
|
|
248
|
+
func `surfaces claude folder trust prompt with CRLF and spaces`() {
|
|
250
249
|
let sample = "Do you trust the files in this folder?\r\n\r\n/Users/example/My Project\r\n"
|
|
251
250
|
|
|
252
251
|
do {
|
|
@@ -261,7 +260,7 @@ struct StatusProbeTests {
|
|
|
261
260
|
}
|
|
262
261
|
|
|
263
262
|
@Test
|
|
264
|
-
func
|
|
263
|
+
func `surfaces claude folder trust prompt without folder path`() {
|
|
265
264
|
let sample = """
|
|
266
265
|
Do you trust the files in this folder?
|
|
267
266
|
"""
|
|
@@ -279,25 +278,25 @@ struct StatusProbeTests {
|
|
|
279
278
|
}
|
|
280
279
|
|
|
281
280
|
@Test
|
|
282
|
-
func
|
|
281
|
+
func `parses claude reset time only`() throws {
|
|
283
282
|
let now = Date(timeIntervalSince1970: 1_733_690_000)
|
|
284
283
|
let parsed = ClaudeStatusProbe.parseResetDate(from: "Resets 12:59pm (Europe/Helsinki)", now: now)
|
|
285
|
-
let tz = TimeZone(identifier: "Europe/Helsinki")
|
|
284
|
+
let tz = try #require(TimeZone(identifier: "Europe/Helsinki"))
|
|
286
285
|
var calendar = Calendar(identifier: .gregorian)
|
|
287
286
|
calendar.timeZone = tz
|
|
288
|
-
var expected = calendar.date(bySettingHour: 12, minute: 59, second: 0, of: now)
|
|
287
|
+
var expected = try #require(calendar.date(bySettingHour: 12, minute: 59, second: 0, of: now))
|
|
289
288
|
if expected < now {
|
|
290
|
-
expected = calendar.date(byAdding: .day, value: 1, to: expected)
|
|
289
|
+
expected = try #require(calendar.date(byAdding: .day, value: 1, to: expected))
|
|
291
290
|
}
|
|
292
291
|
#expect(parsed == expected)
|
|
293
292
|
}
|
|
294
293
|
|
|
295
294
|
@Test
|
|
296
|
-
func
|
|
295
|
+
func `parses claude reset date and time`() throws {
|
|
297
296
|
let now = Date(timeIntervalSince1970: 1_733_690_000)
|
|
298
297
|
let parsed = ClaudeStatusProbe.parseResetDate(from: "Resets Dec 9, 8:59am (Europe/Helsinki)", now: now)
|
|
299
298
|
var calendar = Calendar(identifier: .gregorian)
|
|
300
|
-
calendar.timeZone = TimeZone(identifier: "Europe/Helsinki")
|
|
299
|
+
calendar.timeZone = try #require(TimeZone(identifier: "Europe/Helsinki"))
|
|
301
300
|
let expected = calendar.date(from: DateComponents(
|
|
302
301
|
year: calendar.component(.year, from: now),
|
|
303
302
|
month: 12,
|
|
@@ -309,24 +308,24 @@ struct StatusProbeTests {
|
|
|
309
308
|
}
|
|
310
309
|
|
|
311
310
|
@Test
|
|
312
|
-
func
|
|
311
|
+
func `parses claude reset with dot separated time`() throws {
|
|
313
312
|
let now = Date(timeIntervalSince1970: 1_733_690_000)
|
|
314
313
|
let parsed = ClaudeStatusProbe.parseResetDate(from: "Resets Dec 9 at 5.27am (UTC)", now: now)
|
|
315
314
|
var calendar = Calendar(identifier: .gregorian)
|
|
316
|
-
calendar.timeZone = TimeZone(identifier: "UTC")
|
|
315
|
+
calendar.timeZone = try #require(TimeZone(identifier: "UTC"))
|
|
317
316
|
let expected = calendar.date(from: DateComponents(year: 2024, month: 12, day: 9, hour: 5, minute: 27))
|
|
318
317
|
#expect(parsed == expected)
|
|
319
318
|
}
|
|
320
319
|
|
|
321
320
|
@Test
|
|
322
|
-
func
|
|
321
|
+
func `parses claude reset with compact times`() throws {
|
|
323
322
|
let now = Date(timeIntervalSince1970: 1_733_690_000)
|
|
324
323
|
let parsedTimeOnly = ClaudeStatusProbe.parseResetDate(from: "Resets 1pm (UTC)", now: now)
|
|
325
324
|
var calendar = Calendar(identifier: .gregorian)
|
|
326
|
-
calendar.timeZone = TimeZone(identifier: "UTC")
|
|
327
|
-
var expected = calendar.date(bySettingHour: 13, minute: 0, second: 0, of: now)
|
|
325
|
+
calendar.timeZone = try #require(TimeZone(identifier: "UTC"))
|
|
326
|
+
var expected = try #require(calendar.date(bySettingHour: 13, minute: 0, second: 0, of: now))
|
|
328
327
|
if expected < now {
|
|
329
|
-
expected = calendar.date(byAdding: .day, value: 1, to: expected)
|
|
328
|
+
expected = try #require(calendar.date(byAdding: .day, value: 1, to: expected))
|
|
330
329
|
}
|
|
331
330
|
#expect(parsedTimeOnly == expected)
|
|
332
331
|
|
|
@@ -343,7 +342,7 @@ struct StatusProbeTests {
|
|
|
343
342
|
}
|
|
344
343
|
|
|
345
344
|
@Test
|
|
346
|
-
func
|
|
345
|
+
func `live codex status`() async throws {
|
|
347
346
|
guard ProcessInfo.processInfo.environment["LIVE_CODEX_STATUS"] == "1" else { return }
|
|
348
347
|
|
|
349
348
|
let probe = CodexStatusProbe()
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import Testing
|
|
3
|
-
|
|
4
3
|
@testable import RunicCore
|
|
5
4
|
|
|
6
|
-
@Suite
|
|
7
5
|
struct SubprocessRunnerTests {
|
|
8
6
|
@Test
|
|
9
|
-
func
|
|
7
|
+
func `reads large stdout without deadlock`() async throws {
|
|
10
8
|
let result = try await SubprocessRunner.run(
|
|
11
9
|
binary: "/usr/bin/python3",
|
|
12
10
|
arguments: ["-c", "print('x' * 1_000_000)"],
|
|
@@ -2,12 +2,11 @@ import Foundation
|
|
|
2
2
|
import Testing
|
|
3
3
|
@testable import Runic
|
|
4
4
|
|
|
5
|
-
@Suite
|
|
6
5
|
struct SubscriptionDetectionTests {
|
|
7
6
|
// MARK: - Subscription plans should be detected
|
|
8
7
|
|
|
9
8
|
@Test
|
|
10
|
-
func
|
|
9
|
+
func `detects max plan`() {
|
|
11
10
|
#expect(UsageStore.isSubscriptionPlan("Claude Max") == true)
|
|
12
11
|
#expect(UsageStore.isSubscriptionPlan("Max") == true)
|
|
13
12
|
#expect(UsageStore.isSubscriptionPlan("claude max") == true)
|
|
@@ -15,21 +14,21 @@ struct SubscriptionDetectionTests {
|
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
@Test
|
|
18
|
-
func
|
|
17
|
+
func `detects pro plan`() {
|
|
19
18
|
#expect(UsageStore.isSubscriptionPlan("Claude Pro") == true)
|
|
20
19
|
#expect(UsageStore.isSubscriptionPlan("Pro") == true)
|
|
21
20
|
#expect(UsageStore.isSubscriptionPlan("pro") == true)
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
@Test
|
|
25
|
-
func
|
|
24
|
+
func `detects ultra plan`() {
|
|
26
25
|
#expect(UsageStore.isSubscriptionPlan("Claude Ultra") == true)
|
|
27
26
|
#expect(UsageStore.isSubscriptionPlan("Ultra") == true)
|
|
28
27
|
#expect(UsageStore.isSubscriptionPlan("ultra") == true)
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
@Test
|
|
32
|
-
func
|
|
31
|
+
func `detects team plan`() {
|
|
33
32
|
#expect(UsageStore.isSubscriptionPlan("Claude Team") == true)
|
|
34
33
|
#expect(UsageStore.isSubscriptionPlan("Team") == true)
|
|
35
34
|
#expect(UsageStore.isSubscriptionPlan("team") == true)
|
|
@@ -38,18 +37,18 @@ struct SubscriptionDetectionTests {
|
|
|
38
37
|
// MARK: - Non-subscription plans should return false
|
|
39
38
|
|
|
40
39
|
@Test
|
|
41
|
-
func
|
|
40
|
+
func `nil login method returns false`() {
|
|
42
41
|
#expect(UsageStore.isSubscriptionPlan(nil) == false)
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
@Test
|
|
46
|
-
func
|
|
45
|
+
func `empty login method returns false`() {
|
|
47
46
|
#expect(UsageStore.isSubscriptionPlan("") == false)
|
|
48
47
|
#expect(UsageStore.isSubscriptionPlan(" ") == false)
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
@Test
|
|
52
|
-
func
|
|
51
|
+
func `unknown plan returns false`() {
|
|
53
52
|
#expect(UsageStore.isSubscriptionPlan("API") == false)
|
|
54
53
|
#expect(UsageStore.isSubscriptionPlan("Free") == false)
|
|
55
54
|
#expect(UsageStore.isSubscriptionPlan("Unknown") == false)
|
|
@@ -57,7 +56,7 @@ struct SubscriptionDetectionTests {
|
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
@Test
|
|
60
|
-
func
|
|
59
|
+
func `api key users return false`() {
|
|
61
60
|
// API users typically don't have a login method or have non-subscription identifiers
|
|
62
61
|
#expect(UsageStore.isSubscriptionPlan("api_key") == false)
|
|
63
62
|
#expect(UsageStore.isSubscriptionPlan("console") == false)
|
|
@@ -2,10 +2,9 @@ import Foundation
|
|
|
2
2
|
import Testing
|
|
3
3
|
@testable import RunicCore
|
|
4
4
|
|
|
5
|
-
@Suite
|
|
6
5
|
struct TTYCommandRunnerEnvTests {
|
|
7
6
|
@Test
|
|
8
|
-
func
|
|
7
|
+
func `preserves environment and sets term`() {
|
|
9
8
|
let baseEnv: [String: String] = [
|
|
10
9
|
"PATH": "/custom/bin",
|
|
11
10
|
"HOME": "/Users/tester",
|
|
@@ -25,7 +24,7 @@ struct TTYCommandRunnerEnvTests {
|
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
@Test
|
|
28
|
-
func
|
|
27
|
+
func `backfills home when missing`() {
|
|
29
28
|
let merged = TTYCommandRunner.enrichedEnvironment(
|
|
30
29
|
baseEnv: ["PATH": "/custom/bin"],
|
|
31
30
|
loginPATH: nil,
|
|
@@ -35,7 +34,7 @@ struct TTYCommandRunnerEnvTests {
|
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
@Test
|
|
38
|
-
func
|
|
37
|
+
func `preserves existing term and custom vars`() {
|
|
39
38
|
let merged = TTYCommandRunner.enrichedEnvironment(
|
|
40
39
|
baseEnv: [
|
|
41
40
|
"PATH": "/custom/bin",
|
|
@@ -53,7 +52,7 @@ struct TTYCommandRunnerEnvTests {
|
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
@Test
|
|
56
|
-
func
|
|
55
|
+
func `sets working directory when provided`() throws {
|
|
57
56
|
let fm = FileManager.default
|
|
58
57
|
let dir = fm.temporaryDirectory.appendingPathComponent("runic-tty-\(UUID().uuidString)", isDirectory: true)
|
|
59
58
|
try fm.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
@@ -65,7 +64,7 @@ struct TTYCommandRunnerEnvTests {
|
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
@Test
|
|
68
|
-
func
|
|
67
|
+
func `auto responds to trust prompt`() throws {
|
|
69
68
|
let fm = FileManager.default
|
|
70
69
|
let dir = fm.temporaryDirectory.appendingPathComponent("runic-tty-\(UUID().uuidString)", isDirectory: true)
|
|
71
70
|
try fm.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
@@ -102,7 +101,7 @@ struct TTYCommandRunnerEnvTests {
|
|
|
102
101
|
}
|
|
103
102
|
|
|
104
103
|
@Test
|
|
105
|
-
func
|
|
104
|
+
func `stops when output is idle`() throws {
|
|
106
105
|
let fm = FileManager.default
|
|
107
106
|
let dir = fm.temporaryDirectory.appendingPathComponent("runic-tty-\(UUID().uuidString)", isDirectory: true)
|
|
108
107
|
try fm.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
@@ -130,7 +129,7 @@ struct TTYCommandRunnerEnvTests {
|
|
|
130
129
|
}
|
|
131
130
|
|
|
132
131
|
@Test
|
|
133
|
-
func
|
|
132
|
+
func `rolling buffer detects needle across boundary`() {
|
|
134
133
|
var scanner = TTYCommandRunner.RollingBuffer(maxNeedle: 6)
|
|
135
134
|
let needle = Data("hello".utf8)
|
|
136
135
|
let first = scanner.append(Data("he".utf8))
|
|
@@ -140,7 +139,7 @@ struct TTYCommandRunnerEnvTests {
|
|
|
140
139
|
}
|
|
141
140
|
|
|
142
141
|
@Test
|
|
143
|
-
func
|
|
142
|
+
func `lowercased ASCII only touches ascii`() {
|
|
144
143
|
let data = Data("UpDaTe".utf8)
|
|
145
144
|
let lowered = TTYCommandRunner.lowercasedASCII(data)
|
|
146
145
|
#expect(String(data: lowered, encoding: .utf8) == "update")
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import Testing
|
|
3
|
-
|
|
4
3
|
@testable import RunicCore
|
|
5
4
|
|
|
6
|
-
@Suite
|
|
7
5
|
struct TeamShowbackExporterTests {
|
|
8
6
|
@Test
|
|
9
|
-
func
|
|
7
|
+
func `builds showback report for team owned projects`() throws {
|
|
10
8
|
let period = DateInterval(
|
|
11
9
|
start: self.date(year: 2026, month: 2, day: 1),
|
|
12
10
|
end: self.date(year: 2026, month: 3, day: 1))
|
|
@@ -81,7 +79,7 @@ struct TeamShowbackExporterTests {
|
|
|
81
79
|
}
|
|
82
80
|
|
|
83
81
|
@Test
|
|
84
|
-
func
|
|
82
|
+
func `exports json and csv from report`() throws {
|
|
85
83
|
let period = DateInterval(
|
|
86
84
|
start: self.date(year: 2026, month: 2, day: 1),
|
|
87
85
|
end: self.date(year: 2026, month: 3, day: 1))
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import RunicCore
|
|
2
2
|
import Testing
|
|
3
3
|
|
|
4
|
-
@Suite
|
|
5
4
|
struct TextParsingTests {
|
|
6
5
|
@Test
|
|
7
|
-
func
|
|
6
|
+
func `strip ANSI codes removes cursor visibility CSI`() {
|
|
8
7
|
let input = "\u{001B}[?25hhello\u{001B}[0m"
|
|
9
8
|
let stripped = TextParsing.stripANSICodes(input)
|
|
10
9
|
#expect(stripped == "hello")
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import RunicCore
|
|
2
1
|
import Foundation
|
|
2
|
+
import RunicCore
|
|
3
3
|
import Testing
|
|
4
4
|
@testable import Runic
|
|
5
5
|
|
|
6
|
-
@Suite
|
|
7
6
|
struct UsageFormatterTests {
|
|
8
7
|
@Test
|
|
9
|
-
func
|
|
8
|
+
func `formats usage line`() {
|
|
10
9
|
let line = UsageFormatter.usageLine(remaining: 25, used: 75)
|
|
11
10
|
#expect(line == "25% left")
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
@Test
|
|
15
|
-
func
|
|
14
|
+
func `relative updated recent`() {
|
|
16
15
|
let now = Date()
|
|
17
16
|
let fiveHoursAgo = now.addingTimeInterval(-5 * 3600)
|
|
18
17
|
let text = UsageFormatter.updatedString(from: fiveHoursAgo, now: now)
|
|
@@ -22,7 +21,7 @@ struct UsageFormatterTests {
|
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
@Test
|
|
25
|
-
func
|
|
24
|
+
func `absolute updated old`() {
|
|
26
25
|
let now = Date()
|
|
27
26
|
let dayAgo = now.addingTimeInterval(-26 * 3600)
|
|
28
27
|
let text = UsageFormatter.updatedString(from: dayAgo, now: now)
|
|
@@ -31,42 +30,42 @@ struct UsageFormatterTests {
|
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
@Test
|
|
34
|
-
func
|
|
33
|
+
func `reset countdown minutes`() {
|
|
35
34
|
let now = Date(timeIntervalSince1970: 1_000_000)
|
|
36
35
|
let reset = now.addingTimeInterval(10 * 60 + 1)
|
|
37
36
|
#expect(UsageFormatter.resetCountdownDescription(from: reset, now: now) == "in 11m")
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
@Test
|
|
41
|
-
func
|
|
40
|
+
func `reset countdown hours and minutes`() {
|
|
42
41
|
let now = Date(timeIntervalSince1970: 1_000_000)
|
|
43
42
|
let reset = now.addingTimeInterval(3 * 3600 + 31 * 60)
|
|
44
43
|
#expect(UsageFormatter.resetCountdownDescription(from: reset, now: now) == "in 3h 31m")
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
@Test
|
|
48
|
-
func
|
|
47
|
+
func `reset countdown days and hours`() {
|
|
49
48
|
let now = Date(timeIntervalSince1970: 1_000_000)
|
|
50
49
|
let reset = now.addingTimeInterval((26 * 3600) + 10)
|
|
51
50
|
#expect(UsageFormatter.resetCountdownDescription(from: reset, now: now) == "in 1d 2h")
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
@Test
|
|
55
|
-
func
|
|
54
|
+
func `reset countdown exact hour`() {
|
|
56
55
|
let now = Date(timeIntervalSince1970: 1_000_000)
|
|
57
56
|
let reset = now.addingTimeInterval(60 * 60)
|
|
58
57
|
#expect(UsageFormatter.resetCountdownDescription(from: reset, now: now) == "in 1h")
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
@Test
|
|
62
|
-
func
|
|
61
|
+
func `reset countdown past date`() {
|
|
63
62
|
let now = Date(timeIntervalSince1970: 1_000_000)
|
|
64
63
|
let reset = now.addingTimeInterval(-10)
|
|
65
64
|
#expect(UsageFormatter.resetCountdownDescription(from: reset, now: now) == "now")
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
@Test
|
|
69
|
-
func
|
|
68
|
+
func `model display name strips trailing dates`() {
|
|
70
69
|
#expect(UsageFormatter.modelDisplayName("claude-opus-4-5-20251101") == "claude-opus-4-5")
|
|
71
70
|
#expect(UsageFormatter.modelDisplayName("gpt-4o-2024-08-06") == "gpt-4o")
|
|
72
71
|
#expect(UsageFormatter.modelDisplayName("Claude Opus 4.5 2025 1101") == "Claude Opus 4.5")
|
|
@@ -74,12 +73,12 @@ struct UsageFormatterTests {
|
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
@Test
|
|
77
|
-
func
|
|
76
|
+
func `clean plan maps O auth to ollama`() {
|
|
78
77
|
#expect(UsageFormatter.cleanPlanName("oauth") == "Ollama")
|
|
79
78
|
}
|
|
80
79
|
|
|
81
80
|
@Test
|
|
82
|
-
func
|
|
81
|
+
func `model context from known model`() {
|
|
83
82
|
#expect(UsageFormatter.modelContextLabel(for: "gpt-4o") == "ctx 128K")
|
|
84
83
|
#expect(UsageFormatter.modelContextLabel(for: "claude-opus-4-5") == "ctx 200K")
|
|
85
84
|
#expect(UsageFormatter.modelContextLabel(for: "gpt-5") == "ctx 400K")
|
|
@@ -89,18 +88,18 @@ struct UsageFormatterTests {
|
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
@Test
|
|
92
|
-
func
|
|
91
|
+
func `model context from name suffix`() {
|
|
93
92
|
#expect(UsageFormatter.modelContextWindow(for: "qwen2.5-128k-instruct") == 128_000)
|
|
94
93
|
#expect(UsageFormatter.modelContextLabel(for: "provider/qwen2.5-2m-long-context") == "ctx 2M")
|
|
95
94
|
}
|
|
96
95
|
|
|
97
96
|
@Test
|
|
98
|
-
func
|
|
97
|
+
func `model context unknown returns nil`() {
|
|
99
98
|
#expect(UsageFormatter.modelContextWindow(for: "unknown-custom-model-v1") == nil)
|
|
100
99
|
}
|
|
101
100
|
|
|
102
101
|
@Test
|
|
103
|
-
func
|
|
102
|
+
func `token summary includes breakdown`() {
|
|
104
103
|
let totals = UsageLedgerTotals(
|
|
105
104
|
inputTokens: 1000,
|
|
106
105
|
outputTokens: 200,
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import RunicCore
|
|
3
3
|
import Testing
|
|
4
|
-
|
|
5
4
|
@testable import Runic
|
|
6
5
|
|
|
7
|
-
@Suite
|
|
8
6
|
struct UsageLedgerAnomalyDetectionTests {
|
|
9
7
|
@Test
|
|
10
|
-
func
|
|
8
|
+
func `detects token spike against seven day baseline`() {
|
|
11
9
|
let now = Date(timeIntervalSince1970: 1_771_718_400) // 2026-02-22T00:00:00Z
|
|
12
10
|
var calendar = Calendar(identifier: .gregorian)
|
|
13
11
|
calendar.timeZone = TimeZone.current
|
|
@@ -17,7 +15,7 @@ struct UsageLedgerAnomalyDetectionTests {
|
|
|
17
15
|
summaries.append(self.dailySummary(
|
|
18
16
|
provider: .codex,
|
|
19
17
|
dayOffset: offset,
|
|
20
|
-
tokens:
|
|
18
|
+
tokens: 1000,
|
|
21
19
|
cost: nil,
|
|
22
20
|
now: now,
|
|
23
21
|
calendar: calendar))
|
|
@@ -25,7 +23,7 @@ struct UsageLedgerAnomalyDetectionTests {
|
|
|
25
23
|
summaries.append(self.dailySummary(
|
|
26
24
|
provider: .codex,
|
|
27
25
|
dayOffset: 0,
|
|
28
|
-
tokens:
|
|
26
|
+
tokens: 2500,
|
|
29
27
|
cost: nil,
|
|
30
28
|
now: now,
|
|
31
29
|
calendar: calendar))
|
|
@@ -43,7 +41,7 @@ struct UsageLedgerAnomalyDetectionTests {
|
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
@Test
|
|
46
|
-
func
|
|
44
|
+
func `detects spend spike when cost data exists`() {
|
|
47
45
|
let now = Date(timeIntervalSince1970: 1_771_718_400) // 2026-02-22T00:00:00Z
|
|
48
46
|
var calendar = Calendar(identifier: .gregorian)
|
|
49
47
|
calendar.timeZone = TimeZone.current
|
|
@@ -53,7 +51,7 @@ struct UsageLedgerAnomalyDetectionTests {
|
|
|
53
51
|
summaries.append(self.dailySummary(
|
|
54
52
|
provider: .claude,
|
|
55
53
|
dayOffset: offset,
|
|
56
|
-
tokens:
|
|
54
|
+
tokens: 1000,
|
|
57
55
|
cost: 2.0,
|
|
58
56
|
now: now,
|
|
59
57
|
calendar: calendar))
|
|
@@ -61,7 +59,7 @@ struct UsageLedgerAnomalyDetectionTests {
|
|
|
61
59
|
summaries.append(self.dailySummary(
|
|
62
60
|
provider: .claude,
|
|
63
61
|
dayOffset: 0,
|
|
64
|
-
tokens:
|
|
62
|
+
tokens: 1100,
|
|
65
63
|
cost: 7.0,
|
|
66
64
|
now: now,
|
|
67
65
|
calendar: calendar))
|
|
@@ -79,7 +77,7 @@ struct UsageLedgerAnomalyDetectionTests {
|
|
|
79
77
|
}
|
|
80
78
|
|
|
81
79
|
@Test
|
|
82
|
-
func
|
|
80
|
+
func `does not emit anomaly when history has fewer than seven days`() {
|
|
83
81
|
let now = Date(timeIntervalSince1970: 1_771_718_400) // 2026-02-22T00:00:00Z
|
|
84
82
|
var calendar = Calendar(identifier: .gregorian)
|
|
85
83
|
calendar.timeZone = TimeZone.current
|
|
@@ -97,7 +95,7 @@ struct UsageLedgerAnomalyDetectionTests {
|
|
|
97
95
|
summaries.append(self.dailySummary(
|
|
98
96
|
provider: .codex,
|
|
99
97
|
dayOffset: 0,
|
|
100
|
-
tokens:
|
|
98
|
+
tokens: 4000,
|
|
101
99
|
cost: 8.0,
|
|
102
100
|
now: now,
|
|
103
101
|
calendar: calendar))
|
|
@@ -112,15 +110,15 @@ struct UsageLedgerAnomalyDetectionTests {
|
|
|
112
110
|
}
|
|
113
111
|
|
|
114
112
|
@Test
|
|
115
|
-
func
|
|
113
|
+
func `explanation includes headline and contributing factors`() {
|
|
116
114
|
let summary = UsageLedgerAnomalySummary(
|
|
117
115
|
provider: .codex,
|
|
118
116
|
baselineDays: 7,
|
|
119
117
|
tokenAnomaly: UsageLedgerAnomalySummary.MetricAnomaly(
|
|
120
118
|
metric: .tokens,
|
|
121
119
|
severity: .high,
|
|
122
|
-
todayValue:
|
|
123
|
-
baselineAverage:
|
|
120
|
+
todayValue: 4200,
|
|
121
|
+
baselineAverage: 1500,
|
|
124
122
|
percentIncrease: 1.8),
|
|
125
123
|
spendAnomaly: UsageLedgerAnomalySummary.MetricAnomaly(
|
|
126
124
|
metric: .spend,
|
|
@@ -137,7 +135,7 @@ struct UsageLedgerAnomalyDetectionTests {
|
|
|
137
135
|
}
|
|
138
136
|
|
|
139
137
|
@Test
|
|
140
|
-
func
|
|
138
|
+
func `explanation is nil when no anomalies exist`() {
|
|
141
139
|
let summary = UsageLedgerAnomalySummary(
|
|
142
140
|
provider: .codex,
|
|
143
141
|
baselineDays: 7,
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import Testing
|
|
3
|
-
|
|
4
3
|
@testable import RunicCore
|
|
5
4
|
|
|
6
|
-
@Suite
|
|
7
5
|
struct UsageLedgerForecastTests {
|
|
8
6
|
private let utc = TimeZone(secondsFromGMT: 0)!
|
|
9
7
|
|
|
10
8
|
@Test
|
|
11
|
-
func
|
|
9
|
+
func `provider forecast uses observed days in current month`() {
|
|
12
10
|
let now = self.date(year: 2026, month: 2, day: 20)
|
|
13
11
|
let entries = [
|
|
14
12
|
self.entry(
|
|
@@ -50,7 +48,7 @@ struct UsageLedgerForecastTests {
|
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
@Test
|
|
53
|
-
func
|
|
51
|
+
func `project forecasts are computed per project`() {
|
|
54
52
|
let now = self.date(year: 2026, month: 2, day: 20)
|
|
55
53
|
let entries = [
|
|
56
54
|
self.entry(
|
|
@@ -91,7 +89,7 @@ struct UsageLedgerForecastTests {
|
|
|
91
89
|
}
|
|
92
90
|
|
|
93
91
|
@Test
|
|
94
|
-
func
|
|
92
|
+
func `provider forecast includes confidence band quantiles when enough daily history exists`() {
|
|
95
93
|
let now = self.date(year: 2026, month: 2, day: 20)
|
|
96
94
|
let entries = [
|
|
97
95
|
self.entry(
|
|
@@ -142,7 +140,7 @@ struct UsageLedgerForecastTests {
|
|
|
142
140
|
}
|
|
143
141
|
|
|
144
142
|
@Test
|
|
145
|
-
func
|
|
143
|
+
func `budget ETA is computed only when forecast breaches budget`() {
|
|
146
144
|
let base = UsageLedgerSpendForecast(
|
|
147
145
|
provider: .codex,
|
|
148
146
|
projectKey: "id:proj-a",
|