@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,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent Tests
|
|
3
|
+
*
|
|
4
|
+
* Validates subagent creation, configuration, tool restrictions, and
|
|
5
|
+
* the @agent mention parser.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, test, expect } from 'bun:test';
|
|
9
|
+
import {
|
|
10
|
+
createSubagent,
|
|
11
|
+
parseAgentMention,
|
|
12
|
+
type SubagentType,
|
|
13
|
+
exploreConfig,
|
|
14
|
+
infraConfig,
|
|
15
|
+
securityConfig,
|
|
16
|
+
costConfig,
|
|
17
|
+
generalConfig,
|
|
18
|
+
Subagent,
|
|
19
|
+
} from '../agent/subagents/index';
|
|
20
|
+
|
|
21
|
+
// ===========================================================================
|
|
22
|
+
// createSubagent
|
|
23
|
+
// ===========================================================================
|
|
24
|
+
|
|
25
|
+
describe('createSubagent', () => {
|
|
26
|
+
test('createSubagent("explore") returns Subagent with correct config', () => {
|
|
27
|
+
const agent = createSubagent('explore');
|
|
28
|
+
expect(agent).toBeInstanceOf(Subagent);
|
|
29
|
+
expect(agent.config.name).toBe('explore');
|
|
30
|
+
expect(agent.config.model).toBe('anthropic/claude-haiku-4-5');
|
|
31
|
+
expect(agent.config.maxTurns).toBe(15);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('createSubagent("infra") returns Subagent with correct tools', () => {
|
|
35
|
+
const agent = createSubagent('infra');
|
|
36
|
+
expect(agent.config.name).toBe('infra');
|
|
37
|
+
const toolNames = agent.config.tools.map(t => t.name);
|
|
38
|
+
expect(toolNames).toContain('read_file');
|
|
39
|
+
expect(toolNames).toContain('cloud_discover');
|
|
40
|
+
expect(toolNames).toContain('cost_estimate');
|
|
41
|
+
expect(toolNames).toContain('drift_detect');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('createSubagent("security") returns Subagent with security prompt', () => {
|
|
45
|
+
const agent = createSubagent('security');
|
|
46
|
+
expect(agent.config.name).toBe('security');
|
|
47
|
+
expect(agent.config.systemPrompt).toContain('security');
|
|
48
|
+
expect(agent.config.systemPrompt).toContain('CRITICAL');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('createSubagent("cost") uses haiku model', () => {
|
|
52
|
+
const agent = createSubagent('cost');
|
|
53
|
+
expect(agent.config.model).toContain('haiku');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('createSubagent("general") has bash and webfetch tools', () => {
|
|
57
|
+
const agent = createSubagent('general');
|
|
58
|
+
const toolNames = agent.config.tools.map(t => t.name);
|
|
59
|
+
expect(toolNames).toContain('bash');
|
|
60
|
+
expect(toolNames).toContain('webfetch');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('all subagent types are valid and produce Subagent instances', () => {
|
|
64
|
+
const types: SubagentType[] = ['explore', 'infra', 'security', 'cost', 'general'];
|
|
65
|
+
for (const type of types) {
|
|
66
|
+
const agent = createSubagent(type);
|
|
67
|
+
expect(agent).toBeInstanceOf(Subagent);
|
|
68
|
+
expect(agent.config.name).toBe(type);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ===========================================================================
|
|
74
|
+
// parseAgentMention
|
|
75
|
+
// ===========================================================================
|
|
76
|
+
|
|
77
|
+
describe('parseAgentMention', () => {
|
|
78
|
+
test('parses "@explore find TODOs" correctly', () => {
|
|
79
|
+
const result = parseAgentMention('@explore find TODOs');
|
|
80
|
+
expect(result).not.toBeNull();
|
|
81
|
+
expect(result!.agent).toBe('explore');
|
|
82
|
+
expect(result!.prompt).toBe('find TODOs');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('parses "@infra check EKS" correctly', () => {
|
|
86
|
+
const result = parseAgentMention('@infra check EKS');
|
|
87
|
+
expect(result).not.toBeNull();
|
|
88
|
+
expect(result!.agent).toBe('infra');
|
|
89
|
+
expect(result!.prompt).toBe('check EKS');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('parses "@security scan for secrets" correctly', () => {
|
|
93
|
+
const result = parseAgentMention('@security scan for secrets');
|
|
94
|
+
expect(result).not.toBeNull();
|
|
95
|
+
expect(result!.agent).toBe('security');
|
|
96
|
+
expect(result!.prompt).toBe('scan for secrets');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('parses "@cost estimate monthly spend" correctly', () => {
|
|
100
|
+
const result = parseAgentMention('@cost estimate monthly spend');
|
|
101
|
+
expect(result).not.toBeNull();
|
|
102
|
+
expect(result!.agent).toBe('cost');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('parses "@general research topic" correctly', () => {
|
|
106
|
+
const result = parseAgentMention('@general research topic');
|
|
107
|
+
expect(result).not.toBeNull();
|
|
108
|
+
expect(result!.agent).toBe('general');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('returns null for normal messages', () => {
|
|
112
|
+
expect(parseAgentMention('normal message without @mention')).toBeNull();
|
|
113
|
+
expect(parseAgentMention('fix the bug')).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('returns null for empty string', () => {
|
|
117
|
+
expect(parseAgentMention('')).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('returns null for unknown @agent prefix', () => {
|
|
121
|
+
expect(parseAgentMention('@unknown do something')).toBeNull();
|
|
122
|
+
expect(parseAgentMention('@deploy run it')).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('returns null when @agent has no prompt', () => {
|
|
126
|
+
// The regex requires at least one character after the agent name
|
|
127
|
+
expect(parseAgentMention('@explore')).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ===========================================================================
|
|
132
|
+
// Subagent Tool Restrictions
|
|
133
|
+
// ===========================================================================
|
|
134
|
+
|
|
135
|
+
describe('Subagent tool restrictions', () => {
|
|
136
|
+
test('all subagent configs exclude the "task" tool (no nesting)', () => {
|
|
137
|
+
const configs = [exploreConfig, infraConfig, securityConfig, costConfig, generalConfig];
|
|
138
|
+
for (const config of configs) {
|
|
139
|
+
const hasTask = config.tools.some(t => t.name === 'task');
|
|
140
|
+
expect(hasTask).toBe(false);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('explore subagent only has read-only tools', () => {
|
|
145
|
+
const toolNames = exploreConfig.tools.map(t => t.name);
|
|
146
|
+
expect(toolNames).toEqual(['read_file', 'glob', 'grep', 'list_dir']);
|
|
147
|
+
// None of these are destructive
|
|
148
|
+
for (const tool of exploreConfig.tools) {
|
|
149
|
+
expect(tool.isDestructive).toBeFalsy();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('security subagent only has read-only tools', () => {
|
|
154
|
+
const toolNames = securityConfig.tools.map(t => t.name);
|
|
155
|
+
expect(toolNames).toEqual(['read_file', 'glob', 'grep', 'list_dir']);
|
|
156
|
+
for (const tool of securityConfig.tools) {
|
|
157
|
+
expect(tool.isDestructive).toBeFalsy();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('infra subagent has cloud discovery tools', () => {
|
|
162
|
+
const toolNames = infraConfig.tools.map(t => t.name);
|
|
163
|
+
expect(toolNames).toContain('cloud_discover');
|
|
164
|
+
expect(toolNames).toContain('cost_estimate');
|
|
165
|
+
expect(toolNames).toContain('drift_detect');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('cost subagent has cost_estimate and cloud_discover', () => {
|
|
169
|
+
const toolNames = costConfig.tools.map(t => t.name);
|
|
170
|
+
expect(toolNames).toContain('cost_estimate');
|
|
171
|
+
expect(toolNames).toContain('cloud_discover');
|
|
172
|
+
// But should not have destructive tools
|
|
173
|
+
expect(toolNames).not.toContain('terraform');
|
|
174
|
+
expect(toolNames).not.toContain('kubectl');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System Prompt Tests
|
|
3
|
+
*
|
|
4
|
+
* Validates that buildSystemPrompt assembles the correct prompt sections
|
|
5
|
+
* based on mode, tools, NIMBUS.md, subagent state, and environment context.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import * as os from 'node:os';
|
|
12
|
+
import { buildSystemPrompt, loadNimbusMd } from '../agent/system-prompt';
|
|
13
|
+
import type { ToolDefinition } from '../tools/schemas/types';
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/** Create a minimal ToolDefinition for prompt tests. */
|
|
21
|
+
function makeTool(name: string): ToolDefinition {
|
|
22
|
+
return {
|
|
23
|
+
name,
|
|
24
|
+
description: `Description of ${name}`,
|
|
25
|
+
inputSchema: z.object({}),
|
|
26
|
+
execute: async () => ({ output: 'ok', isError: false }),
|
|
27
|
+
permissionTier: 'auto_allow',
|
|
28
|
+
category: 'standard',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Temp directory for NIMBUS.md tests
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
let tmpDir: string;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-prompt-test-'));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ===========================================================================
|
|
47
|
+
// buildSystemPrompt
|
|
48
|
+
// ===========================================================================
|
|
49
|
+
|
|
50
|
+
describe('buildSystemPrompt', () => {
|
|
51
|
+
test('includes base identity', () => {
|
|
52
|
+
const prompt = buildSystemPrompt({ mode: 'build', tools: [] });
|
|
53
|
+
expect(prompt).toContain('You are Nimbus');
|
|
54
|
+
expect(prompt).toContain('AI-powered DevOps');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('includes mode-specific instructions for "plan"', () => {
|
|
58
|
+
const prompt = buildSystemPrompt({ mode: 'plan', tools: [] });
|
|
59
|
+
expect(prompt).toContain('Mode: PLAN');
|
|
60
|
+
expect(prompt).toContain('NOT allowed');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('includes mode-specific instructions for "build"', () => {
|
|
64
|
+
const prompt = buildSystemPrompt({ mode: 'build', tools: [] });
|
|
65
|
+
expect(prompt).toContain('Mode: BUILD');
|
|
66
|
+
expect(prompt).toContain('Edit and create files');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('includes mode-specific instructions for "deploy"', () => {
|
|
70
|
+
const prompt = buildSystemPrompt({ mode: 'deploy', tools: [] });
|
|
71
|
+
expect(prompt).toContain('Mode: DEPLOY');
|
|
72
|
+
expect(prompt).toContain('terraform apply');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('includes tool-use guidelines', () => {
|
|
76
|
+
const prompt = buildSystemPrompt({ mode: 'build', tools: [] });
|
|
77
|
+
expect(prompt).toContain('Tool-Use Guidelines');
|
|
78
|
+
expect(prompt).toContain('read_file');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('includes tools summary with correct count', () => {
|
|
82
|
+
const tools = [makeTool('alpha'), makeTool('beta'), makeTool('gamma')];
|
|
83
|
+
const prompt = buildSystemPrompt({ mode: 'build', tools });
|
|
84
|
+
expect(prompt).toContain('Available Tools (3)');
|
|
85
|
+
expect(prompt).toContain('**alpha**');
|
|
86
|
+
expect(prompt).toContain('**beta**');
|
|
87
|
+
expect(prompt).toContain('**gamma**');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('includes NIMBUS.md content when provided', () => {
|
|
91
|
+
const prompt = buildSystemPrompt({
|
|
92
|
+
mode: 'build',
|
|
93
|
+
tools: [],
|
|
94
|
+
nimbusInstructions: 'Always use TypeScript strict mode.',
|
|
95
|
+
});
|
|
96
|
+
expect(prompt).toContain('Project Instructions (NIMBUS.md)');
|
|
97
|
+
expect(prompt).toContain('Always use TypeScript strict mode.');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('includes environment context', () => {
|
|
101
|
+
const prompt = buildSystemPrompt({
|
|
102
|
+
mode: 'build',
|
|
103
|
+
tools: [],
|
|
104
|
+
cwd: tmpDir,
|
|
105
|
+
});
|
|
106
|
+
expect(prompt).toContain('# Environment');
|
|
107
|
+
expect(prompt).toContain(`Working directory: ${tmpDir}`);
|
|
108
|
+
expect(prompt).toContain(`Platform: ${process.platform}`);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('includes subagent instructions when activeSubagent set', () => {
|
|
112
|
+
const prompt = buildSystemPrompt({
|
|
113
|
+
mode: 'build',
|
|
114
|
+
tools: [],
|
|
115
|
+
activeSubagent: 'explore',
|
|
116
|
+
});
|
|
117
|
+
expect(prompt).toContain('Subagent Mode: explore');
|
|
118
|
+
expect(prompt).toContain('Do NOT spawn further subagents');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('does not include subagent section when activeSubagent is not set', () => {
|
|
122
|
+
const prompt = buildSystemPrompt({ mode: 'build', tools: [] });
|
|
123
|
+
expect(prompt).not.toContain('Subagent Mode');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('includes date in environment context', () => {
|
|
127
|
+
const prompt = buildSystemPrompt({ mode: 'build', tools: [] });
|
|
128
|
+
// Should contain a date-like string YYYY-MM-DD
|
|
129
|
+
expect(prompt).toMatch(/Date: \d{4}-\d{2}-\d{2}/);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ===========================================================================
|
|
134
|
+
// loadNimbusMd
|
|
135
|
+
// ===========================================================================
|
|
136
|
+
|
|
137
|
+
describe('loadNimbusMd', () => {
|
|
138
|
+
test('returns null when no file exists', () => {
|
|
139
|
+
const result = loadNimbusMd(tmpDir);
|
|
140
|
+
expect(result).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('loads NIMBUS.md from cwd', () => {
|
|
144
|
+
const content = '# Custom Instructions\nDo the thing.';
|
|
145
|
+
fs.writeFileSync(path.join(tmpDir, 'NIMBUS.md'), content, 'utf-8');
|
|
146
|
+
const result = loadNimbusMd(tmpDir);
|
|
147
|
+
expect(result).toBe(content);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('loads NIMBUS.md from .nimbus subdirectory', () => {
|
|
151
|
+
const nimbusDir = path.join(tmpDir, '.nimbus');
|
|
152
|
+
fs.mkdirSync(nimbusDir, { recursive: true });
|
|
153
|
+
const content = 'Sub-dir instructions';
|
|
154
|
+
fs.writeFileSync(path.join(nimbusDir, 'NIMBUS.md'), content, 'utf-8');
|
|
155
|
+
const result = loadNimbusMd(tmpDir);
|
|
156
|
+
expect(result).toBe(content);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('prefers cwd NIMBUS.md over .nimbus subdirectory', () => {
|
|
160
|
+
// Write both
|
|
161
|
+
fs.writeFileSync(path.join(tmpDir, 'NIMBUS.md'), 'root-level', 'utf-8');
|
|
162
|
+
const nimbusDir = path.join(tmpDir, '.nimbus');
|
|
163
|
+
fs.mkdirSync(nimbusDir, { recursive: true });
|
|
164
|
+
fs.writeFileSync(path.join(nimbusDir, 'NIMBUS.md'), 'sub-level', 'utf-8');
|
|
165
|
+
|
|
166
|
+
const result = loadNimbusMd(tmpDir);
|
|
167
|
+
expect(result).toBe('root-level');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Converter Tests
|
|
3
|
+
*
|
|
4
|
+
* Validates the Zod-to-JSON-Schema converter and the provider-specific
|
|
5
|
+
* format converters (Anthropic, OpenAI, Google).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, test, expect } from 'bun:test';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import {
|
|
11
|
+
zodToJsonSchema,
|
|
12
|
+
toAnthropicTool,
|
|
13
|
+
toOpenAITool,
|
|
14
|
+
toGoogleTool,
|
|
15
|
+
type ToolDefinition,
|
|
16
|
+
} from '../tools/schemas/types';
|
|
17
|
+
import {
|
|
18
|
+
toAnthropicFormat,
|
|
19
|
+
toOpenAIFormat,
|
|
20
|
+
toGoogleFormat,
|
|
21
|
+
toProviderFormat,
|
|
22
|
+
} from '../tools/schemas/converter';
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Helpers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/** Create a minimal ToolDefinition for conversion tests. */
|
|
29
|
+
function makeTool(name: string, schema: z.ZodType<unknown>): ToolDefinition {
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
description: `Description of ${name}`,
|
|
33
|
+
inputSchema: schema,
|
|
34
|
+
execute: async () => ({ output: 'ok', isError: false }),
|
|
35
|
+
permissionTier: 'auto_allow',
|
|
36
|
+
category: 'standard',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ===========================================================================
|
|
41
|
+
// zodToJsonSchema — Primitive Types
|
|
42
|
+
// ===========================================================================
|
|
43
|
+
|
|
44
|
+
describe('zodToJsonSchema', () => {
|
|
45
|
+
test('converts z.string() to { type: "string" }', () => {
|
|
46
|
+
const result = zodToJsonSchema(z.string());
|
|
47
|
+
expect(result).toEqual({ type: 'string' });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('converts z.number() to { type: "number" }', () => {
|
|
51
|
+
const result = zodToJsonSchema(z.number());
|
|
52
|
+
expect(result).toEqual({ type: 'number' });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('converts z.boolean() to { type: "boolean" }', () => {
|
|
56
|
+
const result = zodToJsonSchema(z.boolean());
|
|
57
|
+
expect(result).toEqual({ type: 'boolean' });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('converts z.object({ name: z.string() }) with required', () => {
|
|
61
|
+
const result = zodToJsonSchema(z.object({ name: z.string() }));
|
|
62
|
+
expect(result.type).toBe('object');
|
|
63
|
+
expect(result.properties).toBeDefined();
|
|
64
|
+
expect((result.properties as Record<string, unknown>).name).toEqual({ type: 'string' });
|
|
65
|
+
expect(result.required).toEqual(['name']);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('handles optional fields (not in required array)', () => {
|
|
69
|
+
const result = zodToJsonSchema(
|
|
70
|
+
z.object({
|
|
71
|
+
required_field: z.string(),
|
|
72
|
+
optional_field: z.string().optional(),
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
expect(result.required).toEqual(['required_field']);
|
|
76
|
+
expect((result.properties as Record<string, unknown>).optional_field).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('handles z.enum(["a", "b"]) as { type: "string", enum: ["a", "b"] }', () => {
|
|
80
|
+
const result = zodToJsonSchema(z.enum(['a', 'b']));
|
|
81
|
+
expect(result.type).toBe('string');
|
|
82
|
+
expect(result.enum).toEqual(['a', 'b']);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('handles z.array(z.string()) as { type: "array", items: { type: "string" } }', () => {
|
|
86
|
+
const result = zodToJsonSchema(z.array(z.string()));
|
|
87
|
+
expect(result.type).toBe('array');
|
|
88
|
+
expect(result.items).toEqual({ type: 'string' });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('handles nested objects', () => {
|
|
92
|
+
const result = zodToJsonSchema(
|
|
93
|
+
z.object({
|
|
94
|
+
inner: z.object({
|
|
95
|
+
value: z.number(),
|
|
96
|
+
}),
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
expect(result.type).toBe('object');
|
|
100
|
+
const inner = (result.properties as Record<string, any>).inner;
|
|
101
|
+
expect(inner.type).toBe('object');
|
|
102
|
+
expect(inner.properties.value).toEqual({ type: 'number' });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('handles z.number().optional().default() with default value', () => {
|
|
106
|
+
const result = zodToJsonSchema(z.number().optional().default(42));
|
|
107
|
+
expect(result.type).toBe('number');
|
|
108
|
+
expect(result.default).toBe(42);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ===========================================================================
|
|
113
|
+
// toAnthropicTool
|
|
114
|
+
// ===========================================================================
|
|
115
|
+
|
|
116
|
+
describe('toAnthropicTool', () => {
|
|
117
|
+
test('produces correct format with input_schema.type = "object"', () => {
|
|
118
|
+
const tool = makeTool('test_tool', z.object({ x: z.string() }));
|
|
119
|
+
const result = toAnthropicTool(tool);
|
|
120
|
+
|
|
121
|
+
expect(result.name).toBe('test_tool');
|
|
122
|
+
expect(result.description).toBe('Description of test_tool');
|
|
123
|
+
expect(result.input_schema).toBeDefined();
|
|
124
|
+
expect(result.input_schema.type).toBe('object');
|
|
125
|
+
expect(result.input_schema.properties).toBeDefined();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ===========================================================================
|
|
130
|
+
// toOpenAITool
|
|
131
|
+
// ===========================================================================
|
|
132
|
+
|
|
133
|
+
describe('toOpenAITool', () => {
|
|
134
|
+
test('produces correct format with type = "function"', () => {
|
|
135
|
+
const tool = makeTool('test_tool', z.object({ y: z.number() }));
|
|
136
|
+
const result = toOpenAITool(tool);
|
|
137
|
+
|
|
138
|
+
expect(result.type).toBe('function');
|
|
139
|
+
expect(result.function.name).toBe('test_tool');
|
|
140
|
+
expect(result.function.description).toBe('Description of test_tool');
|
|
141
|
+
expect(result.function.parameters).toBeDefined();
|
|
142
|
+
expect(result.function.parameters.type).toBe('object');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ===========================================================================
|
|
147
|
+
// toGoogleTool
|
|
148
|
+
// ===========================================================================
|
|
149
|
+
|
|
150
|
+
describe('toGoogleTool', () => {
|
|
151
|
+
test('produces correct format with functionDeclarations', () => {
|
|
152
|
+
const tool1 = makeTool('tool_a', z.object({ a: z.string() }));
|
|
153
|
+
const tool2 = makeTool('tool_b', z.object({ b: z.number() }));
|
|
154
|
+
const result = toGoogleTool([tool1, tool2]);
|
|
155
|
+
|
|
156
|
+
expect(result.functionDeclarations).toBeDefined();
|
|
157
|
+
expect(result.functionDeclarations).toHaveLength(2);
|
|
158
|
+
expect(result.functionDeclarations[0].name).toBe('tool_a');
|
|
159
|
+
expect(result.functionDeclarations[1].name).toBe('tool_b');
|
|
160
|
+
expect(result.functionDeclarations[0].parameters.type).toBe('OBJECT');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ===========================================================================
|
|
165
|
+
// Batch Conversion Functions
|
|
166
|
+
// ===========================================================================
|
|
167
|
+
|
|
168
|
+
describe('toAnthropicFormat', () => {
|
|
169
|
+
test('converts array of tools to Anthropic format', () => {
|
|
170
|
+
const tools = [makeTool('a', z.object({})), makeTool('b', z.object({}))];
|
|
171
|
+
const result = toAnthropicFormat(tools);
|
|
172
|
+
expect(result).toHaveLength(2);
|
|
173
|
+
expect(result[0].name).toBe('a');
|
|
174
|
+
expect(result[1].name).toBe('b');
|
|
175
|
+
expect(result[0].input_schema.type).toBe('object');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('toOpenAIFormat', () => {
|
|
180
|
+
test('converts array of tools to OpenAI format', () => {
|
|
181
|
+
const tools = [makeTool('a', z.object({})), makeTool('b', z.object({}))];
|
|
182
|
+
const result = toOpenAIFormat(tools);
|
|
183
|
+
expect(result).toHaveLength(2);
|
|
184
|
+
expect(result[0].type).toBe('function');
|
|
185
|
+
expect(result[1].function.name).toBe('b');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('toGoogleFormat', () => {
|
|
190
|
+
test('produces single tool object with all declarations', () => {
|
|
191
|
+
const tools = [
|
|
192
|
+
makeTool('x', z.object({})),
|
|
193
|
+
makeTool('y', z.object({})),
|
|
194
|
+
makeTool('z', z.object({})),
|
|
195
|
+
];
|
|
196
|
+
const result = toGoogleFormat(tools);
|
|
197
|
+
expect(result.functionDeclarations).toHaveLength(3);
|
|
198
|
+
expect(result.functionDeclarations.map(d => d.name)).toEqual(['x', 'y', 'z']);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ===========================================================================
|
|
203
|
+
// toProviderFormat
|
|
204
|
+
// ===========================================================================
|
|
205
|
+
|
|
206
|
+
describe('toProviderFormat', () => {
|
|
207
|
+
const tools = [makeTool('t', z.object({ a: z.string() }))];
|
|
208
|
+
|
|
209
|
+
test('dispatches to Anthropic for "anthropic"', () => {
|
|
210
|
+
const result = toProviderFormat(tools, 'anthropic');
|
|
211
|
+
expect(Array.isArray(result)).toBe(true);
|
|
212
|
+
const arr = result as any[];
|
|
213
|
+
expect(arr[0].input_schema).toBeDefined();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('dispatches to OpenAI for "openai"', () => {
|
|
217
|
+
const result = toProviderFormat(tools, 'openai');
|
|
218
|
+
expect(Array.isArray(result)).toBe(true);
|
|
219
|
+
const arr = result as any[];
|
|
220
|
+
expect(arr[0].type).toBe('function');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('dispatches to Google for "google"', () => {
|
|
224
|
+
const result = toProviderFormat(tools, 'google');
|
|
225
|
+
expect(Array.isArray(result)).toBe(false);
|
|
226
|
+
expect((result as any).functionDeclarations).toBeDefined();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('dispatches to OpenAI for "ollama"', () => {
|
|
230
|
+
const result = toProviderFormat(tools, 'ollama');
|
|
231
|
+
expect(Array.isArray(result)).toBe(true);
|
|
232
|
+
const arr = result as any[];
|
|
233
|
+
expect(arr[0].type).toBe('function');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('dispatches to OpenAI for "openrouter"', () => {
|
|
237
|
+
const result = toProviderFormat(tools, 'openrouter');
|
|
238
|
+
expect(Array.isArray(result)).toBe(true);
|
|
239
|
+
const arr = result as any[];
|
|
240
|
+
expect(arr[0].type).toBe('function');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('dispatches to Anthropic for "bedrock"', () => {
|
|
244
|
+
const result = toProviderFormat(tools, 'bedrock');
|
|
245
|
+
expect(Array.isArray(result)).toBe(true);
|
|
246
|
+
const arr = result as any[];
|
|
247
|
+
expect(arr[0].input_schema).toBeDefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('dispatches to OpenAI for "openai-compatible"', () => {
|
|
251
|
+
const result = toProviderFormat(tools, 'openai-compatible');
|
|
252
|
+
expect(Array.isArray(result)).toBe(true);
|
|
253
|
+
const arr = result as any[];
|
|
254
|
+
expect(arr[0].type).toBe('function');
|
|
255
|
+
});
|
|
256
|
+
});
|