Files
Jyotisha/frontend/tests/chart-library-other-profile.test.ts
T
jesse-uxandGrok (agent loop) eee8564b7f
Independent Staging Quality Gate / validate (push) Failing after 9m16s
Independent Staging Quality Gate / publish (push) Skipped
星盘档案可以拖动排序,合盘开始前先确认扣点。
人物卡、菜单、编辑和删除换成组件库。chart_profiles 增加可空的 archive_order,本人始终排在第一。合盘选人页两个名字并排,开始按钮居中,点了先说出点数和余额。

自测:tsc 0;lint 0 error(122 警告,与上一张相同);定向测试通过;next build 后 / 仍是 Static;首页 JS gzip-9 639609,相对 640131 少 0.08%。test:db 79 通过、13 失败,失败名单与 windows-known-failures.txt 一致,排序列断言在通过的那一组。没有浏览器登录态。

Co-Authored-By: Grok (agent loop)
2026-10-25 05:05:00 +08:00

152 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/);
// 原值: /method: editingOther \? "PUT" : "POST"/(写在人物页 saveOther 里)
// 新值: /method: id \? "PUT" : "POST"/(saveOtherProfile;有 id 则 PUT,否则 POST)
// 原因: 星盘编辑框和档案页共用这一处请求,页面里不再各写一份。
const otherSave = readFileSync(new URL("../src/lib/other-profile-save.ts", import.meta.url), "utf8");
assert.match(otherSave, /method: id \? "PUT" : "POST"/);
// 原值: /async function saveOther\([\s\S]{0,1700}catch \{[\s\S]{0,180}保存失败,请重试/
// 新值: /async function saveOther\([\s\S]{0,1700}catch \(caught\) \{[\s\S]{0,280}保存失败,请重试/
// 原因: 409「填报的出生时间不能再改」从共用函数抛出,档案页要显示这句,不能一律盖成保存失败。
assert.match(source, /async function saveOther\([\s\S]{0,1700}catch \(caught\) \{[\s\S]{0,280}保存失败,请重试/);
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<string \| null>\(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|<form/);
assert.match(list, /添加一个人/);
// 原值:editing ? ( 后面立刻是 ChartProfileForm
// 新值:editing ? ( 后面是 Dialog,同一段里仍有 ChartProfileForm
// 原因:K4 档案编辑改用 Dialog。星盘页已经有自己的对话框,所以不把 Dialog 放进共用表单
assert.match(charts, /editing \? \(\s*<Dialog[\s\S]{0,400}<ChartProfileForm/);
assert.match(charts, /setEditing\(true\)/);
assert.match(charts, /setEditing\(false\)/);
});
test("other chart details own actions and self details cannot delete", () => {
// 原值 / 新值 / 原因:旧 selfView/合盘历史面板 / 人物页本人不可删与本人合盘绑定 / 替代入口覆盖,不保留死组件。
const charts = readFileSync(new URL("../src/components/people/people-page.tsx", import.meta.url), "utf8");
assert.match(charts, /selected\.role === "self" \? <p>本人不能删除/);
// 原值: 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/);
});