181 lines
9.0 KiB
TypeScript
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`));
|
|
}
|
|
});
|