@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,581 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot Manager Tests
|
|
3
|
+
*
|
|
4
|
+
* Validates the SnapshotManager for both git-based and non-git (filesystem)
|
|
5
|
+
* projects. Tests cover snapshot capture/restore, undo/redo, history tracking,
|
|
6
|
+
* cleanup of old snapshots, and the static `shouldSnapshot` decision logic.
|
|
7
|
+
*
|
|
8
|
+
* Git-based tests use real temporary git repositories to exercise the actual
|
|
9
|
+
* git write-tree / read-tree / checkout-index workflow.
|
|
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
|
+
import { execSync } from 'node:child_process';
|
|
17
|
+
|
|
18
|
+
import { SnapshotManager } from '../snapshots/manager';
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Helpers
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/** Create a temporary directory. */
|
|
25
|
+
function createTempDir(): string {
|
|
26
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-snap-test-'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Remove a temporary directory and all its contents. */
|
|
30
|
+
function removeTempDir(dir: string): void {
|
|
31
|
+
try {
|
|
32
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
33
|
+
} catch {
|
|
34
|
+
// Best effort
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Initialize a temporary directory as a git repository with an initial commit.
|
|
40
|
+
* Returns the directory path.
|
|
41
|
+
*/
|
|
42
|
+
function createTempGitRepo(): string {
|
|
43
|
+
const tmpDir = createTempDir();
|
|
44
|
+
execSync('git init', { cwd: tmpDir, stdio: 'pipe' });
|
|
45
|
+
execSync('git config user.email "test@test.com"', { cwd: tmpDir, stdio: 'pipe' });
|
|
46
|
+
execSync('git config user.name "Test"', { cwd: tmpDir, stdio: 'pipe' });
|
|
47
|
+
fs.writeFileSync(path.join(tmpDir, 'file.txt'), 'original');
|
|
48
|
+
execSync('git add -A && git commit -m "init"', { cwd: tmpDir, stdio: 'pipe' });
|
|
49
|
+
return tmpDir;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Common snapshot params for testing. */
|
|
53
|
+
function snapshotParams(desc: string) {
|
|
54
|
+
return {
|
|
55
|
+
sessionId: 'test-session',
|
|
56
|
+
messageId: 'msg-001',
|
|
57
|
+
toolCallId: 'tc-001',
|
|
58
|
+
description: desc,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ===========================================================================
|
|
63
|
+
// Constructor — project detection
|
|
64
|
+
// ===========================================================================
|
|
65
|
+
|
|
66
|
+
describe('SnapshotManager constructor', () => {
|
|
67
|
+
let gitDir: string;
|
|
68
|
+
let nonGitDir: string;
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
gitDir = createTempGitRepo();
|
|
72
|
+
nonGitDir = createTempDir();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
removeTempDir(gitDir);
|
|
77
|
+
removeTempDir(nonGitDir);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('detects git project', () => {
|
|
81
|
+
const _manager = new SnapshotManager({ projectDir: gitDir });
|
|
82
|
+
// Capture a snapshot to verify it uses git (treeHash will be non-empty)
|
|
83
|
+
// We verify indirectly through the snapshot's isGitProject flag
|
|
84
|
+
expect(fs.existsSync(path.join(gitDir, '.git'))).toBe(true);
|
|
85
|
+
// The manager itself is opaque, but we can verify via a snapshot
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('detects non-git project', () => {
|
|
89
|
+
const _manager = new SnapshotManager({ projectDir: nonGitDir });
|
|
90
|
+
// For non-git projects, the manager creates the snapshot directory
|
|
91
|
+
expect(fs.existsSync(path.join(nonGitDir, '.nimbus', 'snapshots'))).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ===========================================================================
|
|
96
|
+
// shouldSnapshot (static method)
|
|
97
|
+
// ===========================================================================
|
|
98
|
+
|
|
99
|
+
describe('SnapshotManager.shouldSnapshot', () => {
|
|
100
|
+
test('edit_file returns true', () => {
|
|
101
|
+
expect(SnapshotManager.shouldSnapshot('edit_file')).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('multi_edit returns true', () => {
|
|
105
|
+
expect(SnapshotManager.shouldSnapshot('multi_edit')).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('write_file returns true', () => {
|
|
109
|
+
expect(SnapshotManager.shouldSnapshot('write_file')).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('bash with "rm -rf dist" returns true', () => {
|
|
113
|
+
expect(SnapshotManager.shouldSnapshot('bash', { command: 'rm -rf dist' })).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('bash with "npm test" returns false', () => {
|
|
117
|
+
expect(SnapshotManager.shouldSnapshot('bash', { command: 'npm test' })).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('read_file returns false', () => {
|
|
121
|
+
expect(SnapshotManager.shouldSnapshot('read_file')).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('glob returns false', () => {
|
|
125
|
+
expect(SnapshotManager.shouldSnapshot('glob')).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('bash with "mv old.txt new.txt" returns true', () => {
|
|
129
|
+
expect(SnapshotManager.shouldSnapshot('bash', { command: 'mv old.txt new.txt' })).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('bash with "cp src dest" returns true', () => {
|
|
133
|
+
expect(SnapshotManager.shouldSnapshot('bash', { command: 'cp src dest' })).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('bash with "echo hello > output.txt" returns true (redirect)', () => {
|
|
137
|
+
expect(SnapshotManager.shouldSnapshot('bash', { command: 'echo hello > output.txt' })).toBe(
|
|
138
|
+
true
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('bash with "ls -la" returns false', () => {
|
|
143
|
+
expect(SnapshotManager.shouldSnapshot('bash', { command: 'ls -la' })).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('bash with "sed -i s/old/new/ file.txt" returns true', () => {
|
|
147
|
+
expect(SnapshotManager.shouldSnapshot('bash', { command: 'sed -i s/old/new/ file.txt' })).toBe(
|
|
148
|
+
true
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('bash with empty command returns false', () => {
|
|
153
|
+
expect(SnapshotManager.shouldSnapshot('bash', { command: '' })).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('bash with no input returns false', () => {
|
|
157
|
+
expect(SnapshotManager.shouldSnapshot('bash')).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('terraform returns false (not a file-modifying tool)', () => {
|
|
161
|
+
expect(SnapshotManager.shouldSnapshot('terraform')).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('kubectl returns false', () => {
|
|
165
|
+
expect(SnapshotManager.shouldSnapshot('kubectl')).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ===========================================================================
|
|
170
|
+
// Git-based snapshot capture and restore
|
|
171
|
+
// ===========================================================================
|
|
172
|
+
|
|
173
|
+
describe('Git-based snapshots', () => {
|
|
174
|
+
let gitDir: string;
|
|
175
|
+
|
|
176
|
+
beforeEach(() => {
|
|
177
|
+
gitDir = createTempGitRepo();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
afterEach(() => {
|
|
181
|
+
removeTempDir(gitDir);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('captureSnapshot creates a snapshot in a git project', async () => {
|
|
185
|
+
const manager = new SnapshotManager({ projectDir: gitDir });
|
|
186
|
+
const snap = await manager.captureSnapshot(snapshotParams('edit_file: file.txt'));
|
|
187
|
+
|
|
188
|
+
expect(snap.id).toBeTruthy();
|
|
189
|
+
expect(snap.treeHash).toBeTruthy();
|
|
190
|
+
expect(snap.treeHash.length).toBeGreaterThan(0);
|
|
191
|
+
expect(snap.isGitProject).toBe(true);
|
|
192
|
+
expect(snap.sessionId).toBe('test-session');
|
|
193
|
+
expect(snap.description).toBe('edit_file: file.txt');
|
|
194
|
+
expect(snap.timestamp).toBeInstanceOf(Date);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('restoreSnapshot restores files in a git project', async () => {
|
|
198
|
+
const manager = new SnapshotManager({ projectDir: gitDir });
|
|
199
|
+
const filePath = path.join(gitDir, 'file.txt');
|
|
200
|
+
|
|
201
|
+
// Capture state before modification
|
|
202
|
+
const before = await manager.captureSnapshot(snapshotParams('before edit'));
|
|
203
|
+
|
|
204
|
+
// Modify the file
|
|
205
|
+
fs.writeFileSync(filePath, 'modified content');
|
|
206
|
+
execSync('git add -A', { cwd: gitDir, stdio: 'pipe' });
|
|
207
|
+
|
|
208
|
+
// Capture state after modification
|
|
209
|
+
await manager.captureSnapshot(snapshotParams('after edit'));
|
|
210
|
+
|
|
211
|
+
// Verify file was modified
|
|
212
|
+
expect(fs.readFileSync(filePath, 'utf-8')).toBe('modified content');
|
|
213
|
+
|
|
214
|
+
// Restore to the before-edit state
|
|
215
|
+
const result = await manager.restoreSnapshot(before.id);
|
|
216
|
+
expect(result.restored).toBe(true);
|
|
217
|
+
expect(result.description).toContain('before edit');
|
|
218
|
+
expect(fs.readFileSync(filePath, 'utf-8')).toBe('original');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('restoreSnapshot returns false for unknown snapshot id', async () => {
|
|
222
|
+
const manager = new SnapshotManager({ projectDir: gitDir });
|
|
223
|
+
const result = await manager.restoreSnapshot('nonexistent-id');
|
|
224
|
+
expect(result.restored).toBe(false);
|
|
225
|
+
expect(result.description).toContain('not found');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('undo reverts the last change', async () => {
|
|
229
|
+
const manager = new SnapshotManager({ projectDir: gitDir });
|
|
230
|
+
const filePath = path.join(gitDir, 'file.txt');
|
|
231
|
+
|
|
232
|
+
// Capture initial state
|
|
233
|
+
await manager.captureSnapshot(snapshotParams('initial'));
|
|
234
|
+
|
|
235
|
+
// Modify and capture
|
|
236
|
+
fs.writeFileSync(filePath, 'changed');
|
|
237
|
+
execSync('git add -A', { cwd: gitDir, stdio: 'pipe' });
|
|
238
|
+
await manager.captureSnapshot(snapshotParams('edit_file: file.txt'));
|
|
239
|
+
|
|
240
|
+
expect(fs.readFileSync(filePath, 'utf-8')).toBe('changed');
|
|
241
|
+
|
|
242
|
+
// Undo
|
|
243
|
+
const result = await manager.undo();
|
|
244
|
+
expect(result.success).toBe(true);
|
|
245
|
+
expect(result.description).toContain('Undone');
|
|
246
|
+
expect(fs.readFileSync(filePath, 'utf-8')).toBe('original');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('redo re-applies undone change', async () => {
|
|
250
|
+
const manager = new SnapshotManager({ projectDir: gitDir });
|
|
251
|
+
const filePath = path.join(gitDir, 'file.txt');
|
|
252
|
+
|
|
253
|
+
// Capture initial state
|
|
254
|
+
await manager.captureSnapshot(snapshotParams('initial'));
|
|
255
|
+
|
|
256
|
+
// Modify and capture
|
|
257
|
+
fs.writeFileSync(filePath, 'changed');
|
|
258
|
+
execSync('git add -A', { cwd: gitDir, stdio: 'pipe' });
|
|
259
|
+
await manager.captureSnapshot(snapshotParams('edit_file: file.txt'));
|
|
260
|
+
|
|
261
|
+
// Undo
|
|
262
|
+
await manager.undo();
|
|
263
|
+
expect(fs.readFileSync(filePath, 'utf-8')).toBe('original');
|
|
264
|
+
|
|
265
|
+
// Redo
|
|
266
|
+
const result = await manager.redo();
|
|
267
|
+
expect(result.success).toBe(true);
|
|
268
|
+
expect(result.description).toContain('Redone');
|
|
269
|
+
expect(fs.readFileSync(filePath, 'utf-8')).toBe('changed');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('undo with only one snapshot returns failure', async () => {
|
|
273
|
+
const manager = new SnapshotManager({ projectDir: gitDir });
|
|
274
|
+
await manager.captureSnapshot(snapshotParams('only one'));
|
|
275
|
+
const result = await manager.undo();
|
|
276
|
+
expect(result.success).toBe(false);
|
|
277
|
+
expect(result.description).toContain('Nothing to undo');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('redo with empty redo stack returns failure', async () => {
|
|
281
|
+
const manager = new SnapshotManager({ projectDir: gitDir });
|
|
282
|
+
const result = await manager.redo();
|
|
283
|
+
expect(result.success).toBe(false);
|
|
284
|
+
expect(result.description).toContain('Nothing to redo');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('new capture clears the redo stack', async () => {
|
|
288
|
+
const manager = new SnapshotManager({ projectDir: gitDir });
|
|
289
|
+
const filePath = path.join(gitDir, 'file.txt');
|
|
290
|
+
|
|
291
|
+
// initial -> edit -> undo -> new capture -> redo should fail
|
|
292
|
+
await manager.captureSnapshot(snapshotParams('initial'));
|
|
293
|
+
|
|
294
|
+
fs.writeFileSync(filePath, 'v2');
|
|
295
|
+
execSync('git add -A', { cwd: gitDir, stdio: 'pipe' });
|
|
296
|
+
await manager.captureSnapshot(snapshotParams('v2'));
|
|
297
|
+
|
|
298
|
+
await manager.undo();
|
|
299
|
+
|
|
300
|
+
// Now capture a new state (this should clear the redo stack)
|
|
301
|
+
fs.writeFileSync(filePath, 'v3');
|
|
302
|
+
execSync('git add -A', { cwd: gitDir, stdio: 'pipe' });
|
|
303
|
+
await manager.captureSnapshot(snapshotParams('v3'));
|
|
304
|
+
|
|
305
|
+
// Redo should fail since the stack was cleared
|
|
306
|
+
const result = await manager.redo();
|
|
307
|
+
expect(result.success).toBe(false);
|
|
308
|
+
expect(result.description).toContain('Nothing to redo');
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ===========================================================================
|
|
313
|
+
// getHistory and count
|
|
314
|
+
// ===========================================================================
|
|
315
|
+
|
|
316
|
+
describe('getHistory and count', () => {
|
|
317
|
+
let gitDir: string;
|
|
318
|
+
|
|
319
|
+
beforeEach(() => {
|
|
320
|
+
gitDir = createTempGitRepo();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
afterEach(() => {
|
|
324
|
+
removeTempDir(gitDir);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('getHistory returns all snapshots', async () => {
|
|
328
|
+
const manager = new SnapshotManager({ projectDir: gitDir });
|
|
329
|
+
|
|
330
|
+
await manager.captureSnapshot(snapshotParams('first'));
|
|
331
|
+
await manager.captureSnapshot(snapshotParams('second'));
|
|
332
|
+
await manager.captureSnapshot(snapshotParams('third'));
|
|
333
|
+
|
|
334
|
+
const history = manager.getHistory();
|
|
335
|
+
expect(history).toHaveLength(3);
|
|
336
|
+
expect(history[0].description).toBe('first');
|
|
337
|
+
expect(history[1].description).toBe('second');
|
|
338
|
+
expect(history[2].description).toBe('third');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('getHistory filters by sessionId', async () => {
|
|
342
|
+
const manager = new SnapshotManager({ projectDir: gitDir });
|
|
343
|
+
|
|
344
|
+
await manager.captureSnapshot({
|
|
345
|
+
sessionId: 'session-a',
|
|
346
|
+
messageId: 'msg-1',
|
|
347
|
+
toolCallId: 'tc-1',
|
|
348
|
+
description: 'a-first',
|
|
349
|
+
});
|
|
350
|
+
await manager.captureSnapshot({
|
|
351
|
+
sessionId: 'session-b',
|
|
352
|
+
messageId: 'msg-2',
|
|
353
|
+
toolCallId: 'tc-2',
|
|
354
|
+
description: 'b-first',
|
|
355
|
+
});
|
|
356
|
+
await manager.captureSnapshot({
|
|
357
|
+
sessionId: 'session-a',
|
|
358
|
+
messageId: 'msg-3',
|
|
359
|
+
toolCallId: 'tc-3',
|
|
360
|
+
description: 'a-second',
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const sessionA = manager.getHistory('session-a');
|
|
364
|
+
expect(sessionA).toHaveLength(2);
|
|
365
|
+
expect(sessionA[0].description).toBe('a-first');
|
|
366
|
+
expect(sessionA[1].description).toBe('a-second');
|
|
367
|
+
|
|
368
|
+
const sessionB = manager.getHistory('session-b');
|
|
369
|
+
expect(sessionB).toHaveLength(1);
|
|
370
|
+
expect(sessionB[0].description).toBe('b-first');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('count getter returns correct count', async () => {
|
|
374
|
+
const manager = new SnapshotManager({ projectDir: gitDir });
|
|
375
|
+
|
|
376
|
+
expect(manager.count).toBe(0);
|
|
377
|
+
|
|
378
|
+
await manager.captureSnapshot(snapshotParams('first'));
|
|
379
|
+
expect(manager.count).toBe(1);
|
|
380
|
+
|
|
381
|
+
await manager.captureSnapshot(snapshotParams('second'));
|
|
382
|
+
expect(manager.count).toBe(2);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('count reflects undo operations', async () => {
|
|
386
|
+
const manager = new SnapshotManager({ projectDir: gitDir });
|
|
387
|
+
|
|
388
|
+
await manager.captureSnapshot(snapshotParams('first'));
|
|
389
|
+
await manager.captureSnapshot(snapshotParams('second'));
|
|
390
|
+
expect(manager.count).toBe(2);
|
|
391
|
+
|
|
392
|
+
await manager.undo();
|
|
393
|
+
expect(manager.count).toBe(1);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// ===========================================================================
|
|
398
|
+
// cleanup
|
|
399
|
+
// ===========================================================================
|
|
400
|
+
|
|
401
|
+
describe('cleanup', () => {
|
|
402
|
+
let gitDir: string;
|
|
403
|
+
|
|
404
|
+
beforeEach(() => {
|
|
405
|
+
gitDir = createTempGitRepo();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
afterEach(() => {
|
|
409
|
+
removeTempDir(gitDir);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test('removes old snapshots exceeding maxSnapshots', async () => {
|
|
413
|
+
const manager = new SnapshotManager({
|
|
414
|
+
projectDir: gitDir,
|
|
415
|
+
maxSnapshots: 2,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
await manager.captureSnapshot(snapshotParams('first'));
|
|
419
|
+
await manager.captureSnapshot(snapshotParams('second'));
|
|
420
|
+
await manager.captureSnapshot(snapshotParams('third'));
|
|
421
|
+
await manager.captureSnapshot(snapshotParams('fourth'));
|
|
422
|
+
|
|
423
|
+
expect(manager.count).toBe(4);
|
|
424
|
+
|
|
425
|
+
const removed = await manager.cleanup();
|
|
426
|
+
expect(removed).toBe(2);
|
|
427
|
+
expect(manager.count).toBe(2);
|
|
428
|
+
|
|
429
|
+
// The remaining snapshots should be the most recent ones
|
|
430
|
+
const history = manager.getHistory();
|
|
431
|
+
expect(history[0].description).toBe('third');
|
|
432
|
+
expect(history[1].description).toBe('fourth');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test('cleanup returns 0 when nothing to clean', async () => {
|
|
436
|
+
const manager = new SnapshotManager({
|
|
437
|
+
projectDir: gitDir,
|
|
438
|
+
maxSnapshots: 100,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
await manager.captureSnapshot(snapshotParams('first'));
|
|
442
|
+
const removed = await manager.cleanup();
|
|
443
|
+
expect(removed).toBe(0);
|
|
444
|
+
expect(manager.count).toBe(1);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// ===========================================================================
|
|
449
|
+
// Non-git (filesystem) snapshots
|
|
450
|
+
// ===========================================================================
|
|
451
|
+
|
|
452
|
+
describe('Non-git snapshots', () => {
|
|
453
|
+
let nonGitDir: string;
|
|
454
|
+
|
|
455
|
+
beforeEach(() => {
|
|
456
|
+
nonGitDir = createTempDir();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
afterEach(() => {
|
|
460
|
+
removeTempDir(nonGitDir);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test('capture creates a snapshot directory for non-git project', async () => {
|
|
464
|
+
const filePath = path.join(nonGitDir, 'app.js');
|
|
465
|
+
fs.writeFileSync(filePath, 'console.log("hello");');
|
|
466
|
+
|
|
467
|
+
const manager = new SnapshotManager({ projectDir: nonGitDir });
|
|
468
|
+
const snap = await manager.captureSnapshot(snapshotParams('write_file: app.js'));
|
|
469
|
+
|
|
470
|
+
expect(snap.isGitProject).toBe(false);
|
|
471
|
+
expect(snap.treeHash).toBe('');
|
|
472
|
+
expect(snap.id).toBeTruthy();
|
|
473
|
+
|
|
474
|
+
// Verify the snapshot directory was created
|
|
475
|
+
const snapDir = path.join(nonGitDir, '.nimbus', 'snapshots', snap.id);
|
|
476
|
+
expect(fs.existsSync(snapDir)).toBe(true);
|
|
477
|
+
|
|
478
|
+
// Verify the file was copied
|
|
479
|
+
expect(fs.existsSync(path.join(snapDir, 'app.js'))).toBe(true);
|
|
480
|
+
expect(fs.readFileSync(path.join(snapDir, 'app.js'), 'utf-8')).toBe('console.log("hello");');
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test('restore recovers files for non-git project', async () => {
|
|
484
|
+
const filePath = path.join(nonGitDir, 'data.txt');
|
|
485
|
+
fs.writeFileSync(filePath, 'version 1');
|
|
486
|
+
|
|
487
|
+
const manager = new SnapshotManager({ projectDir: nonGitDir });
|
|
488
|
+
|
|
489
|
+
// Capture v1
|
|
490
|
+
const snap1 = await manager.captureSnapshot(snapshotParams('v1'));
|
|
491
|
+
|
|
492
|
+
// Modify to v2
|
|
493
|
+
fs.writeFileSync(filePath, 'version 2');
|
|
494
|
+
await manager.captureSnapshot(snapshotParams('v2'));
|
|
495
|
+
|
|
496
|
+
expect(fs.readFileSync(filePath, 'utf-8')).toBe('version 2');
|
|
497
|
+
|
|
498
|
+
// Restore v1
|
|
499
|
+
const result = await manager.restoreSnapshot(snap1.id);
|
|
500
|
+
expect(result.restored).toBe(true);
|
|
501
|
+
expect(fs.readFileSync(filePath, 'utf-8')).toBe('version 1');
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test('undo works for non-git project', async () => {
|
|
505
|
+
const filePath = path.join(nonGitDir, 'code.py');
|
|
506
|
+
fs.writeFileSync(filePath, 'print("v1")');
|
|
507
|
+
|
|
508
|
+
const manager = new SnapshotManager({ projectDir: nonGitDir });
|
|
509
|
+
|
|
510
|
+
await manager.captureSnapshot(snapshotParams('initial'));
|
|
511
|
+
|
|
512
|
+
fs.writeFileSync(filePath, 'print("v2")');
|
|
513
|
+
await manager.captureSnapshot(snapshotParams('edit'));
|
|
514
|
+
|
|
515
|
+
expect(fs.readFileSync(filePath, 'utf-8')).toBe('print("v2")');
|
|
516
|
+
|
|
517
|
+
const result = await manager.undo();
|
|
518
|
+
expect(result.success).toBe(true);
|
|
519
|
+
expect(fs.readFileSync(filePath, 'utf-8')).toBe('print("v1")');
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test('redo works for non-git project', async () => {
|
|
523
|
+
const filePath = path.join(nonGitDir, 'code.py');
|
|
524
|
+
fs.writeFileSync(filePath, 'print("v1")');
|
|
525
|
+
|
|
526
|
+
const manager = new SnapshotManager({ projectDir: nonGitDir });
|
|
527
|
+
|
|
528
|
+
await manager.captureSnapshot(snapshotParams('initial'));
|
|
529
|
+
|
|
530
|
+
fs.writeFileSync(filePath, 'print("v2")');
|
|
531
|
+
await manager.captureSnapshot(snapshotParams('edit'));
|
|
532
|
+
|
|
533
|
+
await manager.undo();
|
|
534
|
+
expect(fs.readFileSync(filePath, 'utf-8')).toBe('print("v1")');
|
|
535
|
+
|
|
536
|
+
const result = await manager.redo();
|
|
537
|
+
expect(result.success).toBe(true);
|
|
538
|
+
expect(fs.readFileSync(filePath, 'utf-8')).toBe('print("v2")');
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test('cleanup removes snapshot directories for non-git project', async () => {
|
|
542
|
+
fs.writeFileSync(path.join(nonGitDir, 'f.txt'), 'data');
|
|
543
|
+
|
|
544
|
+
const manager = new SnapshotManager({
|
|
545
|
+
projectDir: nonGitDir,
|
|
546
|
+
maxSnapshots: 1,
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
const snap1 = await manager.captureSnapshot(snapshotParams('first'));
|
|
550
|
+
await manager.captureSnapshot(snapshotParams('second'));
|
|
551
|
+
await manager.captureSnapshot(snapshotParams('third'));
|
|
552
|
+
|
|
553
|
+
// Before cleanup, all snapshot dirs exist
|
|
554
|
+
const snap1Dir = path.join(nonGitDir, '.nimbus', 'snapshots', snap1.id);
|
|
555
|
+
expect(fs.existsSync(snap1Dir)).toBe(true);
|
|
556
|
+
|
|
557
|
+
const removed = await manager.cleanup();
|
|
558
|
+
expect(removed).toBe(2);
|
|
559
|
+
expect(manager.count).toBe(1);
|
|
560
|
+
|
|
561
|
+
// The oldest snapshot directory should be cleaned up
|
|
562
|
+
expect(fs.existsSync(snap1Dir)).toBe(false);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test('non-git snapshot skips node_modules and .git directories', async () => {
|
|
566
|
+
// Create directories that should be skipped
|
|
567
|
+
fs.mkdirSync(path.join(nonGitDir, 'node_modules'), { recursive: true });
|
|
568
|
+
fs.writeFileSync(path.join(nonGitDir, 'node_modules', 'pkg.json'), '{}');
|
|
569
|
+
fs.mkdirSync(path.join(nonGitDir, 'src'), { recursive: true });
|
|
570
|
+
fs.writeFileSync(path.join(nonGitDir, 'src', 'index.ts'), 'export {};');
|
|
571
|
+
|
|
572
|
+
const manager = new SnapshotManager({ projectDir: nonGitDir });
|
|
573
|
+
const snap = await manager.captureSnapshot(snapshotParams('with-node-modules'));
|
|
574
|
+
|
|
575
|
+
const snapDir = path.join(nonGitDir, '.nimbus', 'snapshots', snap.id);
|
|
576
|
+
// node_modules should NOT be copied
|
|
577
|
+
expect(fs.existsSync(path.join(snapDir, 'node_modules'))).toBe(false);
|
|
578
|
+
// src should be copied
|
|
579
|
+
expect(fs.existsSync(path.join(snapDir, 'src', 'index.ts'))).toBe(true);
|
|
580
|
+
});
|
|
581
|
+
});
|