fix(account): answer profile writes with the derived birth-time truth
Independent Staging Quality Gate / validate (push) Failing after 10m56s
Independent Staging Quality Gate / publish (push) Has been skipped

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:
Jesse_Chen
2026-08-17 22:13:03 +08:00
co-authored by Cursor
parent 6ce4e67186
commit fd415e1d11
10 changed files with 254 additions and 27 deletions
+85
View File
@@ -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, /当前使用候选时间排盘/);
});
+45
View File
@@ -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\(\)/);