Files
Jyotisha/frontend/tests/personal-report-job-state.test.ts
2026-08-15 05:10:37 +08:00

181 lines
9.0 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
PersonalReportJobStateError,
assertPersonalReportJobProgressPhase,
assertPersonalReportJobTransition,
canHeartbeatPersonalReportJobLease,
canRetryPersonalReportJob,
canTransitionPersonalReportJob,
classifyPersonalReportJobRequest,
isPersonalReportJobLeaseExpired,
isPersonalReportJobTerminal,
isPersonalReportRequestFingerprint,
recoverExpiredPersonalReportJobLease,
} from "../src/lib/personal-report-job-state.ts";
const now = new Date("2026-08-14T03:00:00.000Z");
test("rejects illegal status transitions", () => {
assert.equal(canTransitionPersonalReportJob("queued", "running"), true);
assert.equal(canTransitionPersonalReportJob("running", "retrying"), true);
assert.equal(canTransitionPersonalReportJob("queued", "ready"), false);
assert.equal(canTransitionPersonalReportJob("suspended", "ready"), false);
assert.throws(
() => assertPersonalReportJobTransition("queued", "ready"),
(error) => error instanceof PersonalReportJobStateError
&& error.reason === "invalid_transition",
);
});
test("bounds retries at maxAttempts", () => {
assert.equal(canRetryPersonalReportJob(0, 3), true);
assert.equal(canRetryPersonalReportJob(2, 3), true);
assert.equal(canRetryPersonalReportJob(3, 3), false);
assert.throws(
() => canRetryPersonalReportJob(4, 3),
(error) => error instanceof PersonalReportJobStateError
&& error.reason === "invalid_retry_budget",
);
});
test("recovers an expired running lease into retrying while budget remains", () => {
const job = {
status: "running" as const,
leaseOwner: "worker-a",
leaseExpiresAt: "2026-08-14T02:59:59.000Z",
attemptCount: 1,
maxAttempts: 3,
};
assert.equal(isPersonalReportJobLeaseExpired(job, now), true);
assert.deepEqual(
recoverExpiredPersonalReportJobLease(job, now),
{ kind: "retry", nextStatus: "retrying" },
);
});
test("fails expired work after the final permitted attempt", () => {
assert.deepEqual(
recoverExpiredPersonalReportJobLease({
status: "running",
leaseOwner: "worker-a",
leaseExpiresAt: "2026-08-14T02:59:59.000Z",
attemptCount: 3,
maxAttempts: 3,
}, now),
{ kind: "exhausted", nextStatus: "failed" },
);
});
test("keeps active leases owned by the claiming worker", () => {
const lease = {
status: "running" as const,
leaseOwner: "worker-a",
leaseExpiresAt: "2026-08-14T03:00:30.000Z",
};
assert.equal(isPersonalReportJobLeaseExpired(lease, now), false);
assert.equal(canHeartbeatPersonalReportJobLease(lease, "worker-a", now), true);
assert.equal(canHeartbeatPersonalReportJobLease(lease, "worker-b", now), false);
});
test("terminal states are immutable", () => {
for (const status of ["ready", "failed", "cancelled"] as const) {
assert.equal(isPersonalReportJobTerminal(status), true);
for (const target of ["queued", "running", "suspended", "retrying", "ready", "failed", "cancelled"] as const) {
assert.equal(canTransitionPersonalReportJob(status, target), false);
}
}
});
test("classifies request-id replay and fingerprint conflict", () => {
const fingerprint = "a".repeat(64);
const existing = { requestId: "request-1", requestFingerprint: fingerprint };
assert.equal(isPersonalReportRequestFingerprint(fingerprint), true);
assert.equal(isPersonalReportRequestFingerprint("A".repeat(64)), false);
assert.equal(classifyPersonalReportJobRequest(existing, existing), "replay");
assert.equal(classifyPersonalReportJobRequest(existing, {
requestId: "request-1",
requestFingerprint: "b".repeat(64),
}), "conflict");
assert.equal(classifyPersonalReportJobRequest(existing, {
requestId: "request-2",
requestFingerprint: fingerprint,
}), "new");
});
test("accepts machine-readable progress phases only", () => {
assert.doesNotThrow(() => assertPersonalReportJobProgressPhase("evidence_collection"));
assert.throws(
() => assertPersonalReportJobProgressPhase("Evidence Collection"),
(error) => error instanceof PersonalReportJobStateError
&& error.reason === "invalid_progress_phase",
);
});
const localJobMigration = readFileSync(
new URL("../db/migrations/20260814040000_personal_report_jobs_v2.sql", import.meta.url),
"utf8",
);
const supabaseJobMigration = readFileSync(
new URL("../supabase/migrations/20260814040000_personal_report_jobs_v2.sql", import.meta.url),
"utf8",
);
test("job migration mirror is exact and preserves personal_reports as the document projection", () => {
assert.equal(localJobMigration, supabaseJobMigration);
assert.match(supabaseJobMigration, /create table if not exists public\.personal_report_jobs/);
assert.doesNotMatch(supabaseJobMigration, /alter table public\.personal_reports/);
assert.match(supabaseJobMigration, /references public\.personal_reports \(user_id, request_id\)[\s\S]*on delete cascade/);
});
test("job migration persists the full state, lease, retry and progress contract", () => {
assert.match(supabaseJobMigration, /status in \([\s\S]*'queued'[\s\S]*'running'[\s\S]*'suspended'[\s\S]*'retrying'[\s\S]*'ready'[\s\S]*'failed'[\s\S]*'cancelled'[\s\S]*\)/);
assert.match(supabaseJobMigration, /attempt_count integer not null default 0/);
assert.match(supabaseJobMigration, /max_attempts integer not null default 3/);
assert.match(supabaseJobMigration, /attempt_count between 0 and max_attempts/);
assert.match(supabaseJobMigration, /lease_token uuid/);
assert.match(supabaseJobMigration, /lease_expires_at timestamptz/);
assert.match(supabaseJobMigration, /heartbeat_at timestamptz/);
assert.match(supabaseJobMigration, /progress_phase text not null default 'queued'/);
assert.match(supabaseJobMigration, /personal_report_jobs_one_active_per_user[\s\S]*where status in \('queued', 'running', 'suspended', 'retrying'\)/);
});
test("job migration enforces recovery and terminal immutability in PostgreSQL", () => {
assert.match(supabaseJobMigration, /personal_report_job_terminal_immutable/);
assert.match(supabaseJobMigration, /old\.status = 'running' and new\.status in \('suspended', 'retrying', 'ready', 'failed', 'cancelled'\)/);
assert.match(supabaseJobMigration, /create or replace function public\.claim_personal_report_job/);
assert.match(supabaseJobMigration, /create or replace function public\.heartbeat_personal_report_job/);
assert.match(supabaseJobMigration, /create or replace function public\.complete_personal_report_job/);
assert.match(supabaseJobMigration, /v_job\.status <> 'running'[\s\S]*v_job\.lease_token is distinct from p_lease_token[\s\S]*v_job\.lease_expires_at <= v_completed_at/);
assert.match(supabaseJobMigration, /update public\.personal_reports[\s\S]*status = 'ready'[\s\S]*update public\.personal_report_jobs[\s\S]*status = 'ready'/);
assert.match(supabaseJobMigration, /create or replace function public\.fail_personal_report_job/);
assert.match(supabaseJobMigration, /v_job\.status <> 'running'[\s\S]*v_job\.lease_token is distinct from p_lease_token[\s\S]*v_job\.lease_expires_at <= v_failed_at/);
assert.match(supabaseJobMigration, /update public\.personal_reports[\s\S]*status = 'failed'[\s\S]*update public\.personal_report_jobs[\s\S]*status = 'failed'/);
assert.match(supabaseJobMigration, /create or replace function public\.recover_expired_personal_report_jobs/);
assert.match(supabaseJobMigration, /when candidates\.report_status = 'ready' then 'ready'[\s\S]*when candidates\.report_status = 'failed' then 'failed'[\s\S]*when job\.attempt_count < job\.max_attempts then 'retrying'[\s\S]*else 'failed'/);
});
test("job migration keeps owner access select-only and service_role owns writes", () => {
assert.match(supabaseJobMigration, /alter table public\.personal_report_jobs enable row level security/);
assert.match(supabaseJobMigration, /create policy personal_report_jobs_select_own[\s\S]*for select[\s\S]*to authenticated[\s\S]*using \(auth\.uid\(\) = user_id\)/);
assert.doesNotMatch(supabaseJobMigration, /create policy personal_report_jobs_delete_own[\s\S]*for delete/);
assert.match(supabaseJobMigration, /grant select on table public\.personal_report_jobs to authenticated/);
assert.doesNotMatch(supabaseJobMigration, /grant (?:select, )?(?:insert|update|delete)[^;]* on table public\.personal_report_jobs to authenticated/);
assert.match(supabaseJobMigration, /grant select, insert, update, delete on table public\.personal_report_jobs to service_role/);
for (const signature of [
"claim_personal_report_job(text, integer, uuid)",
"complete_personal_report_job(uuid, uuid, uuid, uuid, uuid, text, text, jsonb, text, text, text, text, text, text)",
"fail_personal_report_job(uuid, uuid, uuid, uuid, uuid, text, text, text)",
]) {
const escaped = signature.replace(/[()]/g, "\\$&");
assert.match(supabaseJobMigration, new RegExp(`grant execute on function public\\.${escaped} to service_role`));
assert.doesNotMatch(supabaseJobMigration, new RegExp(`grant execute on function public\\.${escaped} to authenticated`));
}
});