Skip to content

Commit 7998108

Browse files
committed
test(automation): big-loop run-history integration test + fix pre-existing test-file type errors
Adds an end-to-end run-history.test.ts case that drives a >MAX-step loop through the real engine (execute → recordTerminal → restart → getRun) and asserts the region-aware history compaction (#3234) keeps the loop container + most-recent iterations with no orphans across a restart — complementing the pure-function unit tests already merged. Also clears the pre-existing `tsc --noEmit` errors in the package's test files so the whole @objectstack/service-automation package type-checks clean: - engine.test.ts: add required `maturity: 'ga'` to two inline action descriptors; underscore an unused executor param. - nested-composition.test.ts: type the `{}`-inferred flow output before indexing. - connector-descriptor-audit.test.ts: annotate two implicit-any callback params. - connector-nodes.test.ts / connector-materialization.test.ts: `as unknown as Connector` on the partial connector-def test doubles (the runtime reads a subset). Test-only — no runtime/API change. Full suite 329/329 green; tsc --noEmit clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VuvxWgoadqryqBcjs7TpVi
1 parent bd7df45 commit 7998108

7 files changed

Lines changed: 87 additions & 11 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
---
3+
4+
test(automation): big-loop run-history integration test + fix pre-existing test-file type errors
5+
6+
Test-only; no runtime/API change (nothing under `src/` behaviour is touched), so
7+
this releases nothing. Adds an end-to-end `run-history.test.ts` case exercising
8+
the region-aware history compaction (#3234) through the real engine
9+
(execute → recordTerminal → restart → getRun) with a >MAX-step loop, and clears
10+
the pre-existing `tsc --noEmit` errors across the package's test files (missing
11+
`maturity`, implicit `any`, `ConnectorProviderFactory` test-double defs, an
12+
unused param, a `{}`-typed output access) so the whole package type-checks clean.

packages/services/service-automation/src/builtin/connector-nodes.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ describe('AutomationEngine connector registry', () => {
193193
inputSchema: { message: { type: 'string' } },
194194
},
195195
],
196-
} as Connector,
196+
} as unknown as Connector,
197197
{ async echo() { return {}; } },
198198
);
199199

packages/services/service-automation/src/builtin/nested-composition.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,6 @@ describe('nested control-flow composition (ADR-0031)', () => {
183183
expect(result.success).toBe(true);
184184
// The loop body's mutation of `lastSeen` survived to the flow output — the
185185
// region ran in the enclosing scope, last iteration wins.
186-
expect(result.output?.lastSeen).toBe('n:Z');
186+
expect((result.output as Record<string, unknown> | undefined)?.lastSeen).toBe('n:Z');
187187
});
188188
});

packages/services/service-automation/src/connector-descriptor-audit.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ async function bootWithConnectors(
8181

8282
function auditWarnings(warnSpy: ReturnType<typeof vi.spyOn>): string[] {
8383
return warnSpy.mock.calls
84-
.map((c) => String(c[0]))
85-
.filter((m) => m.includes('declarative connector'));
84+
.map((c: unknown[]) => String(c[0]))
85+
.filter((m: string) => m.includes('declarative connector'));
8686
}
8787

8888
describe('findInertDeclaredConnectors (pure contract)', () => {

packages/services/service-automation/src/connector-materialization.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import * as path from 'node:path';
1515
import { describe, it, expect, afterAll, afterEach, vi } from 'vitest';
1616
import { LiteKernel } from '@objectstack/core';
1717
import type {
18+
Connector,
1819
ConnectorProviderContext,
1920
ConnectorProviderFactory,
2021
ConnectorInstanceAuth,
@@ -69,7 +70,7 @@ function makeFakeProvider() {
6970
description: ctx.description,
7071
authentication: { type: 'none' },
7172
actions: [{ key: 'ping', label: 'Ping' }],
72-
},
73+
} as unknown as Connector,
7374
handlers: {
7475
ping: async (input: Record<string, unknown>) => ({
7576
ok: true,
@@ -155,7 +156,7 @@ function makeClosableProvider() {
155156
type: 'api',
156157
authentication: { type: 'none' },
157158
actions: [{ key: 'ping', label: 'Ping' }],
158-
},
159+
} as unknown as Connector,
159160
handlers: { ping: async () => ({ ok: true }) },
160161
close: async () => { closed.push(ctx.name); },
161162
};
@@ -459,7 +460,7 @@ function makeFileReadingProvider() {
459460
const text = await ctx.loadPackageFile!(String(ctx.providerConfig.spec));
460461
reads.push(text);
461462
return {
462-
def: { name: ctx.name, label: ctx.label, type: 'api', authentication: { type: 'none' }, actions: [{ key: 'ping', label: 'Ping' }] },
463+
def: { name: ctx.name, label: ctx.label, type: 'api', authentication: { type: 'none' }, actions: [{ key: 'ping', label: 'Ping' }] } as unknown as Connector,
463464
handlers: { ping: async () => ({ ok: true }) },
464465
};
465466
};
@@ -573,7 +574,7 @@ function makeFlakyUpstreamProvider(opts: { down: boolean }) {
573574
type: 'api',
574575
authentication: { type: 'none' },
575576
actions: [{ key: 'ping', label: 'Ping' }],
576-
},
577+
} as unknown as Connector,
577578
handlers: { ping: async () => ({ ok: true }) },
578579
close: async () => { closed.push(ctx.name); },
579580
};

packages/services/service-automation/src/engine.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ describe('AutomationEngine', () => {
173173
it('should execute nodes and collect output', async () => {
174174
engine.registerNodeExecutor({
175175
type: 'assignment',
176-
async execute(node, variables) {
176+
async execute(_node, variables) {
177177
variables.set('result', 42);
178178
return { success: true };
179179
},
@@ -2242,6 +2242,7 @@ describe('Action Descriptor Registry (ADR-0018)', () => {
22422242
isAsync: false,
22432243
source: 'plugin',
22442244
deprecated: false,
2245+
maturity: 'ga',
22452246
},
22462247
async execute() { return { success: true }; },
22472248
});
@@ -2262,7 +2263,7 @@ describe('Action Descriptor Registry (ADR-0018)', () => {
22622263
type: 'send_sms', version: '1.0.0', name: 'Send SMS',
22632264
category: 'io', paradigms: ['flow'], supportsPause: false,
22642265
supportsCancellation: false, supportsRetry: true,
2265-
needsOutbox: false, isAsync: false, source: 'plugin', deprecated: false,
2266+
needsOutbox: false, isAsync: false, source: 'plugin', deprecated: false, maturity: 'ga',
22662267
},
22672268
async execute() { return { success: true }; },
22682269
});

packages/services/service-automation/src/run-history.test.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
import { describe, it, expect } from 'vitest';
1010
import { AutomationEngine, compactStepLogForHistory, MAX_PERSISTED_HISTORY_STEPS } from './engine.js';
11-
import type { StepLogEntry } from './engine.js';
11+
import type { StepLogEntry, NodeExecutor } from './engine.js';
12+
import { registerLoopNode } from './builtin/loop-node.js';
1213
import { InMemorySuspendedRunStore } from './suspended-run-store.js';
1314
import type { AutomationContext } from '@objectstack/spec/contracts';
1415

@@ -256,3 +257,64 @@ describe('compactStepLogForHistory (#3234 region-aware history compaction)', ()
256257
expect(out.some((s) => s.nodeId === 'outer')).toBe(true);
257258
});
258259
});
260+
261+
// End-to-end through the real engine: a >MAX-step loop must fold its per-iteration
262+
// body steps into the run log (#1479), then survive durable persistence +
263+
// rehydration (#2585) with region-aware compaction (#3234) — not just the pure
264+
// `compactStepLogForHistory` unit above.
265+
describe('region-aware compaction — end-to-end through the engine (#3234)', () => {
266+
const pluginCtx = () => ({ logger: silent, getService() { throw new Error('none'); } }) as never;
267+
268+
it('a >MAX-step loop persists its container + recent iterations (no orphans) across a restart', async () => {
269+
const store = new InMemorySuspendedRunStore();
270+
const engineA = new AutomationEngine(silent, store);
271+
registerLoopNode(engineA, pluginCtx());
272+
// A trivial body node so each iteration contributes exactly one logged step.
273+
engineA.registerNodeExecutor({ type: 'touch', async execute() { return { success: true }; } } as NodeExecutor);
274+
275+
const N = 250; // > MAX_PERSISTED_HISTORY_STEPS (200)
276+
engineA.registerFlow('big_loop', {
277+
name: 'big_loop', label: 'Big Loop', type: 'autolaunched',
278+
nodes: [
279+
{ id: 'start', type: 'start', label: 'Start' },
280+
{ id: 'loop', type: 'loop', label: 'Loop', config: {
281+
collection: Array.from({ length: N }, (_, i) => i),
282+
iteratorVariable: 'item', indexVariable: 'i',
283+
body: { nodes: [{ id: 'touch', type: 'touch', label: 'Touch' }], edges: [] },
284+
} },
285+
{ id: 'end', type: 'end', label: 'End' },
286+
],
287+
edges: [
288+
{ id: 'e1', source: 'start', target: 'loop' },
289+
{ id: 'e2', source: 'loop', target: 'end' },
290+
],
291+
} as never);
292+
293+
const res = await engineA.execute('big_loop', { event: 'test' } as AutomationContext);
294+
expect(res.success).toBe(true);
295+
await flush(); // recordTerminal is fire-and-forget
296+
297+
// The in-memory log keeps FULL detail: the container + all N body steps.
298+
const live = (await engineA.listRuns('big_loop'))[0];
299+
expect(live.steps.filter((s) => s.nodeId === 'touch')).toHaveLength(N);
300+
301+
// Simulate a restart: a fresh engine with empty in-memory logs sharing the
302+
// same durable store → getRun serves the COMPACTED history row.
303+
const engineB = new AutomationEngine(silent, store);
304+
const [listed] = await engineB.listRuns('big_loop', { limit: 1 });
305+
const run = await engineB.getRun(listed.id);
306+
expect(run).not.toBeNull();
307+
expect(run!.status).toBe('completed');
308+
309+
const steps = run!.steps;
310+
expect(steps.length).toBeLessThanOrEqual(MAX_PERSISTED_HISTORY_STEPS); // bounded (#2585)
311+
// The loop CONTAINER survived — pre-#3234 the plain tail-slice dropped it,
312+
// orphaning every retained body step.
313+
expect(steps.some((s) => s.nodeId === 'loop')).toBe(true);
314+
// The most recent iteration survived (tail preserved).
315+
const bodyIters = steps.filter((s) => s.nodeId === 'touch').map((s) => s.iteration!);
316+
expect(Math.max(...bodyIters)).toBe(N - 1);
317+
// No retained body step is orphaned from its container.
318+
expect(hasNoOrphans(steps)).toBe(true);
319+
});
320+
});

0 commit comments

Comments
 (0)