136 lines
5.0 KiB
TypeScript
136 lines
5.0 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
// `vi.mock` factories are hoisted above regular top-level `const`s, so
|
|
// any cross-references between the spy/mock fns and the factories have
|
|
// to live inside `vi.hoisted`. See `mollifierDrainerHandler.test.ts`
|
|
// for the same pattern.
|
|
const { warnSpy, applyMetadataMutationToBufferedRunMock } = vi.hoisted(() => ({
|
|
warnSpy: vi.fn(),
|
|
applyMetadataMutationToBufferedRunMock: vi.fn(),
|
|
}));
|
|
|
|
// The route module's import graph (createActionApiRoute, the env, the
|
|
// services singleton) is heavier than the helper actually needs. Stub
|
|
// the leaf modules so only the helper under test executes; the route's
|
|
// top-level `createActionApiRoute(...)` call runs against the stubbed
|
|
// builder and never touches platform.v3.server / prisma.
|
|
vi.mock("~/db.server", () => ({ prisma: {}, $replica: {} }));
|
|
vi.mock("~/env.server", () => ({
|
|
env: { TASK_RUN_METADATA_MAXIMUM_SIZE: 256 * 1024 },
|
|
}));
|
|
vi.mock("~/services/routeBuilders/apiBuilder.server", () => ({
|
|
createActionApiRoute: () => ({ action: vi.fn() }),
|
|
}));
|
|
vi.mock("~/services/apiAuth.server", () => ({
|
|
authenticateApiRequest: vi.fn(),
|
|
}));
|
|
vi.mock("~/v3/services/common.server", () => ({
|
|
ServiceValidationError: class extends Error {
|
|
constructor(
|
|
public override message: string,
|
|
public status?: number
|
|
) {
|
|
super(message);
|
|
}
|
|
},
|
|
}));
|
|
vi.mock("~/services/metadata/updateMetadataInstance.server", () => ({
|
|
updateMetadataService: { call: vi.fn(async () => undefined) },
|
|
}));
|
|
vi.mock("~/v3/mollifier/applyMetadataMutation.server", () => ({
|
|
applyMetadataMutationToBufferedRun: applyMetadataMutationToBufferedRunMock,
|
|
}));
|
|
vi.mock("~/v3/mollifier/readFallback.server", () => ({
|
|
findRunByIdWithMollifierFallback: vi.fn(),
|
|
}));
|
|
vi.mock("~/services/logger.server", () => ({
|
|
logger: {
|
|
warn: warnSpy,
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
debug: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
import { routeOperationsToRun } from "~/routes/api.v1.runs.$runId.metadata";
|
|
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
|
|
|
const env = {
|
|
id: "env_a",
|
|
organizationId: "org_1",
|
|
} as unknown as AuthenticatedEnvironment;
|
|
|
|
const opsFixture = [{ type: "set", key: "k", value: "v" }] as Parameters<
|
|
typeof routeOperationsToRun
|
|
>[1];
|
|
|
|
describe("routeOperationsToRun — non-throw buffer outcome logging", () => {
|
|
// Each non-success outcome `applyMetadataMutationToBufferedRun` can
|
|
// return (`not_found`, `busy`, `version_exhausted`, `metadata_too_large`)
|
|
// must produce a warn log so ops can trace silent drops. Without this
|
|
// branch the parent/root operation would disappear with no record —
|
|
// `tryCatch` only catches throws, and the outcome object was
|
|
// previously ignored.
|
|
for (const kind of ["not_found", "busy", "version_exhausted", "metadata_too_large"] as const) {
|
|
it(`warn-logs when buffer outcome is { kind: "${kind}" }`, async () => {
|
|
warnSpy.mockClear();
|
|
applyMetadataMutationToBufferedRunMock.mockResolvedValueOnce({ kind });
|
|
|
|
await routeOperationsToRun("run_buffered_1", opsFixture, env);
|
|
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
"metadata route: parent/root buffer op did not apply",
|
|
expect.objectContaining({ targetRunId: "run_buffered_1", kind })
|
|
);
|
|
});
|
|
}
|
|
|
|
it("does NOT warn on the happy path (kind: 'applied')", async () => {
|
|
warnSpy.mockClear();
|
|
applyMetadataMutationToBufferedRunMock.mockResolvedValueOnce({
|
|
kind: "applied",
|
|
newMetadata: { k: "v" },
|
|
parentTaskRunFriendlyId: undefined,
|
|
rootTaskRunFriendlyId: undefined,
|
|
});
|
|
|
|
await routeOperationsToRun("run_buffered_1", opsFixture, env);
|
|
|
|
expect(warnSpy).not.toHaveBeenCalledWith(
|
|
"metadata route: parent/root buffer op did not apply",
|
|
expect.anything()
|
|
);
|
|
});
|
|
|
|
it("warn-logs once when the helper throws (the pre-existing throw branch keeps working)", async () => {
|
|
warnSpy.mockClear();
|
|
applyMetadataMutationToBufferedRunMock.mockRejectedValueOnce(new Error("ECONNRESET"));
|
|
|
|
await routeOperationsToRun("run_buffered_1", opsFixture, env);
|
|
|
|
// Pre-existing branch — the catch logs `buffer fallback for parent/root
|
|
// op failed`. The new non-throw branch must NOT also fire (we return
|
|
// early on bufferError).
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
"metadata route: buffer fallback for parent/root op failed",
|
|
expect.objectContaining({ targetRunId: "run_buffered_1" })
|
|
);
|
|
expect(warnSpy).not.toHaveBeenCalledWith(
|
|
"metadata route: parent/root buffer op did not apply",
|
|
expect.anything()
|
|
);
|
|
});
|
|
|
|
it("skips both PG and buffer when targetRunId is missing or operations is empty", async () => {
|
|
warnSpy.mockClear();
|
|
applyMetadataMutationToBufferedRunMock.mockClear();
|
|
|
|
await routeOperationsToRun(undefined, opsFixture, env);
|
|
await routeOperationsToRun("run_x", undefined, env);
|
|
await routeOperationsToRun("run_x", [], env);
|
|
|
|
expect(applyMetadataMutationToBufferedRunMock).not.toHaveBeenCalled();
|
|
expect(warnSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|