fix(housekeeping): complete people workflow fidelity
This commit is contained in:
1 parent
25b76437ff
commit
91c9efcbb4
29 files changed
+1365
-290
No files matched your search
+54
-47
@@ -3,34 +3,25 @@ import { PERMS } from "@/lib/permission-slugs";
|
||||
import type { AuditEntry } from "@/lib/services/audit";
|
||||
import type { HousekeepingCapabilityContext } from "../../../foundation/contracts";
|
||||
|
||||
const { alertUser, audit, resolveServerContext, selectLimit } = vi.hoisted(
|
||||
() => ({
|
||||
alertUser: vi.fn(),
|
||||
audit: vi.fn(),
|
||||
resolveServerContext: vi.fn(),
|
||||
selectLimit: vi.fn(),
|
||||
}),
|
||||
);
|
||||
const { alertUser, audit, resolveServerContext } = vi.hoisted(() => ({
|
||||
alertUser: vi.fn(),
|
||||
audit: vi.fn(),
|
||||
resolveServerContext: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
|
||||
getHousekeepingCapabilityContext: resolveServerContext,
|
||||
}));
|
||||
vi.mock("@/lib/auth", () => ({ invalidateLoginCache: vi.fn() }));
|
||||
vi.mock("@/lib/auth/password", () => ({ hashPassword: vi.fn() }));
|
||||
vi.mock("@/lib/db", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/lib/db")>();
|
||||
return {
|
||||
...actual,
|
||||
db: {
|
||||
...actual.db,
|
||||
select: vi.fn(() => ({
|
||||
from: vi.fn(() => ({
|
||||
where: vi.fn(() => ({ limit: selectLimit })),
|
||||
})),
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("@/lib/db", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@/lib/db")>()),
|
||||
db: {
|
||||
select: vi.fn(() => {
|
||||
throw new Error("alert must not query DB");
|
||||
}),
|
||||
},
|
||||
}));
|
||||
vi.mock("@/lib/services/audit", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@/lib/services/audit")>()),
|
||||
logAudit: audit,
|
||||
@@ -61,17 +52,6 @@ const invocation = {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
resolveServerContext.mockResolvedValue(context());
|
||||
selectLimit.mockResolvedValue([
|
||||
{
|
||||
id: 7,
|
||||
username: "Alice",
|
||||
rank: 2,
|
||||
motto: "Ready",
|
||||
credits: 100,
|
||||
pixels: 50,
|
||||
online: "1",
|
||||
},
|
||||
]);
|
||||
alertUser.mockResolvedValue(true);
|
||||
audit.mockResolvedValue(undefined);
|
||||
});
|
||||
@@ -93,7 +73,7 @@ describe("People external audit contract", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("persists a correlated failure outcome when the external operation fails", async () => {
|
||||
it("persists a correlated failure outcome when a pre-mutation external operation fails", async () => {
|
||||
alertUser.mockResolvedValue(false);
|
||||
|
||||
const result = await peopleMutationService.execute(
|
||||
@@ -120,24 +100,51 @@ describe("People external audit contract", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("throws typed completed-operation evidence when final outcome persistence fails", async () => {
|
||||
it("returns serializable partial completion when final outcome persistence fails", async () => {
|
||||
audit.mockImplementation(async (entry: AuditEntry) => {
|
||||
if (entry.outcome === "success") throw new Error("outcome unavailable");
|
||||
});
|
||||
|
||||
await expect(
|
||||
peopleMutationService.execute(invocation, "user.alert", {
|
||||
userId: 7,
|
||||
message: "Hello",
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
name: "AuditOutcomePersistenceError",
|
||||
operationCompleted: true,
|
||||
operationResult: {
|
||||
before: expect.objectContaining({ id: 7 }),
|
||||
after: expect.objectContaining({ alertDelivered: true }),
|
||||
const result = await peopleMutationService.execute(
|
||||
invocation,
|
||||
"user.alert",
|
||||
{ userId: 7, message: "Hello" },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
completion: {
|
||||
status: "partial",
|
||||
external: "completed",
|
||||
audit: "persisted",
|
||||
},
|
||||
data: {
|
||||
before: null,
|
||||
after: { userId: 7, alertDelivered: true },
|
||||
},
|
||||
correlationId: "audit-contract",
|
||||
});
|
||||
expect(() => JSON.stringify(result)).not.toThrow();
|
||||
expect(alertUser).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
audit.mock.calls.map((call) => (call[0] as AuditEntry).outcome),
|
||||
).toEqual(["intent", "success", "partial"]);
|
||||
});
|
||||
|
||||
it("attempts legacy alert without a target lookup when the database is unavailable", async () => {
|
||||
const result = await peopleMutationService.execute(
|
||||
{ ...invocation, legacy: true },
|
||||
"user.alert",
|
||||
{ userId: 900719925, message: "Direct RCON" },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
data: {
|
||||
before: null,
|
||||
after: { userId: 900719925, alertDelivered: true },
|
||||
},
|
||||
});
|
||||
expect(alertUser).toHaveBeenCalledTimes(1);
|
||||
expect(alertUser).toHaveBeenCalledWith(900719925, "Direct RCON");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user