import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { homeSurface as source } from "./home-surface.ts"; const route = readFileSync(new URL("../src/app/api/chart-profiles/route.ts", import.meta.url), "utf8"); test("other chart saves do not require the owner's rectification state", () => { assert.match(source, /function missingOtherProfileStep\(profile: Profile\)/); // 原值 / 新值 / 原因:nextProfile 校验 / 人物页 draft 校验 / 替代保存入口仍不依赖本人校正。 assert.match(source, /if \(missingOtherProfileStep\(draft\) \|\| saving\) return;/); assert.doesNotMatch(source, /saveOtherChart[\s\S]{0,500}missingProfileStep\(nextProfile\)/); }); test("other chart save fails closed when cloud sync fails", () => { // Former values that locked the dual-truth fallback: // `let cloudSaved = false`, catch copy "已保存到本地星盘库;云端同步失败", // `localStorage.setItem(chartLibraryStorageKey(accountId), JSON.stringify(next))`, // delete copy "已从本地星盘库删除;云端同步失败". Cloud write failure is now failure. assert.doesNotMatch(source, /let cloudSaved = false/); // 原值 / 新值 / 原因:旧 helper 保存 / 人物页 POST/PUT 与 catch / 死面板退役,云失败仍不得伪成功。 assert.match(source, /method: editingOther \? "PUT" : "POST"/); assert.match(source, /async function saveOther\([\s\S]{0,1700}catch \{[\s\S]{0,180}保存失败,请重试/); assert.doesNotMatch(source, /localStorage\.setItem\(chartLibraryStorageKey/); assert.doesNotMatch(source, /已保存到本地星盘库/); assert.match(source, /invalidateSubjectCatalog\(\);\s*await load\(\)/); assert.match(source, /async function confirmDelete[\s\S]{0,500}method: "DELETE"/); assert.match(source, /async function confirmDelete[\s\S]{0,700}if \(!response\.ok\) \{[\s\S]{0,160}删除失败,请重试[\s\S]{0,80}return;/); assert.doesNotMatch(source, /已从本地星盘库删除/); }); test("adding another chart waits for the user to choose a relationship type", () => { assert.doesNotMatch(source, /async function saveOtherChart[\s\S]{0,1400}await draftSynastryQuestionFromChart\(/); // 原值 / 新值 / 原因:保存提示让用户再选关系 / 显式合盘按钮 / 人物页保存不自动启动合盘。 assert.match(source, /和我合盘/); assert.doesNotMatch(source, /async function saveOther\([\s\S]{0,1700}draftSynastryQuestionFromChart\(/); assert.match(source, /\/api\/synastry/); }); test("a successful cloud read replaces stale local other charts", () => { assert.match( source, /fetchCloudChartLibrary\(\)[\s\S]{0,800}chartLibraryFromCloudOthers\(cloudLibrary, profileForLibrary, upsertSelfChart\)/, ); assert.doesNotMatch(source, /fetchCloudChartLibrary\(\)[\s\S]{0,800}new Map\(\[[\s\S]{0,500}current\.filter\(\(record\) => record\.role === "other"\)/); }); test("other chart creation lets the database create its UUID", () => { // 原值: insert({ user_id, role, profile, updated_at }) with role taken from the body. // 新值: insert always uses role "other" plus the validated typed columns. The id is still omitted. // 原因: self mirrors are retired and other rows are typed columns, not an unvalidated jsonb blob. assert.match(route, /role: "other"/); assert.match(route, /\.insert\(\{/); assert.doesNotMatch(route, /id: body/); assert.doesNotMatch(route, /\.upsert\(record, \{ onConflict: "id" \}\)/); assert.doesNotMatch(source, /id: record\.role === "self" \? undefined : record\.id/); }); test("cloud save failures preserve the server error message", () => { assert.match(source, /const payload = await response\.json\(\)\.catch\(\(\) => null\) as \{ error\?: string \} \| null;/); assert.match(source, /throw new Error\(payload\?\.error \|\| "cloud_chart_profile_save_failed"\);/); }); test("synastry selection exposes a pending state while the evidence packet is computed", () => { assert.match(source, /const \[synastryPendingId, setSynastryPendingId\] = useState\(null\);/); assert.match(source, /setSynastryPendingId\(record\.id\);/); assert.match(source, /finally \{\s*setSynastryPendingId\(null\);\s*\}/); // 原值 / 新值 / 原因:死面板 disabled/按钮 spinner / hook 重入门与既有通知 / 锁实际合盘请求入口。 assert.match(source, /if \(synastryPendingId\) return;/); assert.match(source, /正在计算基础合盘证据,请稍候。/); }); test("relationship intent selects domain-specific evidence instead of treating every pairing as romance", () => { const route = readFileSync(new URL("../src/app/api/synastry/route.ts", import.meta.url), "utf8"); assert.match(source, /商业合作/); assert.match(source, /亲友\/家庭/); assert.match(source, /其他关系/); assert.match(source, /relationshipType: SynastryRelationshipType/); assert.match(source, /body: JSON\.stringify\(\{ partnerChartProfileId: record\.id, relationshipType \}\)/); assert.doesNotMatch(source, /selfProfile: profile, partnerProfile: record\.profile/); assert.match(route, /partnerChartProfileId/); // 原值: the route selected profiles and the chart_profiles jsonb itself. // 新值: both people are read by loadSubjectBirth. // 原因: D13. The other person's birth is the typed columns. "我" stays the household profile. assert.match(route, /loadSubjectBirth/); assert.doesNotMatch(route, /globalBirthProfileFromStoredChart/); assert.match(route, /consumeUserRequestRateLimit\("synastry"/); assert.match(route, /relationshipType === "business"/); assert.match(route, /divisions: \["D2", "D10", "D11"\]/); assert.match(route, /"D10_Dasamsa"/); assert.match(route, /"D11_Rudramsa"/); assert.match(route, /blockedLayers:/); assert.match(route, /"A10"/); assert.match(route, /"functional_benefic_malefic"/); assert.match(route, /"vimshottari_narayana"/); assert.match(source, /基础商业合作证据筛查/); assert.match(source, /relationshipType === "business"/); }); test("synastry history is replaced by the cloud list and save failures stay in page memory", () => { // Former value: `new Map([...current, ...cloudHistory])` unioned local+cloud // and `writeSynastryHistory(accountId, next)` on cloud save failure. assert.doesNotMatch(source, /new Map\(\[\.\.\.current, \.\.\.cloudHistory\]/); assert.match(source, /未能存入历史/); assert.doesNotMatch(source, /writeSynastryHistory\(/); assert.match(source, /discardLegacyCloudMirrorKeys\(/); }); test("current chart selection is local and does not overwrite the owner's profile", () => { // 原值: activeChartStorageKey and the "set as current" copy lived in the home surface. // 新值: those symbols are gone from frontend/src. The page profile is not replaced from the archive. // 原因: D11. The current person is a request parameter, not a second profile. const page = readFileSync(new URL("../src/app/(app)/page.tsx", import.meta.url), "utf8"); assert.doesNotMatch(page, /activeChartStorageKey|makeDefaultChart|setProfile\(record\.profile\)/); assert.match(readFileSync(new URL("../src/app/api/chart-profiles/[id]/route.ts", import.meta.url), "utf8"), /星盘不存在或无权更新/); }); test("the chart library starts as a list and only renders a form after a view change", () => { // 原值 / 新值 / 原因:旧 ChartLibraryView 列表合同 / 人物页 editing 门控 / 删除死面板,保留列表先行合同。 const charts = readFileSync(new URL("../src/components/people/people-page.tsx", import.meta.url), "utf8"); const list = charts.slice(charts.indexOf('className="people-archive-list"'), charts.indexOf('className="people-archive-detail"')); assert.match(charts, /\[editing, setEditing\] = useState\(false\)/); assert.doesNotMatch(list, /ChartProfileForm|
{ // 原值 / 新值 / 原因:旧 selfView/合盘历史面板 / 人物页本人不可删与本人合盘绑定 / 替代入口覆盖,不保留死组件。 const charts = readFileSync(new URL("../src/components/people/people-page.tsx", import.meta.url), "utf8"); assert.match(charts, /selected\.role === "self" \?

本人不能删除/); // 原值: the detail offered 设为默认. // 新值: that action is gone. // 原因: D11 removes "set as default". assert.doesNotMatch(charts, /设为默认/); assert.match(charts, /和我合盘/); assert.match(source, /startNewChat\(\{ subjectId: "self" \}\)/); assert.match(charts, /pendingDelete\?\.id === selected\.id && selected\.role === "other"/); assert.match(source, /partnerChartId: record\.id/); });