@build-astron-co/nimbus 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +628 -0
- package/bin/nimbus +38 -0
- package/package.json +80 -0
- package/src/__tests__/app.test.ts +76 -0
- package/src/__tests__/audit.test.ts +877 -0
- package/src/__tests__/circuit-breaker.test.ts +116 -0
- package/src/__tests__/cli-run.test.ts +115 -0
- package/src/__tests__/context-manager.test.ts +502 -0
- package/src/__tests__/context.test.ts +242 -0
- package/src/__tests__/enterprise.test.ts +401 -0
- package/src/__tests__/generator.test.ts +433 -0
- package/src/__tests__/hooks.test.ts +582 -0
- package/src/__tests__/init.test.ts +436 -0
- package/src/__tests__/intent-parser.test.ts +229 -0
- package/src/__tests__/llm-router.test.ts +209 -0
- package/src/__tests__/lsp.test.ts +293 -0
- package/src/__tests__/modes.test.ts +336 -0
- package/src/__tests__/permissions.test.ts +338 -0
- package/src/__tests__/serve.test.ts +275 -0
- package/src/__tests__/sessions.test.ts +227 -0
- package/src/__tests__/sharing.test.ts +288 -0
- package/src/__tests__/snapshots.test.ts +581 -0
- package/src/__tests__/state-db.test.ts +334 -0
- package/src/__tests__/stream-with-tools.test.ts +732 -0
- package/src/__tests__/subagents.test.ts +176 -0
- package/src/__tests__/system-prompt.test.ts +169 -0
- package/src/__tests__/tool-converter.test.ts +256 -0
- package/src/__tests__/tool-schemas.test.ts +397 -0
- package/src/__tests__/tools.test.ts +143 -0
- package/src/__tests__/version.test.ts +49 -0
- package/src/agent/compaction-agent.ts +227 -0
- package/src/agent/context-manager.ts +435 -0
- package/src/agent/context.ts +427 -0
- package/src/agent/deploy-preview.ts +426 -0
- package/src/agent/index.ts +68 -0
- package/src/agent/loop.ts +717 -0
- package/src/agent/modes.ts +429 -0
- package/src/agent/permissions.ts +466 -0
- package/src/agent/subagents/base.ts +116 -0
- package/src/agent/subagents/cost.ts +51 -0
- package/src/agent/subagents/explore.ts +42 -0
- package/src/agent/subagents/general.ts +54 -0
- package/src/agent/subagents/index.ts +102 -0
- package/src/agent/subagents/infra.ts +59 -0
- package/src/agent/subagents/security.ts +69 -0
- package/src/agent/system-prompt.ts +436 -0
- package/src/app.ts +122 -0
- package/src/audit/activity-log.ts +290 -0
- package/src/audit/compliance-checker.ts +540 -0
- package/src/audit/cost-tracker.ts +318 -0
- package/src/audit/index.ts +23 -0
- package/src/audit/security-scanner.ts +596 -0
- package/src/auth/guard.ts +75 -0
- package/src/auth/index.ts +56 -0
- package/src/auth/oauth.ts +455 -0
- package/src/auth/providers.ts +470 -0
- package/src/auth/sso.ts +113 -0
- package/src/auth/store.ts +505 -0
- package/src/auth/types.ts +187 -0
- package/src/build.ts +141 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/init.ts +854 -0
- package/src/cli/openapi-spec.ts +356 -0
- package/src/cli/run.ts +237 -0
- package/src/cli/serve-auth.ts +80 -0
- package/src/cli/serve.ts +462 -0
- package/src/cli/web.ts +67 -0
- package/src/cli.ts +1417 -0
- package/src/clients/core-engine-client.ts +227 -0
- package/src/clients/enterprise-client.ts +334 -0
- package/src/clients/generator-client.ts +351 -0
- package/src/clients/git-client.ts +627 -0
- package/src/clients/github-client.ts +410 -0
- package/src/clients/helm-client.ts +504 -0
- package/src/clients/index.ts +80 -0
- package/src/clients/k8s-client.ts +497 -0
- package/src/clients/llm-client.ts +161 -0
- package/src/clients/rest-client.ts +130 -0
- package/src/clients/service-discovery.ts +33 -0
- package/src/clients/terraform-client.ts +482 -0
- package/src/clients/tools-client.ts +1843 -0
- package/src/clients/ws-client.ts +115 -0
- package/src/commands/analyze/index.ts +352 -0
- package/src/commands/apply/helm.ts +473 -0
- package/src/commands/apply/index.ts +213 -0
- package/src/commands/apply/k8s.ts +454 -0
- package/src/commands/apply/terraform.ts +582 -0
- package/src/commands/ask.ts +167 -0
- package/src/commands/audit/index.ts +238 -0
- package/src/commands/auth-cloud.ts +294 -0
- package/src/commands/auth-list.ts +134 -0
- package/src/commands/auth-profile.ts +121 -0
- package/src/commands/auth-status.ts +141 -0
- package/src/commands/aws/ec2.ts +501 -0
- package/src/commands/aws/iam.ts +397 -0
- package/src/commands/aws/index.ts +133 -0
- package/src/commands/aws/lambda.ts +396 -0
- package/src/commands/aws/rds.ts +439 -0
- package/src/commands/aws/s3.ts +439 -0
- package/src/commands/aws/vpc.ts +393 -0
- package/src/commands/aws-discover.ts +649 -0
- package/src/commands/aws-terraform.ts +805 -0
- package/src/commands/azure/aks.ts +376 -0
- package/src/commands/azure/functions.ts +253 -0
- package/src/commands/azure/index.ts +116 -0
- package/src/commands/azure/storage.ts +478 -0
- package/src/commands/azure/vm.ts +355 -0
- package/src/commands/billing/index.ts +256 -0
- package/src/commands/chat.ts +314 -0
- package/src/commands/config.ts +346 -0
- package/src/commands/cost/cloud-cost-estimator.ts +266 -0
- package/src/commands/cost/estimator.ts +79 -0
- package/src/commands/cost/index.ts +594 -0
- package/src/commands/cost/parsers/terraform.ts +273 -0
- package/src/commands/cost/parsers/types.ts +25 -0
- package/src/commands/cost/pricing/aws.ts +544 -0
- package/src/commands/cost/pricing/azure.ts +499 -0
- package/src/commands/cost/pricing/gcp.ts +396 -0
- package/src/commands/cost/pricing/index.ts +40 -0
- package/src/commands/demo.ts +250 -0
- package/src/commands/doctor.ts +794 -0
- package/src/commands/drift/index.ts +439 -0
- package/src/commands/explain.ts +277 -0
- package/src/commands/feedback.ts +389 -0
- package/src/commands/fix.ts +324 -0
- package/src/commands/fs/index.ts +402 -0
- package/src/commands/gcp/compute.ts +325 -0
- package/src/commands/gcp/functions.ts +271 -0
- package/src/commands/gcp/gke.ts +438 -0
- package/src/commands/gcp/iam.ts +344 -0
- package/src/commands/gcp/index.ts +129 -0
- package/src/commands/gcp/storage.ts +284 -0
- package/src/commands/generate-helm.ts +1249 -0
- package/src/commands/generate-k8s.ts +1560 -0
- package/src/commands/generate-terraform.ts +1460 -0
- package/src/commands/gh/index.ts +863 -0
- package/src/commands/git/index.ts +1343 -0
- package/src/commands/helm/index.ts +1126 -0
- package/src/commands/help.ts +539 -0
- package/src/commands/history.ts +142 -0
- package/src/commands/import.ts +868 -0
- package/src/commands/index.ts +367 -0
- package/src/commands/init.ts +1046 -0
- package/src/commands/k8s/index.ts +1137 -0
- package/src/commands/login.ts +631 -0
- package/src/commands/logout.ts +83 -0
- package/src/commands/onboarding.ts +228 -0
- package/src/commands/plan/display.ts +279 -0
- package/src/commands/plan/index.ts +599 -0
- package/src/commands/preview.ts +452 -0
- package/src/commands/questionnaire.ts +1270 -0
- package/src/commands/resume.ts +55 -0
- package/src/commands/team/index.ts +346 -0
- package/src/commands/template.ts +232 -0
- package/src/commands/tf/index.ts +1034 -0
- package/src/commands/upgrade.ts +550 -0
- package/src/commands/usage/index.ts +134 -0
- package/src/commands/version.ts +170 -0
- package/src/compat/index.ts +2 -0
- package/src/compat/runtime.ts +12 -0
- package/src/compat/sqlite.ts +107 -0
- package/src/config/index.ts +17 -0
- package/src/config/manager.ts +530 -0
- package/src/config/safety-policy.ts +358 -0
- package/src/config/schema.ts +125 -0
- package/src/config/types.ts +527 -0
- package/src/context/context-db.ts +199 -0
- package/src/demo/index.ts +349 -0
- package/src/demo/scenarios/full-journey.ts +229 -0
- package/src/demo/scenarios/getting-started.ts +127 -0
- package/src/demo/scenarios/helm-release.ts +341 -0
- package/src/demo/scenarios/k8s-deployment.ts +194 -0
- package/src/demo/scenarios/terraform-vpc.ts +170 -0
- package/src/demo/types.ts +92 -0
- package/src/engine/cost-estimator.ts +438 -0
- package/src/engine/diagram-generator.ts +256 -0
- package/src/engine/drift-detector.ts +902 -0
- package/src/engine/executor.ts +1035 -0
- package/src/engine/index.ts +76 -0
- package/src/engine/orchestrator.ts +636 -0
- package/src/engine/planner.ts +720 -0
- package/src/engine/safety.ts +743 -0
- package/src/engine/verifier.ts +770 -0
- package/src/enterprise/audit.ts +348 -0
- package/src/enterprise/auth.ts +270 -0
- package/src/enterprise/billing.ts +822 -0
- package/src/enterprise/index.ts +17 -0
- package/src/enterprise/teams.ts +443 -0
- package/src/generator/best-practices.ts +1608 -0
- package/src/generator/helm.ts +630 -0
- package/src/generator/index.ts +37 -0
- package/src/generator/intent-parser.ts +514 -0
- package/src/generator/kubernetes.ts +976 -0
- package/src/generator/terraform.ts +1867 -0
- package/src/history/index.ts +8 -0
- package/src/history/manager.ts +322 -0
- package/src/history/types.ts +34 -0
- package/src/hooks/config.ts +432 -0
- package/src/hooks/engine.ts +391 -0
- package/src/hooks/index.ts +4 -0
- package/src/llm/auth-bridge.ts +198 -0
- package/src/llm/circuit-breaker.ts +140 -0
- package/src/llm/config-loader.ts +201 -0
- package/src/llm/cost-calculator.ts +171 -0
- package/src/llm/index.ts +8 -0
- package/src/llm/model-aliases.ts +115 -0
- package/src/llm/provider-registry.ts +63 -0
- package/src/llm/providers/anthropic.ts +433 -0
- package/src/llm/providers/bedrock.ts +477 -0
- package/src/llm/providers/google.ts +405 -0
- package/src/llm/providers/ollama.ts +767 -0
- package/src/llm/providers/openai-compatible.ts +340 -0
- package/src/llm/providers/openai.ts +328 -0
- package/src/llm/providers/openrouter.ts +338 -0
- package/src/llm/router.ts +1035 -0
- package/src/llm/types.ts +232 -0
- package/src/lsp/client.ts +298 -0
- package/src/lsp/languages.ts +116 -0
- package/src/lsp/manager.ts +278 -0
- package/src/mcp/client.ts +402 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/manager.ts +133 -0
- package/src/nimbus.ts +214 -0
- package/src/plugins/index.ts +27 -0
- package/src/plugins/loader.ts +334 -0
- package/src/plugins/manager.ts +376 -0
- package/src/plugins/types.ts +284 -0
- package/src/scanners/cicd-scanner.ts +258 -0
- package/src/scanners/cloud-scanner.ts +466 -0
- package/src/scanners/framework-scanner.ts +469 -0
- package/src/scanners/iac-scanner.ts +388 -0
- package/src/scanners/index.ts +539 -0
- package/src/scanners/language-scanner.ts +276 -0
- package/src/scanners/package-manager-scanner.ts +277 -0
- package/src/scanners/types.ts +172 -0
- package/src/sessions/manager.ts +365 -0
- package/src/sessions/types.ts +44 -0
- package/src/sharing/sync.ts +296 -0
- package/src/sharing/viewer.ts +97 -0
- package/src/snapshots/index.ts +2 -0
- package/src/snapshots/manager.ts +530 -0
- package/src/state/artifacts.ts +147 -0
- package/src/state/audit.ts +137 -0
- package/src/state/billing.ts +240 -0
- package/src/state/checkpoints.ts +117 -0
- package/src/state/config.ts +67 -0
- package/src/state/conversations.ts +14 -0
- package/src/state/credentials.ts +154 -0
- package/src/state/db.ts +58 -0
- package/src/state/index.ts +26 -0
- package/src/state/messages.ts +115 -0
- package/src/state/projects.ts +123 -0
- package/src/state/schema.ts +236 -0
- package/src/state/sessions.ts +147 -0
- package/src/state/teams.ts +200 -0
- package/src/telemetry.ts +108 -0
- package/src/tools/aws-ops.ts +952 -0
- package/src/tools/azure-ops.ts +579 -0
- package/src/tools/file-ops.ts +593 -0
- package/src/tools/gcp-ops.ts +625 -0
- package/src/tools/git-ops.ts +773 -0
- package/src/tools/github-ops.ts +799 -0
- package/src/tools/helm-ops.ts +943 -0
- package/src/tools/index.ts +17 -0
- package/src/tools/k8s-ops.ts +819 -0
- package/src/tools/schemas/converter.ts +184 -0
- package/src/tools/schemas/devops.ts +612 -0
- package/src/tools/schemas/index.ts +73 -0
- package/src/tools/schemas/standard.ts +1144 -0
- package/src/tools/schemas/types.ts +705 -0
- package/src/tools/terraform-ops.ts +862 -0
- package/src/types/ambient.d.ts +193 -0
- package/src/types/config.ts +83 -0
- package/src/types/drift.ts +116 -0
- package/src/types/enterprise.ts +335 -0
- package/src/types/index.ts +20 -0
- package/src/types/plan.ts +44 -0
- package/src/types/request.ts +65 -0
- package/src/types/response.ts +54 -0
- package/src/types/service.ts +51 -0
- package/src/ui/App.tsx +997 -0
- package/src/ui/DeployPreview.tsx +169 -0
- package/src/ui/Header.tsx +68 -0
- package/src/ui/InputBox.tsx +350 -0
- package/src/ui/MessageList.tsx +585 -0
- package/src/ui/PermissionPrompt.tsx +151 -0
- package/src/ui/StatusBar.tsx +158 -0
- package/src/ui/ToolCallDisplay.tsx +409 -0
- package/src/ui/chat-ui.ts +853 -0
- package/src/ui/index.ts +33 -0
- package/src/ui/ink/index.ts +711 -0
- package/src/ui/streaming.ts +176 -0
- package/src/ui/types.ts +57 -0
- package/src/utils/analytics.ts +72 -0
- package/src/utils/cost-warning.ts +27 -0
- package/src/utils/env.ts +46 -0
- package/src/utils/errors.ts +69 -0
- package/src/utils/event-bus.ts +38 -0
- package/src/utils/index.ts +24 -0
- package/src/utils/logger.ts +171 -0
- package/src/utils/rate-limiter.ts +121 -0
- package/src/utils/service-auth.ts +49 -0
- package/src/utils/validation.ts +53 -0
- package/src/version.ts +4 -0
- package/src/watcher/index.ts +163 -0
- package/src/wizard/approval.ts +383 -0
- package/src/wizard/index.ts +25 -0
- package/src/wizard/prompts.ts +338 -0
- package/src/wizard/types.ts +171 -0
- package/src/wizard/ui.ts +556 -0
- package/src/wizard/wizard.ts +304 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the LLM router support modules:
|
|
3
|
+
* - src/llm/model-aliases.ts – resolveModelAlias, getAliases
|
|
4
|
+
* - src/llm/provider-registry.ts – detectProvider
|
|
5
|
+
* - src/llm/cost-calculator.ts – calculateCost, getPricingData
|
|
6
|
+
*
|
|
7
|
+
* The LLMRouter constructor itself requires a fully populated config with valid
|
|
8
|
+
* API keys and is therefore NOT exercised here. All tested functions are pure
|
|
9
|
+
* (no I/O, no network) and execute synchronously.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect } from 'bun:test';
|
|
13
|
+
import { resolveModelAlias, getAliases } from '../llm/model-aliases';
|
|
14
|
+
import { detectProvider } from '../llm/provider-registry';
|
|
15
|
+
import { calculateCost, getPricingData, type CostResult } from '../llm/cost-calculator';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// resolveModelAlias
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
describe('resolveModelAlias', () => {
|
|
22
|
+
it('resolves "sonnet" alias to the full Claude Sonnet model ID', () => {
|
|
23
|
+
const resolved = resolveModelAlias('sonnet');
|
|
24
|
+
expect(resolved).toBe('claude-sonnet-4-20250514');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('resolves "haiku" alias to the full Claude Haiku model ID', () => {
|
|
28
|
+
const resolved = resolveModelAlias('haiku');
|
|
29
|
+
expect(resolved).toBe('claude-haiku-4-20250514');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('resolves "opus" alias to the full Claude Opus model ID', () => {
|
|
33
|
+
expect(resolveModelAlias('opus')).toBe('claude-opus-4-20250514');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('resolves "gpt4o" alias to "gpt-4o"', () => {
|
|
37
|
+
expect(resolveModelAlias('gpt4o')).toBe('gpt-4o');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('resolves "gemini" alias to the Gemini Flash model ID', () => {
|
|
41
|
+
expect(resolveModelAlias('gemini')).toBe('gemini-2.0-flash-exp');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns the original string when no alias match is found', () => {
|
|
45
|
+
const unknown = 'some-unknown-model-id';
|
|
46
|
+
expect(resolveModelAlias(unknown)).toBe(unknown);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('resolves aliases case-insensitively', () => {
|
|
50
|
+
expect(resolveModelAlias('SONNET')).toBe('claude-sonnet-4-20250514');
|
|
51
|
+
expect(resolveModelAlias('Haiku')).toBe('claude-haiku-4-20250514');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('resolves "claude" alias to the default Claude model', () => {
|
|
55
|
+
expect(resolveModelAlias('claude')).toBe('claude-sonnet-4-20250514');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// getAliases
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
describe('getAliases', () => {
|
|
64
|
+
it('returns an object', () => {
|
|
65
|
+
const aliases = getAliases();
|
|
66
|
+
expect(typeof aliases).toBe('object');
|
|
67
|
+
expect(aliases).not.toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('includes the expected shortcut keys', () => {
|
|
71
|
+
const aliases = getAliases();
|
|
72
|
+
expect('sonnet' in aliases).toBe(true);
|
|
73
|
+
expect('haiku' in aliases).toBe(true);
|
|
74
|
+
expect('opus' in aliases).toBe(true);
|
|
75
|
+
expect('gpt4o' in aliases).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('returns a copy so mutation does not affect subsequent calls', () => {
|
|
79
|
+
const aliases = getAliases();
|
|
80
|
+
(aliases as any)['__test__'] = 'should-not-persist';
|
|
81
|
+
const aliases2 = getAliases();
|
|
82
|
+
expect('__test__' in aliases2).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// detectProvider
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
describe('detectProvider', () => {
|
|
91
|
+
it('detects anthropic for "claude-sonnet-4-20250514"', () => {
|
|
92
|
+
expect(detectProvider('claude-sonnet-4-20250514')).toBe('anthropic');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('detects openai for "gpt-4o"', () => {
|
|
96
|
+
expect(detectProvider('gpt-4o')).toBe('openai');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('detects google for "gemini-2.0-flash-exp"', () => {
|
|
100
|
+
expect(detectProvider('gemini-2.0-flash-exp')).toBe('google');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('detects ollama for "llama3.2"', () => {
|
|
104
|
+
expect(detectProvider('llama3.2')).toBe('ollama');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('detects ollama for "mistral"', () => {
|
|
108
|
+
expect(detectProvider('mistral')).toBe('ollama');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('resolves explicit provider prefix "groq/llama-3.1-70b"', () => {
|
|
112
|
+
expect(detectProvider('groq/llama-3.1-70b')).toBe('groq');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('resolves explicit provider prefix "openai/gpt-4o"', () => {
|
|
116
|
+
expect(detectProvider('openai/gpt-4o')).toBe('openai');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('falls back to openrouter for unknown "provider/model" prefix', () => {
|
|
120
|
+
expect(detectProvider('unknown-provider/some-model')).toBe('openrouter');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('defaults to anthropic for unrecognised bare model names', () => {
|
|
124
|
+
expect(detectProvider('totally-unknown-model')).toBe('anthropic');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// calculateCost
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
describe('calculateCost', () => {
|
|
133
|
+
it('returns a CostResult with costUSD and breakdown fields', () => {
|
|
134
|
+
const result: CostResult = calculateCost('anthropic', 'claude-sonnet-4-20250514', 1000, 1000);
|
|
135
|
+
expect(typeof result.costUSD).toBe('number');
|
|
136
|
+
expect(typeof result.breakdown.input).toBe('number');
|
|
137
|
+
expect(typeof result.breakdown.output).toBe('number');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('calculates correct cost for claude-sonnet-4-20250514 at 1K input + 1K output tokens', () => {
|
|
141
|
+
// Pricing: $0.003 / 1K input, $0.015 / 1K output
|
|
142
|
+
const result = calculateCost('anthropic', 'claude-sonnet-4-20250514', 1000, 1000);
|
|
143
|
+
expect(result.breakdown.input).toBeCloseTo(0.003, 6);
|
|
144
|
+
expect(result.breakdown.output).toBeCloseTo(0.015, 6);
|
|
145
|
+
expect(result.costUSD).toBeCloseTo(0.018, 6);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('calculates correct cost for gpt-4o at 2K input + 500 output tokens', () => {
|
|
149
|
+
// Pricing: $0.005 / 1K input, $0.015 / 1K output
|
|
150
|
+
const result = calculateCost('openai', 'gpt-4o', 2000, 500);
|
|
151
|
+
expect(result.breakdown.input).toBeCloseTo(0.01, 6);
|
|
152
|
+
expect(result.breakdown.output).toBeCloseTo(0.0075, 6);
|
|
153
|
+
expect(result.costUSD).toBeCloseTo(0.0175, 6);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('returns zero cost for ollama (local model)', () => {
|
|
157
|
+
const result = calculateCost('ollama', 'llama3.2', 5000, 2000);
|
|
158
|
+
expect(result.costUSD).toBe(0);
|
|
159
|
+
expect(result.breakdown.input).toBe(0);
|
|
160
|
+
expect(result.breakdown.output).toBe(0);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('returns zero cost for an unknown provider', () => {
|
|
164
|
+
const result = calculateCost('nonexistent-provider', 'some-model', 1000, 1000);
|
|
165
|
+
expect(result.costUSD).toBe(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('returns zero cost for a known provider but unknown model', () => {
|
|
169
|
+
const result = calculateCost('openai', 'gpt-99-ultra-fake', 1000, 1000);
|
|
170
|
+
expect(result.costUSD).toBe(0);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('costUSD equals breakdown.input + breakdown.output', () => {
|
|
174
|
+
const result = calculateCost('anthropic', 'claude-opus-4-20250514', 3000, 1500);
|
|
175
|
+
expect(result.costUSD).toBeCloseTo(result.breakdown.input + result.breakdown.output, 10);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('returns zero cost for zero tokens', () => {
|
|
179
|
+
const result = calculateCost('anthropic', 'claude-sonnet-4-20250514', 0, 0);
|
|
180
|
+
expect(result.costUSD).toBe(0);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// getPricingData
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
describe('getPricingData', () => {
|
|
189
|
+
it('returns an object with anthropic pricing', () => {
|
|
190
|
+
const pricing = getPricingData();
|
|
191
|
+
expect(typeof pricing).toBe('object');
|
|
192
|
+
expect('anthropic' in pricing).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('contains openai pricing', () => {
|
|
196
|
+
expect('openai' in getPricingData()).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('contains google pricing', () => {
|
|
200
|
+
expect('google' in getPricingData()).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('anthropic pricing for claude-sonnet-4-20250514 is a two-element array', () => {
|
|
204
|
+
const pricing = getPricingData();
|
|
205
|
+
const entry = pricing.anthropic['claude-sonnet-4-20250514'];
|
|
206
|
+
expect(Array.isArray(entry)).toBe(true);
|
|
207
|
+
expect(entry.length).toBe(2);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for LSP Manager, Client, Language configs, and Agent Loop integration.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
5
|
+
import { getLanguageForFile, getLanguagePriority, LANGUAGE_CONFIGS } from '../lsp/languages';
|
|
6
|
+
import { LSPManager, resetLSPManager } from '../lsp/manager';
|
|
7
|
+
import { severityLabel } from '../lsp/client';
|
|
8
|
+
|
|
9
|
+
describe('Language Configs', () => {
|
|
10
|
+
describe('getLanguageForFile', () => {
|
|
11
|
+
it('should match TypeScript files', () => {
|
|
12
|
+
expect(getLanguageForFile('src/index.ts')?.id).toBe('typescript');
|
|
13
|
+
expect(getLanguageForFile('component.tsx')?.id).toBe('typescript');
|
|
14
|
+
expect(getLanguageForFile('index.js')?.id).toBe('typescript');
|
|
15
|
+
expect(getLanguageForFile('config.mjs')?.id).toBe('typescript');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should match Go files', () => {
|
|
19
|
+
expect(getLanguageForFile('main.go')?.id).toBe('go');
|
|
20
|
+
expect(getLanguageForFile('pkg/server/handler.go')?.id).toBe('go');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should match Python files', () => {
|
|
24
|
+
expect(getLanguageForFile('app.py')?.id).toBe('python');
|
|
25
|
+
expect(getLanguageForFile('types.pyi')?.id).toBe('python');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should match Terraform files', () => {
|
|
29
|
+
expect(getLanguageForFile('main.tf')?.id).toBe('terraform');
|
|
30
|
+
expect(getLanguageForFile('variables.tfvars')?.id).toBe('terraform');
|
|
31
|
+
expect(getLanguageForFile('config.hcl')?.id).toBe('terraform');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should match YAML files', () => {
|
|
35
|
+
expect(getLanguageForFile('config.yaml')?.id).toBe('yaml');
|
|
36
|
+
expect(getLanguageForFile('deployment.yml')?.id).toBe('yaml');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should match Dockerfile', () => {
|
|
40
|
+
expect(getLanguageForFile('Dockerfile')?.id).toBe('docker');
|
|
41
|
+
expect(getLanguageForFile('app.dockerfile')?.id).toBe('docker');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should return undefined for unknown extensions', () => {
|
|
45
|
+
expect(getLanguageForFile('README.md')).toBeUndefined();
|
|
46
|
+
expect(getLanguageForFile('Makefile')).toBeUndefined();
|
|
47
|
+
expect(getLanguageForFile('data.csv')).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('getLanguagePriority', () => {
|
|
52
|
+
it('should return all configs in priority order', () => {
|
|
53
|
+
const priority = getLanguagePriority();
|
|
54
|
+
expect(priority).toHaveLength(LANGUAGE_CONFIGS.length);
|
|
55
|
+
expect(priority[0].id).toBe('typescript');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('LANGUAGE_CONFIGS', () => {
|
|
60
|
+
it('should have command and installHint for all configs', () => {
|
|
61
|
+
for (const config of LANGUAGE_CONFIGS) {
|
|
62
|
+
expect(config.command).toBeTruthy();
|
|
63
|
+
expect(config.installHint).toBeTruthy();
|
|
64
|
+
expect(config.extensions.length).toBeGreaterThan(0);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('severityLabel', () => {
|
|
71
|
+
it('should return correct labels', () => {
|
|
72
|
+
expect(severityLabel(1)).toBe('Error');
|
|
73
|
+
expect(severityLabel(2)).toBe('Warning');
|
|
74
|
+
expect(severityLabel(3)).toBe('Info');
|
|
75
|
+
expect(severityLabel(4)).toBe('Hint');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('LSPManager', () => {
|
|
80
|
+
let manager: LSPManager;
|
|
81
|
+
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
resetLSPManager();
|
|
84
|
+
manager = new LSPManager('/tmp/test-project');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
afterEach(async () => {
|
|
88
|
+
await manager.stopAll();
|
|
89
|
+
resetLSPManager();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('setEnabled', () => {
|
|
93
|
+
it('should disable LSP integration', () => {
|
|
94
|
+
manager.setEnabled(false);
|
|
95
|
+
// touchFile should be a no-op when disabled
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should re-enable LSP integration', () => {
|
|
99
|
+
manager.setEnabled(false);
|
|
100
|
+
manager.setEnabled(true);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('getDiagnostics', () => {
|
|
105
|
+
it('should return empty array when disabled', async () => {
|
|
106
|
+
manager.setEnabled(false);
|
|
107
|
+
const diags = await manager.getDiagnostics('/tmp/test.ts');
|
|
108
|
+
expect(diags).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should return empty array for unsupported file types', async () => {
|
|
112
|
+
const diags = await manager.getDiagnostics('/tmp/file.csv');
|
|
113
|
+
expect(diags).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should return empty array when server is not running', async () => {
|
|
117
|
+
const diags = await manager.getDiagnostics('/tmp/test.ts', 100);
|
|
118
|
+
expect(diags).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('getErrors', () => {
|
|
123
|
+
it('should return empty array when no errors', async () => {
|
|
124
|
+
const errors = await manager.getErrors('/tmp/test.ts');
|
|
125
|
+
expect(errors).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('formatDiagnosticsForAgent', () => {
|
|
130
|
+
it('should return null for empty diagnostics', () => {
|
|
131
|
+
expect(manager.formatDiagnosticsForAgent([])).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should format error diagnostics', () => {
|
|
135
|
+
const diagnostics = [
|
|
136
|
+
{
|
|
137
|
+
file: '/src/server.ts',
|
|
138
|
+
line: 23,
|
|
139
|
+
column: 5,
|
|
140
|
+
severity: 1 as const,
|
|
141
|
+
message: "Property 'origin' does not exist on type 'CorsConfig'",
|
|
142
|
+
source: 'ts',
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
const result = manager.formatDiagnosticsForAgent(diagnostics);
|
|
146
|
+
expect(result).toContain('[LSP Diagnostics]');
|
|
147
|
+
expect(result).toContain('Error: /src/server.ts:23:5');
|
|
148
|
+
expect(result).toContain("Property 'origin'");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should truncate warnings after 5', () => {
|
|
152
|
+
const diagnostics = Array.from({ length: 10 }, (_, i) => ({
|
|
153
|
+
file: '/src/file.ts',
|
|
154
|
+
line: i + 1,
|
|
155
|
+
column: 1,
|
|
156
|
+
severity: 2 as const,
|
|
157
|
+
message: `Warning ${i + 1}`,
|
|
158
|
+
}));
|
|
159
|
+
const result = manager.formatDiagnosticsForAgent(diagnostics);
|
|
160
|
+
expect(result).toContain('5 more warnings');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should return null for only info/hint diagnostics', () => {
|
|
164
|
+
const diagnostics = [
|
|
165
|
+
{ file: '/src/file.ts', line: 1, column: 1, severity: 3 as const, message: 'Info' },
|
|
166
|
+
{ file: '/src/file.ts', line: 2, column: 1, severity: 4 as const, message: 'Hint' },
|
|
167
|
+
];
|
|
168
|
+
expect(manager.formatDiagnosticsForAgent(diagnostics)).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('getStatus', () => {
|
|
173
|
+
it('should return status for all configured languages', async () => {
|
|
174
|
+
const statuses = await manager.getStatus();
|
|
175
|
+
expect(statuses.length).toBe(LANGUAGE_CONFIGS.length);
|
|
176
|
+
for (const status of statuses) {
|
|
177
|
+
expect(status.language).toBeTruthy();
|
|
178
|
+
expect(typeof status.active).toBe('boolean');
|
|
179
|
+
expect(typeof status.available).toBe('boolean');
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('stopAll', () => {
|
|
185
|
+
it('should not throw when no clients running', async () => {
|
|
186
|
+
await expect(manager.stopAll()).resolves.toBeUndefined();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Agent Loop LSP Integration
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
describe('Agent Loop LSP Integration', () => {
|
|
196
|
+
it('should identify file-editing tools correctly', () => {
|
|
197
|
+
// Verify the tools that should trigger LSP diagnostics
|
|
198
|
+
const fileEditingTools = ['edit_file', 'multi_edit', 'write_file'];
|
|
199
|
+
const nonFileEditingTools = ['read_file', 'bash', 'glob', 'grep', 'terraform', 'kubectl'];
|
|
200
|
+
|
|
201
|
+
for (const tool of fileEditingTools) {
|
|
202
|
+
// These tools have a `path` parameter that LSP integration uses
|
|
203
|
+
expect(['edit_file', 'multi_edit', 'write_file']).toContain(tool);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const tool of nonFileEditingTools) {
|
|
207
|
+
expect(['edit_file', 'multi_edit', 'write_file']).not.toContain(tool);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should extract file path from edit_file input', () => {
|
|
212
|
+
const input = { path: '/src/server.ts', old_string: 'foo', new_string: 'bar' };
|
|
213
|
+
expect(input.path).toBe('/src/server.ts');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should extract file path from write_file input', () => {
|
|
217
|
+
const input = { path: '/src/new-file.ts', content: 'console.log("hello")' };
|
|
218
|
+
expect(input.path).toBe('/src/new-file.ts');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should extract file path from multi_edit input', () => {
|
|
222
|
+
const input = { path: '/src/app.ts', edits: [{ old_string: 'a', new_string: 'b' }] };
|
|
223
|
+
expect(input.path).toBe('/src/app.ts');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should format diagnostics for agent conversation injection', () => {
|
|
227
|
+
const manager = new LSPManager('/tmp/test');
|
|
228
|
+
const diagnostics = [
|
|
229
|
+
{
|
|
230
|
+
file: '/src/server.ts',
|
|
231
|
+
line: 23,
|
|
232
|
+
column: 5,
|
|
233
|
+
severity: 1 as const,
|
|
234
|
+
message: "Property 'origin' does not exist on type 'CorsConfig'",
|
|
235
|
+
source: 'ts',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
file: '/src/server.ts',
|
|
239
|
+
line: 45,
|
|
240
|
+
column: 10,
|
|
241
|
+
severity: 2 as const,
|
|
242
|
+
message: 'Unused variable',
|
|
243
|
+
source: 'ts',
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
const formatted = manager.formatDiagnosticsForAgent(diagnostics);
|
|
248
|
+
expect(formatted).not.toBeNull();
|
|
249
|
+
expect(formatted).toContain('[LSP Diagnostics]');
|
|
250
|
+
expect(formatted).toContain('Error:');
|
|
251
|
+
expect(formatted).toContain('Warning:');
|
|
252
|
+
// Verify the formatting includes enough info for the LLM to self-correct
|
|
253
|
+
expect(formatted).toContain('/src/server.ts:23:5');
|
|
254
|
+
expect(formatted).toContain("Property 'origin'");
|
|
255
|
+
resetLSPManager();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should append diagnostics to tool output when errors exist', () => {
|
|
259
|
+
// Simulate the behavior of the agent loop's LSP injection
|
|
260
|
+
const originalOutput = 'File edited successfully.';
|
|
261
|
+
const diagnosticText = '[LSP Diagnostics]\n Error: /src/server.ts:23:5 — Type error (ts)';
|
|
262
|
+
|
|
263
|
+
// This mirrors the logic in executeToolCall
|
|
264
|
+
const combined = `${originalOutput}\n\n${diagnosticText}`;
|
|
265
|
+
expect(combined).toContain(originalOutput);
|
|
266
|
+
expect(combined).toContain('[LSP Diagnostics]');
|
|
267
|
+
expect(combined).toContain('Type error');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should not append diagnostics when there are no errors', () => {
|
|
271
|
+
const manager = new LSPManager('/tmp/test');
|
|
272
|
+
const formatted = manager.formatDiagnosticsForAgent([]);
|
|
273
|
+
expect(formatted).toBeNull();
|
|
274
|
+
resetLSPManager();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should not inject diagnostics for non-file-editing tools', () => {
|
|
278
|
+
// read_file, bash, grep etc. should not trigger LSP
|
|
279
|
+
const nonEditTools = ['read_file', 'bash', 'glob', 'grep', 'list_dir', 'terraform'];
|
|
280
|
+
for (const tool of nonEditTools) {
|
|
281
|
+
// These should not have file path extraction attempted
|
|
282
|
+
const isFileEditing = ['edit_file', 'multi_edit', 'write_file'].includes(tool);
|
|
283
|
+
expect(isFileEditing).toBe(false);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should gracefully handle missing path in input', () => {
|
|
288
|
+
// If somehow the input doesn't have a path field, extraction returns null
|
|
289
|
+
const input = { content: 'some content' };
|
|
290
|
+
const hasPath = 'path' in input;
|
|
291
|
+
expect(hasPath).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
});
|