197 lines · 7.5 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Effect, Predicate, Result } from "effect"; import { makeTestExecutor } from "./testing"; // The `executor.artifacts` surface against the real SQLite test db. Artifacts// are owner-scoped rows with no plugin involvement, so the default test// executor (one tenant + one bound subject) is the whole fixture. const CODE = "export default function App() { return <div>hi</div>; }"; describe("executor.artifacts", () => {  it.effect("list is empty when nothing is saved", () =>    Effect.gen(function* () {      const executor = yield* makeTestExecutor();      expect(yield* executor.artifacts.list()).toEqual([]);    }),  );   it.effect("save mints an id and stores the source", () =>    Effect.gen(function* () {      const executor = yield* makeTestExecutor();      const saved = yield* executor.artifacts.save({        title: "Active users",        description: "Daily active users over time",        code: CODE,      });      expect(saved.id).not.toBe("");      expect(saved.owner).toBe("user");      expect(saved.title).toBe("Active users");      expect(saved.description).toBe("Daily active users over time");      expect(saved.code).toBe(CODE);       const fetched = yield* executor.artifacts.get(saved.id);      expect(fetched).toEqual(saved);    }),  );   it.effect("save defaults a missing description to null", () =>    Effect.gen(function* () {      const executor = yield* makeTestExecutor();      const saved = yield* executor.artifacts.save({ title: "Untitled", code: CODE });      expect(saved.description).toBe(null);    }),  );   it.effect("list omits the source", () =>    Effect.gen(function* () {      const executor = yield* makeTestExecutor();      const saved = yield* executor.artifacts.save({        title: "Active users",        description: "described",        code: CODE,      });       const listed = yield* executor.artifacts.list();      // The list projection is deliberately code-free: a saved component can be      // large and a picker only needs title/description to match on.      expect(listed).toEqual([        {          id: saved.id,          owner: "user",          title: "Active users",          description: "described",          createdAt: saved.createdAt,          updatedAt: saved.updatedAt,        },      ]);    }),  );   // `it.live` (real clock) with a full-second gap: `dateColumn` lands in SQLite  // as a SECOND-resolution integer, so two artifacts saved inside one second  // carry the same `updated_at` and only the id tiebreak separates them. This  // pins the ordering the picker actually depends on, at the resolution the  // storage layer really has.  it.live("list returns the most recently updated artifact first", () =>    Effect.gen(function* () {      const executor = yield* makeTestExecutor();      const first = yield* executor.artifacts.save({ title: "First", code: CODE });      yield* Effect.sleep("1100 millis");      const second = yield* executor.artifacts.save({ title: "Second", code: CODE });      expect(second.updatedAt.getTime()).toBeGreaterThan(first.updatedAt.getTime());       expect((yield* executor.artifacts.list()).map((a) => a.id)).toEqual([second.id, first.id]);       // Touching the OLDER artifact floats it back to the top — the list is      // keyed on `updated_at`, not on creation order.      yield* Effect.sleep("1100 millis");      yield* executor.artifacts.rename({ id: first.id, title: "First, touched" });      expect((yield* executor.artifacts.list()).map((a) => a.id)).toEqual([first.id, second.id]);    }),  );   it.effect("save with an id overwrites in place", () =>    Effect.gen(function* () {      const executor = yield* makeTestExecutor();      const saved = yield* executor.artifacts.save({ title: "Draft", code: CODE });      const updated = yield* executor.artifacts.save({        id: saved.id,        title: "Final",        description: "now described",        code: "export default function App() { return null; }",      });       expect(updated.id).toBe(saved.id);      expect(updated.title).toBe("Final");      expect(updated.description).toBe("now described");      expect(updated.code).toBe("export default function App() { return null; }");      expect(updated.createdAt.getTime()).toBe(saved.createdAt.getTime());       // Overwrite, not append: still exactly one row.      const listed = yield* executor.artifacts.list();      expect(listed.map((a) => a.id)).toEqual([saved.id]);    }),  );   it.effect("save fails for an id that names no visible artifact", () =>    Effect.gen(function* () {      const executor = yield* makeTestExecutor();      const result = yield* Effect.result(        executor.artifacts.save({ id: "art_missing", title: "Ghost", code: CODE }),      );      expect(Result.isFailure(result)).toBe(true);      if (!Result.isFailure(result)) return;      expect(Predicate.isTagged("ArtifactNotFoundError")(result.failure)).toBe(true);    }),  );   it.effect("get fails for an unknown id", () =>    Effect.gen(function* () {      const executor = yield* makeTestExecutor();      const result = yield* Effect.result(executor.artifacts.get("art_missing"));      expect(Result.isFailure(result)).toBe(true);      if (!Result.isFailure(result)) return;      expect(Predicate.isTagged("ArtifactNotFoundError")(result.failure)).toBe(true);    }),  );   it.effect("rename changes only the title", () =>    Effect.gen(function* () {      const executor = yield* makeTestExecutor();      const saved = yield* executor.artifacts.save({        title: "Old name",        description: "kept",        code: CODE,      });      const renamed = yield* executor.artifacts.rename({ id: saved.id, title: "New name" });      expect(renamed.title).toBe("New name");      expect(renamed.description).toBe("kept");      expect(renamed.code).toBe(CODE);    }),  );   it.effect("rename fails for an unknown id", () =>    Effect.gen(function* () {      const executor = yield* makeTestExecutor();      const result = yield* Effect.result(        executor.artifacts.rename({ id: "art_missing", title: "Nope" }),      );      expect(Result.isFailure(result)).toBe(true);      if (!Result.isFailure(result)) return;      expect(Predicate.isTagged("ArtifactNotFoundError")(result.failure)).toBe(true);    }),  );   it.effect("remove hard-deletes the artifact", () =>    Effect.gen(function* () {      const executor = yield* makeTestExecutor();      const saved = yield* executor.artifacts.save({ title: "Temp", code: CODE });      yield* executor.artifacts.remove({ id: saved.id });       expect(yield* executor.artifacts.list()).toEqual([]);      const result = yield* Effect.result(executor.artifacts.get(saved.id));      expect(Result.isFailure(result)).toBe(true);      if (!Result.isFailure(result)) return;      expect(Predicate.isTagged("ArtifactNotFoundError")(result.failure)).toBe(true);    }),  );   it.effect("remove is idempotent", () =>    Effect.gen(function* () {      const executor = yield* makeTestExecutor();      yield* executor.artifacts.remove({ id: "art_missing" });    }),  );   it.effect("an executor with no subject cannot save a user artifact", () =>    Effect.gen(function* () {      // Pure-org executors (host request paths with no member bound) have no      // user partition to file a personal artifact into.      const executor = yield* makeTestExecutor({ subject: null });      const result = yield* Effect.result(executor.artifacts.save({ title: "Orphan", code: CODE }));      expect(Result.isFailure(result)).toBe(true);    }),  );});