/** * Chart-page `/?rectify=1` uses the homepage open, which resumes a case * already in progress. Intent "new" would surface a conflict instead. * Fictional profile only. BUG-1446. */ import assert from "node:assert/strict"; import test from "node:test"; import React, { useRef, useState } from "react"; import { EMPTY_RECTIFICATION_SHELL, useRectificationSurface, } from "../src/hooks/use-rectification-surface.ts"; import type { Account, Profile } from "../src/lib/home-types.ts"; import type { PublicLanguageModelCatalog } from "../src/lib/public-models.ts"; import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support.ts"; import { loadRectificationSurface } from "../src/lib/rectification-surface-loader.ts"; Object.assign(globalThis, { React }); const SESSION_ID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1"; const profile = { name: "示例", date: "1990-06-15", time: "12:00", reportedTime: "12:00", birthTimeSource: "hospital_record", birthTimePeriod: "", declaredWindowStart: "", declaredWindowEnd: "", birthTimeClue: "", uncertaintyBeforeMinutes: 2, uncertaintyAfterMinutes: 2, birthTimeStatus: "reported", countryCode: "", provinceCode: "", cityCode: "", districtCode: "", birthPlaceLabel: "北京", birthPlaceType: "city", birthPlaceProvider: "test", birthPlaceProviderId: "beijing", timezoneId: "Asia/Shanghai", timezoneSource: "manual", latitude: 39.9, longitude: 116.4, timezoneOffset: 8, rectificationCaseId: "", ayanamsa: "raman", } satisfies Profile; const catalog: PublicLanguageModelCatalog = { models: [{ id: "m", label: "M", description: "", creditCost: 1, isDefault: true }], defaultModelId: "m", }; function jsonResponse(body: unknown, status = 200) { return { ok: status >= 200 && status < 300, status, json: async () => body, } as Response; } function installLocation(search: string) { const location = { pathname: "/", search, href: `https://jyotisha.chat/${search}`, }; const history = { replaceState(_data: null, _title: string, next: string) { const url = new URL(next, "https://jyotisha.chat"); location.pathname = url.pathname; location.search = url.search; location.href = url.toString(); }, pushState(_data: null, _title: string, next: string) { history.replaceState(_data, _title, next); }, }; const view = globalThis.window as { location?: typeof location; history?: typeof history }; view.location = location; view.history = history; Object.assign(globalThis, { location, history }); } function Probe() { const [rectification, setRectification] = useState(EMPTY_RECTIFICATION_SHELL); const activeSessionIdRef = useRef(""); const sessionSelectionSource = useRef<"user" | "history">("user"); const uiPreview = useRef(false); const rectificationSessionOpenerRef = useRef<(exactSessionId: string) => Promise>(async () => undefined); useRectificationSurface({ account: { id: "user-1" } as unknown as Account, activeSessionIdRef, chartLibrary: [], creatingSession: false, modelCatalog: catalog, persistSession: async () => undefined, profile, rectification, setRectification, sessionSelectionSource, sessions: [], setActiveSessionId: () => undefined, setDraft: () => undefined, setDraftEntrypoint: () => undefined, setDraftTheme: () => undefined, setOnboardingStep: () => undefined, setProfileNotice: () => undefined, setSessions: () => undefined, uiPreview, updateSession: () => undefined, openAccountDialog: () => undefined, refreshAccount: async () => undefined, rectificationSessionOpenerRef, }); return React.createElement("p", { "data-testid": "rectify-result", "data-error": rectification.error, "data-session": rectification.sessionId ?? "", }); } async function openFromChart(disposition: "resumed" | "created") { const posts: Array<{ intent?: string }> = []; const previousFetch = globalThis.fetch; const harness = createClientLifecycleHarness(); // 原值:打开流程不等校正界面代码,两次 idle(约 20 ms)就设好会话 // 新值:先把校正界面代码载入,再走同样的打开流程 // 原因:issue #124 校正界面改为按需下载,打开时要等它到齐才设会话(一次揭幕); // tsx 现场转译 74 个文件远超 20 ms,本组测的是打开意图与会话,不是下载时长 await loadRectificationSurface(); installLocation("?rectify=1"); globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); if (url === "/api/rectification/cases/open") { posts.push(JSON.parse(String(init?.body ?? "{}")) as { intent?: string }); return jsonResponse({ disposition, caseId: "case-1", sessionId: SESSION_ID, status: "draft", shouldStartOpening: disposition === "created", skillVersion: "10.0.33", }); } if (url.includes("/source")) return jsonResponse({ source: null }); if (url.startsWith("/api/sessions/")) return jsonResponse({ session: null }); return jsonResponse({}); }) as typeof fetch; try { await harness.render(React.createElement(Probe)); await harness.idle(); await harness.idle(); const node = harness.elements().find((element) => element.getAttribute("data-testid") === "rectify-result"); return { posts, box: { errors: node?.getAttribute("data-error") ? [node.getAttribute("data-error") ?? ""] : [], sessionId: node?.getAttribute("data-session") || null, }, errors: harness.errors, }; } finally { globalThis.fetch = previousFetch; await harness.close(); } } test("?rectify=1 resumes an open case with the homepage intent and does not show a conflict", async () => { const { posts, box, errors } = await openFromChart("resumed"); assert.equal(errors.length, 0); assert.equal(posts.length, 1); assert.equal(posts[0]?.intent, "homepage"); assert.equal(box.sessionId, SESSION_ID); assert.deepEqual(box.errors, []); assert.equal(box.errors.join(""), ""); }); test("?rectify=1 starts a case with the homepage intent when none is open", async () => { const { posts, box, errors } = await openFromChart("created"); assert.equal(errors.length, 0); assert.equal(posts.length, 1); assert.equal(posts[0]?.intent, "homepage"); assert.equal(box.sessionId, SESSION_ID); assert.deepEqual(box.errors, []); });