Files
Jyotisha/frontend/tests/onboarding-cache-policy.test.ts

209 lines
7.5 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import {
createOnboardingCacheIdentity,
decideOnboardingCache,
ONBOARDING_CLAIM_TTL_MS,
} from "../src/lib/onboarding-cache-policy.ts";
const profileA = {
name: "林遥",
birthDate: "1990-06-15",
birthTime: "12:30",
activeBirthTime: "12:30",
birthTimeStatus: "confirmed",
countryCode: "CN",
provinceCode: "110000",
cityCode: "110100",
} as const;
test("changed profile cannot reuse a ready cache from the previous profile", () => {
// Given: profile A has a valid ready cache.
const identityA = createOnboardingCacheIdentity(profileA);
const identityB = createOnboardingCacheIdentity({ ...profileA, name: "周宁" });
// When: profile B observes A's ready version.
const decision = decideOnboardingCache({
identity: identityB,
observedVersion: identityA.readyVersion,
generatedAtMs: Date.parse("2026-07-19T10:00:00.000Z"),
nowMs: Date.parse("2026-07-19T10:00:01.000Z"),
cachedPayload: {},
});
// Then: B must claim its own pending identity from the exact observed version.
assert.deepEqual(decision, {
kind: "claim",
expectedVersion: identityA.readyVersion,
pendingVersion: identityB.pendingVersion,
});
assert.notEqual(identityA.readyVersion, identityB.readyVersion);
assert.doesNotMatch(identityB.readyVersion, /周宁|1990|12:30|110000/);
});
test("changed profile cannot wait on the previous profile's active pending claim", () => {
// Given: profile A has a fresh pending claim.
const identityA = createOnboardingCacheIdentity(profileA);
const identityB = createOnboardingCacheIdentity({ ...profileA, activeBirthTime: "12:45" });
// When: profile B observes A's pending version within the claim TTL.
const decision = decideOnboardingCache({
identity: identityB,
observedVersion: identityA.pendingVersion,
generatedAtMs: Date.parse("2026-07-19T10:00:00.000Z"),
nowMs: Date.parse("2026-07-19T10:00:01.000Z"),
cachedPayload: null,
});
// Then: B claims immediately instead of returning A's pending response.
assert.deepEqual(decision, {
kind: "claim",
expectedVersion: identityA.pendingVersion,
pendingVersion: identityB.pendingVersion,
});
});
test("period-only declaration fields participate in the onboarding cache identity", () => {
const earlyMorning = {
...profileA,
birthTime: null,
activeBirthTime: null,
birthTimeStatus: "reported",
reportedBirthTime: null,
birthTimeSource: "period_only",
birthTimePeriod: "early_morning",
birthTimeClue: "凌晨或清晨",
uncertaintyBeforeMinutes: null,
uncertaintyAfterMinutes: null,
};
const morning = { ...earlyMorning, birthTimePeriod: "morning" };
assert.notEqual(
createOnboardingCacheIdentity(earlyMorning).readyVersion,
createOnboardingCacheIdentity(morning).readyVersion,
);
});
test("current profile accepts only valid ready content and an active current pending claim", () => {
// Given: one current profile identity and a valid cached payload.
const identity = createOnboardingCacheIdentity(profileA);
const payload = { greeting: "current" };
const nowMs = Date.parse("2026-07-19T10:01:00.000Z");
// When/Then: exact ready and fresh pending identities retain their existing behavior.
assert.deepEqual(decideOnboardingCache({
identity,
observedVersion: identity.readyVersion,
generatedAtMs: nowMs - 60_000,
nowMs,
cachedPayload: payload,
}), { kind: "ready", payload });
assert.deepEqual(decideOnboardingCache({
identity,
observedVersion: identity.pendingVersion,
generatedAtMs: nowMs - 60_000,
nowMs,
cachedPayload: null,
}), { kind: "pending" });
});
test("invalid ready content and expired pending claims are reclaimed", () => {
// Given: the current profile sees unusable ready content or an expired pending claim.
const identity = createOnboardingCacheIdentity(profileA);
const nowMs = Date.parse("2026-07-19T10:03:00.000Z");
const observations = [
{
identity,
observedVersion: identity.readyVersion,
generatedAtMs: nowMs - 1_000,
nowMs,
cachedPayload: null,
},
{
identity,
observedVersion: identity.pendingVersion,
generatedAtMs: nowMs - 180_000,
nowMs,
cachedPayload: null,
},
] as const;
// When/Then: each stale state becomes a compare-and-set claim for the current identity.
for (const observation of observations) {
assert.deepEqual(decideOnboardingCache(observation), {
kind: "claim",
expectedVersion: observation.observedVersion,
pendingVersion: identity.pendingVersion,
});
}
});
test("every selected profile input participates in the cache identity", () => {
// Given: the exact eight profile inputs used by onboarding generation/completeness.
const baseIdentity = createOnboardingCacheIdentity(profileA);
const mutations = [
{ field: "name", profile: { ...profileA, name: "周宁" } },
{ field: "birthDate", profile: { ...profileA, birthDate: "1991-06-15" } },
{ field: "birthTime", profile: { ...profileA, birthTime: "12:31" } },
{ field: "activeBirthTime", profile: { ...profileA, activeBirthTime: "12:45" } },
{ field: "birthTimeStatus", profile: { ...profileA, birthTimeStatus: "candidate" } },
{ field: "countryCode", profile: { ...profileA, countryCode: "TW" } },
{ field: "provinceCode", profile: { ...profileA, provinceCode: "310000" } },
{ field: "cityCode", profile: { ...profileA, cityCode: "310100" } },
] as const;
// When/Then: mutating any one input changes both ready and pending identities.
for (const mutation of mutations) {
const changed = createOnboardingCacheIdentity(mutation.profile);
assert.notEqual(changed.readyVersion, baseIdentity.readyVersion, mutation.field);
assert.notEqual(changed.pendingVersion, baseIdentity.pendingVersion, mutation.field);
}
});
test("pending claim TTL is active through TTL minus one and reclaimable at TTL", () => {
// Given: the current profile owns the pending identity at a fixed time.
const identity = createOnboardingCacheIdentity(profileA);
const nowMs = Date.parse("2026-07-19T10:03:00.000Z");
// When/Then: the exact boundary preserves the existing strict-less-than policy.
assert.deepEqual(decideOnboardingCache({
identity,
observedVersion: identity.pendingVersion,
generatedAtMs: nowMs - ONBOARDING_CLAIM_TTL_MS + 1,
nowMs,
cachedPayload: null,
}), { kind: "pending" });
assert.deepEqual(decideOnboardingCache({
identity,
observedVersion: identity.pendingVersion,
generatedAtMs: nowMs - ONBOARDING_CLAIM_TTL_MS,
nowMs,
cachedPayload: null,
}), {
kind: "claim",
expectedVersion: identity.pendingVersion,
pendingVersion: identity.pendingVersion,
});
});
test("pending claims with null or invalid generation timestamps are reclaimed", () => {
// Given: null and invalid timestamps have both been normalized to non-finite milliseconds.
const identity = createOnboardingCacheIdentity(profileA);
const observations = [Number.NaN, Date.parse("not-a-timestamp")];
// When/Then: neither timestamp can keep a pending claim active.
for (const generatedAtMs of observations) {
assert.deepEqual(decideOnboardingCache({
identity,
observedVersion: identity.pendingVersion,
generatedAtMs,
nowMs: Date.parse("2026-07-19T10:03:00.000Z"),
cachedPayload: null,
}), {
kind: "claim",
expectedVersion: identity.pendingVersion,
pendingVersion: identity.pendingVersion,
});
}
});