126 lines
3.5 KiB
TypeScript
126 lines
3.5 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { PERMS } from "@/lib/permission-slugs";
|
|
import type { HousekeepingCapabilityContext } from "../../../foundation/contracts";
|
|
import {
|
|
createInMemoryStudioOperationRepository,
|
|
createStudioOperationQuery,
|
|
STUDIO_OPERATION_KINDS,
|
|
} from "./studio";
|
|
|
|
function context(granted: readonly string[]): HousekeepingCapabilityContext {
|
|
const permissions = new Set(granted);
|
|
return {
|
|
actor: { id: 42, username: "operator", rank: 7 },
|
|
isSuperAdmin: false,
|
|
has: (slug) => permissions.has(slug),
|
|
hasAny: (...slugs) => slugs.some((slug) => permissions.has(slug)),
|
|
hasAll: (...slugs) => slugs.every((slug) => permissions.has(slug)),
|
|
};
|
|
}
|
|
|
|
describe("Studio operation query", () => {
|
|
it("defines the exact ten operation kinds", () => {
|
|
expect(STUDIO_OPERATION_KINDS).toEqual([
|
|
"badge",
|
|
"clone",
|
|
"clothing",
|
|
"effect",
|
|
"furni",
|
|
"maintenance",
|
|
"pet",
|
|
"repair-icons",
|
|
"sync",
|
|
"upload",
|
|
]);
|
|
});
|
|
|
|
it("fails closed before invoking an unauthorized adapter", async () => {
|
|
const load = vi.fn();
|
|
const result = await createStudioOperationQuery({ load }).run(context([]), {
|
|
routeId: "hotel.studio.furni",
|
|
});
|
|
expect(result).toMatchObject({ ok: false, error: { code: "FORBIDDEN" } });
|
|
expect(load).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("normalizes filters and preserves truthful external-source outages", async () => {
|
|
const load = vi.fn(async (input) => ({
|
|
kind: "studio" as const,
|
|
items: [],
|
|
total: 0,
|
|
partialDependencies: ["external-sources"] as const,
|
|
input,
|
|
}));
|
|
const result = await createStudioOperationQuery({ load }).run(
|
|
context([PERMS.ASSETS_IMPORT]),
|
|
{
|
|
routeId: "hotel.studio.clone",
|
|
list: { search: " sofa ", pageSize: 999, offset: 999_999 },
|
|
},
|
|
);
|
|
expect(load).toHaveBeenCalledWith({
|
|
routeId: "hotel.studio.clone",
|
|
operationKind: "clone",
|
|
list: { search: "sofa", pageSize: 100, offset: 100_000 },
|
|
});
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
data: { partialDependencies: ["external-sources"] },
|
|
});
|
|
});
|
|
|
|
it("loads the audit route across all operation kinds without inventing a command kind", async () => {
|
|
const load = vi.fn(async () => ({
|
|
kind: "studio" as const,
|
|
items: [],
|
|
total: 0,
|
|
partialDependencies: [] as const,
|
|
}));
|
|
const result = await createStudioOperationQuery({ load }).run(
|
|
context([PERMS.ASSETS_IMPORT]),
|
|
{ routeId: "hotel.studio.audit" },
|
|
);
|
|
expect(result.ok).toBe(true);
|
|
expect(load).toHaveBeenCalledWith(
|
|
expect.objectContaining({ operationKind: null }),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Studio operation repository", () => {
|
|
it("enforces monotonic progress and terminal event ordering", async () => {
|
|
const repository = createInMemoryStudioOperationRepository();
|
|
await repository.persistIntent({
|
|
operationId: "op-1",
|
|
kind: "furni",
|
|
actorId: 42,
|
|
correlationId: "corr-1",
|
|
createdAt: "2026-08-30T10:00:00.000Z",
|
|
});
|
|
await repository.appendEvent({
|
|
operationId: "op-1",
|
|
kind: "furni",
|
|
phase: "running",
|
|
completed: 2,
|
|
total: 4,
|
|
messageKey: "pages.housekeeping.studio.running",
|
|
correlationId: "corr-1",
|
|
});
|
|
await expect(
|
|
repository.appendEvent({
|
|
operationId: "op-1",
|
|
kind: "furni",
|
|
phase: "running",
|
|
completed: 1,
|
|
total: 4,
|
|
messageKey: "pages.housekeeping.studio.running",
|
|
correlationId: "corr-1",
|
|
}),
|
|
).rejects.toThrow("non-monotonic Studio progress");
|
|
expect(await repository.get("op-1")).toMatchObject({
|
|
operationId: "op-1",
|
|
completed: 2,
|
|
});
|
|
});
|
|
});
|