PSA/tools/workflow-harness/generate-fixture-catalog.cjs
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

194 lines
6.1 KiB
JavaScript
Executable File

#!/usr/bin/env node
/* eslint-disable no-console */
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const DEFAULT_PLAN_DIR = path.resolve(
process.cwd(),
'ee/docs/plans/2026-01-26-workflow-harness-fixture-suite'
);
const DEFAULT_FIXTURE_ROOT = path.resolve(process.cwd(), 'ee/test-data/workflow-harness');
const SCHEMA_REGISTRY_PATH = path.resolve(
process.cwd(),
'shared/workflow/runtime/schemas/workflowEventPayloadSchemas.ts'
);
function usage() {
console.error(`
Generate scaffolded fixture folders for the workflow harness, based on the plan's tests.json.
Usage:
node tools/workflow-harness/generate-fixture-catalog.cjs [--plan-dir <dir>] [--fixtures-root <dir>] [--dry-run]
Notes:
- Only creates missing fixture folders.
- Marks scaffolded fixtures with a .scaffolded file.
`);
}
function parseArgs(argv) {
const args = { planDir: DEFAULT_PLAN_DIR, fixturesRoot: DEFAULT_FIXTURE_ROOT, dryRun: false };
for (let i = 0; i < argv.length; i++) {
const t = argv[i];
if (t === '--help' || t === '-h') args.help = true;
else if (t === '--dry-run') args.dryRun = true;
else if (t === '--plan-dir') args.planDir = argv[++i];
else if (t === '--fixtures-root') args.fixturesRoot = argv[++i];
}
return args;
}
function pascalFromEventName(eventName) {
return String(eventName)
.toLowerCase()
.split('_')
.filter(Boolean)
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
.join('');
}
function listKnownSchemaRefs() {
const text = fs.readFileSync(SCHEMA_REGISTRY_PATH, 'utf8');
const matches = [...text.matchAll(/'payload\\.([A-Za-z0-9]+)\\.v1'/g)].map(
(m) => `payload.${m[1]}.v1`
);
return new Set(matches);
}
function stableWorkflowUuidFromFixtureName(fixtureName) {
const hash = crypto.createHash('sha256').update(`fixture:${fixtureName}`).digest('hex').slice(0, 32);
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`;
}
function makeBundle({ fixtureName, eventName, schemaRef }) {
const workflowKey = `fixture.${fixtureName}`;
const workflowId = stableWorkflowUuidFromFixtureName(fixtureName);
const pretty = fixtureName
.split('-')
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
.join(' ');
const definition = {
id: workflowId,
version: 1,
name: `Fixture: ${pretty}`,
description: `Scaffolded catalog fixture for ${eventName}.`,
payloadSchemaRef: schemaRef,
trigger: { type: 'event', eventName },
steps: [
{ id: 'ready', type: 'state.set', config: { state: 'READY' } },
{
id: 'assign-marker',
type: 'transform.assign',
config: {
assign: {
'vars.marker': { $expr: `'[fixture ${fixtureName}]'` }
}
}
},
{ id: 'done', type: 'control.return' }
]
};
return {
format: 'alga-psa.workflow-bundle',
formatVersion: 1,
exportedAt: new Date().toISOString(),
workflows: [
{
key: workflowKey,
metadata: {
name: `Fixture: ${pretty}`,
description: `Scaffolded catalog fixture for ${eventName}.`,
payloadSchemaRef: schemaRef,
payloadSchemaMode: 'pinned',
pinnedPayloadSchemaRef: schemaRef,
trigger: { type: 'event', eventName },
isSystem: false,
isVisible: true,
isPaused: false,
concurrencyLimit: null,
autoPauseOnFailure: false,
failureRateThreshold: null,
failureRateMinRuns: null,
retentionPolicyOverride: null
},
dependencies: {
actions: [],
nodeTypes: ['state.set', 'transform.assign'],
schemaRefs: [schemaRef]
},
draft: { draftVersion: 1, definition },
publishedVersions: [{ version: 1, definition, payloadSchemaJson: null }]
}
]
};
}
function ensureDir(dir, { dryRun }) {
if (dryRun) return;
fs.mkdirSync(dir, { recursive: true });
}
function writeFile(filePath, contents, { dryRun }) {
if (dryRun) return;
fs.writeFileSync(filePath, contents, 'utf8');
}
function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
usage();
process.exit(0);
}
const planDir = path.resolve(args.planDir);
const fixturesRoot = path.resolve(args.fixturesRoot);
const testsPath = path.join(planDir, 'tests.json');
if (!fs.existsSync(testsPath)) {
throw new Error(`tests.json not found: ${testsPath}`);
}
const knownSchemaRefs = listKnownSchemaRefs();
const tests = JSON.parse(fs.readFileSync(testsPath, 'utf8'));
const fixtureByName = new Map();
for (const t of tests) {
if (!t.fixture || !t.eventType) continue;
if (!fixtureByName.has(t.fixture)) {
fixtureByName.set(t.fixture, t.eventType);
}
}
let created = 0;
for (const [fixtureName, eventName] of fixtureByName.entries()) {
const dir = path.join(fixturesRoot, fixtureName);
if (fs.existsSync(dir)) continue;
const computedSchemaRef = `payload.${pascalFromEventName(eventName)}.v1`;
const schemaRef = knownSchemaRefs.has(computedSchemaRef) ? computedSchemaRef : 'payload.TicketCreated.v1';
ensureDir(dir, { dryRun: args.dryRun });
writeFile(path.join(dir, '.scaffolded'), `createdBy=generate-fixture-catalog\n`, { dryRun: args.dryRun });
writeFile(path.join(dir, 'bundle.json'), `${JSON.stringify(makeBundle({ fixtureName, eventName, schemaRef }), null, 2)}\n`, { dryRun: args.dryRun });
writeFile(
path.join(dir, 'test.cjs'),
`const { runScaffoldedFixture } = require('../_lib/scaffolded-fixture.cjs');\n\nmodule.exports = async function run(ctx) {\n return runScaffoldedFixture(ctx, {\n fixtureName: ${JSON.stringify(fixtureName)},\n eventName: ${JSON.stringify(eventName)},\n schemaRef: ${JSON.stringify(schemaRef)}\n });\n};\n`,
{ dryRun: args.dryRun }
);
created += 1;
}
console.log(`Created ${created} scaffolded fixture(s) under ${fixturesRoot}${args.dryRun ? ' (dry-run)' : ''}.`);
}
try {
main();
} catch (err) {
console.error(err?.stack ?? err?.message ?? String(err));
usage();
process.exit(1);
}