fix(rectification): open history cases with their bound skill identity (BUG-621)
Session open was sending the live registry version into v2, so every Skill bump rejected older cases with an unmapped 500 and no error on the clicked row. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-09-09 — 历史生时校正按它自己那一版打开
|
||||
|
||||
从历史对话点开生时校正时,不再要求和当前注册的 Skill 版本完全相同。Case 仍钉住创建时的版本;只要那一版快照还在注册表里,就按那一版打开。首页「开始校正」仍用当前版本。打开失败时,错误出现在被点的那一行下面,不会再看起来像没点到。Skill 版本不变。
|
||||
|
||||
|
||||
## 2026-09-09 — 长报告星盘改成两栏卡片,滚动时不再整页重建
|
||||
|
||||
个人长报告里的本命盘、婚盘、月亮盘和 Vargas I 不再叠在同一块:每张盘和它的标题进一张卡片,连续的卡片走原来的两栏网格;窄屏和打印仍是单栏。滚动目录高亮时,正文里的图不会被拆掉重画。导出的 `.md` 仍带引擎 SVG、不含网页用的 JSON 围栏。Skill 版本不变。
|
||||
|
||||
@@ -9636,6 +9636,22 @@
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-621 | 升 Skill 版本后历史生时校正从列表点不开
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-09
|
||||
- 最近更新:2026-09-09
|
||||
- 影响面:`openRectificationCase`、`open_agentic_rectification_case_v2`、历史会话列表、`KNOWN_RPC_ERROR_CODES`
|
||||
- 用户现象:历史对话里的生时校正(含当天较早创建的)点了没反应,主栏也不换会话。
|
||||
- 触发条件:生时校正 Skill 已从 10.0.15 升到 10.0.19;点开绑定旧版本的历史校正。
|
||||
- 根因:08-14 不可变注册表让 Case 钉住自己的 `skill_name/version/sha256/source_commit`,agent 也按绑定版本跑。打开 RPC `open_agentic_rectification_case_v2` 却把**当前注册版本**与 Case 做相等比较,任一不同就 `skill_identity_mismatch`。该错误码未映射,变成 500「校正服务暂时不可用」;客户端再兜成「暂时无法打开生时校正」。错误只画在首页起始卡下,而 BUG-599 之后 `selectSession` 等 Case 打开成功才切会话,历史列表点击看起来像没反应。
|
||||
- 修复:`intent === "session"` 时先走 v1 按会话取 Case,再读绑定身份,用 `resolveExactSkillPackage` 核验快照存在且 sha 一致,核验通过后把**绑定身份**交给 v2。`homepage` / `new` 仍用当前注册版本。映射 `skill_identity_mismatch` → 409。历史打开失败把错误画在被点的那一行下面,起始卡不重复同一条。不改迁移、不改采用门。
|
||||
- 验证:`frontend/tests/rectification-v9-case-service.test.ts`、`frontend/tests/rectification-history-open-20260909.test.ts`、`frontend/tests/skill-registry.test.ts`。Docker DB 套件本机跑不了(环境缺口);本轮无新迁移。
|
||||
- 防复发:按会话打开必须用 Case 绑定身份,不得把当前注册版本交给 v2 做相等比较。升 Skill 版本后,历史校正必须仍能从历史对话打开,且以其绑定版本运行。废弃版本快照须仍可 `resolveExactSkillPackage`。
|
||||
- 相关记录:BUG-599、BUG-583、BUG-593、BUG-595、BUG-597、BUG-604
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-622 | staging 质量门因两条过期前端合同红掉,Deploy staging 未派发
|
||||
|
||||
- 状态:resolved
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# PROGRESS · 历史生时校正打开时用绑定 Skill 身份(BUG-621,2026-09-09)
|
||||
|
||||
工作树:`.worktrees/rectification-history-open-20260909`
|
||||
分支:`codex/rectification-history-open-20260909`
|
||||
任务书:`docs/tasks/TASK-rectification-history-open-20260909.md`
|
||||
基线:任务书写 `origin/staging` @ `af70ae77`;本 worktree 从任务书提交 `e8d46f12` 切出,开工后快进合并 `c41afe26`(BUG-622,对齐过期合同测试)。
|
||||
|
||||
本单状态:**待验收**。未 push,未合入 staging/main。合入时更新落点 SHA。
|
||||
|
||||
预检:本机 `python3 scripts/pre_work_check.py` 失败(系统 Python 3.14 无 pytest、无 swisseph,ERR-078)。不把预检失败写成门禁已过。前端用 worktree 里指向其他 worktree 的 `frontend/node_modules` 符号链接。
|
||||
|
||||
未改:数据库迁移、`adopt-skill` 路由、采用门 / 确认门、只读终态、`homepage` / `new` 仍用当前注册版本、Skill 版本号。
|
||||
|
||||
## 做了什么
|
||||
|
||||
- **决策 1** `openRectificationCase` 在 `intent === "session"` 时:`open_agentic_rectification_case`(v1)按会话取 Case → `get_agentic_rectification_skill_identity` 读绑定身份 → `resolveExactSkillPackage` 核验快照与 sha → 把绑定身份作为 `p_skill_*` 调 v2。绑定版本不在注册表 → `skill_identity_missing`。legacy sha 为空仍走既有 `skill_identity_unverifiable`。
|
||||
- **决策 2** `KNOWN_RPC_ERROR_CODES` 增加 `agentic_rectification_skill_identity_mismatch` → 409,文案「该校正绑定的规则版本与当前不一致,请刷新后重试」。
|
||||
- **决策 3** 历史打开失败把 `rectificationErrorSessionId` 绑到被点会话;侧栏行下 `.session-open-error` 一行;`StarterHome` 收到空字符串,起始卡不重复。成功或另选会话则清除。
|
||||
- **记录** `docs/BUG_HISTORY.md` BUG-621;`CHANGELOG.md`;`docs/testing/rectification-history-open-20260909.md`。
|
||||
|
||||
## 三栏
|
||||
|
||||
| 用例 | 原值 | 新值 | 理由 |
|
||||
| --- | --- | --- | --- |
|
||||
| 历史会话绑定 10.0.17(快照仍在) | v2 收到当前 10.0.19 → mismatch → 500 | v2 收到 10.0.17,打开成功 | 决策 1:按绑定版本打开 |
|
||||
| 绑定版本不在注册表 | 同上,落到 500 | `skill_identity_missing`,不调 v2 | 决策 1:核验失败走既有 missing |
|
||||
| homepage / new | 当前注册版本 | 仍是当前注册版本 | 只有 session 改绑定身份 |
|
||||
| `skill_identity_mismatch` | 未映射 → 500「校正服务暂时不可用」 | 409,请刷新后重试 | 决策 2 |
|
||||
| 历史列表点开失败 | 错误在首页起始卡下;会话不切 | 错误在被点那一行下;起始卡不重复 | 决策 3;BUG-599 让点击不再先切会话 |
|
||||
|
||||
## 测试
|
||||
|
||||
- `frontend` `./node_modules/.bin/tsc --noEmit`:exit 0
|
||||
- 本单改动文件 `eslint`:0 error(`page.tsx` / `app-sidebar.tsx` 等既有 hooks warning 未新增)
|
||||
- 全仓 `npm run lint`:4 error / 108 warning。4 error 全在 `rectification-agentic-chat.tsx` 的既有 `react-hooks/refs`(约 L1461),本单未改该文件
|
||||
- 针对性 `tsx --test`(case-service / history-open / agentic-entry / skill-registry / sidebar-contract / surface-contract / home-bootstrap-reveal):**147 passed / 0 failed**
|
||||
- 任务书切片 `ls tests/rectification-*.test.ts tests/skill-registry*.test.ts tests/home-bootstrap*.test.ts | grep -v database | xargs tsx --test`:**1093 passed / 0 failed**
|
||||
- Docker DB / `test:db`:本机无 Docker,记环境缺口;本轮无新迁移,不触发该硬要求
|
||||
- 浏览器真人走查:无登录态,未见真实历史 Case。走查清单见 `docs/testing/rectification-history-open-20260909.md`
|
||||
|
||||
## 偏离
|
||||
|
||||
1. 任务书基线 `af70ae77`;执行分支从已推任务书的 `e8d46f12` 切出,并快进 `c41afe26`(BUG-622)。
|
||||
2. 侧栏错误用 `.session-open-error`(overline + danger),不再引入已删除的 `composer-notice` 类名。
|
||||
3. 未跑 Docker 中的打开 RPC 真库套件。
|
||||
|
||||
## 环境缺口
|
||||
|
||||
- 本机系统 Python 3.14 无 pytest / swisseph,预检失败(ERR-078)。
|
||||
- 无 Docker,不能跑 `test:db`。
|
||||
- 无 staging 登录态,不能真人点历史校正。
|
||||
@@ -88,7 +88,7 @@
|
||||
| `TASK-rectification-conversation-economy-20260909.md` | `PROGRESS-rectification-conversation-economy-20260909.md` | 对照竞品后产品拍板三条:开场三句讲做法 + 一次收多件(推翻 opening brief「不要一次说完/不举例」与 SKILL L52);采集题「没有 / 记不清」按钮(不做示例骨架条);每轮只留一句(方法句进活动记录、点选旁白去领先落后、证据轮正文一句复述 + 服务端裁剪);Skill 10.0.19 | 已验收通过(2 条 P3 备注) | `aa7ccb30` + `1453fb16`(BUG-604~606,Skill 10.0.19) |
|
||||
| `TASK-consultation-daily-empty-answer-20260909.md` | `PROGRESS-consultation-daily-empty-answer-20260909.md` | 首页「深入看今日」计算完成却 `empty_answer`:入口定的 `timing` 被模型改成 `general`(`canonicalDomainPlan` 以模型为准),分段写作标题与今日格式错位,分段 `maxSteps=1` 且工具仍可调 → 模型在分段里再调工具、零正文;思考流按 chunk 过滤漏出缺词英文 | 待验收 | `codex/consultation-daily-empty-answer-20260909`(BUG-612~613);走查 `docs/testing/consultation-daily-empty-answer-20260909.md` |
|
||||
| `TASK-rectification-compare-card-polish-20260909.md` | `PROGRESS-rectification-compare-card-polish-20260909.md` | 三列卡两列经历对照 0、三列无未来窗:引擎按引擎前三算 by_time,卡片按推断前三查表,键不匹配(BUG-614);交付旁白被 BUG-606 裁成一句(BUG-615);设计减法:相同性格句只写一次、经历对照说人话、删小标题、右栏本命宫位改北印星盘、换升时刻默认折叠 | 待验收 | `codex/rectification-compare-card-polish-20260909`(BUG-614~615) |
|
||||
| `TASK-rectification-history-open-20260909.md` | `PROGRESS-rectification-history-open-20260909.md` | 历史生时校正打不开:`open_agentic_rectification_case_v2` 把 Case 绑定 Skill 身份与当前注册版本做相等比较,两天内 10.0.15→10.0.19 五次 bump 让所有旧 Case 被拒;错误码未映射成 500;错误只画在首页卡下 → 表现为点了没反应。决策:session 打开用绑定身份(v1 取 Case → 读身份 → 注册表核验 → v2),不改迁移 | 待执行 | `codex/rectification-history-open-20260909`(BUG-621) |
|
||||
| `TASK-rectification-history-open-20260909.md` | `PROGRESS-rectification-history-open-20260909.md` | 历史生时校正打不开:`open_agentic_rectification_case_v2` 把 Case 绑定 Skill 身份与当前注册版本做相等比较,两天内 10.0.15→10.0.19 五次 bump 让所有旧 Case 被拒;错误码未映射成 500;错误只画在首页卡下 → 表现为点了没反应。决策:session 打开用绑定身份(v1 取 Case → 读身份 → 注册表核验 → v2),不改迁移 | 待验收 | `codex/rectification-history-open-20260909`(BUG-621) |
|
||||
| `TASK-rectification-window-cluster-cap-20260909.md` | `PROGRESS-rectification-window-cluster-cap-20260909.md` | **P0** 真实用户:一小时窗口分 17 个签名簇,`select_signature_representatives` 按时间取前 12 个,14:40 之后整簇丢弃(本机复现表);区间按代表分钟跨度而非簇覆盖;中途说出真实时段时助手口头答应却未改 → 改为固定回复"范围开始时按资料定、中途不改"(产品否决口头改范围);intake 自定义范围等产品答复 | 待执行 | `codex/rectification-window-cluster-cap-20260909`(BUG-623~625,Skill 10.0.20) |
|
||||
|
||||
### 聊天主链路与首页
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# 生时校正 · 升 Skill 版本后历史对话仍能打开(2026-09-09)
|
||||
|
||||
对应 BUG-621。自动化锁的是打开 RPC 身份与侧栏错误位置,**锁不住**真实历史 Case 在 staging 上按绑定版本跑完一轮。
|
||||
|
||||
## 0. 确认测的是新版本
|
||||
|
||||
浏览器打开 `https://staging.jyotisha.chat/api/health`,看 `deployment.gitCommit` 前 8 位是否等于本单合入 staging 后的提交。不一致 = 先别测。
|
||||
|
||||
## 1. 升版本之后,旧校正仍能从历史对话打开(P0)
|
||||
|
||||
前置:账号里至少有一条在当前 Skill 版本之前创建的生时校正(例如 10.0.17 / 10.0.18 那天建的)。当前注册版本更高。
|
||||
|
||||
1. 停在首页,不要先点「生时校正」入口。
|
||||
2. 在左侧历史列表点那条旧校正。
|
||||
|
||||
- ✅ 预期:主栏打开该校正,沿用它绑定的那一版规则继续;不是空白、不是「暂时无法打开」。
|
||||
- ❌ 失败:点了没反应;或首页起始卡下才出现错误;或打开后按当前新版本重跑。
|
||||
|
||||
## 2. 绑定版本若已从注册表消失(P1)
|
||||
|
||||
只有在故意缺少快照的环境才测。正常 staging 应仍保留 10.0.15–10.0.18。
|
||||
|
||||
- ✅ 预期:被点的那一行下面出现一行错误(Skill 版本不可用),首页起始卡不再重复同一条。
|
||||
- ❌ 失败:仍像没点到,或主栏/起始卡出现 500「校正服务暂时不可用」。
|
||||
|
||||
## 3. 新开一场仍用当前版本(P1)
|
||||
|
||||
从首页点「生时校正」新建。
|
||||
|
||||
- ✅ 预期:新 Case 绑定当前注册版本(现为 10.0.19)。
|
||||
- ❌ 失败:新 Case 也去套某个旧版本。
|
||||
@@ -764,6 +764,18 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
font-weight: 500;
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
.session-open-error {
|
||||
grid-column: 1 / -1;
|
||||
margin: 0 var(--space-2) var(--space-2);
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--color-danger);
|
||||
font-size: var(--type-overline);
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
.session-main small, .session-subtitle { color: var(--color-ink-tertiary); line-height: 1.3; font-size: var(--type-overline); }
|
||||
.sidebar-group-label { margin: var(--space-2) 0 var(--space-1); color: var(--color-ink-tertiary); font-size: var(--type-overline); font-weight: 500; letter-spacing: .02em; }
|
||||
.session-list-sentinel { height: 1px; }
|
||||
|
||||
@@ -298,6 +298,7 @@ export default function Home() {
|
||||
const [rectificationLoading, setRectificationLoading] = useState(false);
|
||||
const [rectificationMutationPending, setRectificationMutationPending] = useState(false);
|
||||
const [rectificationError, setRectificationError] = useState("");
|
||||
const [rectificationErrorSessionId, setRectificationErrorSessionId] = useState<string | null>(null);
|
||||
const [rectificationReadonly, setRectificationReadonly] = useState(false);
|
||||
const [rectificationShouldStartOpening, setRectificationShouldStartOpening] = useState(false);
|
||||
const [rectificationTurns, setRectificationTurns] = useState<PersistedRectificationTurn[]>([]);
|
||||
@@ -448,7 +449,7 @@ export default function Home() {
|
||||
modelPersistence, modelSelectionVersions, modelSyncFailures, pendingSessionId, profile,
|
||||
rectificationSessionId, sessionDetailInFlight, sessionSelectionSource, sessions, sessionsRef,
|
||||
setActiveChartId, setActiveSessionId, setBirthTimeConsultationConsent, setCreatingSession,
|
||||
setDraft, setDraftEntrypoint, setDraftTheme, setRectificationError, setRequestError,
|
||||
setDraft, setDraftEntrypoint, setDraftTheme, setRectificationError, setRectificationErrorSessionId, setRequestError,
|
||||
setSessionDetailLoadingId, setSessionFullPrompt, setSessions, sessionsCursor, setSessionsCursor,
|
||||
showArchivedSessions, setShowArchivedSessions, uiPreview, visibleSessions,
|
||||
openRectificationSession: (exactSessionId) => rectificationSessionOpenerRef.current(exactSessionId),
|
||||
@@ -489,7 +490,7 @@ export default function Home() {
|
||||
rectificationOpenInFlight, rectificationSessionId, resumeRectificationSession,
|
||||
sessionSelectionSource, sessions, setActiveSessionId, setDraft, setDraftEntrypoint,
|
||||
setDraftTheme, setOnboardingStep, setProfileNotice, setRectificationCaseId,
|
||||
setRectificationEntrySummary, setRectificationError, setRectificationLoading,
|
||||
setRectificationEntrySummary, setRectificationError, setRectificationErrorSessionId, setRectificationLoading,
|
||||
setRectificationPendingQuestion, setRectificationReadonly, setRectificationSessionId,
|
||||
setRectificationShouldStartOpening, setRectificationSnapshot, setRectificationOpeningSessionId,
|
||||
setRectificationTurns, setSessions, uiPreview,
|
||||
@@ -1710,6 +1711,8 @@ export default function Home() {
|
||||
charts={sidebarCharts}
|
||||
activeSessionId={activeSession?.id ?? null}
|
||||
openingSessionId={rectificationOpeningSessionId}
|
||||
openErrorSessionId={rectificationErrorSessionId}
|
||||
openErrorMessage={rectificationErrorSessionId ? rectificationErrorMessage : ""}
|
||||
account={sidebarAccount}
|
||||
accountMenuOpen={accountMenuOpen}
|
||||
accountTriggerRef={accountTrigger}
|
||||
@@ -1876,9 +1879,9 @@ export default function Home() {
|
||||
rectificationMutationPending={rectificationMutationPending}
|
||||
openRectificationFromHomepage={openRectificationFromHomepage}
|
||||
rectificationEntrySummary={rectificationEntrySummary}
|
||||
rectificationError={rectificationError}
|
||||
rectificationError={rectificationErrorSessionId ? "" : rectificationError}
|
||||
rectificationSurfaceOpen={rectificationSurfaceOpen}
|
||||
rectificationErrorMessage={rectificationErrorMessage}
|
||||
rectificationErrorMessage={rectificationErrorSessionId ? "" : rectificationErrorMessage}
|
||||
starterThemes={starterThemes}
|
||||
starterSuggestions={starterSuggestions}
|
||||
startSuggestedConsultation={startSuggestedConsultation}
|
||||
|
||||
@@ -62,6 +62,9 @@ export type AppSidebarProps = {
|
||||
activeSessionId: string | null;
|
||||
/** A rectification session whose Case is being opened and hydrated; its row says so, statically. */
|
||||
openingSessionId?: string | null;
|
||||
/** Open failed for this history session; shown under that row only. */
|
||||
openErrorSessionId?: string | null;
|
||||
openErrorMessage?: string;
|
||||
account: SidebarAccount;
|
||||
accountMenuOpen: boolean;
|
||||
accountTriggerRef: Ref<HTMLButtonElement>;
|
||||
@@ -86,6 +89,8 @@ export function AppSidebar({
|
||||
charts,
|
||||
activeSessionId,
|
||||
openingSessionId = null,
|
||||
openErrorSessionId = null,
|
||||
openErrorMessage = "",
|
||||
account,
|
||||
accountMenuOpen,
|
||||
accountTriggerRef,
|
||||
@@ -159,6 +164,7 @@ export function AppSidebar({
|
||||
session={session}
|
||||
active={session.id === activeSessionId}
|
||||
opening={session.id === openingSessionId}
|
||||
error={session.id === openErrorSessionId && openErrorMessage ? openErrorMessage : undefined}
|
||||
disabled={sessionControls.disabled}
|
||||
menuOpen={sessionControls.menuSessionId === session.id}
|
||||
onMenuOpenChange={(open) => sessionControls.onMenuSessionChange(open ? session.id : null)}
|
||||
|
||||
@@ -46,6 +46,8 @@ type SidebarSessionRowProps = {
|
||||
readonly active: boolean;
|
||||
/** Its Case is being opened: a static note, no spinner. */
|
||||
readonly opening?: boolean;
|
||||
/** Open failed for this row: one line under the title, same notice scale as composer. */
|
||||
readonly error?: string;
|
||||
readonly disabled: boolean;
|
||||
readonly menuOpen: boolean;
|
||||
readonly onMenuOpenChange: (open: boolean) => void;
|
||||
@@ -61,6 +63,7 @@ export const SidebarSessionRow = forwardRef<HTMLButtonElement, SidebarSessionRow
|
||||
session,
|
||||
active,
|
||||
opening = false,
|
||||
error,
|
||||
disabled,
|
||||
menuOpen,
|
||||
onMenuOpenChange,
|
||||
@@ -110,6 +113,9 @@ export const SidebarSessionRow = forwardRef<HTMLButtonElement, SidebarSessionRow
|
||||
>
|
||||
<MoreHorizontal aria-hidden="true" />
|
||||
</Menu.Trigger>
|
||||
{error ? (
|
||||
<p className="session-open-error" role="alert">{error}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<Menu.Portal>
|
||||
<Menu.Positioner className="session-actions-positioner" side="bottom" align="end" sideOffset={4} collisionPadding={12}>
|
||||
|
||||
@@ -62,6 +62,7 @@ export type RectificationSurfaceParams = {
|
||||
setRectificationCaseId: Dispatch<SetStateAction<string | null>>;
|
||||
setRectificationEntrySummary: Dispatch<SetStateAction<RectificationEntrySummary | null>>;
|
||||
setRectificationError: Dispatch<SetStateAction<string>>;
|
||||
setRectificationErrorSessionId: Dispatch<SetStateAction<string | null>>;
|
||||
setRectificationLoading: Dispatch<SetStateAction<boolean>>;
|
||||
setRectificationPendingQuestion: Dispatch<SetStateAction<string | null>>;
|
||||
setRectificationReadonly: Dispatch<SetStateAction<boolean>>;
|
||||
@@ -104,6 +105,7 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
|
||||
setRectificationCaseId,
|
||||
setRectificationEntrySummary,
|
||||
setRectificationError,
|
||||
setRectificationErrorSessionId,
|
||||
setRectificationLoading,
|
||||
setRectificationPendingQuestion,
|
||||
setRectificationReadonly,
|
||||
@@ -167,11 +169,17 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
|
||||
rectificationOpenInFlight.current = true;
|
||||
setRectificationLoading(true);
|
||||
setRectificationError("");
|
||||
setRectificationErrorSessionId(null);
|
||||
// Read the selection source now: `selectSession` resets it synchronously
|
||||
// right after handing off, and the URL write below happens after awaits.
|
||||
const selectionSource = sessionSelectionSource.current;
|
||||
setRectificationOpeningSessionId(exactSessionId);
|
||||
|
||||
function assignOpenError(message: string) {
|
||||
setRectificationError(message);
|
||||
setRectificationErrorSessionId(intent === "session" ? exactSessionId : null);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/rectification/cases/open", {
|
||||
method: "POST",
|
||||
@@ -182,22 +190,23 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
|
||||
if (!response.ok) {
|
||||
const code = payload?.code;
|
||||
if (code === "profile_incomplete") {
|
||||
assignOpenError("profile_incomplete");
|
||||
handleRectificationProfileIncomplete();
|
||||
} else if (code === "invalid_open_request") {
|
||||
setRectificationError(payload?.error || "生时校正打开请求不正确,请刷新后重试。");
|
||||
assignOpenError(payload?.error || "生时校正打开请求不正确,请刷新后重试。");
|
||||
} else if (code === "active_case_conflict") {
|
||||
setRectificationError(payload?.error || "仍有未完成的校正,请先回到当前校正。");
|
||||
assignOpenError(payload?.error || "仍有未完成的校正,请先回到当前校正。");
|
||||
} else if (code === "case_session_not_found") {
|
||||
setRectificationError("该校正会话不存在或已结束。");
|
||||
assignOpenError("该校正会话不存在或已结束。");
|
||||
} else {
|
||||
setRectificationError(payload?.error || "暂时无法打开生时校正。");
|
||||
assignOpenError(payload?.error || "暂时无法打开生时校正。");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const opened = openResponseFromPayload(payload);
|
||||
if (!opened) {
|
||||
setRectificationError("生时校正服务响应不正确,请稍后重试。");
|
||||
assignOpenError("生时校正服务响应不正确,请稍后重试。");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -253,7 +262,7 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
|
||||
void refreshRectificationEntrySummary();
|
||||
return opened;
|
||||
} catch {
|
||||
setRectificationError("生时校正会话暂时无法打开,请稍后重试。");
|
||||
assignOpenError("生时校正会话暂时无法打开,请稍后重试。");
|
||||
return null;
|
||||
} finally {
|
||||
sessionSelectionSource.current = "user";
|
||||
|
||||
@@ -72,6 +72,7 @@ export type SessionManagementParams = {
|
||||
setDraftEntrypoint: (entrypoint: ConsultationEntrypoint | null) => void;
|
||||
setDraftTheme: (theme: Theme | null) => void;
|
||||
setRectificationError: Dispatch<SetStateAction<string>>;
|
||||
setRectificationErrorSessionId: Dispatch<SetStateAction<string | null>>;
|
||||
setRequestError: Dispatch<SetStateAction<RequestError | null>>;
|
||||
setSessionDetailLoadingId: Dispatch<SetStateAction<string | null>>;
|
||||
setSessionFullPrompt: Dispatch<SetStateAction<{ question: string; theme: Theme } | null>>;
|
||||
@@ -116,6 +117,7 @@ export function useSessionManagement(params: SessionManagementParams) {
|
||||
setDraftEntrypoint,
|
||||
setDraftTheme,
|
||||
setRectificationError,
|
||||
setRectificationErrorSessionId,
|
||||
setRequestError,
|
||||
setSessionDetailLoadingId,
|
||||
setSessionFullPrompt,
|
||||
@@ -337,6 +339,8 @@ export function useSessionManagement(params: SessionManagementParams) {
|
||||
setDraft("");
|
||||
setDraftEntrypoint(null);
|
||||
setComposerNotice("");
|
||||
setRectificationError("");
|
||||
setRectificationErrorSessionId(null);
|
||||
if (nextSession?.chartProfileId) {
|
||||
const boundChart = chartLibrary.find((record) => record.id === nextSession.chartProfileId);
|
||||
if (boundChart && boundChart.id !== activeChartId && accountId) {
|
||||
|
||||
@@ -17,7 +17,9 @@ import {
|
||||
import { resolveAyanamsa, type AyanamsaName } from "../../ayanamsa.ts";
|
||||
import { normalizePersistedBirthDate } from "../../birth-time-intake-model.ts";
|
||||
import {
|
||||
SkillPackageRegistryError,
|
||||
resolveActiveSkillPackage,
|
||||
resolveExactSkillPackage,
|
||||
resolveSkillPackageVersion,
|
||||
} from "../../skill-package-registry.ts";
|
||||
import {
|
||||
@@ -307,6 +309,7 @@ const KNOWN_RPC_ERROR_CODES = new Map<string, { status: number; code: string; me
|
||||
["agentic_rectification_case_session_mismatch", { status: 409, code: "case_session_mismatch", message: "校正记录与会话绑定不一致" }],
|
||||
["agentic_rectification_case_owner_mismatch", { status: 403, code: "case_owner_mismatch", message: "无权访问该校正记录" }],
|
||||
["agentic_rectification_skill_identity_missing", { status: 409, code: "skill_identity_missing", message: "该校正绑定的 Skill 版本不可用,请联系支持人员" }],
|
||||
["agentic_rectification_skill_identity_mismatch", { status: 409, code: "skill_identity_mismatch", message: "该校正绑定的规则版本与当前不一致,请刷新后重试" }],
|
||||
["agentic_rectification_legacy_skill_identity_unverifiable", { status: 409, code: "skill_identity_unverifiable", message: "该校正绑定的是无法核验的历史 Skill,请先采用当前注册版本" }],
|
||||
["agentic_rectification_skill_identity_already_verified", { status: 409, code: "skill_identity_already_verified", message: "该校正已经绑定可核验的 Skill,不能重复采用历史 Skill" }],
|
||||
["agentic_rectification_invalid_skill_identity", { status: 400, code: "invalid_skill_identity", message: "Skill 版本身份不正确" }],
|
||||
@@ -355,6 +358,75 @@ export function mapRectificationRpcError(error: unknown): RectificationServiceEr
|
||||
return { status: 500, code: "rectification_service_failed", message: "校正服务暂时不可用" };
|
||||
}
|
||||
|
||||
type OpenSkillIdentity = Readonly<{
|
||||
name: string;
|
||||
version: string;
|
||||
sha256: string;
|
||||
sourceCommit: string | null;
|
||||
}>;
|
||||
|
||||
function readBoundSkillIdentity(row: unknown): OpenSkillIdentity {
|
||||
if (!row || typeof row !== "object") {
|
||||
throw new RectificationCaseServiceError("invalid_skill_identity");
|
||||
}
|
||||
const value = row as Record<string, unknown>;
|
||||
const name = typeof value.skill_name === "string" ? value.skill_name : "";
|
||||
const version = typeof value.skill_version === "string" ? value.skill_version : "";
|
||||
const sha256 = typeof value.skill_sha256 === "string" ? value.skill_sha256 : "";
|
||||
const sourceCommit = value.skill_source_commit === null || value.skill_source_commit === undefined
|
||||
? null
|
||||
: typeof value.skill_source_commit === "string"
|
||||
? value.skill_source_commit
|
||||
: null;
|
||||
if (
|
||||
!name
|
||||
|| !version
|
||||
|| !/^[0-9a-f]{64}$/.test(sha256)
|
||||
|| (sourceCommit !== null && !/^[0-9a-f]{40}$/.test(sourceCommit))
|
||||
) {
|
||||
throw new RectificationCaseServiceError("invalid_skill_identity");
|
||||
}
|
||||
return { name, version, sha256, sourceCommit };
|
||||
}
|
||||
|
||||
async function resolveBoundSkillForSessionOpen(
|
||||
accounting: AccountingClient,
|
||||
userId: string,
|
||||
request: Extract<OpenRectificationCaseRequest, { intent: "session" }>,
|
||||
placeholder: OpenSkillIdentity,
|
||||
): Promise<OpenSkillIdentity> {
|
||||
const { data, error } = await accounting.rpc("open_agentic_rectification_case", {
|
||||
p_user_id: userId,
|
||||
p_request_id: request.requestId,
|
||||
p_intent: request.intent,
|
||||
p_session_id: request.sessionId,
|
||||
p_skill_name: placeholder.name,
|
||||
p_skill_version: placeholder.version,
|
||||
p_baseline_profile_fingerprint: "session-view",
|
||||
p_baseline_birth_snapshot: {},
|
||||
p_candidate_range: { start_time: "00:00", end_time: "23:59" },
|
||||
});
|
||||
if (error) throw new RectificationCaseServiceError(mapRectificationRpcError(error).code);
|
||||
const preview = openResponse(readRpcData(data));
|
||||
if (!preview) throw new RectificationCaseServiceError("invalid_open_response");
|
||||
|
||||
const identity = await accounting.rpc("get_agentic_rectification_skill_identity", {
|
||||
p_user_id: userId,
|
||||
p_case_id: preview.caseId,
|
||||
});
|
||||
if (identity.error) throw new RectificationCaseServiceError(mapRectificationRpcError(identity.error).code);
|
||||
const bound = readBoundSkillIdentity(readRpcData(identity.data));
|
||||
try {
|
||||
resolveExactSkillPackage(bound.name, bound.version, bound.sha256);
|
||||
} catch (caught) {
|
||||
if (caught instanceof SkillPackageRegistryError) {
|
||||
throw new RectificationCaseServiceError("skill_identity_missing");
|
||||
}
|
||||
throw caught;
|
||||
}
|
||||
return bound;
|
||||
}
|
||||
|
||||
export async function openRectificationCase(
|
||||
accounting: AccountingClient,
|
||||
userId: string,
|
||||
@@ -369,15 +441,18 @@ export async function openRectificationCase(
|
||||
if (activeSkill.version !== RECTIFICATION_SKILL_VERSION) {
|
||||
throw new RectificationCaseServiceError("skill_registry_version_mismatch");
|
||||
}
|
||||
const skill = request.intent === "session"
|
||||
? await resolveBoundSkillForSessionOpen(accounting, userId, request, activeSkill)
|
||||
: activeSkill;
|
||||
const { data, error } = await accounting.rpc("open_agentic_rectification_case_v2", {
|
||||
p_user_id: userId,
|
||||
p_request_id: request.requestId,
|
||||
p_intent: request.intent,
|
||||
p_session_id: request.intent === "session" ? request.sessionId : null,
|
||||
p_skill_name: activeSkill.name,
|
||||
p_skill_version: activeSkill.version,
|
||||
p_skill_sha256: activeSkill.sha256,
|
||||
p_skill_source_commit: activeSkill.sourceCommit,
|
||||
p_skill_name: skill.name,
|
||||
p_skill_version: skill.version,
|
||||
p_skill_sha256: skill.sha256,
|
||||
p_skill_source_commit: skill.sourceCommit,
|
||||
p_baseline_profile_fingerprint: profile?.baselineFingerprint ?? "session-view",
|
||||
p_baseline_birth_snapshot: profile?.baseline ?? {},
|
||||
p_candidate_range: profile?.candidateRange ?? { start_time: "00:00", end_time: "23:59" },
|
||||
|
||||
@@ -163,11 +163,12 @@ test("homepage rectification open failures always surface a visible recovery mes
|
||||
);
|
||||
assert.match(
|
||||
opening,
|
||||
/code === "invalid_open_request"[\s\S]*?setRectificationError\(/,
|
||||
/code === "invalid_open_request"[\s\S]*?assignOpenError\(/,
|
||||
);
|
||||
assert.match(opening, /function assignOpenError\(message: string\) \{\s*setRectificationError\(message\);/);
|
||||
assert.match(
|
||||
opening,
|
||||
/catch \{[\s\S]*?setRectificationError\("生时校正会话暂时无法打开,请稍后重试。"\)/,
|
||||
/catch \{[\s\S]*?assignOpenError\("生时校正会话暂时无法打开,请稍后重试。"\)/,
|
||||
);
|
||||
assert.match(
|
||||
page,
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { cssDeclarations } from "./css-contract-test-support.ts";
|
||||
import { homeSurface as page } from "./home-surface.ts";
|
||||
|
||||
const read = (relativePath: string) => readFileSync(new URL(relativePath, import.meta.url), "utf8");
|
||||
const sidebar = read("../src/components/app-sidebar.tsx");
|
||||
const sidebarRow = read("../src/components/sidebar-session-row.tsx");
|
||||
const starter = read("../src/components/starter-home.tsx");
|
||||
const styles = read("../src/app/globals.css");
|
||||
const pageSource = read("../src/app/page.tsx");
|
||||
const hook = read("../src/hooks/use-rectification-surface.ts");
|
||||
const sessions = read("../src/hooks/use-session-management.ts");
|
||||
const caseService = read("../src/lib/rectification-agentic/v9/case-service.ts");
|
||||
|
||||
test("history session open uses the bound skill identity instead of the live registry version", () => {
|
||||
// 原值:openRectificationCase 对 session 也把 resolveActiveSkillPackage() 交给 v2
|
||||
// 新值:session 先 v1 取 Case,再读绑定身份,注册表核验后把绑定身份交给 v2
|
||||
// 决策 1:打开 RPC 不再要求等于当前版本
|
||||
assert.match(caseService, /request\.intent === "session"/);
|
||||
assert.match(caseService, /open_agentic_rectification_case"/);
|
||||
assert.match(caseService, /get_agentic_rectification_skill_identity"/);
|
||||
assert.match(caseService, /resolveExactSkillPackage\(/);
|
||||
assert.match(caseService, /skill_identity_missing/);
|
||||
const homepageOpen = caseService.slice(
|
||||
caseService.indexOf("export async function openRectificationCase"),
|
||||
caseService.indexOf("export async function getRectificationEntrySummary"),
|
||||
);
|
||||
assert.match(homepageOpen, /intent === "homepage" \|\| request\.intent === "new"/);
|
||||
assert.match(homepageOpen, /open_agentic_rectification_case_v2/);
|
||||
});
|
||||
|
||||
test("skill_identity_mismatch maps to 409 instead of the 500 fallback", () => {
|
||||
// 原值:KNOWN_RPC_ERROR_CODES 没有 mismatch,落到 校正服务暂时不可用
|
||||
// 新值:409 skill_identity_mismatch,文案要求刷新
|
||||
// 决策 2:修完决策 1 后不应再出现;映射只为不再露 500
|
||||
assert.match(
|
||||
caseService,
|
||||
/"agentic_rectification_skill_identity_mismatch", \{ status: 409, code: "skill_identity_mismatch", message: "该校正绑定的规则版本与当前不一致,请刷新后重试" \}/,
|
||||
);
|
||||
});
|
||||
|
||||
test("a history-list open failure shows under the clicked row, not on the starter card", () => {
|
||||
// 原值:rectificationError 只由 starter-home 画;selectSession 不切会话,表现为点了没反应
|
||||
// 新值:session 失败绑到被点的那一行;首页起始卡不重复同一条
|
||||
// 决策 3:错误就地显示
|
||||
assert.match(pageSource, /const \[rectificationErrorSessionId, setRectificationErrorSessionId\]/);
|
||||
assert.match(
|
||||
pageSource,
|
||||
/rectificationError=\{rectificationErrorSessionId \? "" : rectificationError\}/,
|
||||
);
|
||||
assert.match(pageSource, /openErrorSessionId=\{rectificationErrorSessionId\}/);
|
||||
assert.match(
|
||||
pageSource,
|
||||
/openErrorMessage=\{rectificationErrorSessionId \? rectificationErrorMessage : ""\}/,
|
||||
);
|
||||
assert.match(starter, /rectificationError && !rectificationSurfaceOpen && \(/);
|
||||
assert.match(sidebar, /openErrorSessionId/);
|
||||
assert.match(sidebarRow, /error \? \(/);
|
||||
assert.match(sidebarRow, /className="session-open-error"/);
|
||||
assert.match(sidebarRow, /role="alert"/);
|
||||
assert.match(hook, /setRectificationErrorSessionId\(intent === "session" \? exactSessionId : null\)/);
|
||||
assert.match(sessions, /setRectificationErrorSessionId\(null\)/);
|
||||
const notice = cssDeclarations(".session-open-error", styles);
|
||||
assert.match(notice, /color:\s*var\(--color-danger\)/);
|
||||
assert.match(notice, /font-size:\s*var\(--type-overline\)/);
|
||||
assert.match(page, /openErrorSessionId=\{rectificationErrorSessionId\}/);
|
||||
});
|
||||
@@ -13,7 +13,12 @@ import {
|
||||
openRectificationCase,
|
||||
upgradeRectificationSkill,
|
||||
} from "../src/lib/rectification-agentic/v9/case-service.ts";
|
||||
import {
|
||||
RECTIFICATION_SKILL_NAME,
|
||||
RECTIFICATION_SKILL_VERSION,
|
||||
} from "../src/lib/rectification-agentic/v9/case-status.ts";
|
||||
import type { OpenRectificationCaseRequest } from "../src/lib/rectification-agentic/v9/open-request.ts";
|
||||
import { resolveSkillPackageVersion } from "../src/lib/skill-package-registry.ts";
|
||||
|
||||
type RpcHandler = (fn: string, args: Record<string, unknown>) => Promise<{
|
||||
data: unknown;
|
||||
@@ -471,6 +476,7 @@ test("homepage and new opens ignore active time and keep family_exact at ±15",
|
||||
uncertainty_after_minutes: 10,
|
||||
ayanamsa: "raman",
|
||||
});
|
||||
assert.equal(capturedArgs.value?.p_skill_version, RECTIFICATION_SKILL_VERSION);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -494,33 +500,111 @@ test("homepage open may create a new case even when resumable history exists", a
|
||||
assert.equal(response.shouldStartOpening, true);
|
||||
});
|
||||
|
||||
test("session intent passes the exact sessionId and never the profile snapshot", async () => {
|
||||
const capturedArgs: { value: Record<string, unknown> | null } = { value: null };
|
||||
function sessionOpenPayload(skillVersion: string) {
|
||||
return {
|
||||
disposition: "readonly" as const,
|
||||
case_id: caseId,
|
||||
session_id: sessionId,
|
||||
status: "closed",
|
||||
should_start_opening: false,
|
||||
skill_version: skillVersion,
|
||||
};
|
||||
}
|
||||
|
||||
test("session open uses the Case-bound skill identity, not the current registry version", async () => {
|
||||
// 原值:session 把当前注册版本(10.0.19)交给 v2,旧 Case 命中 skill_identity_mismatch
|
||||
// 新值:v1 按会话取 Case → 读绑定身份 → 注册表核验 → v2 收到绑定版本
|
||||
// 决策 1:历史校正以其自己那一版打开
|
||||
const bound = resolveSkillPackageVersion(RECTIFICATION_SKILL_NAME, "10.0.17");
|
||||
assert.notEqual(bound.version, RECTIFICATION_SKILL_VERSION);
|
||||
const captured: { v2: Record<string, unknown> | null; names: string[] } = { v2: null, names: [] };
|
||||
const accounting = fakeAccounting({
|
||||
profile: null,
|
||||
rpc: async (fn, args) => {
|
||||
capturedArgs.value = args;
|
||||
return {
|
||||
data: {
|
||||
disposition: "readonly",
|
||||
case_id: caseId,
|
||||
session_id: sessionId,
|
||||
status: "closed",
|
||||
should_start_opening: false,
|
||||
skill_version: "9.0.0",
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
captured.names.push(fn);
|
||||
if (fn === "open_agentic_rectification_case") {
|
||||
assert.equal(args.p_session_id, sessionId);
|
||||
assert.deepEqual(args.p_baseline_birth_snapshot, {});
|
||||
return { data: sessionOpenPayload(bound.version), error: null };
|
||||
}
|
||||
if (fn === "get_agentic_rectification_skill_identity") {
|
||||
assert.equal(args.p_case_id, caseId);
|
||||
return {
|
||||
data: {
|
||||
skill_name: bound.name,
|
||||
skill_version: bound.version,
|
||||
skill_sha256: bound.sha256,
|
||||
skill_source_commit: bound.sourceCommit,
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
if (fn === "open_agentic_rectification_case_v2") {
|
||||
captured.v2 = args;
|
||||
return { data: sessionOpenPayload(bound.version), error: null };
|
||||
}
|
||||
return { data: null, error: { message: `unexpected rpc ${fn}` } };
|
||||
},
|
||||
});
|
||||
|
||||
const response = await openRectificationCase(accounting, "user-1", {
|
||||
intent: "session",
|
||||
requestId,
|
||||
sessionId,
|
||||
});
|
||||
|
||||
assert.equal(response.disposition, "readonly");
|
||||
assert.equal(capturedArgs.value?.p_session_id, sessionId);
|
||||
assert.deepEqual(capturedArgs.value?.p_baseline_birth_snapshot, {});
|
||||
assert.deepEqual(captured.names, [
|
||||
"open_agentic_rectification_case",
|
||||
"get_agentic_rectification_skill_identity",
|
||||
"open_agentic_rectification_case_v2",
|
||||
]);
|
||||
assert.equal(captured.v2?.p_session_id, sessionId);
|
||||
assert.equal(captured.v2?.p_skill_name, bound.name);
|
||||
assert.equal(captured.v2?.p_skill_version, "10.0.17");
|
||||
assert.equal(captured.v2?.p_skill_sha256, bound.sha256);
|
||||
assert.equal(captured.v2?.p_skill_source_commit, bound.sourceCommit);
|
||||
assert.notEqual(captured.v2?.p_skill_version, RECTIFICATION_SKILL_VERSION);
|
||||
assert.deepEqual(captured.v2?.p_baseline_birth_snapshot, {});
|
||||
});
|
||||
|
||||
test("session open with a bound version missing from the registry is skill_identity_missing", async () => {
|
||||
const captured: { names: string[] } = { names: [] };
|
||||
const accounting = fakeAccounting({
|
||||
profile: null,
|
||||
rpc: async (fn, args) => {
|
||||
captured.names.push(fn);
|
||||
if (fn === "open_agentic_rectification_case") {
|
||||
return { data: sessionOpenPayload("10.0.1"), error: null };
|
||||
}
|
||||
if (fn === "get_agentic_rectification_skill_identity") {
|
||||
return {
|
||||
data: {
|
||||
skill_name: RECTIFICATION_SKILL_NAME,
|
||||
skill_version: "10.0.1",
|
||||
skill_sha256: "a".repeat(64),
|
||||
skill_source_commit: null,
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
return { data: null, error: { message: `unexpected rpc ${fn}` } };
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => openRectificationCase(accounting, "user-1", {
|
||||
intent: "session",
|
||||
requestId,
|
||||
sessionId,
|
||||
}),
|
||||
(error: unknown) =>
|
||||
error instanceof RectificationCaseServiceError && error.code === "skill_identity_missing",
|
||||
);
|
||||
assert.deepEqual(captured.names, [
|
||||
"open_agentic_rectification_case",
|
||||
"get_agentic_rectification_skill_identity",
|
||||
]);
|
||||
});
|
||||
|
||||
test("non-owner session opens surface a safe not-found error", async () => {
|
||||
@@ -725,6 +809,12 @@ test("rpc errors map to safe public views without leaking database text", async
|
||||
assert.equal(publicLegacy.status, 409);
|
||||
assert.equal(publicLegacy.code, "skill_identity_unverifiable");
|
||||
|
||||
const mismatch = mapRectificationRpcError(new Error("agentic_rectification_skill_identity_mismatch"));
|
||||
assert.equal(mismatch.status, 409);
|
||||
assert.equal(mismatch.code, "skill_identity_mismatch");
|
||||
assert.equal(mismatch.message, "该校正绑定的规则版本与当前不一致,请刷新后重试");
|
||||
assert.doesNotMatch(mismatch.message, /500|暂时不可用/);
|
||||
|
||||
const staleProbe = mapRectificationRpcError(new Error("agentic_rectification_stale_probe"));
|
||||
assert.equal(staleProbe.status, 409);
|
||||
assert.equal(staleProbe.code, "stale_probe");
|
||||
|
||||
@@ -151,6 +151,17 @@ test("checked-in registry verifies hashed product packages and leaves consult on
|
||||
{ projectRoot },
|
||||
);
|
||||
assert.equal(deprecated1011.status, "deprecated");
|
||||
const deprecated1017 = resolveExactSkillPackage(
|
||||
"jyotish-birth-time-rectification",
|
||||
"10.0.17",
|
||||
"f77b06de392feaa7c23e64beb05a90bcc0734db73747eb7915cba338b8eff1e6",
|
||||
{ projectRoot },
|
||||
);
|
||||
assert.equal(deprecated1017.status, "deprecated");
|
||||
assert.equal(
|
||||
deprecated1017.sha256,
|
||||
"f77b06de392feaa7c23e64beb05a90bcc0734db73747eb7915cba338b8eff1e6",
|
||||
);
|
||||
});
|
||||
|
||||
test("exact resolution of an old deprecated package is independent of active switches", (t) => {
|
||||
|
||||
Reference in New Issue
Block a user