161 lines
7.0 KiB
TypeScript
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");
|