فهرست منبع

refactor test

JAQUIN_C 1 ماه پیش
والد
کامیت
dbee9c4278
3فایلهای تغییر یافته به همراه597 افزوده شده و 878 حذف شده
  1. 95 0
      src/engine/SimulationBuilder.ts
  2. 460 878
      src/engine/simulator.test.ts
  3. 42 0
      src/engine/test-factories.ts

+ 95 - 0
src/engine/SimulationBuilder.ts

@@ -0,0 +1,95 @@
+import {
+  FactorioEngineOrchestrator,
+  MachineSimulator,
+  InserterSimulator,
+  FilterableInserterSimulator,
+  Chest,
+  Belt,
+} from "./simulator";
+import type { Recipe } from "../../scripts/factorio-dump/helpers/recipes.helper";
+import type { Machine } from "../../scripts/factorio-dump/process-data.models";
+import type { MachineSetup } from "./model";
+import data from "../assets/data/2.0/data.json"
+
+// --- Shared Fixtures ---
+export const StandardSetups = {
+  assembler1: { machine: { crafting_speed: 1 } as Machine, machineModules: [], beacons: [], machineQualityLevel: 0 },
+  assembler2: { machine: { name: "assembling-machine-2", crafting_speed: 10 } as Machine, machineModules: [], beacons: [], machineQualityLevel: 0 },
+  empSpeed: { machine: { name: "electromagnetic-plant", crafting_speed: 6 } as Machine, machineModules: [], beacons: [], machineQualityLevel: 5 },
+  fastAssembler: { machine: { crafting_speed: 60 } as Machine, machineModules: [], beacons: [], machineQualityLevel: 0 },
+  instantAssembler: { machine: { crafting_speed: 30 } as Machine, machineModules: [], beacons: [], machineQualityLevel: 0 }
+};
+
+// --- The Builder ---
+export class SimulationBuilder {
+  public orchestrator = new FactorioEngineOrchestrator();
+  
+  public machines: Record<string, MachineSimulator> = {};
+  public chests: Record<string, Chest> = {};
+  public belts: Record<string, Belt> = {};
+  public inserters: Record<string, InserterSimulator | FilterableInserterSimulator> = {};
+
+  addMachine(id: string, recipe: Recipe, setup: MachineSetup = StandardSetups.assembler1 ) {
+    const machine = new MachineSimulator(setup, recipe, data.stackSizes);
+    this.machines[id] = machine;
+    this.orchestrator.registerMachine(machine);
+    return this;
+  }
+
+  addChest(id: string, initialItem?: string) {
+    this.chests[id] = initialItem ? new Chest(initialItem) : new Chest();
+    return this;
+  }
+
+  addBelt(id: string, ...items: string[]) {
+    this.belts[id] = new Belt(...items);
+    return this;
+  }
+
+  addInserter(
+    id: string,
+    handSize: number,
+    sourceId: string,
+    targetId: string,
+    filters?: string | string[],
+    isDynamicFilter: boolean = false
+  ) {
+    const source = this.getEntity(sourceId);
+    const target = this.getEntity(targetId);
+
+    let inserter;
+    if (Array.isArray(filters) || isDynamicFilter) {
+      inserter = new FilterableInserterSimulator(handSize, source, target, Array.isArray(filters) ? filters : undefined);
+      if (isDynamicFilter) inserter.useDynamicFilters = true;
+    } else {
+      inserter = new InserterSimulator(handSize, source, target, filters as string);
+    }
+
+    this.inserters[id] = inserter;
+    this.orchestrator.registerInserter(inserter);
+    return this;
+  }
+
+  bindToClock(inserterId: string, rowId: string) {
+    const inserter = this.inserters[inserterId];
+    if (!inserter) throw new Error(`Inserter ${inserterId} not found`);
+    this.orchestrator.bindInserterToRow(inserter, rowId);
+    return this;
+  }
+
+  private getEntity(id: string) {
+    const entity = this.chests[id] || this.machines[id] || this.belts[id];
+    if (!entity) throw new Error(`Entity '${id}' not found in builder registry. Declare it before using it in an inserter.`);
+    return entity;
+  }
+
+  build() {
+    return {
+      orchestrator: this.orchestrator,
+      machines: this.machines,
+      chests: this.chests,
+      belts: this.belts,
+      inserters: this.inserters,
+    };
+  }
+}

+ 460 - 878
src/engine/simulator.test.ts

@@ -1,943 +1,525 @@
 import { describe, it, expect, vi } from "vitest";
 import * as statsModule from "./stats";
-import {
-  MachineSimulator,
-  InserterSimulator,
-  Chest,
-  InserterState,
-  FactorioEngineOrchestrator,
-  Belt,
-  FilterableInserterSimulator,
-} from "./simulator";
-import type { Recipe } from "../../scripts/factorio-dump/helpers/recipes.helper";
-import type { Machine } from "../../scripts/factorio-dump/process-data.models";
+import { InserterState } from "./simulator";
 import type { ClockBlock, ClockRow } from "../assets/ClockTimeline/model";
-import type { MachineSetup } from "./model";
+import { Recipes, tickTimes } from "./test-factories";
+import { SimulationBuilder, StandardSetups } from "./SimulationBuilder";
 
 describe("Simulator Engine", () => {
-  const mockMachineSetup: MachineSetup = {
-    machine: { crafting_speed: 1 } as Machine,
-    machineModules: [],
-    beacons: [],
-    machineQualityLevel: 0,
-  };
-
-  const basicRecipe: Recipe = {
-    name: "iron-gear-wheel",
-    energy_required: 0.5, // 30 ticks
-    ingredients: [{ type: "item", name: "iron-plate", amount: 2 }],
-    results: [{ type: "item", name: "iron-gear-wheel", amount: 1 }],
-  } as Recipe;
-
-  it("Machine Simulator: Crafts correctly over time and consumes inputs instantly", () => {
-    const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { "iron-gear-wheel": 100 });
-
-    // Feed it enough for 2 crafts
-    machine.inputBuffer["iron-plate"] = 4;
-
-    machine.tick();
-
-    // Tick 1: Inputs consumed instantly, craft starts
-    expect(machine.isCrafting).toBe(true);
-    expect(machine.inputBuffer["iron-plate"]).toBe(2);
-    expect(machine.outputBuffer["iron-gear-wheel"]).toBe(0);
-
-    // Fast forward 28 ticks (29 total)
-    for (let i = 0; i < 29; i++) machine.tick();
-    expect(machine.outputBuffer["iron-gear-wheel"]).toBe(0); // Not done yet
-
-    // Tick 30: Craft completes!
-    machine.tick();
-    expect(machine.outputBuffer["iron-gear-wheel"]).toBe(1);
-    expect(machine.inputBuffer["iron-plate"]).toBe(0);
-
-    machine.tick();
-    // Tick 31: It should immediately start the next craft in the same tick if buffer allows
-    expect(machine.isCrafting).toBe(true);
-    expect(machine.inputBuffer["iron-plate"]).toBe(0);
-
-    for (let i = 0; i < 29; i++) machine.tick();
-    expect(machine.outputBuffer["iron-gear-wheel"]).toBe(2);
-  });
+  describe("Basic Mechanics", () => {
+    it("Machine Simulator: Crafts correctly over time and consumes inputs instantly", () => {
+      const { machines } = new SimulationBuilder()
+        .addMachine("machine", Recipes.ironGear)
+        .build();
 
-  it("Machine Simulator: Processes multiple crafts in a single tick if speed is extreme", () => {
-    const fastSetup: MachineSetup = {
-      machine: { crafting_speed: 60 } as Machine,
-      machineModules: [],
-      beacons: [],
-      machineQualityLevel: 0,
-    };
+      const machine = machines.machine;
+      machine.inputBuffer["iron-plate"] = 4;
 
-    const machine = new MachineSimulator(fastSetup, basicRecipe, { "iron-gear-wheel": 100 });
-    machine.inputBuffer["iron-plate"] = 20; // 10 crafts worth
+      machine.tick();
+      expect(machine.isCrafting).toBe(true);
+      expect(machine.inputBuffer["iron-plate"]).toBe(2);
+      expect(machine.outputBuffer["iron-gear-wheel"]).toBe(0);
 
-    // Tick 1: Should complete 2 crafts instantly (60 speed / 30 ticks required = 2 per tick)
-    machine.tick();
+      tickTimes(machine, 29);
+      expect(machine.outputBuffer["iron-gear-wheel"]).toBe(0);
 
-    expect(machine.inputBuffer["iron-plate"]).toBe(14); // 20 - 6 (2 first craft plus next tick craft)
-    expect(machine.outputBuffer["iron-gear-wheel"]).toBe(2);
-  });
+      machine.tick();
+      expect(machine.outputBuffer["iron-gear-wheel"]).toBe(1);
+      expect(machine.inputBuffer["iron-plate"]).toBe(0);
 
-  it("Inserter Simulator: Executes exact tick phases for a Chest -> Machine transfer", () => {
-    const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { "iron-gear-wheel": 100 });
-    const sourceChest = new Chest("iron-plate");
-    const inserter = new InserterSimulator(16, sourceChest, machine, "iron-plate");
-
-    expect(inserter.state).toBe(InserterState.Idle);
-
-    inserter.tick();
-    // Tick 1: Wakes up, extracts instantly, and transitions to SwingingForward
-    expect(inserter.state).toBe(InserterState.SwingingForward);
-
-    inserter.tick();
-    inserter.tick();
-    inserter.tick();
-    // Ticks 2-4: Rotation (3 ticks) complete, transition to Dropping
-    expect(inserter.state).toBe(InserterState.Dropping);
-
-    inserter.tick();
-    // Tick 5: Drop complete (1 tick machine), contents deposited!
-    expect(machine.inputBuffer["iron-plate"]).toBe(16);
-    expect(inserter.state).toBe(InserterState.SwingingBack);
-
-    inserter.tick();
-    inserter.tick();
-    inserter.tick();
-    // Ticks 6-8: Rotation back (3 ticks)
-    expect(inserter.state).toBe(InserterState.Idle);
-  });
+      machine.tick();
+      expect(machine.isCrafting).toBe(true);
+      expect(machine.inputBuffer["iron-plate"]).toBe(0);
 
-  it("Overload Limit validation: Inserter ignores target size if buffer is under limit", () => {
-    // We override the stats module or set up the config so overloadMultiplier is 3
-    // For this test, we assume computeMachineStats returned an overloadMultiplier of 3.
-    const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { "iron-gear-wheel": 100 });
-    machine.timings.overloadMultiplier = 3; // Forcing it to 3 for the test assertion
+      tickTimes(machine, 29);
+      expect(machine.outputBuffer["iron-gear-wheel"]).toBe(2);
+    });
 
-    const sourceChest = new Chest("iron-plate");
-    const inserter = new InserterSimulator(16, sourceChest, machine, "iron-plate");
+    it("Machine Simulator: Processes multiple crafts in a single tick if speed is extreme", () => {
+      const { machines } = new SimulationBuilder()
+        .addMachine("machine", Recipes.ironGear, StandardSetups.fastAssembler)
+        .build();
 
-    expect(machine.timings.overloadMultiplier).toBe(3);
-    // Overload limit = 2 (recipe amount) * 3 = 6 iron plates limit.
+      const machine = machines.machine;
+      machine.inputBuffer["iron-plate"] = 20;
 
-    // It should wake up and insert all 16 because the buffer is currently 0 (< 6)
-    for (let i = 0; i < 5; i++) inserter.tick(); // 5 ticks for Chest->Machine swing
+      machine.tick();
 
-    expect(machine.inputBuffer["iron-plate"]).toBe(16); // Overstuffed!
+      expect(machine.inputBuffer["iron-plate"]).toBe(14);
+      expect(machine.outputBuffer["iron-gear-wheel"]).toBe(2);
+    });
 
-    // The machine instantly consumes 2, leaving 14.
-    machine.tick();
-    expect(machine.inputBuffer["iron-plate"]).toBe(14);
+    it("Inserter Simulator: Executes exact tick phases for a Chest -> Machine transfer", () => {
+      const { machines, inserters } = new SimulationBuilder()
+        .addChest("source", "iron-plate")
+        .addMachine("machine", Recipes.ironGear)
+        .addInserter("inserter", 16, "source", "machine", "iron-plate")
+        .build();
 
-    // The inserter returns to idle
-    for (let i = 0; i < 3; i++) inserter.tick(); // 3 ticks for rotation back
-    expect(inserter.state).toBe(InserterState.Idle);
+      const inserter = inserters.inserter;
+      const machine = machines.machine;
 
-    // Tick the inserter again. It should NOT wake up, because 14 is NOT < 6.
-    inserter.tick();
-    expect(inserter.state).toBe(InserterState.Idle);
-  });
-});
-describe("Factorio Strict Phase Orchestrator", () => {
-  const mockMachineSetup: MachineSetup = {
-    machine: { crafting_speed: 1 } as Machine,
-    machineModules: [],
-    beacons: [],
-    machineQualityLevel: 0,
-  };
-
-  const basicRecipe: Recipe = {
-    name: "iron-gear-wheel",
-    energy_required: 0.5, // 30 ticks
-    ingredients: [{ type: "item", name: "iron-plate", amount: 2 }],
-    results: [{ type: "item", name: "iron-gear-wheel", amount: 1 }],
-  } as Recipe;
-
-  it("End-to-End: Proves the 1-tick delay caused by Inserters updating before Machines", () => {
-    const orchestrator = new FactorioEngineOrchestrator();
-
-    const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { "iron-gear-wheel": 100 });
-    const sourceChest = new Chest("iron-plate");
-    const sinkChest = new Chest();
-
-    const inputInserter = new InserterSimulator(2, sourceChest, machine, "iron-plate");
-    const outputInserter = new InserterSimulator(1, machine, sinkChest, "iron-gear-wheel");
-
-    orchestrator.registerInserter(inputInserter);
-    orchestrator.registerInserter(outputInserter);
-    orchestrator.registerMachine(machine);
-
-    expect(orchestrator.currentTick).toBe(0);
-
-    orchestrator.tickUntil(() => inputInserter.state == InserterState.Dropping);
-    expect(orchestrator.currentTick).toBe(4);
-
-    orchestrator.tickUntil(() => (machine.outputBuffer["iron-gear-wheel"] || 0) >= 1);
-    expect(orchestrator.currentTick).toBe(35);
-
-    orchestrator.tickUntil(() => outputInserter.heldItems >= 1);
-    expect(orchestrator.currentTick).toBe(36);
-
-    orchestrator.tickUntil(() => (sinkChest.receivedCounts["iron-gear-wheel"] || 0) >= 1);
-
-    // EXACT TIMELINE TRACE:
-    // T0: Input Picks.
-    // T1-T4: Input Swings.
-    // T5 (Inserter Phase): Input Drops item into machine. Output is asleep.
-    // T5 (Machine Phase): Machine sees item, crafts (1/30).
-    // ...
-    // T34 (Machine Phase): Craft finishes (30/30). Item is output!
-    // T36 (Inserter Phase): Output finally sees item, wakes up, Picks.
-    // T37-39: Output Swings.
-    // T40: Output Drops item into chest.
-
-    expect(orchestrator.currentTick).toBe(40);
-  });
+      expect(inserter.state).toBe(InserterState.Idle);
 
-  it("Round-Robin: Two inserters compete for same output and share perfectly", () => {
-    const orchestrator = new FactorioEngineOrchestrator();
-    const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { "iron-gear-wheel": 100 });
+      inserter.tick();
+      expect(inserter.state).toBe(InserterState.SwingingForward);
 
-    const sourceChest = new Chest("iron-plate");
-    const sinkChestA = new Chest();
-    const sinkChestB = new Chest();
+      tickTimes(inserter, 3);
+      expect(inserter.state).toBe(InserterState.Dropping);
 
-    const input = new InserterSimulator(16, sourceChest, machine, "iron-plate");
+      inserter.tick();
+      expect(machine.inputBuffer["iron-plate"]).toBe(16);
+      expect(inserter.state).toBe(InserterState.SwingingBack);
 
-    // Both output inserters have a hand size of 1
-    const outA = new InserterSimulator(1, machine, sinkChestA, "iron-gear-wheel");
-    const outB = new InserterSimulator(1, machine, sinkChestB, "iron-gear-wheel");
+      tickTimes(inserter, 3);
+      expect(inserter.state).toBe(InserterState.Idle);
+    });
 
-    orchestrator.registerInserter(input);
-    orchestrator.registerInserter(outA); // A registered first!
-    orchestrator.registerInserter(outB);
-    orchestrator.registerMachine(machine);
+    it("Overload Limit validation: Inserter ignores target size if buffer is under limit", () => {
+      const { machines, inserters } = new SimulationBuilder()
+        .addChest("source", "iron-plate")
+        .addMachine("machine", Recipes.ironGear)
+        .addInserter("inserter", 16, "source", "machine", "iron-plate")
+        .build();
 
-    // Run until 4 items are produced and extracted
-    orchestrator.tickUntil(
-      () =>
-        (sinkChestA.receivedCounts["iron-gear-wheel"] || 0) + (sinkChestB.receivedCounts["iron-gear-wheel"] || 0) === 4,
-    );
+      const machine = machines.machine;
+      const inserter = inserters.inserter;
+      
+      machine.timings.overloadMultiplier = 3;
 
-    // If build-order was absolute, A would have 4 and B would have 0.
-    // Because of the Round-Robin queue, they should have exactly 2 each!
-    expect(sinkChestA.receivedCounts["iron-gear-wheel"]).toBe(2);
-    expect(sinkChestB.receivedCounts["iron-gear-wheel"]).toBe(2);
-  });
-  it("Progressive Belt Pickup: Stack inserter extracts 4 items per tick until full", () => {
-    const orchestrator = new FactorioEngineOrchestrator();
-
-    // Using the Belt helper from our previous setup
-    const sourceBelt = new Belt("iron-plate");
-    const sinkChest = new Chest();
-
-    // We set a weird hand size of 14.
-    // At 4 items per tick, this should take exactly 4 ticks (4, 4, 4, 2).
-    const inserter = new InserterSimulator(14, sourceBelt, sinkChest, "iron-plate");
-    orchestrator.registerInserter(inserter);
-
-    // Tick 1: Wakes up, picks 4.
-    orchestrator.tick();
-    expect(inserter.state).toBe(InserterState.Picking);
-    expect(inserter.heldItems).toBe(4);
-
-    // Tick 2: Picks 4.
-    orchestrator.tick();
-    expect(inserter.heldItems).toBe(8);
-
-    // Tick 3: Picks 4.
-    orchestrator.tick();
-    expect(inserter.heldItems).toBe(12);
-
-    // Tick 4: Picks remaining 2. Hand is full! Instantly transitions to Swinging.
-    orchestrator.tick();
-    expect(inserter.heldItems).toBe(14);
-    expect(inserter.state).toBe(InserterState.SwingingForward);
-  });
-  it("Round-Robin: Two inserters compete 4 stack for same output and share perfectly", () => {
-    const orchestrator = new FactorioEngineOrchestrator();
-    const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { "iron-gear-wheel": 100 });
+      tickTimes(inserter, 5);
+
+      expect(machine.inputBuffer["iron-plate"]).toBe(16);
+
+      machine.tick();
+      expect(machine.inputBuffer["iron-plate"]).toBe(14);
 
-    const sourceChest = new Chest("iron-plate");
-    const sinkChestA = new Chest();
-    const sinkChestB = new Chest();
+      tickTimes(inserter, 3);
+      expect(inserter.state).toBe(InserterState.Idle);
 
-    const input = new InserterSimulator(16, sourceChest, machine, "iron-plate");
+      inserter.tick();
+      expect(inserter.state).toBe(InserterState.Idle);
+    });
+  });
 
-    // Both output inserters have a hand size of 4
-    const outA = new InserterSimulator(4, machine, sinkChestA, "iron-gear-wheel");
-    const outB = new InserterSimulator(4, machine, sinkChestB, "iron-gear-wheel");
+  describe("Tick/Phase Mechanics", () => {
+    it("End-to-End: Proves the 1-tick delay caused by Inserters updating before Machines", () => {
+      const { orchestrator, machines, chests, inserters } = new SimulationBuilder()
+        .addChest("source", "iron-plate")
+        .addChest("sink")
+        .addMachine("machine", Recipes.ironGear)
+        .addInserter("input", 2, "source", "machine", "iron-plate")
+        .addInserter("output", 1, "machine", "sink", "iron-gear-wheel")
+        .build();
 
-    orchestrator.registerInserter(input);
-    orchestrator.registerInserter(outA); // A registered first!
-    orchestrator.registerInserter(outB);
-    orchestrator.registerMachine(machine);
+      expect(orchestrator.currentTick).toBe(0);
 
-    // Run until 4 items are produced and extracted
-    orchestrator.tickUntil(() => false, 5 + 30 * 4 + 1);
+      orchestrator.tickUntil(() => inserters.input.state == InserterState.Dropping);
+      expect(orchestrator.currentTick).toBe(4);
 
-    // If build-order was absolute, A would have 4 and B would have 0.
-    // Because of the Round-Robin queue, they should have exactly 2 each!
-    expect(outA.heldItems).toBe(2);
-    expect(outB.heldItems).toBe(2);
+      orchestrator.tickUntil(() => (machines.machine.outputBuffer["iron-gear-wheel"] || 0) >= 1);
+      expect(orchestrator.currentTick).toBe(35);
 
-    orchestrator.tickUntil(() => sinkChestB.receivedCounts["iron-gear-wheel"] == 4);
+      orchestrator.tickUntil(() => inserters.output.heldItems >= 1);
+      expect(orchestrator.currentTick).toBe(36);
 
-    expect(sinkChestB.receivedCounts["iron-gear-wheel"]).toBe(4);
-    expect(outA.heldItems).toBe(0);
-    expect(outB.heldItems).toBe(0);
+      orchestrator.tickUntil(() => (chests.sink.receivedCounts["iron-gear-wheel"] || 0) >= 1);
+      expect(orchestrator.currentTick).toBe(40);
+    });
   });
 
-  it("True Round-Robin: Stack inserters alternate picking from a machine while hovering", () => {
-    const orchestrator = new FactorioEngineOrchestrator();
-
-    // Custom recipe: Produces exactly 2 gears per craft
-    const twoGearRecipe: Recipe = {
-      name: "iron-gear-wheel",
-      energy_required: 0.5,
-      ingredients: [{ type: "item", name: "iron-plate", amount: 2 }],
-      results: [{ type: "item", name: "iron-gear-wheel", amount: 2 }], // Yields 2!
-    } as Recipe;
-
-    // Use quick machine (instant craft, 1 craft per tick)
-    const machine = new MachineSimulator(
-      { machine: { crafting_speed: 30 } as Machine, machineModules: [], beacons: [] } as MachineSetup,
-      twoGearRecipe,
-      {
-        "iron-gear-wheel": 100,
-      },
-    );
-
-    // Infinite input so the machine never stops
-    const sourceChest = new Chest("iron-plate");
-    const sinkChest = new Chest();
-
-    // Input inserter to keep machine stuffed
-    const input = new InserterSimulator(16, sourceChest, machine, "iron-plate");
-
-    // The two competing output inserters
-    const outA = new InserterSimulator(16, machine, sinkChest, "iron-gear-wheel");
-    const outB = new InserterSimulator(16, machine, sinkChest, "iron-gear-wheel");
-
-    // Register: outA gets priority first
-    orchestrator.registerInserter(input);
-    orchestrator.registerInserter(outA);
-    orchestrator.registerInserter(outB);
-    orchestrator.registerMachine(machine);
-
-    // --- Tick 1 ---
-    orchestrator.tick();
-    // Input grabs 16 plates.
-    // --- Tick 6 ---
-    // Fast forward to when input drops items into the machine
-    orchestrator.tickUntil(() => machine.inputBuffer["iron-plate"] > 0);
-    // Machine now has plates, and will craft 2 gears during its phase!
-
-    // --- Tick 7: The Competition Begins ---
-    orchestrator.tick();
-    // Inserter phase: outA wakes up, grabs the 2 gears. outB wakes up, but machine is empty.
-    expect(outA.state).toBe(InserterState.Picking);
-    expect(outB.state).toBe(InserterState.Idle);
-    expect(outA.heldItems).toBe(2);
-    expect(outB.heldItems).toBe(0);
-    // Machine phase: Crafts 2 more gears.
-
-    // --- Tick 8: Alternation! ---
-    orchestrator.tick();
-    // Inserter phase: outB has priority now! B grabs 2 gears. A gets nothing.
-    expect(outA.heldItems).toBe(2);
-    expect(outB.heldItems).toBe(2);
-    // Machine phase: Crafts 2 more gears.
-
-    // --- Tick 9 ---
-    orchestrator.tick();
-    // Inserter phase: outA has priority! A grabs 2 gears (now holding 4).
-    expect(outA.heldItems).toBe(4);
-    expect(outB.heldItems).toBe(2);
-
-    // --- Tick 10 ---
-    orchestrator.tick();
-    // B's turn again.
-    expect(outA.heldItems).toBe(4);
-    expect(outB.heldItems).toBe(4);
-
-    // Run until outA fills its hand (16 items) and transitions to SwingingForward
-    orchestrator.tickUntil(() => outA.state === InserterState.SwingingForward);
-
-    // Because they alternate 2 items at a time, when A hits 16 (and swings),
-    // B should be right behind it holding exactly 14!
-    expect(outA.heldItems).toBe(16);
-    expect(outB.heldItems).toBe(14);
-    expect(outB.state).toBe(InserterState.Picking);
-  });
-  it("Complex Recipe: Advanced Circuits with Productivity and multiple inputs", () => {
-    const orchestrator = new FactorioEngineOrchestrator();
-
-    const advCircuitRecipe: Recipe = {
-      name: "advanced-circuit",
-      energy_required: 6, // 360 ticks
-      ingredients: [
-        { type: "item", name: "plastic-bar", amount: 2 },
-        { type: "item", name: "copper-cable", amount: 4 },
-        { type: "item", name: "electronic-circuit", amount: 2 },
-      ],
-      results: [{ type: "item", name: "advanced-circuit", amount: 1 }],
-    } as Recipe;
-
-    // A fast machine with +40% productivity
-    const advSetup: MachineSetup = {
-      machine: { crafting_speed: 6 } as Machine, // 60 ticks per craft
-      machineModules: [],
-      beacons: [],
-      machineQualityLevel: 0,
-    };
-
-    const machine = new MachineSimulator(advSetup, advCircuitRecipe, { "advanced-circuit": 200 });
-    // Manually inject productivity for test
-    machine.timings.productivityBonus = 0.4;
-    machine.timings.overloadMultiplier = 10;
-
-    const sourcePlastic = new Chest("plastic-bar");
-    const sourceCable = new Chest("copper-cable");
-    const sourceGreen = new Chest("electronic-circuit");
-    const sinkRed = new Chest();
-
-    orchestrator.registerInserter(new InserterSimulator(4, sourcePlastic, machine, "plastic-bar"));
-    orchestrator.registerInserter(new InserterSimulator(8, sourceCable, machine, "copper-cable"));
-    orchestrator.registerInserter(new InserterSimulator(4, sourceGreen, machine, "electronic-circuit"));
-    orchestrator.registerInserter(new InserterSimulator(16, machine, sinkRed, "advanced-circuit"));
-    orchestrator.registerMachine(machine);
-    // Run until 14 Red Chips are produced (10 base + 4 prod bonus)
-    const success = orchestrator.tickUntil(() => (sinkRed.receivedCounts["advanced-circuit"] || 0) >= 14);
-
-    expect(success).toBe(true);
-  });
+  describe("Queue & Round-Robin Behaviors", () => {
+    it("Round-Robin: Two inserters compete for same output and share perfectly", () => {
+      const { orchestrator, chests } = new SimulationBuilder()
+        .addChest("source", "iron-plate")
+        .addChest("sinkA")
+        .addChest("sinkB")
+        .addMachine("machine", Recipes.ironGear)
+        .addInserter("input", 16, "source", "machine", "iron-plate")
+        .addInserter("outA", 1, "machine", "sinkA", "iron-gear-wheel")
+        .addInserter("outB", 1, "machine", "sinkB", "iron-gear-wheel")
+        .build();
+
+      orchestrator.tickUntil(
+        () => (chests.sinkA.receivedCounts["iron-gear-wheel"] || 0) + (chests.sinkB.receivedCounts["iron-gear-wheel"] || 0) === 4
+      );
+
+      expect(chests.sinkA.receivedCounts["iron-gear-wheel"]).toBe(2);
+      expect(chests.sinkB.receivedCounts["iron-gear-wheel"]).toBe(2);
+    });
 
-  it("Performance Stress Test: 1,000 machines for 10,000 ticks", () => {
-    const orchestrator = new FactorioEngineOrchestrator();
+    it("Progressive Belt Pickup: Stack inserter extracts 4 items per tick until full", () => {
+      const { orchestrator, inserters } = new SimulationBuilder()
+        .addBelt("belt", "iron-plate")
+        .addChest("sink")
+        .addInserter("inserter", 14, "belt", "sink", "iron-plate")
+        .build();
 
-    const source = new Chest("iron-plate");
-    const sink = new Chest();
+      const inserter = inserters.inserter;
 
-    // Create a massive factory block
-    for (let i = 0; i < 1000; i++) {
-      const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { "iron-gear-wheel": 100 });
-      orchestrator.registerMachine(machine);
-      orchestrator.registerInserter(new InserterSimulator(16, source, machine, "iron-plate"));
-      orchestrator.registerInserter(new InserterSimulator(16, machine, sink, "iron-gear-wheel"));
-    }
+      orchestrator.tick();
+      expect(inserter.state).toBe(InserterState.Picking);
+      expect(inserter.heldItems).toBe(4);
 
-    const start = performance.now();
+      orchestrator.tick();
+      expect(inserter.heldItems).toBe(8);
 
-    // Run for 10,000 ticks (about 2.7 minutes of real Factorio game time)
-    orchestrator.tickUntil(() => false, 10000);
+      orchestrator.tick();
+      expect(inserter.heldItems).toBe(12);
 
-    const end = performance.now();
-    const durationMs = end - start;
+      orchestrator.tick();
+      expect(inserter.heldItems).toBe(14);
+      expect(inserter.state).toBe(InserterState.SwingingForward);
+    });
 
-    // Ensure the O(1) optimizations keep it fast!
-    // 1000 machines * 2000 inserters * 10,000 ticks = 30 million entity ticks.
-    // Should easily run in under 500ms in Node.js/Vitest.
-    expect(durationMs).toBeLessThan(1000);
+    it("Round-Robin: Two inserters compete 4 stack for same output and share perfectly", () => {
+      const { orchestrator, chests, inserters } = new SimulationBuilder()
+        .addChest("source", "iron-plate")
+        .addChest("sinkA")
+        .addChest("sinkB")
+        .addMachine("machine", Recipes.ironGear)
+        .addInserter("input", 16, "source", "machine", "iron-plate")
+        .addInserter("outA", 4, "machine", "sinkA", "iron-gear-wheel")
+        .addInserter("outB", 4, "machine", "sinkB", "iron-gear-wheel")
+        .build();
 
-    // Verify they actually produced items
-    expect(sink.receivedCounts["iron-gear-wheel"]).toBeGreaterThan(100000);
-  });
-  it("Endgame Setup: Legendary EMP with Beacons perfectly matches batch metrics", () => {
-    const statsSpy = vi.spyOn(statsModule, "computeMachineStats").mockReturnValue({
-      actualCraftingSpeed: 84,
-      productivityBonus: 1.75,
-      singleCraftTicks: 4.2857142857142865,
-      craftsPerSecond: 14,
-      overloadMultiplier: 17,
+      orchestrator.tickUntil(() => false, 5 + 30 * 4 + 1);
+
+      expect(inserters.outA.heldItems).toBe(2);
+      expect(inserters.outB.heldItems).toBe(2);
+
+      orchestrator.tickUntil(() => chests.sinkB.receivedCounts["iron-gear-wheel"] == 4);
+
+      expect(chests.sinkB.receivedCounts["iron-gear-wheel"]).toBe(4);
+      expect(inserters.outA.heldItems).toBe(0);
+      expect(inserters.outB.heldItems).toBe(0);
     });
 
-    const orchestrator = new FactorioEngineOrchestrator();
-
-    const advCircuitRecipe: Recipe = {
-      name: "advanced-circuit",
-      energy_required: 6,
-      ingredients: [
-        { type: "item", name: "plastic-bar", amount: 2 },
-        { type: "item", name: "copper-cable", amount: 4 },
-        { type: "item", name: "electronic-circuit", amount: 2 },
-      ],
-      results: [{ type: "item", name: "advanced-circuit", amount: 1 }],
-    } as Recipe;
-
-    const empSetup: MachineSetup = {
-      machine: { name: "electromagnetic-plant" } as Machine,
-      machineModules: [],
-      beacons: [],
-      machineQualityLevel: 5,
-    };
-
-    // The MachineSimulator will call the spy during its constructor and get our exact timings
-    const machine = new MachineSimulator(empSetup, advCircuitRecipe, { "advanced-circuit": 200 });
-
-    expect((machine as any).outputBlockLimits["advanced-circuit"]).toBe(17);
-
-    const sourcePlastic = new Chest("plastic-bar");
-    const sourceCable = new Chest("copper-cable");
-    const sourceGreen = new Chest("electronic-circuit");
-    const sinkRed = new Chest();
-
-    orchestrator.registerInserter(new InserterSimulator(16, sourcePlastic, machine, "plastic-bar"));
-    orchestrator.registerInserter(new InserterSimulator(16, sourceCable, machine, "copper-cable"));
-    orchestrator.registerInserter(new InserterSimulator(16, sourceGreen, machine, "electronic-circuit"));
-    orchestrator.registerInserter(new InserterSimulator(16, machine, sinkRed, "advanced-circuit"));
-    orchestrator.registerMachine(machine);
-
-    // Run until chest receives the batch size of 176
-    const success = orchestrator.tickUntil(() => (sinkRed.receivedCounts["advanced-circuit"] || 0) >= 176);
-    expect(success).toBe(true);
-
-    expect(sinkRed.receivedCounts["advanced-circuit"]).toBe(176);
-    expect(sourcePlastic.extractedCounts["plastic-bar"]).toBeGreaterThanOrEqual(128);
-    expect(sourceCable.extractedCounts["copper-cable"]).toBeGreaterThanOrEqual(256);
-    expect(sourceGreen.extractedCounts["electronic-circuit"]).toBeGreaterThanOrEqual(128);
-
-    // Timeline Duration: Base 275 + 5 + 4
-    expect(orchestrator.currentTick).toBe(284);
-
-    statsSpy.mockRestore();
-  });
-  it("Optimized Clock: Pub/Sub row mapping controls inserter activation with zero scan overhead", () => {
-    const orchestrator = new FactorioEngineOrchestrator();
-
-    const machine = new MachineSimulator(
-      {
-        machine: { crafting_speed: 1 } as Machine,
-        machineQualityLevel: 0,
-        machineModules: [],
-        beacons: [],
-      } as MachineSetup,
-      basicRecipe,
-      {
-        "iron-gear-wheel": 100,
-      },
-    );
-    const source = new Chest("iron-plate");
-    const sink = new Chest();
-
-    // Register Clock Rows (43-tick cycle)
-
-    orchestrator.registerClockRow({ id: "row_input" } as ClockRow, [{ start: 0, duration: 8 } as ClockBlock], 43);
-    orchestrator.registerClockRow({ id: "row_output" } as ClockRow, [{ start: 35, duration: 8 } as ClockBlock], 43);
-
-    // Create Inserters
-    const inputInserter = new InserterSimulator(2, source, machine, "iron-plate");
-    const outputInserter = new InserterSimulator(1, machine, sink, "iron-gear-wheel");
-
-    // Bind Inserters to Rows via Pub/Sub
-    orchestrator.bindInserterToRow(inputInserter, "row_input");
-    orchestrator.bindInserterToRow(outputInserter, "row_output");
-
-    orchestrator.registerInserter(inputInserter);
-    orchestrator.registerInserter(outputInserter);
-    orchestrator.registerMachine(machine);
-
-    orchestrator.tickUntil(() => false, 43);
-
-    // 5. Validate execution
-    expect(sink.receivedCounts["iron-gear-wheel"]).toBe(1);
-    expect(inputInserter.swingCount).toBe(1);
-    expect(outputInserter.swingCount).toBe(1);
+    it("True Round-Robin: Stack inserters alternate picking from a machine while hovering", () => {
+      const { orchestrator, machines, inserters } = new SimulationBuilder()
+        .addChest("source", "iron-plate")
+        .addChest("sink")
+        .addMachine("machine", Recipes.twoGear, StandardSetups.instantAssembler)
+        .addInserter("input", 16, "source", "machine", "iron-plate")
+        .addInserter("outA", 16, "machine", "sink", "iron-gear-wheel")
+        .addInserter("outB", 16, "machine", "sink", "iron-gear-wheel")
+        .build();
+
+      orchestrator.tick();
+      orchestrator.tickUntil(() => machines.machine.inputBuffer["iron-plate"] > 0);
+
+      orchestrator.tick();
+      expect(inserters.outA.state).toBe(InserterState.Picking);
+      expect(inserters.outB.state).toBe(InserterState.Idle);
+      expect(inserters.outA.heldItems).toBe(2);
+      expect(inserters.outB.heldItems).toBe(0);
+
+      orchestrator.tick();
+      expect(inserters.outA.heldItems).toBe(2);
+      expect(inserters.outB.heldItems).toBe(2);
+
+      orchestrator.tick();
+      expect(inserters.outA.heldItems).toBe(4);
+      expect(inserters.outB.heldItems).toBe(2);
+
+      orchestrator.tick();
+      expect(inserters.outA.heldItems).toBe(4);
+      expect(inserters.outB.heldItems).toBe(4);
+
+      orchestrator.tickUntil(() => inserters.outA.state === InserterState.SwingingForward);
+
+      expect(inserters.outA.heldItems).toBe(16);
+      expect(inserters.outB.heldItems).toBe(14);
+      expect(inserters.outB.state).toBe(InserterState.Picking);
+    });
   });
 
-  it("Unclocked Baseline: 3:2 Copper to GC stabilizes and executes perfect swing ratios", () => {
-    const orchestrator = new FactorioEngineOrchestrator();
-
-    const copperCableRecipe: Recipe = {
-      name: "copper-cable",
-      energy_required: 0.5,
-      ingredients: [{ type: "item", name: "copper-plate", amount: 1 }],
-      results: [{ type: "item", name: "copper-cable", amount: 2 }],
-    } as Recipe;
-
-    const greenCircuitRecipe: Recipe = {
-      name: "electronic-circuit",
-      energy_required: 0.5,
-      ingredients: [
-        { type: "item", name: "iron-plate", amount: 1 },
-        { type: "item", name: "copper-cable", amount: 3 },
-      ],
-      results: [{ type: "item", name: "electronic-circuit", amount: 1 }],
-    } as Recipe;
-
-    const assemblerSetup: MachineSetup = {
-      machine: { name: "assembling-machine-2", crafting_speed: 1 } as Machine,
-      machineModules: [],
-      beacons: [],
-      machineQualityLevel: 0,
-    };
-
-    const cop1 = new MachineSimulator(assemblerSetup, copperCableRecipe, { "copper-cable": 200 });
-    const cop2 = new MachineSimulator(assemblerSetup, copperCableRecipe, { "copper-cable": 200 });
-    const cop3 = new MachineSimulator(assemblerSetup, copperCableRecipe, { "copper-cable": 200 });
-
-    const circ1 = new MachineSimulator(assemblerSetup, greenCircuitRecipe, { "electronic-circuit": 200 });
-    const circ2 = new MachineSimulator(assemblerSetup, greenCircuitRecipe, { "electronic-circuit": 200 });
-
-    const sourceCopper = new Chest("copper-plate");
-    const sourceIron = new Chest("iron-plate");
-    const sinkGreenChips = new Chest();
-
-    // ALL inserters set to Stack Size 16
-    const inCop1 = new InserterSimulator(16, sourceCopper, cop1, "copper-plate");
-    const inCop2 = new InserterSimulator(16, sourceCopper, cop2, "copper-plate");
-    const inCop3 = new InserterSimulator(16, sourceCopper, cop3, "copper-plate");
-
-    const inIron1 = new InserterSimulator(16, sourceIron, circ1, "iron-plate");
-    const inIron2 = new InserterSimulator(16, sourceIron, circ2, "iron-plate");
-
-    // Direct insertions (Stack size 16)
-    const mid1 = new InserterSimulator(16, cop1, circ1, "copper-cable");
-    const mid2a = new InserterSimulator(16, cop2, circ1, "copper-cable");
-    const mid2b = new InserterSimulator(16, cop2, circ2, "copper-cable");
-    const mid3 = new InserterSimulator(16, cop3, circ2, "copper-cable");
-
-    const outCirc1 = new InserterSimulator(16, circ1, sinkGreenChips, "electronic-circuit");
-    const outCirc2 = new InserterSimulator(16, circ2, sinkGreenChips, "electronic-circuit");
-
-    const allInserters = [inCop1, inCop2, inCop3, inIron1, inIron2, mid1, mid2a, mid2b, mid3, outCirc1, outCirc2];
-
-    allInserters.forEach((ins) => orchestrator.registerInserter(ins));
-    [cop1, cop2, cop3, circ1, circ2].forEach((m) => orchestrator.registerMachine(m));
-
-    // --- PHASE 1: STABILIZATION ---
-    // 16 items * 30 ticks = 488 ticks per complete buffer cycle
-    orchestrator.tickUntil(() => false, 5000);
-
-    // Reset all swing counters to 0 to prepare for the measurement window
-    allInserters.forEach((ins) => (ins.swingCount = 0));
-
-    // --- PHASE 2: MEASUREMENT WINDOW ---
-    // Run for exactly 1440 ticks.
-    // Speed 1 machine = 30 ticks per craft. 1440 / 30 = 48 crafts perfectly.
-    // 48 crafts * 1 plate = 48 plates. 48 / 16 stack size = exactly 3 swings!
-    const targetTick = orchestrator.currentTick + 1440;
-    orchestrator.tickUntil(() => false, targetTick);
-
-    // Validate Outputs (3 swings each = 48 chips per machine)
-    expect(outCirc1.swingCount).toBe(3);
-    expect(outCirc2.swingCount).toBe(3);
-
-    // Validate Iron Inputs (3 swings each = 48 plates per machine)
-    expect(inIron1.swingCount).toBe(3);
-    expect(inIron2.swingCount).toBe(3);
-
-    // Validate Copper Inputs (3 swings each = 48 plates per machine)
-    expect([inCop1.swingCount, inCop2.swingCount, inCop3.swingCount]).toStrictEqual([3, 3, 3]);
-  });
+  describe("Complex Layouts & Clocking", () => {
+    it("Complex Recipe: Advanced Circuits with Productivity and multiple inputs", () => {
+      const { orchestrator, machines, chests } = new SimulationBuilder()
+        .addChest("srcPlastic", "plastic-bar")
+        .addChest("srcCable", "copper-cable")
+        .addChest("srcGreen", "electronic-circuit")
+        .addChest("sinkRed")
+        .addMachine("machine", Recipes.advancedCircuit, StandardSetups.empSpeed)
+        .addInserter("inPlastic", 4, "srcPlastic", "machine", "plastic-bar")
+        .addInserter("inCable", 8, "srcCable", "machine", "copper-cable")
+        .addInserter("inGreen", 4, "srcGreen", "machine", "electronic-circuit")
+        .addInserter("outRed", 16, "machine", "sinkRed", "advanced-circuit")
+        .build();
+
+      machines.machine.timings.productivityBonus = 0.4;
+      machines.machine.timings.overloadMultiplier = 10;
+
+      const success = orchestrator.tickUntil(() => (chests.sinkRed.receivedCounts["advanced-circuit"] || 0) >= 14);
+      expect(success).toBe(true);
+    });
 
-  it("Clocked Baseline: 3:2 setup with staggered shared-inserter rows and exact 8-tick windows", () => {
-    const orchestrator = new FactorioEngineOrchestrator();
-    const stackSizes = {
-      "copper-cable": 200,
-      "electronic-circuit": 200,
-    };
-
-    const copperCableRecipe: Recipe = {
-      name: "copper-cable",
-      energy_required: 0.5,
-      ingredients: [{ type: "item", name: "copper-plate", amount: 1 }],
-      results: [{ type: "item", name: "copper-cable", amount: 2 }],
-    } as Recipe;
-
-    const greenCircuitRecipe: Recipe = {
-      name: "electronic-circuit",
-      energy_required: 0.5,
-      ingredients: [
-        { type: "item", name: "iron-plate", amount: 1 },
-        { type: "item", name: "copper-cable", amount: 3 },
-      ],
-      results: [{ type: "item", name: "electronic-circuit", amount: 1 }],
-    } as Recipe;
-
-    const assemblerSetup: MachineSetup = {
-      machine: { name: "assembling-machine-2", crafting_speed: 10 } as Machine,
-      machineModules: [],
-      beacons: [],
-      machineQualityLevel: 0,
-    };
-
-    const cop1 = new MachineSimulator(assemblerSetup, copperCableRecipe, stackSizes);
-    const cop2 = new MachineSimulator(assemblerSetup, copperCableRecipe, stackSizes);
-    const cop3 = new MachineSimulator(assemblerSetup, copperCableRecipe, stackSizes);
-
-    const circ1 = new MachineSimulator(assemblerSetup, greenCircuitRecipe, stackSizes);
-    const circ2 = new MachineSimulator(assemblerSetup, greenCircuitRecipe, stackSizes);
-
-    const sourceCopper = new Chest("copper-plate");
-    const sourceIron = new Chest("iron-plate");
-    const sinkGreenChips = new Chest();
-
-    // --- MATHEMATICAL CLOCK SETUP ---
-    // Cycle: 480 ticks. 16 crafts per cycle.
-
-    // Inputs (Copper/Iron): 16 items needed = 1 swing per cycle.
-    const rowInputs: ClockRow = { id: "row_inputs", name: "Inputs", signals: [], stackSize: 16, inserterCount: 5 };
-    const rowMidOuter: ClockRow = {
-      id: "row_mid_outer",
-      name: "Mid Outer",
-      signals: [],
-      stackSize: 16,
-      inserterCount: 2,
-    };
-    const rowMidInner: ClockRow = {
-      id: "row_mid_inner",
-      name: "Mid Inner",
-      signals: [],
-      stackSize: 16,
-      inserterCount: 2,
-    };
-    const rowOutputs: ClockRow = { id: "row_outputs", name: "Outputs", signals: [], stackSize: 16, inserterCount: 2 };
-
-    orchestrator.registerClockRow(
-      rowInputs,
-      [{ id: "b1", rowId: "row_inputs", presetId: "custom", start: 1, duration: 8, count: 16 }],
-      480,
-    );
-
-    // Outer Mid Inserters (cop1 -> circ1, cop3 -> circ2): 32 cables generated = 2 swings per cycle.
-    orchestrator.registerClockRow(
-      rowMidOuter,
-      [
-        { id: "b2", rowId: "row_mid_outer", presetId: "custom", start: 1, duration: 8, count: 16 },
-        { id: "b3", rowId: "row_mid_outer", presetId: "custom", start: 321, duration: 8, count: 16 },
-      ],
-      480,
-    );
-    orchestrator.registerClockRow(
-      rowMidInner,
-      [{ id: "b4", rowId: "row_mid_inner", presetId: "custom", start: 161, duration: 8, count: 16 }],
-      480,
-    );
-
-    orchestrator.registerClockRow(
-      rowOutputs,
-      [{ id: "b5", rowId: "row_outputs", presetId: "custom", start: 0, duration: 16, count: 16 }],
-      480,
-    );
-
-    // Instantiate Inserters (Stack Size 16)
-    const inCop1 = new InserterSimulator(16, sourceCopper, cop1, "copper-plate");
-    const inCop2 = new InserterSimulator(16, sourceCopper, cop2, "copper-plate");
-    const inCop3 = new InserterSimulator(16, sourceCopper, cop3, "copper-plate");
-    const inIron1 = new InserterSimulator(16, sourceIron, circ1, "iron-plate");
-    const inIron2 = new InserterSimulator(16, sourceIron, circ2, "iron-plate");
-
-    const mid1 = new InserterSimulator(16, cop1, circ1, "copper-cable");
-    const mid2a = new InserterSimulator(16, cop2, circ1, "copper-cable"); // Inner A
-    const mid2b = new InserterSimulator(16, cop2, circ2, "copper-cable"); // Inner B
-    const mid3 = new InserterSimulator(16, cop3, circ2, "copper-cable");
-
-    const outCirc1 = new InserterSimulator(16, circ1, sinkGreenChips, "electronic-circuit");
-    const outCirc2 = new InserterSimulator(16, circ2, sinkGreenChips, "electronic-circuit");
-
-    [inCop1, inCop2, inCop3, inIron1, inIron2].forEach((ins) => orchestrator.bindInserterToRow(ins, "row_inputs"));
-    [mid1, mid3].forEach((ins) => orchestrator.bindInserterToRow(ins, "row_mid_outer"));
-    orchestrator.bindInserterToRow(mid2a, "row_mid_inner");
-    orchestrator.bindInserterToRow(mid2b, "row_mid_inner");
-    [outCirc1, outCirc2].forEach((ins) => orchestrator.bindInserterToRow(ins, "row_outputs"));
-
-    const allInserters = [inCop1, inCop2, inCop3, inIron1, inIron2, mid1, mid2a, mid2b, mid3, outCirc1, outCirc2];
-    allInserters.forEach((ins) => orchestrator.registerInserter(ins));
-    [cop1, cop2, cop3, circ1, circ2].forEach((m) => orchestrator.registerMachine(m));
-
-    // --- PHASE 1: STABILIZATION ---
-    orchestrator.tickUntil(() => false, 5000);
-    allInserters.forEach((ins) => (ins.swingCount = 0));
-    const startTarget = orchestrator.currentTick;
-
-    // --- PHASE 2: MEASUREMENT WINDOW ---
-    orchestrator.tickUntil(() => false, startTarget + 1440);
-
-    // --- VALIDATION ---
-    expect(inCop1.swingCount).toBe(3);
-    expect(inCop2.swingCount).toBe(3);
-    expect(inIron1.swingCount).toBe(3);
-    expect(mid1.swingCount).toBe(6);
-    expect(mid3.swingCount).toBe(6);
-    expect(mid2a.swingCount).toBe(3);
-    expect(mid2b.swingCount).toBe(3);
-    expect(outCirc1.swingCount).toBe(3);
-    expect(outCirc2.swingCount).toBe(3);
-
-    // Output yield matches maximum physical theoretical limit
-    // 3 swings * 16 items * 2 machines = 96
-    const outputAfterPhase1 = sinkGreenChips.receivedCounts["electronic-circuit"];
-    expect(outputAfterPhase1 - (sinkGreenChips.extractedCounts["electronic-circuit"] || 0)).toBeGreaterThanOrEqual(96);
-  });
+    it("Performance Stress Test: 1,000 machines for 10,000 ticks", () => {
+      const builder = new SimulationBuilder()
+        .addChest("source", "iron-plate")
+        .addChest("sink");
+
+      for (let i = 0; i < 1000; i++) {
+        builder
+          .addMachine(`machine${i}`, Recipes.ironGear)
+          .addInserter(`in${i}`, 16, "source", `machine${i}`, "iron-plate")
+          .addInserter(`out${i}`, 16, `machine${i}`, "sink", "iron-gear-wheel");
+      }
+
+      const { orchestrator, chests } = builder.build();
+
+      const start = performance.now();
+      orchestrator.tickUntil(() => false, 10000);
+      const end = performance.now();
+      
+      expect(end - start).toBeLessThan(1000);
+      expect(chests.sink.receivedCounts["iron-gear-wheel"]).toBeGreaterThan(100000);
+    });
 
-  // src/engine/simulator.test.ts
+    it("Endgame Setup: Legendary EMP with Beacons perfectly matches batch metrics", () => {
+      const statsSpy = vi.spyOn(statsModule, "computeMachineStats").mockReturnValue({
+        actualCraftingSpeed: 84,
+        productivityBonus: 1.75,
+        singleCraftTicks: 4.2857142857142865,
+        craftsPerSecond: 14,
+        overloadMultiplier: 17,
+      });
+
+      const { orchestrator, machines, chests } = new SimulationBuilder()
+        .addChest("srcPlastic", "plastic-bar")
+        .addChest("srcCable", "copper-cable")
+        .addChest("srcGreen", "electronic-circuit")
+        .addChest("sinkRed")
+        .addMachine("machine", Recipes.advancedCircuit, StandardSetups.empSpeed)
+        .addInserter("inPlastic", 16, "srcPlastic", "machine", "plastic-bar")
+        .addInserter("inCable", 16, "srcCable", "machine", "copper-cable")
+        .addInserter("inGreen", 16, "srcGreen", "machine", "electronic-circuit")
+        .addInserter("outRed", 16, "machine", "sinkRed", "advanced-circuit")
+        .build();
+
+      expect((machines.machine as any).outputBlockLimits["advanced-circuit"]).toBe(17);
+
+      const success = orchestrator.tickUntil(() => (chests.sinkRed.receivedCounts["advanced-circuit"] || 0) >= 176);
+      expect(success).toBe(true);
+
+      expect(chests.sinkRed.receivedCounts["advanced-circuit"]).toBe(176);
+      expect(chests.srcPlastic.extractedCounts["plastic-bar"]).toBeGreaterThanOrEqual(128);
+      expect(chests.srcCable.extractedCounts["copper-cable"]).toBeGreaterThanOrEqual(256);
+      expect(chests.srcGreen.extractedCounts["electronic-circuit"]).toBeGreaterThanOrEqual(128);
+
+      expect(orchestrator.currentTick).toBe(284);
+      statsSpy.mockRestore();
+    });
 
-  it("Advanced Filtering: Filter Inserter dynamically changes target on a mixed belt", () => {
-    const orchestrator = new FactorioEngineOrchestrator();
+    it("Optimized Clock: Pub/Sub row mapping controls inserter activation with zero scan overhead", () => {
+      const builder = new SimulationBuilder()
+        .addChest("source", "iron-plate")
+        .addChest("sink")
+        .addMachine("machine", Recipes.ironGear)
+        .addInserter("in", 2, "source", "machine", "iron-plate")
+        .addInserter("out", 1, "machine", "sink", "iron-gear-wheel");
 
-    const mixedBelt = new Belt("iron-plate", "copper-plate");
-    const sortingChest = new Chest();
-    const filterInserter = new FilterableInserterSimulator(16, mixedBelt, sortingChest);
-    filterInserter.useDynamicFilters = true;
+      const { orchestrator, chests, inserters } = builder.build();
 
-    filterInserter.updateDynamicFilters(["copper-plate"]);
+      orchestrator.registerClockRow({ id: "row_input" } as ClockRow, [{ start: 0, duration: 8 } as ClockBlock], 43);
+      orchestrator.registerClockRow({ id: "row_output" } as ClockRow, [{ start: 35, duration: 8 } as ClockBlock], 43);
 
-    orchestrator.registerInserter(filterInserter);
-    orchestrator.tick();
+      builder.bindToClock("in", "row_input").bindToClock("out", "row_output");
 
-    expect(filterInserter.currentTargetItem).toBe("copper-plate");
-    expect(filterInserter.state).toBe(1);
-    expect(filterInserter.heldItems).toBe(4);
-    orchestrator.tickUntil(() => false, 100);
-    expect(mixedBelt.extractedCounts["copper-plate"]).toBeGreaterThan(0);
-    expect(mixedBelt.extractedCounts["iron-plate"]).toBeUndefined();
-    expect(sortingChest.receivedCounts["copper-plate"]).toBeGreaterThan(0);
-    expect(sortingChest.receivedCounts["iron-plate"]).toBeUndefined();
+      orchestrator.tickUntil(() => false, 43);
 
-    // The Combinator Flips! ---
-    filterInserter.updateDynamicFilters(["iron-plate"]);
+      expect(chests.sink.receivedCounts["iron-gear-wheel"]).toBe(1);
+      expect(inserters.in.swingCount).toBe(1);
+      expect(inserters.out.swingCount).toBe(1);
+    });
 
-    // Run for another 100 ticks
-    orchestrator.tickUntil(() => false, 200);
+    it("Unclocked Baseline: 3:2 Copper to GC stabilizes and executes perfect swing ratios", () => {
+      const { orchestrator, inserters } = new SimulationBuilder()
+        .addChest("cu_src", "copper-plate").addChest("fe_src", "iron-plate").addChest("gc_sink")
+        .addMachine("cop1", Recipes.copperCable).addMachine("cop2", Recipes.copperCable).addMachine("cop3", Recipes.copperCable)
+        .addMachine("circ1", Recipes.greenCircuit).addMachine("circ2", Recipes.greenCircuit)
+        .addInserter("inCop1", 16, "cu_src", "cop1", "copper-plate")
+        .addInserter("inCop2", 16, "cu_src", "cop2", "copper-plate")
+        .addInserter("inCop3", 16, "cu_src", "cop3", "copper-plate")
+        .addInserter("inIron1", 16, "fe_src", "circ1", "iron-plate")
+        .addInserter("inIron2", 16, "fe_src", "circ2", "iron-plate")
+        .addInserter("mid1", 16, "cop1", "circ1", "copper-cable")
+        .addInserter("mid2a", 16, "cop2", "circ1", "copper-cable")
+        .addInserter("mid2b", 16, "cop2", "circ2", "copper-cable")
+        .addInserter("mid3", 16, "cop3", "circ2", "copper-cable")
+        .addInserter("outCirc1", 16, "circ1", "gc_sink", "electronic-circuit")
+        .addInserter("outCirc2", 16, "circ2", "gc_sink", "electronic-circuit")
+        .build();
+
+      orchestrator.tickUntil(() => false, 5000);
+      Object.values(inserters).forEach((ins) => (ins.swingCount = 0));
+
+      const targetTick = orchestrator.currentTick + 1440;
+      orchestrator.tickUntil(() => false, targetTick);
+
+      expect(inserters.outCirc1.swingCount).toBe(3);
+      expect(inserters.outCirc2.swingCount).toBe(3);
+      expect(inserters.inIron1.swingCount).toBe(3);
+      expect(inserters.inIron2.swingCount).toBe(3);
+      expect([inserters.inCop1.swingCount, inserters.inCop2.swingCount, inserters.inCop3.swingCount]).toStrictEqual([3, 3, 3]);
+    });
 
-    // It should now have extracted Iron as well!
-    expect(mixedBelt.extractedCounts["iron-plate"]).toBeGreaterThan(0);
-    expect(sortingChest.receivedCounts["iron-plate"]).toBeGreaterThan(0);
+    it("Clocked Baseline: 3:2 setup with staggered shared-inserter rows and exact 8-tick windows", () => {
+      const builder = new SimulationBuilder()
+        .addChest("cu_src", "copper-plate").addChest("fe_src", "iron-plate").addChest("gc_sink")
+        .addMachine("cop1", Recipes.copperCable, StandardSetups.assembler2)
+        .addMachine("cop2", Recipes.copperCable, StandardSetups.assembler2)
+        .addMachine("cop3", Recipes.copperCable, StandardSetups.assembler2)
+        .addMachine("circ1", Recipes.greenCircuit, StandardSetups.assembler2)
+        .addMachine("circ2", Recipes.greenCircuit, StandardSetups.assembler2)
+        .addInserter("inCop1", 16, "cu_src", "cop1", "copper-plate")
+        .addInserter("inCop2", 16, "cu_src", "cop2", "copper-plate")
+        .addInserter("inCop3", 16, "cu_src", "cop3", "copper-plate")
+        .addInserter("inIron1", 16, "fe_src", "circ1", "iron-plate")
+        .addInserter("inIron2", 16, "fe_src", "circ2", "iron-plate")
+        .addInserter("mid1", 16, "cop1", "circ1", "copper-cable")
+        .addInserter("mid2a", 16, "cop2", "circ1", "copper-cable")
+        .addInserter("mid2b", 16, "cop2", "circ2", "copper-cable")
+        .addInserter("mid3", 16, "cop3", "circ2", "copper-cable")
+        .addInserter("outCirc1", 16, "circ1", "gc_sink", "electronic-circuit")
+        .addInserter("outCirc2", 16, "circ2", "gc_sink", "electronic-circuit");
+
+      const { orchestrator, chests, inserters } = builder.build();
+
+      const rowInputs: ClockRow = { id: "row_inputs", name: "Inputs", signals: [], stackSize: 16, inserterCount: 5 };
+      const rowMidOuter: ClockRow = { id: "row_mid_outer", name: "Mid Outer", signals: [], stackSize: 16, inserterCount: 2 };
+      const rowMidInner: ClockRow = { id: "row_mid_inner", name: "Mid Inner", signals: [], stackSize: 16, inserterCount: 2 };
+      const rowOutputs: ClockRow = { id: "row_outputs", name: "Outputs", signals: [], stackSize: 16, inserterCount: 2 };
+
+      orchestrator.registerClockRow(rowInputs, [{ id: "b1", rowId: "row_inputs", presetId: "custom", start: 1, duration: 8, count: 16 }], 480);
+      orchestrator.registerClockRow(rowMidOuter, [
+        { id: "b2", rowId: "row_mid_outer", presetId: "custom", start: 1, duration: 8, count: 16 },
+        { id: "b3", rowId: "row_mid_outer", presetId: "custom", start: 321, duration: 8, count: 16 }
+      ], 480);
+      orchestrator.registerClockRow(rowMidInner, [{ id: "b4", rowId: "row_mid_inner", presetId: "custom", start: 161, duration: 8, count: 16 }], 480);
+      orchestrator.registerClockRow(rowOutputs, [{ id: "b5", rowId: "row_outputs", presetId: "custom", start: 0, duration: 16, count: 16 }], 480);
+
+      ["inCop1", "inCop2", "inCop3", "inIron1", "inIron2"].forEach(id => builder.bindToClock(id, "row_inputs"));
+      ["mid1", "mid3"].forEach(id => builder.bindToClock(id, "row_mid_outer"));
+      ["mid2a", "mid2b"].forEach(id => builder.bindToClock(id, "row_mid_inner"));
+      ["outCirc1", "outCirc2"].forEach(id => builder.bindToClock(id, "row_outputs"));
+
+      orchestrator.tickUntil(() => false, 5000);
+      Object.values(inserters).forEach((ins) => (ins.swingCount = 0));
+      const startTarget = orchestrator.currentTick;
+
+      orchestrator.tickUntil(() => false, startTarget + 1440);
+
+      expect(inserters.inCop1.swingCount).toBe(3);
+      expect(inserters.inCop2.swingCount).toBe(3);
+      expect(inserters.inIron1.swingCount).toBe(3);
+      expect(inserters.mid1.swingCount).toBe(6);
+      expect(inserters.mid3.swingCount).toBe(6);
+      expect(inserters.mid2a.swingCount).toBe(3);
+      expect(inserters.mid2b.swingCount).toBe(3);
+      expect(inserters.outCirc1.swingCount).toBe(3);
+      expect(inserters.outCirc2.swingCount).toBe(3);
+
+      const outputAfterPhase1 = chests.gc_sink.receivedCounts["electronic-circuit"];
+      expect(outputAfterPhase1 - (chests.gc_sink.extractedCounts["electronic-circuit"] || 0)).toBeGreaterThanOrEqual(96);
+    });
   });
 
-  it("Advanced Filtering: Static multi-item filtering respects item locking", () => {
-    const orchestrator = new FactorioEngineOrchestrator();
-    const mixedBelt = new Belt("iron-plate", "copper-plate");
-    const sortingChest = new Chest();
+  describe("Filtering Logic", () => {
+    it("Advanced Filtering: Filter Inserter dynamically changes target on a mixed belt", () => {
+      const { orchestrator, belts, chests, inserters } = new SimulationBuilder()
+        .addBelt("belt", "iron-plate", "copper-plate")
+        .addChest("sink")
+        .addInserter("filterIns", 16, "belt", "sink", undefined, true)
+        .build();
+
+      const filterInserter = inserters.filterIns as any;
+      filterInserter.updateDynamicFilters(["copper-plate"]);
+
+      orchestrator.tick();
+
+      expect(filterInserter.currentTargetItem).toBe("copper-plate");
+      expect(filterInserter.state).toBe(1);
+      expect(filterInserter.heldItems).toBe(4);
+      
+      orchestrator.tickUntil(() => false, 100);
+      
+      expect(belts.belt.extractedCounts["copper-plate"]).toBeGreaterThan(0);
+      expect(belts.belt.extractedCounts["iron-plate"]).toBeUndefined();
+      expect(chests.sink.receivedCounts["copper-plate"]).toBeGreaterThan(0);
+      expect(chests.sink.receivedCounts["iron-plate"]).toBeUndefined();
+
+      filterInserter.updateDynamicFilters(["iron-plate"]);
+      orchestrator.tickUntil(() => false, 200);
+
+      expect(belts.belt.extractedCounts["iron-plate"]).toBeGreaterThan(0);
+      expect(chests.sink.receivedCounts["iron-plate"]).toBeGreaterThan(0);
+    });
 
-    // Configure static filters for BOTH Iron and Copper
-    const filterInserter = new FilterableInserterSimulator(16, mixedBelt, sortingChest, ["iron-plate", "copper-plate"]);
-    orchestrator.registerInserter(filterInserter);
+    it("Advanced Filtering: Static multi-item filtering respects item locking", () => {
+      const { orchestrator, inserters } = new SimulationBuilder()
+        .addBelt("belt", "iron-plate", "copper-plate")
+        .addChest("sink")
+        .addInserter("filterIns", 16, "belt", "sink", ["iron-plate", "copper-plate"])
+        .build();
 
-    // Tick 1: It evaluates canWakeUp(). Iron is first in the list, so it locks onto Iron!
-    orchestrator.tick();
+      const filterInserter = inserters.filterIns as any;
 
-    expect(filterInserter.state).toBe(InserterState.Picking);
-    expect(filterInserter.currentTargetItem).toBe("iron-plate"); // Locked!
-    expect(filterInserter.heldItems).toBe(4); // Belt pickup rate is 4
+      orchestrator.tick();
 
-    // Tick 2: It continues picking Iron. It does NOT mix Copper into the hand!
-    orchestrator.tick();
-    expect(filterInserter.heldItems).toBe(8);
-    expect(filterInserter.currentTargetItem).toBe("iron-plate");
-  });
+      expect(filterInserter.state).toBe(InserterState.Picking);
+      expect(filterInserter.currentTargetItem).toBe("iron-plate");
+      expect(filterInserter.heldItems).toBe(4);
 
-  it("Advanced Filtering: Single multi-filter inserter autonomously feeds a Red Science machine", () => {
-    const orchestrator = new FactorioEngineOrchestrator();
-
-    // The Red Science Recipe
-    const redScienceRecipe: Recipe = {
-      name: "automation-science-pack",
-      energy_required: 5, // 300 ticks per craft at Speed 1
-      ingredients: [
-        { name: "copper-plate", amount: 1, type: "item" },
-        { name: "iron-gear-wheel", amount: 1, type: "item" },
-      ],
-      results: [{ type: "item", name: "automation-science-pack", amount: 1 }],
-    } as Recipe;
-
-    const assemblerSetup: MachineSetup = {
-      machine: { name: "assembling-machine-2", crafting_speed: 1 } as Machine,
-      machineModules: [],
-      beacons: [],
-      machineQualityLevel: 0,
-    };
-
-    // Setup the Machine and the Mixed Input Belt
-    const machine = new MachineSimulator(assemblerSetup, redScienceRecipe, { "automation-science-pack": 100 });
-    const mixedBelt = new Belt("copper-plate", "iron-gear-wheel");
-    const sinkChest = new Chest();
-
-    // A single Filter Inserter configured to grab BOTH ingredients!
-    // We use a small hand size (4) to force it to make multiple trips.
-    const smartInput = new FilterableInserterSimulator(4, mixedBelt, machine, ["copper-plate", "iron-gear-wheel"]);
-    const output = new InserterSimulator(4, machine, sinkChest, "automation-science-pack");
-
-    orchestrator.registerInserter(smartInput);
-    orchestrator.registerMachine(machine);
-    orchestrator.registerInserter(output);
-
-    // Run the simulation
-    // It takes 300 ticks for a single craft.
-    const targetTick = orchestrator.currentTick + 300 * 5;
-    orchestrator.tickUntil(() => false, targetTick);
-
-    // --- VALIDATION ---
-
-    // The inserter MUST have extracted both items from the mixed belt.
-    // If it had locked up trying to stuff infinite copper, the gear count would be undefined/0.
-    expect(mixedBelt.extractedCounts["copper-plate"]).toBeGreaterThan(0);
-    expect(mixedBelt.extractedCounts["iron-gear-wheel"]).toBeGreaterThan(0);
-
-    expect(sinkChest.receivedCounts["automation-science-pack"]).toBeGreaterThanOrEqual(2);
-
-    // The extracted copper should not wildly exceed the extracted gears,
-    // proving the machine's buffer choked the copper input and forced the inserter to switch.
-    // They should be reasonably close to each other (within 1 hand size margin)
-    const copperPulled = mixedBelt.extractedCounts["copper-plate"];
-    const gearsPulled = mixedBelt.extractedCounts["iron-gear-wheel"];
-    expect(Math.abs(copperPulled - gearsPulled)).toBeLessThanOrEqual(4);
-  });
-  it("Advanced dynamic Filtering: Single multi-filter inserter autonomously feeds a Red Science machine", () => {
-    const orchestrator = new FactorioEngineOrchestrator();
-
-    // The Red Science Recipe
-    const redScienceRecipe: Recipe = {
-      name: "automation-science-pack",
-      energy_required: 5, // 300 ticks per craft at Speed 1
-      ingredients: [
-        { name: "copper-plate", amount: 1, type: "item" },
-        { name: "iron-gear-wheel", amount: 1, type: "item" },
-      ],
-      results: [{ type: "item", name: "automation-science-pack", amount: 1 }],
-    } as Recipe;
-
-    const assemblerSetup: MachineSetup = {
-      machine: { name: "assembling-machine-2", crafting_speed: 1 } as Machine,
-      machineModules: [],
-      beacons: [],
-      machineQualityLevel: 0,
-    };
-
-    // Setup the Machine and the Mixed Input Belt
-    const machine = new MachineSimulator(assemblerSetup, redScienceRecipe, { "automation-science-pack": 100 });
-    const mixedBelt = new Belt("copper-plate", "iron-gear-wheel");
-    const sinkChest = new Chest();
-
-    // A single Filter Inserter configured to grab BOTH ingredients!
-    // We use a small hand size (4) to force it to make multiple trips.
-    const smartInput = new FilterableInserterSimulator(4, mixedBelt, machine);
-    const output = new InserterSimulator(4, machine, sinkChest, "automation-science-pack");
-
-    orchestrator.registerInserter(smartInput);
-    orchestrator.registerMachine(machine);
-    orchestrator.registerInserter(output);
-    smartInput.updateDynamicFilters(["copper-plate", "iron-gear-wheel"]);
-    smartInput.useDynamicFilters = true;
-    // Run the simulation
-    // It takes 300 ticks for a single craft.
-    const targetTick = orchestrator.currentTick + 300 * 5;
-    orchestrator.tickUntil(() => false, targetTick);
-
-    // --- VALIDATION ---
-
-    // The inserter MUST have extracted both items from the mixed belt.
-    // If it had locked up trying to stuff infinite copper, the gear count would be undefined/0.
-    expect(mixedBelt.extractedCounts["copper-plate"]).toBeGreaterThan(0);
-    expect(mixedBelt.extractedCounts["iron-gear-wheel"]).toBeGreaterThan(0);
-
-    expect(sinkChest.receivedCounts["automation-science-pack"]).toBeGreaterThanOrEqual(2);
-
-    // The extracted copper should not wildly exceed the extracted gears,
-    // proving the machine's buffer choked the copper input and forced the inserter to switch.
-    // They should be reasonably close to each other (within 1 hand size margin)
-    const copperPulled = mixedBelt.extractedCounts["copper-plate"];
-    const gearsPulled = mixedBelt.extractedCounts["iron-gear-wheel"];
-    expect(Math.abs(copperPulled - gearsPulled)).toBeLessThanOrEqual(4);
+      orchestrator.tick();
+      expect(filterInserter.heldItems).toBe(8);
+      expect(filterInserter.currentTargetItem).toBe("iron-plate");
+    });
+
+    it("Advanced Filtering: Single multi-filter inserter autonomously feeds a Red Science machine", () => {
+      const { orchestrator, belts, chests } = new SimulationBuilder()
+        .addBelt("belt", "copper-plate", "iron-gear-wheel")
+        .addChest("sink")
+        .addMachine("machine", Recipes.redScience)
+        .addInserter("input", 4, "belt", "machine", ["copper-plate", "iron-gear-wheel"])
+        .addInserter("output", 4, "machine", "sink", "automation-science-pack")
+        .build();
+
+      const targetTick = orchestrator.currentTick + 300 * 5;
+      orchestrator.tickUntil(() => false, targetTick);
+
+      expect(belts.belt.extractedCounts["copper-plate"]).toBeGreaterThan(0);
+      expect(belts.belt.extractedCounts["iron-gear-wheel"]).toBeGreaterThan(0);
+      expect(chests.sink.receivedCounts["automation-science-pack"]).toBeGreaterThanOrEqual(2);
+
+      const copperPulled = belts.belt.extractedCounts["copper-plate"];
+      const gearsPulled = belts.belt.extractedCounts["iron-gear-wheel"];
+      expect(Math.abs(copperPulled - gearsPulled)).toBeLessThanOrEqual(4);
+    });
+
+    it("Advanced dynamic Filtering: Single multi-filter inserter autonomously feeds a Red Science machine", () => {
+      const { orchestrator, belts, chests, inserters } = new SimulationBuilder()
+        .addBelt("belt", "copper-plate", "iron-gear-wheel")
+        .addChest("sink")
+        .addMachine("machine", Recipes.redScience)
+        .addInserter("input", 4, "belt", "machine", undefined, true)
+        .addInserter("output", 4, "machine", "sink", "automation-science-pack")
+        .build();
+
+      const smartInput = inserters.input as any;
+      smartInput.updateDynamicFilters(["copper-plate", "iron-gear-wheel"]);
+
+      const targetTick = orchestrator.currentTick + 300 * 5;
+      orchestrator.tickUntil(() => false, targetTick);
+
+      expect(belts.belt.extractedCounts["copper-plate"]).toBeGreaterThan(0);
+      expect(belts.belt.extractedCounts["iron-gear-wheel"]).toBeGreaterThan(0);
+      expect(chests.sink.receivedCounts["automation-science-pack"]).toBeGreaterThanOrEqual(2);
+
+      const copperPulled = belts.belt.extractedCounts["copper-plate"];
+      const gearsPulled = belts.belt.extractedCounts["iron-gear-wheel"];
+      expect(Math.abs(copperPulled - gearsPulled)).toBeLessThanOrEqual(4);
+    });
   });
-});
+});

+ 42 - 0
src/engine/test-factories.ts

@@ -0,0 +1,42 @@
+import type { Recipe } from "../../scripts/factorio-dump/helpers/recipes.helper";
+import type { Machine } from "../../scripts/factorio-dump/process-data.models";
+import type { MachineSetup } from "./model";
+
+export const createMachineSetup = (overrides?: Partial<MachineSetup>): MachineSetup => ({
+  machine: { crafting_speed: 1, ...overrides?.machine } as Machine,
+  machineModules: [],
+  beacons: [],
+  machineQualityLevel: 0,
+  ...overrides,
+});
+
+export const createRecipe = (name: string, energy: number, ingredients: any[], results: any[]): Recipe => ({
+  name,
+  energy_required: energy,
+  ingredients,
+  results,
+} as Recipe);
+
+// Pre-baked common recipes
+export const Recipes = {
+  ironGear: createRecipe("iron-gear-wheel", 0.5, [{ type: "item", name: "iron-plate", amount: 2 }], [{ type: "item", name: "iron-gear-wheel", amount: 1 }]),
+  redScience: createRecipe("automation-science-pack", 5, [
+    { name: "copper-plate", amount: 1, type: "item" },
+    { name: "iron-gear-wheel", amount: 1, type: "item" }
+  ], [{ type: "item", name: "automation-science-pack", amount: 1 }]),
+  copperCable: createRecipe("copper-cable", 0.5, [{ type: "item", name: "copper-plate", amount: 1 }], [{ type: "item", name: "copper-cable", amount: 2 }]),
+  greenCircuit: createRecipe("electronic-circuit", 0.5, [
+    { type: "item", name: "iron-plate", amount: 1 },
+    { type: "item", name: "copper-cable", amount: 3 }
+  ], [{ type: "item", name: "electronic-circuit", amount: 1 }]),
+  advancedCircuit: createRecipe("advanced-circuit", 6, [
+    { type: "item", name: "plastic-bar", amount: 2 },
+    { type: "item", name: "copper-cable", amount: 4 },
+    { type: "item", name: "electronic-circuit", amount: 2 }
+  ], [{ type: "item", name: "advanced-circuit", amount: 1 }]),
+  twoGear: createRecipe("iron-gear-wheel", 0.5, [{ type: "item", name: "iron-plate", amount: 2 }], [{ type: "item", name: "iron-gear-wheel", amount: 2 }]),
+};
+
+export const tickTimes = (entity: { tick: () => void }, times: number) => {
+  for (let i = 0; i < times; i++) entity.tick();
+};