@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,582 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks System Tests
|
|
3
|
+
*
|
|
4
|
+
* Validates the hooks configuration loader, hook definition validation,
|
|
5
|
+
* the HookEngine class (matching, execution), and the convenience
|
|
6
|
+
* functions (runPreToolHooks, runPostToolHooks, runPermissionHooks).
|
|
7
|
+
*
|
|
8
|
+
* Tests that require actual script execution use temporary directories
|
|
9
|
+
* with real hook scripts and hooks.yaml configuration files.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import * as os from 'node:os';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
loadHooksConfig,
|
|
19
|
+
validateHookDefinition,
|
|
20
|
+
DEFAULT_HOOK_TIMEOUT,
|
|
21
|
+
type HookDefinition,
|
|
22
|
+
} from '../hooks/config';
|
|
23
|
+
import {
|
|
24
|
+
HookEngine,
|
|
25
|
+
runPreToolHooks,
|
|
26
|
+
runPostToolHooks,
|
|
27
|
+
runPermissionHooks,
|
|
28
|
+
type HookContext,
|
|
29
|
+
} from '../hooks/engine';
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/** Create a temporary directory for test isolation. */
|
|
36
|
+
function createTempDir(): string {
|
|
37
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-hooks-test-'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Remove a temporary directory and all its contents. */
|
|
41
|
+
function removeTempDir(dir: string): void {
|
|
42
|
+
try {
|
|
43
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
44
|
+
} catch {
|
|
45
|
+
// Best effort
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Create a .nimbus/hooks.yaml file in the given project directory. */
|
|
50
|
+
function writeHooksYaml(projectDir: string, content: string): void {
|
|
51
|
+
const nimbusDir = path.join(projectDir, '.nimbus');
|
|
52
|
+
fs.mkdirSync(nimbusDir, { recursive: true });
|
|
53
|
+
fs.writeFileSync(path.join(nimbusDir, 'hooks.yaml'), content, 'utf-8');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Write an executable script file. */
|
|
57
|
+
function writeScript(filePath: string, content: string): void {
|
|
58
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
59
|
+
fs.writeFileSync(filePath, content, { mode: 0o755 });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Build a minimal HookContext for testing. */
|
|
63
|
+
function makeContext(toolName: string): HookContext {
|
|
64
|
+
return {
|
|
65
|
+
tool: toolName,
|
|
66
|
+
input: {},
|
|
67
|
+
sessionId: 'test-session',
|
|
68
|
+
agent: 'build',
|
|
69
|
+
timestamp: new Date().toISOString(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ===========================================================================
|
|
74
|
+
// loadHooksConfig
|
|
75
|
+
// ===========================================================================
|
|
76
|
+
|
|
77
|
+
describe('loadHooksConfig', () => {
|
|
78
|
+
let tmpDir: string;
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
tmpDir = createTempDir();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
removeTempDir(tmpDir);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('returns null when no file exists', () => {
|
|
89
|
+
const config = loadHooksConfig(tmpDir);
|
|
90
|
+
expect(config).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('loads valid hooks.yaml', () => {
|
|
94
|
+
writeHooksYaml(
|
|
95
|
+
tmpDir,
|
|
96
|
+
`hooks:
|
|
97
|
+
PreToolUse:
|
|
98
|
+
- match: "edit_file|write_file"
|
|
99
|
+
command: ".nimbus/hooks/pre-edit.sh"
|
|
100
|
+
timeout: 5000
|
|
101
|
+
PostToolUse:
|
|
102
|
+
- match: ".*"
|
|
103
|
+
command: ".nimbus/hooks/post-all.sh"
|
|
104
|
+
`
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const config = loadHooksConfig(tmpDir);
|
|
108
|
+
expect(config).not.toBeNull();
|
|
109
|
+
expect(config!.hooks.PreToolUse).toHaveLength(1);
|
|
110
|
+
expect(config!.hooks.PreToolUse[0].match).toBe('edit_file|write_file');
|
|
111
|
+
expect(config!.hooks.PreToolUse[0].command).toBe('.nimbus/hooks/pre-edit.sh');
|
|
112
|
+
expect(config!.hooks.PreToolUse[0].timeout).toBe(5000);
|
|
113
|
+
expect(config!.hooks.PostToolUse).toHaveLength(1);
|
|
114
|
+
expect(config!.hooks.PostToolUse[0].match).toBe('.*');
|
|
115
|
+
// PermissionRequest should default to empty array
|
|
116
|
+
expect(config!.hooks.PermissionRequest).toHaveLength(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('throws for invalid hook event name', () => {
|
|
120
|
+
writeHooksYaml(
|
|
121
|
+
tmpDir,
|
|
122
|
+
`hooks:
|
|
123
|
+
InvalidEvent:
|
|
124
|
+
- match: ".*"
|
|
125
|
+
command: "echo hi"
|
|
126
|
+
`
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(() => loadHooksConfig(tmpDir)).toThrow(/unknown hook event/);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('throws for missing top-level hooks key', () => {
|
|
133
|
+
writeHooksYaml(
|
|
134
|
+
tmpDir,
|
|
135
|
+
`something_else:
|
|
136
|
+
PreToolUse:
|
|
137
|
+
- match: ".*"
|
|
138
|
+
command: "echo hi"
|
|
139
|
+
`
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
expect(() => loadHooksConfig(tmpDir)).toThrow(/missing top-level "hooks" key/);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ===========================================================================
|
|
147
|
+
// validateHookDefinition
|
|
148
|
+
// ===========================================================================
|
|
149
|
+
|
|
150
|
+
describe('validateHookDefinition', () => {
|
|
151
|
+
test('passes for valid hook', () => {
|
|
152
|
+
const hook: HookDefinition = {
|
|
153
|
+
match: 'edit_file|write_file',
|
|
154
|
+
command: '.nimbus/hooks/pre-edit.sh',
|
|
155
|
+
timeout: 5000,
|
|
156
|
+
};
|
|
157
|
+
const errors = validateHookDefinition(hook);
|
|
158
|
+
expect(errors).toHaveLength(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('passes for valid hook without timeout', () => {
|
|
162
|
+
const hook: HookDefinition = {
|
|
163
|
+
match: '.*',
|
|
164
|
+
command: 'echo hello',
|
|
165
|
+
};
|
|
166
|
+
const errors = validateHookDefinition(hook);
|
|
167
|
+
expect(errors).toHaveLength(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('catches invalid regex in match', () => {
|
|
171
|
+
const hook: HookDefinition = {
|
|
172
|
+
match: '[invalid(regex',
|
|
173
|
+
command: 'echo hello',
|
|
174
|
+
};
|
|
175
|
+
const errors = validateHookDefinition(hook);
|
|
176
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
177
|
+
expect(errors.some(e => e.includes('not a valid regex'))).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('catches empty command', () => {
|
|
181
|
+
const hook: HookDefinition = {
|
|
182
|
+
match: '.*',
|
|
183
|
+
command: '',
|
|
184
|
+
};
|
|
185
|
+
const errors = validateHookDefinition(hook);
|
|
186
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
187
|
+
expect(errors.some(e => e.includes('command'))).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('catches empty match', () => {
|
|
191
|
+
const hook = {
|
|
192
|
+
match: '',
|
|
193
|
+
command: 'echo hello',
|
|
194
|
+
} as HookDefinition;
|
|
195
|
+
const errors = validateHookDefinition(hook);
|
|
196
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
197
|
+
expect(errors.some(e => e.includes('match'))).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('catches negative timeout', () => {
|
|
201
|
+
const hook: HookDefinition = {
|
|
202
|
+
match: '.*',
|
|
203
|
+
command: 'echo hello',
|
|
204
|
+
timeout: -1,
|
|
205
|
+
};
|
|
206
|
+
const errors = validateHookDefinition(hook);
|
|
207
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
208
|
+
expect(errors.some(e => e.includes('timeout'))).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('catches zero timeout', () => {
|
|
212
|
+
const hook: HookDefinition = {
|
|
213
|
+
match: '.*',
|
|
214
|
+
command: 'echo hello',
|
|
215
|
+
timeout: 0,
|
|
216
|
+
};
|
|
217
|
+
const errors = validateHookDefinition(hook);
|
|
218
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
219
|
+
expect(errors.some(e => e.includes('timeout'))).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ===========================================================================
|
|
224
|
+
// HookEngine — matching
|
|
225
|
+
// ===========================================================================
|
|
226
|
+
|
|
227
|
+
describe('HookEngine.hasHooks', () => {
|
|
228
|
+
let tmpDir: string;
|
|
229
|
+
|
|
230
|
+
beforeEach(() => {
|
|
231
|
+
tmpDir = createTempDir();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
afterEach(() => {
|
|
235
|
+
removeTempDir(tmpDir);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('returns true for matching tools', () => {
|
|
239
|
+
writeHooksYaml(
|
|
240
|
+
tmpDir,
|
|
241
|
+
`hooks:
|
|
242
|
+
PreToolUse:
|
|
243
|
+
- match: "edit_file|write_file"
|
|
244
|
+
command: "echo pre"
|
|
245
|
+
`
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const engine = new HookEngine(tmpDir);
|
|
249
|
+
expect(engine.hasHooks('PreToolUse', 'edit_file')).toBe(true);
|
|
250
|
+
expect(engine.hasHooks('PreToolUse', 'write_file')).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('returns false for non-matching tools', () => {
|
|
254
|
+
writeHooksYaml(
|
|
255
|
+
tmpDir,
|
|
256
|
+
`hooks:
|
|
257
|
+
PreToolUse:
|
|
258
|
+
- match: "edit_file|write_file"
|
|
259
|
+
command: "echo pre"
|
|
260
|
+
`
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const engine = new HookEngine(tmpDir);
|
|
264
|
+
expect(engine.hasHooks('PreToolUse', 'read_file')).toBe(false);
|
|
265
|
+
expect(engine.hasHooks('PreToolUse', 'glob')).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('returns false when no config is loaded', () => {
|
|
269
|
+
const engine = new HookEngine();
|
|
270
|
+
expect(engine.hasHooks('PreToolUse', 'edit_file')).toBe(false);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('returns false for event types without hooks', () => {
|
|
274
|
+
writeHooksYaml(
|
|
275
|
+
tmpDir,
|
|
276
|
+
`hooks:
|
|
277
|
+
PreToolUse:
|
|
278
|
+
- match: "edit_file"
|
|
279
|
+
command: "echo pre"
|
|
280
|
+
`
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const engine = new HookEngine(tmpDir);
|
|
284
|
+
expect(engine.hasHooks('PostToolUse', 'edit_file')).toBe(false);
|
|
285
|
+
expect(engine.hasHooks('PermissionRequest', 'edit_file')).toBe(false);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe('HookEngine.getMatchingHooks', () => {
|
|
290
|
+
let tmpDir: string;
|
|
291
|
+
|
|
292
|
+
beforeEach(() => {
|
|
293
|
+
tmpDir = createTempDir();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
afterEach(() => {
|
|
297
|
+
removeTempDir(tmpDir);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('returns correct hooks for matching tool', () => {
|
|
301
|
+
writeHooksYaml(
|
|
302
|
+
tmpDir,
|
|
303
|
+
`hooks:
|
|
304
|
+
PreToolUse:
|
|
305
|
+
- match: "edit_file"
|
|
306
|
+
command: "echo first"
|
|
307
|
+
- match: "edit_file|write_file"
|
|
308
|
+
command: "echo second"
|
|
309
|
+
- match: "bash"
|
|
310
|
+
command: "echo bash-only"
|
|
311
|
+
`
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const engine = new HookEngine(tmpDir);
|
|
315
|
+
const matches = engine.getMatchingHooks('PreToolUse', 'edit_file');
|
|
316
|
+
expect(matches).toHaveLength(2);
|
|
317
|
+
expect(matches[0].command).toBe('echo first');
|
|
318
|
+
expect(matches[1].command).toBe('echo second');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('returns empty array for no matches', () => {
|
|
322
|
+
writeHooksYaml(
|
|
323
|
+
tmpDir,
|
|
324
|
+
`hooks:
|
|
325
|
+
PreToolUse:
|
|
326
|
+
- match: "edit_file"
|
|
327
|
+
command: "echo first"
|
|
328
|
+
`
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const engine = new HookEngine(tmpDir);
|
|
332
|
+
const matches = engine.getMatchingHooks('PreToolUse', 'read_file');
|
|
333
|
+
expect(matches).toHaveLength(0);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('returns empty array when no config loaded', () => {
|
|
337
|
+
const engine = new HookEngine();
|
|
338
|
+
const matches = engine.getMatchingHooks('PreToolUse', 'edit_file');
|
|
339
|
+
expect(matches).toHaveLength(0);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('wildcard pattern matches all tools', () => {
|
|
343
|
+
writeHooksYaml(
|
|
344
|
+
tmpDir,
|
|
345
|
+
`hooks:
|
|
346
|
+
PostToolUse:
|
|
347
|
+
- match: ".*"
|
|
348
|
+
command: "echo all"
|
|
349
|
+
`
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const engine = new HookEngine(tmpDir);
|
|
353
|
+
expect(engine.getMatchingHooks('PostToolUse', 'edit_file')).toHaveLength(1);
|
|
354
|
+
expect(engine.getMatchingHooks('PostToolUse', 'read_file')).toHaveLength(1);
|
|
355
|
+
expect(engine.getMatchingHooks('PostToolUse', 'terraform')).toHaveLength(1);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// ===========================================================================
|
|
360
|
+
// Convenience functions — no-op behavior
|
|
361
|
+
// ===========================================================================
|
|
362
|
+
|
|
363
|
+
describe('runPreToolHooks (no hooks match)', () => {
|
|
364
|
+
test('returns allowed: true when no hooks match', async () => {
|
|
365
|
+
const engine = new HookEngine(); // no config loaded
|
|
366
|
+
const ctx = makeContext('edit_file');
|
|
367
|
+
const result = await runPreToolHooks(engine, ctx);
|
|
368
|
+
expect(result.allowed).toBe(true);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe('runPostToolHooks (no hooks match)', () => {
|
|
373
|
+
test('completes without error when no hooks match', async () => {
|
|
374
|
+
const engine = new HookEngine();
|
|
375
|
+
const ctx = makeContext('edit_file');
|
|
376
|
+
// Should not throw
|
|
377
|
+
await runPostToolHooks(engine, ctx);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe('runPermissionHooks (no hooks match)', () => {
|
|
382
|
+
test('completes without error when no hooks match', async () => {
|
|
383
|
+
const engine = new HookEngine();
|
|
384
|
+
const ctx = makeContext('terraform');
|
|
385
|
+
// Should not throw
|
|
386
|
+
await runPermissionHooks(engine, ctx);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// ===========================================================================
|
|
391
|
+
// HookEngine — execution with real scripts
|
|
392
|
+
// ===========================================================================
|
|
393
|
+
|
|
394
|
+
describe('HookEngine execution with real scripts', () => {
|
|
395
|
+
let tmpDir: string;
|
|
396
|
+
|
|
397
|
+
beforeEach(() => {
|
|
398
|
+
tmpDir = createTempDir();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
afterEach(() => {
|
|
402
|
+
removeTempDir(tmpDir);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test('PreToolUse hook with exit 0 allows the tool', async () => {
|
|
406
|
+
const scriptPath = path.join(tmpDir, '.nimbus', 'hooks', 'allow.sh');
|
|
407
|
+
writeScript(scriptPath, '#!/bin/sh\nexit 0\n');
|
|
408
|
+
|
|
409
|
+
writeHooksYaml(
|
|
410
|
+
tmpDir,
|
|
411
|
+
`hooks:
|
|
412
|
+
PreToolUse:
|
|
413
|
+
- match: "edit_file"
|
|
414
|
+
command: "${scriptPath}"
|
|
415
|
+
timeout: 10000
|
|
416
|
+
`
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
const engine = new HookEngine(tmpDir);
|
|
420
|
+
const ctx = makeContext('edit_file');
|
|
421
|
+
const result = await runPreToolHooks(engine, ctx);
|
|
422
|
+
expect(result.allowed).toBe(true);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('PreToolUse hook with exit 2 blocks the tool', async () => {
|
|
426
|
+
const scriptPath = path.join(tmpDir, '.nimbus', 'hooks', 'block.sh');
|
|
427
|
+
writeScript(scriptPath, '#!/bin/sh\necho "Blocked by policy" >&2\nexit 2\n');
|
|
428
|
+
|
|
429
|
+
writeHooksYaml(
|
|
430
|
+
tmpDir,
|
|
431
|
+
`hooks:
|
|
432
|
+
PreToolUse:
|
|
433
|
+
- match: "edit_file"
|
|
434
|
+
command: "${scriptPath}"
|
|
435
|
+
timeout: 10000
|
|
436
|
+
`
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
const engine = new HookEngine(tmpDir);
|
|
440
|
+
const ctx = makeContext('edit_file');
|
|
441
|
+
const result = await runPreToolHooks(engine, ctx);
|
|
442
|
+
expect(result.allowed).toBe(false);
|
|
443
|
+
expect(result.message).toContain('Blocked by policy');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test('executeHooks returns results for each matching hook', async () => {
|
|
447
|
+
const allowPath = path.join(tmpDir, '.nimbus', 'hooks', 'allow.sh');
|
|
448
|
+
writeScript(allowPath, '#!/bin/sh\nexit 0\n');
|
|
449
|
+
|
|
450
|
+
const logPath = path.join(tmpDir, '.nimbus', 'hooks', 'log.sh');
|
|
451
|
+
writeScript(logPath, '#!/bin/sh\necho "logged" >&2\nexit 0\n');
|
|
452
|
+
|
|
453
|
+
writeHooksYaml(
|
|
454
|
+
tmpDir,
|
|
455
|
+
`hooks:
|
|
456
|
+
PreToolUse:
|
|
457
|
+
- match: "edit_file"
|
|
458
|
+
command: "${allowPath}"
|
|
459
|
+
timeout: 10000
|
|
460
|
+
- match: ".*"
|
|
461
|
+
command: "${logPath}"
|
|
462
|
+
timeout: 10000
|
|
463
|
+
`
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
const engine = new HookEngine(tmpDir);
|
|
467
|
+
const ctx = makeContext('edit_file');
|
|
468
|
+
const results = await engine.executeHooks('PreToolUse', ctx);
|
|
469
|
+
expect(results).toHaveLength(2);
|
|
470
|
+
expect(results[0].allowed).toBe(true);
|
|
471
|
+
expect(results[0].exitCode).toBe(0);
|
|
472
|
+
expect(results[1].allowed).toBe(true);
|
|
473
|
+
expect(results[1].exitCode).toBe(0);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test('hook with non-zero non-2 exit code still allows but reports error', async () => {
|
|
477
|
+
const scriptPath = path.join(tmpDir, '.nimbus', 'hooks', 'error.sh');
|
|
478
|
+
writeScript(scriptPath, '#!/bin/sh\necho "something failed" >&2\nexit 1\n');
|
|
479
|
+
|
|
480
|
+
writeHooksYaml(
|
|
481
|
+
tmpDir,
|
|
482
|
+
`hooks:
|
|
483
|
+
PreToolUse:
|
|
484
|
+
- match: "bash"
|
|
485
|
+
command: "${scriptPath}"
|
|
486
|
+
timeout: 10000
|
|
487
|
+
`
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const engine = new HookEngine(tmpDir);
|
|
491
|
+
const ctx = makeContext('bash');
|
|
492
|
+
const results = await engine.executeHooks('PreToolUse', ctx);
|
|
493
|
+
expect(results).toHaveLength(1);
|
|
494
|
+
expect(results[0].allowed).toBe(true);
|
|
495
|
+
expect(results[0].exitCode).toBe(1);
|
|
496
|
+
expect(results[0].message).toContain('something failed');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test('PostToolUse hooks execute successfully', async () => {
|
|
500
|
+
const scriptPath = path.join(tmpDir, '.nimbus', 'hooks', 'post.sh');
|
|
501
|
+
writeScript(scriptPath, '#!/bin/sh\necho "post-hook ran"\nexit 0\n');
|
|
502
|
+
|
|
503
|
+
writeHooksYaml(
|
|
504
|
+
tmpDir,
|
|
505
|
+
`hooks:
|
|
506
|
+
PostToolUse:
|
|
507
|
+
- match: "edit_file"
|
|
508
|
+
command: "${scriptPath}"
|
|
509
|
+
timeout: 10000
|
|
510
|
+
`
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
const engine = new HookEngine(tmpDir);
|
|
514
|
+
const ctx: HookContext = {
|
|
515
|
+
...makeContext('edit_file'),
|
|
516
|
+
result: { output: 'file edited', isError: false },
|
|
517
|
+
};
|
|
518
|
+
// Should complete without throwing
|
|
519
|
+
await runPostToolHooks(engine, ctx);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test('hook receives context on stdin', async () => {
|
|
523
|
+
// This script writes stdin to a file so we can verify it
|
|
524
|
+
const outputFile = path.join(tmpDir, 'stdin-capture.json');
|
|
525
|
+
const scriptPath = path.join(tmpDir, '.nimbus', 'hooks', 'capture.sh');
|
|
526
|
+
writeScript(scriptPath, `#!/bin/sh\ncat > "${outputFile}"\nexit 0\n`);
|
|
527
|
+
|
|
528
|
+
writeHooksYaml(
|
|
529
|
+
tmpDir,
|
|
530
|
+
`hooks:
|
|
531
|
+
PreToolUse:
|
|
532
|
+
- match: "terraform"
|
|
533
|
+
command: "${scriptPath}"
|
|
534
|
+
timeout: 10000
|
|
535
|
+
`
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
const engine = new HookEngine(tmpDir);
|
|
539
|
+
const ctx = makeContext('terraform');
|
|
540
|
+
ctx.input = { action: 'plan', workdir: '/tmp/tf' };
|
|
541
|
+
await engine.executeHooks('PreToolUse', ctx);
|
|
542
|
+
|
|
543
|
+
// Verify the script received the JSON context
|
|
544
|
+
const captured = fs.readFileSync(outputFile, 'utf-8');
|
|
545
|
+
const parsed = JSON.parse(captured);
|
|
546
|
+
expect(parsed.tool).toBe('terraform');
|
|
547
|
+
expect(parsed.sessionId).toBe('test-session');
|
|
548
|
+
expect(parsed.input.action).toBe('plan');
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test('hook duration is tracked', async () => {
|
|
552
|
+
const scriptPath = path.join(tmpDir, '.nimbus', 'hooks', 'fast.sh');
|
|
553
|
+
writeScript(scriptPath, '#!/bin/sh\nexit 0\n');
|
|
554
|
+
|
|
555
|
+
writeHooksYaml(
|
|
556
|
+
tmpDir,
|
|
557
|
+
`hooks:
|
|
558
|
+
PreToolUse:
|
|
559
|
+
- match: "edit_file"
|
|
560
|
+
command: "${scriptPath}"
|
|
561
|
+
timeout: 10000
|
|
562
|
+
`
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
const engine = new HookEngine(tmpDir);
|
|
566
|
+
const ctx = makeContext('edit_file');
|
|
567
|
+
const results = await engine.executeHooks('PreToolUse', ctx);
|
|
568
|
+
expect(results).toHaveLength(1);
|
|
569
|
+
expect(typeof results[0].duration).toBe('number');
|
|
570
|
+
expect(results[0].duration).toBeGreaterThanOrEqual(0);
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// ===========================================================================
|
|
575
|
+
// DEFAULT_HOOK_TIMEOUT
|
|
576
|
+
// ===========================================================================
|
|
577
|
+
|
|
578
|
+
describe('DEFAULT_HOOK_TIMEOUT', () => {
|
|
579
|
+
test('is 30 seconds', () => {
|
|
580
|
+
expect(DEFAULT_HOOK_TIMEOUT).toBe(30_000);
|
|
581
|
+
});
|
|
582
|
+
});
|