@grada-run/grada 0.32.0 → 0.33.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/.github/workflows/e2e.yml +73 -0
- package/apps/docs/src/content/docs/cli/add.md +2 -1
- package/apps/docs/src/content/docs/cli/apply.md +5 -2
- package/apps/docs/src/content/docs/cli/destroy.md +7 -3
- package/apps/docs/src/content/docs/cli/eject.md +6 -2
- package/apps/docs/src/content/docs/guides/headless.md +12 -0
- package/apps/docs/src/content/docs/roadmap.md +5 -3
- package/apps/docs/src/content/docs/testing-strategy.md +6 -1
- package/bin/cli.js +7 -7
- package/package.json +3 -1
- package/specs/e2e-testing.md +50 -0
- package/specs/test-suite-deduplication.md +42 -0
- package/src/commands/apply.js +4 -4
- package/src/commands/destroy.js +3 -3
- package/src/commands/eject.js +5 -2
- package/src/core/parser.js +2 -0
- package/src/utils/command.js +13 -0
- package/tests/add.test.js +12 -31
- package/tests/ai.test.js +9 -12
- package/tests/apply.test.js +6 -48
- package/tests/capabilities.test.js +5 -8
- package/tests/command.test.js +9 -10
- package/tests/db.test.js +6 -37
- package/tests/destroy.test.js +6 -32
- package/tests/diagnose.test.js +7 -21
- package/tests/doctor.test.js +6 -13
- package/tests/domain.test.js +12 -29
- package/tests/drift.test.js +12 -27
- package/tests/e2e/helpers.js +122 -0
- package/tests/e2e/tier0.e2e.test.js +142 -0
- package/tests/e2e/tier1.live.e2e.test.js +118 -0
- package/tests/exec.test.js +6 -20
- package/tests/gc.test.js +6 -36
- package/tests/headless.test.js +37 -108
- package/tests/helpers/clack.js +103 -0
- package/tests/helpers/console.js +41 -0
- package/tests/helpers/telemetry.js +37 -0
- package/tests/helpers/tmpdir.js +36 -0
- package/tests/logs.test.js +4 -15
- package/tests/resolvers.test.js +5 -8
- package/tests/rollback.test.js +6 -28
- package/tests/secrets.test.js +6 -29
- package/tests/sleep-wake.test.js +11 -29
- package/tests/status.test.js +6 -20
- package/tests/visualizer.test.js +12 -28
- package/vitest.config.js +2 -1
- package/vitest.e2e.tier0.config.js +8 -0
- package/vitest.e2e.tier1.config.js +8 -0
- package/specs/deploy-stack-to-grada-run-rebrand.md +0 -45
package/tests/apply.test.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
// NOTE: helper imports must stay above src imports: vi.mock factories run
|
|
3
|
+
// during module evaluation and need the factories initialized.
|
|
4
|
+
import { clackPromptsMockFactory, clackMocks as clack } from './helpers/clack.js';
|
|
5
|
+
import { telemetryMockFactory } from './helpers/telemetry.js';
|
|
2
6
|
import fs from 'fs';
|
|
3
7
|
import os from 'os';
|
|
4
8
|
import path from 'path';
|
|
@@ -17,56 +21,10 @@ vi.mock('child_process', () => ({
|
|
|
17
21
|
}));
|
|
18
22
|
|
|
19
23
|
// --- Silence interactive UI ---
|
|
20
|
-
|
|
21
|
-
mockIntro: vi.fn(),
|
|
22
|
-
mockOutro: vi.fn(),
|
|
23
|
-
mockConfirm: vi.fn(),
|
|
24
|
-
mockCancel: vi.fn(),
|
|
25
|
-
mockIsCancel: vi.fn(() => false),
|
|
26
|
-
mockSpinnerStart: vi.fn(),
|
|
27
|
-
mockSpinnerStop: vi.fn(),
|
|
28
|
-
mockSpinnerMessage: vi.fn(),
|
|
29
|
-
mockLogSuccess: vi.fn(),
|
|
30
|
-
mockLogWarn: vi.fn(),
|
|
31
|
-
mockLogError: vi.fn(),
|
|
32
|
-
mockLogMessage: vi.fn(),
|
|
33
|
-
}));
|
|
34
|
-
|
|
35
|
-
vi.mock('@clack/prompts', () => ({
|
|
36
|
-
intro: clack.mockIntro,
|
|
37
|
-
outro: clack.mockOutro,
|
|
38
|
-
confirm: clack.mockConfirm,
|
|
39
|
-
cancel: clack.mockCancel,
|
|
40
|
-
isCancel: clack.mockIsCancel,
|
|
41
|
-
spinner: vi.fn(() => ({
|
|
42
|
-
start: clack.mockSpinnerStart,
|
|
43
|
-
stop: clack.mockSpinnerStop,
|
|
44
|
-
message: clack.mockSpinnerMessage,
|
|
45
|
-
})),
|
|
46
|
-
log: {
|
|
47
|
-
success: clack.mockLogSuccess,
|
|
48
|
-
warn: clack.mockLogWarn,
|
|
49
|
-
error: clack.mockLogError,
|
|
50
|
-
message: clack.mockLogMessage,
|
|
51
|
-
},
|
|
52
|
-
}));
|
|
24
|
+
vi.mock('@clack/prompts', () => clackPromptsMockFactory());
|
|
53
25
|
|
|
54
26
|
// --- Never hit network / AWS ---
|
|
55
|
-
vi.mock('../src/core/telemetry.js', () =>
|
|
56
|
-
const trackEvent = vi.fn();
|
|
57
|
-
const flushTelemetry = vi.fn().mockResolvedValue();
|
|
58
|
-
// Mirrors the real trackSuccess delegation so success-path assertions
|
|
59
|
-
// keep observing trackEvent (the real helper is unit-tested separately).
|
|
60
|
-
const trackSuccess = vi.fn(async (event, properties) => {
|
|
61
|
-
trackEvent(event, { ...properties, success: true });
|
|
62
|
-
await flushTelemetry();
|
|
63
|
-
});
|
|
64
|
-
const trackFailure = vi.fn(async (event, properties) => {
|
|
65
|
-
trackEvent(event, { ...properties, success: false });
|
|
66
|
-
await flushTelemetry();
|
|
67
|
-
});
|
|
68
|
-
return { trackEvent, flushTelemetry, trackSuccess, trackFailure };
|
|
69
|
-
});
|
|
27
|
+
vi.mock('../src/core/telemetry.js', (importOriginal) => telemetryMockFactory(importOriginal));
|
|
70
28
|
|
|
71
29
|
vi.mock('../src/utils/aws.js', async (importOriginal) => {
|
|
72
30
|
const actual = await importOriginal();
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { createTmpDirTracker } from './helpers/tmpdir.js';
|
|
2
3
|
import fs from 'fs';
|
|
3
4
|
import os from 'os';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
import { detectProjectCapabilities } from '../src/utils/capabilities.js';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
const tmp = createTmpDirTracker();
|
|
8
9
|
|
|
9
10
|
function makeTmp() {
|
|
10
|
-
|
|
11
|
-
tmpDirs.push(dir);
|
|
12
|
-
return dir;
|
|
11
|
+
return tmp.makeTmp('capabilities-test-');
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
function writeFile(dir, relPath, content) {
|
|
@@ -24,13 +23,11 @@ function writeJson(dir, relPath, value) {
|
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
beforeEach(() => {
|
|
27
|
-
|
|
26
|
+
tmp.reset();
|
|
28
27
|
});
|
|
29
28
|
|
|
30
29
|
afterEach(() => {
|
|
31
|
-
|
|
32
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
33
|
-
}
|
|
30
|
+
tmp.cleanup();
|
|
34
31
|
});
|
|
35
32
|
|
|
36
33
|
describe('detectProjectCapabilities: empty and malformed projects', () => {
|
package/tests/command.test.js
CHANGED
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
// NOTE: helper imports must stay above src imports: vi.mock factories run
|
|
3
|
+
// during module evaluation and need the factories initialized.
|
|
4
|
+
import { telemetryMockFactory } from './helpers/telemetry.js';
|
|
5
|
+
import { mockConsoleTrio } from './helpers/console.js';
|
|
2
6
|
import { failCommand, failProjectNotInitialized } from '../src/utils/command.js';
|
|
3
7
|
import { trackEvent, flushTelemetry } from '../src/core/telemetry.js';
|
|
4
8
|
|
|
5
|
-
vi.mock('../src/core/telemetry.js', () => (
|
|
6
|
-
trackEvent: vi.fn(),
|
|
7
|
-
flushTelemetry: vi.fn().mockResolvedValue(),
|
|
8
|
-
}));
|
|
9
|
+
vi.mock('../src/core/telemetry.js', (importOriginal) => telemetryMockFactory(importOriginal));
|
|
9
10
|
|
|
10
11
|
describe('failCommand', () => {
|
|
11
12
|
let exitSpy;
|
|
12
13
|
let logSpy;
|
|
13
14
|
let errorSpy;
|
|
15
|
+
let spies;
|
|
14
16
|
|
|
15
17
|
beforeEach(() => {
|
|
16
18
|
vi.clearAllMocks();
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
19
|
+
spies = mockConsoleTrio();
|
|
20
|
+
({ exitSpy, logSpy, errorSpy } = spies);
|
|
20
21
|
});
|
|
21
22
|
|
|
22
23
|
afterEach(() => {
|
|
23
|
-
|
|
24
|
-
logSpy.mockRestore();
|
|
25
|
-
errorSpy.mockRestore();
|
|
24
|
+
spies.restore();
|
|
26
25
|
});
|
|
27
26
|
|
|
28
27
|
it('prints, tracks, flushes, exits, and returns the standard result', async () => {
|
package/tests/db.test.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
// NOTE: helper imports must stay above src imports: vi.mock factories run
|
|
3
|
+
// during module evaluation and need the factories initialized.
|
|
4
|
+
import { clackPromptsMockFactory, mockText, mockSelect, mockConfirm, mockPassword, mockSpinner } from './helpers/clack.js';
|
|
5
|
+
import { telemetryMockFactory, mockTrackEvent } from './helpers/telemetry.js';
|
|
2
6
|
import fs from 'fs';
|
|
3
7
|
import os from 'os';
|
|
4
8
|
import path from 'path';
|
|
@@ -47,44 +51,9 @@ import {
|
|
|
47
51
|
import { injectMigrationGate, quoteShellArg, buildMigrationCommand, findGateBlock } from '../src/commands/db/migrate.js';
|
|
48
52
|
import { resolveWorkspaceSuffix } from '../src/utils/resolvers.js';
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
mockText: vi.fn(),
|
|
52
|
-
mockSelect: vi.fn(),
|
|
53
|
-
mockConfirm: vi.fn(),
|
|
54
|
-
mockPassword: vi.fn(),
|
|
55
|
-
mockSpinner: vi.fn(() => ({ start: vi.fn(), stop: vi.fn(), message: vi.fn() })),
|
|
56
|
-
}));
|
|
57
|
-
|
|
58
|
-
vi.mock('@clack/prompts', () => ({
|
|
59
|
-
intro: vi.fn(),
|
|
60
|
-
outro: vi.fn(),
|
|
61
|
-
spinner: (...args) => mockSpinner(...args),
|
|
62
|
-
text: (...args) => mockText(...args),
|
|
63
|
-
select: (...args) => mockSelect(...args),
|
|
64
|
-
confirm: (...args) => mockConfirm(...args),
|
|
65
|
-
password: (...args) => mockPassword(...args),
|
|
66
|
-
cancel: vi.fn(),
|
|
67
|
-
isCancel: (value) => typeof value === 'symbol',
|
|
68
|
-
}));
|
|
69
|
-
|
|
70
|
-
const { mockTrackEvent } = vi.hoisted(() => ({
|
|
71
|
-
mockTrackEvent: vi.fn(),
|
|
72
|
-
}));
|
|
54
|
+
vi.mock('@clack/prompts', () => clackPromptsMockFactory());
|
|
73
55
|
|
|
74
|
-
vi.mock('../src/core/telemetry.js', () =>
|
|
75
|
-
const flushTelemetry = vi.fn(() => Promise.resolve());
|
|
76
|
-
// Mirrors the real trackSuccess delegation so success-path assertions
|
|
77
|
-
// keep observing trackEvent (the real helper is unit-tested separately).
|
|
78
|
-
const trackSuccess = vi.fn(async (event, properties) => {
|
|
79
|
-
mockTrackEvent(event, { ...properties, success: true });
|
|
80
|
-
await flushTelemetry();
|
|
81
|
-
});
|
|
82
|
-
const trackFailure = vi.fn(async (event, properties) => {
|
|
83
|
-
mockTrackEvent(event, { ...properties, success: false });
|
|
84
|
-
await flushTelemetry();
|
|
85
|
-
});
|
|
86
|
-
return { trackEvent: mockTrackEvent, flushTelemetry, trackSuccess, trackFailure };
|
|
87
|
-
});
|
|
56
|
+
vi.mock('../src/core/telemetry.js', (importOriginal) => telemetryMockFactory(importOriginal));
|
|
88
57
|
|
|
89
58
|
const {
|
|
90
59
|
MockDescribeDBInstancesCommand,
|
package/tests/destroy.test.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
// NOTE: helper imports must stay above src imports: vi.mock factories run
|
|
3
|
+
// during module evaluation and need the factories initialized.
|
|
4
|
+
import { clackPromptsMockFactory, clackMocks as clack } from './helpers/clack.js';
|
|
5
|
+
import { telemetryMockFactory } from './helpers/telemetry.js';
|
|
2
6
|
import fs from 'fs';
|
|
3
7
|
import os from 'os';
|
|
4
8
|
import path from 'path';
|
|
@@ -20,27 +24,7 @@ vi.mock('child_process', async (importOriginal) => {
|
|
|
20
24
|
});
|
|
21
25
|
|
|
22
26
|
// --- Silence interactive UI; confirm is controllable per test ---
|
|
23
|
-
|
|
24
|
-
mockIntro: vi.fn(),
|
|
25
|
-
mockOutro: vi.fn(),
|
|
26
|
-
mockConfirm: vi.fn(),
|
|
27
|
-
mockCancel: vi.fn(),
|
|
28
|
-
mockSpinnerStart: vi.fn(),
|
|
29
|
-
mockSpinnerStop: vi.fn(),
|
|
30
|
-
mockSpinnerMessage: vi.fn(),
|
|
31
|
-
}));
|
|
32
|
-
|
|
33
|
-
vi.mock('@clack/prompts', () => ({
|
|
34
|
-
intro: clack.mockIntro,
|
|
35
|
-
outro: clack.mockOutro,
|
|
36
|
-
confirm: clack.mockConfirm,
|
|
37
|
-
cancel: clack.mockCancel,
|
|
38
|
-
spinner: vi.fn(() => ({
|
|
39
|
-
start: clack.mockSpinnerStart,
|
|
40
|
-
stop: clack.mockSpinnerStop,
|
|
41
|
-
message: clack.mockSpinnerMessage,
|
|
42
|
-
})),
|
|
43
|
-
}));
|
|
27
|
+
vi.mock('@clack/prompts', () => clackPromptsMockFactory());
|
|
44
28
|
|
|
45
29
|
// --- Never touch AWS or the network ---
|
|
46
30
|
vi.mock('../src/utils/aws.js', () => ({
|
|
@@ -78,17 +62,7 @@ vi.mock('../src/utils/system.js', async (importOriginal) => {
|
|
|
78
62
|
};
|
|
79
63
|
});
|
|
80
64
|
|
|
81
|
-
vi.mock('../src/core/telemetry.js', () =>
|
|
82
|
-
const trackEvent = vi.fn();
|
|
83
|
-
const flushTelemetry = vi.fn().mockResolvedValue();
|
|
84
|
-
// Mirrors the real trackSuccess delegation so success-path assertions
|
|
85
|
-
// keep observing trackEvent (the real helper is unit-tested separately).
|
|
86
|
-
const trackSuccess = vi.fn(async (event, properties) => {
|
|
87
|
-
trackEvent(event, { ...properties, success: true });
|
|
88
|
-
await flushTelemetry();
|
|
89
|
-
});
|
|
90
|
-
return { trackEvent, flushTelemetry, trackSuccess };
|
|
91
|
-
});
|
|
65
|
+
vi.mock('../src/core/telemetry.js', (importOriginal) => telemetryMockFactory(importOriginal));
|
|
92
66
|
|
|
93
67
|
import { destroyStack } from '../src/commands/destroy.js';
|
|
94
68
|
import { teardownStateBucket } from '../src/utils/aws.js';
|
package/tests/diagnose.test.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
// NOTE: helper imports must stay above src imports: vi.mock factories run
|
|
3
|
+
// during module evaluation and need the factories initialized.
|
|
4
|
+
import { clackPromptsMockFactory } from './helpers/clack.js';
|
|
5
|
+
import { telemetryMockFactory } from './helpers/telemetry.js';
|
|
2
6
|
import { stripVTControlCharacters } from 'node:util';
|
|
3
7
|
import fs from 'fs';
|
|
4
8
|
import os from 'os';
|
|
@@ -56,27 +60,9 @@ vi.mock('@aws-sdk/client-cloudwatch-logs', () => {
|
|
|
56
60
|
});
|
|
57
61
|
|
|
58
62
|
// Silence interactive prompts and telemetry during tests
|
|
59
|
-
vi.mock('@clack/prompts', () => (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
spinner: () => ({ start: vi.fn(), stop: vi.fn(), message: vi.fn() })
|
|
63
|
-
}));
|
|
64
|
-
|
|
65
|
-
vi.mock('../src/core/telemetry.js', () => {
|
|
66
|
-
const trackEvent = vi.fn();
|
|
67
|
-
const flushTelemetry = vi.fn().mockResolvedValue();
|
|
68
|
-
// Mirrors the real trackSuccess delegation so success-path assertions
|
|
69
|
-
// keep observing trackEvent (the real helper is unit-tested separately).
|
|
70
|
-
const trackSuccess = vi.fn(async (event, properties) => {
|
|
71
|
-
trackEvent(event, { ...properties, success: true });
|
|
72
|
-
await flushTelemetry();
|
|
73
|
-
});
|
|
74
|
-
const trackFailure = vi.fn(async (event, properties) => {
|
|
75
|
-
trackEvent(event, { ...properties, success: false });
|
|
76
|
-
await flushTelemetry();
|
|
77
|
-
});
|
|
78
|
-
return { trackEvent, flushTelemetry, trackSuccess, trackFailure };
|
|
79
|
-
});
|
|
63
|
+
vi.mock('@clack/prompts', () => clackPromptsMockFactory());
|
|
64
|
+
|
|
65
|
+
vi.mock('../src/core/telemetry.js', (importOriginal) => telemetryMockFactory(importOriginal));
|
|
80
66
|
|
|
81
67
|
describe('Command: diagnose', () => {
|
|
82
68
|
beforeEach(() => {
|
package/tests/doctor.test.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
// NOTE: helper imports must stay above src imports: vi.mock factories run
|
|
3
|
+
// during module evaluation and need the factories initialized.
|
|
4
|
+
import { clackPromptsMockFactory } from './helpers/clack.js';
|
|
5
|
+
import { telemetryMockFactory } from './helpers/telemetry.js';
|
|
2
6
|
import { runDoctor, installHint, DOCTOR_CHECKS } from '../src/commands/doctor.js';
|
|
3
7
|
import { checkDependency } from '../src/utils/system.js';
|
|
4
8
|
import { trackEvent, flushTelemetry } from '../src/core/telemetry.js';
|
|
@@ -7,20 +11,9 @@ vi.mock('../src/utils/system.js', () => ({
|
|
|
7
11
|
checkDependency: vi.fn(),
|
|
8
12
|
}));
|
|
9
13
|
|
|
10
|
-
vi.mock('@clack/prompts', () => (
|
|
11
|
-
intro: vi.fn(),
|
|
12
|
-
outro: vi.fn(),
|
|
13
|
-
spinner: () => ({ start: vi.fn(), stop: vi.fn(), message: vi.fn() }),
|
|
14
|
-
}));
|
|
14
|
+
vi.mock('@clack/prompts', () => clackPromptsMockFactory());
|
|
15
15
|
|
|
16
|
-
vi.mock('../src/core/telemetry.js',
|
|
17
|
-
const actual = await importOriginal();
|
|
18
|
-
return {
|
|
19
|
-
trackEvent: vi.fn(),
|
|
20
|
-
flushTelemetry: vi.fn().mockResolvedValue(),
|
|
21
|
-
detectCiProvider: actual.detectCiProvider,
|
|
22
|
-
};
|
|
23
|
-
});
|
|
16
|
+
vi.mock('../src/core/telemetry.js', (importOriginal) => telemetryMockFactory(importOriginal));
|
|
24
17
|
|
|
25
18
|
const realPlatform = process.platform;
|
|
26
19
|
|
package/tests/domain.test.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
// NOTE: helper imports must stay above src imports: vi.mock factories run
|
|
3
|
+
// during module evaluation and need the factories initialized.
|
|
4
|
+
import { clackPromptsMockFactory } from './helpers/clack.js';
|
|
5
|
+
import { telemetryMockFactory } from './helpers/telemetry.js';
|
|
6
|
+
import { createTmpDirTracker } from './helpers/tmpdir.js';
|
|
2
7
|
import fs from 'fs';
|
|
3
8
|
import os from 'os';
|
|
4
9
|
import path from 'path';
|
|
@@ -34,36 +39,16 @@ import {
|
|
|
34
39
|
import { trackEvent, flushTelemetry } from '../src/core/telemetry.js';
|
|
35
40
|
import { confirm, outro } from '@clack/prompts';
|
|
36
41
|
|
|
37
|
-
vi.mock('@clack/prompts', () => (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
confirm: vi.fn(),
|
|
41
|
-
text: vi.fn(),
|
|
42
|
-
spinner: () => ({ start: vi.fn(), stop: vi.fn(), message: vi.fn() }),
|
|
43
|
-
log: { info: vi.fn(), warn: vi.fn(), message: vi.fn(), success: vi.fn(), error: vi.fn() },
|
|
44
|
-
cancel: vi.fn(),
|
|
45
|
-
isCancel: (value) => typeof value === 'symbol',
|
|
46
|
-
}));
|
|
47
|
-
|
|
48
|
-
vi.mock('../src/core/telemetry.js', async (importOriginal) => {
|
|
49
|
-
const actual = await importOriginal();
|
|
50
|
-
const trackEvent = vi.fn();
|
|
51
|
-
const flushTelemetry = vi.fn().mockResolvedValue();
|
|
52
|
-
const trackSuccess = vi.fn(async (event, properties) => {
|
|
53
|
-
trackEvent(event, { ...properties, success: true });
|
|
54
|
-
await flushTelemetry();
|
|
55
|
-
});
|
|
56
|
-
return { trackEvent, flushTelemetry, trackSuccess, isActiveEnvValue: actual.isActiveEnvValue, detectCiProvider: actual.detectCiProvider };
|
|
57
|
-
});
|
|
42
|
+
vi.mock('@clack/prompts', () => clackPromptsMockFactory());
|
|
43
|
+
|
|
44
|
+
vi.mock('../src/core/telemetry.js', (importOriginal) => telemetryMockFactory(importOriginal));
|
|
58
45
|
|
|
59
|
-
|
|
46
|
+
const tmp = createTmpDirTracker();
|
|
60
47
|
let exitSpy;
|
|
61
48
|
let logSpy;
|
|
62
49
|
|
|
63
50
|
function makeTmp() {
|
|
64
|
-
|
|
65
|
-
tmpDirs.push(dir);
|
|
66
|
-
return dir;
|
|
51
|
+
return tmp.makeTmp('domain-test-');
|
|
67
52
|
}
|
|
68
53
|
|
|
69
54
|
const CLOUDFRONT_TF = [
|
|
@@ -129,7 +114,7 @@ function capturedOutput() {
|
|
|
129
114
|
}
|
|
130
115
|
|
|
131
116
|
beforeEach(() => {
|
|
132
|
-
|
|
117
|
+
tmp.reset();
|
|
133
118
|
vi.clearAllMocks();
|
|
134
119
|
exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
|
|
135
120
|
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
@@ -138,9 +123,7 @@ beforeEach(() => {
|
|
|
138
123
|
afterEach(() => {
|
|
139
124
|
exitSpy.mockRestore();
|
|
140
125
|
logSpy.mockRestore();
|
|
141
|
-
|
|
142
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
143
|
-
}
|
|
126
|
+
tmp.cleanup();
|
|
144
127
|
});
|
|
145
128
|
|
|
146
129
|
describe('hcl: findResourceBlock', () => {
|
package/tests/drift.test.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
// NOTE: helper imports must stay above src imports: vi.mock factories run
|
|
3
|
+
// during module evaluation and need the factories initialized.
|
|
4
|
+
import { clackPromptsMockFactory } from './helpers/clack.js';
|
|
5
|
+
import { telemetryMockFactory } from './helpers/telemetry.js';
|
|
6
|
+
import { createTmpDirTracker } from './helpers/tmpdir.js';
|
|
2
7
|
import fs from 'fs';
|
|
3
8
|
import os from 'os';
|
|
4
9
|
import path from 'path';
|
|
@@ -12,34 +17,16 @@ import {
|
|
|
12
17
|
} from '../src/commands/drift.js';
|
|
13
18
|
import { trackEvent, flushTelemetry } from '../src/core/telemetry.js';
|
|
14
19
|
|
|
15
|
-
vi.mock('@clack/prompts', () => (
|
|
16
|
-
intro: vi.fn(),
|
|
17
|
-
outro: vi.fn(),
|
|
18
|
-
spinner: () => ({ start: vi.fn(), stop: vi.fn(), message: vi.fn() }),
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
vi.mock('../src/core/telemetry.js', () => {
|
|
22
|
-
const trackEvent = vi.fn();
|
|
23
|
-
const flushTelemetry = vi.fn().mockResolvedValue();
|
|
24
|
-
const trackSuccess = vi.fn(async (event, properties) => {
|
|
25
|
-
trackEvent(event, { ...properties, success: true });
|
|
26
|
-
await flushTelemetry();
|
|
27
|
-
});
|
|
28
|
-
const trackFailure = vi.fn(async (event, properties) => {
|
|
29
|
-
trackEvent(event, { ...properties, success: false });
|
|
30
|
-
await flushTelemetry();
|
|
31
|
-
});
|
|
32
|
-
return { trackEvent, flushTelemetry, trackSuccess, trackFailure };
|
|
33
|
-
});
|
|
20
|
+
vi.mock('@clack/prompts', () => clackPromptsMockFactory());
|
|
34
21
|
|
|
35
|
-
|
|
22
|
+
vi.mock('../src/core/telemetry.js', (importOriginal) => telemetryMockFactory(importOriginal));
|
|
23
|
+
|
|
24
|
+
const tmp = createTmpDirTracker();
|
|
36
25
|
let exitSpy;
|
|
37
26
|
let logSpy;
|
|
38
27
|
|
|
39
28
|
function makeTmp() {
|
|
40
|
-
|
|
41
|
-
tmpDirs.push(dir);
|
|
42
|
-
return dir;
|
|
29
|
+
return tmp.makeTmp('drift-test-');
|
|
43
30
|
}
|
|
44
31
|
|
|
45
32
|
function writeDeployYml(dir, roleArn = 'arn:aws:iam::123456789012:role/myapp-github-actions-role') {
|
|
@@ -60,7 +47,7 @@ function writeMainTf(dir, region = 'us-east-2') {
|
|
|
60
47
|
}
|
|
61
48
|
|
|
62
49
|
beforeEach(() => {
|
|
63
|
-
|
|
50
|
+
tmp.reset();
|
|
64
51
|
vi.clearAllMocks();
|
|
65
52
|
exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
|
|
66
53
|
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
@@ -69,9 +56,7 @@ beforeEach(() => {
|
|
|
69
56
|
afterEach(() => {
|
|
70
57
|
exitSpy.mockRestore();
|
|
71
58
|
logSpy.mockRestore();
|
|
72
|
-
|
|
73
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
74
|
-
}
|
|
59
|
+
tmp.cleanup();
|
|
75
60
|
});
|
|
76
61
|
|
|
77
62
|
describe('parseDriftArgs', () => {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Shared helpers for the E2E suites (Tier 0 + Tier 1). Kept separate from
|
|
2
|
+
// tests/helpers/ on purpose: unit mocks must never leak into black-box runs.
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
export const ROOT = path.resolve(HERE, '..', '..');
|
|
11
|
+
export const CLI = path.join(ROOT, 'bin', 'cli.js');
|
|
12
|
+
|
|
13
|
+
// Repo-local workspace root so CI can upload it as an artifact on failure.
|
|
14
|
+
// Overridable via E2E_TMP_ROOT for local debugging.
|
|
15
|
+
export const E2E_TMP_ROOT = process.env.E2E_TMP_ROOT || path.join(HERE, '.tmp');
|
|
16
|
+
|
|
17
|
+
// Shared provider cache across runs (mirrors scripts/test-iac.js).
|
|
18
|
+
const pluginCacheDir = path.join(os.tmpdir(), 'grada-e2e-plugin-cache');
|
|
19
|
+
|
|
20
|
+
export function e2eEnv({ mockAws }) {
|
|
21
|
+
fs.mkdirSync(pluginCacheDir, { recursive: true });
|
|
22
|
+
const env = {
|
|
23
|
+
...process.env,
|
|
24
|
+
DO_NOT_TRACK: '1',
|
|
25
|
+
TF_PLUGIN_CACHE_DIR: pluginCacheDir,
|
|
26
|
+
};
|
|
27
|
+
if (mockAws) {
|
|
28
|
+
env.CI_MOCK_AWS = 'true';
|
|
29
|
+
} else {
|
|
30
|
+
delete env.CI_MOCK_AWS;
|
|
31
|
+
}
|
|
32
|
+
return env;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function commandExists(cmd) {
|
|
36
|
+
try {
|
|
37
|
+
const result = spawnSync(cmd, ['--version'], { stdio: 'pipe' });
|
|
38
|
+
return result.status === 0;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Fail fast with a helpful error when local prerequisites are missing.
|
|
45
|
+
export function assertPrerequisites() {
|
|
46
|
+
if (!commandExists('terraform')) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'E2E requires the Terraform CLI on PATH (https://developer.hashicorp.com/terraform/install).'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function makeTmpDir(prefix) {
|
|
54
|
+
fs.mkdirSync(E2E_TMP_ROOT, { recursive: true });
|
|
55
|
+
return fs.mkdtempSync(path.join(E2E_TMP_ROOT, `${prefix}-`));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function removeDir(dir) {
|
|
59
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Run the real CLI with stdin always closed (headless purity: a prompt
|
|
63
|
+
// crashes loudly instead of hanging). Output is inherited for visibility
|
|
64
|
+
// unless capture is requested for assertions.
|
|
65
|
+
export function runCli(args, { cwd, env, capture = false } = {}) {
|
|
66
|
+
const result = spawnSync('node', [CLI, ...args], {
|
|
67
|
+
cwd,
|
|
68
|
+
env,
|
|
69
|
+
stdio: capture ? ['ignore', 'pipe', 'pipe'] : ['ignore', 'inherit', 'inherit'],
|
|
70
|
+
encoding: 'utf-8',
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
status: result.status,
|
|
74
|
+
stdout: result.stdout ?? '',
|
|
75
|
+
stderr: result.stderr ?? '',
|
|
76
|
+
output: `${result.stdout ?? ''}\n${result.stderr ?? ''}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function runTerraform(args, { cwd, env }) {
|
|
81
|
+
const result = spawnSync('terraform', args, {
|
|
82
|
+
cwd,
|
|
83
|
+
env,
|
|
84
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
85
|
+
encoding: 'utf-8',
|
|
86
|
+
});
|
|
87
|
+
if (result.status !== 0) {
|
|
88
|
+
throw new Error(`terraform ${args.join(' ')} failed with exit ${result.status}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Isolated workspace that is removed on success and preserved (with its
|
|
93
|
+
// path printed) on failure for debugging and CI artifact upload.
|
|
94
|
+
export async function withTmpDir(prefix, fn) {
|
|
95
|
+
const dir = makeTmpDir(prefix);
|
|
96
|
+
try {
|
|
97
|
+
await fn(dir);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.log(`E2E workspace preserved for debugging: ${dir}`);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
removeDir(dir);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function sleep(ms) {
|
|
106
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readBackendValue(projectDir, key) {
|
|
110
|
+
const backend = fs.readFileSync(path.join(projectDir, 'terraform', 'backend.tf'), 'utf-8');
|
|
111
|
+
const match = backend.match(new RegExp(`${key}\\s*=\\s*"([^"]+)"`));
|
|
112
|
+
if (!match) throw new Error(`could not parse ${key} from terraform/backend.tf`);
|
|
113
|
+
return match[1];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function readBackendBucket(projectDir) {
|
|
117
|
+
return readBackendValue(projectDir, 'bucket');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function readBackendRegion(projectDir) {
|
|
121
|
+
return readBackendValue(projectDir, 'region');
|
|
122
|
+
}
|