@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,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Context Manager + Compaction Agent
|
|
3
|
+
*
|
|
4
|
+
* Covers token estimation, context breakdown calculation, message
|
|
5
|
+
* selection for compaction, and message reassembly.
|
|
6
|
+
*
|
|
7
|
+
* @module __tests__/context-manager
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, beforeEach } from 'bun:test';
|
|
11
|
+
import { ContextManager, estimateTokens, estimateMessageTokens } from '../agent/context-manager';
|
|
12
|
+
import type { LLMMessage } from '../llm/types';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// estimateTokens
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
describe('estimateTokens', () => {
|
|
19
|
+
it('should return 0 for empty string', () => {
|
|
20
|
+
expect(estimateTokens('')).toBe(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should estimate tokens from character count', () => {
|
|
24
|
+
// 5 chars / 4 = 1.25 -> ceil -> 2
|
|
25
|
+
expect(estimateTokens('hello')).toBe(2);
|
|
26
|
+
expect(estimateTokens('a'.repeat(100))).toBe(25);
|
|
27
|
+
expect(estimateTokens('a'.repeat(400))).toBe(100);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should round up for non-divisible lengths', () => {
|
|
31
|
+
// 7 chars / 4 = 1.75 -> ceil -> 2
|
|
32
|
+
expect(estimateTokens('abcdefg')).toBe(2);
|
|
33
|
+
// 1 char / 4 = 0.25 -> ceil -> 1
|
|
34
|
+
expect(estimateTokens('x')).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// estimateMessageTokens
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
describe('estimateMessageTokens', () => {
|
|
43
|
+
it('should estimate tokens for a simple message', () => {
|
|
44
|
+
const msg: LLMMessage = { role: 'user', content: 'Hello world' };
|
|
45
|
+
const tokens = estimateMessageTokens(msg);
|
|
46
|
+
// "Hello world" = 11 chars / 4 = 3 + 4 overhead = 7
|
|
47
|
+
expect(tokens).toBe(7);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should add overhead for role framing', () => {
|
|
51
|
+
const msg: LLMMessage = { role: 'assistant', content: '' };
|
|
52
|
+
// Empty content = 0 tokens + 4 overhead = 4
|
|
53
|
+
expect(estimateMessageTokens(msg)).toBe(4);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should account for tool calls', () => {
|
|
57
|
+
const msg: LLMMessage = {
|
|
58
|
+
role: 'assistant',
|
|
59
|
+
content: 'Let me read that file.',
|
|
60
|
+
toolCalls: [
|
|
61
|
+
{
|
|
62
|
+
id: 'tc1',
|
|
63
|
+
type: 'function',
|
|
64
|
+
function: {
|
|
65
|
+
name: 'read_file',
|
|
66
|
+
arguments: '{"path":"src/index.ts"}',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
const tokens = estimateMessageTokens(msg);
|
|
72
|
+
// Should be more than just the content tokens
|
|
73
|
+
const contentOnly = estimateTokens('Let me read that file.') + 4;
|
|
74
|
+
expect(tokens).toBeGreaterThan(contentOnly);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should handle multiple tool calls', () => {
|
|
78
|
+
const singleCall: LLMMessage = {
|
|
79
|
+
role: 'assistant',
|
|
80
|
+
content: 'Working.',
|
|
81
|
+
toolCalls: [
|
|
82
|
+
{
|
|
83
|
+
id: 'tc1',
|
|
84
|
+
type: 'function',
|
|
85
|
+
function: { name: 'read_file', arguments: '{"path":"a.ts"}' },
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
const doubleCalls: LLMMessage = {
|
|
90
|
+
role: 'assistant',
|
|
91
|
+
content: 'Working.',
|
|
92
|
+
toolCalls: [
|
|
93
|
+
{
|
|
94
|
+
id: 'tc1',
|
|
95
|
+
type: 'function',
|
|
96
|
+
function: { name: 'read_file', arguments: '{"path":"a.ts"}' },
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'tc2',
|
|
100
|
+
type: 'function',
|
|
101
|
+
function: { name: 'write_file', arguments: '{"path":"b.ts","content":"x"}' },
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
expect(estimateMessageTokens(doubleCalls)).toBeGreaterThan(estimateMessageTokens(singleCall));
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// ContextManager
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
describe('ContextManager', () => {
|
|
114
|
+
let cm: ContextManager;
|
|
115
|
+
|
|
116
|
+
beforeEach(() => {
|
|
117
|
+
cm = new ContextManager({
|
|
118
|
+
maxContextTokens: 1000,
|
|
119
|
+
autoCompactThreshold: 0.85,
|
|
120
|
+
preserveRecentMessages: 3,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// -------------------------------------------------------------------------
|
|
125
|
+
// shouldCompact
|
|
126
|
+
// -------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
describe('shouldCompact', () => {
|
|
129
|
+
it('should return false when usage is below threshold', () => {
|
|
130
|
+
const systemPrompt = 'You are a helpful assistant.';
|
|
131
|
+
const messages: LLMMessage[] = [{ role: 'user', content: 'Hello' }];
|
|
132
|
+
expect(cm.shouldCompact(systemPrompt, messages, 50)).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should return true when usage exceeds threshold', () => {
|
|
136
|
+
// ~850 tokens from system prompt alone
|
|
137
|
+
const systemPrompt = 'x'.repeat(3400);
|
|
138
|
+
const messages: LLMMessage[] = [{ role: 'user', content: 'Hello' }];
|
|
139
|
+
// 850 + ~6 + 50 = 906 -> 90.6% of 1000 > 85%
|
|
140
|
+
expect(cm.shouldCompact(systemPrompt, messages, 50)).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should return false when exactly at threshold boundary', () => {
|
|
144
|
+
// With 1000 budget and 0.85 threshold, need >= 85% = 850 tokens
|
|
145
|
+
// 840 tokens from prompt = 3360 chars
|
|
146
|
+
const systemPrompt = 'x'.repeat(3340);
|
|
147
|
+
const messages: LLMMessage[] = [];
|
|
148
|
+
// ~835 + 0 + 0 = 835 -> 83.5% < 85%
|
|
149
|
+
expect(cm.shouldCompact(systemPrompt, messages, 0)).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// -------------------------------------------------------------------------
|
|
154
|
+
// calculateUsage
|
|
155
|
+
// -------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
describe('calculateUsage', () => {
|
|
158
|
+
it('should break down context usage', () => {
|
|
159
|
+
const systemPrompt = 'Base prompt. # NIMBUS.md\nProject instructions here.';
|
|
160
|
+
const messages: LLMMessage[] = [
|
|
161
|
+
{ role: 'user', content: 'What is this project?' },
|
|
162
|
+
{ role: 'assistant', content: 'It is a cloud tool.' },
|
|
163
|
+
];
|
|
164
|
+
const breakdown = cm.calculateUsage(systemPrompt, messages, 100);
|
|
165
|
+
|
|
166
|
+
expect(breakdown.systemPrompt).toBeGreaterThan(0);
|
|
167
|
+
expect(breakdown.nimbusInstructions).toBeGreaterThan(0);
|
|
168
|
+
expect(breakdown.messages).toBeGreaterThan(0);
|
|
169
|
+
expect(breakdown.toolDefinitions).toBe(100);
|
|
170
|
+
expect(breakdown.total).toBe(
|
|
171
|
+
breakdown.systemPrompt +
|
|
172
|
+
breakdown.nimbusInstructions +
|
|
173
|
+
breakdown.messages +
|
|
174
|
+
breakdown.toolDefinitions
|
|
175
|
+
);
|
|
176
|
+
expect(breakdown.budget).toBe(1000);
|
|
177
|
+
expect(breakdown.usagePercent).toBeGreaterThanOrEqual(0);
|
|
178
|
+
expect(breakdown.usagePercent).toBeLessThanOrEqual(100);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should handle system prompt without NIMBUS.md', () => {
|
|
182
|
+
const systemPrompt = 'You are a helpful DevOps assistant.';
|
|
183
|
+
const messages: LLMMessage[] = [];
|
|
184
|
+
const breakdown = cm.calculateUsage(systemPrompt, messages, 0);
|
|
185
|
+
|
|
186
|
+
expect(breakdown.nimbusInstructions).toBe(0);
|
|
187
|
+
expect(breakdown.systemPrompt).toBeGreaterThan(0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should report 0% for zero budget', () => {
|
|
191
|
+
const zeroCm = new ContextManager({ maxContextTokens: 0 });
|
|
192
|
+
const breakdown = zeroCm.calculateUsage('prompt', [], 0);
|
|
193
|
+
expect(breakdown.usagePercent).toBe(0);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// -------------------------------------------------------------------------
|
|
198
|
+
// selectPreservedMessages
|
|
199
|
+
// -------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
describe('selectPreservedMessages', () => {
|
|
202
|
+
it('should preserve all messages when count <= threshold', () => {
|
|
203
|
+
const messages: LLMMessage[] = [
|
|
204
|
+
{ role: 'user', content: 'A' },
|
|
205
|
+
{ role: 'assistant', content: 'B' },
|
|
206
|
+
];
|
|
207
|
+
const { preserved, toSummarize } = cm.selectPreservedMessages(messages);
|
|
208
|
+
expect(preserved).toHaveLength(2);
|
|
209
|
+
expect(toSummarize).toHaveLength(0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should split messages when count > threshold', () => {
|
|
213
|
+
const messages: LLMMessage[] = [
|
|
214
|
+
{ role: 'user', content: 'First' }, // preserved (first)
|
|
215
|
+
{ role: 'assistant', content: 'Second' }, // summarize
|
|
216
|
+
{ role: 'user', content: 'Third' }, // summarize
|
|
217
|
+
{ role: 'assistant', content: 'Fourth' }, // summarize
|
|
218
|
+
{ role: 'user', content: 'Fifth' }, // summarize
|
|
219
|
+
{ role: 'assistant', content: 'Sixth' }, // preserved (recent)
|
|
220
|
+
{ role: 'user', content: 'Seventh' }, // preserved (recent)
|
|
221
|
+
{ role: 'assistant', content: 'Eighth' }, // preserved (recent)
|
|
222
|
+
];
|
|
223
|
+
const { preserved, toSummarize } = cm.selectPreservedMessages(messages);
|
|
224
|
+
expect(preserved).toHaveLength(4); // first + 3 recent
|
|
225
|
+
expect(toSummarize).toHaveLength(4);
|
|
226
|
+
expect(preserved[0].content).toBe('First');
|
|
227
|
+
expect(preserved[1].content).toBe('Sixth');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should preserve summary blocks from previous compactions', () => {
|
|
231
|
+
const messages: LLMMessage[] = [
|
|
232
|
+
{ role: 'user', content: 'First' },
|
|
233
|
+
{
|
|
234
|
+
role: 'user',
|
|
235
|
+
content: '[Context Summary] Previous summary here.',
|
|
236
|
+
},
|
|
237
|
+
{ role: 'assistant', content: 'Middle' },
|
|
238
|
+
{ role: 'user', content: 'Recent1' },
|
|
239
|
+
{ role: 'assistant', content: 'Recent2' },
|
|
240
|
+
{ role: 'user', content: 'Recent3' },
|
|
241
|
+
];
|
|
242
|
+
const { preserved } = cm.selectPreservedMessages(messages);
|
|
243
|
+
const summaryMsg = preserved.find(
|
|
244
|
+
m => typeof m.content === 'string' && m.content.startsWith('[Context Summary]')
|
|
245
|
+
);
|
|
246
|
+
expect(summaryMsg).toBeDefined();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should preserve tool messages near the recent window', () => {
|
|
250
|
+
const messages: LLMMessage[] = [
|
|
251
|
+
{ role: 'user', content: 'First' },
|
|
252
|
+
{ role: 'assistant', content: 'A' },
|
|
253
|
+
{ role: 'user', content: 'B' },
|
|
254
|
+
{ role: 'assistant', content: 'C' },
|
|
255
|
+
{
|
|
256
|
+
role: 'tool',
|
|
257
|
+
content: 'tool output',
|
|
258
|
+
toolCallId: 'tc1',
|
|
259
|
+
name: 'read_file',
|
|
260
|
+
},
|
|
261
|
+
{ role: 'assistant', content: 'D' },
|
|
262
|
+
{ role: 'user', content: 'E' },
|
|
263
|
+
{ role: 'assistant', content: 'F' },
|
|
264
|
+
];
|
|
265
|
+
const { preserved } = cm.selectPreservedMessages(messages);
|
|
266
|
+
const toolMsg = preserved.find(m => m.role === 'tool');
|
|
267
|
+
expect(toolMsg).toBeDefined();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// -------------------------------------------------------------------------
|
|
272
|
+
// buildCompactedMessages
|
|
273
|
+
// -------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
describe('buildCompactedMessages', () => {
|
|
276
|
+
it('should insert summary after first preserved message', () => {
|
|
277
|
+
const preserved: LLMMessage[] = [
|
|
278
|
+
{ role: 'user', content: 'First' },
|
|
279
|
+
{ role: 'assistant', content: 'Recent' },
|
|
280
|
+
];
|
|
281
|
+
const summary = 'User asked about project setup.';
|
|
282
|
+
const result = cm.buildCompactedMessages(preserved, summary);
|
|
283
|
+
|
|
284
|
+
expect(result).toHaveLength(3);
|
|
285
|
+
expect(result[0].content).toBe('First');
|
|
286
|
+
expect(result[1].content).toContain('[Context Summary]');
|
|
287
|
+
expect(result[1].content).toContain(summary);
|
|
288
|
+
expect(result[1].role).toBe('user');
|
|
289
|
+
expect(result[2].content).toBe('Recent');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should handle empty preserved array', () => {
|
|
293
|
+
const result = cm.buildCompactedMessages([], 'Summary text');
|
|
294
|
+
expect(result).toHaveLength(1);
|
|
295
|
+
expect(result[0].content).toContain('[Context Summary]');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should handle single preserved message', () => {
|
|
299
|
+
const preserved: LLMMessage[] = [{ role: 'user', content: 'Only one' }];
|
|
300
|
+
const result = cm.buildCompactedMessages(preserved, 'Summary');
|
|
301
|
+
expect(result).toHaveLength(2);
|
|
302
|
+
expect(result[0].content).toBe('Only one');
|
|
303
|
+
expect(result[1].content).toContain('[Context Summary]');
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// -------------------------------------------------------------------------
|
|
308
|
+
// getConfig / setMaxContextTokens
|
|
309
|
+
// -------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
describe('getConfig', () => {
|
|
312
|
+
it('should return current configuration', () => {
|
|
313
|
+
const config = cm.getConfig();
|
|
314
|
+
expect(config.maxContextTokens).toBe(1000);
|
|
315
|
+
expect(config.autoCompactThreshold).toBe(0.85);
|
|
316
|
+
expect(config.preserveRecentMessages).toBe(3);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe('setMaxContextTokens', () => {
|
|
321
|
+
it('should update the max context tokens', () => {
|
|
322
|
+
cm.setMaxContextTokens(2000);
|
|
323
|
+
expect(cm.getConfig().maxContextTokens).toBe(2000);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should affect shouldCompact calculations', () => {
|
|
327
|
+
const systemPrompt = 'x'.repeat(3400); // ~850 tokens
|
|
328
|
+
const messages: LLMMessage[] = [];
|
|
329
|
+
// At 1000 budget: 85% threshold -> needs 850+ tokens -> should compact
|
|
330
|
+
expect(cm.shouldCompact(systemPrompt, messages, 50)).toBe(true);
|
|
331
|
+
// Increase budget: at 2000, 850/2000 = 42.5% < 85% -> should not compact
|
|
332
|
+
cm.setMaxContextTokens(2000);
|
|
333
|
+
expect(cm.shouldCompact(systemPrompt, messages, 50)).toBe(false);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// -------------------------------------------------------------------------
|
|
338
|
+
// Default options
|
|
339
|
+
// -------------------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
describe('default options', () => {
|
|
342
|
+
it('should use sensible defaults when no options are provided', () => {
|
|
343
|
+
const defaultCm = new ContextManager();
|
|
344
|
+
const config = defaultCm.getConfig();
|
|
345
|
+
expect(config.maxContextTokens).toBe(200_000);
|
|
346
|
+
expect(config.autoCompactThreshold).toBe(0.85);
|
|
347
|
+
expect(config.preserveRecentMessages).toBe(5);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
// Slash Command Parsing (TUI /compact and /context)
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
describe('Slash Command Parsing', () => {
|
|
357
|
+
/** Simulates the command detection logic from App.tsx handleSubmit. */
|
|
358
|
+
function parseSlashCommand(text: string): {
|
|
359
|
+
command: string | null;
|
|
360
|
+
args?: string;
|
|
361
|
+
} {
|
|
362
|
+
const trimmed = text.trim();
|
|
363
|
+
if (trimmed === '/compact') {
|
|
364
|
+
return { command: 'compact' };
|
|
365
|
+
}
|
|
366
|
+
if (trimmed.startsWith('/compact ')) {
|
|
367
|
+
return {
|
|
368
|
+
command: 'compact',
|
|
369
|
+
args: trimmed.slice('/compact '.length).trim(),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (trimmed === '/context') {
|
|
373
|
+
return { command: 'context' };
|
|
374
|
+
}
|
|
375
|
+
return { command: null };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
describe('/compact command', () => {
|
|
379
|
+
it('should detect bare /compact', () => {
|
|
380
|
+
const result = parseSlashCommand('/compact');
|
|
381
|
+
expect(result.command).toBe('compact');
|
|
382
|
+
expect(result.args).toBeUndefined();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should detect /compact with focus area', () => {
|
|
386
|
+
const result = parseSlashCommand('/compact terraform changes');
|
|
387
|
+
expect(result.command).toBe('compact');
|
|
388
|
+
expect(result.args).toBe('terraform changes');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('should handle /compact with leading/trailing whitespace', () => {
|
|
392
|
+
const result = parseSlashCommand(' /compact ');
|
|
393
|
+
expect(result.command).toBe('compact');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should not match /compaction or other prefixes', () => {
|
|
397
|
+
const result = parseSlashCommand('/compaction');
|
|
398
|
+
expect(result.command).toBeNull();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should not match text that contains /compact but does not start with it', () => {
|
|
402
|
+
const result = parseSlashCommand('please /compact this');
|
|
403
|
+
expect(result.command).toBeNull();
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
describe('/context command', () => {
|
|
408
|
+
it('should detect /context', () => {
|
|
409
|
+
const result = parseSlashCommand('/context');
|
|
410
|
+
expect(result.command).toBe('context');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('should handle /context with whitespace', () => {
|
|
414
|
+
const result = parseSlashCommand(' /context ');
|
|
415
|
+
expect(result.command).toBe('context');
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('should not match /contextual', () => {
|
|
419
|
+
const result = parseSlashCommand('/contextual');
|
|
420
|
+
expect(result.command).toBeNull();
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
describe('non-commands', () => {
|
|
425
|
+
it('should not detect regular messages', () => {
|
|
426
|
+
expect(parseSlashCommand('hello world').command).toBeNull();
|
|
427
|
+
expect(parseSlashCommand('fix the CORS issue').command).toBeNull();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should not detect unknown slash commands', () => {
|
|
431
|
+
expect(parseSlashCommand('/unknown').command).toBeNull();
|
|
432
|
+
expect(parseSlashCommand('/help').command).toBeNull();
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
// Context Breakdown Formatting (for /context display)
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
describe('Context Breakdown Formatting', () => {
|
|
442
|
+
it('should format breakdown for display', () => {
|
|
443
|
+
const cm = new ContextManager({
|
|
444
|
+
maxContextTokens: 200_000,
|
|
445
|
+
autoCompactThreshold: 0.85,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const systemPrompt = 'Base prompt. # NIMBUS.md\nProject instructions here.';
|
|
449
|
+
const messages: LLMMessage[] = [
|
|
450
|
+
{ role: 'user', content: 'What is this project?' },
|
|
451
|
+
{ role: 'assistant', content: 'It is a cloud tool.' },
|
|
452
|
+
];
|
|
453
|
+
const breakdown = cm.calculateUsage(systemPrompt, messages, 500);
|
|
454
|
+
|
|
455
|
+
// Verify the breakdown has all fields needed for the TUI display
|
|
456
|
+
expect(typeof breakdown.systemPrompt).toBe('number');
|
|
457
|
+
expect(typeof breakdown.nimbusInstructions).toBe('number');
|
|
458
|
+
expect(typeof breakdown.messages).toBe('number');
|
|
459
|
+
expect(typeof breakdown.toolDefinitions).toBe('number');
|
|
460
|
+
expect(typeof breakdown.total).toBe('number');
|
|
461
|
+
expect(typeof breakdown.budget).toBe('number');
|
|
462
|
+
expect(typeof breakdown.usagePercent).toBe('number');
|
|
463
|
+
|
|
464
|
+
// Total should be positive and within budget for this small example
|
|
465
|
+
expect(breakdown.total).toBeGreaterThan(0);
|
|
466
|
+
expect(breakdown.total).toBeLessThan(breakdown.budget);
|
|
467
|
+
expect(breakdown.usagePercent).toBeLessThan(100);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('should produce a displayable format string', () => {
|
|
471
|
+
const breakdown = {
|
|
472
|
+
systemPrompt: 500,
|
|
473
|
+
nimbusInstructions: 200,
|
|
474
|
+
messages: 1500,
|
|
475
|
+
toolDefinitions: 300,
|
|
476
|
+
total: 2500,
|
|
477
|
+
budget: 200_000,
|
|
478
|
+
usagePercent: 1,
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// Simulates the TUI formatting from App.tsx
|
|
482
|
+
const lines = [
|
|
483
|
+
'Context Usage Breakdown:',
|
|
484
|
+
` System prompt: ${breakdown.systemPrompt.toLocaleString()} tokens`,
|
|
485
|
+
` NIMBUS.md: ${breakdown.nimbusInstructions.toLocaleString()} tokens`,
|
|
486
|
+
` Messages: ${breakdown.messages.toLocaleString()} tokens`,
|
|
487
|
+
` Tool definitions: ${breakdown.toolDefinitions.toLocaleString()} tokens`,
|
|
488
|
+
` ─────────────────────────────`,
|
|
489
|
+
` Total: ${breakdown.total.toLocaleString()} / ${breakdown.budget.toLocaleString()} tokens (${breakdown.usagePercent}%)`,
|
|
490
|
+
];
|
|
491
|
+
const display = lines.join('\n');
|
|
492
|
+
|
|
493
|
+
expect(display).toContain('Context Usage Breakdown');
|
|
494
|
+
expect(display).toContain('System prompt');
|
|
495
|
+
expect(display).toContain('NIMBUS.md');
|
|
496
|
+
expect(display).toContain('Messages');
|
|
497
|
+
expect(display).toContain('Tool definitions');
|
|
498
|
+
expect(display).toContain('2,500');
|
|
499
|
+
expect(display).toContain('200,000');
|
|
500
|
+
expect(display).toContain('1%');
|
|
501
|
+
});
|
|
502
|
+
});
|