import { describe, expect, it, vi } from 'vitest';
import { Agent } from '../../agent/agent.js';
import { MockMessageModel } from '../../__fixtures__/mock-message-model.js';
import { MockSnapshotStorage } from '../../__fixtures__/mock-storage-provider.js';
import { collectGenerator } from '../../__fixtures__/model-test-helpers.js';
import { createCancellableAgent } from '../../__fixtures__/agent-helpers.js';
import { AfterNodeCallEvent, BeforeNodeCallEvent, MultiAgentInitializedEvent } from '../events.js';
import { ReasoningBlock, TextBlock } from '../../types/messages.js';
import { Status, MultiAgentState } from '../state.js';
import { AgentNode, MultiAgentNode } from '../nodes.js';
import { Graph } from '../graph.js';
import { SessionManager } from '../../session/session-manager.js';
function makeAgent(id, text = 'reply') {
    const model = new MockMessageModel().addTurn(new TextBlock(text));
    return new Agent({ model, printer: false, id });
}
describe('Graph', () => {
    describe('constructor', () => {
        it('defaults id to "graph"', () => {
            const graph = new Graph({
                nodes: [makeAgent('a')],
                edges: [],
            });
            expect(graph.id).toBe('graph');
        });
        it('accepts a custom id', () => {
            const graph = new Graph({
                nodes: [makeAgent('a')],
                edges: [],
                id: 'my-graph',
            });
            expect(graph.id).toBe('my-graph');
        });
        it('accepts agent node options', () => {
            const graph = new Graph({
                nodes: [{ agent: makeAgent('a') }],
                edges: [],
            });
            expect(graph.nodes.get('a')).toBeInstanceOf(AgentNode);
        });
        it('accepts multiAgent node options', () => {
            const inner = new Graph({
                id: 'inner',
                nodes: [makeAgent('x')],
                edges: [],
            });
            const graph = new Graph({
                nodes: [{ type: 'multiAgent', orchestrator: inner }],
                edges: [],
            });
            expect(graph.nodes.get('inner')).toBeInstanceOf(MultiAgentNode);
        });
        it('accepts pre-built Node instances', () => {
            const node = new AgentNode({ agent: makeAgent('a') });
            const graph = new Graph({
                nodes: [node],
                edges: [],
            });
            expect(graph.nodes.get('a')).toBe(node);
        });
        it('accepts edge options', () => {
            const graph = new Graph({
                nodes: [makeAgent('a'), makeAgent('b')],
                edges: [{ source: 'a', target: 'b' }],
            });
            expect(graph.edges).toHaveLength(1);
            expect(graph.edges[0]).toEqual(expect.objectContaining({
                source: expect.objectContaining({ id: 'a' }),
                target: expect.objectContaining({ id: 'b' }),
            }));
        });
        it('throws on duplicate node IDs', () => {
            const agent = makeAgent('a');
            expect(() => new Graph({
                nodes: [agent, agent],
                edges: [],
            })).toThrow('node_id=<a> | duplicate node id');
        });
        it('throws on edge referencing unknown source node', () => {
            expect(() => new Graph({
                nodes: [makeAgent('a')],
                edges: [['missing', 'a']],
            })).toThrow('source=<missing> | edge references unknown source node');
        });
        it('throws on edge referencing unknown target node', () => {
            expect(() => new Graph({
                nodes: [makeAgent('a')],
                edges: [['a', 'missing']],
            })).toThrow('target=<missing> | edge references unknown target node');
        });
        it('throws when graph has no source nodes', () => {
            expect(() => new Graph({
                nodes: [makeAgent('a'), makeAgent('b')],
                edges: [
                    ['a', 'b'],
                    ['b', 'a'],
                ],
            })).toThrow('graph has no source nodes');
        });
        it('throws on unreachable nodes', () => {
            expect(() => new Graph({
                nodes: [makeAgent('a'), makeAgent('b'), makeAgent('island1'), makeAgent('island2')],
                edges: [
                    ['a', 'b'],
                    ['island1', 'island2'],
                    ['island2', 'island1'],
                ],
            })).toThrow('node_id=<island1> | unreachable from any source node');
        });
        it('throws when explicit source references unknown node', () => {
            expect(() => new Graph({
                nodes: [makeAgent('a')],
                edges: [],
                sources: ['missing'],
            })).toThrow('source=<missing> | source references unknown node');
        });
        it('throws when maxSteps < 1', () => {
            expect(() => new Graph({
                nodes: [makeAgent('a')],
                edges: [],
                maxSteps: 0,
            })).toThrow('max_steps=<0> | must be at least 1');
        });
        it('throws when maxConcurrency < 1', () => {
            expect(() => new Graph({
                nodes: [makeAgent('a')],
                edges: [],
                maxConcurrency: 0,
            })).toThrow('max_concurrency=<0> | must be at least 1');
        });
        it('defaults maxConcurrency, maxSteps, timeout, and nodeTimeout to Infinity', () => {
            const graph = new Graph({
                nodes: [makeAgent('a')],
                edges: [],
            });
            expect(graph.config.maxConcurrency).toBe(Infinity);
            expect(graph.config.maxSteps).toBe(Infinity);
            expect(graph.config.timeout).toBe(Infinity);
            expect(graph.config.nodeTimeout).toBe(Infinity);
        });
        it('throws when timeout < 1', () => {
            expect(() => new Graph({
                nodes: [makeAgent('a')],
                edges: [],
                timeout: 0,
            })).toThrow('timeout=<0> | must be at least 1');
        });
        it('throws when nodeTimeout < 1', () => {
            expect(() => new Graph({
                nodes: [makeAgent('a')],
                edges: [],
                nodeTimeout: 0,
            })).toThrow('node_timeout=<0> | must be at least 1');
        });
    });
    describe('invoke', () => {
        it('executes linear graph (A -> B -> C) in order', async () => {
            const graph = new Graph({
                nodes: [makeAgent('a', 'a-reply'), makeAgent('b', 'b-reply'), makeAgent('c', 'c-reply')],
                edges: [
                    ['a', 'b'],
                    ['b', 'c'],
                ],
            });
            const result = await graph.invoke('start');
            expect(result).toEqual(expect.objectContaining({
                status: Status.COMPLETED,
                content: expect.arrayContaining([expect.objectContaining({ type: 'textBlock', text: 'c-reply' })]),
                duration: expect.any(Number),
            }));
            expect(result.results.map((r) => r.nodeId)).toStrictEqual(['a', 'b', 'c']);
        });
        it('executes parallel graph (A -> B, A -> C) with B and C after A', async () => {
            const graph = new Graph({
                nodes: [makeAgent('a', 'a-reply'), makeAgent('b', 'b-reply'), makeAgent('c', 'c-reply')],
                edges: [
                    ['a', 'b'],
                    ['a', 'c'],
                ],
            });
            const result = await graph.invoke('start');
            expect(result).toEqual(expect.objectContaining({
                status: Status.COMPLETED,
                content: expect.arrayContaining([
                    expect.objectContaining({ type: 'textBlock', text: 'b-reply' }),
                    expect.objectContaining({ type: 'textBlock', text: 'c-reply' }),
                ]),
                duration: expect.any(Number),
            }));
            expect(result.results.map((r) => r.nodeId).sort()).toStrictEqual(['a', 'b', 'c']);
        });
        it('waits for all dependencies before executing join node (A -> C, B -> C)', async () => {
            const graph = new Graph({
                nodes: [makeAgent('a', 'a-reply'), makeAgent('b', 'b-reply'), makeAgent('c', 'c-reply')],
                edges: [
                    ['a', 'c'],
                    ['b', 'c'],
                ],
                maxConcurrency: 1,
            });
            const result = await graph.invoke('start');
            expect(result).toEqual(expect.objectContaining({
                status: Status.COMPLETED,
                content: expect.arrayContaining([expect.objectContaining({ type: 'textBlock', text: 'c-reply' })]),
                duration: expect.any(Number),
            }));
            expect(result.results).toHaveLength(3);
        });
        it('executes nested graph through MultiAgentNode', async () => {
            const inner = new Graph({
                id: 'inner',
                nodes: [makeAgent('x', 'inner-reply')],
                edges: [],
            });
            const graph = new Graph({
                nodes: [makeAgent('a', 'a-reply'), inner],
                edges: [['a', 'inner']],
            });
            const result = await graph.invoke('start');
            expect(result).toEqual(expect.objectContaining({
                status: Status.COMPLETED,
                content: expect.arrayContaining([expect.objectContaining({ type: 'textBlock', text: 'inner-reply' })]),
                duration: expect.any(Number),
            }));
            expect(result.results.map((r) => r.nodeId)).toStrictEqual(['a', 'inner']);
        });
        it('uses explicit sources instead of auto-detection', async () => {
            const graph = new Graph({
                nodes: [makeAgent('a'), makeAgent('b')],
                edges: [['a', 'b'], { source: 'b', target: 'a', handler: () => false }],
                sources: ['a'],
            });
            const result = await graph.invoke('go');
            expect(result).toEqual(expect.objectContaining({
                status: Status.COMPLETED,
                duration: expect.any(Number),
            }));
            expect(result.results.map((r) => r.nodeId)).toStrictEqual(['a', 'b']);
        });
        it('evaluates conditional edges', async () => {
            const graph = new Graph({
                nodes: [makeAgent('a', 'a-reply'), makeAgent('b', 'b-reply'), makeAgent('c', 'c-reply')],
                edges: [
                    { source: 'a', target: 'b', handler: () => true },
                    { source: 'a', target: 'c', handler: () => false },
                ],
            });
            const result = await graph.invoke('start');
            expect(result).toEqual(expect.objectContaining({
                status: Status.COMPLETED,
                duration: expect.any(Number),
            }));
            expect(result.results.map((r) => r.nodeId)).toStrictEqual(['a', 'b']);
        });
        it('evaluates conditional edges on join node (A -> C false, B -> C)', async () => {
            const graph = new Graph({
                nodes: [makeAgent('a', 'a-reply'), makeAgent('b', 'b-reply'), makeAgent('c', 'c-reply')],
                edges: [{ source: 'a', target: 'c', handler: () => false }, ['b', 'c']],
            });
            const result = await graph.invoke('start');
            expect(result.status).toBe(Status.COMPLETED);
            expect(result.results.map((r) => r.nodeId).sort()).toStrictEqual(['a', 'b']);
        });
        it('evaluates conditional edges on join node (A -> C true, B -> C true)', async () => {
            const graph = new Graph({
                nodes: [makeAgent('a', 'a-reply'), makeAgent('b', 'b-reply'), makeAgent('c', 'c-reply')],
                edges: [
                    { source: 'a', target: 'c', handler: () => true },
                    { source: 'b', target: 'c', handler: () => true },
                ],
            });
            const result = await graph.invoke('start');
            expect(result.status).toBe(Status.COMPLETED);
            expect(result.results.map((r) => r.nodeId).sort()).toStrictEqual(['a', 'b', 'c']);
        });
        it('passes task + dependency content to downstream nodes', async () => {
            const agentB = makeAgent('b');
            const streamSpy = vi.spyOn(agentB, 'stream');
            const graph = new Graph({
                nodes: [makeAgent('a', 'from-a'), agentB],
                edges: [['a', 'b']],
            });
            await graph.invoke('task-input');
            expect(streamSpy).toHaveBeenCalled();
            const input = streamSpy.mock.calls[0][0];
            expect(input.map((b) => b.text)).toStrictEqual(['task-input', '[node: a]', 'from-a']);
        });
        it('does not pass dependency reasoning blocks to downstream nodes', async () => {
            const agentA = new Agent({
                model: new MockMessageModel().addTurn([
                    new ReasoningBlock({ text: 'private reasoning' }),
                    new TextBlock('from-a'),
                ]),
                printer: false,
                id: 'a',
            });
            const agentB = makeAgent('b');
            const streamSpy = vi.spyOn(agentB, 'stream');
            const graph = new Graph({
                nodes: [agentA, agentB],
                edges: [['a', 'b']],
            });
            await graph.invoke('task-input');
            expect(streamSpy).toHaveBeenCalled();
            const input = streamSpy.mock.calls[0][0];
            expect(input.map((b) => b.text)).toStrictEqual(['task-input', '[node: a]', 'from-a']);
            expect(input.map((b) => b.type)).toStrictEqual(['textBlock', 'textBlock', 'textBlock']);
        });
        it('converts ContentBlockData[] input to ContentBlock instances for downstream nodes', async () => {
            const agentB = makeAgent('b');
            const streamSpy = vi.spyOn(agentB, 'stream');
            const graph = new Graph({
                nodes: [makeAgent('a', 'from-a'), agentB],
                edges: [['a', 'b']],
            });
            const dataInput = [{ text: 'data-input' }];
            await graph.invoke(dataInput);
            expect(streamSpy).toHaveBeenCalled();
            const input = streamSpy.mock.calls[0][0];
            expect(input[0]).toBeInstanceOf(TextBlock);
            expect(input.map((b) => b.text)).toStrictEqual(['data-input', '[node: a]', 'from-a']);
        });
        it('returns failed result when agent throws', async () => {
            const model = new MockMessageModel().addTurn(new Error('agent exploded'));
            const agent = new Agent({ model, printer: false, id: 'a' });
            const graph = new Graph({
                nodes: [agent, makeAgent('b', 'b-reply')],
                edges: [['a', 'b']],
            });
            const result = await graph.invoke('go');
            expect(result).toEqual(expect.objectContaining({
                status: Status.FAILED,
                duration: expect.any(Number),
            }));
            expect(result.results).toHaveLength(1);
            expect(result.results[0]).toEqual(expect.objectContaining({ nodeId: 'a', status: Status.FAILED }));
        });
        it('propagates unexpected errors from node execution', async () => {
            const graph = new Graph({
                nodes: [makeAgent('a')],
                edges: [],
            });
            const node = graph.nodes.get('a');
            // eslint-disable-next-line require-yield
            vi.spyOn(node, 'stream').mockImplementation(async function* () {
                throw new Error('unexpected failure');
            });
            await expect(graph.invoke('go')).rejects.toThrow('unexpected failure');
        });
        it('throws when maxSteps is exceeded', async () => {
            const graph = new Graph({
                nodes: [makeAgent('a'), makeAgent('b'), makeAgent('c')],
                edges: [
                    ['a', 'b'],
                    ['b', 'c'],
                ],
                maxSteps: 2,
            });
            await expect(graph.invoke('go')).rejects.toThrow('max steps reached');
        });
        it('throws when a node exceeds nodeTimeout', async () => {
            const graph = new Graph({
                nodes: [{ agent: createCancellableAgent('slow', 100) }],
                edges: [],
                nodeTimeout: 20,
            });
            await expect(graph.invoke('go')).rejects.toThrow(/node_timeout=<20>, node_id=<slow>/);
        });
        it('applies per-node timeout over nodeTimeout', async () => {
            const graph = new Graph({
                nodes: [{ agent: createCancellableAgent('slow', 100), timeout: 15 }],
                edges: [],
                nodeTimeout: 10_000,
            });
            await expect(graph.invoke('go')).rejects.toThrow(/node_timeout=<15>, node_id=<slow>/);
        });
        it('does not throw when nodeTimeout is Infinity', async () => {
            const graph = new Graph({
                nodes: [{ agent: createCancellableAgent('a', 20) }],
                edges: [],
                nodeTimeout: Infinity,
            });
            const result = await graph.invoke('go');
            expect(result.results).toHaveLength(1);
            expect(result.results[0]?.status).toBe(Status.COMPLETED);
        });
        it('per-node timeout of Infinity disables a finite nodeTimeout', async () => {
            const graph = new Graph({
                nodes: [{ agent: createCancellableAgent('slow', 30), timeout: Infinity }],
                edges: [],
                nodeTimeout: 10,
            });
            const result = await graph.invoke('go');
            expect(result.results).toHaveLength(1);
            expect(result.results[0]?.status).toBe(Status.COMPLETED);
        });
        it('throws when timeout is exceeded', async () => {
            const graph = new Graph({
                nodes: [{ agent: createCancellableAgent('a', 30) }, { agent: createCancellableAgent('b', 30) }],
                edges: [['a', 'b']],
                timeout: 20,
            });
            await expect(graph.invoke('go')).rejects.toThrow(/timeout=<20>/);
        });
        it('calls initialize only once across invocations', async () => {
            let callCount = 0;
            const graph = new Graph({
                nodes: [makeAgent('a')],
                edges: [],
            });
            graph.addHook(MultiAgentInitializedEvent, () => {
                callCount++;
            });
            await graph.invoke('first');
            await graph.invoke('second');
            expect(callCount).toBe(1);
        });
        it('respects maxConcurrency limit', async () => {
            let concurrent = 0;
            let maxConcurrent = 0;
            const graph = new Graph({
                nodes: [makeAgent('a'), makeAgent('b'), makeAgent('c')],
                edges: [
                    ['a', 'b'],
                    ['a', 'c'],
                ],
                maxConcurrency: 1,
            });
            graph.addHook(BeforeNodeCallEvent, () => {
                concurrent++;
                maxConcurrent = Math.max(maxConcurrent, concurrent);
            });
            graph.addHook(AfterNodeCallEvent, () => {
                concurrent--;
            });
            const result = await graph.invoke('go');
            expect(result.status).toBe(Status.COMPLETED);
            expect(maxConcurrent).toBe(1);
        });
        it('preserves agent messages and state after execution', async () => {
            const agent = makeAgent('a', 'reply');
            const messagesBefore = [...agent.messages];
            const stateBefore = agent.appState.getAll();
            const graph = new Graph({
                nodes: [agent],
                edges: [],
            });
            await graph.invoke('hello');
            expect(agent.messages).toStrictEqual(messagesBefore);
            expect(agent.appState.getAll()).toStrictEqual(stateBefore);
        });
        it('executes join node exactly once when all parents complete concurrently', async () => {
            const graph = new Graph({
                nodes: [makeAgent('a', 'a-reply'), makeAgent('b', 'b-reply'), makeAgent('c', 'c-reply')],
                edges: [
                    ['a', 'c'],
                    ['b', 'c'],
                ],
            });
            const nodeC = graph.nodes.get('c');
            const streamSpy = vi.spyOn(nodeC, 'stream');
            const result = await graph.invoke('go');
            expect(result.status).toBe(Status.COMPLETED);
            expect(streamSpy).toHaveBeenCalledTimes(1);
        });
        it('re-executes node in a cycle when conditional edge allows re-entry', async () => {
            let visits = 0;
            const graph = new Graph({
                nodes: [makeAgent('a', 'a-reply')],
                edges: [
                    {
                        source: 'a',
                        target: 'a',
                        handler: () => {
                            visits++;
                            return visits < 2;
                        },
                    },
                ],
                sources: ['a'],
            });
            const result = await graph.invoke('go');
            expect(result.status).toBe(Status.COMPLETED);
            expect(result.results).toHaveLength(2);
            expect(result.results.map((r) => r.nodeId)).toStrictEqual(['a', 'a']);
            expect(visits).toBe(2);
        });
    });
    describe('stream', () => {
        it('yields lifecycle events in correct order for single node', async () => {
            const graph = new Graph({
                nodes: [makeAgent('a', 'a-reply')],
                edges: [],
            });
            const { items, result } = await collectGenerator(graph.stream('go'));
            const eventTypes = items.map((e) => e.type);
            expect(result.status).toBe(Status.COMPLETED);
            expect(result.results.map((r) => r.nodeId)).toStrictEqual(['a']);
            expect(eventTypes).toStrictEqual([
                'beforeMultiAgentInvocationEvent',
                'beforeNodeCallEvent',
                ...eventTypes.filter((t) => t === 'nodeStreamUpdateEvent'),
                'nodeResultEvent',
                'afterNodeCallEvent',
                'afterMultiAgentInvocationEvent',
                'multiAgentResultEvent',
            ]);
        });
        it('yields handoff events on transitions between nodes', async () => {
            const graph = new Graph({
                nodes: [makeAgent('a', 'a-reply'), makeAgent('b', 'b-reply')],
                edges: [['a', 'b']],
            });
            const { items } = await collectGenerator(graph.stream('go'));
            const handoffEvents = items.filter((e) => e.type === 'multiAgentHandoffEvent');
            expect(handoffEvents).toHaveLength(1);
            expect(handoffEvents[0]).toEqual(expect.objectContaining({
                type: 'multiAgentHandoffEvent',
                source: 'a',
                targets: ['b'],
                state: expect.any(MultiAgentState),
            }));
        });
        it('returns cancelled result when cancel is true', async () => {
            const graph = new Graph({
                nodes: [makeAgent('a')],
                edges: [],
            });
            graph.addHook(BeforeNodeCallEvent, (event) => {
                event.cancel = true;
            });
            const { items, result } = await collectGenerator(graph.stream('go'));
            expect(result.status).toBe(Status.CANCELLED);
            expect(result.results).toHaveLength(1);
            expect(result.results[0]).toEqual(expect.objectContaining({ nodeId: 'a', status: Status.CANCELLED, duration: 0 }));
            const cancelEvent = items.find((e) => e.type === 'nodeCancelEvent');
            expect(cancelEvent).toEqual(expect.objectContaining({ nodeId: 'a', state: expect.any(MultiAgentState), message: 'node cancelled by hook' }));
        });
        it('returns cancelled result with custom message when cancel is a string', async () => {
            const graph = new Graph({
                nodes: [makeAgent('a')],
                edges: [],
            });
            graph.addHook(BeforeNodeCallEvent, (event) => {
                event.cancel = 'node not ready';
            });
            const { items, result } = await collectGenerator(graph.stream('go'));
            expect(result.status).toBe(Status.CANCELLED);
            const cancelEvent = items.find((e) => e.type === 'nodeCancelEvent');
            expect(cancelEvent).toEqual(expect.objectContaining({ nodeId: 'a', state: expect.any(MultiAgentState), message: 'node not ready' }));
        });
        it('cleans up running nodes when consumer breaks mid-stream', async () => {
            const graph = new Graph({
                nodes: [makeAgent('a', 'a-reply'), makeAgent('b', 'b-reply')],
                edges: [['a', 'b']],
            });
            const gen = graph.stream('go');
            const first = await gen.next();
            expect(first.done).toBe(false);
            // Simulates consumer break — should not hang waiting for node streams
            const result = await gen.return(undefined);
            expect(result.done).toBe(true);
        });
    });
    describe('resume with session manager', () => {
        function makeSessionManager(storage) {
            return new SessionManager({
                sessionId: 'test-session',
                storage: { snapshot: storage },
            });
        }
        it('throws when sessionManager appears in both constructor arg and plugins', () => {
            const sm = makeSessionManager(new MockSnapshotStorage());
            expect(() => new Graph({
                nodes: [makeAgent('a')],
                edges: [],
                sessionManager: sm,
                plugins: [sm],
            })).toThrow('sessionManager was provided as both a constructor argument and in the plugins array');
        });
        it('resumes from the next ready node after a linear graph stops (A→B→C, A done, resumes at B)', async () => {
            const storage = new MockSnapshotStorage();
            const graph1 = new Graph({
                id: 'my-graph',
                nodes: [makeAgent('a', 'a-reply'), makeAgent('b', 'b-reply'), makeAgent('c', 'c-reply')],
                edges: [
                    ['a', 'b'],
                    ['b', 'c'],
                ],
                maxSteps: 1,
                sessionManager: makeSessionManager(storage),
            });
            await expect(graph1.invoke('start')).rejects.toThrow('max steps reached');
            const graph2 = new Graph({
                id: 'my-graph',
                nodes: [makeAgent('a', 'a-reply'), makeAgent('b', 'b-reply'), makeAgent('c', 'c-reply')],
                edges: [
                    ['a', 'b'],
                    ['b', 'c'],
                ],
                sessionManager: makeSessionManager(storage),
            });
            const result = await graph2.invoke('start');
            expect(result.status).toBe(Status.COMPLETED);
            const completedIds = result.results.filter((r) => r.status === Status.COMPLETED).map((r) => r.nodeId);
            expect(completedIds).toStrictEqual(['a', 'b', 'c']);
        });
        it('resumes parallel branches independently (A→B, A→C, B done, C cancelled, resumes at C)', async () => {
            const storage = new MockSnapshotStorage();
            const graph1 = new Graph({
                id: 'my-graph',
                nodes: [makeAgent('a', 'a-reply'), makeAgent('b', 'b-reply'), makeAgent('c', 'c-reply')],
                edges: [
                    ['a', 'b'],
                    ['a', 'c'],
                ],
                plugins: [makeSessionManager(storage)],
                maxConcurrency: 1,
            });
            graph1.addHook(BeforeNodeCallEvent, (event) => {
                if (event.nodeId === 'c')
                    event.cancel = 'simulated stop';
            });
            await graph1.invoke('start');
            const graph2 = new Graph({
                id: 'my-graph',
                nodes: [makeAgent('a', 'a-reply'), makeAgent('b', 'b-reply'), makeAgent('c', 'c-reply')],
                edges: [
                    ['a', 'b'],
                    ['a', 'c'],
                ],
                plugins: [makeSessionManager(storage)],
            });
            const result = await graph2.invoke('start');
            const completedIds = result.results.filter((r) => r.status === Status.COMPLETED).map((r) => r.nodeId);
            expect(completedIds).toContain('a');
            expect(completedIds).toContain('b');
            expect(completedIds).toContain('c');
            // A and B should appear once each (not re-executed)
            expect(completedIds.filter((id) => id === 'a')).toHaveLength(1);
            expect(completedIds.filter((id) => id === 'b')).toHaveLength(1);
        });
        it('starts fresh when all nodes completed in the previous run', async () => {
            const storage = new MockSnapshotStorage();
            const graph1 = new Graph({
                id: 'my-graph',
                nodes: [makeAgent('a', 'a-reply'), makeAgent('b', 'b-reply')],
                edges: [['a', 'b']],
                plugins: [makeSessionManager(storage)],
            });
            const result1 = await graph1.invoke('start');
            expect(result1.status).toBe(Status.COMPLETED);
            const graph2 = new Graph({
                id: 'my-graph',
                nodes: [makeAgent('a', 'a-reply'), makeAgent('b', 'b-reply')],
                edges: [['a', 'b']],
                plugins: [makeSessionManager(storage)],
            });
            const result2 = await graph2.invoke('start');
            expect(result2.status).toBe(Status.COMPLETED);
            // A should appear twice — once from restored state, once from fresh execution
            const aCount = result2.results.filter((r) => r.nodeId === 'a' && r.status === Status.COMPLETED).length;
            expect(aCount).toBe(2);
        });
        it('respects conditional edges on resume', async () => {
            const storage = new MockSnapshotStorage();
            // A → B (always), A → C (condition: false)
            // First run: A completes, B completes, C blocked by condition
            // maxSteps=2 allows A and B but graph completes normally since C is blocked
            const graph1 = new Graph({
                id: 'my-graph',
                nodes: [makeAgent('a', 'a-reply'), makeAgent('b', 'b-reply'), makeAgent('c', 'c-reply')],
                edges: [
                    { source: 'a', target: 'b', handler: () => true },
                    { source: 'a', target: 'c', handler: () => false },
                ],
                plugins: [makeSessionManager(storage)],
            });
            const result1 = await graph1.invoke('start');
            expect(result1.results.map((r) => r.nodeId)).toStrictEqual(['a', 'b']);
            // Resume: C should still be blocked by the false condition
            const graph2 = new Graph({
                id: 'my-graph',
                nodes: [makeAgent('a', 'a-reply'), makeAgent('b', 'b-reply'), makeAgent('c', 'c-reply')],
                edges: [
                    { source: 'a', target: 'b', handler: () => true },
                    { source: 'a', target: 'c', handler: () => false },
                ],
                plugins: [makeSessionManager(storage)],
            });
            const result2 = await graph2.invoke('start');
            // C should not appear — condition still blocks it
            const completedIds = result2.results.filter((r) => r.status === Status.COMPLETED).map((r) => r.nodeId);
            expect(completedIds).not.toContain('c');
        });
    });
});
//# sourceMappingURL=graph.test.js.map