Ctrl K
ComunidadGuías…

src/state_machines/task_scope.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { describe, expect, it, vi } from "vitest";
import { TaskScope } from "./task_scope";

function deferred<T>() {
  let resolve!: (value: T) => void;
  let reject!: (error: unknown) => void;
  const promise = new Promise<T>((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { promise, resolve, reject };
}

describe("TaskScope", () => {
  it("replaces and removes keyed cleanups exactly once", () => {
    const scope = new TaskScope<string>();
    const first = vi.fn();
    const second = vi.fn();

    scope.replace("resource", first);
    scope.replace("resource", second);
    scope.remove("resource");
    scope.dispose();

    expect(first).toHaveBeenCalledOnce();
    expect(second).toHaveBeenCalledOnce();
  });

  it("retains ownership of a replacement when the previous cleanup throws", () => {
    const scope = new TaskScope<string>();
    const replacement = vi.fn();
    scope.replace("resource", () => {
      throw new Error("old cleanup failed");
    });

    expect(() => scope.replace("resource", replacement)).toThrow(
      "old cleanup failed",
    );
    scope.dispose();

    expect(replacement).toHaveBeenCalledOnce();
  });

  it("runs registrations after disposal immediately", () => {
    const scope = new TaskScope<string>();
    const cleanup = vi.fn();
    scope.dispose();

    scope.replace("late", cleanup);

    expect(cleanup).toHaveBeenCalledOnce();
  });

  it("compensates promises that settle after disposal", async () => {
    const scope = new TaskScope();
    const setup = deferred<string>();
    const lateCleanup = vi.fn();
    const tracked = scope.trackPromise(setup.promise, lateCleanup);

    scope.dispose();
    setup.resolve("ready");

    await expect(tracked).resolves.toBe("ready");
    expect(lateCleanup).toHaveBeenCalledOnce();
  });

  it("preserves tracked rejection while running late cleanup", async () => {
    const scope = new TaskScope();
    const setup = deferred<void>();
    const lateCleanup = vi.fn();
    const tracked = scope.trackPromise(setup.promise, lateCleanup);

    scope.dispose();
    setup.reject(new Error("setup failed"));

    await expect(tracked).rejects.toThrow("setup failed");
    expect(lateCleanup).toHaveBeenCalledOnce();
  });

  it("retains setup and cleanup errors when late compensation also fails", async () => {
    const scope = new TaskScope();
    const setupError = new Error("setup failed");
    const cleanupError = new Error("cleanup failed");
    scope.dispose();

    const tracked = scope.trackPromise(Promise.reject(setupError), () => {
      throw cleanupError;
    });

    await expect(tracked).rejects.toMatchObject({
      errors: [setupError, cleanupError],
    });
  });

  it("reports late cleanup failure after successful setup", async () => {
    const scope = new TaskScope();
    const cleanupError = new Error("cleanup failed");
    scope.dispose();

    const tracked = scope.trackPromise(Promise.resolve("ready"), () => {
      throw cleanupError;
    });

    await expect(tracked).rejects.toBe(cleanupError);
  });

  it("is idempotent and aggregates disposer errors after all cleanup", () => {
    const scope = new TaskScope<string>();
    const order: string[] = [];
    scope.replace("first", () => {
      order.push("first");
      throw new Error("first failed");
    });
    scope.replace("second", () => {
      order.push("second");
      throw new Error("second failed");
    });

    expect(() => scope.dispose()).toThrow(AggregateError);
    expect(order).toEqual(["second", "first"]);
    expect(() => scope.dispose()).not.toThrow();
  });
});