@agentled/cli
Version:
CLI for Agentled — manage workflows, apps, and knowledge from the command line. Zero context-window cost for AI agents.
155 lines • 7.13 kB
JavaScript
/**
* `agentled fixture capture <executionId> --wf <workflowId>` — save all step
* outputs from an execution as local JSON fixtures for zero-credit replay.
* `agentled fixture list` — list all captured fixture sets.
*
* Auto-updates tests/<workflowId>.test.json so the assertions reference the
* captured execution as their fixture source.
*/
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { AgentledClient } from '../client.js';
import { printOutput, printError } from '../utils/output.js';
import { findWorkspaceDir } from '../utils/workspace-folder.js';
export function registerFixtureCommands(program) {
const fixture = program
.command('fixture')
.description('Manage captured step outputs for zero-credit test replay.');
fixture
.command('capture <executionId>')
.description('Save all step outputs from an execution into fixtures/step-outputs/<executionId>/.')
.requiredOption('--wf <workflowId>', 'Workflow ID for the execution')
.option('--force', 'Overwrite existing fixture files', false)
.option('--format <fmt>', 'Output format', 'json')
.action(async (executionId, opts) => {
try {
const client = new AgentledClient();
const wfId = opts.wf;
// Load execution (used to confirm workflowId for the test-file update)
const execution = await client.getExecution(wfId, executionId);
// Page through timelines in ascending order so dedup-by-stepId Map keeps newest
const allTimelines = [];
let nextToken;
let page = 0;
do {
const result = await client.listTimelines(wfId, executionId, {
limit: 50,
nextToken,
direction: 'asc',
});
allTimelines.push(...(result.items ?? result.timelines ?? []));
nextToken = result.nextToken;
page++;
if (page > 20)
break; // safety cap
} while (nextToken);
// Dedup by stepId (asc order means later set() = newer record)
const byStep = new Map();
for (const tl of allTimelines) {
const stepId = (tl.stepId ?? tl.step_id ?? String(tl.id ?? ''));
if (stepId)
byStep.set(stepId, tl);
}
const stepOutputs = {};
const failures = [];
for (const [stepId, tl] of byStep.entries()) {
try {
const full = await client.getTimeline(wfId, executionId, String(tl.id ?? ''));
stepOutputs[stepId] = {
stepId,
timelineId: tl.id,
status: full.status ?? tl.status,
capturedAt: new Date().toISOString(),
output: full.output ?? full.stepOutput ?? full.result ?? null,
};
}
catch (err) {
failures.push(`${stepId}: ${err instanceof Error ? err.message : String(err)}`);
}
}
const wsDir = findWorkspaceDir();
const outDir = wsDir
? join(wsDir, 'fixtures', 'step-outputs', executionId)
: resolve('fixtures', 'step-outputs', executionId);
mkdirSync(outDir, { recursive: true });
let saved = 0;
const skipped = [];
for (const [stepId, data] of Object.entries(stepOutputs)) {
const filePath = join(outDir, `${stepId}.json`);
if (existsSync(filePath) && !opts.force) {
skipped.push(stepId);
continue;
}
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
saved++;
}
// Auto-update test file fixture references
let testFileUpdated = null;
if (wsDir) {
const wfIdFromExec = String(execution.workflowId ?? execution.workflow_id ?? wfId);
const testPath = join(wsDir, 'tests', `${wfIdFromExec}.test.json`);
if (existsSync(testPath)) {
try {
const testFile = JSON.parse(readFileSync(testPath, 'utf-8'));
if (!testFile.fixtureExecutionId) {
testFile.fixtureExecutionId = executionId;
const testSteps = (testFile.steps ?? []);
for (const ts of testSteps) {
const sid = String(ts.stepId ?? '');
if (!ts.fixtureSource && stepOutputs[sid]) {
ts.fixtureSource = `${executionId}/${sid}`;
}
}
writeFileSync(testPath, JSON.stringify(testFile, null, 2) + '\n');
testFileUpdated = testPath;
}
}
catch { /* malformed test file — skip */ }
}
}
printOutput({
executionId,
workflowId: wfId,
outDir,
saved,
skipped,
failures,
testFileUpdated,
steps: Object.entries(stepOutputs).map(([stepId, data]) => ({
stepId,
status: data.status ?? 'unknown',
})),
}, (opts.format ?? 'json'));
}
catch (e) {
printError(e instanceof Error ? e.message : String(e));
}
});
fixture
.command('list')
.description('List all captured fixture sets in the local workspace.')
.option('--format <fmt>', 'Output format', 'json')
.action((opts) => {
try {
const wsDir = findWorkspaceDir();
const fixtureRoot = wsDir
? join(wsDir, 'fixtures', 'step-outputs')
: resolve('fixtures', 'step-outputs');
if (!existsSync(fixtureRoot)) {
printOutput({ fixtures: [], hint: 'Run `agentled fixture capture <executionId> --wf <workflowId>` after a test run.' }, (opts.format ?? 'json'));
return;
}
const fixtures = readdirSync(fixtureRoot, { withFileTypes: true })
.filter((e) => e.isDirectory())
.map((e) => {
const files = readdirSync(join(fixtureRoot, e.name)).filter((f) => f.endsWith('.json'));
return { executionId: e.name, steps: files.map((f) => f.replace('.json', '')) };
});
printOutput({ fixtures }, (opts.format ?? 'json'));
}
catch (e) {
printError(e instanceof Error ? e.message : String(e));
}
});
}
//# sourceMappingURL=fixture.js.map