Skip to content
Merged
Show file tree
Hide file tree
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
6 changes: 0 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@ pnpm build # Compile to dist/
| `run_graph_workflow` | Execute graph-based workflows |
| `query_trace` | Query execution traces |

## Live integration mode

```bash
NEXUS_LIVE=true npx tsx src/run-live.ts
```

## License

MIT
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,10 @@
"scripts": {
"build": "tsc",
"test": "vitest run",
"typecheck": "tsc --noEmit",
"live": "NEXUS_LIVE=true tsx src/run-live.ts"
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@types/node": "^22.19.21",
"tsx": "^4.19.0",
"typescript": "^5.9.0",
"vitest": "^3.2.6",
"zod": "^3.24.0"
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 0 additions & 51 deletions src/fixtures/mock-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import type {
RunGraphResponse,
GraphWorkflowInfo,
QueryTraceResponse,
RunWorkflowResponse,
RunWorkflowDryRun,
} from '../types.js';

// ============================================================================
Expand Down Expand Up @@ -205,52 +203,3 @@ export const MOCK_TRACE_NOT_FOUND: QueryTraceResponse = {
truncated: false,
source: 'not_found',
};

// ============================================================================
// run_workflow
// ============================================================================

export const MOCK_RUN_WORKFLOW_SUCCESS: RunWorkflowResponse = {
executionId: 'exec-2026-04-17-0001',
workflowName: 'code-review',
status: 'completed',
stepResults: [
{ stepId: 'analyze', status: 'success', durationMs: 1250 },
{ stepId: 'report', status: 'success', durationMs: 340 },
],
output: { summary: 'Review completed: 3 findings.' },
durationMs: 1600,
};

export const MOCK_RUN_WORKFLOW_FAILED: RunWorkflowResponse = {
executionId: 'exec-2026-04-17-0002',
workflowName: 'bug-fix',
status: 'failed',
stepResults: [
{ stepId: 'reproduce', status: 'success', durationMs: 100 },
{ stepId: 'patch', status: 'failed', durationMs: 50, error: 'Adapter unavailable' },
{ stepId: 'verify', status: 'skipped', durationMs: 0 },
],
output: null,
durationMs: 155,
};

export const MOCK_RUN_WORKFLOW_DRY_RUN: RunWorkflowDryRun = {
valid: true,
workflowName: 'documentation-update',
stepCount: 4,
inputsProvided: ['file', 'section'],
inputsRequired: ['file', 'section'],
inputsMissing: [],
validationErrors: [],
};

export const MOCK_RUN_WORKFLOW_DRY_RUN_INVALID: RunWorkflowDryRun = {
valid: false,
workflowName: 'documentation-update',
stepCount: 4,
inputsProvided: ['file'],
inputsRequired: ['file', 'section'],
inputsMissing: ['section'],
validationErrors: ["Missing required input 'section'"],
};
8 changes: 1 addition & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export {
toErrorResult,
countResults,
runWorkflowPipeline,
ToolCallTimeoutError,
} from './runner-pipeline.js';
export { generateReport } from './reporter.js';
export type { ReportFormat } from './reporter.js';
Expand All @@ -30,10 +31,6 @@ export type {
WorkflowRunResult,
RunnerReport,
RunnerConfig,
RunWorkflowInput,
RunWorkflowResponse,
RunWorkflowDryRun,
StepResultSummary,
} from './types.js';
export {
ListWorkflowsInputSchema,
Expand All @@ -43,7 +40,4 @@ export {
GraphWorkflowListSchema,
QueryTraceInputSchema,
QueryTraceResponseSchema,
RunWorkflowInputSchema,
RunWorkflowResponseSchema,
RunWorkflowDryRunSchema,
} from './types.js';
15 changes: 0 additions & 15 deletions src/live-caller.ts

This file was deleted.

38 changes: 0 additions & 38 deletions src/run-live.ts

This file was deleted.

155 changes: 155 additions & 0 deletions src/runner-pipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
toErrorResult,
countResults,
runWorkflowPipeline,
ToolCallTimeoutError,
} from './runner-pipeline.js';
import type { RunnerConfig, WorkflowRunResult } from './types.js';
import {
Expand Down Expand Up @@ -347,4 +348,158 @@ describe('runWorkflowPipeline', () => {
expect(result.graphResults[0]!.status).toBe('error');
expect(result.failed).toBe(1);
});

// --- #15: preserve the real failure cause instead of "Execution failed" ---

it('preserves the real error message on a failed graph execution', async () => {
let graphIdx = 0;
const caller: ToolCaller = {
call: vi.fn(async (toolName: string) => {
if (toolName === 'list_workflows') return MOCK_LIST_WORKFLOWS;
if (toolName === 'run_graph_workflow') {
if (graphIdx++ === 0) return MOCK_GRAPH_LIST.slice(0, 1);
throw new Error('ECONNREFUSED 127.0.0.1:9000');
}
throw new Error(`Unexpected: ${toolName}`);
}),
};

const result = await runWorkflowPipeline(caller);

expect(result.graphResults[0]!.error).toBe('ECONNREFUSED 127.0.0.1:9000');
expect(result.graphResults[0]!.error).not.toBe('Execution failed');
});

it('preserves a Zod schema-mismatch error from a bad server response', async () => {
let graphIdx = 0;
const caller: ToolCaller = {
call: vi.fn(async (toolName: string) => {
if (toolName === 'list_workflows') return MOCK_LIST_WORKFLOWS;
if (toolName === 'run_graph_workflow') {
if (graphIdx++ === 0) return MOCK_GRAPH_LIST.slice(0, 1);
// Unexpected shape — executeGraph's Zod parse should reject this.
return { workflow: 'echo', status: 'unknown-status' };
}
throw new Error(`Unexpected: ${toolName}`);
}),
};

const result = await runWorkflowPipeline(caller);

expect(result.graphResults[0]!.status).toBe('error');
// The Zod failure carries actionable detail, not a generic string.
expect(result.graphResults[0]!.error).not.toBe('Execution failed');
expect(result.graphResults[0]!.error!.length).toBeGreaterThan(0);
});

it('distinguishes a failed trace query from "trace not requested"', async () => {
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
try {
let graphIdx = 0;
const caller: ToolCaller = {
call: vi.fn(async (toolName: string) => {
if (toolName === 'list_workflows') return MOCK_LIST_WORKFLOWS;
if (toolName === 'run_graph_workflow') {
return graphIdx++ === 0 ? MOCK_GRAPH_LIST.slice(0, 1) : MOCK_ECHO_RESULT;
}
if (toolName === 'query_trace') throw new Error('trace store offline');
throw new Error(`Unexpected: ${toolName}`);
}),
};

const result = await runWorkflowPipeline(caller, { traceRunId: 'wf-run-001' });

expect(result.traceResult).toBeNull();
expect(result.traceError).toBe('trace store offline');
} finally {
errSpy.mockRestore();
}
});

it('leaves traceError undefined when no trace is requested', async () => {
let graphIdx = 0;
const caller: ToolCaller = {
call: vi.fn(async (toolName: string) => {
if (toolName === 'list_workflows') return MOCK_LIST_WORKFLOWS;
if (toolName === 'run_graph_workflow') {
return graphIdx++ === 0 ? MOCK_GRAPH_LIST.slice(0, 1) : MOCK_ECHO_RESULT;
}
throw new Error(`Unexpected: ${toolName}`);
}),
};

const result = await runWorkflowPipeline(caller);

expect(result.traceResult).toBeNull();
expect(result.traceError).toBeUndefined();
});
});

// ============================================================================
// #16: per-call timeout / abort
// ============================================================================

describe('timeout enforcement', () => {
it('aborts a hung graph execution and reports it without hanging', async () => {
let graphIdx = 0;
const caller: ToolCaller = {
call: vi.fn((toolName: string, args: Record<string, unknown>) => {
if (toolName === 'list_workflows') return Promise.resolve(MOCK_LIST_WORKFLOWS);
if (toolName === 'run_graph_workflow') {
if (graphIdx++ === 0) return Promise.resolve(MOCK_GRAPH_LIST.slice(0, 1));
// Simulate a hung server: never resolves on its own, but honors abort.
return new Promise((_, reject) => {
const signal = args['signal'] as AbortSignal | undefined;
signal?.addEventListener('abort', () =>
reject(new Error('aborted by signal'))
);
});
}
return Promise.reject(new Error(`Unexpected: ${toolName}`));
}),
};

const result = await runWorkflowPipeline(caller, { timeoutMs: 20 });

expect(result.graphResults.length).toBe(1);
expect(result.graphResults[0]!.status).toBe('error');
expect(result.graphResults[0]!.error).toMatch(/timed out after 20ms/);
expect(result.failed).toBe(1);
});

it('passes an AbortSignal through to the caller', async () => {
const caller: ToolCaller = {
call: vi.fn(async () => MOCK_LIST_WORKFLOWS),
};

await listTemplates(caller, 1000);

const callMock = caller.call as ReturnType<typeof vi.fn>;
const passedArgs = callMock.mock.calls[0]![1] as Record<string, unknown>;
expect(passedArgs['signal']).toBeInstanceOf(AbortSignal);
});

it('does not attach a signal or deadline when timeoutMs is omitted', async () => {
const caller: ToolCaller = {
call: vi.fn(async () => MOCK_LIST_WORKFLOWS),
};

await listTemplates(caller);

const callMock = caller.call as ReturnType<typeof vi.fn>;
const passedArgs = callMock.mock.calls[0]![1] as Record<string, unknown>;
expect(passedArgs['signal']).toBeUndefined();
});

it('throws ToolCallTimeoutError from a step when the call exceeds the budget', async () => {
const caller: ToolCaller = {
call: vi.fn(
() => new Promise(() => {}) // never resolves
),
};

await expect(listTemplates(caller, 15)).rejects.toBeInstanceOf(
ToolCallTimeoutError
);
});
});
Loading
Loading