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); }), );});
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);
}),
);
});