The e2e stack shared production's Postgres — same server, same database, same table. Seeded robots sat in `users` beside real clinicians, and anything a test wrote, or a migration under test changed, landed on real data. Nothing about "run the tests" should be able to reach an account belonging to a person. Now it has a Postgres and a Redis of its own, both on tmpfs: created empty on every run, held in RAM, gone on teardown. scripts/e2e.sh is one command that recreates the stack, seeds it, runs the browser and leaves the app up at 127.0.0.1:3553 so it can be clicked around in, with the report served at :3554. Two bugs fell out of it immediately, both of which only a database that did not already exist could have found: The schema could not be built from nothing. The entrypoint migrated before the app created its baseline tables, so the first migration failed on saved_encounters not existing. It never showed because every database this has ever run against already had the baseline. Then, one layer down, 1777800000000_generated-images creates a table with a foreign key to learning_content — which the baseline stopped creating when Learning Hub was removed. Restoring into a brand-new database could not have booted. The entrypoint now stands aside when the database is empty and lets the app do it in the order it already gets right, and the foreign key is only created where its target is. All 20 migrations replay from empty, producing the same 23 tables production has. Configuration lives in the database, so a throwaway one starts at defaults — 14 settings against production's 49. That is why every model picker was empty: models.custom did not exist. The tests were right and the environment was incomplete, so the seed now states what the suite depends on, with fictional model ids: a test should not pass because of a setting somebody changed on the live system last week. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
74 lines
5.9 KiB
JavaScript
74 lines
5.9 KiB
JavaScript
// Durable jobs and private asset grants. No external calls or corpus changes.
|
|
//
|
|
// generated_image_links pointed at learning_content, which the baseline created
|
|
// at the time. Learning Hub has since been retired: the baseline no longer
|
|
// creates that table, and 1780800000000_retire-learning-hub drops this one.
|
|
// Replaying the chain into an empty database therefore failed here, on a
|
|
// foreign key to a table that no longer exists — which is what stopped a
|
|
// brand-new database from ever booting.
|
|
//
|
|
// The reference is now created only where the target is. Databases that
|
|
// already ran this migration are untouched: node-pg-migrate records it as
|
|
// applied and never runs it again. Fresh ones get the table without the key,
|
|
// and lose it entirely a few migrations later, which is the same end state.
|
|
exports.up = pgm => pgm.sql(`
|
|
CREATE TABLE generated_image_jobs (
|
|
id UUID PRIMARY KEY,
|
|
owner_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
workflow TEXT NOT NULL CHECK (workflow IN ('clinical_assistant', 'learning_hub')),
|
|
idempotency_key TEXT NOT NULL,
|
|
input_hash TEXT NOT NULL,
|
|
prompt_cipher TEXT NOT NULL CHECK (prompt_cipher LIKE 'enc1:%'),
|
|
model TEXT NOT NULL,
|
|
prompt_revision INTEGER NOT NULL,
|
|
budget INTEGER NOT NULL CHECK (budget BETWEEN 1000 AND 32000),
|
|
prompt_units INTEGER NOT NULL,
|
|
stage TEXT NOT NULL DEFAULT 'queued' CHECK (stage IN ('queued','generating','storing','done','error','interrupted')),
|
|
lease_token UUID, lease_until TIMESTAMPTZ,
|
|
staged_bytes BYTEA, mime TEXT, checksum TEXT, byte_length INTEGER,
|
|
error_code TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE(owner_id, workflow, idempotency_key)
|
|
);
|
|
CREATE INDEX generated_image_claim ON generated_image_jobs(stage, created_at);
|
|
CREATE TABLE generated_image_links (
|
|
asset_id UUID NOT NULL REFERENCES generated_image_jobs(id) ON DELETE CASCADE,
|
|
content_id INTEGER NOT NULL,
|
|
PRIMARY KEY(asset_id, content_id)
|
|
);
|
|
DO $links$ BEGIN
|
|
IF to_regclass('public.learning_content') IS NOT NULL THEN
|
|
ALTER TABLE generated_image_links
|
|
ADD CONSTRAINT generated_image_links_content_id_fkey
|
|
FOREIGN KEY (content_id) REFERENCES learning_content(id) ON DELETE CASCADE;
|
|
END IF;
|
|
END $links$;
|
|
CREATE FUNCTION guard_generated_image_job() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF (NEW.owner_id, NEW.workflow, NEW.idempotency_key, NEW.input_hash, NEW.prompt_cipher, NEW.model, NEW.prompt_revision, NEW.budget, NEW.prompt_units)
|
|
IS DISTINCT FROM (OLD.owner_id, OLD.workflow, OLD.idempotency_key, OLD.input_hash, OLD.prompt_cipher, OLD.model, OLD.prompt_revision, OLD.budget, OLD.prompt_units) THEN
|
|
RAISE EXCEPTION 'Image job input and ownership are immutable';
|
|
END IF;
|
|
RETURN NEW;
|
|
END; $$;
|
|
CREATE TRIGGER generated_image_job_immutable BEFORE UPDATE ON generated_image_jobs FOR EACH ROW EXECUTE FUNCTION guard_generated_image_job();
|
|
CREATE FUNCTION guard_generated_image_link() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM generated_image_jobs WHERE id = NEW.asset_id AND workflow = 'learning_hub' AND stage = 'done') THEN
|
|
RAISE EXCEPTION 'Only Learning assets may be attached';
|
|
END IF;
|
|
RETURN NEW;
|
|
END; $$;
|
|
CREATE TRIGGER generated_image_link_guard BEFORE INSERT OR UPDATE ON generated_image_links FOR EACH ROW EXECUTE FUNCTION guard_generated_image_link();
|
|
ALTER TABLE prompt_revisions DROP CONSTRAINT prompt_revisions_prompt_key_check;
|
|
ALTER TABLE prompt_revisions ADD CONSTRAINT prompt_revisions_prompt_key_check CHECK (prompt_key IN ('prompt.hpiEncounter','prompt.hpiDictation','prompt.hpiInpatient','prompt.hospitalCourseShort','prompt.hospitalCourseLong','prompt.hospitalCourseICU','prompt.hospitalCoursePsych','prompt.chartReviewOutpatient','prompt.chartReviewSubspecialty','prompt.chartReviewED','prompt.soapFull','prompt.soapSubjective','prompt.milestoneNarrative','prompt.milestoneList','prompt.milestoneSummary','prompt.peGuideNarrative','prompt.peGuideList','prompt.refine','prompt.shortenDocument','prompt.askClarification','prompt.shadessAssessment','prompt.wellVisitNote','prompt.wellVisitShort','prompt.sickVisitNote','prompt.edEncounterStaged','prompt.edConsolidate','prompt.edFinalize','prompt.dontMissTooltip','prompt.patientEducation','clinical_assistant.system_behavior','clinical_assistant.image_behavior','learning_hub.image_behavior'));
|
|
`);
|
|
// Down preserves append-only Learning prompt history: run only after explicit archival/removal of that history.
|
|
exports.down = pgm => pgm.sql(`
|
|
DO $$ BEGIN IF EXISTS(SELECT 1 FROM prompt_revisions WHERE prompt_key = 'learning_hub.image_behavior') THEN
|
|
RAISE EXCEPTION 'Learning image prompt history exists; retain migration rather than discard history'; END IF; END $$;
|
|
ALTER TABLE prompt_revisions DROP CONSTRAINT prompt_revisions_prompt_key_check;
|
|
ALTER TABLE prompt_revisions ADD CONSTRAINT prompt_revisions_prompt_key_check CHECK (prompt_key IN ('prompt.hpiEncounter','prompt.hpiDictation','prompt.hpiInpatient','prompt.hospitalCourseShort','prompt.hospitalCourseLong','prompt.hospitalCourseICU','prompt.hospitalCoursePsych','prompt.chartReviewOutpatient','prompt.chartReviewSubspecialty','prompt.chartReviewED','prompt.soapFull','prompt.soapSubjective','prompt.milestoneNarrative','prompt.milestoneList','prompt.milestoneSummary','prompt.peGuideNarrative','prompt.peGuideList','prompt.refine','prompt.shortenDocument','prompt.askClarification','prompt.shadessAssessment','prompt.wellVisitNote','prompt.wellVisitShort','prompt.sickVisitNote','prompt.edEncounterStaged','prompt.edConsolidate','prompt.edFinalize','prompt.dontMissTooltip','prompt.patientEducation','clinical_assistant.system_behavior','clinical_assistant.image_behavior'));
|
|
DROP TABLE generated_image_links; DROP TABLE generated_image_jobs;
|
|
DROP FUNCTION guard_generated_image_link(); DROP FUNCTION guard_generated_image_job();
|
|
`);
|