#!/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); });