@vitronai/themis 0.1.12 → 0.1.14

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/CHANGELOG.md CHANGED
@@ -4,12 +4,16 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.1.14 - 2026-03-27
8
+
7
9
  - Added first-party `npx themis test --fix` support so generated-test repair loops can apply fix-handoff autofixes, tighten hints when needed, and rerun the suite directly from the CLI.
8
10
  - Moved the generated contract runtime into the npm package (`@vitronai/themis/contract-runtime`), stopped `init` from creating `tests/example.test.js`, taught generated tests to emit `.generated.test.ts` for TS/TSX sources, and made `init` / `migrate` add `.themis/` to downstream `.gitignore`.
9
11
  - Reorganized framework-managed artifacts under `.themis/` into subdirectories like `.themis/runs/`, `.themis/diffs/`, `.themis/generate/`, `.themis/reports/`, `.themis/migration/`, and `.themis/benchmarks/` so volatile output stays bundled but easier to navigate.
10
12
  - Added native React showcase fixtures for Themis, Jest, and Vitest plus a dedicated first-party Themis CI showcase job.
11
13
  - Added a same-host React showcase benchmark job and uploaded performance artifact so CI now records one direct Themis vs Jest vs Vitest timing comparison for the exact same showcase specs.
12
14
  - Added ESLint with a dedicated CI lint job and folded lint into local validation and prepublish checks.
15
+ - Generated `.generated.test.ts` output now typechecks cleanly under strict TypeScript without requiring downstream `types` overrides, and the packaged `@vitronai/themis/contract-runtime` surface now ships first-party type declarations.
16
+ - Refined the in-repo VS Code extension scaffold with judge-only branding assets, semantic Themis review colors, and a themed HTML report webview shell so artifact review inside VS Code reads like the rest of the product.
13
17
 
14
18
  ## 0.1.4 - 2026-03-26
15
19
 
package/README.md CHANGED
@@ -93,7 +93,8 @@ Themis is built for modern Node.js and TypeScript projects:
93
93
 
94
94
  ## Adopt In Another Repo
95
95
 
96
- Use the AI Quickstart above as the canonical install/generate/test flow. Generated files land under `__themis__/tests` by default.
96
+ Use the AI Quickstart above as the canonical install/generate/test flow. Generated files land under `__themis__/tests` by default. TypeScript-generated tests are emitted as strict-typecheckable artifacts and self-reference Themis globals so downstream TS projects do not need a special `types` override just to compile generated output.
97
+ TypeScript-generated suites use `import` syntax so downstream ESLint and ESM-style rules do not flag Themis output as legacy `require(...)` code.
97
98
 
98
99
  If another repo wants its agents to reliably choose Themis, put the framework choice directly in that repo's agent instructions instead of assuming agents will infer it from package metadata alone.
99
100
 
@@ -295,7 +296,7 @@ Short version:
295
296
  - Migration proof job runs `npm run proof:migration` against checked-in Jest/Vitest fixtures for basic suites, table tests, RTL/jsdom flows, timers, module mocking, and a context/provider-heavy RTL example, then uploads the resulting migration reports plus Themis run artifacts as evidence.
296
297
  - Themis React Showcase job verifies a straight-up native Themis React fixture as a first-party example.
297
298
  - React showcase perf job runs `npm run benchmark:showcase` on the exact same React scenarios for Themis, Jest, and Vitest on one CI host, then uploads `.themis/benchmarks/showcase-comparison/perf-summary.{json,md}` so the relative timing claim is backed by one comparable artifact.
298
- - Release `0.1.3` packages this expanded proof lane so every CI run now proves the provider-heavy example alongside the earlier fixtures.
299
+ - Release `0.1.14` packages this expanded proof lane so every CI run now proves the provider-heavy example alongside the earlier fixtures.
299
300
 
300
301
  ## Agent Guide
301
302
 
@@ -0,0 +1 @@
1
+ export * from './src/contract-runtime';
package/docs/api.md CHANGED
@@ -62,6 +62,7 @@ Default behavior:
62
62
  - input directory: `src`
63
63
  - output directory: `__themis__/tests`
64
64
  - generated files mirror the scanned source tree with `*.generated.test.ts` for TS/TSX sources and `*.generated.test.js` for JS/JSX sources
65
+ - generated TypeScript suites emit `import` syntax so downstream lint and ESM rules do not reject Themis output for using `require(...)`
65
66
  - generated tests import their shared contract runtime from `@vitronai/themis/contract-runtime` instead of writing framework helper files into the repo
66
67
  - generated tests assert normalized runtime export contracts directly in generated source
67
68
  - scenario adapters cover React components, React hooks, Next app components, Next route handlers, generic route handlers, and Node service functions when inputs can be inferred or hinted
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitronai/themis",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Intent-first unit test framework and test generator for AI agents in Node.js and TypeScript",
5
5
  "license": "MIT",
6
6
  "author": "Vitron AI",
@@ -55,6 +55,7 @@
55
55
  "default": "./globals.js"
56
56
  },
57
57
  "./contract-runtime": {
58
+ "types": "./contract-runtime.d.ts",
58
59
  "require": "./src/contract-runtime.js",
59
60
  "default": "./src/contract-runtime.js"
60
61
  },
@@ -63,6 +64,7 @@
63
64
  "files": [
64
65
  "bin",
65
66
  "src/*.js",
67
+ "src/*.d.ts",
66
68
  "src/assets/*",
67
69
  "docs",
68
70
  "templates",
@@ -70,6 +72,7 @@
70
72
  "index.d.ts",
71
73
  "globals.js",
72
74
  "globals.d.ts",
75
+ "contract-runtime.d.ts",
73
76
  "themis.ai.json",
74
77
  "README.md",
75
78
  "CHANGELOG.md",
@@ -0,0 +1,62 @@
1
+ export interface ModuleContractEntry {
2
+ kind: string;
3
+ value?: unknown;
4
+ arity?: number;
5
+ ownKeys?: string[];
6
+ prototypeKeys?: string[];
7
+ length?: number;
8
+ itemTypes?: string[];
9
+ source?: string;
10
+ flags?: string;
11
+ size?: number;
12
+ keys?: string[];
13
+ constructor?: string;
14
+ name?: string;
15
+ }
16
+
17
+ export type ModuleContract = Record<string, ModuleContractEntry>;
18
+
19
+ export interface RequestSpec {
20
+ method?: string;
21
+ url: string;
22
+ headers?: Record<string, string>;
23
+ body?: unknown;
24
+ json?: unknown;
25
+ }
26
+
27
+ export function listExportNames(moduleExports: unknown): string[];
28
+ export function buildModuleContract(moduleExports: unknown): ModuleContract;
29
+ export function readExportValue<TValue = unknown>(moduleExports: unknown, name: string): TValue;
30
+ export function normalizeBehaviorValue(value: unknown): unknown;
31
+ export function normalizeRouteResult(value: unknown): Promise<unknown>;
32
+ export function createRequestFromSpec(spec: RequestSpec): unknown;
33
+ export function assertSourceFreshness(
34
+ sourceFile: string,
35
+ expectedHash: string,
36
+ sourceLabel: string,
37
+ regenerateCommand: string
38
+ ): void;
39
+ export function runComponentInteractionContract(
40
+ sourceFile: string,
41
+ exportName: string,
42
+ props: Record<string, unknown>,
43
+ interactionPlan?: unknown[],
44
+ options?: {
45
+ wrapRender?: (element: unknown) => unknown;
46
+ }
47
+ ): Promise<unknown>;
48
+ export function runComponentBehaviorFlowContract(
49
+ sourceFile: string,
50
+ exportName: string,
51
+ props: Record<string, unknown>,
52
+ flowPlan?: unknown[],
53
+ options?: {
54
+ wrapRender?: (element: unknown) => unknown;
55
+ }
56
+ ): Promise<unknown>;
57
+ export function runHookInteractionContract(
58
+ sourceFile: string,
59
+ exportName: string,
60
+ args: unknown[],
61
+ interactionPlan?: unknown[]
62
+ ): unknown;
package/src/generate.js CHANGED
@@ -3232,69 +3232,198 @@ function renderGeneratedTest({ projectRoot, helperFile, outputFile, analysis })
3232
3232
  const helperImport = helperFile;
3233
3233
  const sourceImport = normalizeRelativeModule(path.relative(path.dirname(outputFile), analysis.file));
3234
3234
  const sourceAbsolutePath = normalizePath(path.relative(path.dirname(outputFile), analysis.file));
3235
+ const useImportSyntax = path.extname(outputFile).toLowerCase() === '.ts';
3236
+ const typeReferences = useImportSyntax ? '/// <reference types="@vitronai/themis/globals" />\n' : '';
3237
+ const typePrelude = useImportSyntax ? `${renderGeneratedTypePrelude()}\n\n` : '';
3235
3238
  const expectedExportContracts = buildExpectedExportContracts(analysis);
3236
3239
  const providerImport = analysis.projectProviderFile
3237
3240
  ? normalizeRelativeModule(path.relative(path.dirname(outputFile), analysis.projectProviderFile))
3238
3241
  : null;
3239
3242
  const suiteName = `generated contract > ${relativeSourcePath}`;
3240
3243
  const exactExportBlock = analysis.exactExports
3241
- ? `test('matches scanned export names', () => {\n const moduleExports = loadModuleExports();\n expect(listExportNames(moduleExports)).toEqual(SCANNED_EXPORTS);\n });`
3242
- : `test('exposes runtime exports after scan', () => {\n const moduleExports = loadModuleExports();\n expect(listExportNames(moduleExports).length).toBeTruthy();\n });`;
3244
+ ? `test('matches scanned export names', async () => {\n const moduleExports = await loadModuleExports();\n expect(listExportNames(moduleExports)).toEqual(SCANNED_EXPORTS);\n });`
3245
+ : `test('exposes runtime exports after scan', async () => {\n const moduleExports = await loadModuleExports();\n expect(listExportNames(moduleExports).length).toBeTruthy();\n });`;
3246
+ const runtimePrelude = useImportSyntax
3247
+ ? renderGeneratedImportPrelude({ helperImport, providerImport })
3248
+ : renderGeneratedRequirePrelude({ helperImport });
3249
+ const providerLoadExpression = useImportSyntax ? 'PROJECT_PROVIDER_MODULE' : 'require(PROJECT_PROVIDER_IMPORT)';
3250
+ const loadModuleBody = useImportSyntax
3251
+ ? ' assertSourceFreshness(SOURCE_FILE, SOURCE_HASH, SOURCE_PATH, REGENERATE_COMMAND);\n return import(SOURCE_IMPORT) as Promise<RuntimeModuleExports>;'
3252
+ : ' assertSourceFreshness(SOURCE_FILE, SOURCE_HASH, SOURCE_PATH, REGENERATE_COMMAND);\n const resolved = require.resolve(SOURCE_IMPORT);\n delete require.cache[resolved];\n return require(SOURCE_IMPORT);';
3253
+ const sourceFileDeclaration = useImportSyntax
3254
+ ? 'const SOURCE_FILE = require.resolve(SOURCE_IMPORT);'
3255
+ : `const SOURCE_FILE = path.resolve(__dirname, ${JSON.stringify(sourceAbsolutePath)});`;
3256
+ const scannedExportsDeclaration = useImportSyntax
3257
+ ? `const SCANNED_EXPORTS: readonly string[] | null = ${analysis.exactExports ? JSON.stringify(analysis.exportNames, null, 2) : 'null'};`
3258
+ : `const SCANNED_EXPORTS = ${analysis.exactExports ? JSON.stringify(analysis.exportNames, null, 2) : 'null'};`;
3259
+ const expectedExportDeclaration = useImportSyntax
3260
+ ? `const EXPECTED_EXPORT_CONTRACTS: RuntimeModuleContractExpectations = ${JSON.stringify(expectedExportContracts, null, 2)};`
3261
+ : `const EXPECTED_EXPORT_CONTRACTS = ${JSON.stringify(expectedExportContracts, null, 2)};`;
3262
+ const providerImportDeclaration = useImportSyntax
3263
+ ? `const PROJECT_PROVIDER_IMPORT: string | null = ${providerImport ? JSON.stringify(providerImport) : 'null'};`
3264
+ : `const PROJECT_PROVIDER_IMPORT = ${providerImport ? JSON.stringify(providerImport) : 'null'};`;
3265
+ const providerIndexesDeclaration = useImportSyntax
3266
+ ? `const PROJECT_PROVIDER_INDEXES: readonly number[] = ${JSON.stringify(analysis.providerRuntimeIndexes || [], null, 2)};`
3267
+ : `const PROJECT_PROVIDER_INDEXES = ${JSON.stringify(analysis.providerRuntimeIndexes || [], null, 2)};`;
3268
+ const providerPresetsDeclaration = useImportSyntax
3269
+ ? `const PROJECT_PROVIDER_PRESETS: readonly ProviderPreset[] = ${JSON.stringify(analysis.providerRuntimePresets || [], null, 2)};`
3270
+ : `const PROJECT_PROVIDER_PRESETS = ${JSON.stringify(analysis.providerRuntimePresets || [], null, 2)};`;
3271
+ const nextAppCasesDeclaration = useImportSyntax
3272
+ ? `const NEXT_APP_CASES: readonly ComponentCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'next-app-component'), null, 2)};`
3273
+ : `const NEXT_APP_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'next-app-component'), null, 2)};`;
3274
+ const nextRouteCasesDeclaration = useImportSyntax
3275
+ ? `const NEXT_ROUTE_CASES: readonly RouteCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'next-route-handler'), null, 2)};`
3276
+ : `const NEXT_ROUTE_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'next-route-handler'), null, 2)};`;
3277
+ const componentCasesDeclaration = useImportSyntax
3278
+ ? `const COMPONENT_CASES: readonly ComponentCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'react-component'), null, 2)};`
3279
+ : `const COMPONENT_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'react-component'), null, 2)};`;
3280
+ const hookCasesDeclaration = useImportSyntax
3281
+ ? `const HOOK_CASES: readonly HookCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'react-hook'), null, 2)};`
3282
+ : `const HOOK_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'react-hook'), null, 2)};`;
3283
+ const routeCasesDeclaration = useImportSyntax
3284
+ ? `const ROUTE_CASES: readonly RouteCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'route-handler'), null, 2)};`
3285
+ : `const ROUTE_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'route-handler'), null, 2)};`;
3286
+ const serviceCasesDeclaration = useImportSyntax
3287
+ ? `const SERVICE_CASES: readonly ServiceCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'node-service'), null, 2)};`
3288
+ : `const SERVICE_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'node-service'), null, 2)};`;
3289
+ const loadModuleSignature = useImportSyntax
3290
+ ? 'function loadModuleExports(): Promise<RuntimeModuleExports> {'
3291
+ : 'function loadModuleExports() {';
3292
+ const resolveProvidersSignature = useImportSyntax
3293
+ ? 'function resolveProjectProviders(loaded: unknown): ProjectProviderRuntime[] {'
3294
+ : 'function resolveProjectProviders(loaded) {';
3295
+ const applyMocksSignature = useImportSyntax
3296
+ ? 'function applyProjectProviderMocks(exportName: string, scenarioName: ScenarioName): void {'
3297
+ : 'function applyProjectProviderMocks(exportName, scenarioName) {';
3298
+ const applyRenderSignature = useImportSyntax
3299
+ ? 'function applyProjectProviderRender(element: unknown, exportName: string, scenarioName: ScenarioName): unknown {'
3300
+ : 'function applyProjectProviderRender(element, exportName, scenarioName) {';
3301
+ const createRenderOptionsSignature = useImportSyntax
3302
+ ? 'function createProjectProviderRenderOptions(exportName: string, scenarioName: ScenarioName): { wrapRender(element: unknown): unknown } {'
3303
+ : 'function createProjectProviderRenderOptions(exportName, scenarioName) {';
3304
+ const applyPresetsSignature = useImportSyntax
3305
+ ? 'function applyProjectProviderPresets(element: unknown): unknown {'
3306
+ : 'function applyProjectProviderPresets(element) {';
3307
+ const withProviderShellSignature = useImportSyntax
3308
+ ? 'function withProviderShell(type: string, element: unknown, attributes: ProviderConfig = {}): ReactTestElement {'
3309
+ : 'function withProviderShell(type, element, attributes = {}) {';
3310
+ const providerHelperSignature = (name) => useImportSyntax
3311
+ ? `function ${name}(element: unknown, config: ProviderConfig = {}): ReactTestElement {`
3312
+ : `function ${name}(element, config = {}) {`;
3313
+ const serializeProviderDataSignature = useImportSyntax
3314
+ ? 'function serializeProviderData(value: unknown): string {'
3315
+ : 'function serializeProviderData(value) {';
3316
+ const assertExpectedRuntimeContractSignature = useImportSyntax
3317
+ ? 'function assertExpectedRuntimeContract(runtime: RuntimeModuleExports): void {'
3318
+ : 'function assertExpectedRuntimeContract(runtime) {';
3319
+ const assertNormalizedRenderContractSignature = useImportSyntax
3320
+ ? 'function assertNormalizedRenderContract(rendered: unknown): void {'
3321
+ : 'function assertNormalizedRenderContract(rendered) {';
3322
+ const assertDomContractShapeSignature = useImportSyntax
3323
+ ? 'function assertDomContractShape(contract: DomContract): void {'
3324
+ : 'function assertDomContractShape(contract) {';
3325
+ const countPlannedStepsSignature = useImportSyntax
3326
+ ? 'function countPlannedSteps(plan: readonly RepeatedPlanStep[] | null | undefined): number {'
3327
+ : 'function countPlannedSteps(plan) {';
3328
+ const assertComponentInteractionContractShapeSignature = useImportSyntax
3329
+ ? 'function assertComponentInteractionContractShape(contract: ComponentInteractionContract, plan: readonly InteractionPlanStep[] | null | undefined): void {'
3330
+ : 'function assertComponentInteractionContractShape(contract, plan) {';
3331
+ const assertFlowContractShapeSignature = useImportSyntax
3332
+ ? 'function assertFlowContractShape(flow: ComponentBehaviorFlowContract, plan: readonly FlowPlanStep[] | null | undefined): void {'
3333
+ : 'function assertFlowContractShape(flow, plan) {';
3334
+ const assertHookResultContractSignature = useImportSyntax
3335
+ ? 'function assertHookResultContract(result: unknown): void {'
3336
+ : 'function assertHookResultContract(result) {';
3337
+ const assertHookInteractionContractShapeSignature = useImportSyntax
3338
+ ? 'function assertHookInteractionContractShape(contract: HookInteractionContract, plan: readonly HookInteractionPlanStep[] | null | undefined): void {'
3339
+ : 'function assertHookInteractionContractShape(contract, plan) {';
3340
+ const assertRouteResultContractShapeSignature = useImportSyntax
3341
+ ? 'function assertRouteResultContractShape(response: unknown): void {'
3342
+ : 'function assertRouteResultContractShape(response) {';
3343
+ const assertServiceResultContractShapeSignature = useImportSyntax
3344
+ ? 'function assertServiceResultContractShape(result: unknown): void {'
3345
+ : 'function assertServiceResultContractShape(result) {';
3346
+ const componentReadExpression = useImportSyntax
3347
+ ? 'readExportValue<(props: Record<string, unknown>) => unknown>(moduleExports, testCase.exportName)'
3348
+ : 'readExportValue(moduleExports, testCase.exportName)';
3349
+ const hookReadExpression = useImportSyntax
3350
+ ? 'readExportValue<(...args: unknown[]) => unknown>(moduleExports, testCase.exportName)'
3351
+ : 'readExportValue(moduleExports, testCase.exportName)';
3352
+ const handlerReadExpression = useImportSyntax
3353
+ ? 'readExportValue<(request: unknown, context?: unknown) => unknown>(moduleExports, testCase.exportName)'
3354
+ : 'readExportValue(moduleExports, testCase.exportName)';
3355
+ const serviceReadExpression = useImportSyntax
3356
+ ? 'readExportValue<(...args: unknown[]) => unknown>(moduleExports, testCase.exportName)'
3357
+ : 'readExportValue(moduleExports, testCase.exportName)';
3358
+ const interactionContractExpression = useImportSyntax
3359
+ ? 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\')) as ComponentInteractionContract'
3360
+ : 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\'))';
3361
+ const nextDomContractExpression = useImportSyntax
3362
+ ? 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\')) as ComponentInteractionContract'
3363
+ : 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\'))';
3364
+ const nextFlowExpression = useImportSyntax
3365
+ ? 'await runComponentBehaviorFlowContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.flows, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\')) as ComponentBehaviorFlowContract'
3366
+ : 'await runComponentBehaviorFlowContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.flows, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\'))';
3367
+ const componentInteractionExpression = useImportSyntax
3368
+ ? 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'react-component\')) as ComponentInteractionContract'
3369
+ : 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'react-component\'))';
3370
+ const componentDomExpression = useImportSyntax
3371
+ ? 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'react-component\')) as ComponentInteractionContract'
3372
+ : 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'react-component\'))';
3373
+ const componentFlowExpression = useImportSyntax
3374
+ ? 'await runComponentBehaviorFlowContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.flows, createProjectProviderRenderOptions(testCase.exportName, \'react-component\')) as ComponentBehaviorFlowContract'
3375
+ : 'await runComponentBehaviorFlowContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.flows, createProjectProviderRenderOptions(testCase.exportName, \'react-component\'))';
3376
+ const hookInteractionExpression = useImportSyntax
3377
+ ? 'runHookInteractionContract(SOURCE_FILE, testCase.exportName, testCase.args, testCase.interactions) as HookInteractionContract'
3378
+ : 'runHookInteractionContract(SOURCE_FILE, testCase.exportName, testCase.args, testCase.interactions)';
3243
3379
 
3244
3380
  return `${GENERATED_MARKER}
3245
- // Source: ${relativeSourcePath}
3381
+ ${typeReferences}// Source: ${relativeSourcePath}
3246
3382
 
3247
- const path = require('path');
3248
- const {
3249
- listExportNames,
3250
- buildModuleContract,
3251
- readExportValue,
3252
- normalizeBehaviorValue,
3253
- normalizeRouteResult,
3254
- createRequestFromSpec,
3255
- assertSourceFreshness,
3256
- runComponentInteractionContract,
3257
- runComponentBehaviorFlowContract,
3258
- runHookInteractionContract
3259
- } = require(${JSON.stringify(helperImport)});
3260
-
3261
- const SOURCE_PATH = ${JSON.stringify(relativeSourcePath)};
3383
+ ${runtimePrelude}
3384
+ ${typePrelude}const SOURCE_PATH = ${JSON.stringify(relativeSourcePath)};
3262
3385
  const SOURCE_IMPORT = ${JSON.stringify(sourceImport)};
3263
- const SOURCE_FILE = path.resolve(__dirname, ${JSON.stringify(sourceAbsolutePath)});
3386
+ ${sourceFileDeclaration}
3264
3387
  const SOURCE_HASH = ${JSON.stringify(analysis.sourceHash)};
3265
3388
  const REGENERATE_COMMAND = ${JSON.stringify(`npx themis generate ${relativeSourcePath}`)};
3266
- const SCANNED_EXPORTS = ${analysis.exactExports ? JSON.stringify(analysis.exportNames, null, 2) : 'null'};
3267
- const EXPECTED_EXPORT_CONTRACTS = ${JSON.stringify(expectedExportContracts, null, 2)};
3268
- const PROJECT_PROVIDER_IMPORT = ${providerImport ? JSON.stringify(providerImport) : 'null'};
3269
- const PROJECT_PROVIDER_INDEXES = ${JSON.stringify(analysis.providerRuntimeIndexes || [], null, 2)};
3270
- const PROJECT_PROVIDER_PRESETS = ${JSON.stringify(analysis.providerRuntimePresets || [], null, 2)};
3271
- const NEXT_APP_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'next-app-component'), null, 2)};
3272
- const NEXT_ROUTE_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'next-route-handler'), null, 2)};
3273
- const COMPONENT_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'react-component'), null, 2)};
3274
- const HOOK_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'react-hook'), null, 2)};
3275
- const ROUTE_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'route-handler'), null, 2)};
3276
- const SERVICE_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'node-service'), null, 2)};
3277
-
3278
- function loadModuleExports() {
3279
- assertSourceFreshness(SOURCE_FILE, SOURCE_HASH, SOURCE_PATH, REGENERATE_COMMAND);
3280
- const resolved = require.resolve(SOURCE_IMPORT);
3281
- delete require.cache[resolved];
3282
- return require(SOURCE_IMPORT);
3389
+ ${scannedExportsDeclaration}
3390
+ ${expectedExportDeclaration}
3391
+ ${providerImportDeclaration}
3392
+ ${providerIndexesDeclaration}
3393
+ ${providerPresetsDeclaration}
3394
+ ${nextAppCasesDeclaration}
3395
+ ${nextRouteCasesDeclaration}
3396
+ ${componentCasesDeclaration}
3397
+ ${hookCasesDeclaration}
3398
+ ${routeCasesDeclaration}
3399
+ ${serviceCasesDeclaration}
3400
+
3401
+ ${loadModuleSignature}
3402
+ ${loadModuleBody}
3403
+ }
3404
+
3405
+ ${resolveProvidersSignature}
3406
+ if (Array.isArray(loaded)) {
3407
+ return loaded;
3408
+ }
3409
+
3410
+ if (loaded && typeof loaded === 'object') {
3411
+ const moduleValue = ${useImportSyntax ? 'loaded as { providers?: ProjectProviderRuntime[] } & ProjectProviderRuntime' : 'loaded'};
3412
+ if (Array.isArray(moduleValue.providers)) {
3413
+ return moduleValue.providers;
3414
+ }
3415
+ return [moduleValue];
3416
+ }
3417
+
3418
+ return [];
3283
3419
  }
3284
3420
 
3285
- function applyProjectProviderMocks(exportName, scenarioName) {
3421
+ ${applyMocksSignature}
3286
3422
  if (!PROJECT_PROVIDER_IMPORT || PROJECT_PROVIDER_INDEXES.length === 0) {
3287
3423
  return;
3288
3424
  }
3289
3425
 
3290
- const loaded = require(PROJECT_PROVIDER_IMPORT);
3291
- const providers = Array.isArray(loaded)
3292
- ? loaded
3293
- : Array.isArray(loaded && loaded.providers)
3294
- ? loaded.providers
3295
- : loaded
3296
- ? [loaded]
3297
- : [];
3426
+ const providers = resolveProjectProviders(${providerLoadExpression});
3298
3427
 
3299
3428
  for (const providerIndex of PROJECT_PROVIDER_INDEXES) {
3300
3429
  const provider = providers[providerIndex];
@@ -3321,21 +3450,14 @@ function applyProjectProviderMocks(exportName, scenarioName) {
3321
3450
  }
3322
3451
  }
3323
3452
 
3324
- function applyProjectProviderRender(element, exportName, scenarioName) {
3453
+ ${applyRenderSignature}
3325
3454
  let current = applyProjectProviderPresets(element);
3326
3455
 
3327
3456
  if (!PROJECT_PROVIDER_IMPORT) {
3328
3457
  return current;
3329
3458
  }
3330
3459
 
3331
- const loaded = require(PROJECT_PROVIDER_IMPORT);
3332
- const providers = Array.isArray(loaded)
3333
- ? loaded
3334
- : Array.isArray(loaded && loaded.providers)
3335
- ? loaded.providers
3336
- : loaded
3337
- ? [loaded]
3338
- : [];
3460
+ const providers = resolveProjectProviders(${providerLoadExpression});
3339
3461
 
3340
3462
  for (const providerIndex of PROJECT_PROVIDER_INDEXES) {
3341
3463
  const provider = providers[providerIndex];
@@ -3350,24 +3472,12 @@ function applyProjectProviderRender(element, exportName, scenarioName) {
3350
3472
  scenario: scenarioName,
3351
3473
  element: current,
3352
3474
  withProviderShell,
3353
- withReactRouter(elementValue, config) {
3354
- return withReactRouter(elementValue, config);
3355
- },
3356
- withNextNavigation(elementValue, config) {
3357
- return withNextNavigation(elementValue, config);
3358
- },
3359
- withAuthSession(elementValue, config) {
3360
- return withAuthSession(elementValue, config);
3361
- },
3362
- withReactQuery(elementValue, config) {
3363
- return withReactQuery(elementValue, config);
3364
- },
3365
- withZustandStore(elementValue, config) {
3366
- return withZustandStore(elementValue, config);
3367
- },
3368
- withReduxStore(elementValue, config) {
3369
- return withReduxStore(elementValue, config);
3370
- }
3475
+ withReactRouter,
3476
+ withNextNavigation,
3477
+ withAuthSession,
3478
+ withReactQuery,
3479
+ withZustandStore,
3480
+ withReduxStore
3371
3481
  });
3372
3482
 
3373
3483
  if (nextValue !== undefined) {
@@ -3378,7 +3488,13 @@ function applyProjectProviderRender(element, exportName, scenarioName) {
3378
3488
  return current;
3379
3489
  }
3380
3490
 
3381
- function applyProjectProviderPresets(element) {
3491
+ ${createRenderOptionsSignature}
3492
+ return {
3493
+ wrapRender: (element${useImportSyntax ? ': unknown' : ''}) => applyProjectProviderRender(element, exportName, scenarioName)
3494
+ };
3495
+ }
3496
+
3497
+ ${applyPresetsSignature}
3382
3498
  let current = element;
3383
3499
 
3384
3500
  for (const preset of PROJECT_PROVIDER_PRESETS) {
@@ -3405,7 +3521,7 @@ function applyProjectProviderPresets(element) {
3405
3521
  return current;
3406
3522
  }
3407
3523
 
3408
- function withProviderShell(type, element, attributes = {}) {
3524
+ ${withProviderShellSignature}
3409
3525
  return {
3410
3526
  $$typeof: 'react.test.element',
3411
3527
  type,
@@ -3418,7 +3534,7 @@ function withProviderShell(type, element, attributes = {}) {
3418
3534
  };
3419
3535
  }
3420
3536
 
3421
- function withReactRouter(element, config = {}) {
3537
+ ${providerHelperSignature('withReactRouter')}
3422
3538
  return withProviderShell('themis-router-provider', element, {
3423
3539
  role: 'navigation',
3424
3540
  'data-themis-provider': 'router',
@@ -3431,7 +3547,7 @@ function withReactRouter(element, config = {}) {
3431
3547
  });
3432
3548
  }
3433
3549
 
3434
- function withReactQuery(element, config = {}) {
3550
+ ${providerHelperSignature('withReactQuery')}
3435
3551
  return withProviderShell('themis-react-query-provider', element, {
3436
3552
  'data-themis-provider': 'react-query',
3437
3553
  'data-query-client': typeof config.clientName === 'string' ? config.clientName : 'themis-query-client',
@@ -3443,7 +3559,7 @@ function withReactQuery(element, config = {}) {
3443
3559
  });
3444
3560
  }
3445
3561
 
3446
- function withNextNavigation(element, config = {}) {
3562
+ ${providerHelperSignature('withNextNavigation')}
3447
3563
  return withProviderShell('themis-next-navigation-provider', element, {
3448
3564
  'data-themis-provider': 'next-navigation',
3449
3565
  'data-next-pathname': typeof config.pathname === 'string' ? config.pathname : '/',
@@ -3454,7 +3570,7 @@ function withNextNavigation(element, config = {}) {
3454
3570
  });
3455
3571
  }
3456
3572
 
3457
- function withAuthSession(element, config = {}) {
3573
+ ${providerHelperSignature('withAuthSession')}
3458
3574
  return withProviderShell('themis-auth-provider', element, {
3459
3575
  'data-themis-provider': 'auth',
3460
3576
  'data-auth-user': typeof config.user === 'string' ? config.user : 'anonymous',
@@ -3465,7 +3581,7 @@ function withAuthSession(element, config = {}) {
3465
3581
  });
3466
3582
  }
3467
3583
 
3468
- function withZustandStore(element, config = {}) {
3584
+ ${providerHelperSignature('withZustandStore')}
3469
3585
  return withProviderShell('themis-zustand-provider', element, {
3470
3586
  'data-themis-provider': 'zustand',
3471
3587
  'data-store-name': typeof config.name === 'string' ? config.name : 'zustand-store',
@@ -3475,7 +3591,7 @@ function withZustandStore(element, config = {}) {
3475
3591
  });
3476
3592
  }
3477
3593
 
3478
- function withReduxStore(element, config = {}) {
3594
+ ${providerHelperSignature('withReduxStore')}
3479
3595
  return withProviderShell('themis-redux-provider', element, {
3480
3596
  'data-themis-provider': 'redux',
3481
3597
  'data-redux-slice': typeof config.slice === 'string' ? config.slice : 'root',
@@ -3485,14 +3601,14 @@ function withReduxStore(element, config = {}) {
3485
3601
  });
3486
3602
  }
3487
3603
 
3488
- function serializeProviderData(value) {
3604
+ ${serializeProviderDataSignature}
3489
3605
  if (value === undefined) {
3490
3606
  return '';
3491
3607
  }
3492
3608
  return JSON.stringify(normalizeBehaviorValue(value));
3493
3609
  }
3494
3610
 
3495
- function assertExpectedRuntimeContract(runtime) {
3611
+ ${assertExpectedRuntimeContractSignature}
3496
3612
  expect(typeof runtime).toBe('object');
3497
3613
  expect(runtime === null).toBe(false);
3498
3614
 
@@ -3502,7 +3618,7 @@ function assertExpectedRuntimeContract(runtime) {
3502
3618
  }
3503
3619
 
3504
3620
  for (const [exportName, expected] of Object.entries(EXPECTED_EXPORT_CONTRACTS)) {
3505
- const actual = runtime[exportName];
3621
+ const actual = ${useImportSyntax ? 'runtime[exportName] as Record<string, unknown>' : 'runtime[exportName]'};
3506
3622
  expect(Boolean(actual)).toBe(true);
3507
3623
 
3508
3624
  if (expected.kind && expected.kind !== 'unknown') {
@@ -3519,7 +3635,7 @@ function assertExpectedRuntimeContract(runtime) {
3519
3635
  }
3520
3636
  }
3521
3637
 
3522
- function assertNormalizedRenderContract(rendered) {
3638
+ ${assertNormalizedRenderContractSignature}
3523
3639
  const normalized = normalizeBehaviorValue(rendered);
3524
3640
  expect(normalized !== undefined && normalized !== null).toBe(true);
3525
3641
 
@@ -3528,21 +3644,22 @@ function assertNormalizedRenderContract(rendered) {
3528
3644
  return;
3529
3645
  }
3530
3646
 
3531
- if (typeof normalized === 'object') {
3532
- if (normalized.kind === 'element') {
3533
- expect(Boolean(normalized.type)).toBe(true);
3534
- expect(typeof normalized.props).toBe('object');
3647
+ if (normalized && typeof normalized === 'object') {
3648
+ const normalizedRecord = ${useImportSyntax ? 'normalized as Record<string, unknown>' : 'normalized'};
3649
+ if (normalizedRecord.kind === 'element') {
3650
+ expect(Boolean(normalizedRecord.type)).toBe(true);
3651
+ expect(typeof normalizedRecord.props).toBe('object');
3535
3652
  return;
3536
3653
  }
3537
3654
 
3538
- expect(Object.keys(normalized).length >= 0).toBe(true);
3655
+ expect(Object.keys(normalizedRecord).length >= 0).toBe(true);
3539
3656
  return;
3540
3657
  }
3541
3658
 
3542
3659
  expect(['string', 'number', 'boolean'].includes(typeof normalized)).toBe(true);
3543
3660
  }
3544
3661
 
3545
- function assertDomContractShape(contract) {
3662
+ ${assertDomContractShapeSignature}
3546
3663
  expect(typeof contract).toBe('object');
3547
3664
  expect(contract === null).toBe(false);
3548
3665
  expect(Array.isArray(contract.nodes)).toBe(true);
@@ -3550,7 +3667,7 @@ function assertDomContractShape(contract) {
3550
3667
  expect(typeof contract.textContent).toBe('string');
3551
3668
  }
3552
3669
 
3553
- function countPlannedSteps(plan) {
3670
+ ${countPlannedStepsSignature}
3554
3671
  if (!Array.isArray(plan)) {
3555
3672
  return 0;
3556
3673
  }
@@ -3561,7 +3678,7 @@ function countPlannedSteps(plan) {
3561
3678
  }, 0);
3562
3679
  }
3563
3680
 
3564
- function assertComponentInteractionContractShape(contract, plan) {
3681
+ ${assertComponentInteractionContractShapeSignature}
3565
3682
  assertNormalizedRenderContract(contract.rendered);
3566
3683
  assertDomContractShape(contract.dom);
3567
3684
  expect(Array.isArray(contract.interactions)).toBe(true);
@@ -3578,7 +3695,7 @@ function assertComponentInteractionContractShape(contract, plan) {
3578
3695
  }
3579
3696
  }
3580
3697
 
3581
- function assertFlowContractShape(flow, plan) {
3698
+ ${assertFlowContractShapeSignature}
3582
3699
  assertDomContractShape(flow.dom);
3583
3700
  expect(Array.isArray(flow.steps)).toBe(true);
3584
3701
  expect(flow.steps.length).toBe(Array.isArray(plan) ? plan.length : 0);
@@ -3631,7 +3748,7 @@ function assertFlowContractShape(flow, plan) {
3631
3748
  }
3632
3749
 
3633
3750
  if (expected.expected && expected.expected.rolesInclude !== undefined) {
3634
- const expectedRoles = Array.isArray(expected.expected.rolesInclude)
3751
+ const expectedRoles${useImportSyntax ? ': string[]' : ''} = Array.isArray(expected.expected.rolesInclude)
3635
3752
  ? expected.expected.rolesInclude
3636
3753
  : [expected.expected.rolesInclude];
3637
3754
  const matchesRoles = [step.immediateDom, step.settledDom].some((dom) => {
@@ -3643,12 +3760,12 @@ function assertFlowContractShape(flow, plan) {
3643
3760
  }
3644
3761
  }
3645
3762
 
3646
- function assertHookResultContract(result) {
3763
+ ${assertHookResultContractSignature}
3647
3764
  const normalized = normalizeBehaviorValue(result);
3648
3765
  expect(normalized !== undefined).toBe(true);
3649
3766
  }
3650
3767
 
3651
- function assertHookInteractionContractShape(contract, plan) {
3768
+ ${assertHookInteractionContractShapeSignature}
3652
3769
  expect(Array.isArray(contract.interactions)).toBe(true);
3653
3770
 
3654
3771
  if (countPlannedSteps(plan) > 0) {
@@ -3661,26 +3778,27 @@ function assertHookInteractionContractShape(contract, plan) {
3661
3778
  }
3662
3779
  }
3663
3780
 
3664
- function assertRouteResultContractShape(response) {
3781
+ ${assertRouteResultContractShapeSignature}
3665
3782
  expect(response !== undefined && response !== null).toBe(true);
3666
3783
 
3667
- if (response && typeof response === 'object' && response.kind === 'response') {
3668
- expect(typeof response.status).toBe('number');
3669
- expect(response.status >= 100).toBe(true);
3670
- expect(response.status < 600).toBe(true);
3671
- expect(typeof response.headers).toBe('object');
3784
+ if (response && typeof response === 'object' && ${useImportSyntax ? '(response as Record<string, unknown>).kind' : 'response.kind'} === 'response') {
3785
+ const routeResponse = ${useImportSyntax ? 'response as { status: number; headers: unknown }' : 'response'};
3786
+ expect(typeof routeResponse.status).toBe('number');
3787
+ expect(routeResponse.status >= 100).toBe(true);
3788
+ expect(routeResponse.status < 600).toBe(true);
3789
+ expect(typeof routeResponse.headers).toBe('object');
3672
3790
  }
3673
3791
  }
3674
3792
 
3675
- function assertServiceResultContractShape(result) {
3793
+ ${assertServiceResultContractShapeSignature}
3676
3794
  expect(result !== undefined).toBe(true);
3677
3795
  }
3678
3796
 
3679
3797
  describe(${JSON.stringify(suiteName)}, () => {
3680
3798
  ${exactExportBlock}
3681
3799
 
3682
- test('captures runtime export contract', () => {
3683
- const moduleExports = loadModuleExports();
3800
+ test('captures runtime export contract', async () => {
3801
+ const moduleExports = await loadModuleExports();
3684
3802
  const runtime = buildModuleContract(moduleExports);
3685
3803
  assertExpectedRuntimeContract(runtime);
3686
3804
  });
@@ -3688,31 +3806,23 @@ describe(${JSON.stringify(suiteName)}, () => {
3688
3806
  if (NEXT_APP_CASES.length > 0) {
3689
3807
  describe('next app component adapter', () => {
3690
3808
  for (const testCase of NEXT_APP_CASES) {
3691
- test(testCase.exportName + ' ' + testCase.caseName, () => {
3809
+ test(testCase.exportName + ' ' + testCase.caseName, async () => {
3692
3810
  applyProjectProviderMocks(testCase.exportName, 'next-app-component');
3693
- const moduleExports = loadModuleExports();
3694
- const component = readExportValue(moduleExports, testCase.exportName);
3811
+ const moduleExports = await loadModuleExports();
3812
+ const component = ${componentReadExpression};
3695
3813
  const rendered = applyProjectProviderRender(component(testCase.props), testCase.exportName, 'next-app-component');
3696
3814
  assertNormalizedRenderContract(rendered);
3697
3815
  });
3698
3816
 
3699
3817
  test(testCase.exportName + ' next interaction contract', async () => {
3700
3818
  applyProjectProviderMocks(testCase.exportName, 'next-app-component');
3701
- const interaction = await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, {
3702
- wrapRender(element) {
3703
- return applyProjectProviderRender(element, testCase.exportName, 'next-app-component');
3704
- }
3705
- });
3819
+ const interaction = ${interactionContractExpression};
3706
3820
  assertComponentInteractionContractShape(interaction, testCase.interactions);
3707
3821
  });
3708
3822
 
3709
3823
  test(testCase.exportName + ' next dom state contract', async () => {
3710
3824
  applyProjectProviderMocks(testCase.exportName, 'next-app-component');
3711
- const contract = await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, {
3712
- wrapRender(element) {
3713
- return applyProjectProviderRender(element, testCase.exportName, 'next-app-component');
3714
- }
3715
- });
3825
+ const contract = ${nextDomContractExpression};
3716
3826
  assertDomContractShape(contract.dom);
3717
3827
  if (Array.isArray(testCase.interactions) && testCase.interactions.length > 0) {
3718
3828
  expect(contract.interactions.length > 0).toBe(true);
@@ -3722,11 +3832,7 @@ describe(${JSON.stringify(suiteName)}, () => {
3722
3832
  if (Array.isArray(testCase.flows) && testCase.flows.length > 0) {
3723
3833
  test(testCase.exportName + ' next behavioral flow contract', async () => {
3724
3834
  applyProjectProviderMocks(testCase.exportName, 'next-app-component');
3725
- const flow = await runComponentBehaviorFlowContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.flows, {
3726
- wrapRender(element) {
3727
- return applyProjectProviderRender(element, testCase.exportName, 'next-app-component');
3728
- }
3729
- });
3835
+ const flow = ${nextFlowExpression};
3730
3836
  assertFlowContractShape(flow, testCase.flows);
3731
3837
  expect(flow.steps.some((step) => !step.skipped)).toBe(true);
3732
3838
  });
@@ -3738,31 +3844,23 @@ describe(${JSON.stringify(suiteName)}, () => {
3738
3844
  if (COMPONENT_CASES.length > 0) {
3739
3845
  describe('react component adapter', () => {
3740
3846
  for (const testCase of COMPONENT_CASES) {
3741
- test(testCase.exportName + ' ' + testCase.caseName, () => {
3847
+ test(testCase.exportName + ' ' + testCase.caseName, async () => {
3742
3848
  applyProjectProviderMocks(testCase.exportName, 'react-component');
3743
- const moduleExports = loadModuleExports();
3744
- const component = readExportValue(moduleExports, testCase.exportName);
3849
+ const moduleExports = await loadModuleExports();
3850
+ const component = ${componentReadExpression};
3745
3851
  const rendered = applyProjectProviderRender(component(testCase.props), testCase.exportName, 'react-component');
3746
3852
  assertNormalizedRenderContract(rendered);
3747
3853
  });
3748
3854
 
3749
3855
  test(testCase.exportName + ' interaction contract', async () => {
3750
3856
  applyProjectProviderMocks(testCase.exportName, 'react-component');
3751
- const interaction = await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, {
3752
- wrapRender(element) {
3753
- return applyProjectProviderRender(element, testCase.exportName, 'react-component');
3754
- }
3755
- });
3857
+ const interaction = ${componentInteractionExpression};
3756
3858
  assertComponentInteractionContractShape(interaction, testCase.interactions);
3757
3859
  });
3758
3860
 
3759
3861
  test(testCase.exportName + ' dom state contract', async () => {
3760
3862
  applyProjectProviderMocks(testCase.exportName, 'react-component');
3761
- const contract = await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, {
3762
- wrapRender(element) {
3763
- return applyProjectProviderRender(element, testCase.exportName, 'react-component');
3764
- }
3765
- });
3863
+ const contract = ${componentDomExpression};
3766
3864
  assertDomContractShape(contract.dom);
3767
3865
  if (Array.isArray(testCase.interactions) && testCase.interactions.length > 0) {
3768
3866
  expect(contract.interactions.length > 0).toBe(true);
@@ -3772,11 +3870,7 @@ describe(${JSON.stringify(suiteName)}, () => {
3772
3870
  if (Array.isArray(testCase.flows) && testCase.flows.length > 0) {
3773
3871
  test(testCase.exportName + ' behavioral flow contract', async () => {
3774
3872
  applyProjectProviderMocks(testCase.exportName, 'react-component');
3775
- const flow = await runComponentBehaviorFlowContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.flows, {
3776
- wrapRender(element) {
3777
- return applyProjectProviderRender(element, testCase.exportName, 'react-component');
3778
- }
3779
- });
3873
+ const flow = ${componentFlowExpression};
3780
3874
  assertFlowContractShape(flow, testCase.flows);
3781
3875
  expect(flow.steps.some((step) => !step.skipped)).toBe(true);
3782
3876
  });
@@ -3788,17 +3882,17 @@ describe(${JSON.stringify(suiteName)}, () => {
3788
3882
  if (HOOK_CASES.length > 0) {
3789
3883
  describe('react hook adapter', () => {
3790
3884
  for (const testCase of HOOK_CASES) {
3791
- test(testCase.exportName + ' ' + testCase.caseName, () => {
3885
+ test(testCase.exportName + ' ' + testCase.caseName, async () => {
3792
3886
  applyProjectProviderMocks(testCase.exportName, 'react-hook');
3793
- const moduleExports = loadModuleExports();
3794
- const hook = readExportValue(moduleExports, testCase.exportName);
3887
+ const moduleExports = await loadModuleExports();
3888
+ const hook = ${hookReadExpression};
3795
3889
  const result = hook(...testCase.args);
3796
3890
  assertHookResultContract(result);
3797
3891
  });
3798
3892
 
3799
3893
  test(testCase.exportName + ' interaction contract', () => {
3800
3894
  applyProjectProviderMocks(testCase.exportName, 'react-hook');
3801
- const interaction = runHookInteractionContract(SOURCE_FILE, testCase.exportName, testCase.args, testCase.interactions);
3895
+ const interaction = ${hookInteractionExpression};
3802
3896
  assertHookInteractionContractShape(interaction, testCase.interactions);
3803
3897
  });
3804
3898
  }
@@ -3810,8 +3904,8 @@ describe(${JSON.stringify(suiteName)}, () => {
3810
3904
  for (const testCase of NEXT_ROUTE_CASES) {
3811
3905
  test(testCase.exportName + ' ' + testCase.caseName, async () => {
3812
3906
  applyProjectProviderMocks(testCase.exportName, 'next-route-handler');
3813
- const moduleExports = loadModuleExports();
3814
- const handler = readExportValue(moduleExports, testCase.exportName);
3907
+ const moduleExports = await loadModuleExports();
3908
+ const handler = ${handlerReadExpression};
3815
3909
  const request = createRequestFromSpec(testCase.request);
3816
3910
  const response = await Promise.resolve(handler(request, testCase.context));
3817
3911
  const normalizedResponse = await normalizeRouteResult(response);
@@ -3826,8 +3920,8 @@ describe(${JSON.stringify(suiteName)}, () => {
3826
3920
  for (const testCase of ROUTE_CASES) {
3827
3921
  test(testCase.exportName + ' ' + testCase.caseName, async () => {
3828
3922
  applyProjectProviderMocks(testCase.exportName, 'route-handler');
3829
- const moduleExports = loadModuleExports();
3830
- const handler = readExportValue(moduleExports, testCase.exportName);
3923
+ const moduleExports = await loadModuleExports();
3924
+ const handler = ${handlerReadExpression};
3831
3925
  const request = createRequestFromSpec(testCase.request);
3832
3926
  const response = await Promise.resolve(handler(request, testCase.context));
3833
3927
  const normalizedResponse = await normalizeRouteResult(response);
@@ -3842,8 +3936,8 @@ describe(${JSON.stringify(suiteName)}, () => {
3842
3936
  for (const testCase of SERVICE_CASES) {
3843
3937
  test(testCase.exportName + ' ' + testCase.caseName, async () => {
3844
3938
  applyProjectProviderMocks(testCase.exportName, 'node-service');
3845
- const moduleExports = loadModuleExports();
3846
- const service = readExportValue(moduleExports, testCase.exportName);
3939
+ const moduleExports = await loadModuleExports();
3940
+ const service = ${serviceReadExpression};
3847
3941
  const result = await Promise.resolve(service(...testCase.args));
3848
3942
  assertServiceResultContractShape(normalizeBehaviorValue(result));
3849
3943
  });
@@ -3854,6 +3948,261 @@ describe(${JSON.stringify(suiteName)}, () => {
3854
3948
  `;
3855
3949
  }
3856
3950
 
3951
+ function renderGeneratedRequirePrelude({ helperImport }) {
3952
+ return `const path = require('path');
3953
+ const {
3954
+ listExportNames,
3955
+ buildModuleContract,
3956
+ readExportValue,
3957
+ normalizeBehaviorValue,
3958
+ normalizeRouteResult,
3959
+ createRequestFromSpec,
3960
+ assertSourceFreshness,
3961
+ runComponentInteractionContract,
3962
+ runComponentBehaviorFlowContract,
3963
+ runHookInteractionContract
3964
+ } = require(${JSON.stringify(helperImport)});`;
3965
+ }
3966
+
3967
+ function renderGeneratedImportPrelude({ helperImport, providerImport }) {
3968
+ const providerLine = providerImport
3969
+ ? `import * as PROJECT_PROVIDER_MODULE from ${JSON.stringify(providerImport)};`
3970
+ : 'const PROJECT_PROVIDER_MODULE: unknown = null;';
3971
+
3972
+ return `import {
3973
+ listExportNames,
3974
+ buildModuleContract,
3975
+ readExportValue,
3976
+ normalizeBehaviorValue,
3977
+ normalizeRouteResult,
3978
+ createRequestFromSpec,
3979
+ assertSourceFreshness,
3980
+ runComponentInteractionContract,
3981
+ runComponentBehaviorFlowContract,
3982
+ runHookInteractionContract
3983
+ } from ${JSON.stringify(helperImport)};
3984
+ import type {
3985
+ AdvanceTimersByTime,
3986
+ FlushMicrotasks,
3987
+ Fn,
3988
+ MockFetch,
3989
+ MockModule,
3990
+ ResetFetchMocks,
3991
+ RestoreFetch,
3992
+ RunAllTimers,
3993
+ UseFakeTimers,
3994
+ UseRealTimers
3995
+ } from "@vitronai/themis";
3996
+ ${providerLine}`;
3997
+ }
3998
+
3999
+ function renderGeneratedTypePrelude() {
4000
+ return `declare const require: {
4001
+ resolve(id: string): string;
4002
+ };
4003
+
4004
+ type ScenarioName =
4005
+ | 'next-app-component'
4006
+ | 'next-route-handler'
4007
+ | 'react-component'
4008
+ | 'react-hook'
4009
+ | 'route-handler'
4010
+ | 'node-service';
4011
+ type RuntimeModuleExports = Record<string, unknown>;
4012
+ type RuntimeModuleContractExpectations = Record<string, {
4013
+ kind: string;
4014
+ arity: number | null;
4015
+ prototypeKeys: string[] | null;
4016
+ }>;
4017
+ type ProviderConfig = Record<string, unknown>;
4018
+
4019
+ interface RepeatedPlanStep {
4020
+ repeat?: number | null;
4021
+ }
4022
+
4023
+ interface InteractionPlanStep extends RepeatedPlanStep {
4024
+ event?: string | null;
4025
+ labelIncludes?: string | null;
4026
+ elementType?: string | null;
4027
+ }
4028
+
4029
+ interface FlowExpectation {
4030
+ immediateTextIncludes?: string;
4031
+ beforeTextIncludes?: string;
4032
+ settledTextIncludes?: string;
4033
+ textExcludes?: string;
4034
+ attributes?: Record<string, unknown>;
4035
+ rolesInclude?: string | string[];
4036
+ }
4037
+
4038
+ interface FlowPlanStep extends RepeatedPlanStep {
4039
+ label?: string | null;
4040
+ event: string;
4041
+ labelIncludes?: string | null;
4042
+ elementType?: string | null;
4043
+ target?: Record<string, unknown>;
4044
+ awaitResult?: boolean;
4045
+ flushMicrotasks?: number;
4046
+ advanceTimersByTime?: number;
4047
+ runAllTimers?: boolean;
4048
+ expected?: FlowExpectation;
4049
+ }
4050
+
4051
+ interface HookInteractionPlanStep extends RepeatedPlanStep {
4052
+ method?: string | null;
4053
+ }
4054
+
4055
+ interface ComponentCase {
4056
+ exportName: string;
4057
+ displayName: string;
4058
+ caseName: string;
4059
+ props: Record<string, unknown>;
4060
+ interactions: InteractionPlanStep[];
4061
+ flows: FlowPlanStep[];
4062
+ confidence: string;
4063
+ }
4064
+
4065
+ interface HookCase {
4066
+ exportName: string;
4067
+ displayName: string;
4068
+ caseName: string;
4069
+ args: unknown[];
4070
+ interactions: HookInteractionPlanStep[];
4071
+ confidence: string;
4072
+ }
4073
+
4074
+ interface RouteCase {
4075
+ exportName: string;
4076
+ displayName: string;
4077
+ caseName: string;
4078
+ request: {
4079
+ method?: string;
4080
+ url: string;
4081
+ headers?: Record<string, string>;
4082
+ body?: unknown;
4083
+ json?: unknown;
4084
+ };
4085
+ context: Record<string, unknown>;
4086
+ confidence: string;
4087
+ }
4088
+
4089
+ interface ServiceCase {
4090
+ exportName: string;
4091
+ displayName: string;
4092
+ caseName: string;
4093
+ args: unknown[];
4094
+ confidence: string;
4095
+ }
4096
+
4097
+ interface ProviderPreset {
4098
+ router?: ProviderConfig;
4099
+ nextNavigation?: ProviderConfig;
4100
+ auth?: ProviderConfig;
4101
+ reactQuery?: ProviderConfig;
4102
+ zustand?: ProviderConfig;
4103
+ redux?: ProviderConfig;
4104
+ }
4105
+
4106
+ type ProviderTransform = (element: unknown, config?: ProviderConfig) => unknown;
4107
+
4108
+ interface ProjectProviderMockContext {
4109
+ sourceFile: string;
4110
+ sourcePath: string;
4111
+ exportName: string;
4112
+ scenario: ScenarioName;
4113
+ mock: MockModule | null;
4114
+ fn: Fn | null;
4115
+ mockFetch: MockFetch | null;
4116
+ resetFetchMocks: ResetFetchMocks | null;
4117
+ restoreFetch: RestoreFetch | null;
4118
+ useFakeTimers: UseFakeTimers | null;
4119
+ useRealTimers: UseRealTimers | null;
4120
+ advanceTimersByTime: AdvanceTimersByTime | null;
4121
+ runAllTimers: RunAllTimers | null;
4122
+ flushMicrotasks: FlushMicrotasks | null;
4123
+ }
4124
+
4125
+ interface ProjectProviderRenderContext {
4126
+ sourceFile: string;
4127
+ sourcePath: string;
4128
+ exportName: string;
4129
+ scenario: ScenarioName;
4130
+ element: unknown;
4131
+ withProviderShell(type: string, element: unknown, attributes?: ProviderConfig): unknown;
4132
+ withReactRouter: ProviderTransform;
4133
+ withNextNavigation: ProviderTransform;
4134
+ withAuthSession: ProviderTransform;
4135
+ withReactQuery: ProviderTransform;
4136
+ withZustandStore: ProviderTransform;
4137
+ withReduxStore: ProviderTransform;
4138
+ }
4139
+
4140
+ interface ProjectProviderRuntime {
4141
+ providers?: ProjectProviderRuntime[];
4142
+ applyMocks?(context: ProjectProviderMockContext): void;
4143
+ wrapRender?(context: ProjectProviderRenderContext): unknown;
4144
+ }
4145
+
4146
+ interface DomRoleContract {
4147
+ role: string;
4148
+ name: string;
4149
+ path: string;
4150
+ type: string;
4151
+ attributes: Record<string, unknown>;
4152
+ }
4153
+
4154
+ type DomNodeContract =
4155
+ | { kind: 'text'; value: string; path: string }
4156
+ | { kind: 'value'; value: unknown; path: string }
4157
+ | { kind: 'element'; type: string; path: string; textContent: string; attributes: Record<string, unknown> };
4158
+
4159
+ interface DomContract {
4160
+ textContent: string;
4161
+ roles: DomRoleContract[];
4162
+ nodes: DomNodeContract[];
4163
+ }
4164
+
4165
+ interface ComponentInteractionEntry {
4166
+ label: string;
4167
+ beforeDom: DomContract;
4168
+ afterDom: DomContract;
4169
+ }
4170
+
4171
+ interface ComponentInteractionContract {
4172
+ rendered: unknown;
4173
+ dom: DomContract;
4174
+ interactions: ComponentInteractionEntry[];
4175
+ }
4176
+
4177
+ interface FlowStepContract {
4178
+ label: string;
4179
+ skipped?: boolean;
4180
+ beforeDom: DomContract;
4181
+ immediateDom: DomContract;
4182
+ settledDom: DomContract;
4183
+ }
4184
+
4185
+ interface ComponentBehaviorFlowContract {
4186
+ dom: DomContract;
4187
+ steps: FlowStepContract[];
4188
+ }
4189
+
4190
+ interface HookInteractionEntry {
4191
+ label: string;
4192
+ }
4193
+
4194
+ interface HookInteractionContract {
4195
+ interactions: HookInteractionEntry[];
4196
+ }
4197
+
4198
+ interface ReactTestElement {
4199
+ $$typeof: 'react.test.element';
4200
+ type: string;
4201
+ key: null;
4202
+ props: Record<string, unknown>;
4203
+ }`;
4204
+ }
4205
+
3857
4206
  function flattenScenarioCases(scenarios, kind) {
3858
4207
  const scenario = scenarios.find((candidate) => candidate.kind === kind);
3859
4208
  return scenario && Array.isArray(scenario.cases) ? scenario.cases : [];