@celilo/cli 0.1.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/README.md +1566 -0
- package/bin/celilo +16 -0
- package/drizzle/0000_complex_puma.sql +179 -0
- package/drizzle/0001_dizzy_wolfpack.sql +2 -0
- package/drizzle/0002_web_routes.sql +16 -0
- package/drizzle/0003_backup_storage.sql +32 -0
- package/drizzle/meta/0000_snapshot.json +1151 -0
- package/drizzle/meta/0001_snapshot.json +1167 -0
- package/drizzle/meta/0002_snapshot.json +1257 -0
- package/drizzle/meta/_journal.json +27 -0
- package/package.json +64 -0
- package/schemas/system_config.json +106 -0
- package/src/__integration__/container-services-cli.integration.test.ts +246 -0
- package/src/ansible/dependencies.test.ts +309 -0
- package/src/ansible/dependencies.ts +896 -0
- package/src/ansible/inventory.test.ts +463 -0
- package/src/ansible/inventory.ts +445 -0
- package/src/ansible/secrets.ts +222 -0
- package/src/ansible/validation.test.ts +92 -0
- package/src/ansible/validation.ts +272 -0
- package/src/api-clients/digitalocean.ts +94 -0
- package/src/api-clients/proxmox.ts +655 -0
- package/src/capabilities/logging-wrapper.test.ts +217 -0
- package/src/capabilities/lookup.test.ts +149 -0
- package/src/capabilities/lookup.ts +89 -0
- package/src/capabilities/public-web-helpers.test.ts +198 -0
- package/src/capabilities/public-web-publish.test.ts +458 -0
- package/src/capabilities/registration.test.ts +395 -0
- package/src/capabilities/registration.ts +200 -0
- package/src/capabilities/route-validation.test.ts +121 -0
- package/src/capabilities/route-validation.ts +96 -0
- package/src/capabilities/secret-ref.test.ts +313 -0
- package/src/capabilities/secret-validation.ts +157 -0
- package/src/capabilities/secrets.test.ts +750 -0
- package/src/capabilities/secrets.ts +244 -0
- package/src/capabilities/validation.test.ts +613 -0
- package/src/capabilities/validation.ts +160 -0
- package/src/capabilities/well-known.test.ts +238 -0
- package/src/capabilities/well-known.ts +222 -0
- package/src/cli/cli.test.ts +654 -0
- package/src/cli/command-registry.ts +742 -0
- package/src/cli/command-tree-parser.test.ts +180 -0
- package/src/cli/command-tree-parser.ts +193 -0
- package/src/cli/commands/backup-create.ts +137 -0
- package/src/cli/commands/backup-delete.ts +74 -0
- package/src/cli/commands/backup-import.ts +97 -0
- package/src/cli/commands/backup-list.ts +132 -0
- package/src/cli/commands/backup-name.ts +73 -0
- package/src/cli/commands/backup-prune.ts +98 -0
- package/src/cli/commands/backup-restore.ts +122 -0
- package/src/cli/commands/capability-info.ts +121 -0
- package/src/cli/commands/capability-list.ts +47 -0
- package/src/cli/commands/completion.ts +87 -0
- package/src/cli/commands/hook-run.ts +176 -0
- package/src/cli/commands/ipam.ts +607 -0
- package/src/cli/commands/machine-add.ts +235 -0
- package/src/cli/commands/machine-earmark.ts +82 -0
- package/src/cli/commands/machine-list.ts +77 -0
- package/src/cli/commands/machine-remove.ts +90 -0
- package/src/cli/commands/machine-status.ts +131 -0
- package/src/cli/commands/module-audit.ts +51 -0
- package/src/cli/commands/module-build.ts +60 -0
- package/src/cli/commands/module-config.ts +170 -0
- package/src/cli/commands/module-deploy.ts +71 -0
- package/src/cli/commands/module-generate.ts +236 -0
- package/src/cli/commands/module-health.ts +108 -0
- package/src/cli/commands/module-import.ts +80 -0
- package/src/cli/commands/module-list.ts +43 -0
- package/src/cli/commands/module-logs.ts +73 -0
- package/src/cli/commands/module-remove.ts +162 -0
- package/src/cli/commands/module-show.ts +208 -0
- package/src/cli/commands/module-status.ts +131 -0
- package/src/cli/commands/module-types.ts +189 -0
- package/src/cli/commands/module-upgrade.ts +192 -0
- package/src/cli/commands/package.ts +68 -0
- package/src/cli/commands/secret-list.ts +99 -0
- package/src/cli/commands/secret-set.ts +134 -0
- package/src/cli/commands/service-add-digitalocean.ts +133 -0
- package/src/cli/commands/service-add-proxmox.ts +342 -0
- package/src/cli/commands/service-config-get.ts +83 -0
- package/src/cli/commands/service-config-set.ts +145 -0
- package/src/cli/commands/service-list.ts +74 -0
- package/src/cli/commands/service-reconfigure.ts +230 -0
- package/src/cli/commands/service-remove.ts +103 -0
- package/src/cli/commands/service-verify.ts +240 -0
- package/src/cli/commands/status.ts +216 -0
- package/src/cli/commands/storage-add-local.ts +106 -0
- package/src/cli/commands/storage-add-s3.ts +114 -0
- package/src/cli/commands/storage-list.ts +72 -0
- package/src/cli/commands/storage-remove.ts +54 -0
- package/src/cli/commands/storage-set-default.ts +44 -0
- package/src/cli/commands/storage-verify.ts +54 -0
- package/src/cli/commands/system-config.ts +168 -0
- package/src/cli/commands/system-init.ts +314 -0
- package/src/cli/commands/system-secret-get.ts +98 -0
- package/src/cli/commands/system-secret-set.ts +76 -0
- package/src/cli/commands/system-vault-password.ts +34 -0
- package/src/cli/completion.test.ts +37 -0
- package/src/cli/completion.ts +482 -0
- package/src/cli/fuel-gauge.test.ts +208 -0
- package/src/cli/fuel-gauge.ts +405 -0
- package/src/cli/generate-zsh-completion.test.ts +95 -0
- package/src/cli/generate-zsh-completion.ts +497 -0
- package/src/cli/index.ts +1583 -0
- package/src/cli/interactive-config.test.ts +201 -0
- package/src/cli/interactive-config.ts +62 -0
- package/src/cli/parser.test.ts +227 -0
- package/src/cli/parser.ts +244 -0
- package/src/cli/prompts.test.ts +33 -0
- package/src/cli/prompts.ts +121 -0
- package/src/cli/types.ts +38 -0
- package/src/cli/validators.test.ts +235 -0
- package/src/cli/validators.ts +188 -0
- package/src/config/env.ts +41 -0
- package/src/config/paths.test.ts +172 -0
- package/src/config/paths.ts +108 -0
- package/src/db/client.ts +190 -0
- package/src/db/migrate.ts +30 -0
- package/src/db/schema.test.ts +221 -0
- package/src/db/schema.ts +434 -0
- package/src/hooks/capability-loader-firewall.test.ts +246 -0
- package/src/hooks/capability-loader.test.ts +100 -0
- package/src/hooks/capability-loader.ts +520 -0
- package/src/hooks/define-hook.test.ts +488 -0
- package/src/hooks/executor.test.ts +462 -0
- package/src/hooks/executor.ts +469 -0
- package/src/hooks/logger.test.ts +54 -0
- package/src/hooks/logger.ts +95 -0
- package/src/hooks/test-fixtures/failing-hook.ts +13 -0
- package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
- package/src/hooks/test-fixtures/success-hook.ts +20 -0
- package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
- package/src/hooks/test-fixtures/void-hook.ts +13 -0
- package/src/hooks/types.ts +89 -0
- package/src/infrastructure/property-extractor.test.ts +194 -0
- package/src/infrastructure/property-extractor.ts +151 -0
- package/src/ipam/allocator.test.ts +442 -0
- package/src/ipam/allocator.ts +369 -0
- package/src/ipam/auto-allocator.test.ts +247 -0
- package/src/ipam/auto-allocator.ts +270 -0
- package/src/ipam/subnet-parser.test.ts +107 -0
- package/src/ipam/subnet-parser.ts +136 -0
- package/src/manifest/contracts/index.ts +61 -0
- package/src/manifest/contracts/v1.ts +118 -0
- package/src/manifest/json-schema-roundtrip.test.ts +99 -0
- package/src/manifest/schema.ts +367 -0
- package/src/manifest/template-validator.test.ts +231 -0
- package/src/manifest/template-validator.ts +322 -0
- package/src/manifest/validate.test.ts +1180 -0
- package/src/manifest/validate.ts +415 -0
- package/src/module/import.test.ts +355 -0
- package/src/module/import.ts +676 -0
- package/src/module/packaging/audit.ts +169 -0
- package/src/module/packaging/build.ts +228 -0
- package/src/module/packaging/checksum.ts +41 -0
- package/src/module/packaging/extract.ts +234 -0
- package/src/module/packaging/signature.ts +47 -0
- package/src/secrets/encryption.test.ts +284 -0
- package/src/secrets/encryption.ts +162 -0
- package/src/secrets/generators.test.ts +112 -0
- package/src/secrets/generators.ts +127 -0
- package/src/secrets/master-key.test.ts +159 -0
- package/src/secrets/master-key.ts +114 -0
- package/src/secrets/storage.test.ts +115 -0
- package/src/secrets/storage.ts +106 -0
- package/src/secrets/vault.test.ts +35 -0
- package/src/secrets/vault.ts +42 -0
- package/src/services/backup-create.ts +532 -0
- package/src/services/backup-metadata.ts +198 -0
- package/src/services/backup-restore.ts +229 -0
- package/src/services/backup-retention.ts +84 -0
- package/src/services/backup-storage.ts +281 -0
- package/src/services/build-stream.test.ts +122 -0
- package/src/services/build-stream.ts +201 -0
- package/src/services/config-interview.ts +694 -0
- package/src/services/container-service.test.ts +298 -0
- package/src/services/container-service.ts +401 -0
- package/src/services/cross-module-data-manager.test.ts +405 -0
- package/src/services/cross-module-data-manager.ts +412 -0
- package/src/services/deploy-ansible.ts +88 -0
- package/src/services/deploy-planner.ts +153 -0
- package/src/services/deploy-preflight.ts +274 -0
- package/src/services/deploy-ssh.ts +131 -0
- package/src/services/deploy-terraform.test.ts +55 -0
- package/src/services/deploy-terraform.ts +445 -0
- package/src/services/deploy-validation.ts +311 -0
- package/src/services/dns-auto-register.ts +211 -0
- package/src/services/health-runner.ts +184 -0
- package/src/services/infrastructure-selector.test.ts +485 -0
- package/src/services/infrastructure-selector.ts +245 -0
- package/src/services/infrastructure-variable-resolver.test.ts +751 -0
- package/src/services/infrastructure-variable-resolver.ts +234 -0
- package/src/services/machine-detector.ts +328 -0
- package/src/services/machine-pool.test.ts +405 -0
- package/src/services/machine-pool.ts +316 -0
- package/src/services/manifest-validation.ts +120 -0
- package/src/services/module-build.test.ts +290 -0
- package/src/services/module-build.ts +431 -0
- package/src/services/module-config.test.ts +237 -0
- package/src/services/module-config.ts +298 -0
- package/src/services/module-deploy.ts +862 -0
- package/src/services/module-types-drift.test.ts +73 -0
- package/src/services/module-types-generator.test.ts +288 -0
- package/src/services/module-types-generator.ts +189 -0
- package/src/services/proxmox-state-recovery.ts +140 -0
- package/src/services/schema-validation.ts +155 -0
- package/src/services/secret-schema-loader.test.ts +311 -0
- package/src/services/secret-schema-loader.ts +239 -0
- package/src/services/ssh-key-manager.test.ts +283 -0
- package/src/services/ssh-key-manager.ts +193 -0
- package/src/services/storage-providers/local.ts +105 -0
- package/src/services/storage-providers/s3.ts +182 -0
- package/src/services/storage-providers/types.ts +24 -0
- package/src/services/system-config-schema-types.ts +25 -0
- package/src/services/system-config-validator.test.ts +160 -0
- package/src/services/system-config-validator.ts +74 -0
- package/src/services/system-init.test.ts +153 -0
- package/src/services/system-init.ts +253 -0
- package/src/services/terraform-safety.ts +174 -0
- package/src/services/zone-detector.test.ts +110 -0
- package/src/services/zone-detector.ts +102 -0
- package/src/services/zone-policy.test.ts +97 -0
- package/src/services/zone-policy.ts +126 -0
- package/src/templates/generator.test.ts +645 -0
- package/src/templates/generator.ts +1119 -0
- package/src/templates/types.ts +62 -0
- package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
- package/src/test-utils/cli-context-interactive.test.ts +152 -0
- package/src/test-utils/cli-context-server.test.ts +66 -0
- package/src/test-utils/cli-context.test.ts +273 -0
- package/src/test-utils/cli-context.ts +677 -0
- package/src/test-utils/cli-result.test.ts +282 -0
- package/src/test-utils/cli-result.ts +241 -0
- package/src/test-utils/cli.ts +55 -0
- package/src/test-utils/completion-harness.test.ts +126 -0
- package/src/test-utils/completion-harness.ts +82 -0
- package/src/test-utils/database.test.ts +182 -0
- package/src/test-utils/database.ts +126 -0
- package/src/test-utils/filesystem.test.ts +208 -0
- package/src/test-utils/filesystem.ts +142 -0
- package/src/test-utils/fixtures.test.ts +123 -0
- package/src/test-utils/fixtures.ts +160 -0
- package/src/test-utils/golden-diff.ts +197 -0
- package/src/test-utils/index.ts +77 -0
- package/src/test-utils/integration.ts +81 -0
- package/src/test-utils/module-fixtures.ts +468 -0
- package/src/test-utils/modules.test.ts +144 -0
- package/src/test-utils/modules.ts +183 -0
- package/src/test-utils/setup-test-db.ts +90 -0
- package/src/test-utils/value-extractor.test.ts +231 -0
- package/src/test-utils/value-extractor.ts +228 -0
- package/src/types/infrastructure.ts +157 -0
- package/src/utils/shell.test.ts +365 -0
- package/src/utils/shell.ts +159 -0
- package/src/validation/schemas.ts +166 -0
- package/src/variables/ansible-resolver.test.ts +142 -0
- package/src/variables/ansible-resolver.ts +69 -0
- package/src/variables/capability-self-ref.test.ts +220 -0
- package/src/variables/context.test.ts +1265 -0
- package/src/variables/context.ts +624 -0
- package/src/variables/declarative-derivation.test.ts +743 -0
- package/src/variables/declarative-derivation.ts +200 -0
- package/src/variables/parser.test.ts +231 -0
- package/src/variables/parser.ts +76 -0
- package/src/variables/resolver.test.ts +458 -0
- package/src/variables/resolver.ts +282 -0
- package/src/variables/types.ts +59 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Completion Test Harness
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for testing CLI completion by invoking --get-completions directly.
|
|
5
|
+
* This bypasses shell complexity and tests the completion function itself.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { expect } from 'bun:test';
|
|
9
|
+
import { runCli } from './cli';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Test harness for CLI completion
|
|
13
|
+
*/
|
|
14
|
+
export class CompletionHarness {
|
|
15
|
+
constructor(private cli: string) {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get completions for a given set of words
|
|
19
|
+
*
|
|
20
|
+
* @param words - The command words (e.g., ['celilo', 'service'])
|
|
21
|
+
* @param partial - If true, complete the last word (for prefix matching). If false (default), complete after all words
|
|
22
|
+
* @returns Array of completion suggestions
|
|
23
|
+
*/
|
|
24
|
+
async getCompletions(words: string[], partial = false): Promise<string[]> {
|
|
25
|
+
// If partial=true, we're completing the last word itself (prefix matching)
|
|
26
|
+
// If partial=false, we're completing the next word after all current words
|
|
27
|
+
const currentIndex = partial && words.length > 0 ? words.length - 1 : words.length;
|
|
28
|
+
try {
|
|
29
|
+
const result = runCli(this.cli, `--get-completions ${words.join(' ')} ${currentIndex}`);
|
|
30
|
+
|
|
31
|
+
// Parse completion output (one per line)
|
|
32
|
+
return result.split('\n').filter(Boolean);
|
|
33
|
+
} catch (_error) {
|
|
34
|
+
// Completion errors return empty array
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Assert that completions include all expected values
|
|
41
|
+
*
|
|
42
|
+
* @param words - The command words
|
|
43
|
+
* @param expected - Expected completion values
|
|
44
|
+
*/
|
|
45
|
+
async expectCompletionsInclude(words: string[], expected: string[]): Promise<void> {
|
|
46
|
+
const actual = await this.getCompletions(words);
|
|
47
|
+
for (const exp of expected) {
|
|
48
|
+
expect(actual).toContain(exp);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Assert that completions exactly match expected values (order-independent)
|
|
54
|
+
*
|
|
55
|
+
* @param words - The command words
|
|
56
|
+
* @param expected - Expected completion values
|
|
57
|
+
*/
|
|
58
|
+
async expectCompletionsExact(words: string[], expected: string[]): Promise<void> {
|
|
59
|
+
const actual = await this.getCompletions(words);
|
|
60
|
+
expect(actual.sort()).toEqual(expected.sort());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Assert that no completions are returned
|
|
65
|
+
*
|
|
66
|
+
* @param words - The command words
|
|
67
|
+
*/
|
|
68
|
+
async expectNoCompletions(words: string[]): Promise<void> {
|
|
69
|
+
const actual = await this.getCompletions(words);
|
|
70
|
+
expect(actual).toEqual([]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Assert that at least one completion is returned
|
|
75
|
+
*
|
|
76
|
+
* @param words - The command words
|
|
77
|
+
*/
|
|
78
|
+
async expectSomeCompletions(words: string[]): Promise<void> {
|
|
79
|
+
const actual = await this.getCompletions(words);
|
|
80
|
+
expect(actual.length).toBeGreaterThan(0);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { modules } from '../db/schema';
|
|
4
|
+
import {
|
|
5
|
+
cleanupMultipleTestDatabases,
|
|
6
|
+
cleanupTestDatabase,
|
|
7
|
+
setupMultipleTestDatabases,
|
|
8
|
+
setupTestDatabase,
|
|
9
|
+
setupTestDatabaseFile,
|
|
10
|
+
} from './database';
|
|
11
|
+
|
|
12
|
+
describe('Database Test Utilities', () => {
|
|
13
|
+
describe('setupTestDatabase', () => {
|
|
14
|
+
test('creates in-memory database', async () => {
|
|
15
|
+
const db = await setupTestDatabase();
|
|
16
|
+
expect(db).toBeDefined();
|
|
17
|
+
await cleanupTestDatabase(db);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('applies migrations', async () => {
|
|
21
|
+
const db = await setupTestDatabase();
|
|
22
|
+
|
|
23
|
+
// Should be able to query modules table
|
|
24
|
+
const result = await db.select().from(modules);
|
|
25
|
+
expect(result).toEqual([]);
|
|
26
|
+
|
|
27
|
+
await cleanupTestDatabase(db);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('creates isolated databases', async () => {
|
|
31
|
+
const db1 = await setupTestDatabase();
|
|
32
|
+
const db2 = await setupTestDatabase();
|
|
33
|
+
|
|
34
|
+
// Insert into db1
|
|
35
|
+
await db1.insert(modules).values({
|
|
36
|
+
id: 'test-module',
|
|
37
|
+
name: 'Test Module',
|
|
38
|
+
version: '1.0.0',
|
|
39
|
+
state: 'IMPORTED',
|
|
40
|
+
manifestData: {},
|
|
41
|
+
sourcePath: '/tmp/test',
|
|
42
|
+
importedAt: new Date(),
|
|
43
|
+
updatedAt: new Date(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// db2 should be empty
|
|
47
|
+
const result1 = await db1.select().from(modules);
|
|
48
|
+
const result2 = await db2.select().from(modules);
|
|
49
|
+
|
|
50
|
+
expect(result1).toHaveLength(1);
|
|
51
|
+
expect(result2).toHaveLength(0);
|
|
52
|
+
|
|
53
|
+
await cleanupTestDatabase(db1);
|
|
54
|
+
await cleanupTestDatabase(db2);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('setupTestDatabaseFile', () => {
|
|
59
|
+
test('creates file-based database', async () => {
|
|
60
|
+
const { db, path: dbPath, cleanup } = await setupTestDatabaseFile();
|
|
61
|
+
|
|
62
|
+
expect(db).toBeDefined();
|
|
63
|
+
expect(dbPath).toBeTruthy();
|
|
64
|
+
expect(existsSync(dbPath)).toBe(true);
|
|
65
|
+
|
|
66
|
+
await cleanup();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('applies migrations', async () => {
|
|
70
|
+
const { db, cleanup } = await setupTestDatabaseFile();
|
|
71
|
+
|
|
72
|
+
// Should be able to query modules table
|
|
73
|
+
const result = await db.select().from(modules);
|
|
74
|
+
expect(result).toEqual([]);
|
|
75
|
+
|
|
76
|
+
await cleanup();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('cleanup removes database file', async () => {
|
|
80
|
+
const { path: dbPath, cleanup } = await setupTestDatabaseFile();
|
|
81
|
+
|
|
82
|
+
expect(existsSync(dbPath)).toBe(true);
|
|
83
|
+
|
|
84
|
+
await cleanup();
|
|
85
|
+
|
|
86
|
+
// Database file should be gone
|
|
87
|
+
expect(existsSync(dbPath)).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('database path can be used by CLI', async () => {
|
|
91
|
+
const { db, path: dbPath, cleanup } = await setupTestDatabaseFile();
|
|
92
|
+
|
|
93
|
+
// Path should be a valid file path
|
|
94
|
+
expect(dbPath).toMatch(/\/celilo-test-.*\/test\.db$/);
|
|
95
|
+
|
|
96
|
+
// Insert some data
|
|
97
|
+
await db.insert(modules).values({
|
|
98
|
+
id: 'cli-test',
|
|
99
|
+
name: 'CLI Test',
|
|
100
|
+
version: '1.0.0',
|
|
101
|
+
state: 'IMPORTED',
|
|
102
|
+
manifestData: {},
|
|
103
|
+
sourcePath: '/tmp/cli',
|
|
104
|
+
importedAt: new Date(),
|
|
105
|
+
updatedAt: new Date(),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Data should be persisted (can be accessed by CLI)
|
|
109
|
+
const result = await db.select().from(modules);
|
|
110
|
+
expect(result).toHaveLength(1);
|
|
111
|
+
|
|
112
|
+
await cleanup();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('setupMultipleTestDatabases', () => {
|
|
117
|
+
test('creates multiple databases', async () => {
|
|
118
|
+
const databases = await setupMultipleTestDatabases(3);
|
|
119
|
+
|
|
120
|
+
expect(databases).toHaveLength(3);
|
|
121
|
+
expect(databases[0]).toBeDefined();
|
|
122
|
+
expect(databases[1]).toBeDefined();
|
|
123
|
+
expect(databases[2]).toBeDefined();
|
|
124
|
+
|
|
125
|
+
await cleanupMultipleTestDatabases(databases);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('databases are isolated', async () => {
|
|
129
|
+
const [db1, db2, db3] = await setupMultipleTestDatabases(3);
|
|
130
|
+
|
|
131
|
+
// Insert different data in each
|
|
132
|
+
await db1.insert(modules).values({
|
|
133
|
+
id: 'module-1',
|
|
134
|
+
name: 'Module 1',
|
|
135
|
+
version: '1.0.0',
|
|
136
|
+
state: 'IMPORTED',
|
|
137
|
+
manifestData: {},
|
|
138
|
+
sourcePath: '/tmp/1',
|
|
139
|
+
importedAt: new Date(),
|
|
140
|
+
updatedAt: new Date(),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await db2.insert(modules).values({
|
|
144
|
+
id: 'module-2',
|
|
145
|
+
name: 'Module 2',
|
|
146
|
+
version: '1.0.0',
|
|
147
|
+
state: 'IMPORTED',
|
|
148
|
+
manifestData: {},
|
|
149
|
+
sourcePath: '/tmp/2',
|
|
150
|
+
importedAt: new Date(),
|
|
151
|
+
updatedAt: new Date(),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Verify isolation
|
|
155
|
+
const result1 = await db1.select().from(modules);
|
|
156
|
+
const result2 = await db2.select().from(modules);
|
|
157
|
+
const result3 = await db3.select().from(modules);
|
|
158
|
+
|
|
159
|
+
expect(result1).toHaveLength(1);
|
|
160
|
+
expect(result1[0].id).toBe('module-1');
|
|
161
|
+
|
|
162
|
+
expect(result2).toHaveLength(1);
|
|
163
|
+
expect(result2[0].id).toBe('module-2');
|
|
164
|
+
|
|
165
|
+
expect(result3).toHaveLength(0);
|
|
166
|
+
|
|
167
|
+
await cleanupMultipleTestDatabases([db1, db2, db3]);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('cleanupTestDatabase', () => {
|
|
172
|
+
test('closes database', async () => {
|
|
173
|
+
const db = await setupTestDatabase();
|
|
174
|
+
|
|
175
|
+
await cleanupTestDatabase(db);
|
|
176
|
+
|
|
177
|
+
// Database should be closed (attempting to use it should fail)
|
|
178
|
+
// Note: Testing database closure is difficult with Drizzle's lazy query builders
|
|
179
|
+
expect(db).toBeDefined();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Test Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides functions for creating and managing isolated test databases.
|
|
5
|
+
* Each test gets its own database to prevent state leakage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Database } from 'bun:sqlite';
|
|
9
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { drizzle } from 'drizzle-orm/bun-sqlite';
|
|
13
|
+
import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
|
|
14
|
+
import { type DbClient, findMigrationsFolder } from '../db/client';
|
|
15
|
+
import * as schema from '../db/schema';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create isolated test database (in-memory)
|
|
19
|
+
*
|
|
20
|
+
* Use for fast tests that don't need CLI interaction.
|
|
21
|
+
* Database is destroyed when closed.
|
|
22
|
+
*
|
|
23
|
+
* @returns In-memory SQLite database with migrations applied
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const db = await setupTestDatabase();
|
|
28
|
+
* // Use db...
|
|
29
|
+
* await cleanupTestDatabase(db);
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export async function setupTestDatabase(): Promise<DbClient> {
|
|
33
|
+
const sqlite = new Database(':memory:');
|
|
34
|
+
const db = drizzle(sqlite, { schema });
|
|
35
|
+
const migrationsFolder = findMigrationsFolder();
|
|
36
|
+
await migrate(db, { migrationsFolder });
|
|
37
|
+
return db;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create isolated test database (temporary file)
|
|
42
|
+
*
|
|
43
|
+
* Use for tests that need CLI to access database.
|
|
44
|
+
* Database is in a temporary directory that's cleaned up.
|
|
45
|
+
*
|
|
46
|
+
* @returns Object with database, path, and cleanup function
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const { db, path: dbPath, cleanup } = await setupTestDatabaseFile();
|
|
51
|
+
* // Use db and dbPath...
|
|
52
|
+
* await cleanup();
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export async function setupTestDatabaseFile(): Promise<{
|
|
56
|
+
db: DbClient;
|
|
57
|
+
path: string;
|
|
58
|
+
cleanup: () => Promise<void>;
|
|
59
|
+
}> {
|
|
60
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'celilo-test-'));
|
|
61
|
+
const dbPath = join(tempDir, 'test.db');
|
|
62
|
+
const sqlite = new Database(dbPath);
|
|
63
|
+
const db = drizzle(sqlite, { schema });
|
|
64
|
+
const migrationsFolder = findMigrationsFolder();
|
|
65
|
+
await migrate(db, { migrationsFolder });
|
|
66
|
+
|
|
67
|
+
const cleanup = async () => {
|
|
68
|
+
sqlite.close();
|
|
69
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return { db, path: dbPath, cleanup };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Clean up test database
|
|
77
|
+
*
|
|
78
|
+
* Closes the database connection.
|
|
79
|
+
* For in-memory databases, this destroys all data.
|
|
80
|
+
*
|
|
81
|
+
* @param db - Database to close
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* const db = await setupTestDatabase();
|
|
86
|
+
* // ... tests ...
|
|
87
|
+
* await cleanupTestDatabase(db);
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export async function cleanupTestDatabase(db: DbClient): Promise<void> {
|
|
91
|
+
db.$client.close();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create multiple isolated databases
|
|
96
|
+
*
|
|
97
|
+
* Useful for tests that need multiple independent databases.
|
|
98
|
+
*
|
|
99
|
+
* @param count - Number of databases to create
|
|
100
|
+
* @returns Array of in-memory databases
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* const [db1, db2] = await setupMultipleTestDatabases(2);
|
|
105
|
+
* // Use db1 and db2 independently...
|
|
106
|
+
* await cleanupMultipleTestDatabases([db1, db2]);
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export async function setupMultipleTestDatabases(count: number): Promise<DbClient[]> {
|
|
110
|
+
const databases: DbClient[] = [];
|
|
111
|
+
for (let i = 0; i < count; i++) {
|
|
112
|
+
databases.push(await setupTestDatabase());
|
|
113
|
+
}
|
|
114
|
+
return databases;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Clean up multiple test databases
|
|
119
|
+
*
|
|
120
|
+
* @param databases - Array of databases to close
|
|
121
|
+
*/
|
|
122
|
+
export async function cleanupMultipleTestDatabases(databases: DbClient[]): Promise<void> {
|
|
123
|
+
for (const db of databases) {
|
|
124
|
+
await cleanupTestDatabase(db);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import {
|
|
6
|
+
copyFixture,
|
|
7
|
+
createMultipleTempDirectories,
|
|
8
|
+
createTempDirectory,
|
|
9
|
+
createTempDirectoryWithCleanup,
|
|
10
|
+
removeMultipleTempDirectories,
|
|
11
|
+
removeTempDirectory,
|
|
12
|
+
} from './filesystem';
|
|
13
|
+
|
|
14
|
+
describe('Filesystem Test Utilities', () => {
|
|
15
|
+
const createdDirs: string[] = [];
|
|
16
|
+
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
// Cleanup any directories that weren't cleaned up in test
|
|
19
|
+
for (const dir of createdDirs) {
|
|
20
|
+
try {
|
|
21
|
+
if (existsSync(dir)) {
|
|
22
|
+
await removeTempDirectory(dir);
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Ignore cleanup errors
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
createdDirs.length = 0;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('createTempDirectory', () => {
|
|
32
|
+
test('creates unique temporary directory', async () => {
|
|
33
|
+
const dir = await createTempDirectory();
|
|
34
|
+
createdDirs.push(dir);
|
|
35
|
+
|
|
36
|
+
expect(existsSync(dir)).toBe(true);
|
|
37
|
+
expect(dir).toMatch(/celilo-test-/);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('creates different directories on multiple calls', async () => {
|
|
41
|
+
const dir1 = await createTempDirectory();
|
|
42
|
+
const dir2 = await createTempDirectory();
|
|
43
|
+
createdDirs.push(dir1, dir2);
|
|
44
|
+
|
|
45
|
+
expect(dir1).not.toBe(dir2);
|
|
46
|
+
expect(existsSync(dir1)).toBe(true);
|
|
47
|
+
expect(existsSync(dir2)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('directory is writable', async () => {
|
|
51
|
+
const dir = await createTempDirectory();
|
|
52
|
+
createdDirs.push(dir);
|
|
53
|
+
|
|
54
|
+
const testFile = join(dir, 'test.txt');
|
|
55
|
+
await writeFile(testFile, 'test content');
|
|
56
|
+
|
|
57
|
+
expect(existsSync(testFile)).toBe(true);
|
|
58
|
+
const content = await readFile(testFile, 'utf-8');
|
|
59
|
+
expect(content).toBe('test content');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('removeTempDirectory', () => {
|
|
64
|
+
test('removes empty directory', async () => {
|
|
65
|
+
const dir = await createTempDirectory();
|
|
66
|
+
expect(existsSync(dir)).toBe(true);
|
|
67
|
+
|
|
68
|
+
await removeTempDirectory(dir);
|
|
69
|
+
expect(existsSync(dir)).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('removes directory with files', async () => {
|
|
73
|
+
const dir = await createTempDirectory();
|
|
74
|
+
await writeFile(join(dir, 'test.txt'), 'content');
|
|
75
|
+
|
|
76
|
+
expect(existsSync(dir)).toBe(true);
|
|
77
|
+
|
|
78
|
+
await removeTempDirectory(dir);
|
|
79
|
+
expect(existsSync(dir)).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('removes directory with subdirectories', async () => {
|
|
83
|
+
const dir = await createTempDirectory();
|
|
84
|
+
const subdir = join(dir, 'subdir');
|
|
85
|
+
await writeFile(join(dir, 'test.txt'), 'content');
|
|
86
|
+
|
|
87
|
+
// Create subdirectory with file
|
|
88
|
+
const { mkdir } = await import('node:fs/promises');
|
|
89
|
+
await mkdir(subdir);
|
|
90
|
+
await writeFile(join(subdir, 'nested.txt'), 'nested');
|
|
91
|
+
|
|
92
|
+
expect(existsSync(dir)).toBe(true);
|
|
93
|
+
expect(existsSync(subdir)).toBe(true);
|
|
94
|
+
|
|
95
|
+
await removeTempDirectory(dir);
|
|
96
|
+
expect(existsSync(dir)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('handles non-existent directory gracefully', async () => {
|
|
100
|
+
// Should not throw
|
|
101
|
+
await expect(removeTempDirectory('/tmp/does-not-exist-12345')).resolves.toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('copyFixture', () => {
|
|
106
|
+
test('copies fixture to destination', async () => {
|
|
107
|
+
const tempDir = await createTempDirectory();
|
|
108
|
+
createdDirs.push(tempDir);
|
|
109
|
+
|
|
110
|
+
const destPath = join(tempDir, 'artifact-test');
|
|
111
|
+
await copyFixture('./test-fixtures/modules/artifact-test', destPath);
|
|
112
|
+
|
|
113
|
+
expect(existsSync(destPath)).toBe(true);
|
|
114
|
+
expect(existsSync(join(destPath, 'manifest.yml'))).toBe(true);
|
|
115
|
+
expect(existsSync(join(destPath, 'terraform'))).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('preserves directory structure', async () => {
|
|
119
|
+
const tempDir = await createTempDirectory();
|
|
120
|
+
createdDirs.push(tempDir);
|
|
121
|
+
|
|
122
|
+
const destPath = join(tempDir, 'artifact-test');
|
|
123
|
+
await copyFixture('./test-fixtures/modules/artifact-test', destPath);
|
|
124
|
+
|
|
125
|
+
expect(existsSync(join(destPath, 'terraform', 'main.tf'))).toBe(true);
|
|
126
|
+
expect(existsSync(join(destPath, 'build', 'build.sh'))).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('preserves file contents', async () => {
|
|
130
|
+
const tempDir = await createTempDirectory();
|
|
131
|
+
createdDirs.push(tempDir);
|
|
132
|
+
|
|
133
|
+
const destPath = join(tempDir, 'artifact-test');
|
|
134
|
+
await copyFixture('./test-fixtures/modules/artifact-test', destPath);
|
|
135
|
+
|
|
136
|
+
const manifest = await readFile(join(destPath, 'manifest.yml'), 'utf-8');
|
|
137
|
+
expect(manifest).toContain('id: artifact-test');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('createMultipleTempDirectories', () => {
|
|
142
|
+
test('creates multiple directories', async () => {
|
|
143
|
+
const dirs = await createMultipleTempDirectories(3);
|
|
144
|
+
createdDirs.push(...dirs);
|
|
145
|
+
|
|
146
|
+
expect(dirs).toHaveLength(3);
|
|
147
|
+
expect(existsSync(dirs[0])).toBe(true);
|
|
148
|
+
expect(existsSync(dirs[1])).toBe(true);
|
|
149
|
+
expect(existsSync(dirs[2])).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('directories are unique', async () => {
|
|
153
|
+
const dirs = await createMultipleTempDirectories(3);
|
|
154
|
+
createdDirs.push(...dirs);
|
|
155
|
+
|
|
156
|
+
expect(dirs[0]).not.toBe(dirs[1]);
|
|
157
|
+
expect(dirs[1]).not.toBe(dirs[2]);
|
|
158
|
+
expect(dirs[0]).not.toBe(dirs[2]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('creates zero directories', async () => {
|
|
162
|
+
const dirs = await createMultipleTempDirectories(0);
|
|
163
|
+
expect(dirs).toHaveLength(0);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('removeMultipleTempDirectories', () => {
|
|
168
|
+
test('removes multiple directories', async () => {
|
|
169
|
+
const dirs = await createMultipleTempDirectories(3);
|
|
170
|
+
|
|
171
|
+
expect(existsSync(dirs[0])).toBe(true);
|
|
172
|
+
expect(existsSync(dirs[1])).toBe(true);
|
|
173
|
+
expect(existsSync(dirs[2])).toBe(true);
|
|
174
|
+
|
|
175
|
+
await removeMultipleTempDirectories(dirs);
|
|
176
|
+
|
|
177
|
+
expect(existsSync(dirs[0])).toBe(false);
|
|
178
|
+
expect(existsSync(dirs[1])).toBe(false);
|
|
179
|
+
expect(existsSync(dirs[2])).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('handles empty array', async () => {
|
|
183
|
+
await expect(removeMultipleTempDirectories([])).resolves.toBeUndefined();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('createTempDirectoryWithCleanup', () => {
|
|
188
|
+
test('creates directory and cleanup function', async () => {
|
|
189
|
+
const { path: dir, cleanup } = await createTempDirectoryWithCleanup();
|
|
190
|
+
|
|
191
|
+
expect(existsSync(dir)).toBe(true);
|
|
192
|
+
expect(typeof cleanup).toBe('function');
|
|
193
|
+
|
|
194
|
+
await cleanup();
|
|
195
|
+
expect(existsSync(dir)).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('cleanup removes directory', async () => {
|
|
199
|
+
const { path: dir, cleanup } = await createTempDirectoryWithCleanup();
|
|
200
|
+
await writeFile(join(dir, 'test.txt'), 'content');
|
|
201
|
+
|
|
202
|
+
expect(existsSync(dir)).toBe(true);
|
|
203
|
+
|
|
204
|
+
await cleanup();
|
|
205
|
+
expect(existsSync(dir)).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|