fix(account): answer profile writes with the derived birth-time truth
A zero-uncertainty exact declaration is accepted server-side as the active
minute, but the account write only answered {ok:true}. Every save path then
kept the draft it submitted, so the first consultation after initialization
asked for unverified_birth_time against an accepted profile and was rejected
with mode_changed before billing.
The account route now returns the status and active minute it derived, and
every profile save adopts that result instead of its own local guess.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import test from "node:test";
|
||||
import {
|
||||
accountProfilePatchSchema,
|
||||
resolveAccountBirthTimeApplicationPatch,
|
||||
resolveAppliedAccountBirthTime,
|
||||
} from "../src/lib/account-profile-patch.ts";
|
||||
|
||||
const source = readFileSync(new URL("../src/app/api/account/route.ts", import.meta.url), "utf8");
|
||||
@@ -288,6 +289,90 @@ test("zero-uncertainty family exact time becomes an accepted usable chart time",
|
||||
}), {});
|
||||
});
|
||||
|
||||
test("account PATCH answers with the birth-time truth it derived server-side", () => {
|
||||
const exactDeclaration = {
|
||||
birth_date: "1997-08-08",
|
||||
reported_birth_time: "05:00",
|
||||
birth_time_source: "family_exact",
|
||||
birth_time_period: null,
|
||||
birth_time_clue: null,
|
||||
uncertainty_before_minutes: 0,
|
||||
uncertainty_after_minutes: 0,
|
||||
} as const;
|
||||
const reportedExactProfile = {
|
||||
...exactDeclaration,
|
||||
reported_birth_time: "05:00:00",
|
||||
active_birth_time: null,
|
||||
birth_time: null,
|
||||
birth_time_status: "reported",
|
||||
rectification_case_id: null,
|
||||
} as const;
|
||||
|
||||
// A client that keeps the declaration it submitted would consult as
|
||||
// unverified_birth_time and be rejected by the consultation truth check.
|
||||
assert.deepEqual(
|
||||
resolveAppliedAccountBirthTime(
|
||||
null,
|
||||
resolveAccountBirthTimeApplicationPatch(null, exactDeclaration),
|
||||
),
|
||||
{ status: "accepted", activeTime: "05:00" },
|
||||
);
|
||||
assert.deepEqual(
|
||||
resolveAppliedAccountBirthTime(
|
||||
reportedExactProfile,
|
||||
resolveAccountBirthTimeApplicationPatch(reportedExactProfile, exactDeclaration),
|
||||
),
|
||||
{ status: "accepted", activeTime: "05:00" },
|
||||
);
|
||||
const periodDeclaration = {
|
||||
...exactDeclaration,
|
||||
reported_birth_time: null,
|
||||
birth_time_source: "period_only",
|
||||
birth_time_period: "early_morning",
|
||||
uncertainty_before_minutes: null,
|
||||
uncertainty_after_minutes: null,
|
||||
} as const;
|
||||
assert.deepEqual(
|
||||
resolveAppliedAccountBirthTime(
|
||||
reportedExactProfile,
|
||||
resolveAccountBirthTimeApplicationPatch(reportedExactProfile, periodDeclaration),
|
||||
),
|
||||
{ status: "reported", activeTime: null },
|
||||
);
|
||||
|
||||
// An untouched application keeps the stored truth, including a legacy confirmed minute.
|
||||
const confirmedProfile = {
|
||||
...reportedExactProfile,
|
||||
active_birth_time: "05:18:00",
|
||||
birth_time_status: "confirmed",
|
||||
} as const;
|
||||
assert.deepEqual(
|
||||
resolveAppliedAccountBirthTime(
|
||||
confirmedProfile,
|
||||
resolveAccountBirthTimeApplicationPatch(confirmedProfile, { district_code: "130407" }),
|
||||
),
|
||||
{ status: "confirmed", activeTime: "05:18" },
|
||||
);
|
||||
const legacyProfile = {
|
||||
...reportedExactProfile,
|
||||
birth_time: "05:18:00",
|
||||
birth_time_status: null,
|
||||
} as const;
|
||||
assert.deepEqual(
|
||||
resolveAppliedAccountBirthTime(
|
||||
legacyProfile,
|
||||
resolveAccountBirthTimeApplicationPatch(legacyProfile, { district_code: "130407" }),
|
||||
),
|
||||
{ status: "confirmed", activeTime: "05:18" },
|
||||
);
|
||||
assert.deepEqual(
|
||||
resolveAppliedAccountBirthTime(null, resolveAccountBirthTimeApplicationPatch(null, { name: "岳辰" })),
|
||||
{ status: null, activeTime: null },
|
||||
);
|
||||
|
||||
assert.match(source, /birthTime: resolveAppliedAccountBirthTime\(currentProfile, applicationPatch\)/);
|
||||
});
|
||||
|
||||
test("only a strict family exact zero-uncertainty declaration is auto-accepted", () => {
|
||||
const base = {
|
||||
birth_date: "1997-08-08",
|
||||
|
||||
@@ -225,7 +225,7 @@ test("homepage and profile result copy use the source-aware consultation options
|
||||
const page = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
||||
const intake = readFileSync(new URL("../src/components/birth-time-intake.tsx", import.meta.url), "utf8");
|
||||
|
||||
assert.match(page, /birthTimeConsultationOptionsCopy\(profileDraft\)/);
|
||||
assert.match(page, /birthTimeConsultationOptionsCopy\(savedProfile\)/);
|
||||
assert.doesNotMatch(page, /birthTimeConsultationOptionsCopy\(profile\)/);
|
||||
assert.match(intake, /birthTimeConsultationOptionsCopy\(value\)/);
|
||||
});
|
||||
|
||||
@@ -150,7 +150,7 @@ test("terminal CJK copy stays intact while homepage candidates remain unconfirme
|
||||
const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
||||
|
||||
assert.match(candidateResultSource, /候选范围已保留,但当前证据不足以将具体分钟写入当前排盘时间。补充经历后可重新评估。/);
|
||||
assert.match(pageSource, /`出生资料已保存。\$\{birthTimeConsultationOptionsCopy\(profileDraft\)\}`/);
|
||||
assert.match(pageSource, /`出生资料已保存。\$\{birthTimeConsultationOptionsCopy\(savedProfile\)\}`/);
|
||||
assert.match(pageSource, /<ConversationalBirthTimeRectification/);
|
||||
assert.doesNotMatch(pageSource, /当前使用候选时间排盘/);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import {
|
||||
applyBirthTimeDraftPatch,
|
||||
applyPersistedBirthTime,
|
||||
assistantIntentCopy,
|
||||
birthTimeDisplayState,
|
||||
birthTimeDraftReadyHint,
|
||||
@@ -350,3 +351,47 @@ test("fresh intake preserves exact, approximate-period, and unknown-time paths w
|
||||
assert.match(source, /birthTimeStatus: "reported"/);
|
||||
assert.match(source, /birthTimeConsultationOptionsCopy\(value\)/);
|
||||
});
|
||||
|
||||
test("a saved declaration adopts the birth-time truth the account write returned", () => {
|
||||
const exactDeclaration = {
|
||||
...emptyDraft,
|
||||
birthTimeSource: "family_exact",
|
||||
reportedTime: "05:00",
|
||||
uncertaintyBeforeMinutes: 0,
|
||||
uncertaintyAfterMinutes: 0,
|
||||
birthTimeStatus: "reported",
|
||||
} as const satisfies BirthTimeDraft;
|
||||
|
||||
// The server accepts a zero-uncertainty exact time as the active minute, so the
|
||||
// submitted draft must not keep claiming the time is still only reported.
|
||||
assert.deepEqual(
|
||||
applyPersistedBirthTime(exactDeclaration, { status: "accepted", activeTime: "05:00" }),
|
||||
{ ...exactDeclaration, time: "05:00", birthTimeStatus: "accepted" },
|
||||
);
|
||||
assert.deepEqual(
|
||||
applyPersistedBirthTime({ ...exactDeclaration, time: "05:00", birthTimeStatus: "accepted" }, {
|
||||
status: "reported",
|
||||
activeTime: null,
|
||||
}),
|
||||
exactDeclaration,
|
||||
);
|
||||
assert.deepEqual(
|
||||
applyPersistedBirthTime(exactDeclaration, { status: "accepted", activeTime: "05:00:00" }),
|
||||
{ ...exactDeclaration, time: "05:00", birthTimeStatus: "accepted" },
|
||||
);
|
||||
|
||||
// A response without usable birth-time truth must leave the draft untouched.
|
||||
for (const applied of [
|
||||
undefined,
|
||||
null,
|
||||
{},
|
||||
{ status: "unknown_status", activeTime: "05:00" },
|
||||
{ status: 7, activeTime: "05:00" },
|
||||
]) {
|
||||
assert.deepEqual(applyPersistedBirthTime(exactDeclaration, applied), exactDeclaration);
|
||||
}
|
||||
assert.deepEqual(
|
||||
applyPersistedBirthTime(exactDeclaration, { status: "accepted", activeTime: "5:0" }),
|
||||
{ ...exactDeclaration, time: "", birthTimeStatus: "accepted" },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,24 @@ test("upserts a missing profile when saving account details", () => {
|
||||
assert.match(source, /credentials:\s*"same-origin"/);
|
||||
});
|
||||
|
||||
test("every profile save adopts the birth-time truth the account write returned", () => {
|
||||
const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
||||
|
||||
// Given: the account write derives birth-time status and active minute.
|
||||
assert.match(source, /const savedProfile = applyPersistedBirthTime\(nextProfile, payload\?\.birthTime\)/);
|
||||
assert.match(source, /return savedProfile;/);
|
||||
|
||||
// When: any save path resolves.
|
||||
// Then: it must apply the returned profile, or the next consultation runs under a
|
||||
// mode the server rejects with mode_changed.
|
||||
const savePaths = source.match(/[^\n]*await persistProfile\([^\n]*/g) ?? [];
|
||||
assert.equal(savePaths.length > 0, true);
|
||||
for (const savePath of savePaths) {
|
||||
assert.match(savePath, /const savedProfile = await persistProfile\(/);
|
||||
}
|
||||
assert.doesNotMatch(source, /setProfile\(profileDraft\)/);
|
||||
});
|
||||
|
||||
test("account route upserts profiles with the server admin client", () => {
|
||||
const source = readFileSync(new URL("../src/app/api/account/route.ts", import.meta.url), "utf8");
|
||||
assert.match(source, /createAdminSupabaseClient\(\)/);
|
||||
|
||||
Reference in New Issue
Block a user