@funnelsgrove/cli 0.1.28 → 0.1.35

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 (47) hide show
  1. package/README.md +7 -4
  2. package/dist/cli.d.ts +20 -0
  3. package/dist/cli.js +33 -8
  4. package/dist/githubSyncFlow.d.ts +6 -0
  5. package/dist/githubSyncFlow.js +55 -10
  6. package/funnel-contract-compatibility.json +28 -28
  7. package/package.json +8 -3
  8. package/template_docs/.funnelsgrove-docs.json +3 -3
  9. package/template_docs/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
  10. package/template_docs/funnel-docs.config.json +1 -1
  11. package/template_scaffold/.funnelsgrove-docs.json +3 -3
  12. package/template_scaffold/.funnelsgrove-scaffold.json +150 -150
  13. package/template_scaffold/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
  14. package/template_scaffold/funnel-docs.config.json +1 -1
  15. package/template_scaffold/package-lock.json +13 -13
  16. package/template_scaffold/package.json +3 -3
  17. package/template_scaffold/public/paywall/money-back-badge.png +0 -0
  18. package/template_scaffold/src/steps/step-32-paywall.tsx +2 -2
  19. package/template_scaffold/src/steps/step-33-subscription-started.tsx +1 -43
  20. package/template_scaffold/{funnel-agent-docs.test.ts → tests/funnel-agent-docs.test.ts} +4 -4
  21. package/template_scaffold/{package.test.ts → tests/package.test.ts} +4 -4
  22. package/template_scaffold/{src → tests/src}/app/layout.test.ts +2 -2
  23. package/template_scaffold/{src → tests/src}/components/FunnelFlow.test.tsx +2 -2
  24. package/template_scaffold/{src → tests/src}/config/billing.plans.test.ts +4 -4
  25. package/template_scaffold/{src → tests/src}/config/experiments.test.ts +1 -1
  26. package/template_scaffold/{src → tests/src}/config/funnel.manifest.test.ts +1 -1
  27. package/template_scaffold/{src → tests/src}/contract/funnel-project.validation.test.ts +2 -2
  28. package/template_scaffold/{src → tests/src}/contract/golden/golden-fixtures.test.ts +16 -16
  29. package/template_scaffold/{src → tests/src}/contract/validate-funnel.cli.test.ts +2 -2
  30. package/template_scaffold/{src → tests/src}/runtime/checkout-runtime-config.test.ts +3 -3
  31. package/template_scaffold/{src → tests/src}/runtime/funnel-runtime.test.ts +1 -1
  32. package/template_scaffold/{src → tests/src}/runtime/step-content-context.test.ts +1 -1
  33. package/template_scaffold/{src → tests/src}/runtime/use-funnel-flow-controller.integration.test.ts +4 -4
  34. package/template_scaffold/{src → tests/src}/runtime/use-funnel-flow-controller.test.ts +3 -3
  35. package/template_scaffold/{src → tests/src}/steps/paywall-checkout-options.test.ts +1 -1
  36. package/template_scaffold/{src → tests/src}/steps/shared-header-contract.test.ts +3 -1
  37. package/template_scaffold/{src → tests/src}/steps/step-02.test.ts +3 -3
  38. package/template_scaffold/{src → tests/src}/steps/step-03.test.ts +1 -1
  39. package/template_scaffold/{src → tests/src}/steps/step-04.test.ts +1 -1
  40. package/template_scaffold/{src → tests/src}/steps/step-31-email-capture.test.ts +1 -1
  41. package/template_scaffold/{src → tests/src}/steps/step-32-paywall.test.ts +43 -43
  42. package/template_scaffold/{src → tests/src}/steps/step-33-subscription-started.test.ts +6 -7
  43. package/template_scaffold/{src → tests/src}/steps/step-34-upsell-form.test.ts +3 -3
  44. package/template_scaffold/{src → tests/src}/steps/step-image-migration.test.ts +1 -1
  45. /package/template_scaffold/{src → tests/src}/app/[stepId]/page.test.tsx +0 -0
  46. /package/template_scaffold/{src → tests/src}/components/FunnelStepImage.test.ts +0 -0
  47. /package/template_scaffold/{src → tests/src}/steps/step-01.test.ts +0 -0
@@ -7,9 +7,9 @@ import { promisify } from 'node:util';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { describe, expect, it } from 'vitest';
9
9
 
10
- import contract from '../funnel-runtime/contracts/step-contract-v2.json';
10
+ import contract from '../../funnel-runtime/contracts/step-contract-v2.json';
11
11
 
12
- const templateRoot = dirname(fileURLToPath(import.meta.url));
12
+ const templateRoot = dirname(dirname(fileURLToPath(import.meta.url)));
13
13
  const repoRoot = resolve(templateRoot, '../..');
14
14
  const cliDocsRoot = join(repoRoot, 'apps/cli/template_docs');
15
15
 
@@ -34,7 +34,7 @@ const renderCodes = (values: readonly unknown[]): string => (
34
34
  );
35
35
 
36
36
  async function loadGenerator() {
37
- return import('../../scripts/generate-funnel-agent-docs.mjs');
37
+ return import('../../../scripts/generate-funnel-agent-docs.mjs');
38
38
  }
39
39
 
40
40
  describe('funnel agent documentation supply', () => {
@@ -340,7 +340,7 @@ describe('funnel agent documentation supply', () => {
340
340
 
341
341
  expect(manifest).toMatchObject({
342
342
  schemaVersion: 1,
343
- bundleVersion: '2.0.16',
343
+ bundleVersion: '2.0.23',
344
344
  stepContractVersion: contract.stepContractVersion,
345
345
  contractHash: contract.contractHash,
346
346
  });
@@ -11,10 +11,10 @@ const readPackageJson = (url: URL): PackageJson =>
11
11
 
12
12
  describe('funnel-template shared package dependencies', () => {
13
13
  it('tracks the current shared runtime, analytics, and payments package versions', () => {
14
- const templatePackage = readPackageJson(new URL('./package.json', import.meta.url));
15
- const runtimePackage = readPackageJson(new URL('../funnel-runtime/package.json', import.meta.url));
16
- const analyticsPackage = readPackageJson(new URL('../funnel-analytics/package.json', import.meta.url));
17
- const paymentsPackage = readPackageJson(new URL('../funnel-payments/package.json', import.meta.url));
14
+ const templatePackage = readPackageJson(new URL('../package.json', import.meta.url));
15
+ const runtimePackage = readPackageJson(new URL('../../funnel-runtime/package.json', import.meta.url));
16
+ const analyticsPackage = readPackageJson(new URL('../../funnel-analytics/package.json', import.meta.url));
17
+ const paymentsPackage = readPackageJson(new URL('../../funnel-payments/package.json', import.meta.url));
18
18
 
19
19
  expect(templatePackage.dependencies?.['@funnelsgrove/runtime']).toBe(`^${runtimePackage.version}`);
20
20
  expect(templatePackage.dependencies?.['@funnelsgrove/analytics']).toBe(`^${analyticsPackage.version}`);
@@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest';
4
4
 
5
5
  describe('RootLayout metadata source', () => {
6
6
  it('wires project page metadata settings into Next metadata', () => {
7
- const layoutSource = readFileSync(path.resolve(__dirname, './layout.tsx'), 'utf8');
7
+ const layoutSource = readFileSync(path.resolve(__dirname, '../../../src/app/layout.tsx'), 'utf8');
8
8
 
9
9
  expect(layoutSource).toContain('runtimePublicConfig.pageTitle');
10
10
  expect(layoutSource).toContain('runtimePublicConfig.pageDescription');
@@ -15,7 +15,7 @@ describe('RootLayout metadata source', () => {
15
15
  });
16
16
 
17
17
  it('renders Google tag head scripts through the shared analytics package', () => {
18
- const layoutSource = readFileSync(path.resolve(__dirname, './layout.tsx'), 'utf8');
18
+ const layoutSource = readFileSync(path.resolve(__dirname, '../../../src/app/layout.tsx'), 'utf8');
19
19
 
20
20
  expect(layoutSource).toContain("import { buildGoogleTagHeadScripts } from '@funnelsgrove/analytics'");
21
21
  expect(layoutSource).toContain('const googleTagHeadScripts = buildGoogleTagHeadScripts();');
@@ -3,9 +3,9 @@ import { describe, expect, it } from 'vitest';
3
3
 
4
4
  describe('FunnelFlow lifecycle wiring', () => {
5
5
  it('delegates one analytics adapter and the authoritative manifest version to the shared controller', () => {
6
- const flowSource = readFileSync(new URL('./FunnelFlow.tsx', import.meta.url), 'utf8');
6
+ const flowSource = readFileSync(new URL('../../../src/components/FunnelFlow.tsx', import.meta.url), 'utf8');
7
7
  const adapterSource = readFileSync(
8
- new URL('../runtime/use-funnel-flow-controller.ts', import.meta.url),
8
+ new URL('../../../src/runtime/use-funnel-flow-controller.ts', import.meta.url),
9
9
  'utf8',
10
10
  );
11
11
 
@@ -45,7 +45,7 @@ describe('claimbee billing contract', () => {
45
45
  });
46
46
 
47
47
  it('exports minimal live billing config and normalized discounts', async () => {
48
- const billingModule = await import('./billing.plans');
48
+ const billingModule = await import('../../../src/config/billing.plans');
49
49
  const {
50
50
  billing,
51
51
  billingDiscountList,
@@ -87,7 +87,7 @@ describe('claimbee billing contract', () => {
87
87
 
88
88
  it('uses test provider price ids when Stripe mode is test', async () => {
89
89
  stubRuntimeMode('test');
90
- const { billing } = await import('./billing.plans');
90
+ const { billing } = await import('../../../src/config/billing.plans');
91
91
 
92
92
  expect(billing.plans.primary.projectPlanId).toBe(CLAIMBEE_PROJECT_PLAN_IDS.weekly);
93
93
  expect(billing.plans.primary.providerPlanId).toBe(CLAIMBEE_TEST_PROVIDER_PLAN_IDS.weekly);
@@ -98,7 +98,7 @@ describe('claimbee billing contract', () => {
98
98
  });
99
99
 
100
100
  it('exports the split test plan catalog under the generic billingTestPlans name', async () => {
101
- const billingTestModule = await import('./billing.test.plans') as Record<string, unknown>;
101
+ const billingTestModule = await import('../../../src/config/billing.test.plans') as Record<string, unknown>;
102
102
 
103
103
  expect(billingTestModule.billingTestPlans).toEqual({
104
104
  primary: expect.objectContaining({
@@ -117,7 +117,7 @@ describe('claimbee billing contract', () => {
117
117
  });
118
118
 
119
119
  it('lets payments helpers derive ordered paywall plans from semantic keys', async () => {
120
- const { billingPlans } = await import('./billing.plans');
120
+ const { billingPlans } = await import('../../../src/config/billing.plans');
121
121
 
122
122
  expect(buildConfigPaywallPlans(billingPlans, ['primary', 'secondary', 'tertiary'])).toEqual([
123
123
  expect.objectContaining({
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { experiments, manifestExperiments } from './experiments';
2
+ import { experiments, manifestExperiments } from '../../../src/config/experiments';
3
3
 
4
4
  describe('funnel experiments config', () => {
5
5
  it('ships the generic template without brand-specific experiments', () => {
@@ -8,7 +8,7 @@ import {
8
8
  import { stepRegistry } from '@/runtime/step-registry';
9
9
  import { stepTwoContent, stepTwoOptions } from '@/steps/content/step-02.content';
10
10
  import { contentOptionsByStepId } from '@/steps/step-content.registry';
11
- import { funnelManifest } from './funnel.manifest';
11
+ import { funnelManifest } from '../../../src/config/funnel.manifest';
12
12
 
13
13
  describe('funnel manifest breakpoints', () => {
14
14
  it('ships the four default QA breakpoints for new funnels', () => {
@@ -5,14 +5,14 @@ import {
5
5
  CURRENT_STEP_CONTRACT_VERSION,
6
6
  type FunnelStepContractProjectSnapshot,
7
7
  } from '@funnelsgrove/runtime';
8
- import docsManifest from '../../.funnelsgrove-docs.json';
8
+ import docsManifest from '../../../.funnelsgrove-docs.json';
9
9
  import { manifestExperiments } from '@/config/experiments';
10
10
  import { funnelManifest, rawFunnelManifest } from '@/config/funnel.manifest';
11
11
  import { stepRegistry } from '@/runtime/step-registry';
12
12
  import { contentOptionsByStepId } from '@/steps/step-content.registry';
13
13
 
14
14
  const loadProjectValidation = async () => {
15
- return import('./funnel-project.validation');
15
+ return import('../../../src/contract/funnel-project.validation');
16
16
  };
17
17
 
18
18
  describe('funnel project validation', () => {
@@ -9,22 +9,22 @@ import {
9
9
  type FunnelManifest,
10
10
  } from '@funnelsgrove/runtime';
11
11
  import { publicAnalyticsSdk } from '@funnelsgrove/analytics';
12
- import { goldenFixture as cancellationOffer } from './cancellation_offer.fixture';
13
- import { goldenFixture as checkout } from './checkout.fixture';
14
- import { goldenFixture as purchaseCompleted } from './purchase_completed.fixture';
15
- import { goldenFixture as formInput } from './form_input.fixture';
16
- import { goldenFixture as introHero } from './intro_hero.fixture';
17
- import { goldenFixture as multiSelectChoice } from './multi_select_choice.fixture';
18
- import { goldenFixture as paywallOffer } from './paywall_offer.fixture';
19
- import { goldenFixture as progressInterstitial } from './progress_interstitial.fixture';
20
- import { goldenFixture as singleStepChoice } from './single_step_choice.fixture';
21
- import { goldenFixture as singleStepChoiceEmoji } from './single_step_choice_emoji.fixture';
22
- import { goldenFixture as socialProof } from './social_proof.fixture';
23
- import { goldenFixture as subscriptionHandoff } from './subscription_handoff.fixture';
24
- import { goldenFixture as subscriptionManagement } from './subscription_management.fixture';
25
- import { goldenFixture as summaryConfirmation } from './summary_confirmation.fixture';
26
- import { goldenFixture as upsellOffer } from './upsell_offer.fixture';
27
- import { goldenFixture as valuePropStory } from './value_prop_story.fixture';
12
+ import { goldenFixture as cancellationOffer } from '../../../../src/contract/golden/cancellation_offer.fixture';
13
+ import { goldenFixture as checkout } from '../../../../src/contract/golden/checkout.fixture';
14
+ import { goldenFixture as purchaseCompleted } from '../../../../src/contract/golden/purchase_completed.fixture';
15
+ import { goldenFixture as formInput } from '../../../../src/contract/golden/form_input.fixture';
16
+ import { goldenFixture as introHero } from '../../../../src/contract/golden/intro_hero.fixture';
17
+ import { goldenFixture as multiSelectChoice } from '../../../../src/contract/golden/multi_select_choice.fixture';
18
+ import { goldenFixture as paywallOffer } from '../../../../src/contract/golden/paywall_offer.fixture';
19
+ import { goldenFixture as progressInterstitial } from '../../../../src/contract/golden/progress_interstitial.fixture';
20
+ import { goldenFixture as singleStepChoice } from '../../../../src/contract/golden/single_step_choice.fixture';
21
+ import { goldenFixture as singleStepChoiceEmoji } from '../../../../src/contract/golden/single_step_choice_emoji.fixture';
22
+ import { goldenFixture as socialProof } from '../../../../src/contract/golden/social_proof.fixture';
23
+ import { goldenFixture as subscriptionHandoff } from '../../../../src/contract/golden/subscription_handoff.fixture';
24
+ import { goldenFixture as subscriptionManagement } from '../../../../src/contract/golden/subscription_management.fixture';
25
+ import { goldenFixture as summaryConfirmation } from '../../../../src/contract/golden/summary_confirmation.fixture';
26
+ import { goldenFixture as upsellOffer } from '../../../../src/contract/golden/upsell_offer.fixture';
27
+ import { goldenFixture as valuePropStory } from '../../../../src/contract/golden/value_prop_story.fixture';
28
28
 
29
29
  const fixtures = [
30
30
  cancellationOffer,
@@ -12,7 +12,7 @@ import {
12
12
  const execFileAsync = promisify(execFile);
13
13
 
14
14
  const loadCli = async () => {
15
- return import('./validate-funnel.cli');
15
+ return import('../../../src/contract/validate-funnel.cli');
16
16
  };
17
17
 
18
18
  const diagnostic = createFunnelContractDiagnostic({
@@ -150,7 +150,7 @@ describe('validate funnel CLI protocol', () => {
150
150
  });
151
151
 
152
152
  it('runs through vite-node with one clean stdout protocol object', async () => {
153
- const templateRoot = path.resolve(fileURLToPath(new URL('../..', import.meta.url)));
153
+ const templateRoot = path.resolve(fileURLToPath(new URL('../../..', import.meta.url)));
154
154
  const viteNode = path.resolve(templateRoot, '../../node_modules/.bin/vite-node');
155
155
  const script = path.resolve(templateRoot, 'src/contract/validate-funnel.cli.ts');
156
156
 
@@ -23,7 +23,7 @@ describe('checkoutRuntimeConfig', () => {
23
23
  it('does not invent a checkout SDK publishable key from generic Stripe env', async () => {
24
24
  process.env.NEXT_PUBLIC_STRIPE_TEST_PUBLISHABLE_KEY = 'pk_test_generic_runtime_value';
25
25
 
26
- const { checkoutRuntimeConfig } = await import('./checkout-runtime-config');
26
+ const { checkoutRuntimeConfig } = await import('../../../src/runtime/checkout-runtime-config');
27
27
 
28
28
  expect(checkoutRuntimeConfig.funnelSdkPublishableKey).toBeNull();
29
29
  });
@@ -43,7 +43,7 @@ describe('checkoutRuntimeConfig', () => {
43
43
  };
44
44
  });
45
45
 
46
- const { checkoutRuntimeConfig, resolveClaimbeeCheckoutMode } = await import('./checkout-runtime-config');
46
+ const { checkoutRuntimeConfig, resolveClaimbeeCheckoutMode } = await import('../../../src/runtime/checkout-runtime-config');
47
47
 
48
48
  expect(checkoutRuntimeConfig.funnelSdkPublishableKey).toBe(
49
49
  'sdk_pub_claimbee_project',
@@ -52,7 +52,7 @@ describe('checkoutRuntimeConfig', () => {
52
52
  });
53
53
 
54
54
  it('uses the server-provided runtime mode when no preview key is injected', async () => {
55
- const { resolveClaimbeeCheckoutMode } = await import('./checkout-runtime-config');
55
+ const { resolveClaimbeeCheckoutMode } = await import('../../../src/runtime/checkout-runtime-config');
56
56
 
57
57
  expect(resolveClaimbeeCheckoutMode('live')).toBe('live');
58
58
  expect(resolveClaimbeeCheckoutMode('test')).toBe('test');
@@ -4,7 +4,7 @@ import {
4
4
  defaultFunnelStepId,
5
5
  resolveConfiguredNextStep,
6
6
  resolveRuntimeInitialStepId,
7
- } from './funnel-runtime';
7
+ } from '../../../src/runtime/funnel-runtime';
8
8
 
9
9
  describe('funnel runtime', () => {
10
10
  it('falls back to the first manifest step when no valid step is requested', () => {
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
2
2
  import {
3
3
  getStepContentLocale,
4
4
  getStepCountryCode,
5
- } from './step-content-context';
5
+ } from '../../../src/runtime/step-content-context';
6
6
 
7
7
  describe('step content context helpers', () => {
8
8
  it('prefers stored browser language and explicit country when present', () => {
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
3
 
4
4
  const { posthogFlagsPath } = vi.hoisted(() => ({
5
5
  posthogFlagsPath: new URL(
6
- '../../../funnel-runtime/src/runtime/posthog-flags.ts',
6
+ '../../../../funnel-runtime/src/runtime/posthog-flags.ts',
7
7
  import.meta.url,
8
8
  ).pathname,
9
9
  }));
@@ -31,7 +31,7 @@ import {
31
31
  resolveExperimentVariant,
32
32
  resolveNextStepId,
33
33
  } from '@funnelsgrove/runtime';
34
- import { computeRenderSuspended } from './use-funnel-flow-controller';
34
+ import { computeRenderSuspended } from '../../../src/runtime/use-funnel-flow-controller';
35
35
 
36
36
  const mockedResolveVariant = vi.mocked(resolveExperimentVariant);
37
37
 
@@ -187,9 +187,9 @@ describe.skipIf(!insideMonorepo)('PostHog variant resolution (integration)', ()
187
187
  });
188
188
 
189
189
  it('renders experiment variants on the source URL before normal continuation', () => {
190
- const adapterSource = readFileSync(new URL('./use-funnel-flow-controller.ts', import.meta.url), 'utf8');
190
+ const adapterSource = readFileSync(new URL('../../../src/runtime/use-funnel-flow-controller.ts', import.meta.url), 'utf8');
191
191
  const runtimeSource = readFileSync(
192
- new URL('../../../funnel-runtime/src/runtime/use-funnel-flow-controller.ts', import.meta.url),
192
+ new URL('../../../../funnel-runtime/src/runtime/use-funnel-flow-controller.ts', import.meta.url),
193
193
  'utf8',
194
194
  );
195
195
 
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { readFileSync } from 'node:fs';
3
- import { computeRenderSuspended } from './use-funnel-flow-controller';
3
+ import { computeRenderSuspended } from '../../../src/runtime/use-funnel-flow-controller';
4
4
 
5
5
  describe('computeRenderSuspended', () => {
6
6
  const experiments = [
@@ -52,7 +52,7 @@ describe('computeRenderSuspended', () => {
52
52
 
53
53
  describe('qualified visitor analytics', () => {
54
54
  it('delegates flow behavior to the shared runtime controller with template analytics', () => {
55
- const source = readFileSync(new URL('./use-funnel-flow-controller.ts', import.meta.url), 'utf8');
55
+ const source = readFileSync(new URL('../../../src/runtime/use-funnel-flow-controller.ts', import.meta.url), 'utf8');
56
56
 
57
57
  expect(source).toContain('useFunnelFlowController as useSharedFunnelFlowController');
58
58
  expect(source).toContain('return useSharedFunnelFlowController<FunnelStepId>({');
@@ -66,7 +66,7 @@ describe('qualified visitor analytics', () => {
66
66
  });
67
67
 
68
68
  it('does not carry a local copy of shared controller internals', () => {
69
- const source = readFileSync(new URL('./use-funnel-flow-controller.ts', import.meta.url), 'utf8');
69
+ const source = readFileSync(new URL('../../../src/runtime/use-funnel-flow-controller.ts', import.meta.url), 'utf8');
70
70
 
71
71
  expect(source).not.toContain('FIRST_STEP_ENGAGEMENT_THRESHOLD_MS');
72
72
  expect(source).not.toContain('apiService.bootstrapSession');
@@ -3,7 +3,7 @@ import {
3
3
  buildPaywallCheckoutBillingDetails,
4
4
  buildPaywallCheckoutExpressOptions,
5
5
  buildPaywallCheckoutPaymentElementOptions,
6
- } from './paywall-checkout-options';
6
+ } from '../../../src/steps/paywall-checkout-options';
7
7
 
8
8
  describe('paywall checkout options', () => {
9
9
  it('prioritizes Link and stacked express checkout buttons for the checkout dialog', () => {
@@ -2,8 +2,10 @@ import { readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { describe, expect, it } from 'vitest';
4
4
 
5
+ const sourceStepsDirectory = path.resolve(__dirname, '../../../src/steps');
6
+
5
7
  const readStepSource = (fileName: string): string =>
6
- readFileSync(path.resolve(__dirname, fileName), 'utf8');
8
+ readFileSync(path.resolve(sourceStepsDirectory, fileName), 'utf8');
7
9
 
8
10
  describe('template shared header contract', () => {
9
11
  it('renders template step headers through the shared FunnelStepHeader component', () => {
@@ -1,10 +1,10 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { describe, expect, it } from 'vitest';
4
- import { stepTwoContent, stepTwoOptions } from './content/step-02.content';
5
- import { stepTwo } from './step-02';
4
+ import { stepTwoContent, stepTwoOptions } from '../../../src/steps/content/step-02.content';
5
+ import { stepTwo } from '../../../src/steps/step-02';
6
6
 
7
- const stepSource = readFileSync(path.resolve(__dirname, './step-02.tsx'), 'utf8');
7
+ const stepSource = readFileSync(path.resolve(__dirname, '../../../src/steps/step-02.tsx'), 'utf8');
8
8
 
9
9
  describe('StepTwo', () => {
10
10
  it('declares its typed multi-select answer contract and immutable option IDs', () => {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { resolveStepThreeAutoAdvanceMs, stepThree } from './step-03';
2
+ import { resolveStepThreeAutoAdvanceMs, stepThree } from '../../../src/steps/step-03';
3
3
 
4
4
  describe('StepThree', () => {
5
5
  it('keeps the shell action bar hidden because the step owns its own auto-advance behavior', () => {
@@ -14,7 +14,7 @@ describe('StepFour scratchpad', () => {
14
14
  });
15
15
 
16
16
  it('reveals the scratch ticket before advancing through the manifest edge', () => {
17
- const stepSource = readFileSync(path.resolve(__dirname, './step-04.tsx'), 'utf8');
17
+ const stepSource = readFileSync(path.resolve(__dirname, '../../../src/steps/step-04.tsx'), 'utf8');
18
18
 
19
19
  expect(stepSource).toContain("data-node-id='7390:1456'");
20
20
  expect(stepSource).toContain("data-node-id='6992:6168'");
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest';
3
3
 
4
4
  describe('email capture workflow ownership', () => {
5
5
  it('only validates UI state and calls the shared context helper', () => {
6
- const source = readFileSync(new URL('./step-31-email-capture.tsx', import.meta.url), 'utf8');
6
+ const source = readFileSync(new URL('../../../src/steps/step-31-email-capture.tsx', import.meta.url), 'utf8');
7
7
 
8
8
  expect(source).toContain('submitEmailCapture } = useFunnel();');
9
9
  expect(source).toContain('await submitEmailCapture(email);');