Files
growqr-backend/scripts/onboarding-preferences.test.ts

161 lines
7.0 KiB
TypeScript

import assert from "node:assert/strict";
import {
DEFAULT_ONBOARDING_DATA,
defaultOnboardingData,
extractOnboardingData,
assertOnboardingRevision,
deriveOnboardingPayload,
mergeOnboardingPatch,
OnboardingRevisionConflict,
} from "../src/events/onboarding-ledger.js";
/**
* Focused tests for the onboarding preference helpers (pure — no DB/network).
* Covers: default v3 shape, revision conflict, partial update merge,
* status/completed_at consistency + payload derivation, and unrelated
* preference preservation. Matches the scripts/X.test.ts convention
* (node:assert over pure functions).
*/
// ── 1. default v3 response ───────────────────────────────────────────────────
{
const def = defaultOnboardingData();
assert.equal(def.schema_version, 3, "default is v3");
assert.equal(def.revision, 0);
assert.equal(def.status, "in_progress");
assert.equal(def.access_choice, null);
assert.equal(def.qx_estimate, null);
assert.equal(def.completed_at, null);
assert.equal(def.consent.privacy_accepted, false);
assert.equal(def.progress.stage, "consent");
// DEFAULT constant must match the factory.
assert.deepEqual(def, DEFAULT_ONBOARDING_DATA);
// Factory returns a fresh clone, not the shared constant.
assert.notEqual(def, DEFAULT_ONBOARDING_DATA);
}
// extractOnboardingData returns the default when nothing is stored.
{
const empty = extractOnboardingData(undefined);
assert.equal(empty.schema_version, 3);
assert.equal(empty.revision, 0);
const fromNonObject = extractOnboardingData("nonsense");
assert.equal(fromNonObject.status, "in_progress");
}
// ── 2. revision conflict (optimistic concurrency) ───────────────────────────
{
const current = extractOnboardingData({ revision: 5 });
// Matching revision is accepted (no throw).
assert.doesNotThrow(() => assertOnboardingRevision(5, current));
// Mismatched revision throws the typed conflict.
assert.throws(
() => assertOnboardingRevision(4, current),
(err) => err instanceof OnboardingRevisionConflict && err.expected === 4 && err.actual === 5,
);
}
// ── 3. partial update merge (revision bump + progress stamp) ────────────────
{
const stored = extractOnboardingData({ revision: 2 });
const incoming = { ...stored, revision: 2, profile: { ...stored.profile, mode: "founder" } };
const merged = mergeOnboardingPatch({ onboarding: stored }, incoming);
const next = extractOnboardingData(merged.onboarding);
assert.equal(next.revision, 3, "revision bumps by one from the stored value");
assert.equal(next.profile.mode, "founder", "incoming profile change is applied");
assert.ok(next.progress.updated_at, "progress.updated_at is stamped on save");
}
// ── 4. status/completed_at consistency + payload derivation ─────────────────
{
const base = defaultOnboardingData();
const completed = {
...base,
revision: 0,
status: "completed" as const,
profile: { ...base.profile, intent: "land-a-role", mode: "student", icp: "intern", question_branch: "student" },
responses: {
...base.responses,
career_barriers: ["no-network"],
desired_outcomes: ["interviews", "offer"],
target_role: "Product Intern",
target_field: "technical",
weekly_time_commitment: "5-10",
experience_level: "0-2",
work_context: "audience",
},
// deliberately omit completed_at; merge must stamp it.
};
const merged = mergeOnboardingPatch({}, completed);
const next = extractOnboardingData(merged.onboarding);
assert.equal(next.status, "completed");
assert.ok(next.completed_at, "completed status without completed_at is stamped");
// Payload is a faithful projection — no invented completion math.
const payload = deriveOnboardingPayload(next);
assert.equal(payload.schema_version, 1);
assert.equal(payload.source_revision, next.revision);
assert.equal(payload.mode, "student");
assert.equal(payload.onboarding_icp, "intern");
assert.equal(payload.target_role, "Product Intern");
assert.equal(payload.target_field, "technical");
assert.deepEqual(payload.goals, ["interviews", "offer"]);
assert.deepEqual(payload.barriers, ["no-network"]);
assert.equal(payload.weekly_time_commitment, "5-10");
assert.equal(payload.experience_context, "0-2 · audience");
}
// completed_at is cleared when regressing to in_progress with an explicit null.
{
const completed = { ...defaultOnboardingData(), status: "completed" as const, completed_at: "2026-07-10T00:00:00.000Z" };
const regressed = { ...completed, status: "in_progress" as const, completed_at: null };
const merged = mergeOnboardingPatch({}, regressed);
const next = extractOnboardingData(merged.onboarding);
assert.equal(next.status, "in_progress");
assert.equal(next.completed_at, null, "stale completed_at is cleared on regression");
}
// ── 5. unrelated preference preservation ────────────────────────────────────
{
const preferences = {
onboarding: { revision: 1, status: "in_progress" },
interview_preferences: { focus_areas: ["behavioral"] },
resume_preferences: { target_title: "Data Scientist" },
mission_preferences: { active_goal: "land-offer" },
target_roles: ["Product Intern"],
target_companies: ["Acme"],
};
const incoming = { ...extractOnboardingData(preferences.onboarding), revision: 1, profile: { intent: "x", mode: "student", icp: "intern", question_branch: "student" } };
const merged = mergeOnboardingPatch(preferences, incoming);
// The onboarding blob is updated.
assert.equal(extractOnboardingData(merged.onboarding).revision, 2);
// Every unrelated preference key is preserved verbatim.
assert.deepEqual(merged.interview_preferences, { focus_areas: ["behavioral"] });
assert.deepEqual(merged.resume_preferences, { target_title: "Data Scientist" });
assert.deepEqual(merged.mission_preferences, { active_goal: "land-offer" });
assert.deepEqual(merged.target_roles, ["Product Intern"]);
assert.deepEqual(merged.target_companies, ["Acme"]);
}
// ── 6. access_choice persistence (trial | full | null) ───────────────────────
{
const base = defaultOnboardingData();
const trial = { ...base, revision: 0, access_choice: "trial" as const };
const mergedTrial = mergeOnboardingPatch({}, trial);
assert.equal(extractOnboardingData(mergedTrial.onboarding).access_choice, "trial");
const full = { ...base, revision: 0, access_choice: "full" as const };
const mergedFull = mergeOnboardingPatch({}, full);
assert.equal(extractOnboardingData(mergedFull.onboarding).access_choice, "full");
// Garbage access_choice is rejected back to null by extraction.
const garbage = extractOnboardingData({ access_choice: "pro" });
assert.equal(garbage.access_choice, null);
}
console.log("onboarding-preferences: all assertions passed");