import assert from "node:assert/strict"; import { existsSync, readFileSync } from "node:fs"; import test from "node:test"; const projectFile = (path: string) => new URL(`../${path}`, import.meta.url); const readProjectFile = (path: string) => readFileSync(projectFile(path), "utf8"); const hookSource = readProjectFile("src/hooks/use-billing-panel.ts"); const panelSource = readProjectFile("src/components/billing-panel.tsx"); const billingSource = `${hookSource}\n${panelSource}`; const membershipLib = readProjectFile("src/lib/membership.ts"); const nextConfig = readProjectFile("next.config.ts"); const pageSource = readProjectFile("src/app/page.tsx"); const tabsSource = readProjectFile("src/components/ui/tabs.tsx"); function countMatches(source: string, pattern: RegExp) { return source.match(pattern)?.length ?? 0; } test("membership routes are gone and billing loads without a spinner", () => { // 原值: src/app/membership/page.tsx 存在且含 Suspense / membership-loading // 新值: 页面删除;billing-panel 是 client 组件,无 spinner / 骨架 / 「正在加载」 // 原因: 账户与点数进入设置弹窗,等待态遵守 AGENTS §6 assert.equal(existsSync(projectFile("src/app/membership/page.tsx")), false); assert.equal(existsSync(projectFile("src/app/membership/orders/page.tsx")), false); assert.match(panelSource, /"use client"/); assert.doesNotMatch(billingSource, /membership-loading|正在加载|Suspense/); assert.doesNotMatch(panelSource, /正在加载订单/); assert.match(nextConfig, /source: "\/membership"/); assert.match(nextConfig, /destination: "\/\?settings=billing&source=legacy"/); assert.match(nextConfig, /source: "\/membership\/orders"/); assert.match(nextConfig, /destination: "\/\?settings=billing&tab=orders"/); }); test("membership page loads account, packages and redeem endpoints without fetching orders", () => { // 原值: 会员页不请求 /api/payment/orders // 新值: 挂载只并发账户与套餐;订单仅在 orders 页签拉取 // 原因: 订单记录改成分区内页签,不能一打开就打列表接口 assert.match(hookSource, /fetch\("\/api\/account"/); assert.match(hookSource, /fetch\("\/api\/payment\/packages"/); assert.match(hookSource, /fetch\("\/api\/payment\/epay\/create"/); assert.match(hookSource, /fetch\(`\/api\/payment\/epay\/status\?orderNo=/); assert.match(hookSource, /fetch\("\/api\/redeem"/); assert.match(hookSource, /await Promise\.allSettled\(\[fetchAccountData\(\), fetchPackages\(\)\]\)/); const mountStart = hookSource.indexOf("await Promise.allSettled"); const mountEnd = hookSource.indexOf("}, [fetchAccountData, fetchPackages]);"); const mountEffect = hookSource.slice(mountStart, mountEnd); assert.doesNotMatch(mountEffect, /fetchOrders|\/api\/payment\/orders/); assert.match(panelSource, /if \(value === "orders"\) void fetchOrders\(\)/); }); test("creates orders with productId and opens the signed cashier URL safely", () => { assert.match(hookSource, /JSON\.stringify\(\{ productId \}\)/); assert.match(hookSource, /window\.open\(payload\.payUrl, "_blank", "noopener,noreferrer"\)/); assert.doesNotMatch(billingSource, /document\.createElement\("form"\)|payload\.submitUrl|payload\.fields/); assert.match(hookSource, /if \(!paymentOrder \|\| paymentOrder\.status !== "pending"\) return;/); }); test("terminal payment failures stop polling and offer re-pay with the same product", () => { assert.match(hookSource, /\["failed", "closed", "cancelled"\]\.includes\(payload\.status\)/); assert.match(hookSource, /payload\.grantStatus === "failed"/); assert.match(hookSource, /status !== "pending"\) return;/); assert.match(hookSource, /status: paid \? "paid" : failed \? "failed" : "pending"/); assert.match(panelSource, /paymentOrder\.status === "failed"/); assert.match(panelSource, /支付失败/); assert.match(panelSource, /createPayment\(paymentOrder\.productId\)/); assert.match(panelSource, /重新支付/); }); test("failed status beats paid and non-terminal statuses keep polling", () => { assert.match(hookSource, /const paid = payload\.status === "paid" && !failed;/); assert.match(hookSource, /status: paid \? "paid" : failed \? "failed" : "pending"/); assert.match(hookSource, /if \(paid\) \{/); assert.doesNotMatch(hookSource, /status: paid \? "paid" : failed \? "failed" : payload\.status/); assert.doesNotMatch(hookSource, /payload\.status === "paid" \? "paid"/); }); test("polls order status and refreshes only the balance on paid", () => { const poll = hookSource.slice( hookSource.indexOf("const checkPaymentStatus"), hookSource.indexOf("let timer = 0"), ); assert.match(poll, /const paid = payload\.status === "paid" && !failed;/); assert.match(poll, /if \(paid\) \{/); assert.match(poll, /status: paid \? "paid" : failed \? "failed" : "pending"/); assert.match(poll, /fetchAccountData\(\)/); assert.match(poll, /notifyBalanceChanged\(refreshed\?\.credits/); assert.doesNotMatch(poll, /fetchOrders/); }); test("keeps the selected product and allows retry on payment failure", () => { assert.match(hookSource, /setSelectedProductId\(productId\)/); assert.match(hookSource, /setPaymentError\(caught instanceof Error \? caught\.message : "创建支付失败"\)/); assert.match(hookSource, /finally \{\s*setPayingProductId\(null\);/); assert.match(hookSource, /if \(payingProductId\) return;/); assert.match(panelSource, /createPayment\(selectedProductId\)/); assert.match(panelSource, /membership-payment-error/); assert.doesNotMatch(hookSource, /setPayingProductId\(null\)[\s\S]{0,40}setPaymentOrder\(null\)/); }); test("membership never opens the redeem dialog from the URL", () => { // 原值: redeemOpen 状态,URL 不得带 redeem= // 新值: 兑换码是页签;settings-url 的 tab=redeem 打开页签,不再套子弹窗 // 原因: 决策 5,兑换码不再是子弹窗 assert.doesNotMatch(billingSource, /redeemOpen|setRedeemOpen/); assert.doesNotMatch(billingSource, /searchParams\.get\("redeem"\)|searchParams\.delete\("redeem"\)|redeem:\s*true/); assert.match(panelSource, /value="redeem"/); assert.doesNotMatch(panelSource, /className="account-modal membership-redeem"/); }); test("returning prefers history.back and keeps browser back semantics", () => { // 原值: goBack → history.back 或 assign("/") // 新值: 账单在首页弹窗内,无 goBack;深链接用 stripSettingsQuery 抹参数 // 原因: 删除整页后返回就是关掉弹窗,浏览器后退仍是原生行为 assert.doesNotMatch(billingSource, /goBack|history\.back\(/); assert.doesNotMatch(panelSource, /window\.location\.assign\("\/"\)/); assert.match(pageSource, /stripSettingsQuery\(/); }); test("trims the redeem code without changing its case", () => { assert.match(hookSource, /const code = redeemCode\.trim\(\);/); assert.doesNotMatch(hookSource, /redeemCode\.(?:toUpperCase|toLowerCase)\(\)/); assert.doesNotMatch(hookSource, /normalizeRedeemCode/); assert.match(hookSource, /JSON\.stringify\(\{ code \}\)/); }); test("redeem success reports awarded credits, latest balance and completes", () => { assert.match(hookSource, /awardedCredits/); assert.match(hookSource, /本次到账/); assert.match(hookSource, /最新余额/); assert.match(hookSource, /setRedeemDone\(true\)/); assert.match(hookSource, /setRedeemCode\(""\)/); assert.match(hookSource, /const refreshed = await fetchAccountData\(\);/); assert.match(hookSource, /notifyBalanceChanged\(refreshed\?\.credits \?\? result\.credits\)/); assert.match(panelSource, /完成/); assert.match(panelSource, /onClick=\{closeRedeem\}/); }); test("supports plan hints without rendering source notices", () => { assert.match(panelSource, /const highlighted = alias === highlightedPlan;/); assert.doesNotMatch(billingSource, /searchParams\.get\("source"\)|membershipSourceNotice|sourceNotice/); assert.doesNotMatch(billingSource, /membership-notice|已为你定位到/); assert.doesNotMatch(billingSource, /体验 \/ 月卡 \/ 年卡见下方套餐|用于咨询与生时校正等服务/); }); test("membership section shows fixed trial/monthly/yearly plans with monthly recommended", () => { assert.match(panelSource, /selectMembershipPlans\(paymentPackages\)/); assert.match(panelSource, /planAlias\(product\)/); assert.match(panelSource, /alias === "monthly"/); assert.match(panelSource, /membership-plan-card--recommended/); assert.match(membershipLib, /trial: "体验"/); assert.match(membershipLib, /monthly: "月卡"/); assert.match(membershipLib, /yearly: "年卡"/); assert.match(panelSource, /会员套餐/); // 原值: 点数充值 // 新值: 点数包 // 原因: VOICE 页签名「会员套餐 / 点数包 / 兑换码 / 订单记录」 assert.match(panelSource, /点数包/); assert.match(panelSource, /id="membership-plans-tab"/); assert.match(panelSource, /id="membership-credits-tab"/); assert.match(panelSource, /]*value="membership"/); assert.match(panelSource, /]*value="credits"/); assert.match(panelSource, /]*value="membership"/); assert.match(panelSource, /]*value="credits"/); }); test("plan cards carry price, duration, audience, entitlements, purchase and rules", () => { assert.match(panelSource, /formatPrice\(product\.priceCents, product\.currency\)/); assert.match(panelSource, /formatProductDuration\(product\)/); assert.match(panelSource, /productAudience\(product\)/); assert.match(panelSource, /entitlementLabel\(entitlement\)/); assert.match(panelSource, /
/); assert.match(panelSource, /详细规则<\/summary>/); assert.match(panelSource, /import \{ Button \} from "@\/components\/ui\/button"/); assert.match(panelSource, /className="membership-buy"/); assert.match(panelSource, /variant=\{recommended \? "default" : "outline"\}/); }); test("renewal semantics map the active subscription to the matching plan", () => { assert.match(hookSource, /const currentProductCode = activeSubscription\?\.status === "active"/); assert.match(panelSource, /currentProductCode === product\.code/); assert.match(panelSource, /isCurrent \? "续费" : product\.productType === "trial" \? "立即开通" : "立即购买"/); assert.match(panelSource, /当前套餐/); }); test("membership and credits use the local Base UI tabs wrapper without manual segment state", () => { assert.match(panelSource, /import \{ Tabs, TabsContent, TabsList, TabsTrigger \} from "@\/components\/ui\/tabs"/); assert.doesNotMatch(billingSource, /activeSegment|setActiveSegment/); assert.match(tabsSource, /import \{ Tabs as TabsPrimitive \} from "@base-ui\/react\/tabs"/); assert.match(tabsSource, /TabsPrimitive\.Root/); assert.match(tabsSource, /TabsPrimitive\.List/); assert.match(tabsSource, /TabsPrimitive\.Tab/); assert.match(tabsSource, /TabsPrimitive\.Panel/); assert.match(panelSource, /const plans = selectMembershipPlans\(paymentPackages\);/); assert.match(panelSource, /const creditPacks = paymentPackages\.filter\(\(product\) => product\.productType === "credit_pack"\);/); assert.match(panelSource, /className="membership-plan-grid"/); assert.match(panelSource, /className="membership-credit-grid"/); assert.doesNotMatch(panelSource, /creditPacks\.map[\.\s\S]{0,120}membership-plan-card/); }); test("plan param forces the membership segment and highlights the matching card", () => { assert.match(panelSource, /const highlighted = alias === highlightedPlan;/); assert.match(panelSource, /membership-plan-card--highlighted/); assert.match(panelSource, /aria-current=\{highlighted \? "true" : undefined\}/); assert.match(panelSource, /highlightedCardRef/); assert.match(hookSource, /scrollIntoView\(\{ behavior: "smooth", block: "center" \}\)/); assert.match(hookSource, /card\.focus\(\{ preventScroll: true \}\)/); assert.match(hookSource, /initialTab: input\.highlightedPlan \? "membership" : input\.initialTab/); assert.doesNotMatch(billingSource, /setActiveSegment\("credits"\)[\s\S]{0,80}highlightedPlan/); }); test("page main title is 套餐与会员", () => { // 原值:

套餐与会员

// 新值: 分区标题由弹窗壳提供「账户与点数」,摘要行写余额与会员状态 // 原因: 不再是独立页面 assert.doesNotMatch(panelSource, /

/); assert.match(panelSource, /className="billing-summary"/); assert.match(panelSource, /aria-label="账户与点数"/); }); test("payment errors sit in a common position above the visible segment", () => { const errorAt = panelSource.indexOf("membership-payment-error"); const plansPanelAt = panelSource.indexOf(''); assert.notEqual(errorAt, -1, "missing common payment error surface"); assert.notEqual(plansPanelAt, -1, "missing plans panel"); assert.ok(errorAt < plansPanelAt, "payment error must render before the segment panels"); assert.doesNotMatch(panelSource, /

\{paymentError\}<\/p>/); }); test("summary stays on the membership page and links to the independent orders page", () => { // 原值: membership-summary-card + Link 到 /membership/orders // 新值: billing-summary + 订单记录页签,行内列出订单 // 原因: 删除独立订单页 assert.match(panelSource, /className="billing-summary"/); assert.match(panelSource, /value="orders"/); assert.match(panelSource, /membership-order-row/); assert.match(panelSource, /orderStatusLabel/); assert.doesNotMatch(panelSource, /href="\/membership\/orders"/); }); test("independent orders page preserves loading, empty, error, refresh and list states", () => { // 原值: 独立订单页含「正在加载订单…」 // 新值: 页签内 idle/ready/unavailable;空与失败有文案,没有加载句 // 原因: AGENTS §6 禁止「正在加载」 assert.match(hookSource, /fetch\("\/api\/payment\/orders"/); assert.match(hookSource, /response\.status === 401[\s\S]*window\.location\.assign\("\/login"\)/); assert.doesNotMatch(billingSource, /正在加载订单/); assert.match(panelSource, /订单记录暂时不可用,请稍后刷新。/); assert.match(panelSource, /暂无订单。/); assert.match(panelSource, /onClick=\{\(\) => void fetchOrders\(\)\}>刷新/); assert.match(panelSource, /membership-order-row/); assert.match(panelSource, /orderStatusLabel\(order\.status\)/); assert.match(panelSource, /formatPrice\(order\.moneyCents, order\.currency\)/); assert.match(membershipLib, /paid: "已支付"/); }); test("orders return replaces its history entry so membership back cannot reopen orders", () => { // 原值: Link replace 回 /membership // 新值: 页签切换不写历史;无 membership-back // 原因: 不再有两页互相推历史 assert.doesNotMatch(billingSource, /membership-back|href="\/membership"/); }); test("payment and plan copy points users to the separate orders page", () => { // 原值: 本页会自动检查支付状态并刷新余额 / 「订单记录」页面 // 新值: 收银台打开后自动检查;订单在页签查看 // 原因: 订单不再是另一页 assert.match(panelSource, /请在已打开的收银台页面完成支付;打开后会自动检查支付状态并刷新余额。/); assert.match(panelSource, /「订单记录」页签/); assert.doesNotMatch(panelSource, /本页会自动刷新余额与订单记录|本页「订单记录」|用于咨询、报告与生时校正等服务/); }); test("redeem modal surface contains title, balance, input, redeem, rules and close", () => { // 原值: 兑换子弹窗含 aria-label=关闭 // 新值: 兑换码页签含标题、余额、输入、兑换、规则;关闭属于设置弹窗外框 // 原因: 决策 5 assert.match(panelSource, /id="membership-redeem-title"/); assert.match(panelSource, /

兑换点数<\/h2>/); assert.match(panelSource, /className="redeem-balance"/); assert.match(panelSource, /id="membership-redeem-code"/); assert.match(panelSource, /立即兑换/); assert.match(panelSource, /兑换规则<\/summary>/); assert.match(panelSource, /className="redeem-form"/); assert.match(panelSource, /