test-workflow-rerun-route-state.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. #!/usr/bin/env node
  2. import assert from 'assert';
  3. import express from 'express';
  4. import fs from 'fs';
  5. import os from 'os';
  6. import path from 'path';
  7. import { setupWorkflowRoutes } from './src/server/routes/workflow.js';
  8. import { ToolRegistry } from './src/core/tool-registry.js';
  9. import { createReadFileTool } from './src/tools/read-file.js';
  10. function createTempProject() {
  11. const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vlcode-rerun-route-'));
  12. fs.mkdirSync(path.join(dir, '.vl-code', 'workflows'), { recursive: true });
  13. fs.mkdirSync(path.join(dir, 'Process'), { recursive: true });
  14. fs.writeFileSync(path.join(dir, '.vl-code', 'workspace.json'), '{}');
  15. fs.writeFileSync(path.join(dir, 'VL.md'), '# RouteStateTest\n// VL_VERSION:3.5\n', 'utf8');
  16. return dir;
  17. }
  18. async function readSSE(response, onEvent) {
  19. assert.equal(response.status, 200);
  20. const reader = response.body.getReader();
  21. const decoder = new TextDecoder();
  22. let buffer = '';
  23. while (true) {
  24. const { value, done } = await reader.read();
  25. if (done) break;
  26. buffer += decoder.decode(value, { stream: true });
  27. while (buffer.includes('\n\n')) {
  28. const idx = buffer.indexOf('\n\n');
  29. const raw = buffer.slice(0, idx);
  30. buffer = buffer.slice(idx + 2);
  31. if (!raw.trim()) continue;
  32. const lines = raw.split('\n');
  33. const event = (lines.find((line) => line.startsWith('event: ')) || '').slice(7).trim();
  34. const dataLine = lines.find((line) => line.startsWith('data: '));
  35. const data = dataLine ? JSON.parse(dataLine.slice(6)) : {};
  36. await onEvent(event, data);
  37. }
  38. }
  39. }
  40. async function main() {
  41. const workDir = createTempProject();
  42. const workflowName = 'pause-tool-smoke';
  43. const workflowPath = path.join(workDir, '.vl-code', 'workflows', `${workflowName}.json`);
  44. fs.writeFileSync(workflowPath, JSON.stringify({
  45. version: '3.16',
  46. name: 'PauseToolSmoke',
  47. steps: [
  48. { id: 'Set_010_Seed', target: '$msg', value: '="start"', next: 'Tool_020_ReadFile' },
  49. { id: 'Tool_020_ReadFile', input: { file_path: '="VL.md"' }, out: { '$file': '=_result' }, next: 'Pause_030_Approve' },
  50. { id: 'Pause_030_Approve', resumeResultTarget: '$approved', next: 'Branch_040_Check' },
  51. { id: 'Branch_040_Check', cases: [{ condition: '=$approved === false', next: 'Stop_050_Cancel' }], default: 'Set_060_Result' },
  52. { id: 'Set_060_Result', target: '$final', value: '=$approved ? "approved" : "rejected"', next: 'Write_070_Output' },
  53. { id: 'Write_070_Output', target: '="Process/pause-tool-smoke.txt"', value: '=$final + "|" + $file.slice(0, 10)', next: 'Stop_080_End' },
  54. { id: 'Stop_050_Cancel' },
  55. { id: 'Stop_080_End' },
  56. ],
  57. }, null, 2), 'utf8');
  58. const toolRegistry = new ToolRegistry();
  59. toolRegistry.register('ReadFile', createReadFileTool({ workDir }));
  60. const fakeServer = {
  61. config: {
  62. workDir,
  63. model: 'claude-opus-4-6',
  64. toolRegistry,
  65. },
  66. symbolIndex: null,
  67. _activeExecutor: null,
  68. _activeExecutors: new Map(),
  69. _currentRunState: null,
  70. _uiState: null,
  71. broadcast() {},
  72. autoExtractMeta() {},
  73. projectContext: {
  74. async scan() {},
  75. },
  76. };
  77. const app = express();
  78. app.use(express.json({ limit: '10mb' }));
  79. setupWorkflowRoutes(app, fakeServer);
  80. const httpServer = app.listen(0);
  81. await new Promise((resolve, reject) => {
  82. httpServer.once('listening', resolve);
  83. httpServer.once('error', reject);
  84. });
  85. const { port } = httpServer.address();
  86. const base = `http://127.0.0.1:${port}`;
  87. try {
  88. let runID = null;
  89. let activeState = null;
  90. const clientRunToken = 'route-state-1';
  91. const executeEvents = [];
  92. const executeResponse = await fetch(`${base}/api/workflow/execute`, {
  93. method: 'POST',
  94. headers: { 'Content-Type': 'application/json' },
  95. body: JSON.stringify({ workflowName, clientRunToken }),
  96. });
  97. await readSSE(executeResponse, async (event, data) => {
  98. executeEvents.push(event);
  99. if (data.runID && !runID) runID = data.runID;
  100. if (event === 'pause') {
  101. const activeStateResponse = await fetch(`${base}/api/workflow/current-state`);
  102. activeState = await activeStateResponse.json();
  103. const resumeResponse = await fetch(`${base}/api/workflow/resume`, {
  104. method: 'POST',
  105. headers: { 'Content-Type': 'application/json' },
  106. body: JSON.stringify({ runID, nodeId: data.nodeId, payload: true }),
  107. });
  108. const resumeBody = await resumeResponse.json();
  109. assert.equal(resumeBody.ok, true);
  110. }
  111. });
  112. assert(runID, 'runID missing from execute stream');
  113. assert(executeEvents.includes('tool_start'), 'missing tool_start event');
  114. assert(executeEvents.includes('tool_done'), 'missing tool_done event');
  115. assert(executeEvents.includes('checkpoint'), 'missing checkpoint event');
  116. assert(executeEvents.includes('pause'), 'missing pause event');
  117. assert(executeEvents.includes('resumed'), 'missing resumed event');
  118. assert(executeEvents.includes('done'), 'missing done event');
  119. assert.equal(activeState.active, true);
  120. assert.equal(activeState.workflowName, workflowName);
  121. assert.equal(activeState.runID, runID);
  122. assert.equal(activeState.clientRunToken, clientRunToken);
  123. assert.equal(activeState.checkpoint?.currentStepID, 'Tool_020_ReadFile');
  124. const checkpointResponse = await fetch(`${base}/api/workflow/${runID}/checkpoint`);
  125. const checkpoint = await checkpointResponse.json();
  126. assert.equal(checkpoint.status, 'completed');
  127. const currentStateResponse = await fetch(`${base}/api/workflow/current-state`);
  128. const currentState = await currentStateResponse.json();
  129. assert.equal(currentState.active, false);
  130. assert.equal(currentState.workflowName, null);
  131. assert.equal(currentState.nodeStatuses.Stop_080_End, 'done');
  132. const outputPath = path.join(workDir, 'Process', 'pause-tool-smoke.txt');
  133. assert(fs.readFileSync(outputPath, 'utf8').startsWith('approved|'));
  134. const rerunEvents = [];
  135. const rerunResponse = await fetch(`${base}/api/workflow/rerun`, {
  136. method: 'POST',
  137. headers: { 'Content-Type': 'application/json' },
  138. body: JSON.stringify({
  139. workflowName,
  140. checkpoint,
  141. stepID: 'Write_070_Output',
  142. overrides: { '$final': 'rerun' },
  143. }),
  144. });
  145. await readSSE(rerunResponse, async (event, _data) => {
  146. rerunEvents.push(event);
  147. });
  148. assert(rerunEvents.includes('done'), 'missing rerun done event');
  149. const rerunCheckpointResponse = await fetch(`${base}/api/workflow/${runID}/checkpoint`);
  150. const rerunCheckpoint = await rerunCheckpointResponse.json();
  151. assert.equal(rerunCheckpoint.status, 'completed');
  152. const rerunStateResponse = await fetch(`${base}/api/workflow/current-state`);
  153. const rerunState = await rerunStateResponse.json();
  154. assert.equal(rerunState.active, false);
  155. assert.equal(rerunState.workflowName, null);
  156. assert.equal(rerunState.nodeStatuses.Write_070_Output, 'done');
  157. assert.equal(rerunState.nodeStatuses.Stop_080_End, 'done');
  158. assert(fs.readFileSync(outputPath, 'utf8').startsWith('rerun|'));
  159. } finally {
  160. await new Promise((resolve) => httpServer.close(resolve));
  161. fs.rmSync(workDir, { recursive: true, force: true });
  162. }
  163. console.log('\n-- Workflow Rerun Route State --');
  164. console.log('PASS test-workflow-rerun-route-state.js');
  165. }
  166. main().catch((err) => {
  167. console.error(err);
  168. process.exit(1);
  169. });