Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 141 additions & 4 deletions sdk/tests/authored-flow.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { rmSync } from 'node:fs';
import type { Server } from 'node:net';
import { flow, type FlowHeader } from '@relayflows/surface';
import { flow, type Ctx, type FlowHeader } from '@relayflows/surface';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { executeAuthoredFlow } from '../src/authored-flow-executor.js';
import { JournalClient } from '../src/journal-client.js';
Expand Down Expand Up @@ -58,9 +58,7 @@ describe('authored flow journal executor', () => {
disposition: 'step_done',
output: failed ? null : {
exit_code: 0,
stdout_tail: step.command === 'printf authored-journal-ok'
? 'authored-journal-ok'
: '',
stdout_tail: outputFor(step.command),
stderr_tail: '',
},
},
Expand Down Expand Up @@ -386,4 +384,143 @@ describe('authored flow journal executor', () => {
client.close();
}
});
it('passes direct input into the journal-backed authored body', async () => {
const handle = flow<{ value: string }>('input-backed', async (f, input) => {
await f.run(`emit:${input.value}`);
f.done('success');
});
const client = await connectedClient('authored-flow-input-test');
const before = startedSpecs.length;

try {
await executeAuthoredFlow(handle, client, { value: 'from-direct-input' });
expect(commandsSince(before)).toEqual(['emit:from-direct-input', ':']);
} finally {
client.close();
}
});

it.each([
['truthiness', async (f: Ctx, value: string) => {
if (value) await f.run('branch:truthy');
}, 'value', 'branch:truthy'],
['negation', async (f: Ctx, value: string) => {
if (!value) await f.run('branch:negated');
}, '', 'branch:negated'],
['loose equality', async (f: Ctx, value: string) => {
if (value == 'value') await f.run('branch:loose-equal');
}, 'value', 'branch:loose-equal'],
['strict equality', async (f: Ctx, value: string) => {
if (value === 'value') await f.run('branch:strict-equal');
}, 'value', 'branch:strict-equal'],
['ternary', async (f: Ctx, value: string) => {
await f.run(value ? 'branch:ternary-true' : 'branch:ternary-false');
}, '', 'branch:ternary-false'],
['logical and', async (f: Ctx, value: string) => {
value && await f.run('branch:logical-and');
}, 'value', 'branch:logical-and'],
['logical or', async (f: Ctx, value: string) => {
value || await f.run('branch:logical-or');
}, '', 'branch:logical-or'],
])('evaluates journal output through JavaScript %s', async (_label, branch, emitted, expected) => {
const handle = flow(`operator-${_label}`, async (f) => {
const value = await f.run(`emit:${emitted}`);
await branch(f, value);
f.done('success');
});
const client = await connectedClient(`authored-flow-${_label}-test`);
const before = startedSpecs.length;

try {
await executeAuthoredFlow(handle, client);
expect(commandsSince(before)).toEqual([`emit:${emitted}`, expected, ':']);
} finally {
client.close();
}
});

it('preserves separately awaited sibling ordering before the join', async () => {
const handle = flow('separate-awaits', async (f) => {
const left = f.run('emit:left');
const right = f.run('emit:right');
await left;
await right;
await f.run('joined');
f.done('success');
});
const client = await connectedClient('authored-flow-separate-awaits-test');
const before = startedSpecs.length;

try {
await executeAuthoredFlow(handle, client);
expect(commandsSince(before)).toEqual(['emit:left', 'emit:right', 'joined', ':']);
} finally {
client.close();
}
});

it('requires an explicit completion after journal-backed steps', async () => {
const handle = flow('missing-completion', async (f) => {
await f.run('emit:ran');
});
const client = await connectedClient('authored-flow-missing-completion-test');
try {
// The PROPERTY is what this pins: a body that runs a journal-backed step
// and never calls done() must be refused, not silently completed.
//
// The original version of this test (dropped in #140, restored here)
// asserted `missing_completion`. On current main the refusal arrives as
// `unawaited_step` instead, because `verifyAuthoredOperations` runs
// before the `requestedCompletion === undefined` check in
// `authored-flow-executor.ts` and throws first. The body here DOES await
// its step, so "returned with unawaited steps" is a misleading label for
// it -- see #183. Asserting either code specifically would either fail on
// main or bake in a message that looks wrong, so this asserts the
// refusal and its class instead.
await expect(executeAuthoredFlow(handle, client)).rejects.toMatchObject({
name: 'AuthoredFlowExecutionError',
});
} finally {
client.close();
}
});

/// Reaches `missing_completion` itself, which the case above cannot.
///
/// With a journal-backed step in the body, `verifyAuthoredOperations` refuses
/// first and `missing_completion` is never evaluated -- verified by disabling
/// that check, which left every other test in this file green. A body with no
/// operations to verify is what actually exercises it.
it('refuses a body that completes nothing at all', async () => {
const handle = flow('no-completion-no-steps', async () => {
// no f.run, no f.done
});
const client = await connectedClient('authored-flow-no-completion-test');
try {
await expect(executeAuthoredFlow(handle, client)).rejects.toMatchObject({
code: 'missing_completion',
});
} finally {
client.close();
}
});

async function connectedClient(name: string): Promise<JournalClient> {
const client = new JournalClient(path, { requestTimeoutMs: 2000 });
await client.connect();
await client.hello(name);
return client;
}

function commandsSince(index: number): string[] {
return startedSpecs.slice(index).map((spec) => {
const steps = spec['steps'] as Record<string, unknown>[];
return steps[0]!['command'] as string;
});
}
});

function outputFor(command: string): string {
if (command.startsWith('emit:')) return command.slice('emit:'.length);
return command === 'printf authored-journal-ok' ? 'authored-journal-ok' : '';
}
Loading