@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.
Files changed (49) hide show
  1. package/.github/workflows/e2e.yml +73 -0
  2. package/apps/docs/src/content/docs/cli/add.md +2 -1
  3. package/apps/docs/src/content/docs/cli/apply.md +5 -2
  4. package/apps/docs/src/content/docs/cli/destroy.md +7 -3
  5. package/apps/docs/src/content/docs/cli/eject.md +6 -2
  6. package/apps/docs/src/content/docs/guides/headless.md +12 -0
  7. package/apps/docs/src/content/docs/roadmap.md +5 -3
  8. package/apps/docs/src/content/docs/testing-strategy.md +6 -1
  9. package/bin/cli.js +7 -7
  10. package/package.json +3 -1
  11. package/specs/e2e-testing.md +50 -0
  12. package/specs/test-suite-deduplication.md +42 -0
  13. package/src/commands/apply.js +4 -4
  14. package/src/commands/destroy.js +3 -3
  15. package/src/commands/eject.js +5 -2
  16. package/src/core/parser.js +2 -0
  17. package/src/utils/command.js +13 -0
  18. package/tests/add.test.js +12 -31
  19. package/tests/ai.test.js +9 -12
  20. package/tests/apply.test.js +6 -48
  21. package/tests/capabilities.test.js +5 -8
  22. package/tests/command.test.js +9 -10
  23. package/tests/db.test.js +6 -37
  24. package/tests/destroy.test.js +6 -32
  25. package/tests/diagnose.test.js +7 -21
  26. package/tests/doctor.test.js +6 -13
  27. package/tests/domain.test.js +12 -29
  28. package/tests/drift.test.js +12 -27
  29. package/tests/e2e/helpers.js +122 -0
  30. package/tests/e2e/tier0.e2e.test.js +142 -0
  31. package/tests/e2e/tier1.live.e2e.test.js +118 -0
  32. package/tests/exec.test.js +6 -20
  33. package/tests/gc.test.js +6 -36
  34. package/tests/headless.test.js +37 -108
  35. package/tests/helpers/clack.js +103 -0
  36. package/tests/helpers/console.js +41 -0
  37. package/tests/helpers/telemetry.js +37 -0
  38. package/tests/helpers/tmpdir.js +36 -0
  39. package/tests/logs.test.js +4 -15
  40. package/tests/resolvers.test.js +5 -8
  41. package/tests/rollback.test.js +6 -28
  42. package/tests/secrets.test.js +6 -29
  43. package/tests/sleep-wake.test.js +11 -29
  44. package/tests/status.test.js +6 -20
  45. package/tests/visualizer.test.js +12 -28
  46. package/vitest.config.js +2 -1
  47. package/vitest.e2e.tier0.config.js +8 -0
  48. package/vitest.e2e.tier1.config.js +8 -0
  49. package/specs/deploy-stack-to-grada-run-rebrand.md +0 -45
@@ -0,0 +1,103 @@
1
+ // Centralized `@clack/prompts` mock for the test suite.
2
+ //
3
+ // Every handle is a stable per-file singleton (Vitest isolates modules per
4
+ // test file), so tests can import handles directly:
5
+ //
6
+ // import { clackPromptsMockFactory, mockConfirm } from './helpers/clack.js';
7
+ // vi.mock('@clack/prompts', () => clackPromptsMockFactory());
8
+ //
9
+ // Rules preserved from the previously hand-rolled blocks:
10
+ // - Bare defaults: no preset return values. Tests needing a specific answer
11
+ // override per-test (e.g. `mockConfirm.mockResolvedValue(true)`).
12
+ // - `isCancel` keeps the real behavior (`typeof value === 'symbol'`), wrapped
13
+ // in a `vi.fn` so files like apply.test.js can still override it.
14
+ // - The spinner returns a fresh per-call instance (isolated start/stop/
15
+ // message) that also forwards into the shared aggregate handles, so both
16
+ // per-instance assertions (`spin.stop`) and aggregate assertions
17
+ // (`mockSpinnerStart`) keep working. Every instance is pushed to
18
+ // `createdSpinners` for message-sequence assertions.
19
+ import { vi } from 'vitest';
20
+
21
+ export const mockIntro = vi.fn();
22
+ export const mockOutro = vi.fn();
23
+ export const mockText = vi.fn();
24
+ export const mockSelect = vi.fn();
25
+ export const mockConfirm = vi.fn();
26
+ export const mockPassword = vi.fn();
27
+ export const mockMultiselect = vi.fn();
28
+ export const mockNote = vi.fn();
29
+ export const mockGroup = vi.fn();
30
+ export const mockCancel = vi.fn();
31
+ export const mockIsCancel = vi.fn((value) => typeof value === 'symbol');
32
+
33
+ export const mockSpinnerStart = vi.fn();
34
+ export const mockSpinnerStop = vi.fn();
35
+ export const mockSpinnerMessage = vi.fn();
36
+
37
+ export const mockLogInfo = vi.fn();
38
+ export const mockLogWarn = vi.fn();
39
+ export const mockLogMessage = vi.fn();
40
+ export const mockLogSuccess = vi.fn();
41
+ export const mockLogError = vi.fn();
42
+
43
+ export const createdSpinners = [];
44
+
45
+ export const mockSpinner = vi.fn(() => {
46
+ const instance = {
47
+ start: vi.fn((...args) => mockSpinnerStart(...args)),
48
+ stop: vi.fn((...args) => mockSpinnerStop(...args)),
49
+ message: vi.fn((...args) => mockSpinnerMessage(...args)),
50
+ };
51
+ createdSpinners.push(instance);
52
+ return instance;
53
+ });
54
+
55
+ // Back-compat handle map for files written against the `clack.mockXxx`
56
+ // hoisted-object naming (apply, destroy, gc, headless). Import as:
57
+ // import { clackMocks as clack } from './helpers/clack.js';
58
+ export const clackMocks = {
59
+ mockIntro,
60
+ mockOutro,
61
+ mockText,
62
+ mockSelect,
63
+ mockConfirm,
64
+ mockPassword,
65
+ mockMultiselect,
66
+ mockNote,
67
+ mockGroup,
68
+ mockCancel,
69
+ mockIsCancel,
70
+ mockSpinner,
71
+ mockSpinnerStart,
72
+ mockSpinnerStop,
73
+ mockSpinnerMessage,
74
+ mockLogInfo,
75
+ mockLogWarn,
76
+ mockLogMessage,
77
+ mockLogSuccess,
78
+ mockLogError,
79
+ };
80
+
81
+ export function clackPromptsMockFactory() {
82
+ return {
83
+ intro: mockIntro,
84
+ outro: mockOutro,
85
+ text: mockText,
86
+ select: mockSelect,
87
+ confirm: mockConfirm,
88
+ password: mockPassword,
89
+ multiselect: mockMultiselect,
90
+ note: mockNote,
91
+ group: mockGroup,
92
+ cancel: mockCancel,
93
+ isCancel: mockIsCancel,
94
+ spinner: mockSpinner,
95
+ log: {
96
+ info: mockLogInfo,
97
+ warn: mockLogWarn,
98
+ message: mockLogMessage,
99
+ success: mockLogSuccess,
100
+ error: mockLogError,
101
+ },
102
+ };
103
+ }
@@ -0,0 +1,41 @@
1
+ // Centralized process/console spy helpers for the test suite.
2
+ //
3
+ // The recurring trio (`process.exit` + `console.log` + `console.error`) is
4
+ // available as one bundle; individual spies cover the pair/single cases.
5
+ // Bespoke variants (e.g. an exit spy that throws) stay inline in their files.
6
+ //
7
+ // Usage:
8
+ // import { mockConsoleTrio } from './helpers/console.js';
9
+ // let spies;
10
+ // beforeEach(() => { spies = mockConsoleTrio(); });
11
+ // afterEach(() => { spies.restore(); });
12
+ // // assertions read spies.exitSpy / spies.logSpy / spies.errorSpy
13
+ import { vi } from 'vitest';
14
+
15
+ export function mockProcessExit() {
16
+ return vi.spyOn(process, 'exit').mockImplementation(() => {});
17
+ }
18
+
19
+ export function mockConsoleLog() {
20
+ return vi.spyOn(console, 'log').mockImplementation(() => {});
21
+ }
22
+
23
+ export function mockConsoleError() {
24
+ return vi.spyOn(console, 'error').mockImplementation(() => {});
25
+ }
26
+
27
+ export function mockConsoleTrio() {
28
+ const exitSpy = mockProcessExit();
29
+ const logSpy = mockConsoleLog();
30
+ const errorSpy = mockConsoleError();
31
+ return {
32
+ exitSpy,
33
+ logSpy,
34
+ errorSpy,
35
+ restore() {
36
+ exitSpy.mockRestore();
37
+ logSpy.mockRestore();
38
+ errorSpy.mockRestore();
39
+ },
40
+ };
41
+ }
@@ -0,0 +1,37 @@
1
+ // Centralized `src/core/telemetry.js` mock for the test suite.
2
+ //
3
+ // Usage (one-liner per test file):
4
+ // import { telemetryMockFactory } from './helpers/telemetry.js';
5
+ // vi.mock('../src/core/telemetry.js', (importOriginal) => telemetryMockFactory(importOriginal));
6
+ //
7
+ // `trackSuccess`/`trackFailure` mirror the real delegation (calling through
8
+ // to `trackEvent` + `flushTelemetry`) so success/failure-path assertions that
9
+ // observe `trackEvent.mock.calls` keep working. `isActiveEnvValue` and
10
+ // `detectCiProvider` pass through to the real module, covering the union of
11
+ // what mocked consumers import (add, doctor, domain).
12
+ //
13
+ // Do NOT use this in tests/telemetry.test.js (it tests the real module).
14
+ import { vi } from 'vitest';
15
+
16
+ export const mockTrackEvent = vi.fn();
17
+ export const mockFlushTelemetry = vi.fn().mockResolvedValue();
18
+ export const mockTrackSuccess = vi.fn(async (event, properties) => {
19
+ mockTrackEvent(event, { ...properties, success: true });
20
+ await mockFlushTelemetry();
21
+ });
22
+ export const mockTrackFailure = vi.fn(async (event, properties) => {
23
+ mockTrackEvent(event, { ...properties, success: false });
24
+ await mockFlushTelemetry();
25
+ });
26
+
27
+ export async function telemetryMockFactory(importOriginal) {
28
+ const actual = await importOriginal();
29
+ return {
30
+ trackEvent: mockTrackEvent,
31
+ flushTelemetry: mockFlushTelemetry,
32
+ trackSuccess: mockTrackSuccess,
33
+ trackFailure: mockTrackFailure,
34
+ isActiveEnvValue: actual.isActiveEnvValue,
35
+ detectCiProvider: actual.detectCiProvider,
36
+ };
37
+ }
@@ -0,0 +1,36 @@
1
+ // Centralized tmpdir fixture helper for the test suite.
2
+ //
3
+ // Usage:
4
+ // import { createTmpDirTracker } from './helpers/tmpdir.js';
5
+ // const tmp = createTmpDirTracker();
6
+ // beforeEach(() => { tmp.reset(); });
7
+ // afterEach(() => { tmp.cleanup(); });
8
+ // // tests create dirs with: const dir = tmp.makeTmp('my-test-');
9
+ import fs from 'fs';
10
+ import os from 'os';
11
+ import path from 'path';
12
+
13
+ export function makeTmpDir(prefix) {
14
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
15
+ }
16
+
17
+ export function createTmpDirTracker() {
18
+ const dirs = [];
19
+ return {
20
+ dirs,
21
+ makeTmp(prefix) {
22
+ const dir = makeTmpDir(prefix);
23
+ dirs.push(dir);
24
+ return dir;
25
+ },
26
+ reset() {
27
+ dirs.length = 0;
28
+ },
29
+ cleanup() {
30
+ for (const dir of dirs) {
31
+ fs.rmSync(dir, { recursive: true, force: true });
32
+ }
33
+ dirs.length = 0;
34
+ },
35
+ };
36
+ }
@@ -1,4 +1,7 @@
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';
2
5
  import fs from 'fs';
3
6
  import os from 'os';
4
7
  import path from 'path';
@@ -46,21 +49,7 @@ vi.mock('@aws-sdk/client-cloudwatch-logs', () => ({
46
49
  DescribeLogStreamsCommand: MockDescribeLogStreamsCommand,
47
50
  }));
48
51
 
49
- vi.mock('../src/core/telemetry.js', () => {
50
- const trackEvent = vi.fn();
51
- const flushTelemetry = vi.fn(() => Promise.resolve());
52
- // Mirrors the real trackSuccess delegation so success-path assertions
53
- // keep observing trackEvent (the real helper is unit-tested separately).
54
- const trackSuccess = vi.fn(async (event, properties) => {
55
- trackEvent(event, { ...properties, success: true });
56
- await flushTelemetry();
57
- });
58
- const trackFailure = vi.fn(async (event, properties) => {
59
- trackEvent(event, { ...properties, success: false });
60
- await flushTelemetry();
61
- });
62
- return { trackEvent, flushTelemetry, trackSuccess, trackFailure };
63
- });
52
+ vi.mock('../src/core/telemetry.js', (importOriginal) => telemetryMockFactory(importOriginal));
64
53
 
65
54
  function makeEvents(count, prefix = 'log line') {
66
55
  const base = Date.now() - count * 1000;
@@ -1,25 +1,22 @@
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 { readFileSafe, readTerraformProjectName, readTerraformRegion, resolveProjectName, resolveRegion, resolveLogGroup, resolveHeadless, resolveAppName, resolveCwd, resolveCluster, resolveService, resolveWorkspaceSuffix, detectComputeTargetFromMainTf, readTerraformComputeTarget } from '../src/utils/resolvers.js';
6
7
 
7
- let tmpDirs = [];
8
+ const tmp = createTmpDirTracker();
8
9
 
9
10
  function makeTmp() {
10
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'resolvers-test-'));
11
- tmpDirs.push(dir);
12
- return dir;
11
+ return tmp.makeTmp('resolvers-test-');
13
12
  }
14
13
 
15
14
  beforeEach(() => {
16
- tmpDirs = [];
15
+ tmp.reset();
17
16
  });
18
17
 
19
18
  afterEach(() => {
20
- for (const dir of tmpDirs) {
21
- fs.rmSync(dir, { recursive: true, force: true });
22
- }
19
+ tmp.cleanup();
23
20
  });
24
21
 
25
22
  describe('readTerraformProjectName', () => {
@@ -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, createdSpinners } 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';
@@ -14,35 +18,9 @@ import { trackEvent, flushTelemetry } from '../src/core/telemetry.js';
14
18
  import { select } from '@clack/prompts';
15
19
  import { handleAwsAuthError } from '../src/utils/aws.js';
16
20
 
17
- const createdSpinners = vi.hoisted(() => []);
18
-
19
- vi.mock('@clack/prompts', () => ({
20
- intro: vi.fn(),
21
- outro: vi.fn(),
22
- spinner: () => {
23
- const instance = { start: vi.fn(), stop: vi.fn(), message: vi.fn() };
24
- createdSpinners.push(instance);
25
- return instance;
26
- },
27
- select: vi.fn(),
28
- isCancel: (value) => typeof value === 'symbol',
29
- }));
21
+ vi.mock('@clack/prompts', () => clackPromptsMockFactory());
30
22
 
31
- vi.mock('../src/core/telemetry.js', () => {
32
- const trackEvent = vi.fn();
33
- const flushTelemetry = vi.fn().mockResolvedValue();
34
- // Mirrors the real trackSuccess delegation so success-path assertions
35
- // keep observing trackEvent (the real helper is unit-tested separately).
36
- const trackSuccess = vi.fn(async (event, properties) => {
37
- trackEvent(event, { ...properties, success: true });
38
- await flushTelemetry();
39
- });
40
- const trackFailure = vi.fn(async (event, properties) => {
41
- trackEvent(event, { ...properties, success: false });
42
- await flushTelemetry();
43
- });
44
- return { trackEvent, flushTelemetry, trackSuccess, trackFailure };
45
- });
23
+ vi.mock('../src/core/telemetry.js', (importOriginal) => telemetryMockFactory(importOriginal));
46
24
 
47
25
  vi.mock('../src/utils/aws.js', () => ({
48
26
  hasAwsCli: vi.fn().mockReturnValue(true),
@@ -1,4 +1,8 @@
1
1
  import { vi, describe, it, expect, 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, mockConfirm, mockOutro, mockSpinnerStart, mockSpinnerStop } from './helpers/clack.js';
5
+ import { telemetryMockFactory } from './helpers/telemetry.js';
2
6
  import fs from 'fs/promises';
3
7
  import path from 'path';
4
8
  import { pushSecrets, pullSecrets, auditSecrets, resolveSecretsRegion } from '../src/commands/secrets.js';
@@ -22,13 +26,6 @@ const { mockSend, MockSecretsManagerClient, MockUpdateSecretCommand, MockGetSecr
22
26
  };
23
27
  });
24
28
 
25
- const { mockConfirm, mockOutro, mockSpinnerStart, mockSpinnerStop } = vi.hoisted(() => ({
26
- mockConfirm: vi.fn(),
27
- mockOutro: vi.fn(),
28
- mockSpinnerStart: vi.fn(),
29
- mockSpinnerStop: vi.fn(),
30
- }));
31
-
32
29
  const { mockEcsSend, MockECSClient, MockUpdateServiceCommand } = vi.hoisted(() => {
33
30
  const ecsSendFn = vi.fn().mockResolvedValue({});
34
31
  return {
@@ -56,30 +53,10 @@ vi.mock('@aws-sdk/client-ecs', () => ({
56
53
  }));
57
54
 
58
55
  // 3. Mock @clack/prompts to prevent hangs on user input during testing
59
- vi.mock('@clack/prompts', () => ({
60
- spinner: vi.fn(() => ({ start: mockSpinnerStart, stop: mockSpinnerStop, message: vi.fn() })),
61
- confirm: (...args) => mockConfirm(...args),
62
- outro: (...args) => mockOutro(...args),
63
- intro: vi.fn(),
64
- isCancel: (value) => typeof value === 'symbol',
65
- }));
56
+ vi.mock('@clack/prompts', () => clackPromptsMockFactory());
66
57
 
67
58
  // 4. Mock telemetry to prevent real network calls during testing
68
- vi.mock('../src/core/telemetry.js', () => {
69
- const trackEvent = vi.fn();
70
- const flushTelemetry = vi.fn().mockResolvedValue();
71
- // Mirrors the real trackSuccess delegation so success-path assertions
72
- // keep observing trackEvent (the real helper is unit-tested separately).
73
- const trackSuccess = vi.fn(async (event, properties) => {
74
- trackEvent(event, { ...properties, success: true });
75
- await flushTelemetry();
76
- });
77
- const trackFailure = vi.fn(async (event, properties) => {
78
- trackEvent(event, { ...properties, success: false });
79
- await flushTelemetry();
80
- });
81
- return { trackEvent, flushTelemetry, trackSuccess, trackFailure };
82
- });
59
+ vi.mock('../src/core/telemetry.js', (importOriginal) => telemetryMockFactory(importOriginal));
83
60
 
84
61
  describe('Secrets Push Command', () => {
85
62
  const originalCwd = process.cwd();
@@ -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';
@@ -25,42 +30,21 @@ import {
25
30
  setWorkerScalingSuspended,
26
31
  } from '../src/utils/sleep-targets.js';
27
32
 
28
- vi.mock('@clack/prompts', () => ({
29
- intro: vi.fn(),
30
- outro: vi.fn(),
31
- spinner: () => ({ start: vi.fn(), stop: vi.fn(), message: vi.fn() }),
32
- confirm: vi.fn(),
33
- cancel: vi.fn(),
34
- isCancel: (value) => typeof value === 'symbol',
35
- }));
33
+ vi.mock('@clack/prompts', () => clackPromptsMockFactory());
36
34
 
37
- vi.mock('../src/core/telemetry.js', () => {
38
- const trackEvent = vi.fn();
39
- const flushTelemetry = vi.fn().mockResolvedValue();
40
- const trackSuccess = vi.fn(async (event, properties) => {
41
- trackEvent(event, { ...properties, success: true });
42
- await flushTelemetry();
43
- });
44
- const trackFailure = vi.fn(async (event, properties) => {
45
- trackEvent(event, { ...properties, success: false });
46
- await flushTelemetry();
47
- });
48
- return { trackEvent, flushTelemetry, trackSuccess, trackFailure };
49
- });
35
+ vi.mock('../src/core/telemetry.js', (importOriginal) => telemetryMockFactory(importOriginal));
50
36
 
51
37
  vi.mock('../src/utils/aws.js', () => ({
52
38
  handleAuthErrorBranch: () => false,
53
39
  resolveClient: (injected) => injected,
54
40
  }));
55
41
 
56
- let tmpDirs = [];
42
+ const tmp = createTmpDirTracker();
57
43
  let exitSpy;
58
44
  let logSpy;
59
45
 
60
46
  function makeTmp() {
61
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sleep-wake-test-'));
62
- tmpDirs.push(dir);
63
- return dir;
47
+ return tmp.makeTmp('sleep-wake-test-');
64
48
  }
65
49
 
66
50
  // Minimal ECS/RDS recorder: `ecs` maps service names to service objects,
@@ -125,7 +109,7 @@ function activeService(name, desiredCount) {
125
109
  }
126
110
 
127
111
  beforeEach(() => {
128
- tmpDirs = [];
112
+ tmp.reset();
129
113
  vi.clearAllMocks();
130
114
  exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
131
115
  logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
@@ -134,9 +118,7 @@ beforeEach(() => {
134
118
  afterEach(() => {
135
119
  exitSpy.mockRestore();
136
120
  logSpy.mockRestore();
137
- for (const dir of tmpDirs) {
138
- fs.rmSync(dir, { recursive: true, force: true });
139
- }
121
+ tmp.cleanup();
140
122
  });
141
123
 
142
124
  describe('parseSleepArgs / parseWakeArgs', () => {
@@ -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 fs from 'fs';
3
7
  import os from 'os';
4
8
  import path from 'path';
@@ -10,27 +14,9 @@ vi.mock('../src/commands/diagnose.js', () => ({
10
14
  runDiagnose: vi.fn().mockResolvedValue({ healthy: false }),
11
15
  }));
12
16
 
13
- vi.mock('@clack/prompts', () => ({
14
- intro: vi.fn(),
15
- outro: vi.fn(),
16
- spinner: () => ({ start: vi.fn(), stop: vi.fn(), message: vi.fn() }),
17
- }));
17
+ vi.mock('@clack/prompts', () => clackPromptsMockFactory());
18
18
 
19
- vi.mock('../src/core/telemetry.js', () => {
20
- const trackEvent = vi.fn();
21
- const flushTelemetry = vi.fn().mockResolvedValue();
22
- // Mirrors the real trackSuccess delegation so success-path assertions
23
- // keep observing trackEvent (the real helper is unit-tested separately).
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
- });
19
+ vi.mock('../src/core/telemetry.js', (importOriginal) => telemetryMockFactory(importOriginal));
34
20
 
35
21
  function mockEcsClient(serviceDesc) {
36
22
  return { send: vi.fn().mockResolvedValue({ services: [serviceDesc] }) };
@@ -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, mockNote } 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';
@@ -15,33 +20,14 @@ import { ADDON_REGISTRY } from '../src/utils/addons.js';
15
20
  import { trackEvent, flushTelemetry } from '../src/core/telemetry.js';
16
21
  import { confirm } from '@clack/prompts';
17
22
 
18
- const { mockNote } = vi.hoisted(() => ({ mockNote: vi.fn() }));
19
-
20
- vi.mock('@clack/prompts', () => ({
21
- note: mockNote,
22
- confirm: vi.fn(),
23
- isCancel: (value) => typeof value === 'symbol',
24
- cancel: vi.fn(),
25
- }));
26
-
27
- vi.mock('../src/core/telemetry.js', () => {
28
- const trackEvent = vi.fn();
29
- const flushTelemetry = vi.fn().mockResolvedValue();
30
- // Mirrors the real trackFailure delegation so failure-path assertions
31
- // keep observing trackEvent (the real helper is unit-tested separately).
32
- const trackFailure = vi.fn(async (event, properties) => {
33
- trackEvent(event, { ...properties, success: false });
34
- await flushTelemetry();
35
- });
36
- return { trackEvent, flushTelemetry, trackFailure };
37
- });
23
+ vi.mock('@clack/prompts', () => clackPromptsMockFactory());
24
+
25
+ vi.mock('../src/core/telemetry.js', (importOriginal) => telemetryMockFactory(importOriginal));
38
26
 
39
- let tmpDirs = [];
27
+ const tmp = createTmpDirTracker();
40
28
 
41
29
  function makeTmp() {
42
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'visualizer-test-'));
43
- tmpDirs.push(dir);
44
- return dir;
30
+ return tmp.makeTmp('visualizer-test-');
45
31
  }
46
32
 
47
33
  function writeTf(dir, files = {}) {
@@ -73,14 +59,12 @@ function stripAnsi(text) {
73
59
  }
74
60
 
75
61
  beforeEach(() => {
76
- tmpDirs = [];
62
+ tmp.reset();
77
63
  vi.clearAllMocks();
78
64
  });
79
65
 
80
66
  afterEach(() => {
81
- for (const dir of tmpDirs) {
82
- fs.rmSync(dir, { recursive: true, force: true });
83
- }
67
+ tmp.cleanup();
84
68
  });
85
69
 
86
70
  describe('parseTerraformConfig', () => {
package/vitest.config.js CHANGED
@@ -1,9 +1,10 @@
1
- import { defineConfig } from 'vitest/config';
1
+ import { defineConfig, defaultExclude } from 'vitest/config';
2
2
 
3
3
  export default defineConfig({
4
4
  test: {
5
5
  env: {
6
6
  FORCE_COLOR: '1',
7
7
  },
8
+ exclude: [...defaultExclude, 'tests/e2e/**'],
8
9
  },
9
10
  });
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['tests/e2e/tier0.e2e.test.js'],
6
+ testTimeout: 300000,
7
+ },
8
+ });
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['tests/e2e/tier1.live.e2e.test.js'],
6
+ testTimeout: 1200000,
7
+ },
8
+ });
@@ -1,45 +0,0 @@
1
- # Specification: The Grada Brand Transition
2
-
3
- ## 1. Overview
4
- Transition the `deploy-stack` codebase to the new brand identity: **Grada**.
5
- Existing deployments must not break; implement robust dual-read fallbacks for state files, machine markers, and tags. Version bump to `0.32.0`.
6
-
7
- ## 2. NPM, Repository & Binary Renaming
8
- - `package.json`: Update name to `grada-run`. Update `author` and `keywords` to include Grada terminology.
9
- - `package.json`: Map the `bin` field to: `"grada": "./bin/cli.js"`, `"grada-run": "./bin/cli.js"`, and a deprecated fallback `"deploy-stack": "./bin/cli.js"`.
10
- - `package.json` fields (`repository`, `bugs`, `homepage`) must point to `[https://github.com/grada-run/grada](https://github.com/grada-run/grada)`.
11
- - Replace hardcoded `anton-codes-iac/deploy-stack` GitHub URLs across the codebase (error logs, docs) with `grada-run/grada`.
12
- - Rename `CLI_ENTRY_BASENAMES` in `src/core/telemetry.js` to include `grada` and `grada-run`.
13
- - Update CLI banner texts, `.muserules` title, AI rules filenames (write new ones, do not delete user legacy ones), and help texts to use `Grada` / `npx grada-run`.
14
-
15
- ## 3. Dual-Read Local State & Env Vars
16
- Read from `.grada/` first, fall back to `.deploy-stack/`. Write new state to `.grada/`.
17
- - **Telemetry:** Check `~/.grada/telemetry-id` (raw text file) -> fallback to `~/.deploy-stack/telemetry-id`. Copy to new path on fallback.
18
- - **Sleep Ledger:** Check `.grada/sleep-state.json` -> fallback to `.deploy-stack/sleep-state.json`. In `ensureSleepGitignore`, append `.grada/` but leave `.deploy-stack/`.
19
- - **Bedrock Cache:** Check `~/.grada/bedrock-models-cache.json` -> fallback `~/.deploy-stack/bedrock-models-cache.json`.
20
- - **Env Vars:** Rename `DEPLOY_STACK_TELEMETRY_ID_PATH` -> `GRADA_TELEMETRY_ID_PATH`, `DEPLOY_STACK_FRAMEWORK` -> `GRADA_FRAMEWORK`, and `DEPLOY_STACK_BEDROCK_CACHE_PATH` -> `GRADA_BEDROCK_CACHE_PATH` (all with legacy read fallbacks). Leave `DO_NOT_TRACK` as the sole telemetry opt-out. Rename `DEPLOY_STACK_BYPASS` in the Svelte template to `GRADA_BYPASS`.
21
-
22
- ## 4. Machine Markers (Dual-Read)
23
- The CLI must parse both `deploy-stack` and `grada` variants for existing files, but write `grada` for new ones.
24
- - Domain mode: `# deploy-stack:domain-mode=` / `# grada:domain-mode=` AND `# deploy-stack:zone-id=` / `# grada:zone-id=`
25
- - Migrate gate: `# deploy-stack:db-migrate-start` / `# grada:db-migrate-start` AND `# deploy-stack:db-migrate-end` / `# grada:db-migrate-end`
26
- - AI Context: Dual read both HTML (`<!-- BEGIN GRADA CONTEXT -->` / `<!-- END GRADA CONTEXT -->`) and hash (`# BEGIN GRADA CONTEXT` / `# END GRADA CONTEXT`) block variants.
27
-
28
- ## 5. Templates, Tagging & Eject
29
- - Rename *all* cosmetic occurrences of the brand in templates (e.g., `npx deploy-stack` -> `npx grada-run`, `Generated by deploy-stack`, `# deploy-stack generated infrastructure`, `# deploy-stack addon:`, `deploy-stack-cron`, and git commit messages).
30
- - Update `ManagedBy = "grada"` for new writes in templates, S3 state buckets, RDS snapshots, and providers. Rename `startedBy` to `grada-db-*`.
31
- - In `src/commands/eject.js`, ensure the cleanup regex strips *both variants* of all four block types: `ManagedBy` tag blocks, cosmetic addon headers, `# deploy-stack backups` gitignore comments, and `# deploy-stack generated infrastructure` main.tf headers.
32
-
33
- ## 6. Documentation & Generators
34
- - **Docs Site (`apps/docs/`):** Update `astro.config.mjs` (`site: '[https://grada.run](https://grada.run)'`, remove the `base` property entirely, update `title`, `editLink`, `social` hrefs, redirects, and sidebar labels). Execute Astro build to regenerate `apps/docs/dist/`. Update internal `/deploy-stack/` links across `README.md`, `release_notes.md`, and all `apps/docs/src/` pages.
35
- - **Generator Fallback Doc:** Rename fallback constant to `GRADA.md`. `resolveDocDest` precedence chain: `README.md` -> `DEPLOYMENT.md` -> `GRADA.md` -> legacy `DEPLOY-STACK.md` (returning the first missing-or-managed).
36
- - **Cost Sync:** `syncDocCostEstimate` must scan `GRADA.md` and legacy `DEPLOY-STACK.md` in addition to `README.md` and `DEPLOYMENT.md`.
37
-
38
- ## 7. GitHub Actions & CI
39
- - Update `uses:` step from `anton-codes-iac/deploy-stack-action@v1` to `grada-run/grada-action@v1`.
40
- - Update `scripts/test-iac.js` and `iac-validation.yml` to assert the new `grada` migrate markers.
41
- - Validate that `.github/workflows/publish.yml` publishes `grada-run` and the scoped alias `@grada-run/grada`.
42
-
43
- ## 8. Testing
44
- - Use `npm run test:update` to carefully update the 131 legacy snapshot matches across the 12 test files.
45
- - Add unit tests asserting the dual-read marker logic, telemetry fallback, sleep ledger fallback, and `eject` multi-brand cleanup.