Skip to content

Commit 34ef42a

Browse files
os-zhuangclaude
andcommitted
test(trigger-record-change): end-to-end regression coverage for record-change flows (#1491)
#1491 reported record-change flows never firing on data writes (7.4.1–7.7.0). It no longer reproduces on current main — the triggers-first-class-dir + flow-engine-alignment refactor fixed the wiring. The gap that let it regress silently: the existing tests only used a FAKE data engine, never the real dispatch path. Adds a full-kernel integration test (ObjectQL + automation + record-change trigger + in-memory driver) that boots the real stack, inserts a row, and asserts a `record-after-create` flow fires and its `update_record` writes back — in BOTH registration orderings: 1. flow registered after the trigger (engine.registerFlow), and 2. flow pulled from the registry at automation.start(), bound when the trigger registers on kernel:ready (the production sequence). Adds @objectstack/driver-memory as a devDependency for the test. Test-only; empty changeset (no shipped-code change). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8c7e7e4 commit 34ef42a

4 files changed

Lines changed: 155 additions & 8 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
---
3+
4+
test(trigger-record-change): add end-to-end regression coverage for record-change flows (#1491)
5+
6+
Test-only — no shipped-code change, so no version bump. #1491 reported that
7+
record-change flows never fired on data writes (7.4.1–7.7.0); it no longer
8+
reproduces on current `main` (fixed by the triggers-first-class-dir + flow-engine
9+
alignment refactor). The existing tests only used a fake data engine, so the real
10+
path was uncovered. Adds a full-kernel integration test (ObjectQL + automation +
11+
record-change trigger + in-memory driver) asserting a `record-after-create` flow
12+
fires and its `update_record` writes back — in both registration orderings
13+
(direct `registerFlow`, and registry-pull at `automation.start()` with the
14+
trigger binding on `kernel:ready`).

packages/triggers/trigger-record-change/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
"@objectstack/spec": "workspace:*"
2222
},
2323
"devDependencies": {
24+
"@objectstack/driver-memory": "workspace:*",
25+
"@objectstack/objectql": "workspace:*",
26+
"@objectstack/service-automation": "workspace:*",
2427
"@types/node": "^25.9.2",
2528
"typescript": "^6.0.3",
2629
"vitest": "^4.1.8"
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* End-to-end integration test for the record-change trigger (#1491).
5+
*
6+
* #1491 reported that record-change flows never fired on data writes (observed
7+
* 7.4.1–7.7.0). The existing unit tests only exercised a *fake* data engine, so
8+
* they never covered the real path: a flow pulled into the automation engine,
9+
* the trigger binding to an ObjectQL lifecycle hook on `kernel:ready`, an actual
10+
* insert firing that hook, and the flow's `update_record` writing back through
11+
* the live data engine. This test boots a real kernel (ObjectQL + automation +
12+
* record-change trigger + in-memory driver) and asserts the full chain — in BOTH
13+
* registration orderings, since the engine relies on re-activating already-pulled
14+
* flows when the trigger registers later.
15+
*/
16+
17+
import { describe, it, expect } from 'vitest';
18+
import { ObjectKernel } from '@objectstack/core';
19+
import { ObjectQLPlugin } from '@objectstack/objectql';
20+
import { AutomationServicePlugin, type AutomationEngine } from '@objectstack/service-automation';
21+
import { InMemoryDriver } from '@objectstack/driver-memory';
22+
import { RecordChangeTriggerPlugin } from './plugin.js';
23+
24+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
25+
26+
/** A flow that stamps `stamp: 'done'` on the just-created record of `object`. */
27+
function stampFlow(name: string, object: string) {
28+
return {
29+
name,
30+
label: name,
31+
type: 'autolaunched',
32+
nodes: [
33+
{ id: 'start', type: 'start', label: 'Start', config: { objectName: object, triggerType: 'record-after-create' } },
34+
{ id: 'stamp', type: 'update_record', label: 'Stamp', config: { objectName: object, filter: { id: '{record.id}' }, fields: { stamp: 'done' } } },
35+
{ id: 'end', type: 'end', label: 'End' },
36+
],
37+
edges: [
38+
{ id: 'e1', source: 'start', target: 'stamp' },
39+
{ id: 'e2', source: 'stamp', target: 'end' },
40+
],
41+
};
42+
}
43+
44+
const objectDef = (name: string) => ({
45+
name,
46+
label: name,
47+
fields: {
48+
status: { name: 'status', label: 'S', type: 'text' },
49+
stamp: { name: 'stamp', label: 'St', type: 'text' },
50+
},
51+
});
52+
53+
describe('record-change trigger — end-to-end (#1491)', () => {
54+
it('fires a record-after-create flow registered AFTER the trigger (engine.registerFlow path)', async () => {
55+
const kernel = new ObjectKernel({ logLevel: 'silent' });
56+
await kernel.use(new ObjectQLPlugin());
57+
await kernel.use(new AutomationServicePlugin());
58+
await kernel.use(new RecordChangeTriggerPlugin());
59+
await kernel.bootstrap();
60+
61+
const objectql = kernel.getService('objectql') as any;
62+
const data = kernel.getService('data') as any;
63+
const automation = kernel.getService<AutomationEngine>('automation');
64+
65+
objectql.registerDriver(new InMemoryDriver(), true);
66+
objectql.registry.registerObject(objectDef('wid'), 'test', 'test');
67+
automation.registerFlow('stamp_flow', stampFlow('stamp_flow', 'wid') as any);
68+
69+
// The flow bound to the trigger…
70+
expect((automation as any).getActiveTriggerBindings()).toContainEqual({
71+
flowName: 'stamp_flow',
72+
triggerType: 'record_change',
73+
});
74+
75+
const created = await data.insert('wid', { status: 'new' });
76+
const id = Array.isArray(created) ? created[0]?.id : created?.id ?? created;
77+
await sleep(200);
78+
79+
const row = await data.findOne('wid', { where: { id } });
80+
expect(row?.stamp).toBe('done');
81+
}, 15000);
82+
83+
it('fires a flow PULLED FROM THE REGISTRY at automation.start(), bound when the trigger registers on kernel:ready (production ordering)', async () => {
84+
const flowDef = stampFlow('stamp_flow2', 'wid2');
85+
86+
// Seeds the driver + object + flow into the registry in start(), which runs
87+
// before AutomationServicePlugin.start() pulls flows — the production
88+
// sequence (metadata seeds → automation pulls → trigger binds on
89+
// kernel:ready via re-activation of the already-registered flow).
90+
const seeder = {
91+
name: 'test.seeder',
92+
type: 'standard',
93+
version: '1.0.0',
94+
dependencies: ['com.objectstack.engine.objectql'],
95+
async init() {},
96+
async start(ctx: any) {
97+
const ql = ctx.getService('objectql');
98+
ql.registerDriver(new InMemoryDriver(), true);
99+
ql.registry.registerObject(objectDef('wid2'), 'test', 'test');
100+
ql.registry.registerItem('flow', flowDef, 'name', 'test');
101+
},
102+
};
103+
104+
const kernel = new ObjectKernel({ logLevel: 'silent' });
105+
await kernel.use(new ObjectQLPlugin());
106+
await kernel.use(seeder as any);
107+
await kernel.use(new AutomationServicePlugin());
108+
await kernel.use(new RecordChangeTriggerPlugin());
109+
await kernel.bootstrap();
110+
111+
const data = kernel.getService('data') as any;
112+
const automation = kernel.getService<AutomationEngine>('automation');
113+
114+
// The registry-pulled flow bound to the trigger after kernel:ready.
115+
expect((automation as any).getActiveTriggerBindings()).toContainEqual({
116+
flowName: 'stamp_flow2',
117+
triggerType: 'record_change',
118+
});
119+
120+
const created = await data.insert('wid2', { status: 'new' });
121+
const id = Array.isArray(created) ? created[0]?.id : created?.id ?? created;
122+
await sleep(200);
123+
124+
const row = await data.findOne('wid2', { where: { id } });
125+
expect(row?.stamp).toBe('done');
126+
}, 15000);
127+
});

pnpm-lock.yaml

Lines changed: 11 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)