Skip to content
Merged
6 changes: 3 additions & 3 deletions web/lib/flow-onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ export function factoryCodeSections(draft: FactoryDraft, target: 'cloud' | 'loca
// such a step runs unmetered, so a dollar cap cannot bound it. Wall-clock is
// enforced on every step regardless of pricing, which is why it stays the
// default here; `{ dollars, wallclock }` together is also valid. Its length
// is the flow's time plan (FLOW_TIME): Cloud's 60-minute cap on a hosted run
// (AgentWorkforce/cloud#4235), and 3h for a local run, which has no sandbox
// lifetime and cannot stop an agent at a limit.
// is the flow's time plan (FLOW_TIME): 2h, an hour under Cloud's 180-minute
// cap on a hosted run (AgentWorkforce/cloud#4270), and 3h for a local run,
// which has no sandbox lifetime and cannot stop an agent at a limit.
const budget = `{ wallclock: "${(target === 'cloud' ? FLOW_TIME.headerMinutes : FLOW_TIME.localHeaderMinutes) / 60}h" }`;
if (!draft.sources.length) return [{ id: 'empty', code: `import { flow } from "@relayflows/surface";

Expand Down
326 changes: 253 additions & 73 deletions web/lib/flow-workflows.ts

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion web/lib/test/flow-agent-settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ describe('per-step agent settings', () => {

it('pins every Claude step in the simple prebuilt flow to a probeable model', async () => {
const calls = await execute({ ...draft, agents: ['claude'], workflow: 'simple' });
expect(Object.keys(calls)).toEqual(['check-discovery', 'implementer', 'check-repair']);
// Checks that still fail after the first repair get a second round.
expect(Object.keys(calls)).toEqual(['check-discovery', 'implementer', 'check-repair', 'check-repair-2']);
for (const options of Object.values(calls)) {
expect(options).toMatchObject({ cli: 'claude', model: 'claude-sonnet-5' });
}
Expand Down
321 changes: 273 additions & 48 deletions web/lib/test/flow-budget.test.ts

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions web/lib/test/flow-local.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ import { FLOW_BASE_CHECK_COMMAND, FLOW_CHECK_RUN_COMMAND, FLOW_REVIEW_BLOCKED_CO
*/
// Every push goes through the workflow-file guard (run 065fd98f).
const PUSH = 'base=; ' + FLOW_PUSH_COMMAND + ' --set-upstream origin HEAD';
/** A check command without the limit the flow gives it (RELAYFLOW_CHECK_TIMEOUT), so it compares as written. */
const plain = (command: string) => command.replace(/RELAYFLOW_CHECK_TIMEOUT=\d+; /, '');
/**
* A check command without what the flow gives it, so it compares as written:
* a branch check's ID, total and wait, in that order, or the base check's
* limit alone. A branch check missing any of the three no longer matches.
*/
const plain = (command: string) => command.replace(/^RELAYFLOW_CHECK_ID=\d+-\d+; RELAYFLOW_CHECK_TIMEOUT=\d+; RELAYFLOW_CHECK_WAIT=\d+; /, '').replace(/^(base=[^;]*; )RELAYFLOW_CHECK_TIMEOUT=\d+; /, '$1');

function answer(raw: string, { publish = 'publish', clean = 'yes', check = 'pass' as string | (() => string), baseline = 'pass' } = {}) {
const command = plain(raw);
Expand Down
19 changes: 12 additions & 7 deletions web/lib/test/flow-onboarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ import { FLOW_PUSH_COMMAND, FLOW_REFERENCE_LIMIT } from '../flow-workflows';

// Every push goes through the workflow-file guard (run 065fd98f).
const PUSH = 'base=abc123; ' + FLOW_PUSH_COMMAND + ' --set-upstream origin HEAD';
/** A check command without the limit the flow gives it (RELAYFLOW_CHECK_TIMEOUT), so it compares as written. */
const plain = (command: string) => command.replace(/RELAYFLOW_CHECK_TIMEOUT=\d+; /, '');
/**
* A check command without what the flow gives it, so it compares as written:
* a branch check's ID, total and wait, in that order, or the base check's
* limit alone. A branch check missing any of the three no longer matches.
*/
const plain = (command: string) => command.replace(/^RELAYFLOW_CHECK_ID=\d+-\d+; RELAYFLOW_CHECK_TIMEOUT=\d+; RELAYFLOW_CHECK_WAIT=\d+; /, '').replace(/^(base=[^;]*; )RELAYFLOW_CHECK_TIMEOUT=\d+; /, '$1');

const matchingIssue = { source: 'github', title: ' Fix login ', body: 'Login fails', labels: ['ready', 'bug'], repository: 'acme/app', identifier: '#507', url: 'https://github.com/acme/app/issues/507' };

Expand Down Expand Up @@ -101,7 +105,7 @@ describe('software factory onboarding', () => {
const again = source.indexOf('if (checkPlan === "none") await f.run(resolveChecks);');
expect(implementer, workflow).toBeGreaterThan(-1);
expect(again, workflow).toBeGreaterThan(implementer);
expect(source.indexOf('await timedCheck("", runChecks'), workflow).toBeGreaterThan(again);
expect(source.indexOf('await spannedCheck(checkLimit)'), workflow).toBeGreaterThan(again);
}
});

Expand Down Expand Up @@ -249,8 +253,8 @@ describe('software factory onboarding', () => {
it('gives Cloud flows a wall-clock budget so unpriced agents are never refused', () => {
for (const agents of [['claude', 'codex'], ['codex'], ['claude']] as FactoryDraft['agents'][]) {
const source = factorySource({ ...completed, agents });
// Cloud's cap on a hosted run (AgentWorkforce/cloud#4235).
expect(source).toContain('{ budget: { wallclock: "1h" } }');
// An hour under Cloud's 180-minute cap on a hosted run (AgentWorkforce/cloud#4270).
expect(source).toContain('{ budget: { wallclock: "2h" } }');
expect(source).not.toMatch(/budget: "\$\d/);
}
});
Expand Down Expand Up @@ -532,8 +536,9 @@ describe('software factory onboarding', () => {
it('opens a draft and keeps reviewing when the base commit fails the same checks', async () => {
// 139d1a46's shape: the failure is the environment's, not the change's.
const { calls, finish, errors } = await runFactory([true, true], true, matchingIssue, completed, 'publish', ['fail', 'fail', 'fail'], 'fail');
// One repair round, then the base commit (agentrelay.com#155).
expect(calls.filter(call => call.startsWith('check-repair'))).toHaveLength(1);
// Two repair rounds, the second only because the re-check still fails,
// then the base commit (agentrelay.com#155).
expect(calls.filter(call => call.startsWith('check-repair'))).toEqual(['check-repair:claude', 'check-repair-2:claude']);
expect(calls.find(call => call.endsWith(FLOW_BASE_CHECK_COMMAND))).toBe('base=abc123; ' + FLOW_BASE_CHECK_COMMAND);
expect(reportCall(calls)).toMatch(/^check=fail; baseline=fail; /);
expect(createCall(calls)).toContain('--draft');
Expand Down
Loading
Loading