Skip to content
2 changes: 2 additions & 0 deletions packages/cli/src/ui/components/InputPrompt.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ const mockSlashCommands: SlashCommand[] = [
name: 'memory',
kind: CommandKind.BUILT_IN,
description: 'Manage memory',
// InputPrompt's live-slash submit gate requires action !== undefined.
action: vi.fn(),
subCommands: [
{
name: 'show',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/ui/components/InputPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1892,6 +1892,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
[
focus,
buffer,
slashCommands,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declined — the requested test cannot exist, because the stale-closure state it would pin is unreachable.

exportCompletion is also a dependency of this same useCallback, and its identity is derived from slashCommands: useExportCompletion computes exportFormatSuggestions = useMemo(..., [slashCommands]), which flows into exportCycleFormats, then into handleExportInput and suggestionDisplayProps, then into the memoized exportCompletion. So any change to the slashCommands prop identity already recreates the keypress callback and captures the current list, whether or not the explicit entry is there.

I built the test you sketched (render with memory filtered out, rerender with the full list, Enter on a buffered /memory) and ran your acceptance criterion:

dep present:  PASS - onSubmit('/memory')
dep deleted:  PASS - onSubmit('/memory')   <- does not go red

Instrumenting the gate at Enter time, with the dep deleted, shows why:

PROBE slashCommands.length=4   <- the fresh list; the stale one had 3

Mutating the array in place is not an alternative either: a stale closure over the same array object reads the mutated content, and Object.is would not fire for the explicit dep either way.

So there is no input where deleting the dep changes behavior, and react-hooks/exhaustive-deps under CI's --max-warnings 0 remains the operative guard, exactly as your witness showed. The dep itself stays: it is lint-required and correct on its own terms.

One caveat in your favour, recorded so it is not lost: the redundancy is indirect. It holds only while useExportCompletion keeps deriving from slashCommands. If that derivation is ever removed, the explicit entry becomes load-bearing and lint would again be the only guard. That is a reason to keep the dep, not a reason to add a test that asserts nothing — so I dropped the drafted test rather than ship 40 lines that are green either way.

中文说明

不予采纳 —— 所要求的测试无法存在,因为它想钉住的"过期闭包"状态是不可达的。

exportCompletion 同样是这个 useCallback 的依赖,而它的 identity 是由 slashCommands 派生出来的:useExportCompletion 计算 exportFormatSuggestions = useMemo(..., [slashCommands]),它流向 exportCycleFormats,再流向 handleExportInput 与 suggestionDisplayProps,最后进入被 memo 的 exportCompletion。因此只要 slashCommands 这个 prop 的 identity 发生变化,按键回调就已经会被重建并捕获最新的命令列表——无论显式依赖项在不在。

我按你的草图写了这个测试(先用过滤掉 memory 的列表渲染,再用完整列表 rerender,然后在缓冲区已有 /memory 的情况下按 Enter),并执行了你给出的验收标准:

依赖项存在:  PASS - onSubmit('/memory')
依赖项删除:  PASS - onSubmit('/memory')   <- 没有变红

在删除依赖项的情况下对 Enter 时的判定打点,说明了原因:

PROBE slashCommands.length=4   <- 拿到的是最新列表;过期的那份是 3

"原地修改数组"也不是另一条路:对同一个数组对象的过期闭包会读到被修改后的内容,而 Object.is 在两种情况下都不会为显式依赖项触发。

所以不存在任何输入能让"删除该依赖项"改变行为,而在 CI 的 --max-warnings 0 下,react-hooks/exhaustive-deps 仍然是真正起作用的守卫——这与你的 witness 结论一致。依赖项本身保留:它是 lint 所要求的,就其自身而言也是正确的。

有一点对你有利,记录下来以免丢失:这种冗余是间接的。它只在 useExportCompletion 继续从 slashCommands 派生时成立。一旦这个派生关系被移除,显式依赖项就会变成关键项,而 lint 又将再次成为唯一的守卫。这是保留该依赖项的理由,而不是新增一个什么都断言不了的测试的理由——因此我删掉了已起草的测试,而不是提交 40 行两种情况下都绿的代码。

completion,
shellModeActive,
setShellModeActive,
Expand Down
Loading