import fs from 'fs';
import path from 'path';

const html = fs.readFileSync(path.join(process.cwd(), 'public/workflow-editor.html'), 'utf8');
const scriptMatch = html.match(/<script>([\s\S]*)<\/script>/);
if (!scriptMatch) {
  console.error('Cannot extract <script> from public/workflow-editor.html');
  process.exit(1);
}
const jsCode = scriptMatch[1];

const elements = {};
function createEl(id) {
  const classSet = new Set();
  const listeners = new Map();
  const el = {
    id,
    textContent: '',
    value: '',
    children: [],
    dataset: {},
    style: { display: '', left: '', top: '' },
    className: '',
    classList: {
      add(...names) { names.forEach((name) => classSet.add(name)); },
      remove(...names) { names.forEach((name) => classSet.delete(name)); },
      contains(name) { return classSet.has(name); },
      toggle(name, force) {
        if (force === undefined) {
          if (classSet.has(name)) classSet.delete(name);
          else classSet.add(name);
          return classSet.has(name);
        }
        if (force) classSet.add(name);
        else classSet.delete(name);
        return !!force;
      },
    },
    offsetLeft: 100,
    offsetTop: 100,
    offsetWidth: 240,
    offsetHeight: 120,
    clientWidth: 800,
    clientHeight: 600,
    scrollLeft: 0,
    scrollTop: 0,
    appendChild(child) { this.children.push(child); return child; },
    querySelectorAll() { return []; },
    addEventListener(type, handler) {
      if (!listeners.has(type)) listeners.set(type, []);
      listeners.get(type).push(handler);
    },
    setAttribute() {},
    click() {},
    scrollIntoView() {},
    closest() { return null; },
    remove() {},
    getContext() {
      return {
        clearRect() {}, beginPath() {}, moveTo() {}, lineTo() {}, stroke() {},
        fillRect() {}, fill() {}, fillText() {}, arc() {}, bezierCurveTo() {},
        quadraticCurveTo() {}, closePath() {}, setLineDash() {}, strokeRect() {},
        scale() {},
        set globalAlpha(v) {},
        get globalAlpha() { return 1; },
        set fillStyle(v) {},
        get fillStyle() { return ''; },
        set strokeStyle(v) {},
        get strokeStyle() { return ''; },
        set lineWidth(v) {},
        set font(v) {},
        set textAlign(v) {},
      };
    },
    get files() { return []; },
  };
  let innerHTML = '';
  Object.defineProperty(el, 'innerHTML', {
    get() { return innerHTML; },
    set(value) {
      innerHTML = value;
      if (value === '') this.children = [];
    },
  });
  el.dispatch = async (type, event = {}) => {
    const handlers = listeners.get(type) || [];
    for (const handler of handlers) {
      await handler({
        preventDefault() {},
        stopPropagation() {},
        button: 0,
        ...event,
      });
    }
  };
  return el;
}

function $(id) {
  if (!elements[id]) elements[id] = createEl(id);
  return elements[id];
}

global.document = {
  getElementById: (id) => $(id),
  createElement: (tag) => createEl(tag),
  createElementNS: (ns, tag) => createEl(tag),
  addEventListener() {},
  body: createEl('body'),
};
global.window = {
  parent: { postMessage() {} },
  addEventListener() {},
  innerWidth: 1200,
  innerHeight: 800,
  location: { search: '' },
};
Object.defineProperty(globalThis, 'navigator', {
  configurable: true,
  value: { clipboard: { writeText() {} } },
});
global.localStorage = {
  _store: {},
  getItem(k) { return this._store[k] || null; },
  setItem(k, v) { this._store[k] = v; },
  removeItem(k) { delete this._store[k]; },
};
global.URL = { createObjectURL() { return 'blob:test'; } };
global.Blob = class Blob { constructor() {} };
global.requestAnimationFrame = (fn) => fn();
global.setTimeout = (fn) => fn();
global.fetch = async (url) => {
  if (url === '/api/workflow/ephemeral') {
    return { json: async () => ({ ok: true, name: '_ephemeral-test' }) };
  }
  if (String(url).startsWith('/api/workflow-content?')) {
    return {
      ok: true,
      json: async () => ({
        workflowRef: 'child-demo',
        title: 'Child Demo Workflow',
        workflow: {
          name: 'Child Demo Workflow',
          version: '3.16',
          steps: [{ id: 'Stop_End' }],
        },
      }),
    };
  }
  return {
    ok: true,
    json: async () => ({}),
    body: {
      getReader() {
        return { read: async () => ({ done: true }) };
      },
    },
  };
};

const bootEditor = new Function('global', `
  ${jsCode}
  global._editor = {
    parseWorkflow,
    getStepType,
    streamSSE,
    handleExecEvent,
    ensureWorkflowRef,
    runWorkflow,
    rerunFromStep,
    openSubflowNode,
    navigateBack,
    loadWorkflowFromLocation,
    toggleCompactMode,
    toggleSimplifyGraph,
    toggleTypeFilter,
    toggleEventPanel,
    getTypeIcon,
    selectRunSession,
    getState: () => state,
    getRenderedNodeHtml: (nodeId) => {
      const layer = $('nodesLayer');
      const node = (layer.children || []).find((child) => child.id === \`node-\${nodeId}\`);
      return node?.innerHTML || '';
    },
    getCheckpoint: () => _lastCheckpoint,
    getRunID: () => _currentRunID,
    getStatus: () => $('statusLabel').textContent,
    getSelectedEvents: () => (getSelectedRunSession()?.events || []).map((event) => ({
      type: event.type,
      summary: event.summary,
      detail: event.detail,
      level: event.level,
    })),
    getRunSessions: () => Array.from(_runSessions.values()).map((session) => ({
      sessionID: session.sessionID,
      clientRunToken: session.clientRunToken,
      runID: session.runID,
      label: session.label,
      status: session.status,
      currentStepID: session.currentStepID,
      eventCount: Array.isArray(session.events) ? session.events.length : 0,
    })),
    getBreadcrumb: () => $('wfBreadcrumb').textContent,
    getTypeSidebarHtml: () => $('typeList').innerHTML,
    getTypeSummaryHtml: () => $('typeSummary').innerHTML,
    getTypePanelTitle: () => $('typePanelTitle').textContent,
    getRenderedNodeIds: () => (($('nodesLayer').children || []).map((child) => child.id)),
    getCanvasSize: () => ({ width: $('canvas').style.width, height: $('canvas').style.height }),
    getNodeCoords: (nodeId) => {
      const node = state.nodes.find((entry) => entry.id === nodeId);
      return node ? { x: node.x, y: node.y } : null;
    },
    triggerNodeEvent: async (nodeId, type, event = {}) => {
      const layer = $('nodesLayer');
      const node = (layer.children || []).find((child) => child.id === \`node-\${nodeId}\`);
      if (!node?.dispatch) throw new Error('node element missing');
      await node.dispatch(type, event);
    },
    selectNode: (nodeId) => {
      state.selectedNodeId = nodeId;
      renderNodes();
      renderConnections();
      updateNavigationChrome();
    },
    deselectNode: () => {
      deselectSelectedNode();
    },
    getSelectedNodeId: () => state.selectedNodeId,
    triggerElementEvent: async (id, type, event = {}) => {
      const el = $(id);
      if (!el?.dispatch) throw new Error('element missing');
      await el.dispatch(type, event);
    },
    bodyHasClass: (name) => document.body.classList.contains(name),
  };
`);
bootEditor(global);

const editor = global._editor;

let passed = 0;
let failed = 0;
function test(name, fn) {
  try {
    fn();
    console.log(`  ✓ ${name}`);
    passed++;
  } catch (e) {
    console.log(`  ✗ ${name}: ${e.message}`);
    failed++;
  }
}
async function testAsync(name, fn) {
  try {
    await fn();
    console.log(`  ✓ ${name}`);
    passed++;
  } catch (e) {
    console.log(`  ✗ ${name}: ${e.message}`);
    failed++;
  }
}
function assert(cond, msg) {
  if (!cond) throw new Error(msg || 'Assertion failed');
}
function assertEqual(a, b, msg) {
  if (a !== b) throw new Error(msg || `Expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}`);
}

function makeStreamResponse(chunks) {
  let idx = 0;
  return {
    body: {
      getReader() {
        return {
          async read() {
            if (idx >= chunks.length) return { done: true };
            return { done: false, value: Buffer.from(chunks[idx++], 'utf8') };
          },
        };
      },
    },
  };
}

const workflow = {
  name: 'EditorRegression',
  version: '3.16',
  steps: [
    { id: 'Set_Start', target: '$msg', value: '="hello"', next: 'MetaDiff_Compute' },
    { id: 'MetaDiff_Compute', in: { oldMeta: '{}', newMeta: '{}' }, next: 'Stop_End' },
    { id: 'Stop_End' },
  ],
};

console.log('\n── Workflow Editor Regression ──');

test('normalizes explicit Branch_ type', () => {
  assertEqual(editor.getStepType({ id: 'Branch_Check', type: 'Branch_' }), 'Branch');
});

test('preserves custom step prefix instead of forcing LLM', () => {
  assertEqual(editor.getStepType({ id: 'MetaDiff_020_DiffMeta' }), 'MetaDiff');
});

test('custom step uses readable fallback icon', () => {
  assertEqual(editor.getTypeIcon('MetaDiff'), 'ME');
});

test('Tool steps use dedicated icon', () => {
  assertEqual(editor.getTypeIcon('Tool'), 'TL');
});

test('WorkflowRun tool is promoted to Subflow display type', () => {
  assertEqual(editor.getStepType({ id: 'Tool_RunChild', tool: 'WorkflowRun' }), 'Subflow');
});

test('Subflow prefix stays first-class in the editor', () => {
  assertEqual(editor.getStepType({ id: 'Subflow_020_ExploreChild', workflow_path: 'child.json' }), 'Subflow');
  assertEqual(editor.getTypeIcon('Subflow'), 'SF');
});

test('parseWorkflow keeps custom node type for rendering', () => {
  editor.parseWorkflow(workflow, 'editor-regression');
  const node = editor.getState().nodes.find((n) => n.id === 'MetaDiff_Compute');
  assert(node, 'MetaDiff node missing');
  assertEqual(node.type, 'MetaDiff');
});

test('parseWorkflow keeps Tool node as first-class type', () => {
  editor.parseWorkflow({
    name: 'ToolWorkflow',
    version: '3.16',
    steps: [
      { id: 'Tool_UseReadFile', tool: 'ReadFile', input: { file_path: 'README.md' }, next: 'Stop_End' },
      { id: 'Stop_End' },
    ],
  }, 'tool-workflow');
  const node = editor.getState().nodes.find((n) => n.id === 'Tool_UseReadFile');
  assert(node, 'Tool node missing');
  assertEqual(node.type, 'Tool');
});

test('parseWorkflow renders subflow nodes with subflow type metadata', () => {
  editor.parseWorkflow({
    name: 'SubflowWorkflow',
    version: '3.16',
    steps: [
      { id: 'Subflow_020_ExploreChild', workflow_path: 'child.json', params: { seed: '="alpha"' }, next: 'Stop_End' },
      { id: 'Stop_End' },
    ],
  }, 'subflow-workflow');
  const node = editor.getState().nodes.find((n) => n.id === 'Subflow_020_ExploreChild');
  assert(node, 'Subflow node missing');
  assertEqual(node.type, 'Subflow');
  const html = editor.getRenderedNodeHtml('Subflow_020_ExploreChild');
  assert(html.includes('Subflow'), 'rendered html should include subflow type pill');
  assert(html.includes('Idle'), 'rendered html should include idle state pill');
  assert(editor.getTypeSidebarHtml().includes('Subflow'), 'type sidebar should include subflow entry');
  assert(editor.getTypeSummaryHtml().includes('Supported'), 'type summary should render stats');
  assert(!editor.getTypeSidebarHtml().includes('Service'), 'used-only sidebar should hide zero-count built-ins by default');
});

test('editor starts in compact embedded mode with events collapsed', () => {
  assert(editor.bodyHasClass('compact-ui'), 'body should start in compact mode');
  assert(editor.bodyHasClass('events-collapsed'), 'events should start collapsed');
});

test('dense view keeps Set/Write/Stop visible and compresses layout only', () => {
  editor.parseWorkflow(workflow, 'editor-regression');
  let rendered = editor.getRenderedNodeIds();
  assert(rendered.includes('node-MetaDiff_Compute'), 'dense view should keep structural nodes');
  assert(rendered.includes('node-Set_Start'), 'dense view should keep Set nodes');
  assert(rendered.includes('node-Stop_End'), 'dense view should keep Stop nodes');
  assertEqual(editor.getTypePanelTitle(), 'Node Types');
  assert(editor.getCanvasSize().width, 'canvas footprint should be tightened to rendered graph');
  const denseX = editor.getNodeCoords('MetaDiff_Compute')?.x;

  editor.toggleSimplifyGraph();
  rendered = editor.getRenderedNodeIds();
  const relaxedX = editor.getNodeCoords('MetaDiff_Compute')?.x;
  assert(denseX < relaxedX, 'dense view should place layers closer than relaxed view');

  editor.toggleSimplifyGraph();
});

test('selected node expands to show full inputs without truncation', () => {
  const longValue = '/Users/ivx/Documents/VLProjects/_tests/WorkflowEditorComplexDemoTest/.vl-code/workflows/cave-deep-scout-generated.json';
  editor.parseWorkflow({
    name: 'ExpandedNodeWorkflow',
    version: '3.16',
    steps: [
      {
        id: 'Tool_010_LongInput',
        tool: 'WriteFile',
        input: {
          file_path: longValue,
          content: 'x'.repeat(72),
          encoding: 'utf8',
          mode: 'overwrite',
          extraFlag: 'keep-this-visible',
        },
        next: 'Stop_End',
      },
      { id: 'Stop_End' },
    ],
  }, 'expanded-node-workflow');

  let html = editor.getRenderedNodeHtml('Tool_010_LongInput');
  assert(!html.includes('keep-this-visible'), 'collapsed node should still summarize inputs');

  editor.selectNode('Tool_010_LongInput');
  html = editor.getRenderedNodeHtml('Tool_010_LongInput');
  assert(html.includes(longValue), 'selected node should show the full file_path');
  assert(html.includes('keep-this-visible'), 'selected node should show additional input fields');
  assert(html.includes('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'), 'selected node should show the full long content');
});

await testAsync('clicking canvas blank space collapses the expanded node', async () => {
  editor.parseWorkflow(workflow, 'editor-regression');
  editor.selectNode('Set_Start');
  assertEqual(editor.getSelectedNodeId(), 'Set_Start');

  await editor.triggerElementEvent('canvasWrap', 'click', {
    target: { closest: () => null },
  });
  assertEqual(editor.getSelectedNodeId(), null);
});

await testAsync('node dismiss button collapses the expanded node', async () => {
  editor.parseWorkflow(workflow, 'editor-regression');
  editor.selectNode('Set_Start');
  assertEqual(editor.getSelectedNodeId(), 'Set_Start');

  await editor.triggerNodeEvent('Set_Start', 'click', {
    target: { closest: (selector) => (selector === '.node-dismiss' ? {} : null) },
  });
  assertEqual(editor.getSelectedNodeId(), null);
});

await testAsync('double-clicking a Subflow node opens the child workflow', async () => {
  editor.parseWorkflow({
    name: 'ParentWithChildDoubleClick',
    version: '3.16',
    steps: [
      { id: 'Subflow_010_ExploreChild', workflow_path: 'child-demo', next: 'Stop_End' },
      { id: 'Stop_End' },
    ],
  }, 'parent-with-child-dbl');

  await editor.triggerNodeEvent('Subflow_010_ExploreChild', 'dblclick');
  assertEqual($('wfTitle').textContent, 'Child Demo Workflow');

  editor.navigateBack();
});

test('type filter toggle can reveal all built-in node types', () => {
  editor.toggleTypeFilter();
  assert(editor.getTypeSidebarHtml().includes('Service'), 'all-types mode should include zero-count built-ins');
  editor.toggleTypeFilter();
});

await testAsync('can drill into a static subflow and navigate back to the parent workflow', async () => {
  editor.parseWorkflow({
    name: 'ParentWithChild',
    version: '3.16',
    steps: [
      { id: 'Subflow_010_ExploreChild', workflow_path: 'child-demo', next: 'Stop_End' },
      { id: 'Stop_End' },
    ],
  }, 'parent-with-child');
  const parentTitle = $('wfTitle').textContent;
  editor.getState().selectedNodeId = 'Subflow_010_ExploreChild';

  await editor.openSubflowNode(editor.getState().nodes.find((node) => node.id === 'Subflow_010_ExploreChild'));
  assertEqual($('wfTitle').textContent, 'Child Demo Workflow');
  assert(editor.getBreadcrumb().includes('ParentWithChild'), 'breadcrumb should include parent workflow');

  editor.navigateBack();
  assertEqual($('wfTitle').textContent, parentTitle);
  assert(editor.getBreadcrumb().includes('ParentWithChild'), 'breadcrumb should restore parent workflow');
});

await testAsync('loadWorkflowFromLocation reads workflow query parameter', async () => {
  window.location.search = '?workflow=child-demo';
  await editor.loadWorkflowFromLocation();
  assertEqual($('wfTitle').textContent, 'Child Demo Workflow');
  window.location.search = '';
});

await testAsync('streamSSE maps event names into payload type and captures checkpoint', async () => {
  editor.parseWorkflow(workflow, 'editor-regression');
  const response = makeStreamResponse([
    'event: node_start\ndata: {"nodeId":"Set_Start"}\n\n',
    'event: checkpoint\ndata: {"runID":"run_123","stepID":"Set_Start","completedSteps":["Set_Start"],"checkpoint":{"currentStepID":"Set_Start","variables":{"$msg":"hello"}}}\n\n',
    'event: node_done\ndata: {"nodeId":"Set_Start"}\n\n',
    'event: done\ndata: {"filesWritten":[]}\n\n',
  ]);

  await editor.streamSSE(response);

  const node = editor.getState().nodes.find((n) => n.id === 'Set_Start');
  assert(node?.status === 'done', 'Set_Start should be marked done');
  assertEqual(editor.getRunID(), 'run_123');
  assertEqual(editor.getCheckpoint()?.currentStepID, 'Set_Start');
  assertEqual(editor.getCheckpoint()?.variables?.$msg, 'hello');
  assertEqual(editor.getStatus(), 'Complete Run 1! 0 files written');
});

await testAsync('keeps separate run sessions and can switch selected run', async () => {
  localStorage._store = {};
  editor.parseWorkflow(workflow, 'editor-regression');
  await editor.streamSSE(makeStreamResponse([
    'event: checkpoint\ndata: {"runID":"run_A","checkpoint":{"currentStepID":"Set_Start","variables":{"$msg":"A"}}}\n\n',
    'event: node_done\ndata: {"runID":"run_A","nodeId":"Set_Start"}\n\n',
  ]), 'run_A');
  await editor.streamSSE(makeStreamResponse([
    'event: checkpoint\ndata: {"runID":"run_B","checkpoint":{"currentStepID":"MetaDiff_Compute","variables":{"$msg":"B"}}}\n\n',
    'event: pause\ndata: {"runID":"run_B","nodeId":"MetaDiff_Compute"}\n\n',
  ]), 'run_B');

  const sessions = editor.getRunSessions();
  assertEqual(sessions.length, 2, 'should track two runs');
  assert(sessions.some((session) => session.runID === 'run_A' && session.status === 'running'), 'run_A missing');
  assert(sessions.some((session) => session.runID === 'run_B' && session.status === 'paused'), 'run_B missing');

  editor.selectRunSession('run_A');
  assertEqual(editor.getRunID(), 'run_A');
  assertEqual(editor.getCheckpoint()?.variables?.$msg, 'A');

  editor.selectRunSession('run_B');
  assertEqual(editor.getRunID(), 'run_B');
  assertEqual(editor.getCheckpoint()?.variables?.$msg, 'B');
});

await testAsync('client run token stays stable before first checkpoint and isolates concurrent starts', async () => {
  localStorage._store = {};
  editor.parseWorkflow(workflow, 'editor-regression');

  await editor.streamSSE(makeStreamResponse([
    'event: node_start\ndata: {"clientRunToken":"client:A","nodeId":"Set_Start"}\n\n',
    'event: checkpoint\ndata: {"clientRunToken":"client:A","runID":"run_A","checkpoint":{"currentStepID":"Set_Start","variables":{"$msg":"A"}}}\n\n',
  ]), 'client:A');
  await editor.streamSSE(makeStreamResponse([
    'event: node_start\ndata: {"clientRunToken":"client:B","nodeId":"MetaDiff_Compute"}\n\n',
    'event: pause\ndata: {"clientRunToken":"client:B","runID":"run_B","nodeId":"MetaDiff_Compute"}\n\n',
    'event: checkpoint\ndata: {"clientRunToken":"client:B","runID":"run_B","checkpoint":{"currentStepID":"MetaDiff_Compute","variables":{"$msg":"B"}}}\n\n',
  ]), 'client:B');

  const sessions = editor.getRunSessions();
  assert(sessions.some((session) => session.sessionID === 'client:A' && session.runID === 'run_A'), 'client:A session missing');
  assert(sessions.some((session) => session.sessionID === 'client:B' && session.runID === 'run_B' && session.status === 'paused'), 'client:B session missing');

  editor.selectRunSession('client:A');
  assertEqual(editor.getRunID(), 'run_A');
  assertEqual(editor.getState().nodes.find((n) => n.id === 'Set_Start')?.status, 'running');

  editor.selectRunSession('client:B');
  assertEqual(editor.getRunID(), 'run_B');
  assertEqual(editor.getState().nodes.find((n) => n.id === 'MetaDiff_Compute')?.status, 'paused');
});

await testAsync('captures tool_message and tool lifecycle events in selected run log', async () => {
  localStorage._store = {};
  editor.parseWorkflow({
    name: 'ToolEventWorkflow',
    version: '3.16',
    steps: [
      { id: 'Tool_010_ReadFile', tool: 'ReadFile', input: { file_path: 'README.md' }, next: 'Stop_End' },
      { id: 'Stop_End' },
    ],
  }, 'tool-event-workflow');

  await editor.streamSSE(makeStreamResponse([
    'event: workflow_start\ndata: {"clientRunToken":"client:tool"}\n\n',
    'event: tool_start\ndata: {"clientRunToken":"client:tool","name":"ReadFile","stepId":"Tool_010_ReadFile","input":{"file_path":"README.md"}}\n\n',
    'event: tool_message\ndata: {"clientRunToken":"client:tool","name":"ReadFile","stepId":"Tool_010_ReadFile","level":"info","message":"Reading file","data":{"file_path":"README.md"}}\n\n',
    'event: tool_done\ndata: {"clientRunToken":"client:tool","runID":"run_tool","name":"ReadFile","stepId":"Tool_010_ReadFile","output":{"result":"ok"}}\n\n',
    'event: done\ndata: {"clientRunToken":"client:tool","runID":"run_tool","filesWritten":[]}\n\n',
  ]), 'client:tool');

  const sessions = editor.getRunSessions();
  const run = sessions.find((session) => session.sessionID === 'client:tool');
  assert(run, 'tool run missing');
  assert(run.eventCount >= 4, 'expected tool events to be recorded');

  editor.selectRunSession('client:tool');
  const events = editor.getSelectedEvents();
  assert(events.some((event) => event.type === 'tool_message' && event.summary.includes('Reading file')), 'missing tool_message summary');
  assert(events.some((event) => event.type === 'tool_done'), 'missing tool_done event');
});

await testAsync('tool_start marks the active node as waiting and tool_done returns it to running', async () => {
  localStorage._store = {};
  editor.parseWorkflow({
    name: 'SubflowWaitingWorkflow',
    version: '3.16',
    steps: [
      { id: 'Subflow_010_Explore', workflow_path: 'child.json', next: 'Stop_End' },
      { id: 'Stop_End' },
    ],
  }, 'subflow-waiting');

  editor.handleExecEvent({ type: 'workflow_start', clientRunToken: 'client:wait' }, 'client:wait');
  editor.handleExecEvent({ type: 'node_start', clientRunToken: 'client:wait', nodeId: 'Subflow_010_Explore' }, 'client:wait');
  editor.handleExecEvent({ type: 'tool_start', clientRunToken: 'client:wait', stepId: 'Subflow_010_Explore', name: 'WorkflowRun' }, 'client:wait');
  editor.selectRunSession('client:wait');

  let node = editor.getState().nodes.find((n) => n.id === 'Subflow_010_Explore');
  assertEqual(node?.status, 'waiting');
  assert(editor.getStatus().includes('Waiting'), 'status label should show waiting');

  editor.handleExecEvent({ type: 'tool_done', clientRunToken: 'client:wait', stepId: 'Subflow_010_Explore', name: 'WorkflowRun' }, 'client:wait');
  node = editor.getState().nodes.find((n) => n.id === 'Subflow_010_Explore');
  assertEqual(node?.status, 'running');
});

await testAsync('runWorkflow allows a second run while the first run stream is still active', async () => {
  localStorage._store = {};
  editor.parseWorkflow(workflow, 'editor-regression');

  let executeCalls = 0;
  global.fetch = async (url) => {
    if (url === '/api/workflow/ephemeral') {
      return { json: async () => ({ ok: true, name: '_ephemeral-test' }) };
    }
    if (url === '/api/workflow/execute') {
      executeCalls += 1;
      if (executeCalls === 1) {
        return makeStreamResponse([
          'event: workflow_start\ndata: {"clientRunToken":"client:1"}\n\n',
          'event: node_start\ndata: {"clientRunToken":"client:1","nodeId":"Set_Start"}\n\n',
          'event: checkpoint\ndata: {"clientRunToken":"client:1","runID":"run_1","checkpoint":{"currentStepID":"Set_Start","variables":{"$msg":"one"}}}\n\n',
          'event: pause\ndata: {"clientRunToken":"client:1","runID":"run_1","nodeId":"Set_Start"}\n\n',
        ]);
      }
      return makeStreamResponse([
        'event: workflow_start\ndata: {"clientRunToken":"client:2"}\n\n',
        'event: node_start\ndata: {"clientRunToken":"client:2","nodeId":"MetaDiff_Compute"}\n\n',
        'event: checkpoint\ndata: {"clientRunToken":"client:2","runID":"run_2","checkpoint":{"currentStepID":"MetaDiff_Compute","variables":{"$msg":"two"}}}\n\n',
        'event: pause\ndata: {"clientRunToken":"client:2","runID":"run_2","nodeId":"MetaDiff_Compute"}\n\n',
      ]);
    }
    return {
      ok: true,
      json: async () => ({}),
      body: { getReader() { return { read: async () => ({ done: true }) }; } },
    };
  };

  await Promise.all([editor.runWorkflow(), editor.runWorkflow()]);

  const sessions = editor.getRunSessions();
  assertEqual(executeCalls, 2, 'should issue two execute requests');
  assertEqual(sessions.length, 2, 'should keep two run sessions');
  assert(sessions.some((session) => session.sessionID?.startsWith('client:') && session.runID === 'run_1'), 'first run missing');
  assert(sessions.some((session) => session.sessionID?.startsWith('client:') && session.runID === 'run_2'), 'second run missing');
});

await testAsync('ensureWorkflowRef stages imported workflows via ephemeral API', async () => {
  editor.parseWorkflow({ name: 'UnsavedWorkflow', version: '3.16', steps: [{ id: 'Stop_End' }] });
  const ref = await editor.ensureWorkflowRef();
  assertEqual(ref, '_ephemeral-test');
});

console.log(`\n── Results ──\n\n  ${passed} passed, ${failed} failed\n`);
process.exit(failed > 0 ? 1 : 0);
