| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- #!/usr/bin/env node
- import assert from 'assert';
- import express from 'express';
- import fs from 'fs';
- import os from 'os';
- import path from 'path';
- import { setupWorkflowRoutes } from './src/server/routes/workflow.js';
- import { ToolRegistry } from './src/core/tool-registry.js';
- import { createReadFileTool } from './src/tools/read-file.js';
- function createTempProject() {
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vlcode-rerun-route-'));
- fs.mkdirSync(path.join(dir, '.vl-code', 'workflows'), { recursive: true });
- fs.mkdirSync(path.join(dir, 'Process'), { recursive: true });
- fs.writeFileSync(path.join(dir, '.vl-code', 'workspace.json'), '{}');
- fs.writeFileSync(path.join(dir, 'VL.md'), '# RouteStateTest\n// VL_VERSION:3.5\n', 'utf8');
- return dir;
- }
- async function readSSE(response, onEvent) {
- assert.equal(response.status, 200);
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let buffer = '';
- while (true) {
- const { value, done } = await reader.read();
- if (done) break;
- buffer += decoder.decode(value, { stream: true });
- while (buffer.includes('\n\n')) {
- const idx = buffer.indexOf('\n\n');
- const raw = buffer.slice(0, idx);
- buffer = buffer.slice(idx + 2);
- if (!raw.trim()) continue;
- const lines = raw.split('\n');
- const event = (lines.find((line) => line.startsWith('event: ')) || '').slice(7).trim();
- const dataLine = lines.find((line) => line.startsWith('data: '));
- const data = dataLine ? JSON.parse(dataLine.slice(6)) : {};
- await onEvent(event, data);
- }
- }
- }
- async function main() {
- const workDir = createTempProject();
- const workflowName = 'pause-tool-smoke';
- const workflowPath = path.join(workDir, '.vl-code', 'workflows', `${workflowName}.json`);
- fs.writeFileSync(workflowPath, JSON.stringify({
- version: '3.16',
- name: 'PauseToolSmoke',
- steps: [
- { id: 'Set_010_Seed', target: '$msg', value: '="start"', next: 'Tool_020_ReadFile' },
- { id: 'Tool_020_ReadFile', input: { file_path: '="VL.md"' }, out: { '$file': '=_result' }, next: 'Pause_030_Approve' },
- { id: 'Pause_030_Approve', resumeResultTarget: '$approved', next: 'Branch_040_Check' },
- { id: 'Branch_040_Check', cases: [{ condition: '=$approved === false', next: 'Stop_050_Cancel' }], default: 'Set_060_Result' },
- { id: 'Set_060_Result', target: '$final', value: '=$approved ? "approved" : "rejected"', next: 'Write_070_Output' },
- { id: 'Write_070_Output', target: '="Process/pause-tool-smoke.txt"', value: '=$final + "|" + $file.slice(0, 10)', next: 'Stop_080_End' },
- { id: 'Stop_050_Cancel' },
- { id: 'Stop_080_End' },
- ],
- }, null, 2), 'utf8');
- const toolRegistry = new ToolRegistry();
- toolRegistry.register('ReadFile', createReadFileTool({ workDir }));
- const fakeServer = {
- config: {
- workDir,
- model: 'claude-opus-4-6',
- toolRegistry,
- },
- symbolIndex: null,
- _activeExecutor: null,
- _activeExecutors: new Map(),
- _currentRunState: null,
- _uiState: null,
- broadcast() {},
- autoExtractMeta() {},
- projectContext: {
- async scan() {},
- },
- };
- const app = express();
- app.use(express.json({ limit: '10mb' }));
- setupWorkflowRoutes(app, fakeServer);
- const httpServer = app.listen(0);
- await new Promise((resolve, reject) => {
- httpServer.once('listening', resolve);
- httpServer.once('error', reject);
- });
- const { port } = httpServer.address();
- const base = `http://127.0.0.1:${port}`;
- try {
- let runID = null;
- let activeState = null;
- const clientRunToken = 'route-state-1';
- const executeEvents = [];
- const executeResponse = await fetch(`${base}/api/workflow/execute`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ workflowName, clientRunToken }),
- });
- await readSSE(executeResponse, async (event, data) => {
- executeEvents.push(event);
- if (data.runID && !runID) runID = data.runID;
- if (event === 'pause') {
- const activeStateResponse = await fetch(`${base}/api/workflow/current-state`);
- activeState = await activeStateResponse.json();
- const resumeResponse = await fetch(`${base}/api/workflow/resume`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ runID, nodeId: data.nodeId, payload: true }),
- });
- const resumeBody = await resumeResponse.json();
- assert.equal(resumeBody.ok, true);
- }
- });
- assert(runID, 'runID missing from execute stream');
- assert(executeEvents.includes('tool_start'), 'missing tool_start event');
- assert(executeEvents.includes('tool_done'), 'missing tool_done event');
- assert(executeEvents.includes('checkpoint'), 'missing checkpoint event');
- assert(executeEvents.includes('pause'), 'missing pause event');
- assert(executeEvents.includes('resumed'), 'missing resumed event');
- assert(executeEvents.includes('done'), 'missing done event');
- assert.equal(activeState.active, true);
- assert.equal(activeState.workflowName, workflowName);
- assert.equal(activeState.runID, runID);
- assert.equal(activeState.clientRunToken, clientRunToken);
- assert.equal(activeState.checkpoint?.currentStepID, 'Tool_020_ReadFile');
- const checkpointResponse = await fetch(`${base}/api/workflow/${runID}/checkpoint`);
- const checkpoint = await checkpointResponse.json();
- assert.equal(checkpoint.status, 'completed');
- const currentStateResponse = await fetch(`${base}/api/workflow/current-state`);
- const currentState = await currentStateResponse.json();
- assert.equal(currentState.active, false);
- assert.equal(currentState.workflowName, null);
- assert.equal(currentState.nodeStatuses.Stop_080_End, 'done');
- const outputPath = path.join(workDir, 'Process', 'pause-tool-smoke.txt');
- assert(fs.readFileSync(outputPath, 'utf8').startsWith('approved|'));
- const rerunEvents = [];
- const rerunResponse = await fetch(`${base}/api/workflow/rerun`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- workflowName,
- checkpoint,
- stepID: 'Write_070_Output',
- overrides: { '$final': 'rerun' },
- }),
- });
- await readSSE(rerunResponse, async (event, _data) => {
- rerunEvents.push(event);
- });
- assert(rerunEvents.includes('done'), 'missing rerun done event');
- const rerunCheckpointResponse = await fetch(`${base}/api/workflow/${runID}/checkpoint`);
- const rerunCheckpoint = await rerunCheckpointResponse.json();
- assert.equal(rerunCheckpoint.status, 'completed');
- const rerunStateResponse = await fetch(`${base}/api/workflow/current-state`);
- const rerunState = await rerunStateResponse.json();
- assert.equal(rerunState.active, false);
- assert.equal(rerunState.workflowName, null);
- assert.equal(rerunState.nodeStatuses.Write_070_Output, 'done');
- assert.equal(rerunState.nodeStatuses.Stop_080_End, 'done');
- assert(fs.readFileSync(outputPath, 'utf8').startsWith('rerun|'));
- } finally {
- await new Promise((resolve) => httpServer.close(resolve));
- fs.rmSync(workDir, { recursive: true, force: true });
- }
- console.log('\n-- Workflow Rerun Route State --');
- console.log('PASS test-workflow-rerun-route-state.js');
- }
- main().catch((err) => {
- console.error(err);
- process.exit(1);
- });
|