@multiplier-labs/stepflow 0.3.1 → 0.3.4
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/.codekin/reports/code-review/2026-04-11_code-review-daily.md +146 -0
- package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +74 -0
- package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
- package/.codekin/reports/comments/2026-04-11_comment-assessment.md +161 -0
- package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -0
- package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
- package/.codekin/reports/complexity/2026-04-11_complexity-report.md +152 -0
- package/.codekin/reports/complexity/2026-04-14_complexity-report.md +34 -0
- package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
- package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
- package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
- package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +138 -0
- package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +121 -0
- package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
- package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
- package/.github/CODEOWNERS +15 -0
- package/.github/dependabot.yml +23 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/publish.yml +17 -5
- package/.github/workflows/security.yml +29 -0
- package/CHANGELOG.md +34 -0
- package/README.md +24 -0
- package/SECURITY.md +40 -0
- package/dist/{chunk-FQCT3I2A.js → chunk-5XJBAPXF.js} +527 -551
- package/dist/chunk-5XJBAPXF.js.map +1 -0
- package/dist/{chunk-U5QGN3RT.js → chunk-FMPJBK2N.js} +67 -55
- package/dist/chunk-FMPJBK2N.js.map +1 -0
- package/dist/{chunk-GXY66V7Q.js → chunk-JPXPB7U5.js} +52 -2
- package/dist/chunk-JPXPB7U5.js.map +1 -0
- package/dist/{chunk-XCF4PLN7.js → chunk-U3PCVK6O.js} +276 -61
- package/dist/chunk-U3PCVK6O.js.map +1 -0
- package/dist/events/index.d.ts +60 -7
- package/dist/events/index.js +1 -1
- package/dist/{index-CPfGsmrp.d.ts → index-Cd0IZ40L.d.ts} +56 -17
- package/dist/index.d.ts +15 -23
- package/dist/index.js +784 -547
- package/dist/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +4 -4
- package/dist/scheduler/index.js +2 -2
- package/dist/storage/index.d.ts +322 -56
- package/dist/storage/index.js +4 -2
- package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
- package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
- package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
- package/docs/stepflow-api-reference.md +5 -4
- package/package.json +12 -8
- package/src/core/engine/concurrency.ts +229 -0
- package/src/core/engine/lifecycle.ts +176 -0
- package/src/core/engine.test.ts +536 -522
- package/src/core/engine.ts +116 -235
- package/src/core/index.ts +3 -3
- package/src/core/orchestrator/persistence.ts +165 -0
- package/src/core/orchestrator/plan-resolution.ts +140 -0
- package/src/core/orchestrator/state-transitions.ts +109 -0
- package/src/core/orchestrator/step-runner.ts +427 -0
- package/src/core/orchestrator.ts +57 -395
- package/src/core/types.ts +7 -29
- package/src/events/index.ts +4 -4
- package/src/events/memory.test.ts +39 -41
- package/src/events/memory.ts +7 -10
- package/src/events/socketio.test.ts +112 -162
- package/src/events/socketio.ts +20 -27
- package/src/events/types.ts +16 -19
- package/src/events/webhook.test.ts +489 -235
- package/src/events/webhook.ts +355 -139
- package/src/index.ts +19 -19
- package/src/planning/evaluate-condition.test.ts +153 -0
- package/src/planning/evaluate-condition.ts +105 -0
- package/src/planning/index.ts +6 -3
- package/src/planning/planner.ts +45 -152
- package/src/planning/planning.test.ts +303 -366
- package/src/planning/registry.test.ts +204 -251
- package/src/planning/registry.ts +20 -106
- package/src/planning/types.ts +23 -27
- package/src/scheduler/cron.test.ts +149 -150
- package/src/scheduler/cron.ts +58 -69
- package/src/scheduler/index.ts +4 -14
- package/src/scheduler/postgres-persistence.ts +89 -105
- package/src/scheduler/sqlite-persistence.test.ts +96 -96
- package/src/scheduler/sqlite-persistence.ts +40 -22
- package/src/scheduler/types.ts +4 -9
- package/src/storage/index.ts +5 -5
- package/src/storage/memory.test.ts +133 -183
- package/src/storage/memory.ts +21 -42
- package/src/storage/postgres-core.ts +550 -0
- package/src/storage/postgres-migration.test.ts +69 -0
- package/src/storage/postgres-transaction-pagination.test.ts +107 -0
- package/src/storage/postgres.ts +222 -937
- package/src/storage/sqlite-core.ts +151 -0
- package/src/storage/sqlite.test.ts +330 -281
- package/src/storage/sqlite.ts +225 -170
- package/src/storage/types.ts +33 -58
- package/src/utils/errors.test.ts +78 -80
- package/src/utils/errors.ts +27 -57
- package/src/utils/id.ts +3 -3
- package/src/utils/index.ts +6 -4
- package/src/utils/logger.test.ts +52 -58
- package/src/utils/logger.ts +4 -4
- package/src/utils/pinned-fetch.test.ts +84 -0
- package/src/utils/pinned-fetch.ts +118 -0
- package/src/utils/postgres-deps.ts +20 -4
- package/src/utils/retry.test.ts +47 -67
- package/src/utils/retry.ts +9 -9
- package/src/utils/safe-json.test.ts +166 -0
- package/src/utils/safe-json.ts +107 -0
- package/src/utils/webhook-signature.test.ts +111 -0
- package/src/utils/webhook-signature.ts +74 -0
- package/dist/chunk-FQCT3I2A.js.map +0 -1
- package/dist/chunk-GXY66V7Q.js.map +0 -1
- package/dist/chunk-U5QGN3RT.js.map +0 -1
- package/dist/chunk-XCF4PLN7.js.map +0 -1
- package/src/scheduler/postgres-persistence.test.ts +0 -906
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for PostgresStorageAdapter migration error handling.
|
|
3
|
+
* See audit 2026-04-27 finding C1: schema migrations must surface failures
|
|
4
|
+
* instead of silently swallowing them.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { PostgresStorageAdapter } from './postgres';
|
|
9
|
+
|
|
10
|
+
interface QueryHandler {
|
|
11
|
+
(text: string, params: unknown[]): Promise<{ command: string; rowCount: number; rows: unknown[] }>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build the smallest pg.Pool-like object Kysely's PostgresDialect needs:
|
|
16
|
+
* `connect()` returning a client with `query()` and `release()`, plus `end()`.
|
|
17
|
+
*/
|
|
18
|
+
function buildFakePool(handler: QueryHandler) {
|
|
19
|
+
const client = {
|
|
20
|
+
query: (text: string, params: unknown[] = []) => handler(text, params),
|
|
21
|
+
release: () => {},
|
|
22
|
+
};
|
|
23
|
+
const pool = {
|
|
24
|
+
connect: async () => client,
|
|
25
|
+
end: async () => {},
|
|
26
|
+
on: () => pool,
|
|
27
|
+
off: () => pool,
|
|
28
|
+
};
|
|
29
|
+
return pool;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('PostgresStorageAdapter — migration error propagation', () => {
|
|
33
|
+
it('rethrows a non-benign migration error with a clear message', async () => {
|
|
34
|
+
const handler: QueryHandler = async (text) => {
|
|
35
|
+
if (/ALTER\s+TABLE/i.test(text)) {
|
|
36
|
+
// Simulate a non-benign migration failure (e.g., permission denied).
|
|
37
|
+
const err: Error & { code?: string } = new Error('permission denied for table runs');
|
|
38
|
+
err.code = '42501'; // insufficient_privilege — not in benign list
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
return { command: 'CREATE', rowCount: 0, rows: [] };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const adapter = new PostgresStorageAdapter({
|
|
45
|
+
pool: buildFakePool(handler) as unknown as import('pg').Pool,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await expect(adapter.initialize()).rejects.toThrow(/Schema migration failed/);
|
|
49
|
+
await expect(adapter.initialize()).rejects.toThrow(/permission denied/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('tolerates benign duplicate-column errors during idempotent re-runs', async () => {
|
|
53
|
+
const handler: QueryHandler = async (text) => {
|
|
54
|
+
if (/ALTER\s+TABLE/i.test(text)) {
|
|
55
|
+
const err: Error & { code?: string } = new Error('column "priority" of relation "runs" already exists');
|
|
56
|
+
err.code = '42701'; // duplicate_column — benign
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
return { command: 'CREATE', rowCount: 0, rows: [] };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const adapter = new PostgresStorageAdapter({
|
|
63
|
+
pool: buildFakePool(handler) as unknown as import('pg').Pool,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await expect(adapter.initialize()).resolves.toBeUndefined();
|
|
67
|
+
await adapter.close();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for PostgresTransactionAdapter.listRuns pagination total.
|
|
3
|
+
* See audit 2026-04-27 finding C2: when listRuns is called inside a
|
|
4
|
+
* transaction, the returned `total` must reflect the rows visible to that
|
|
5
|
+
* transaction (via COUNT(*) on the same connection), not the size of the
|
|
6
|
+
* returned page.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
10
|
+
import { PostgresStorageAdapter, PostgresTransactionAdapter } from './postgres';
|
|
11
|
+
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
// PostgresTransactionAdapter.listRuns uses the module-level `sql` template
|
|
14
|
+
// tag from postgres.ts, which is only populated when a PostgresStorageAdapter
|
|
15
|
+
// is initialized. Initialize one with a no-op fake pool so the module state
|
|
16
|
+
// is ready for the direct unit test below.
|
|
17
|
+
const noopPool = {
|
|
18
|
+
connect: async () => ({
|
|
19
|
+
query: async () => ({ command: 'SELECT', rowCount: 0, rows: [] }),
|
|
20
|
+
release: () => {},
|
|
21
|
+
}),
|
|
22
|
+
end: async () => {},
|
|
23
|
+
on: () => noopPool,
|
|
24
|
+
off: () => noopPool,
|
|
25
|
+
};
|
|
26
|
+
const bootstrap = new PostgresStorageAdapter({
|
|
27
|
+
pool: noopPool as unknown as import('pg').Pool,
|
|
28
|
+
});
|
|
29
|
+
await bootstrap.initialize();
|
|
30
|
+
await bootstrap.close();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build a Kysely-transaction-shaped stub backed by an in-memory row list.
|
|
35
|
+
* It distinguishes the COUNT(*) query (started with `selectFrom().select()`)
|
|
36
|
+
* from the data query (started with `selectFrom().selectAll()`) and replies
|
|
37
|
+
* with the right shape for each.
|
|
38
|
+
*/
|
|
39
|
+
function buildFakeTrx<T extends Record<string, unknown>>(rows: T[]) {
|
|
40
|
+
function buildCountChain() {
|
|
41
|
+
const chain: any = {};
|
|
42
|
+
chain.where = () => chain;
|
|
43
|
+
chain.executeTakeFirst = async () => ({ count: rows.length });
|
|
44
|
+
return chain;
|
|
45
|
+
}
|
|
46
|
+
function buildDataChain() {
|
|
47
|
+
const chain: any = {};
|
|
48
|
+
let limit: number | undefined;
|
|
49
|
+
let offset = 0;
|
|
50
|
+
chain.where = () => chain;
|
|
51
|
+
chain.orderBy = () => chain;
|
|
52
|
+
chain.limit = (n: number) => { limit = n; return chain; };
|
|
53
|
+
chain.offset = (n: number) => { offset = n; return chain; };
|
|
54
|
+
chain.execute = async () => {
|
|
55
|
+
const sliced = rows.slice(offset, limit !== undefined ? offset + limit : undefined);
|
|
56
|
+
return sliced;
|
|
57
|
+
};
|
|
58
|
+
return chain;
|
|
59
|
+
}
|
|
60
|
+
const qb = {
|
|
61
|
+
selectFrom: () => ({
|
|
62
|
+
select: () => buildCountChain(),
|
|
63
|
+
selectAll: () => buildDataChain(),
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
return { withSchema: () => qb } as any;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('PostgresTransactionAdapter — listRuns pagination total', () => {
|
|
70
|
+
it('reports total from COUNT(*) on the same transaction, not the page size', async () => {
|
|
71
|
+
// Simulate 7 rows visible in the transaction; ask for a page of 3.
|
|
72
|
+
const fakeRow = {
|
|
73
|
+
id: 'r', kind: 'k', status: 'queued', parent_run_id: null,
|
|
74
|
+
input_json: '{}', metadata_json: '{}', context_json: '{}',
|
|
75
|
+
output_json: null, error_json: null, priority: 0, timeout_ms: null,
|
|
76
|
+
created_at: new Date(), started_at: null, finished_at: null,
|
|
77
|
+
};
|
|
78
|
+
const rows = Array.from({ length: 7 }, (_, i) => ({ ...fakeRow, id: `r${i}` }));
|
|
79
|
+
const trx = buildFakeTrx(rows);
|
|
80
|
+
|
|
81
|
+
const adapter = new PostgresTransactionAdapter(trx, 'public');
|
|
82
|
+
const result = await adapter.listRuns({ limit: 3 });
|
|
83
|
+
|
|
84
|
+
// Page contains only 3 rows...
|
|
85
|
+
expect(result.items).toHaveLength(3);
|
|
86
|
+
// ...but total reflects all 7 rows visible in the transaction.
|
|
87
|
+
// Pre-fix this returned 3 (rows.length).
|
|
88
|
+
expect(result.total).toBe(7);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('total reflects all created rows even when limit exceeds page size', async () => {
|
|
92
|
+
const fakeRow = {
|
|
93
|
+
id: 'r', kind: 'k', status: 'queued', parent_run_id: null,
|
|
94
|
+
input_json: '{}', metadata_json: '{}', context_json: '{}',
|
|
95
|
+
output_json: null, error_json: null, priority: 0, timeout_ms: null,
|
|
96
|
+
created_at: new Date(), started_at: null, finished_at: null,
|
|
97
|
+
};
|
|
98
|
+
const rows = Array.from({ length: 5 }, (_, i) => ({ ...fakeRow, id: `r${i}` }));
|
|
99
|
+
const trx = buildFakeTrx(rows);
|
|
100
|
+
|
|
101
|
+
const adapter = new PostgresTransactionAdapter(trx, 'public');
|
|
102
|
+
const result = await adapter.listRuns({ limit: 100 });
|
|
103
|
+
|
|
104
|
+
expect(result.items).toHaveLength(5);
|
|
105
|
+
expect(result.total).toBe(5);
|
|
106
|
+
});
|
|
107
|
+
});
|