@build-astron-co/nimbus 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +268 -89
- package/README.md +26 -567
- package/dist/src/agent/compaction-agent.js +24 -12
- package/dist/src/agent/context-manager.js +2 -1
- package/dist/src/agent/expand-files.js +2 -1
- package/dist/src/agent/loop.js +71 -33
- package/dist/src/agent/permissions.js +4 -2
- package/dist/src/agent/system-prompt.js +34 -17
- package/dist/src/app.js +1 -1
- package/dist/src/auth/keychain.js +8 -4
- package/dist/src/auth/store.js +70 -107
- package/dist/src/cli/init.js +35 -19
- package/dist/src/cli/run.js +18 -10
- package/dist/src/cli/serve.js +4 -2
- package/dist/src/cli.js +52 -11
- package/dist/src/commands/alias.js +5 -3
- package/dist/src/commands/audit/index.js +2 -1
- package/dist/src/commands/aws-terraform.js +36 -18
- package/dist/src/commands/completions.js +1 -1
- package/dist/src/commands/config.js +3 -2
- package/dist/src/commands/connect-github.js +92 -0
- package/dist/src/commands/cost/index.js +3 -2
- package/dist/src/commands/deploy.js +15 -10
- package/dist/src/commands/doctor.js +9 -6
- package/dist/src/commands/drift/index.js +2 -1
- package/dist/src/commands/export.js +5 -3
- package/dist/src/commands/generate-terraform.js +110 -2
- package/dist/src/commands/import.js +3 -3
- package/dist/src/commands/incident.js +10 -5
- package/dist/src/commands/login.js +8 -93
- package/dist/src/commands/logs.js +16 -8
- package/dist/src/commands/onboarding.js +6 -4
- package/dist/src/commands/pipeline.js +6 -3
- package/dist/src/commands/plugin.js +3 -2
- package/dist/src/commands/profile.js +27 -14
- package/dist/src/commands/questionnaire.js +1 -1
- package/dist/src/commands/rollback.js +3 -2
- package/dist/src/commands/rollout.js +5 -3
- package/dist/src/commands/runbook.js +17 -10
- package/dist/src/commands/schedule.js +10 -5
- package/dist/src/commands/status.js +2 -1
- package/dist/src/commands/team-context.js +12 -7
- package/dist/src/commands/template.js +1 -1
- package/dist/src/commands/tf/index.js +6 -3
- package/dist/src/commands/upgrade.js +5 -3
- package/dist/src/commands/version.js +6 -3
- package/dist/src/commands/watch.js +6 -3
- package/dist/src/compat/sqlite.js +5 -3
- package/dist/src/config/mode-store.js +2 -1
- package/dist/src/config/profiles.js +4 -2
- package/dist/src/config/types.js +2 -1
- package/dist/src/engine/executor.js +8 -4
- package/dist/src/engine/planner.js +9 -5
- package/dist/src/llm/providers/anthropic.js +6 -3
- package/dist/src/llm/providers/ollama.js +1 -1
- package/dist/src/llm/router.js +22 -7
- package/dist/src/nimbus.js +1 -0
- package/dist/src/sessions/manager.js +6 -3
- package/dist/src/sharing/viewer.js +2 -1
- package/dist/src/tools/file-ops.js +1 -2
- package/dist/src/tools/schemas/devops.js +197 -108
- package/dist/src/tools/schemas/standard.js +1 -1
- package/dist/src/ui/App.js +25 -13
- package/dist/src/ui/FileDiffModal.js +22 -11
- package/dist/src/ui/HelpModal.js +2 -1
- package/dist/src/ui/InputBox.js +6 -3
- package/dist/src/ui/MessageList.js +40 -20
- package/dist/src/ui/TerminalPane.js +2 -1
- package/dist/src/ui/ToolCallDisplay.js +12 -6
- package/dist/src/ui/TreePane.js +2 -1
- package/dist/src/ui/ink/index.js +37 -21
- package/dist/src/version.js +1 -1
- package/dist/src/watcher/index.js +8 -4
- package/package.json +3 -5
- package/src/__tests__/alias.test.ts +0 -133
- package/src/__tests__/app.test.ts +0 -76
- package/src/__tests__/audit.test.ts +0 -877
- package/src/__tests__/circuit-breaker.test.ts +0 -116
- package/src/__tests__/cli-run.test.ts +0 -351
- package/src/__tests__/compat-sqlite.test.ts +0 -68
- package/src/__tests__/context-manager.test.ts +0 -632
- package/src/__tests__/context.test.ts +0 -242
- package/src/__tests__/devops-terminal-gaps.test.ts +0 -718
- package/src/__tests__/doctor.test.ts +0 -48
- package/src/__tests__/enterprise.test.ts +0 -401
- package/src/__tests__/export.test.ts +0 -236
- package/src/__tests__/gap-11-18-20.test.ts +0 -958
- package/src/__tests__/generator.test.ts +0 -433
- package/src/__tests__/helm-streaming.test.ts +0 -127
- package/src/__tests__/hooks.test.ts +0 -582
- package/src/__tests__/incident.test.ts +0 -179
- package/src/__tests__/init.test.ts +0 -487
- package/src/__tests__/intent-parser.test.ts +0 -229
- package/src/__tests__/llm-router.test.ts +0 -209
- package/src/__tests__/logs.test.ts +0 -107
- package/src/__tests__/loop-errors.test.ts +0 -244
- package/src/__tests__/lsp.test.ts +0 -293
- package/src/__tests__/modes.test.ts +0 -336
- package/src/__tests__/perf-optimizations.test.ts +0 -847
- package/src/__tests__/permissions.test.ts +0 -338
- package/src/__tests__/pipeline.test.ts +0 -50
- package/src/__tests__/polish-phase3.test.ts +0 -340
- package/src/__tests__/profile.test.ts +0 -237
- package/src/__tests__/rollback.test.ts +0 -83
- package/src/__tests__/runbook.test.ts +0 -219
- package/src/__tests__/schedule.test.ts +0 -206
- package/src/__tests__/serve.test.ts +0 -275
- package/src/__tests__/sessions.test.ts +0 -322
- package/src/__tests__/sharing.test.ts +0 -340
- package/src/__tests__/snapshots.test.ts +0 -581
- package/src/__tests__/standalone-migration.test.ts +0 -199
- package/src/__tests__/state-db.test.ts +0 -334
- package/src/__tests__/status.test.ts +0 -158
- package/src/__tests__/stream-with-tools.test.ts +0 -778
- package/src/__tests__/subagents.test.ts +0 -176
- package/src/__tests__/system-prompt.test.ts +0 -248
- package/src/__tests__/terminal-gap-v2.test.ts +0 -395
- package/src/__tests__/terminal-parity.test.ts +0 -393
- package/src/__tests__/tf-apply.test.ts +0 -187
- package/src/__tests__/tool-converter.test.ts +0 -256
- package/src/__tests__/tool-schemas.test.ts +0 -602
- package/src/__tests__/tools.test.ts +0 -144
- package/src/__tests__/version-json.test.ts +0 -184
- package/src/__tests__/version.test.ts +0 -49
- package/src/__tests__/watch.test.ts +0 -129
- package/src/agent/compaction-agent.ts +0 -266
- package/src/agent/context-manager.ts +0 -499
- package/src/agent/context.ts +0 -427
- package/src/agent/deploy-preview.ts +0 -487
- package/src/agent/expand-files.ts +0 -108
- package/src/agent/index.ts +0 -68
- package/src/agent/loop.ts +0 -1998
- package/src/agent/modes.ts +0 -429
- package/src/agent/permissions.ts +0 -513
- package/src/agent/subagents/base.ts +0 -116
- package/src/agent/subagents/cost.ts +0 -51
- package/src/agent/subagents/explore.ts +0 -42
- package/src/agent/subagents/general.ts +0 -54
- package/src/agent/subagents/index.ts +0 -102
- package/src/agent/subagents/infra.ts +0 -59
- package/src/agent/subagents/security.ts +0 -69
- package/src/agent/system-prompt.ts +0 -990
- package/src/app.ts +0 -180
- package/src/audit/activity-log.ts +0 -290
- package/src/audit/compliance-checker.ts +0 -540
- package/src/audit/cost-tracker.ts +0 -318
- package/src/audit/index.ts +0 -23
- package/src/audit/security-scanner.ts +0 -641
- package/src/auth/guard.ts +0 -75
- package/src/auth/index.ts +0 -56
- package/src/auth/keychain.ts +0 -82
- package/src/auth/oauth.ts +0 -465
- package/src/auth/providers.ts +0 -470
- package/src/auth/sso.ts +0 -113
- package/src/auth/store.ts +0 -505
- package/src/auth/types.ts +0 -187
- package/src/build.ts +0 -141
- package/src/cli/index.ts +0 -16
- package/src/cli/init.ts +0 -1227
- package/src/cli/openapi-spec.ts +0 -356
- package/src/cli/run.ts +0 -628
- package/src/cli/serve-auth.ts +0 -80
- package/src/cli/serve.ts +0 -539
- package/src/cli/web.ts +0 -71
- package/src/cli.ts +0 -1728
- package/src/clients/core-engine-client.ts +0 -227
- package/src/clients/enterprise-client.ts +0 -334
- package/src/clients/generator-client.ts +0 -351
- package/src/clients/git-client.ts +0 -627
- package/src/clients/github-client.ts +0 -410
- package/src/clients/helm-client.ts +0 -504
- package/src/clients/index.ts +0 -80
- package/src/clients/k8s-client.ts +0 -497
- package/src/clients/llm-client.ts +0 -161
- package/src/clients/rest-client.ts +0 -130
- package/src/clients/service-discovery.ts +0 -38
- package/src/clients/terraform-client.ts +0 -482
- package/src/clients/tools-client.ts +0 -1843
- package/src/clients/ws-client.ts +0 -115
- package/src/commands/alias.ts +0 -100
- package/src/commands/analyze/index.ts +0 -352
- package/src/commands/apply/helm.ts +0 -473
- package/src/commands/apply/index.ts +0 -213
- package/src/commands/apply/k8s.ts +0 -454
- package/src/commands/apply/terraform.ts +0 -582
- package/src/commands/ask.ts +0 -167
- package/src/commands/audit/index.ts +0 -357
- package/src/commands/auth-cloud.ts +0 -407
- package/src/commands/auth-list.ts +0 -134
- package/src/commands/auth-profile.ts +0 -121
- package/src/commands/auth-refresh.ts +0 -187
- package/src/commands/auth-status.ts +0 -141
- package/src/commands/aws/ec2.ts +0 -501
- package/src/commands/aws/iam.ts +0 -397
- package/src/commands/aws/index.ts +0 -133
- package/src/commands/aws/lambda.ts +0 -396
- package/src/commands/aws/rds.ts +0 -439
- package/src/commands/aws/s3.ts +0 -439
- package/src/commands/aws/vpc.ts +0 -393
- package/src/commands/aws-discover.ts +0 -542
- package/src/commands/aws-terraform.ts +0 -755
- package/src/commands/azure/aks.ts +0 -376
- package/src/commands/azure/functions.ts +0 -253
- package/src/commands/azure/index.ts +0 -116
- package/src/commands/azure/storage.ts +0 -478
- package/src/commands/azure/vm.ts +0 -355
- package/src/commands/billing/index.ts +0 -256
- package/src/commands/chat.ts +0 -320
- package/src/commands/completions.ts +0 -268
- package/src/commands/config.ts +0 -372
- package/src/commands/cost/cloud-cost-estimator.ts +0 -266
- package/src/commands/cost/estimator.ts +0 -79
- package/src/commands/cost/index.ts +0 -810
- package/src/commands/cost/parsers/terraform.ts +0 -273
- package/src/commands/cost/parsers/types.ts +0 -25
- package/src/commands/cost/pricing/aws.ts +0 -544
- package/src/commands/cost/pricing/azure.ts +0 -499
- package/src/commands/cost/pricing/gcp.ts +0 -396
- package/src/commands/cost/pricing/index.ts +0 -40
- package/src/commands/demo.ts +0 -250
- package/src/commands/deploy.ts +0 -260
- package/src/commands/doctor.ts +0 -1386
- package/src/commands/drift/index.ts +0 -787
- package/src/commands/explain.ts +0 -277
- package/src/commands/export.ts +0 -146
- package/src/commands/feedback.ts +0 -389
- package/src/commands/fix.ts +0 -324
- package/src/commands/fs/index.ts +0 -402
- package/src/commands/gcp/compute.ts +0 -325
- package/src/commands/gcp/functions.ts +0 -271
- package/src/commands/gcp/gke.ts +0 -438
- package/src/commands/gcp/iam.ts +0 -344
- package/src/commands/gcp/index.ts +0 -129
- package/src/commands/gcp/storage.ts +0 -284
- package/src/commands/generate-helm.ts +0 -1249
- package/src/commands/generate-k8s.ts +0 -1508
- package/src/commands/generate-terraform.ts +0 -1202
- package/src/commands/gh/index.ts +0 -863
- package/src/commands/git/index.ts +0 -1343
- package/src/commands/helm/index.ts +0 -1126
- package/src/commands/help.ts +0 -715
- package/src/commands/history.ts +0 -149
- package/src/commands/import.ts +0 -868
- package/src/commands/incident.ts +0 -166
- package/src/commands/index.ts +0 -367
- package/src/commands/init.ts +0 -1051
- package/src/commands/k8s/index.ts +0 -1137
- package/src/commands/login.ts +0 -716
- package/src/commands/logout.ts +0 -83
- package/src/commands/logs.ts +0 -167
- package/src/commands/onboarding.ts +0 -405
- package/src/commands/pipeline.ts +0 -186
- package/src/commands/plan/display.ts +0 -279
- package/src/commands/plan/index.ts +0 -599
- package/src/commands/plugin.ts +0 -398
- package/src/commands/preview.ts +0 -452
- package/src/commands/profile.ts +0 -342
- package/src/commands/questionnaire.ts +0 -1172
- package/src/commands/resume.ts +0 -47
- package/src/commands/rollback.ts +0 -315
- package/src/commands/rollout.ts +0 -88
- package/src/commands/runbook.ts +0 -346
- package/src/commands/schedule.ts +0 -236
- package/src/commands/status.ts +0 -252
- package/src/commands/team/index.ts +0 -346
- package/src/commands/team-context.ts +0 -220
- package/src/commands/template.ts +0 -233
- package/src/commands/tf/index.ts +0 -1093
- package/src/commands/upgrade.ts +0 -607
- package/src/commands/usage/index.ts +0 -134
- package/src/commands/version.ts +0 -174
- package/src/commands/watch.ts +0 -153
- package/src/compat/index.ts +0 -2
- package/src/compat/runtime.ts +0 -12
- package/src/compat/sqlite.ts +0 -177
- package/src/config/index.ts +0 -17
- package/src/config/manager.ts +0 -530
- package/src/config/mode-store.ts +0 -62
- package/src/config/profiles.ts +0 -84
- package/src/config/safety-policy.ts +0 -358
- package/src/config/schema.ts +0 -125
- package/src/config/types.ts +0 -609
- package/src/config/workspace-state.ts +0 -53
- package/src/context/context-db.ts +0 -199
- package/src/demo/index.ts +0 -349
- package/src/demo/scenarios/full-journey.ts +0 -229
- package/src/demo/scenarios/getting-started.ts +0 -127
- package/src/demo/scenarios/helm-release.ts +0 -341
- package/src/demo/scenarios/k8s-deployment.ts +0 -194
- package/src/demo/scenarios/terraform-vpc.ts +0 -170
- package/src/demo/types.ts +0 -92
- package/src/engine/cost-estimator.ts +0 -480
- package/src/engine/diagram-generator.ts +0 -256
- package/src/engine/drift-detector.ts +0 -902
- package/src/engine/executor.ts +0 -1066
- package/src/engine/index.ts +0 -76
- package/src/engine/orchestrator.ts +0 -636
- package/src/engine/planner.ts +0 -787
- package/src/engine/safety.ts +0 -743
- package/src/engine/verifier.ts +0 -770
- package/src/enterprise/audit.ts +0 -348
- package/src/enterprise/auth.ts +0 -270
- package/src/enterprise/billing.ts +0 -822
- package/src/enterprise/index.ts +0 -17
- package/src/enterprise/teams.ts +0 -443
- package/src/generator/best-practices.ts +0 -1608
- package/src/generator/helm.ts +0 -630
- package/src/generator/index.ts +0 -37
- package/src/generator/intent-parser.ts +0 -514
- package/src/generator/kubernetes.ts +0 -976
- package/src/generator/terraform.ts +0 -1875
- package/src/history/index.ts +0 -8
- package/src/history/manager.ts +0 -250
- package/src/history/types.ts +0 -34
- package/src/hooks/config.ts +0 -432
- package/src/hooks/engine.ts +0 -392
- package/src/hooks/index.ts +0 -4
- package/src/llm/auth-bridge.ts +0 -198
- package/src/llm/circuit-breaker.ts +0 -140
- package/src/llm/config-loader.ts +0 -201
- package/src/llm/cost-calculator.ts +0 -171
- package/src/llm/index.ts +0 -8
- package/src/llm/model-aliases.ts +0 -115
- package/src/llm/provider-registry.ts +0 -63
- package/src/llm/providers/anthropic.ts +0 -462
- package/src/llm/providers/bedrock.ts +0 -477
- package/src/llm/providers/google.ts +0 -405
- package/src/llm/providers/ollama.ts +0 -767
- package/src/llm/providers/openai-compatible.ts +0 -340
- package/src/llm/providers/openai.ts +0 -328
- package/src/llm/providers/openrouter.ts +0 -338
- package/src/llm/router.ts +0 -1104
- package/src/llm/types.ts +0 -232
- package/src/lsp/client.ts +0 -298
- package/src/lsp/languages.ts +0 -119
- package/src/lsp/manager.ts +0 -294
- package/src/mcp/client.ts +0 -402
- package/src/mcp/index.ts +0 -5
- package/src/mcp/manager.ts +0 -133
- package/src/nimbus.ts +0 -233
- package/src/plugins/index.ts +0 -27
- package/src/plugins/loader.ts +0 -334
- package/src/plugins/manager.ts +0 -376
- package/src/plugins/types.ts +0 -284
- package/src/scanners/cicd-scanner.ts +0 -258
- package/src/scanners/cloud-scanner.ts +0 -466
- package/src/scanners/framework-scanner.ts +0 -469
- package/src/scanners/iac-scanner.ts +0 -388
- package/src/scanners/index.ts +0 -539
- package/src/scanners/language-scanner.ts +0 -276
- package/src/scanners/package-manager-scanner.ts +0 -277
- package/src/scanners/types.ts +0 -172
- package/src/sessions/manager.ts +0 -472
- package/src/sessions/types.ts +0 -44
- package/src/sharing/sync.ts +0 -300
- package/src/sharing/viewer.ts +0 -163
- package/src/snapshots/index.ts +0 -2
- package/src/snapshots/manager.ts +0 -530
- package/src/state/artifacts.ts +0 -147
- package/src/state/audit.ts +0 -137
- package/src/state/billing.ts +0 -240
- package/src/state/checkpoints.ts +0 -117
- package/src/state/config.ts +0 -67
- package/src/state/conversations.ts +0 -14
- package/src/state/credentials.ts +0 -154
- package/src/state/db.ts +0 -58
- package/src/state/index.ts +0 -26
- package/src/state/messages.ts +0 -115
- package/src/state/projects.ts +0 -123
- package/src/state/schema.ts +0 -236
- package/src/state/sessions.ts +0 -147
- package/src/state/teams.ts +0 -200
- package/src/telemetry.ts +0 -108
- package/src/tools/aws-ops.ts +0 -952
- package/src/tools/azure-ops.ts +0 -579
- package/src/tools/file-ops.ts +0 -615
- package/src/tools/gcp-ops.ts +0 -625
- package/src/tools/git-ops.ts +0 -773
- package/src/tools/github-ops.ts +0 -799
- package/src/tools/helm-ops.ts +0 -943
- package/src/tools/index.ts +0 -17
- package/src/tools/k8s-ops.ts +0 -819
- package/src/tools/schemas/converter.ts +0 -184
- package/src/tools/schemas/devops.ts +0 -3502
- package/src/tools/schemas/index.ts +0 -73
- package/src/tools/schemas/standard.ts +0 -1148
- package/src/tools/schemas/types.ts +0 -735
- package/src/tools/spawn-exec.ts +0 -148
- package/src/tools/terraform-ops.ts +0 -862
- package/src/types/ambient.d.ts +0 -193
- package/src/types/config.ts +0 -83
- package/src/types/drift.ts +0 -116
- package/src/types/enterprise.ts +0 -335
- package/src/types/index.ts +0 -20
- package/src/types/plan.ts +0 -44
- package/src/types/request.ts +0 -65
- package/src/types/response.ts +0 -54
- package/src/types/service.ts +0 -51
- package/src/ui/App.tsx +0 -2114
- package/src/ui/DeployPreview.tsx +0 -174
- package/src/ui/FileDiffModal.tsx +0 -162
- package/src/ui/Header.tsx +0 -131
- package/src/ui/HelpModal.tsx +0 -57
- package/src/ui/InputBox.tsx +0 -503
- package/src/ui/MessageList.tsx +0 -1032
- package/src/ui/PermissionPrompt.tsx +0 -163
- package/src/ui/StatusBar.tsx +0 -277
- package/src/ui/TerminalPane.tsx +0 -84
- package/src/ui/ToolCallDisplay.tsx +0 -643
- package/src/ui/TreePane.tsx +0 -132
- package/src/ui/chat-ui.ts +0 -850
- package/src/ui/index.ts +0 -33
- package/src/ui/ink/index.ts +0 -1444
- package/src/ui/streaming.ts +0 -176
- package/src/ui/theme.ts +0 -104
- package/src/ui/types.ts +0 -75
- package/src/utils/analytics.ts +0 -72
- package/src/utils/cost-warning.ts +0 -27
- package/src/utils/env.ts +0 -46
- package/src/utils/errors.ts +0 -69
- package/src/utils/event-bus.ts +0 -38
- package/src/utils/index.ts +0 -24
- package/src/utils/logger.ts +0 -171
- package/src/utils/rate-limiter.ts +0 -121
- package/src/utils/service-auth.ts +0 -49
- package/src/utils/validation.ts +0 -53
- package/src/version.ts +0 -4
- package/src/watcher/index.ts +0 -214
- package/src/wizard/approval.ts +0 -383
- package/src/wizard/index.ts +0 -25
- package/src/wizard/prompts.ts +0 -338
- package/src/wizard/types.ts +0 -172
- package/src/wizard/ui.ts +0 -556
- package/src/wizard/wizard.ts +0 -304
- package/tsconfig.json +0 -24
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runbook Command Tests — G15
|
|
3
|
-
*
|
|
4
|
-
* Tests the YAML parser, runbookCommand('list'), and step prompt building.
|
|
5
|
-
* runbookCreate involves interactive readline so we test source-level
|
|
6
|
-
* assertions for that path.
|
|
7
|
-
*
|
|
8
|
-
* Note: process.chdir is not supported in vitest workers. The runbook list
|
|
9
|
-
* test isolates the homedir via vi.mock('node:os') and mocks fs operations
|
|
10
|
-
* to control which directories are seen.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
14
|
-
import * as path from 'node:path';
|
|
15
|
-
import * as os from 'node:os';
|
|
16
|
-
import * as fs from 'node:fs';
|
|
17
|
-
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
// Inline reproduction of parseRunbookYaml (matches source implementation)
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
interface RunbookDef {
|
|
23
|
-
name: string;
|
|
24
|
-
description?: string;
|
|
25
|
-
context?: string;
|
|
26
|
-
steps: string[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function parseRunbookYaml(content: string): RunbookDef {
|
|
30
|
-
const lines = content.split('\n');
|
|
31
|
-
const def: RunbookDef = { name: '', steps: [] };
|
|
32
|
-
let inSteps = false;
|
|
33
|
-
|
|
34
|
-
for (const raw of lines) {
|
|
35
|
-
const line = raw.trimEnd();
|
|
36
|
-
if (line.startsWith('#') || !line.trim()) continue;
|
|
37
|
-
|
|
38
|
-
if (line.startsWith('name:')) {
|
|
39
|
-
def.name = line.slice(5).trim().replace(/^['"]|['"]$/g, '');
|
|
40
|
-
inSteps = false;
|
|
41
|
-
} else if (line.startsWith('description:')) {
|
|
42
|
-
def.description = line.slice(12).trim().replace(/^['"]|['"]$/g, '');
|
|
43
|
-
inSteps = false;
|
|
44
|
-
} else if (line.startsWith('context:')) {
|
|
45
|
-
def.context = line.slice(8).trim().replace(/^['"]|['"]$/g, '');
|
|
46
|
-
inSteps = false;
|
|
47
|
-
} else if (line.trim() === 'steps:') {
|
|
48
|
-
inSteps = true;
|
|
49
|
-
} else if (inSteps && /^\s*-\s/.test(line)) {
|
|
50
|
-
def.steps.push(line.replace(/^\s*-\s*/, '').trim().replace(/^['"]|['"]$/g, ''));
|
|
51
|
-
} else {
|
|
52
|
-
inSteps = false;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return def;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// Inline reproduction of buildRunbookPrompt
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
function buildRunbookPrompt(def: RunbookDef): string {
|
|
64
|
-
const parts = [`# Runbook: ${def.name}`];
|
|
65
|
-
if (def.description) parts.push(`\n${def.description}`);
|
|
66
|
-
if (def.context) parts.push(`\nContext/profile: ${def.context}`);
|
|
67
|
-
parts.push('\n## Steps to execute in order:');
|
|
68
|
-
def.steps.forEach((step, i) => parts.push(`${i + 1}. ${step}`));
|
|
69
|
-
parts.push('\nExecute each step in sequence. Check for errors after each step before proceeding. Report progress clearly.');
|
|
70
|
-
return parts.join('\n');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// YAML parser tests
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
|
|
77
|
-
describe('parseRunbookYaml (G15)', () => {
|
|
78
|
-
const SAMPLE = [
|
|
79
|
-
'name: rotate-certs',
|
|
80
|
-
'description: Rotate TLS certs in prod namespace',
|
|
81
|
-
'context: prod',
|
|
82
|
-
'steps:',
|
|
83
|
-
' - Check for expiring certs in all namespaces',
|
|
84
|
-
' - Rotate each cert using cert-manager annotate',
|
|
85
|
-
' - Verify new certs are valid and pods restarted',
|
|
86
|
-
].join('\n');
|
|
87
|
-
|
|
88
|
-
it('parses name field correctly', () => {
|
|
89
|
-
const def = parseRunbookYaml(SAMPLE);
|
|
90
|
-
expect(def.name).toBe('rotate-certs');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('parses description field', () => {
|
|
94
|
-
const def = parseRunbookYaml(SAMPLE);
|
|
95
|
-
expect(def.description).toBe('Rotate TLS certs in prod namespace');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('parses context field', () => {
|
|
99
|
-
const def = parseRunbookYaml(SAMPLE);
|
|
100
|
-
expect(def.context).toBe('prod');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('parses steps array with correct count', () => {
|
|
104
|
-
const def = parseRunbookYaml(SAMPLE);
|
|
105
|
-
expect(def.steps).toHaveLength(3);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('parses step content correctly', () => {
|
|
109
|
-
const def = parseRunbookYaml(SAMPLE);
|
|
110
|
-
expect(def.steps[0]).toBe('Check for expiring certs in all namespaces');
|
|
111
|
-
expect(def.steps[2]).toBe('Verify new certs are valid and pods restarted');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('ignores comment lines', () => {
|
|
115
|
-
const withComments = [
|
|
116
|
-
'name: test-runbook',
|
|
117
|
-
'# this is a comment',
|
|
118
|
-
'description: desc',
|
|
119
|
-
'steps:',
|
|
120
|
-
' - step one',
|
|
121
|
-
].join('\n');
|
|
122
|
-
const def = parseRunbookYaml(withComments);
|
|
123
|
-
expect(def.name).toBe('test-runbook');
|
|
124
|
-
expect(def.steps).toHaveLength(1);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// ---------------------------------------------------------------------------
|
|
129
|
-
// buildRunbookPrompt tests
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
|
-
|
|
132
|
-
describe('buildRunbookPrompt step composition (G15)', () => {
|
|
133
|
-
it('builds a multi-step numbered prompt', () => {
|
|
134
|
-
const def: RunbookDef = {
|
|
135
|
-
name: 'deploy-rollback',
|
|
136
|
-
description: 'Rollback a failed deployment',
|
|
137
|
-
steps: ['Check rollout status', 'Run helm rollback', 'Verify pods are healthy'],
|
|
138
|
-
};
|
|
139
|
-
const prompt = buildRunbookPrompt(def);
|
|
140
|
-
expect(prompt).toContain('# Runbook: deploy-rollback');
|
|
141
|
-
expect(prompt).toContain('1. Check rollout status');
|
|
142
|
-
expect(prompt).toContain('2. Run helm rollback');
|
|
143
|
-
expect(prompt).toContain('3. Verify pods are healthy');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('includes description in prompt when set', () => {
|
|
147
|
-
const def: RunbookDef = { name: 'my-rb', description: 'My description', steps: ['step 1'] };
|
|
148
|
-
const prompt = buildRunbookPrompt(def);
|
|
149
|
-
expect(prompt).toContain('My description');
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('includes context/profile when set', () => {
|
|
153
|
-
const def: RunbookDef = { name: 'rb', context: 'staging', steps: ['step 1'] };
|
|
154
|
-
const prompt = buildRunbookPrompt(def);
|
|
155
|
-
expect(prompt).toContain('Context/profile: staging');
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('includes execution instructions', () => {
|
|
159
|
-
const def: RunbookDef = { name: 'rb', steps: ['step 1'] };
|
|
160
|
-
const prompt = buildRunbookPrompt(def);
|
|
161
|
-
expect(prompt).toContain('Execute each step in sequence');
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
// runbookCommand list — isolate via mocking node:os and node:fs
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
|
|
169
|
-
describe('runbookCommand list with no runbooks (G15)', () => {
|
|
170
|
-
let tmpDir: string;
|
|
171
|
-
|
|
172
|
-
beforeEach(() => {
|
|
173
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-runbook-test-'));
|
|
174
|
-
vi.resetModules();
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
afterEach(() => {
|
|
178
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
179
|
-
vi.restoreAllMocks();
|
|
180
|
-
vi.resetModules();
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('prints "No runbooks found" when no runbooks directory exists', async () => {
|
|
184
|
-
// Mock node:os so homedir() returns our tmp dir (which has no runbooks/ subdir)
|
|
185
|
-
vi.doMock('node:os', async () => {
|
|
186
|
-
const actual = await vi.importActual<typeof os>('node:os');
|
|
187
|
-
return { ...actual, homedir: () => tmpDir };
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
const logs: string[] = [];
|
|
191
|
-
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
192
|
-
logs.push(args.join(' '));
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const { runbookCommand } = await import('../commands/runbook');
|
|
196
|
-
await runbookCommand('list', []);
|
|
197
|
-
|
|
198
|
-
const allOutput = logs.join('\n');
|
|
199
|
-
expect(allOutput).toContain('No runbooks found');
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('shows usage for unknown subcommand', async () => {
|
|
203
|
-
vi.doMock('node:os', async () => {
|
|
204
|
-
const actual = await vi.importActual<typeof os>('node:os');
|
|
205
|
-
return { ...actual, homedir: () => tmpDir };
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
const logs: string[] = [];
|
|
209
|
-
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
210
|
-
logs.push(args.join(' '));
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
const { runbookCommand } = await import('../commands/runbook');
|
|
214
|
-
await runbookCommand('unknown-subcmd', []);
|
|
215
|
-
|
|
216
|
-
const allOutput = logs.join('\n');
|
|
217
|
-
expect(allOutput).toContain('Usage: nimbus runbook');
|
|
218
|
-
});
|
|
219
|
-
});
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Schedule Command Tests — G13
|
|
3
|
-
*
|
|
4
|
-
* Tests schedule list, add, remove, invalid cron rejection, and
|
|
5
|
-
* crontab activation hint output.
|
|
6
|
-
*
|
|
7
|
-
* File I/O is isolated by mocking node:os.homedir to point to a temp dir.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
11
|
-
import * as fs from 'node:fs';
|
|
12
|
-
import * as path from 'node:path';
|
|
13
|
-
import * as os from 'node:os';
|
|
14
|
-
|
|
15
|
-
// We need to control where schedules.json is written.
|
|
16
|
-
// Approach: mock node:os homedir to our temp dir, then resetModules
|
|
17
|
-
// so the schedule module re-evaluates SCHEDULE_FILE with the new homedir.
|
|
18
|
-
|
|
19
|
-
let tmpDir: string;
|
|
20
|
-
|
|
21
|
-
vi.mock('node:os', async () => {
|
|
22
|
-
const actual = await vi.importActual<typeof os>('node:os');
|
|
23
|
-
return {
|
|
24
|
-
...actual,
|
|
25
|
-
homedir: () => tmpDir ?? actual.homedir(),
|
|
26
|
-
};
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
async function getScheduleModule() {
|
|
30
|
-
vi.resetModules();
|
|
31
|
-
return await import('../commands/schedule');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe('scheduleCommand list (G13)', () => {
|
|
35
|
-
beforeEach(() => {
|
|
36
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-schedule-test-'));
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
afterEach(() => {
|
|
40
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
41
|
-
vi.restoreAllMocks();
|
|
42
|
-
vi.resetModules();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('prints "No schedules configured" when schedules.json does not exist', async () => {
|
|
46
|
-
const logs: string[] = [];
|
|
47
|
-
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
48
|
-
logs.push(args.join(' '));
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const { scheduleCommand } = await getScheduleModule();
|
|
52
|
-
await scheduleCommand('list', []);
|
|
53
|
-
|
|
54
|
-
const output = logs.join('\n');
|
|
55
|
-
expect(output).toContain('No schedules configured');
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('scheduleCommand add (G13)', () => {
|
|
60
|
-
beforeEach(() => {
|
|
61
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-schedule-test-'));
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
afterEach(() => {
|
|
65
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
66
|
-
vi.restoreAllMocks();
|
|
67
|
-
vi.resetModules();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('saves a schedule entry when given a valid cron and prompt', async () => {
|
|
71
|
-
const logs: string[] = [];
|
|
72
|
-
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
73
|
-
logs.push(args.join(' '));
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const { scheduleCommand } = await getScheduleModule();
|
|
77
|
-
await scheduleCommand('add', ['0 8 * * *', 'check drift']);
|
|
78
|
-
|
|
79
|
-
// Verify the schedule file was written
|
|
80
|
-
const scheduleFile = path.join(tmpDir, '.nimbus', 'schedules.json');
|
|
81
|
-
expect(fs.existsSync(scheduleFile)).toBe(true);
|
|
82
|
-
|
|
83
|
-
const data = JSON.parse(fs.readFileSync(scheduleFile, 'utf-8')) as Array<{
|
|
84
|
-
id: string; name: string; cron: string; prompt: string;
|
|
85
|
-
}>;
|
|
86
|
-
expect(data).toHaveLength(1);
|
|
87
|
-
expect(data[0].cron).toBe('0 8 * * *');
|
|
88
|
-
expect(data[0].prompt).toBe('check drift');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('prints crontab activation hint after adding a schedule', async () => {
|
|
92
|
-
const logs: string[] = [];
|
|
93
|
-
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
94
|
-
logs.push(args.join(' '));
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const { scheduleCommand } = await getScheduleModule();
|
|
98
|
-
await scheduleCommand('add', ['0 9 * * 1', 'weekly cost report']);
|
|
99
|
-
|
|
100
|
-
const output = logs.join('\n');
|
|
101
|
-
expect(output).toContain('crontab');
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe('scheduleCommand invalid cron rejection (G13)', () => {
|
|
106
|
-
beforeEach(() => {
|
|
107
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-schedule-test-'));
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
afterEach(() => {
|
|
111
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
112
|
-
vi.restoreAllMocks();
|
|
113
|
-
vi.resetModules();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('rejects a cron with fewer than 5 fields', async () => {
|
|
117
|
-
const errors: string[] = [];
|
|
118
|
-
vi.spyOn(console, 'error').mockImplementation((...args: unknown[]) => {
|
|
119
|
-
errors.push(args.join(' '));
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {
|
|
123
|
-
throw new Error('process.exit called');
|
|
124
|
-
}) as never);
|
|
125
|
-
|
|
126
|
-
const { scheduleCommand } = await getScheduleModule();
|
|
127
|
-
|
|
128
|
-
await expect(scheduleCommand('add', ['* * *', 'bad cron'])).rejects.toThrow('process.exit');
|
|
129
|
-
|
|
130
|
-
expect(errors.join('\n')).toContain('Invalid cron expression');
|
|
131
|
-
exitSpy.mockRestore();
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
describe('scheduleCommand remove (G13)', () => {
|
|
136
|
-
beforeEach(() => {
|
|
137
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-schedule-test-'));
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
afterEach(() => {
|
|
141
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
142
|
-
vi.restoreAllMocks();
|
|
143
|
-
vi.resetModules();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('removes an existing schedule by id', async () => {
|
|
147
|
-
// Seed the schedule file directly
|
|
148
|
-
const nimbusDir = path.join(tmpDir, '.nimbus');
|
|
149
|
-
fs.mkdirSync(nimbusDir, { recursive: true });
|
|
150
|
-
const entry = { id: 'abc123', name: 'my-schedule', cron: '0 8 * * *', prompt: 'drift check', createdAt: new Date().toISOString() };
|
|
151
|
-
fs.writeFileSync(path.join(nimbusDir, 'schedules.json'), JSON.stringify([entry]), 'utf-8');
|
|
152
|
-
|
|
153
|
-
const logs: string[] = [];
|
|
154
|
-
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
155
|
-
logs.push(args.join(' '));
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
const { scheduleCommand } = await getScheduleModule();
|
|
159
|
-
await scheduleCommand('remove', ['abc123']);
|
|
160
|
-
|
|
161
|
-
const data = JSON.parse(fs.readFileSync(path.join(nimbusDir, 'schedules.json'), 'utf-8')) as unknown[];
|
|
162
|
-
expect(data).toHaveLength(0);
|
|
163
|
-
expect(logs.join('\n')).toContain('Removed schedule');
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('shows usage when no id is provided to remove', async () => {
|
|
167
|
-
const errors: string[] = [];
|
|
168
|
-
vi.spyOn(console, 'error').mockImplementation((...args: unknown[]) => {
|
|
169
|
-
errors.push(args.join(' '));
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {
|
|
173
|
-
throw new Error('process.exit called');
|
|
174
|
-
}) as never);
|
|
175
|
-
|
|
176
|
-
const { scheduleCommand } = await getScheduleModule();
|
|
177
|
-
await expect(scheduleCommand('remove', [])).rejects.toThrow('process.exit');
|
|
178
|
-
|
|
179
|
-
expect(errors.join('\n')).toContain('Usage');
|
|
180
|
-
exitSpy.mockRestore();
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
describe('scheduleCommand default/unknown subcommand (G13)', () => {
|
|
185
|
-
beforeEach(() => {
|
|
186
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-schedule-test-'));
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
afterEach(() => {
|
|
190
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
191
|
-
vi.restoreAllMocks();
|
|
192
|
-
vi.resetModules();
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('prints usage for unknown subcommand', async () => {
|
|
196
|
-
const logs: string[] = [];
|
|
197
|
-
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
198
|
-
logs.push(args.join(' '));
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const { scheduleCommand } = await getScheduleModule();
|
|
202
|
-
await scheduleCommand('unknown', []);
|
|
203
|
-
|
|
204
|
-
expect(logs.join('\n')).toContain('Usage: nimbus schedule');
|
|
205
|
-
});
|
|
206
|
-
});
|
|
@@ -1,275 +0,0 @@
|
|
|
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 'vitest';
|
|
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
|
-
});
|