/*
Unit tests for fiber covering ComponentFiber. It also guards the documented public contract and regression-sensitive edge cases.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-1311: Compass header sweep — declares the fiber suite's contract for test-file scaffold coverage.
*/
import { describe, it, expect } from "vitest";
import { ComponentFiber } from "../fiber.ts";
import type { ResolvedComponentIdentityV1 } from "@warpgogol/werkstatt-shared/component";
import { RevertibleEffectHandler } from "../effects.ts";
const VALID_SHA = "sha256:" + "a".repeat(64);
function makeIdentity(
overrides: Partial = {},
): ResolvedComponentIdentityV1 {
return {
componentId: "werkstatt/engine",
version: "1.0.0",
artifactHash: VALID_SHA,
...overrides,
};
}
describe("ComponentFiber", () => {
it("starts in declared state", () => {
const fiber = new ComponentFiber(makeIdentity());
expect(fiber.state).toBe("declared");
expect(fiber.canAcceptNewCalls).toBe(false);
});
it("transitions to active and accepts calls", async () => {
const fiber = new ComponentFiber(makeIdentity());
fiber.transitionTo("waiting");
fiber.transitionTo("loading");
fiber.transitionTo("active");
expect(fiber.canAcceptNewCalls).toBe(true);
const result = await fiber.run({
id: "op-1",
execute: async () => 42,
});
expect(result).toBe(42);
expect(fiber.activeOperationCount).toBe(0);
});
it("rejects new calls when not active", async () => {
const fiber = new ComponentFiber(makeIdentity());
await expect(fiber.run({ id: "op-1", execute: async () => 1 })).rejects.toThrow(
/cannot accept new calls/,
);
});
it("drains in-flight operations", async () => {
const fiber = new ComponentFiber(makeIdentity());
fiber.transitionTo("waiting");
fiber.transitionTo("loading");
fiber.transitionTo("active");
let resolveOp: (v: number) => void;
const opPromise = new Promise((resolve) => {
resolveOp = resolve;
});
const runPromise = fiber.run({
id: "op-1",
execute: async () => opPromise,
});
const drainResult = await fiber.drain({ timeoutMs: 100 });
expect(drainResult.timedOut).toBe(true);
expect(drainResult.remainingOperations).toBe(1);
resolveOp!(99);
await runPromise;
const drainResult2 = await fiber.drain({ timeoutMs: 100 });
expect(drainResult2.drained).toBe(true);
expect(drainResult2.remainingOperations).toBe(0);
});
it("dispose transitions to disposed when clean", async () => {
const fiber = new ComponentFiber(makeIdentity());
fiber.transitionTo("waiting");
fiber.transitionTo("loading");
fiber.transitionTo("active");
const report = await fiber.dispose();
expect(report.allSucceeded).toBe(true);
expect(report.quarantined).toBe(false);
expect(fiber.state).toBe("disposed");
expect(fiber.isTerminal).toBe(true);
});
it("dispose unwinds effects in LIFO order", async () => {
const order: string[] = [];
const fiber = new ComponentFiber(makeIdentity());
fiber.transitionTo("waiting");
fiber.transitionTo("loading");
fiber.transitionTo("active");
fiber.registerEffect(
"eff-1",
new RevertibleEffectHandler(async () => {
order.push("eff-1");
}),
);
fiber.registerEffect(
"eff-2",
new RevertibleEffectHandler(async () => {
order.push("eff-2");
}),
);
await fiber.dispose();
expect(order).toEqual(["eff-2", "eff-1"]);
});
it("dispose quarantines when effect unwind fails", async () => {
const fiber = new ComponentFiber(makeIdentity());
fiber.transitionTo("waiting");
fiber.transitionTo("loading");
fiber.transitionTo("active");
fiber.registerEffect(
"eff-1",
new RevertibleEffectHandler(async () => {
throw new Error("fail");
}),
);
const report = await fiber.dispose();
expect(report.quarantined).toBe(true);
expect(report.allSucceeded).toBe(false);
expect(fiber.state).toBe("quarantined");
});
it("dispose is idempotent", async () => {
const fiber = new ComponentFiber(makeIdentity());
fiber.transitionTo("waiting");
fiber.transitionTo("loading");
fiber.transitionTo("active");
await fiber.dispose();
const report2 = await fiber.dispose();
expect(report2.allSucceeded).toBe(true);
expect(report2.entries).toHaveLength(0);
});
it("cancelAllOperations aborts in-flight operations", async () => {
const fiber = new ComponentFiber(makeIdentity());
fiber.transitionTo("waiting");
fiber.transitionTo("loading");
fiber.transitionTo("active");
const runPromise = fiber.run({
id: "op-1",
execute: async (signal) => {
return new Promise((_, reject) => {
signal.addEventListener("abort", () => reject(new DOMException("aborted", "AbortError")));
});
},
});
fiber.cancelAllOperations();
await expect(runPromise).rejects.toThrow();
});
it("addChild and dispose cascades to children", async () => {
const parent = new ComponentFiber(makeIdentity({ componentId: "werkstatt/parent" }));
const child = new ComponentFiber(makeIdentity({ componentId: "werkstatt/child" }));
parent.addChild(child);
parent.transitionTo("waiting");
parent.transitionTo("loading");
parent.transitionTo("active");
child.transitionTo("waiting");
child.transitionTo("loading");
child.transitionTo("active");
await parent.dispose();
expect(parent.state).toBe("disposed");
expect(child.state).toBe("disposed");
});
});