mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
d864efb07b
## Summary & Motivation - Manifest coverage now declares an entry for every matrix app, uses real Vitest skips instead of silent early returns, and fails when a targeted app's manifest is missing, unknown, or unparseable. - Retries are scoped to deployment e2e runs (`DEPLOYMENT_URL` set), so a flaky unit or integration test can no longer be hidden by a second attempt. - The stop-workflow cookbook parks on a sleep between iterations, giving the hook an observable barrier to race instead of a fixed delay, and the AbortController hook test waits on queue state rather than a 10ms timer. - The world-postgres direct-storage fixture drives its run to a terminal state so the conformance worker doesn't recover and replay an unregistered workflow. - Generated e2e result sidecars are ignored and the committed copies removed; they're CI artifacts, not fixtures. ## Test Plan Existing coverage runs in CI. With retries disabled: the cookbook agent suite passed 8/8, the two stop-workflow tests passed 10/10, and the AbortController hook replay test passed 25/25 under `CI=1`. The manifest suite skips 52 apps explicitly when nothing is built, and fails on unknown or missing targeted apps. The Docker-backed Postgres spec could not run locally (Testcontainers found no container runtime); `@workflow/world-postgres` typechecks.
130 lines
4.4 KiB
TypeScript
130 lines
4.4 KiB
TypeScript
import { execSync } from 'node:child_process';
|
|
import { PostgreSqlContainer } from '@testcontainers/postgresql';
|
|
import {
|
|
eventIdToSlot,
|
|
FIRST_EVENT_SLOT,
|
|
SPEC_VERSION_CURRENT,
|
|
} from '@workflow/world';
|
|
import { createTestSuite } from '@workflow/world-testing';
|
|
import { afterAll, beforeAll, expect, test } from 'vitest';
|
|
|
|
// Skip these tests on Windows since it relies on a docker container
|
|
if (process.platform === 'win32') {
|
|
test.skip('skipped on Windows since it relies on a docker container', () => {});
|
|
} else {
|
|
let container: Awaited<ReturnType<PostgreSqlContainer['start']>>;
|
|
|
|
beforeAll(async () => {
|
|
container = await new PostgreSqlContainer('postgres:15-alpine').start();
|
|
const dbUrl = container.getConnectionUri();
|
|
process.env.WORKFLOW_POSTGRES_URL = dbUrl;
|
|
process.env.DATABASE_URL = dbUrl;
|
|
|
|
execSync('pnpm db:push', {
|
|
stdio: 'inherit',
|
|
cwd: process.cwd(),
|
|
env: process.env,
|
|
});
|
|
}, 120_000);
|
|
|
|
afterAll(async () => {
|
|
if (container) {
|
|
await container.stop();
|
|
}
|
|
});
|
|
|
|
test('smoke', () => {});
|
|
|
|
// Sealed-log noop tolerance (specVersion 7): world-postgres allocates
|
|
// positions from its own counter and never seals holes itself, but a
|
|
// `noop` is a legal resident of any spec-7 slot log, and the storage layer
|
|
// must round-trip one — store it at its slot, list it back in order, and
|
|
// keep numbering past it. Direct storage access (not the conformance
|
|
// server): only a backend sealer would ever write one, and the public
|
|
// CreateEventSchema excludes it.
|
|
test('stores, lists, and numbers past a noop event', async () => {
|
|
// Storage layer only — createWorld would also spin up the queue and the
|
|
// streamer's dedicated LISTEN client, which have no shutdown hook here
|
|
// and would die noisily when afterAll stops the container.
|
|
const { createClient } = await import('../dist/drizzle/index.js');
|
|
const { createEventsStorage } = await import('../dist/storage.js');
|
|
const { Pool } = await import('pg');
|
|
const pool = new Pool({
|
|
connectionString: process.env.WORKFLOW_POSTGRES_URL,
|
|
max: 2,
|
|
});
|
|
const world = { events: createEventsStorage(createClient(pool)) };
|
|
|
|
const serialized = (value: unknown) =>
|
|
({ data: JSON.stringify(value), encoding: 'json' }) as any;
|
|
const created = await world.events.create('', {
|
|
eventType: 'run_created',
|
|
specVersion: SPEC_VERSION_CURRENT,
|
|
eventData: {
|
|
deploymentId: 'dpl_noop',
|
|
workflowName: 'noopWorkflow',
|
|
input: serialized([]),
|
|
},
|
|
} as any);
|
|
const runId = created.event!.runId;
|
|
await world.events.create(runId, {
|
|
eventType: 'run_started',
|
|
specVersion: SPEC_VERSION_CURRENT,
|
|
} as any);
|
|
await world.events.create(runId, {
|
|
eventType: 'noop',
|
|
specVersion: SPEC_VERSION_CURRENT,
|
|
eventData: { sealed: true },
|
|
} as any);
|
|
await world.events.create(runId, {
|
|
eventType: 'step_created',
|
|
correlationId: 'step_after_noop',
|
|
specVersion: SPEC_VERSION_CURRENT,
|
|
eventData: { stepName: 'afterNoop', input: serialized([]) },
|
|
} as any);
|
|
|
|
const result = await world.events.list({
|
|
runId,
|
|
pagination: { limit: 100 },
|
|
});
|
|
expect(result.data.map((event: any) => event.eventType)).toEqual([
|
|
'run_created',
|
|
'run_started',
|
|
'noop',
|
|
'step_created',
|
|
]);
|
|
expect(
|
|
result.data.map((event: any) => eventIdToSlot(event.eventId))
|
|
).toEqual([
|
|
FIRST_EVENT_SLOT,
|
|
FIRST_EVENT_SLOT + 1,
|
|
FIRST_EVENT_SLOT + 2,
|
|
FIRST_EVENT_SLOT + 3,
|
|
]);
|
|
|
|
// Leave the direct-storage fixture terminal. The conformance tests below
|
|
// start a real queue worker against the same database; an unfinished run
|
|
// would be recovered and replayed even though its synthetic workflow is
|
|
// intentionally not registered by that server.
|
|
await world.events.create(runId, {
|
|
eventType: 'step_started',
|
|
correlationId: 'step_after_noop',
|
|
specVersion: SPEC_VERSION_CURRENT,
|
|
} as any);
|
|
await world.events.create(runId, {
|
|
eventType: 'step_completed',
|
|
correlationId: 'step_after_noop',
|
|
specVersion: SPEC_VERSION_CURRENT,
|
|
eventData: { result: serialized(null) },
|
|
} as any);
|
|
await world.events.create(runId, {
|
|
eventType: 'run_completed',
|
|
specVersion: SPEC_VERSION_CURRENT,
|
|
eventData: { output: serialized(null) },
|
|
} as any);
|
|
await pool.end();
|
|
}, 60_000);
|
|
|
|
createTestSuite('./dist/index.js');
|
|
}
|