Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
708 lines
28 KiB
TypeScript
708 lines
28 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { existsSync, readFileSync } from "node:fs";
|
||
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");
|
||
const patchSource = readFileSync(new URL("../src/lib/account-profile-patch.ts", import.meta.url), "utf8");
|
||
const reportedStatusMigration = readFileSync(
|
||
new URL("../supabase/migrations/20260726010000_backfill_reported_birth_time_status.sql", import.meta.url),
|
||
"utf8",
|
||
);
|
||
const acceptedExactFamilyMigration = readFileSync(
|
||
new URL("../supabase/migrations/20260816010000_accept_exact_family_birth_times.sql", import.meta.url),
|
||
"utf8",
|
||
);
|
||
const selfHostedMigrator = readFileSync(
|
||
new URL("../scripts/db-migrate.mjs", import.meta.url),
|
||
"utf8",
|
||
);
|
||
|
||
test("account API reads and returns the server-configured rectification price", () => {
|
||
assert.match(source, /parseRectificationPriceCredits\(\s*process\.env\.RECTIFICATION_PRICE_CREDITS,?\s*\)/);
|
||
assert.match(source, /rectificationPriceCredits/);
|
||
assert.doesNotMatch(source, /RECTIFICATION_PRICE_CREDITS[^\n]*\?\?\s*["']1["']/);
|
||
});
|
||
|
||
test("account API separates engine confirmation from an accepted usable chart time", () => {
|
||
assert.match(source, /hasConfirmedBirthTime:\s*profile\.birth_time_status === "confirmed"/);
|
||
assert.match(source, /hasUsableBirthTime:\s*\(profile\.birth_time_status === "accepted" \|\| profile\.birth_time_status === "confirmed"\)/);
|
||
});
|
||
|
||
test("account API routes staging admins to the configured exact admin host", () => {
|
||
assert.match(source, /readSelfHostedIdentityConfig\(process\.env\)\.adminOrigin/);
|
||
assert.match(source, /const adminUrl = isAdmin \? adminEntryUrl\(\) : null/);
|
||
assert.match(source, /adminUrl,/);
|
||
assert.doesNotMatch(source, /NEXT_PUBLIC_ADMIN|AUTH_ADMIN_ORIGIN/);
|
||
});
|
||
|
||
test("account GET falls back when global birthplace columns are not migrated yet", () => {
|
||
const getSource = source.slice(source.indexOf("export async function GET"), source.indexOf("export async function PATCH"));
|
||
|
||
assert.match(getSource, /profileError && isMissingProfileColumn\(profileError\)/);
|
||
assert.match(getSource, /select\("credits,active_birth_time[^"]*timezone_offset(?:,[^"]*)?"\)/);
|
||
assert.match(getSource, /birth_place_label: undefined/);
|
||
assert.match(getSource, /timezone_id: undefined/);
|
||
assert.match(getSource, /timezone_source,ayanamsa,name,birth_time,rectification_case_id/);
|
||
});
|
||
|
||
test("account API no longer reads or returns retired rectification cases", () => {
|
||
assert.doesNotMatch(source, /birth_time_rectification_v4_cases|birth_time_rectification_cases/);
|
||
assert.doesNotMatch(source, /rectificationCase/);
|
||
});
|
||
|
||
test("profile patch schema validates calendar, clock, source requirements, and location bounds", () => {
|
||
const valid = {
|
||
name: "岳辰",
|
||
birth_date: "2000-02-29",
|
||
reported_birth_time: "05:30",
|
||
birth_time_source: "family_exact",
|
||
birth_time_period: null,
|
||
birth_time_clue: null,
|
||
uncertainty_before_minutes: 0,
|
||
uncertainty_after_minutes: 0,
|
||
country_code: "CN",
|
||
province_code: "130000",
|
||
city_code: "130400",
|
||
district_code: "130406",
|
||
latitude: 36.420487,
|
||
longitude: 114.209936,
|
||
timezone_offset: 8,
|
||
};
|
||
|
||
assert.equal(accountProfilePatchSchema.safeParse(valid).success, true);
|
||
assert.equal(accountProfilePatchSchema.safeParse({ ...valid, uncertainty_before_minutes: 10, uncertainty_after_minutes: 10 }).success, true);
|
||
assert.equal(accountProfilePatchSchema.safeParse({ ...valid, birth_date: "2001-02-29" }).success, false);
|
||
assert.equal(accountProfilePatchSchema.safeParse({ ...valid, reported_birth_time: "24:00" }).success, false);
|
||
assert.equal(accountProfilePatchSchema.safeParse({ ...valid, uncertainty_before_minutes: 7, uncertainty_after_minutes: 7 }).success, false);
|
||
assert.equal(accountProfilePatchSchema.safeParse({
|
||
...valid,
|
||
birth_time_source: "approximate",
|
||
uncertainty_before_minutes: 120,
|
||
uncertainty_after_minutes: 120,
|
||
}).success, true);
|
||
assert.equal(accountProfilePatchSchema.safeParse({
|
||
...valid,
|
||
birth_time_source: "approximate",
|
||
uncertainty_before_minutes: 90,
|
||
uncertainty_after_minutes: 90,
|
||
}).success, false);
|
||
assert.equal(accountProfilePatchSchema.safeParse({ ...valid, latitude: 91 }).success, false);
|
||
assert.equal(accountProfilePatchSchema.safeParse({ reported_birth_time: "05:30" }).success, false);
|
||
assert.equal(accountProfilePatchSchema.safeParse({ ...valid, longitude: null }).success, false);
|
||
assert.equal(accountProfilePatchSchema.safeParse({
|
||
...valid,
|
||
timezone_offset: null,
|
||
birth_place_label: "New York, New York, United States",
|
||
birth_place_type: "place",
|
||
birth_place_provider: "mapbox",
|
||
birth_place_provider_id: "place.123",
|
||
timezone_id: "America/New_York",
|
||
timezone_source: "iana_historical",
|
||
}).success, true);
|
||
assert.equal(accountProfilePatchSchema.safeParse({ name: "只改称呼" }).success, true);
|
||
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "lahiri" }).success, true);
|
||
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "kp" }).success, true);
|
||
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "true_pushya" }).success, true);
|
||
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "raman" }).success, true);
|
||
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "fagan" }).success, false);
|
||
assert.equal(accountProfilePatchSchema.safeParse({
|
||
...valid,
|
||
reported_birth_time: null,
|
||
birth_time_source: "period_only",
|
||
birth_time_period: null,
|
||
uncertainty_before_minutes: null,
|
||
uncertainty_after_minutes: null,
|
||
}).success, false);
|
||
assert.equal(accountProfilePatchSchema.safeParse({
|
||
...valid,
|
||
reported_birth_time: null,
|
||
birth_time_source: "period_only",
|
||
birth_time_period: "evening",
|
||
uncertainty_before_minutes: null,
|
||
uncertainty_after_minutes: null,
|
||
}).success, true);
|
||
assert.equal(accountProfilePatchSchema.safeParse({
|
||
...valid,
|
||
reported_birth_time: null,
|
||
birth_time_source: "period_only",
|
||
birth_time_period: null,
|
||
declared_window_start: "14:30",
|
||
declared_window_end: "15:00",
|
||
uncertainty_before_minutes: null,
|
||
uncertainty_after_minutes: null,
|
||
}).success, true);
|
||
assert.equal(accountProfilePatchSchema.safeParse({
|
||
...valid,
|
||
reported_birth_time: null,
|
||
birth_time_source: "period_only",
|
||
birth_time_period: null,
|
||
declared_window_start: "14:30",
|
||
declared_window_end: "14:30",
|
||
uncertainty_before_minutes: null,
|
||
uncertainty_after_minutes: null,
|
||
}).success, false);
|
||
assert.equal(accountProfilePatchSchema.safeParse({
|
||
...valid,
|
||
reported_birth_time: null,
|
||
birth_time_source: "unknown",
|
||
birth_time_period: "morning",
|
||
uncertainty_before_minutes: null,
|
||
uncertainty_after_minutes: null,
|
||
}).success, false);
|
||
});
|
||
|
||
test("ordinary declaration edits clear stale candidate application, and a new declaration drops a confirmed minute too", () => {
|
||
// 原值(测试名):…but never overwrite confirmed active time
|
||
// 新值:…and a new declaration drops a confirmed minute too
|
||
// 原因:产品 2026-09-29 推翻 BUG-264 约定:改了出生日期 / 时间 / 地点就按新资料排盘,即使原来已确认(TASK-mobile-chart-and-confirmed-edit M2)
|
||
const candidate = {
|
||
birth_date: "1997-08-08",
|
||
reported_birth_time: "05:30",
|
||
birth_time_source: "approximate",
|
||
birth_time_period: null,
|
||
birth_time_clue: null,
|
||
uncertainty_before_minutes: 30,
|
||
uncertainty_after_minutes: 30,
|
||
active_birth_time: "05:18",
|
||
birth_time: "05:18",
|
||
birth_time_status: "candidate",
|
||
rectification_case_id: "11111111-1111-4111-8111-111111111111",
|
||
latitude: 36.420487,
|
||
longitude: 114.209936,
|
||
timezone_offset: 8,
|
||
} as const;
|
||
const edited = {
|
||
birth_date: candidate.birth_date,
|
||
reported_birth_time: "06:10",
|
||
birth_time_source: candidate.birth_time_source,
|
||
birth_time_period: null,
|
||
birth_time_clue: null,
|
||
uncertainty_before_minutes: 30,
|
||
uncertainty_after_minutes: 30,
|
||
} as const;
|
||
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(candidate, edited), {
|
||
active_birth_time: null,
|
||
birth_time_status: "reported",
|
||
rectification_case_id: null,
|
||
});
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(candidate, {
|
||
district_code: "130407",
|
||
}), {
|
||
active_birth_time: null,
|
||
birth_time_status: "reported",
|
||
rectification_case_id: null,
|
||
});
|
||
for (const coordinatePatch of [
|
||
{ latitude: 36.420488 },
|
||
{ longitude: 114.209937 },
|
||
{ timezone_offset: 9 },
|
||
]) {
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(candidate, coordinatePatch), {
|
||
active_birth_time: null,
|
||
birth_time_status: "reported",
|
||
rectification_case_id: null,
|
||
});
|
||
}
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||
...candidate,
|
||
birth_time_status: "accepted",
|
||
}, edited), {
|
||
active_birth_time: null,
|
||
birth_time_status: "reported",
|
||
rectification_case_id: null,
|
||
});
|
||
// 原值:confirmed + 新声明 → {}(已确认分钟不动)
|
||
// 新值:confirmed + 新声明 → 回到 reported,并清掉遗留 birth_time(图表会回落读它)
|
||
// 原因:产品 2026-09-29 M2;只改岁差仍然不动(下一条断言原样保留)
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||
...candidate,
|
||
birth_time_status: "confirmed",
|
||
}, edited), {
|
||
active_birth_time: null,
|
||
birth_time_status: "reported",
|
||
rectification_case_id: null,
|
||
birth_time: null,
|
||
});
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||
...candidate,
|
||
birth_time_status: "confirmed",
|
||
}, { ayanamsa: "lahiri" }), {});
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||
...candidate,
|
||
active_birth_time: null,
|
||
birth_time_status: "reported",
|
||
}, { timezone_offset: 7 }), {
|
||
active_birth_time: null,
|
||
birth_time_status: "reported",
|
||
rectification_case_id: null,
|
||
});
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||
...candidate,
|
||
active_birth_time: null,
|
||
birth_time: null,
|
||
birth_time_status: null,
|
||
rectification_case_id: null,
|
||
}, edited), {
|
||
active_birth_time: null,
|
||
birth_time_status: "reported",
|
||
rectification_case_id: null,
|
||
});
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||
...candidate,
|
||
active_birth_time: null,
|
||
birth_time: null,
|
||
birth_time_status: null,
|
||
rectification_case_id: null,
|
||
reported_birth_time: edited.reported_birth_time,
|
||
}, edited), {
|
||
active_birth_time: null,
|
||
birth_time_status: "reported",
|
||
rectification_case_id: null,
|
||
});
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(null, edited), {
|
||
active_birth_time: null,
|
||
birth_time_status: "reported",
|
||
rectification_case_id: null,
|
||
});
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||
...candidate,
|
||
birth_time_status: "accepted",
|
||
}, edited), {
|
||
active_birth_time: null,
|
||
birth_time_status: "reported",
|
||
rectification_case_id: null,
|
||
});
|
||
// 原值 / 新值 / 原因:同上一处 confirmed 断言(产品 2026-09-29 M2)
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||
...candidate,
|
||
birth_time_status: "confirmed",
|
||
}, edited), {
|
||
active_birth_time: null,
|
||
birth_time_status: "reported",
|
||
rectification_case_id: null,
|
||
birth_time: null,
|
||
});
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||
...candidate,
|
||
active_birth_time: null,
|
||
birth_time_status: null,
|
||
reported_birth_time: edited.reported_birth_time,
|
||
}, edited), {});
|
||
});
|
||
|
||
test("zero-uncertainty family exact time becomes an accepted usable chart time", () => {
|
||
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;
|
||
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(null, exactDeclaration), {
|
||
active_birth_time: "05:00",
|
||
birth_time_status: "accepted",
|
||
rectification_case_id: null,
|
||
});
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(reportedExactProfile, exactDeclaration), {
|
||
active_birth_time: "05:00",
|
||
birth_time_status: "accepted",
|
||
rectification_case_id: null,
|
||
});
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||
...reportedExactProfile,
|
||
active_birth_time: "05:00:00",
|
||
birth_time_status: "accepted",
|
||
}, {
|
||
...exactDeclaration,
|
||
reported_birth_time: "05:40",
|
||
}), {
|
||
active_birth_time: "05:40",
|
||
birth_time_status: "accepted",
|
||
rectification_case_id: null,
|
||
});
|
||
// 原值:confirmed / legacy 分钟 + 新的精确家人时间 → {}
|
||
// 新值:→ accepted + 新分钟;legacy 行另清遗留 birth_time
|
||
// 原因:产品 2026-09-29 M2,已确认分钟也跟随新声明
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||
...reportedExactProfile,
|
||
active_birth_time: "05:00:00",
|
||
birth_time_status: "confirmed",
|
||
}, {
|
||
...exactDeclaration,
|
||
reported_birth_time: "05:40",
|
||
}), {
|
||
active_birth_time: "05:40",
|
||
birth_time_status: "accepted",
|
||
rectification_case_id: null,
|
||
});
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||
...reportedExactProfile,
|
||
active_birth_time: "05:00:00",
|
||
birth_time: "05:00:00",
|
||
birth_time_status: null,
|
||
}, {
|
||
...exactDeclaration,
|
||
reported_birth_time: "05:40",
|
||
}), {
|
||
active_birth_time: "05:40",
|
||
birth_time_status: "accepted",
|
||
rectification_case_id: null,
|
||
birth_time: null,
|
||
});
|
||
});
|
||
|
||
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.
|
||
// 原值:confirmed / legacy 分钟 + 改 district_code → 仍 confirmed 05:18
|
||
// 新值:改地点属于新声明 → 按声明走(本夹具声明是零误差家人时间 05:00,于是 accepted 05:00);只改称呼 / 岁差仍保留 confirmed 05:18
|
||
// 原因:产品 2026-09-29 M2(改日期、时间或地点就按新资料排盘)
|
||
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: "accepted", activeTime: "05:00" },
|
||
);
|
||
assert.deepEqual(
|
||
resolveAppliedAccountBirthTime(
|
||
confirmedProfile,
|
||
resolveAccountBirthTimeApplicationPatch(confirmedProfile, { name: "示例", ayanamsa: "lahiri" }),
|
||
),
|
||
{ 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: "accepted", activeTime: "05:00" },
|
||
);
|
||
assert.deepEqual(
|
||
resolveAppliedAccountBirthTime(
|
||
legacyProfile,
|
||
resolveAccountBirthTimeApplicationPatch(legacyProfile, { name: "示例" }),
|
||
),
|
||
{ status: "confirmed", activeTime: "05:18" },
|
||
);
|
||
assert.deepEqual(
|
||
resolveAppliedAccountBirthTime(null, resolveAccountBirthTimeApplicationPatch(null, { name: "岳辰" })),
|
||
{ status: null, activeTime: null },
|
||
);
|
||
|
||
assert.match(source, /\.\.\.resolveAppliedAccountBirthTime\(currentProfile, applicationPatch\)/);
|
||
assert.match(source, /activeDate: data\.active_birth_date \?\? null/);
|
||
assert.match(source, /activeTimezoneOffset: data\.active_birth_timezone_offset \?\? null/);
|
||
});
|
||
|
||
test("only a strict family exact zero-uncertainty declaration is auto-accepted", () => {
|
||
const base = {
|
||
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 declarations = [
|
||
{ ...base, uncertainty_before_minutes: 5, uncertainty_after_minutes: 5 },
|
||
{ ...base, uncertainty_before_minutes: 10, uncertainty_after_minutes: 10 },
|
||
{ ...base, uncertainty_before_minutes: 15, uncertainty_after_minutes: 15 },
|
||
{ ...base, birth_time_source: "approximate", uncertainty_before_minutes: 30, uncertainty_after_minutes: 30 },
|
||
{
|
||
...base,
|
||
reported_birth_time: null,
|
||
birth_time_source: "period_only",
|
||
birth_time_period: "morning",
|
||
uncertainty_before_minutes: null,
|
||
uncertainty_after_minutes: null,
|
||
},
|
||
{
|
||
...base,
|
||
reported_birth_time: null,
|
||
birth_time_source: "unknown",
|
||
uncertainty_before_minutes: null,
|
||
uncertainty_after_minutes: null,
|
||
},
|
||
] as const;
|
||
|
||
for (const declaration of declarations) {
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(null, declaration), {
|
||
active_birth_time: null,
|
||
birth_time_status: "reported",
|
||
rectification_case_id: null,
|
||
});
|
||
}
|
||
});
|
||
|
||
test("reported birth-time status repair is forward-only and applied by the self-hosted migrator", () => {
|
||
assert.match(reportedStatusMigration, /birth_time_status is null/);
|
||
assert.match(reportedStatusMigration, /birth_time_status = 'reported'/);
|
||
assert.match(reportedStatusMigration, /active_birth_time is null/);
|
||
assert.match(reportedStatusMigration, /rectification_case_id is null/);
|
||
// Staging and production migrations run through db-migrate.mjs, which applies
|
||
// frontend/supabase/migrations as the compatibility directory; the repair file
|
||
// must stay there rather than in a retired workflow's explicit file list.
|
||
assert.ok(
|
||
existsSync(new URL("../supabase/migrations/20260726010000_backfill_reported_birth_time_status.sql", import.meta.url)),
|
||
);
|
||
assert.match(selfHostedMigrator, /"\.\.\/supabase\/migrations"/);
|
||
assert.match(selfHostedMigrator, /supabaseCompatibilityDirectory,/);
|
||
});
|
||
|
||
test("profile ayanamsa is added by the self-hosted migrator with Raman as the default", () => {
|
||
const ayanamsaMigration = readFileSync(
|
||
new URL("../supabase/migrations/20260903010000_profile_ayanamsa.sql", import.meta.url),
|
||
"utf8",
|
||
);
|
||
assert.match(ayanamsaMigration, /add column if not exists ayanamsa text not null default 'raman'/);
|
||
assert.match(ayanamsaMigration, /'raman', 'lahiri', 'kp', 'true_pushya'/);
|
||
assert.equal(
|
||
existsSync(new URL("../db/migrations/20260903010000_profile_ayanamsa.sql", import.meta.url)),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test("existing exact family declarations are forward-repaired without claiming confirmation", () => {
|
||
assert.match(acceptedExactFamilyMigration, /update public\.profiles/);
|
||
assert.match(acceptedExactFamilyMigration, /active_birth_time = reported_birth_time/);
|
||
assert.match(acceptedExactFamilyMigration, /birth_time_status = 'accepted'/);
|
||
assert.match(acceptedExactFamilyMigration, /birth_time_source = 'family_exact'/);
|
||
assert.match(acceptedExactFamilyMigration, /uncertainty_before_minutes = 0/);
|
||
assert.match(acceptedExactFamilyMigration, /uncertainty_after_minutes = 0/);
|
||
assert.match(acceptedExactFamilyMigration, /birth_time_status = 'reported'/);
|
||
assert.match(acceptedExactFamilyMigration, /active_birth_time is null/);
|
||
assert.match(acceptedExactFamilyMigration, /rectification_case_id is null/);
|
||
assert.doesNotMatch(acceptedExactFamilyMigration, /birth_time_status = 'confirmed'/);
|
||
assert.equal(
|
||
existsSync(new URL("../db/migrations/20260816010000_accept_exact_family_birth_times.sql", import.meta.url)),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test("account PATCH uses the shared validator and never writes client birth_time over account truth", () => {
|
||
assert.match(source, /accountProfilePatchSchema\.safeParse/);
|
||
assert.match(source, /resolveAccountBirthTimeApplicationPatch/);
|
||
assert.match(patchSource, /birthTimeSource[\s\S]*birth_time_status: "reported"/);
|
||
assert.doesNotMatch(source, /birth_time:\s*nullableString\(payload\.birth_time\)/);
|
||
assert.match(source, /invalidatesUnconfirmedApplication/);
|
||
assert.match(patchSource, /"birth_time_status"/);
|
||
assert.match(patchSource, /"active_birth_time"/);
|
||
assert.match(source, /latitude,longitude,timezone_offset/);
|
||
assert.match(source, /birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source/);
|
||
assert.match(source, /payload\.ayanamsa !== undefined \? \{ ayanamsa: payload\.ayanamsa \}/);
|
||
assert.match(patchSource, /ayanamsa: z\.enum\(AYANAMSA_VALUES\)\.optional\(\)/);
|
||
const declarationBlock = patchSource.slice(
|
||
patchSource.indexOf("const declarationFields"),
|
||
patchSource.indexOf("const concurrencyFields"),
|
||
);
|
||
assert.doesNotMatch(declarationBlock, /ayanamsa/);
|
||
assert.match(source, /applyAccountProfileConcurrencyGuards/);
|
||
assert.match(source, /最新确认结果已保留/);
|
||
});
|
||
|
||
test("candidate invalidation compares coordinates and timezone in the conditional write", async () => {
|
||
const { applyAccountProfileConcurrencyGuards } = await import("../src/lib/account-profile-patch.ts");
|
||
const calls: Array<["eq" | "is", string, unknown]> = [];
|
||
const query = {
|
||
eq(column: string, value: unknown) {
|
||
calls.push(["eq", column, value]);
|
||
return this;
|
||
},
|
||
is(column: string, value: null) {
|
||
calls.push(["is", column, value]);
|
||
return this;
|
||
},
|
||
};
|
||
const current = {
|
||
birth_date: "1997-08-08",
|
||
reported_birth_time: "05:30",
|
||
birth_time_source: "approximate",
|
||
birth_time_period: null,
|
||
birth_time_clue: null,
|
||
uncertainty_before_minutes: 30,
|
||
uncertainty_after_minutes: 30,
|
||
active_birth_time: "05:18",
|
||
birth_time: "05:18",
|
||
birth_time_status: "candidate",
|
||
rectification_case_id: "11111111-1111-4111-8111-111111111111",
|
||
country_code: "CN",
|
||
province_code: "130000",
|
||
city_code: "130400",
|
||
district_code: "130406",
|
||
latitude: 36.420487,
|
||
longitude: 114.209936,
|
||
timezone_offset: 8,
|
||
};
|
||
|
||
applyAccountProfileConcurrencyGuards(query, current);
|
||
|
||
assert.deepEqual(calls.filter((call) => ["latitude", "longitude", "timezone_offset"].includes(call[1])), [
|
||
["eq", "latitude", 36.420487],
|
||
["eq", "longitude", 114.209936],
|
||
["eq", "timezone_offset", 8],
|
||
]);
|
||
assert.deepEqual(calls.find((call) => call[1] === "active_birth_time"), ["eq", "active_birth_time", "05:18"]);
|
||
assert.deepEqual(calls.find((call) => call[1] === "birth_time_status"), ["eq", "birth_time_status", "candidate"]);
|
||
});
|
||
|
||
// TASK-mobile-chart-and-confirmed-edit-20260929 M2 (product 2026-09-29): a new
|
||
// declaration drops a confirmed or adopted minute; nothing else does.
|
||
test("confirmed minute follows a new declaration and survives everything else", () => {
|
||
const confirmed = {
|
||
birth_date: "1994-05-18",
|
||
reported_birth_time: "07:40:00",
|
||
birth_time_source: "approximate",
|
||
birth_time_period: null,
|
||
birth_time_clue: null,
|
||
uncertainty_before_minutes: 30,
|
||
uncertainty_after_minutes: 30,
|
||
active_birth_time: "07:52:00",
|
||
active_birth_date: "1994-05-18",
|
||
active_birth_timezone_offset: 8,
|
||
birth_time: null,
|
||
birth_time_status: "confirmed",
|
||
rectification_case_id: "11111111-1111-4111-8111-111111111111",
|
||
country_code: "CN",
|
||
province_code: "330000",
|
||
city_code: "330100",
|
||
district_code: "330106",
|
||
latitude: 30.27,
|
||
longitude: 120.15,
|
||
timezone_offset: 8,
|
||
} as const;
|
||
// The form resubmits every birth field; stored `HH:MM:SS` vs submitted `HH:MM` is not a change.
|
||
const resubmitted = {
|
||
birth_date: "1994-05-18",
|
||
reported_birth_time: "07:40",
|
||
birth_time_source: "approximate",
|
||
birth_time_period: null,
|
||
birth_time_clue: null,
|
||
uncertainty_before_minutes: 30,
|
||
uncertainty_after_minutes: 30,
|
||
country_code: "CN",
|
||
province_code: "330000",
|
||
city_code: "330100",
|
||
district_code: "330106",
|
||
latitude: 30.27,
|
||
longitude: 120.15,
|
||
timezone_offset: 8,
|
||
} as const;
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(confirmed, { ...resubmitted, name: "示例" }), {});
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(confirmed, { ayanamsa: "lahiri" }), {});
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(confirmed, { gender: "female" }), {});
|
||
|
||
const cleared = { active_birth_date: null, active_birth_timezone_offset: null, active_birth_provenance: null };
|
||
// New exact family time → accepted at the new minute.
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(confirmed, {
|
||
...resubmitted,
|
||
reported_birth_time: "08:05",
|
||
birth_time_source: "family_exact",
|
||
uncertainty_before_minutes: 0,
|
||
uncertainty_after_minutes: 0,
|
||
}), { active_birth_time: "08:05", birth_time_status: "accepted", rectification_case_id: null, ...cleared });
|
||
// Period only → reported, no minute.
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(confirmed, {
|
||
...resubmitted,
|
||
reported_birth_time: null,
|
||
birth_time_source: "period_only",
|
||
birth_time_period: "morning",
|
||
uncertainty_before_minutes: null,
|
||
uncertainty_after_minutes: null,
|
||
}), { active_birth_time: null, birth_time_status: "reported", rectification_case_id: null, ...cleared });
|
||
// A new date drops the adopted cross-midnight date, offset and provenance.
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(confirmed, { ...resubmitted, birth_date: "1994-05-19" }),
|
||
{ active_birth_time: null, birth_time_status: "reported", rectification_case_id: null, ...cleared });
|
||
// A new place (coordinates) is a new declaration too.
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(confirmed, { ...resubmitted, latitude: 31.23, longitude: 121.47 }),
|
||
{ active_birth_time: null, birth_time_status: "reported", rectification_case_id: null, ...cleared });
|
||
// Without adoption columns, nothing extra is written.
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||
...confirmed, active_birth_date: null, active_birth_timezone_offset: null,
|
||
}, { ...resubmitted, birth_date: "1994-05-19" }),
|
||
{ active_birth_time: null, birth_time_status: "reported", rectification_case_id: null });
|
||
|
||
// Legacy minute (status null, birth_time set): same rule, and the legacy column the chart falls back to is cleared.
|
||
const legacy = { ...confirmed, active_birth_time: null, active_birth_date: null, active_birth_timezone_offset: null, birth_time: "07:52:00", birth_time_status: null };
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(legacy, { ...resubmitted, name: "示例" }), {});
|
||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(legacy, { ...resubmitted, birth_date: "1994-05-19" }),
|
||
{ active_birth_time: null, birth_time_status: "reported", rectification_case_id: null, birth_time: null });
|
||
});
|