@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,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for nimbus serve — Headless API
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - OpenAPI specification structure and completeness
|
|
6
|
+
* - HTTP Basic Auth middleware behavior (allow/deny/skip)
|
|
7
|
+
*
|
|
8
|
+
* Integration tests that start the actual HTTP server are intentionally
|
|
9
|
+
* excluded here; they belong in the e2e test suite to avoid port conflicts
|
|
10
|
+
* in parallel test runs.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect } from 'bun:test';
|
|
14
|
+
import { getOpenAPISpec } from '../cli/openapi-spec';
|
|
15
|
+
import { createAuthMiddleware } from '../cli/serve-auth';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// OpenAPI Spec
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
describe('OpenAPI Spec', () => {
|
|
22
|
+
const spec = getOpenAPISpec();
|
|
23
|
+
|
|
24
|
+
it('should return a valid OpenAPI 3.1 document', () => {
|
|
25
|
+
expect(spec.openapi).toBe('3.1.0');
|
|
26
|
+
expect((spec as any).info.title).toBe('Nimbus API');
|
|
27
|
+
expect((spec as any).info.version).toBe('0.2.0');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should define all required endpoint paths', () => {
|
|
31
|
+
const paths = spec.paths as Record<string, unknown>;
|
|
32
|
+
expect(paths['/api/health']).toBeDefined();
|
|
33
|
+
expect(paths['/api/chat']).toBeDefined();
|
|
34
|
+
expect(paths['/api/run']).toBeDefined();
|
|
35
|
+
expect(paths['/api/sessions']).toBeDefined();
|
|
36
|
+
expect(paths['/api/session/{id}']).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should define GET and POST for session/:id', () => {
|
|
40
|
+
const paths = spec.paths as any;
|
|
41
|
+
const sessionPath = paths['/api/session/{id}'];
|
|
42
|
+
expect(sessionPath.get).toBeDefined();
|
|
43
|
+
expect(sessionPath.get.operationId).toBe('getSession');
|
|
44
|
+
expect(sessionPath.post).toBeDefined();
|
|
45
|
+
expect(sessionPath.post.operationId).toBe('continueSession');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should define the chat endpoint with SSE response and required message field', () => {
|
|
49
|
+
const paths = spec.paths as any;
|
|
50
|
+
const chatPost = paths['/api/chat'].post;
|
|
51
|
+
expect(chatPost.operationId).toBe('chat');
|
|
52
|
+
expect(chatPost.requestBody.required).toBe(true);
|
|
53
|
+
|
|
54
|
+
const schema = chatPost.requestBody.content['application/json'].schema;
|
|
55
|
+
expect(schema.required).toContain('message');
|
|
56
|
+
expect(schema.properties.message.type).toBe('string');
|
|
57
|
+
expect(schema.properties.sessionId).toBeDefined();
|
|
58
|
+
expect(schema.properties.model).toBeDefined();
|
|
59
|
+
expect(schema.properties.mode.enum).toEqual(['plan', 'build', 'deploy']);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should define the run endpoint with JSON response schema', () => {
|
|
63
|
+
const paths = spec.paths as any;
|
|
64
|
+
const runPost = paths['/api/run'].post;
|
|
65
|
+
expect(runPost.operationId).toBe('run');
|
|
66
|
+
expect(runPost.requestBody.required).toBe(true);
|
|
67
|
+
|
|
68
|
+
const requestSchema = runPost.requestBody.content['application/json'].schema;
|
|
69
|
+
expect(requestSchema.required).toContain('prompt');
|
|
70
|
+
|
|
71
|
+
const responseSchema = runPost.responses['200'].content['application/json'].schema;
|
|
72
|
+
expect(responseSchema.properties.sessionId).toBeDefined();
|
|
73
|
+
expect(responseSchema.properties.response).toBeDefined();
|
|
74
|
+
expect(responseSchema.properties.turns).toBeDefined();
|
|
75
|
+
expect(responseSchema.properties.cost).toBeDefined();
|
|
76
|
+
expect(responseSchema.properties.usage).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should define the sessions list endpoint', () => {
|
|
80
|
+
const paths = spec.paths as any;
|
|
81
|
+
const sessionsGet = paths['/api/sessions'].get;
|
|
82
|
+
expect(sessionsGet.operationId).toBe('listSessions');
|
|
83
|
+
|
|
84
|
+
const responseSchema = sessionsGet.responses['200'].content['application/json'].schema;
|
|
85
|
+
expect(responseSchema.properties.sessions.type).toBe('array');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should define the health endpoint with expected properties', () => {
|
|
89
|
+
const paths = spec.paths as any;
|
|
90
|
+
const healthGet = paths['/api/health'].get;
|
|
91
|
+
expect(healthGet.operationId).toBe('getHealth');
|
|
92
|
+
|
|
93
|
+
const schema = healthGet.responses['200'].content['application/json'].schema;
|
|
94
|
+
expect(schema.properties.status.enum).toEqual(['ok']);
|
|
95
|
+
expect(schema.properties.uptime.type).toBe('number');
|
|
96
|
+
expect(schema.properties.db.type).toBe('boolean');
|
|
97
|
+
expect(schema.properties.llm.type).toBe('boolean');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should define Session schema in components', () => {
|
|
101
|
+
const components = spec.components as any;
|
|
102
|
+
const sessionSchema = components.schemas.Session;
|
|
103
|
+
expect(sessionSchema).toBeDefined();
|
|
104
|
+
expect(sessionSchema.type).toBe('object');
|
|
105
|
+
expect(sessionSchema.properties.id).toBeDefined();
|
|
106
|
+
expect(sessionSchema.properties.name).toBeDefined();
|
|
107
|
+
expect(sessionSchema.properties.status.enum).toContain('active');
|
|
108
|
+
expect(sessionSchema.properties.status.enum).toContain('suspended');
|
|
109
|
+
expect(sessionSchema.properties.status.enum).toContain('completed');
|
|
110
|
+
expect(sessionSchema.properties.mode.enum).toContain('plan');
|
|
111
|
+
expect(sessionSchema.properties.mode.enum).toContain('build');
|
|
112
|
+
expect(sessionSchema.properties.mode.enum).toContain('deploy');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should define Usage schema in components', () => {
|
|
116
|
+
const components = spec.components as any;
|
|
117
|
+
const usageSchema = components.schemas.Usage;
|
|
118
|
+
expect(usageSchema).toBeDefined();
|
|
119
|
+
expect(usageSchema.properties.promptTokens.type).toBe('integer');
|
|
120
|
+
expect(usageSchema.properties.completionTokens.type).toBe('integer');
|
|
121
|
+
expect(usageSchema.properties.totalTokens.type).toBe('integer');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should define Error schema in components', () => {
|
|
125
|
+
const components = spec.components as any;
|
|
126
|
+
const errorSchema = components.schemas.Error;
|
|
127
|
+
expect(errorSchema).toBeDefined();
|
|
128
|
+
expect(errorSchema.properties.error.type).toBe('string');
|
|
129
|
+
expect(errorSchema.required).toContain('error');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should define basicAuth security scheme', () => {
|
|
133
|
+
const components = spec.components as any;
|
|
134
|
+
expect(components.securitySchemes.basicAuth).toBeDefined();
|
|
135
|
+
expect(components.securitySchemes.basicAuth.type).toBe('http');
|
|
136
|
+
expect(components.securitySchemes.basicAuth.scheme).toBe('basic');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should include server definitions', () => {
|
|
140
|
+
const servers = spec.servers as any[];
|
|
141
|
+
expect(servers.length).toBeGreaterThan(0);
|
|
142
|
+
expect(servers[0].url).toBe('http://localhost:4200');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Auth Middleware
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
describe('Auth Middleware', () => {
|
|
151
|
+
const middleware = createAuthMiddleware({
|
|
152
|
+
username: 'admin',
|
|
153
|
+
password: 'secret',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Helper to invoke the middleware with a given URL and optional headers.
|
|
158
|
+
*/
|
|
159
|
+
function invokeMiddleware(
|
|
160
|
+
url: string,
|
|
161
|
+
method = 'POST',
|
|
162
|
+
headers: Record<string, string> = {}
|
|
163
|
+
): { result: { error: string } | undefined; set: any } {
|
|
164
|
+
const request = new Request(url, { method, headers });
|
|
165
|
+
const set: any = {};
|
|
166
|
+
const result = middleware({ request, set });
|
|
167
|
+
return { result, set };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// -- Public endpoints bypass auth --
|
|
171
|
+
|
|
172
|
+
it('should skip auth for GET /api/health', () => {
|
|
173
|
+
const { result } = invokeMiddleware('http://localhost:4200/api/health', 'GET');
|
|
174
|
+
expect(result).toBeUndefined();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should skip auth for GET /api/openapi.json', () => {
|
|
178
|
+
const { result } = invokeMiddleware('http://localhost:4200/api/openapi.json', 'GET');
|
|
179
|
+
expect(result).toBeUndefined();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should skip auth for CORS OPTIONS preflight', () => {
|
|
183
|
+
const { result } = invokeMiddleware('http://localhost:4200/api/chat', 'OPTIONS');
|
|
184
|
+
expect(result).toBeUndefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// -- Protected endpoints require auth --
|
|
188
|
+
|
|
189
|
+
it('should reject requests without Authorization header', () => {
|
|
190
|
+
const { result, set } = invokeMiddleware('http://localhost:4200/api/chat');
|
|
191
|
+
expect(set.status).toBe(401);
|
|
192
|
+
expect(result).toEqual({ error: 'Authentication required' });
|
|
193
|
+
expect(set.headers['WWW-Authenticate']).toBe('Basic realm="Nimbus API"');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should reject requests with invalid credentials', () => {
|
|
197
|
+
const { result, set } = invokeMiddleware('http://localhost:4200/api/chat', 'POST', {
|
|
198
|
+
Authorization: `Basic ${btoa('wrong:creds')}`,
|
|
199
|
+
});
|
|
200
|
+
expect(set.status).toBe(401);
|
|
201
|
+
expect(result).toEqual({ error: 'Invalid credentials' });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should reject requests with malformed Authorization header', () => {
|
|
205
|
+
const { result, set } = invokeMiddleware('http://localhost:4200/api/chat', 'POST', {
|
|
206
|
+
Authorization: 'Bearer some-token',
|
|
207
|
+
});
|
|
208
|
+
expect(set.status).toBe(401);
|
|
209
|
+
expect(result).toEqual({ error: 'Invalid credentials' });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should allow requests with valid credentials', () => {
|
|
213
|
+
const { result } = invokeMiddleware('http://localhost:4200/api/chat', 'POST', {
|
|
214
|
+
Authorization: `Basic ${btoa('admin:secret')}`,
|
|
215
|
+
});
|
|
216
|
+
expect(result).toBeUndefined();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should allow valid credentials for session endpoints', () => {
|
|
220
|
+
const { result } = invokeMiddleware('http://localhost:4200/api/session/abc-123', 'GET', {
|
|
221
|
+
Authorization: `Basic ${btoa('admin:secret')}`,
|
|
222
|
+
});
|
|
223
|
+
expect(result).toBeUndefined();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should allow valid credentials for the run endpoint', () => {
|
|
227
|
+
const { result } = invokeMiddleware('http://localhost:4200/api/run', 'POST', {
|
|
228
|
+
Authorization: `Basic ${btoa('admin:secret')}`,
|
|
229
|
+
});
|
|
230
|
+
expect(result).toBeUndefined();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should allow valid credentials for the sessions list endpoint', () => {
|
|
234
|
+
const { result } = invokeMiddleware('http://localhost:4200/api/sessions', 'GET', {
|
|
235
|
+
Authorization: `Basic ${btoa('admin:secret')}`,
|
|
236
|
+
});
|
|
237
|
+
expect(result).toBeUndefined();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Additional edge cases
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
describe('Auth Middleware — edge cases', () => {
|
|
246
|
+
it('should work with passwords containing colons', () => {
|
|
247
|
+
const mw = createAuthMiddleware({
|
|
248
|
+
username: 'user',
|
|
249
|
+
password: 'pass:with:colons',
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const request = new Request('http://localhost:4200/api/chat', {
|
|
253
|
+
method: 'POST',
|
|
254
|
+
headers: { Authorization: `Basic ${btoa('user:pass:with:colons')}` },
|
|
255
|
+
});
|
|
256
|
+
const set: any = {};
|
|
257
|
+
const result = mw({ request, set });
|
|
258
|
+
expect(result).toBeUndefined();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should reject empty Authorization header', () => {
|
|
262
|
+
const mw = createAuthMiddleware({
|
|
263
|
+
username: 'admin',
|
|
264
|
+
password: 'secret',
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const request = new Request('http://localhost:4200/api/chat', {
|
|
268
|
+
method: 'POST',
|
|
269
|
+
headers: { Authorization: '' },
|
|
270
|
+
});
|
|
271
|
+
const set: any = {};
|
|
272
|
+
const _result = mw({ request, set });
|
|
273
|
+
expect(set.status).toBe(401);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Multi-Session Manager
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
5
|
+
import { Database } from '../compat/sqlite';
|
|
6
|
+
import { SessionManager } from '../sessions/manager';
|
|
7
|
+
import type { SessionEvent } from '../sessions/types';
|
|
8
|
+
|
|
9
|
+
describe('SessionManager', () => {
|
|
10
|
+
let db: Database;
|
|
11
|
+
let manager: SessionManager;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
db = new Database(':memory:');
|
|
15
|
+
db.exec('PRAGMA journal_mode=WAL');
|
|
16
|
+
db.exec('PRAGMA foreign_keys=ON');
|
|
17
|
+
SessionManager.resetInstance();
|
|
18
|
+
manager = new SessionManager(db);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
db.close();
|
|
23
|
+
SessionManager.resetInstance();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('create', () => {
|
|
27
|
+
it('should create a new session with defaults', () => {
|
|
28
|
+
const session = manager.create({ name: 'Test Session' });
|
|
29
|
+
expect(session.id).toBeTruthy();
|
|
30
|
+
expect(session.name).toBe('Test Session');
|
|
31
|
+
expect(session.status).toBe('active');
|
|
32
|
+
expect(session.mode).toBe('plan');
|
|
33
|
+
expect(session.tokenCount).toBe(0);
|
|
34
|
+
expect(session.costUSD).toBe(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should create a session with custom options', () => {
|
|
38
|
+
const session = manager.create({
|
|
39
|
+
name: 'Deploy Session',
|
|
40
|
+
mode: 'deploy',
|
|
41
|
+
model: 'claude-sonnet',
|
|
42
|
+
cwd: '/tmp/project',
|
|
43
|
+
});
|
|
44
|
+
expect(session.mode).toBe('deploy');
|
|
45
|
+
expect(session.model).toBe('claude-sonnet');
|
|
46
|
+
expect(session.cwd).toBe('/tmp/project');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should emit a created event', () => {
|
|
50
|
+
const events: SessionEvent[] = [];
|
|
51
|
+
manager.onEvent(e => events.push(e));
|
|
52
|
+
manager.create({ name: 'Test' });
|
|
53
|
+
expect(events).toHaveLength(1);
|
|
54
|
+
expect(events[0].type).toBe('created');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('list', () => {
|
|
59
|
+
it('should list all sessions', () => {
|
|
60
|
+
manager.create({ name: 'Session 1' });
|
|
61
|
+
manager.create({ name: 'Session 2' });
|
|
62
|
+
const sessions = manager.list();
|
|
63
|
+
expect(sessions).toHaveLength(2);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should filter by status', () => {
|
|
67
|
+
const _s1 = manager.create({ name: 'Active' });
|
|
68
|
+
const s2 = manager.create({ name: 'Completed' });
|
|
69
|
+
manager.complete(s2.id);
|
|
70
|
+
|
|
71
|
+
const active = manager.list('active');
|
|
72
|
+
expect(active).toHaveLength(1);
|
|
73
|
+
expect(active[0].name).toBe('Active');
|
|
74
|
+
|
|
75
|
+
const completed = manager.list('completed');
|
|
76
|
+
expect(completed).toHaveLength(1);
|
|
77
|
+
expect(completed[0].name).toBe('Completed');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('get', () => {
|
|
82
|
+
it('should return a session by ID', () => {
|
|
83
|
+
const created = manager.create({ name: 'Lookup Test' });
|
|
84
|
+
const fetched = manager.get(created.id);
|
|
85
|
+
expect(fetched).not.toBeNull();
|
|
86
|
+
expect(fetched!.name).toBe('Lookup Test');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should return null for non-existent session', () => {
|
|
90
|
+
expect(manager.get('nonexistent')).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('switchTo', () => {
|
|
95
|
+
it('should switch active session', () => {
|
|
96
|
+
const s1 = manager.create({ name: 'Session 1' });
|
|
97
|
+
const s2 = manager.create({ name: 'Session 2' });
|
|
98
|
+
manager.switchTo(s1.id);
|
|
99
|
+
expect(manager.getActiveSessionId()).toBe(s1.id);
|
|
100
|
+
|
|
101
|
+
manager.switchTo(s2.id);
|
|
102
|
+
expect(manager.getActiveSessionId()).toBe(s2.id);
|
|
103
|
+
|
|
104
|
+
// s1 should be suspended
|
|
105
|
+
const s1Updated = manager.get(s1.id);
|
|
106
|
+
expect(s1Updated!.status).toBe('suspended');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should return null for non-existent session', () => {
|
|
110
|
+
expect(manager.switchTo('nonexistent')).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('suspend and resume', () => {
|
|
115
|
+
it('should suspend an active session', () => {
|
|
116
|
+
const session = manager.create({ name: 'Test' });
|
|
117
|
+
manager.switchTo(session.id);
|
|
118
|
+
manager.suspend(session.id);
|
|
119
|
+
|
|
120
|
+
const updated = manager.get(session.id);
|
|
121
|
+
expect(updated!.status).toBe('suspended');
|
|
122
|
+
expect(manager.getActiveSessionId()).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should resume a suspended session', () => {
|
|
126
|
+
const session = manager.create({ name: 'Test' });
|
|
127
|
+
manager.suspend(session.id);
|
|
128
|
+
manager.resume(session.id);
|
|
129
|
+
|
|
130
|
+
const updated = manager.get(session.id);
|
|
131
|
+
expect(updated!.status).toBe('active');
|
|
132
|
+
expect(manager.getActiveSessionId()).toBe(session.id);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should not resume a completed session', () => {
|
|
136
|
+
const session = manager.create({ name: 'Test' });
|
|
137
|
+
manager.complete(session.id);
|
|
138
|
+
const result = manager.resume(session.id);
|
|
139
|
+
expect(result).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('complete', () => {
|
|
144
|
+
it('should mark session as completed', () => {
|
|
145
|
+
const session = manager.create({ name: 'Test' });
|
|
146
|
+
manager.switchTo(session.id);
|
|
147
|
+
manager.complete(session.id);
|
|
148
|
+
|
|
149
|
+
const updated = manager.get(session.id);
|
|
150
|
+
expect(updated!.status).toBe('completed');
|
|
151
|
+
expect(manager.getActiveSessionId()).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('destroy', () => {
|
|
156
|
+
it('should remove session from database', () => {
|
|
157
|
+
const session = manager.create({ name: 'Test' });
|
|
158
|
+
manager.destroy(session.id);
|
|
159
|
+
expect(manager.get(session.id)).toBeNull();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('updateSession', () => {
|
|
164
|
+
it('should update session fields', () => {
|
|
165
|
+
const session = manager.create({ name: 'Test' });
|
|
166
|
+
manager.updateSession(session.id, {
|
|
167
|
+
tokenCount: 5000,
|
|
168
|
+
costUSD: 0.05,
|
|
169
|
+
mode: 'build',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const updated = manager.get(session.id);
|
|
173
|
+
expect(updated!.tokenCount).toBe(5000);
|
|
174
|
+
expect(updated!.costUSD).toBe(0.05);
|
|
175
|
+
expect(updated!.mode).toBe('build');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('file conflict detection', () => {
|
|
180
|
+
it('should detect when two sessions edit the same file', () => {
|
|
181
|
+
const s1 = manager.create({ name: 'Session 1' });
|
|
182
|
+
const s2 = manager.create({ name: 'Session 2' });
|
|
183
|
+
|
|
184
|
+
manager.recordFileEdit(s1.id, '/src/index.ts');
|
|
185
|
+
const conflicts = manager.recordFileEdit(s2.id, '/src/index.ts');
|
|
186
|
+
|
|
187
|
+
expect(conflicts).toContain(s1.id);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should not detect conflicts for the same session', () => {
|
|
191
|
+
const s1 = manager.create({ name: 'Session 1' });
|
|
192
|
+
|
|
193
|
+
manager.recordFileEdit(s1.id, '/src/index.ts');
|
|
194
|
+
const conflicts = manager.recordFileEdit(s1.id, '/src/index.ts');
|
|
195
|
+
|
|
196
|
+
expect(conflicts).toHaveLength(0);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should emit file_conflict event', () => {
|
|
200
|
+
const events: SessionEvent[] = [];
|
|
201
|
+
manager.onEvent(e => events.push(e));
|
|
202
|
+
|
|
203
|
+
const s1 = manager.create({ name: 'S1' });
|
|
204
|
+
const s2 = manager.create({ name: 'S2' });
|
|
205
|
+
|
|
206
|
+
manager.recordFileEdit(s1.id, '/src/index.ts');
|
|
207
|
+
manager.recordFileEdit(s2.id, '/src/index.ts');
|
|
208
|
+
|
|
209
|
+
const conflictEvents = events.filter(e => e.type === 'file_conflict');
|
|
210
|
+
expect(conflictEvents).toHaveLength(1);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('event listeners', () => {
|
|
215
|
+
it('should support removing listeners', () => {
|
|
216
|
+
const events: SessionEvent[] = [];
|
|
217
|
+
const unsub = manager.onEvent(e => events.push(e));
|
|
218
|
+
|
|
219
|
+
manager.create({ name: 'Before' });
|
|
220
|
+
expect(events).toHaveLength(1);
|
|
221
|
+
|
|
222
|
+
unsub();
|
|
223
|
+
manager.create({ name: 'After' });
|
|
224
|
+
expect(events).toHaveLength(1); // No new events after unsubscribe
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|