workflow-state-machines.snapshot.ts4.5 KBView on GitHub #!/usr/bin/env tsx
/**
* Layer 3 — CDK Snapshot Test (topology regression guard)
*
* Synthesizes the ConversationSync state machine and compares the step
* definitions against a committed snapshot. Fails if any of the following
* change without an explicit snapshot update:
* - Step names (which must match the dispatch table in step-registry.ts)
* - Timeouts
* - Concurrency values on Map states
* - Step chain order (a step is added, removed, or reordered)
*
* Run: pnpm --ignore-workspace --dir aws exec tsx __tests__/workflow-state-machines.snapshot.ts
* Update snapshot: add --update flag
*/
import * as cdk from 'aws-cdk-lib';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import { CedarWorkflowStateMachines } from '../lib/stacks/workflow-state-machines.js';
import type { CedarAwsEnvironmentConfig } from '../lib/config.js';
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const SNAPSHOT_PATH = resolve(__dirname, '__snapshots__/conversation-sync-state-machine.json');
const UPDATE = process.argv.includes('--update');
// ─── Minimal config for synthesis ─────────────────────────────────────────────
const mockConfig = {
name: 'staging',
prefix: 'aws-staging',
secretPrefix: '/cedar/aws-staging',
customDomains: {},
account: '123456789012',
region: 'us-east-1',
} as unknown as CedarAwsEnvironmentConfig;
// ─── Synthesize ───────────────────────────────────────────────────────────────
const app = new cdk.App();
const stack = new cdk.Stack(app, 'TestStack', {
env: { account: '123456789012', region: 'us-east-1' },
});
const queue = new sqs.Queue(stack, 'TestQueue');
// Construct is registered on the stack — the variable is intentionally unused
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _machines = new CedarWorkflowStateMachines(stack, 'WF', {
config: mockConfig,
stepQueue: queue,
});
// Extract the ConversationSync state machine definition
const synth = app.synth();
const template = synth.getStackByName('TestStack').template;
// Pull out just the state machine resources (not queues, roles, etc.)
const stateMachines = Object.entries(template.Resources as Record<string, { Type: string; Properties: Record<string, unknown> }>)
.filter(([, r]) => r.Type === 'AWS::StepFunctions::StateMachine')
.reduce((acc, [key, resource]) => {
// Extract just the name + definition, not ARNs or role refs
const props = resource.Properties;
const name = typeof props.StateMachineName === 'string'
? props.StateMachineName
: (props.StateMachineName as { 'Fn::Join': unknown[] })?.['Fn::Join']?.[1]?.join('') ?? key;
acc[key] = {
name,
definition: props.DefinitionString ?? props.Definition,
timeout: props.TimeoutSeconds,
};
return acc;
}, {} as Record<string, unknown>);
// ─── Snapshot compare / update ────────────────────────────────────────────────
const actual = JSON.stringify(stateMachines, null, 2);
if (UPDATE || !existsSync(SNAPSHOT_PATH)) {
const snapshotDir = resolve(__dirname, '__snapshots__');
if (!existsSync(snapshotDir)) {
const { mkdirSync } = await import('fs');
mkdirSync(snapshotDir, { recursive: true });
}
writeFileSync(SNAPSHOT_PATH, actual, 'utf-8');
console.log(`✓ Snapshot ${UPDATE ? 'updated' : 'created'}: ${SNAPSHOT_PATH}`);
process.exit(0);
}
const expected = readFileSync(SNAPSHOT_PATH, 'utf-8');
if (actual === expected) {
console.log('✓ CDK state machine snapshot matches.');
process.exit(0);
} else {
console.error('✗ CDK state machine snapshot MISMATCH.');
console.error('');
console.error('The Step Functions topology has changed. If this is intentional, run:');
console.error(' pnpm --ignore-workspace --dir aws exec tsx __tests__/workflow-state-machines.snapshot.ts --update');
console.error('');
console.error('This test protects against:');
console.error(' - Step name changes (must match dispatch table in step-registry.ts)');
console.error(' - Timeout changes');
console.error(' - Concurrency changes on Map states');
console.error(' - Steps accidentally dropped from the chain');
process.exit(1);
}