fix(rectification): keep the adopt-card lock in state so lint can pass (BUG-622)
Writing selectionOfferLockRef during render tripped react-hooks/refs after the two contract tests went green. The lock now updates like seededTurns, and nextSelectionCardLock reuses the same object when the key is unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+5
-5
@@ -9631,7 +9631,7 @@
|
||||
- 根因:`showSelectionCards` 用 `!busy` 整张卸卡;采用请求把对话标成 busy。收尾句用未定义样式的 `.rectification-pending-note` 直接挂在消息列表里,没有助手列让位。采用后若 live offer 锚点被清掉,卡也不再回到原消息。
|
||||
- 修复:采用中或已采用时 busy 不再卸卡;用 resultId 锁住首次出示卡片的那条消息。收尾句改到卡片下方,样式与卡片同一条左边线。已采用列按钮保持按下并禁用。不恢复输入框上方状态条。
|
||||
- 验证:`frontend/tests/rectification-adopt-cards-stay-20260909.test.ts`、`frontend/tests/rectification-candidate-offer-anchor.test.ts`。`rectification-surface-contract.test.ts` 锁 `verified_idle` 为 `verifiedIdleCopy && !showSelectionCards`(BUG-622)。
|
||||
- 防复发:交付卡不得因采用 busy 卸载;`verified_idle` 不得使用无 inset 的裸段落;不得把采用状态写回 composer。改 `verified_idle` JSX 条件时必须同步表面合同正则。
|
||||
- 防复发:交付卡不得因采用 busy 卸载;`verified_idle` 不得使用无 inset 的裸段落;不得把采用状态写回 composer。改 `verified_idle` JSX 条件时必须同步表面合同正则。卡锚锁必须走渲染期 `useState`,不得在 render 里读写 ref(`react-hooks/refs`)。
|
||||
- 相关记录:BUG-595、BUG-619、BUG-622
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
@@ -9644,10 +9644,10 @@
|
||||
- 影响面:Gitea Independent Staging Quality Gate(`npm test --prefix frontend`)、`class-name-definition-contract.test.ts`、`rectification-surface-contract.test.ts`
|
||||
- 用户现象:staging 推送后质量门失败,镜像不发布。产品 UI 无新故障。
|
||||
- 触发条件:`ca921c3f`(BUG-618–620)改了 `verified_idle` JSX;`5c476ba7`(BUG-614/615)给 `.personal-report-chart-cell.is-changed` 写了填充。随后 `af70ae77` 再推 staging 仍是同一对失败(runs 2514 / 2516 / 2517)。
|
||||
- 根因:产品代码已改,合同测试仍锁旧字面量。`knownUnstyled` 还列着已有 CSS 规则的 `is-changed`;表面合同仍要求 `questionGap === "verified_idle" && (`,实际已是 `verifiedIdleCopy && !showSelectionCards`。
|
||||
- 修复:`is-changed` 移出 allowlist;`verified_idle` 断言改跟现行 JSX。不改业务组件。
|
||||
- 验证:`frontend/tests/class-name-definition-contract.test.ts`、`frontend/tests/rectification-surface-contract.test.ts`。
|
||||
- 防复发:给已在 allowlist 的 class 加规则必须同时删 allowlist 行;改 `verified_idle` 条件必须改表面合同正则,并写原值/新值/原因。
|
||||
- 根因:产品代码已改,合同测试仍锁旧字面量。`knownUnstyled` 还列着已有 CSS 规则的 `is-changed`;表面合同仍要求 `questionGap === "verified_idle" && (`,实际已是 `verifiedIdleCopy && !showSelectionCards`。测试转绿后 lint 才跑到:BUG-620 在渲染期读写 `selectionOfferLockRef`,`react-hooks/refs` 4 error。
|
||||
- 修复:`is-changed` 移出 allowlist;`verified_idle` 断言改跟现行 JSX。采用卡锁改为 `useState`,在渲染期按 `seededTurns` 同一模式更新(`nextSelectionCardLock` 同值返回同一对象,避免循环 setState)。
|
||||
- 验证:`frontend/tests/class-name-definition-contract.test.ts`、`frontend/tests/rectification-surface-contract.test.ts`、`frontend/tests/rectification-adopt-cards-stay-20260909.test.ts`、`frontend/tests/rectification-candidate-offer-anchor.test.ts`;`npm run lint` 0 error。
|
||||
- 防复发:给已在 allowlist 的 class 加规则必须同时删 allowlist 行;改 `verified_idle` 条件必须改表面合同正则,并写原值/新值/原因。跨渲染保留的卡锚不得写进 ref 再在 render 里读。
|
||||
- 相关记录:BUG-614、BUG-615、BUG-620
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
@@ -519,7 +519,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const stepStartedAtRef = useRef(0);
|
||||
const liveToolRef = useRef<string | null>(null);
|
||||
const liveBaseLabelRef = useRef("正在处理…");
|
||||
const selectionOfferLockRef = useRef<SelectionCardLock | null>(null);
|
||||
const [selectionOfferLock, setSelectionOfferLock] = useState<SelectionCardLock | null>(null);
|
||||
const [compactBoard, setCompactBoard] = useState(false);
|
||||
const [boardOpen, setBoardOpen] = useState(false);
|
||||
const [boardDiff, setBoardDiff] = useState(() => diffRectificationBoard(null, null));
|
||||
@@ -1458,14 +1458,19 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const persistedOfferKey = [...messages].reverse().find((message) => message.candidateOffer)?.renderKey;
|
||||
const liveSelectionCardKey = persistedOfferKey
|
||||
?? (canOfferCards && !candidateResult?.selectedTime ? latestSettledAssistant?.renderKey : undefined);
|
||||
selectionOfferLockRef.current = nextSelectionCardLock(selectionOfferLockRef.current, {
|
||||
// Same render-time store update as seededTurns: the lock must be current
|
||||
// before this paint (BUG-620). A ref write here trips react-hooks/refs.
|
||||
const nextOfferLock = nextSelectionCardLock(selectionOfferLock, {
|
||||
resultId: candidateResult?.resultId,
|
||||
key: liveSelectionCardKey,
|
||||
});
|
||||
if (nextOfferLock !== selectionOfferLock) {
|
||||
setSelectionOfferLock(nextOfferLock);
|
||||
}
|
||||
const selectionCardMessageKey = resolveSelectionCardMessageKey({
|
||||
persistedOfferKey,
|
||||
fallbackKey: latestSettledAssistant?.renderKey,
|
||||
locked: selectionOfferLockRef.current,
|
||||
locked: nextOfferLock,
|
||||
resultId: candidateResult?.resultId,
|
||||
selectedTime: candidateResult?.selectedTime,
|
||||
canOffer: canOfferCards,
|
||||
|
||||
@@ -203,7 +203,10 @@ export function nextSelectionCardLock(
|
||||
input: { resultId: string | null | undefined; key: string | undefined },
|
||||
): SelectionCardLock | null {
|
||||
if (!input.resultId) return null;
|
||||
if (input.key) return { resultId: input.resultId, key: input.key };
|
||||
if (input.key) {
|
||||
if (current?.resultId === input.resultId && current.key === input.key) return current;
|
||||
return { resultId: input.resultId, key: input.key };
|
||||
}
|
||||
if (current?.resultId === input.resultId) return current;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,12 @@ test("clicking 更像这个 does not unmount the range card for busy", () => {
|
||||
test("after adopt the card stays on the locked offering message", () => {
|
||||
assert.match(chat, /resolveSelectionCardMessageKey/);
|
||||
assert.match(chat, /nextSelectionCardLock/);
|
||||
assert.match(chat, /selectionOfferLockRef/);
|
||||
// 原值:selectionOfferLockRef.current = nextSelectionCardLock(...)
|
||||
// 新值:useState + 渲染期 setSelectionOfferLock(与 seededTurns 同一模式)
|
||||
// 原因:react-hooks/refs 禁止渲染期读写 ref;门禁 lint 0 error(BUG-622)
|
||||
assert.match(chat, /const \[selectionOfferLock, setSelectionOfferLock\] = useState/);
|
||||
assert.match(chat, /if \(nextOfferLock !== selectionOfferLock\) \{\s*setSelectionOfferLock\(nextOfferLock\);/);
|
||||
assert.doesNotMatch(chat, /selectionOfferLockRef/);
|
||||
assert.match(messageLoop, /showSelectionCards && candidateResult && message\.renderKey === selectionCardMessageKey/);
|
||||
});
|
||||
|
||||
|
||||
@@ -137,6 +137,10 @@ test("hydrated turns drop a stale client offer when GET says this turn is not th
|
||||
test("the offer lock keeps the original card host after adopt clears the live key", () => {
|
||||
const locked = nextSelectionCardLock(null, { resultId: "result-1", key: "offer" });
|
||||
assert.deepEqual(locked, { resultId: "result-1", key: "offer" });
|
||||
assert.equal(
|
||||
nextSelectionCardLock(locked, { resultId: "result-1", key: "offer" }),
|
||||
locked,
|
||||
);
|
||||
assert.deepEqual(
|
||||
nextSelectionCardLock(locked, { resultId: "result-1", key: undefined }),
|
||||
locked,
|
||||
|
||||
Reference in New Issue
Block a user