Cursor Debug Mode 实战:用日志定位搜索结果回退
先看哪一次请求写入了当前结果,再用相同完成顺序检验修复。
- 用明确完成顺序稳定复现旧响应覆盖
- 用 run_id、请求编号和处理决定核对本轮证据
- 同一搜索词的两次请求仍需区分身份
- 成功与失败都通过最新代次检查后再更新状态
- 保留固定预期,在清理诊断日志后继续回归
1. 搜索词停在 cat,结果为什么又回到 ca
连续输入 ca、cat,屏幕上的搜索词已经是 cat,列表却在稍后回到了 ca 的结果。代码里没有报错,每个请求也都成功了。让模型直接猜一个补丁,很容易得到看似合理的去抖、缓存或重试方案;真正要查的是:哪一次请求,在什么时刻获得了写入当前结果的资格。
本文准备了一个完整的离线 Node 实验。你可以把它放进 Cursor,用 Debug Mode 练习提出假设、观察运行记录、定位写入点和复验。我们先保留一个有错的实现,再检验“只比较搜索词”的修补为什么仍会失效,最后使用每个实例内递增的请求编号隔离旧结果。全文给出四份文件的源码,不依赖另外下载代码。
实测范围:2026-10-13,Linux x86_64、Node v24.19.0,19 项测试通过。Promise 由程序显式完成,不使用网络、计时器、浏览器、React 或模型。本文没有运行真实 Cursor 会话,也没有验证它的日志收集服务、界面操作或修复效果;下面的日志是这个实验自己产生的 JSON。
2. 先约定搜索框应该怎样变化
本例把 UI 压缩成四个字段:query 是当前输入,value 是当前结果,error 是当前错误,status 取 idle、loading、success 或 error。每次发起搜索都清空 value 和 error,并进入 loading。只有这个实例最后发起的请求可以把状态改成 success 或 error。旧请求完成时,即使最新请求仍在等待,也不能暂时填入旧结果。
这是一项明确的产品约定。实际项目也可以保留上一页结果、显示缓存或允许并行面板,但要先写清楚各自的可见状态。本例的 ca@r1、cat@r2 是合成返回值,用 @r1 这样的标记显示响应来源;它们不表示网络地址,也不假设真实接口一定返回请求编号。
| 输入与完成顺序 | 本例要求的最终状态 | 用途 |
|---|---|---|
| ca(r1)、cat(r2);r2→r1 | query=cat,value=cat@r2 | 复现旧响应覆盖 |
| cat(r1)、car(r2)、cat(r3);r2→r3→r1 | query=cat,value=cat@r3 | 识别同词的不同请求 |
| ca(r1)、cat(r2);r1→r2 | query=cat,value=cat@r2 | 顺序对照,单独通过不足以检出故障 |
3. 在 Cursor 中先提交一份可检验的调查任务
截至本文核对日,Cursor 的 Debug Mode 文档把工作过程描述为:提出假设、加入日志、由使用者复现、分析运行记录、定点修复,再复验并清理插桩;适用问题包括执行顺序和异步竞态。可以在 Agent 的模式选择器中选择 Debug Mode。下面是准备好本文文件后可使用的任务说明,属于建议输入,没有声称这段提示词已经由 Cursor 执行。
请调查本文 search-lab.mjs 的搜索结果回退。
先使用 broken 模式,暂不改 eligible,也不要改测试预期。
预期:最后发起的请求拥有结果和错误状态的写入权。
复现:ca(r1)、cat(r2),让 r2 先完成,再让 r1 完成。
请列出可以由本轮日志区分的假设,并说明需要观察哪个字段。
读完指定 run_id 的完整记录后,引用 seq 和 request_id 解释根因。
日志缺失、来自别轮或缺少处理决定时,请报告未知并补采集。
确认根因后再提出最小修改,保留同词请求与失败场景的测试。如果 Cursor 为项目加入了自己的采集代码,先核对本轮实际生成的复现步骤和日志位置。本文的字段和事件名由示例定义,不是 Cursor 的原生 trace 格式;也不需要向某个假定端口发送请求。原始搜索词、响应正文和错误内容可能包含用户资料,迁移到真实项目时应改用合成值或只记录必要标识。
4. 让假设对应到具体的一行证据
阅读代码前,可以先列出下面几种解释。假设的意义在于规定下一步看什么;只有出现相应记录,才能缩小原因。本文的受控返回值排除了接口生成错误内容这条路径,但真实服务仍需要自己的请求与响应核对。
| 假设 | 需要看到的证据 | 本例的判定 |
|---|---|---|
| 输入状态被旧回调改回 ca | 旧请求完成后 state.query 变成 ca | 故障日志仍是 query=cat,未发生这种变化 |
| 服务把 cat 请求答成了 ca | r2 的 resolved.value 就是 ca 的结果 | 本例明确给 r2 返回 cat@r2 |
| 旧请求不受限制地写入当前结果 | latest_id=2 时,r1 仍产生 committed | 故障运行 seq=6 直接显示这一点 |
| 日志不是本轮完整记录 | run_id 或 instance_id 不同,seq 缺失,缺少终结决定 | 只能标为 UNKNOWN,先补采集 |
这些判断只适用于给出的输入、代码和记录。不要把“没有看到某事件”自动理解为“事件没有发生”。如果最后一行丢了,恰好可能丢掉的就是旧请求的写入。
5. 文件一 search-lab.mjs:可控请求、原始故障和插桩
新建一个空目录,把下面内容保存为 search-lab.mjs。controlledTransport 保存 Promise 的 resolve 和 reject,由测试指定哪一个请求先完成。createSearch 的 broken 分支无条件接收完成结果,这就是本例的原始故障。query 分支保留待证伪的修补;generation 分支是随后验证的修复。
run_id 标记一轮场景,instance_id 区分搜索实例,seq 表示该实例内记录顺序。request_id 在发起时分配,latest_id 记录写日志时的最新编号。日志分别覆盖发起、完成和是否提交三个关键位置。requireRun 只检查这个有限实验的采集链是否完整;它不会代替状态断言,也不适合直接作为生产日志校验器。
// Offline teaching fixture. Events are ours, not a Cursor log schema.
export function controlledTransport() {
const pending = new Map();
return {
request(query, id) {
if (pending.has(id)) throw new Error(`duplicate request ${id}`);
let resolve, reject;
const promise = new Promise((ok, fail) => { resolve = ok; reject = fail; });
pending.set(id, { resolve, reject });
return promise;
},
settle(id, outcome) {
const slot = pending.get(id);
if (!slot) throw new Error(`no pending request ${id}`);
if (Object.hasOwn(outcome, 'error')) slot.reject(new Error(outcome.error));
else if (Object.hasOwn(outcome, 'value')) slot.resolve(outcome.value);
else throw new Error('outcome needs value or error');
pending.delete(id);
}
};
}
export function createSearch({ request, mode = 'generation',
runId, instanceId = 'search', events = [] }) {
if (!['broken', 'query', 'generation'].includes(mode)) throw new Error('bad mode');
if (!runId) throw new Error('runId required');
let latest = 0, seq = 0;
const state = { query: '', value: null, error: null, status: 'idle' };
const read = () => ({ ...state });
const emit = (event, id, query, extra = {}) => events.push({
run_id: runId, instance_id: instanceId, seq: ++seq,
event, request_id: id, latest_id: latest, query, ...extra
});
function eligible(id, query) {
if (mode === 'broken') return true; // Original bug: accept every completion.
if (mode === 'query') return query === state.query; // Deliberately weak fix.
return id === latest; // Fixed: identity of the latest request in this instance.
}
async function search(query) {
const id = ++latest;
Object.assign(state, { query, value: null, error: null, status: 'loading' });
emit('issued', id, query);
let outcome;
try {
const value = await request(query, id);
outcome = { value, error: null, status: 'success' };
emit('resolved', id, query, { value });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
outcome = { value: null, error: message, status: 'error' };
emit('rejected', id, query, { error: message });
}
if (eligible(id, query)) {
Object.assign(state, outcome);
emit('committed', id, query, { state: read() });
} else emit('discarded', id, query);
return read();
}
return { search, read, events };
}
export async function scenario(mode, queries, schedule, runId) {
const transport = controlledTransport();
const app = createSearch({ request: transport.request, mode, runId });
const jobs = queries.map(q => app.search(q));
const snapshots = [];
for (const step of schedule) {
transport.settle(step.id, step);
await jobs[step.id - 1]; // Wait for this completion's state update; no sleep.
snapshots.push(app.read());
}
if (schedule.length !== queries.length) throw new Error('incomplete schedule');
await Promise.all(jobs);
return { state: app.read(), snapshots, events: app.events };
}
// A bounded capture check for this fixture, not a general log validator.
export function requireRun(events, runId, instanceId, count) {
const unknown = reason => { throw new Error(`UNKNOWN: ${reason}`); };
if (!events.length) unknown('no events');
const issued = new Set(), arrived = new Set(), finished = new Set();
for (const [index, e] of events.entries()) {
if (e.run_id !== runId || e.instance_id !== instanceId) unknown('mixed identity');
if (e.seq !== index + 1) unknown('missing or reordered sequence');
if (!Number.isInteger(e.request_id) || e.request_id < 1 || e.request_id > count)
unknown('unexpected request');
const id = e.request_id;
if (e.event === 'issued') {
if (issued.has(id)) unknown('duplicate issue');
issued.add(id);
} else if (['resolved', 'rejected'].includes(e.event)) {
if (!issued.has(id) || arrived.has(id)) unknown('unexpected completion');
arrived.add(id);
} else if (['committed', 'discarded'].includes(e.event)) {
if (!arrived.has(id) || finished.has(id)) unknown('unexpected decision');
finished.add(id);
} else unknown('unknown event');
}
if (issued.size !== count || arrived.size !== count || finished.size !== count)
unknown('incomplete capture');
return events; // Passing does not prove correctness; inspect decisions next.
}
6. 文件二 demo.mjs:每次完成哪个请求都写在输入里
保存为 demo.mjs,与上一份文件放在同一目录。这里没有等待 100 毫秒之类的时间猜测:settle(2, …) 明确完成 r2,await 对应任务后才执行下一步。运行标签由命令行传入;重新采集时换一个标签,并保留各自的文件,避免把两次运行都叫 trial-01。
import { scenario, requireRun } from './search-lab.mjs';
const label = process.argv.find(arg => arg.startsWith('--run='))?.slice(6);
if (!label || !/^[a-zA-Z0-9_-]+$/.test(label))
throw new Error('use --run=<new-label>, for example --run=trial-01');
const race = [{ id: 2, value: 'cat@r2' }, { id: 1, value: 'ca@r1' }];
const aba = [{ id: 2, value: 'car@r2' }, { id: 3, value: 'cat@r3' },
{ id: 1, value: 'cat@r1' }];
const ordered = [{ id: 1, value: 'ca@r1' }, { id: 2, value: 'cat@r2' }];
const inputs = [
['race-broken', 'broken', ['ca', 'cat'], race],
['race-fixed', 'generation', ['ca', 'cat'], race],
['aba-query', 'query', ['cat', 'car', 'cat'], aba],
['aba-fixed', 'generation', ['cat', 'car', 'cat'], aba],
['ordered-broken', 'broken', ['ca', 'cat'], ordered],
['ordered-fixed', 'generation', ['ca', 'cat'], ordered]
];
const results = [];
for (const [name, mode, queries, schedule] of inputs) {
const runId = `${label}/${name}`;
const result = await scenario(mode, queries, schedule, runId);
requireRun(result.events, runId, 'search', queries.length);
results.push({ runId, mode, queries, schedule, ...result });
}
if (process.argv.includes('--json')) {
console.log(JSON.stringify({ node: process.version, results }, null, 2));
} else {
for (const r of results) console.log(`${r.runId}: query=${r.state.query} value=${r.state.value} status=${r.state.status}`);
for (const r of results.slice(0, 2)) {
console.log(`\n${r.runId} events:`);
for (const e of r.events) console.log(JSON.stringify(e));
}
}
node --version
node demo.mjs --run=trial-01
node demo.mjs --run=trial-02 --json > trial-02.json第二条命令在终端显示六个场景的结果和前两个场景的完整事件。第三条使用新标签 trial-02 再执行相同的确定性输入,生成包含全部输入、快照和日志的 JSON。调查真实问题时,也要一次执行使用一个新标签,切勿把相同标签的输出追加到同一份证据里。未提供 --run 时脚本会报错并以非零状态退出。
7. 实际输出:有错的实现也能通过顺序场景
Node v24.19.0 下,node demo.mjs --run=trial-01 的完整标准输出如下。前六行中,ordered-broken 与 ordered-fixed 一样,所以只跑顺序完成场景会漏掉回退。aba-query 表明搜索词重新变回 cat 后,旧的 cat 响应依然能越过字符串检查。
trial-01/race-broken: query=cat value=ca@r1 status=success
trial-01/race-fixed: query=cat value=cat@r2 status=success
trial-01/aba-query: query=cat value=cat@r1 status=success
trial-01/aba-fixed: query=cat value=cat@r3 status=success
trial-01/ordered-broken: query=cat value=cat@r2 status=success
trial-01/ordered-fixed: query=cat value=cat@r2 status=success
trial-01/race-broken events:
{"run_id":"trial-01/race-broken","instance_id":"search","seq":1,"event":"issued","request_id":1,"latest_id":1,"query":"ca"}
{"run_id":"trial-01/race-broken","instance_id":"search","seq":2,"event":"issued","request_id":2,"latest_id":2,"query":"cat"}
{"run_id":"trial-01/race-broken","instance_id":"search","seq":3,"event":"resolved","request_id":2,"latest_id":2,"query":"cat","value":"cat@r2"}
{"run_id":"trial-01/race-broken","instance_id":"search","seq":4,"event":"committed","request_id":2,"latest_id":2,"query":"cat","state":{"query":"cat","value":"cat@r2","error":null,"status":"success"}}
{"run_id":"trial-01/race-broken","instance_id":"search","seq":5,"event":"resolved","request_id":1,"latest_id":2,"query":"ca","value":"ca@r1"}
{"run_id":"trial-01/race-broken","instance_id":"search","seq":6,"event":"committed","request_id":1,"latest_id":2,"query":"ca","state":{"query":"cat","value":"ca@r1","error":null,"status":"success"}}
trial-01/race-fixed events:
{"run_id":"trial-01/race-fixed","instance_id":"search","seq":1,"event":"issued","request_id":1,"latest_id":1,"query":"ca"}
{"run_id":"trial-01/race-fixed","instance_id":"search","seq":2,"event":"issued","request_id":2,"latest_id":2,"query":"cat"}
{"run_id":"trial-01/race-fixed","instance_id":"search","seq":3,"event":"resolved","request_id":2,"latest_id":2,"query":"cat","value":"cat@r2"}
{"run_id":"trial-01/race-fixed","instance_id":"search","seq":4,"event":"committed","request_id":2,"latest_id":2,"query":"cat","state":{"query":"cat","value":"cat@r2","error":null,"status":"success"}}
{"run_id":"trial-01/race-fixed","instance_id":"search","seq":5,"event":"resolved","request_id":1,"latest_id":2,"query":"ca","value":"ca@r1"}
{"run_id":"trial-01/race-fixed","instance_id":"search","seq":6,"event":"discarded","request_id":1,"latest_id":2,"query":"ca"}
关注 race-broken 的 seq=4 到 seq=6:r2 已经把 cat@r2 写入,随后 r1 在 latest_id=2 的情况下又提交了 ca@r1。当前 query 没有改变,变化的是 value。race-fixed 的相同调度仍然收到 r1,但最后记录 discarded,状态保留 cat@r2。这比只看最终打印“成功”更能解释修复实际拦住了哪一步。
8. 为什么 query 相等还不够
有人会把接受条件改成 query === state.query。对 ca→cat 的回退,它确实能够挡住旧 ca。换成 cat→car→cat 时,第一个和第三个请求的字符串相同,身份却不同。我们让 r2 先完成,r3 再完成,最后才完成 r1;此时字符串比较会让 r1 把 cat@r3 覆盖成 cat@r1。
本例按每个控制器实例里的递增编号判断 id === latest。编号不需要跟随响应传回:它已经由当前 search 调用保存在闭包中。只要所有可见结果和错误写入都经过这道检查,同一个实例内已经过期的请求就不能更新这些状态。这个条件不取消请求,也不减少流量;它处理的是本地状态归属。
9. 文件三 clean-search.mjs:把成功和失败放在同一道检查后
保存以下清理版。它移除了诊断事件和两个错误模式,保留同样的状态约定。成功和失败都先生成 outcome,再判断本次 id 是否仍为 latest,避免旧请求的 catch 或 finally 单独修改当前错误、结果或 loading。最新请求失败时,本例应显示该失败,不能用随后完成的旧成功响应掩盖它。
// Same state contract after removing all diagnostic logging and bad modes.
export function createCleanSearch(request) {
let latest = 0;
const state = { query: '', value: null, error: null, status: 'idle' };
const read = () => ({ ...state });
async function search(query) {
const id = ++latest;
Object.assign(state, { query, value: null, error: null, status: 'loading' });
let outcome;
try {
outcome = { value: await request(query, id), error: null, status: 'success' };
} catch (error) {
outcome = { value: null,
error: error instanceof Error ? error.message : String(error), status: 'error' };
}
if (id === latest) Object.assign(state, outcome);
return read();
}
return { search, read };
}
递增变量位于工厂函数内部,两个搜索框分别创建控制器即可拥有独立代次。不要放成一个所有组件共享的全局计数器。真实界面还需要根据框架处理卸载、缓存、取消和重试等生命周期;这里没有实现这些功能。React 官方 useEffect 示例也指出响应可能乱序,并通过清理时忽略失效结果来处理相应场景;这为失效结果隔离提供原理参考,本文没有测试 React 实现。
10. 文件四 search.test.mjs:把结果写死,再逐次检查状态
预期值写在测试文件中,没有拿 generation 的输出来生成答案。六个基础用例包括两个已知错误输出:断言 broken 最后得到 ca@r1、query 最后得到 cat@r1,是为了证明反例确实存在,不代表这两个实现符合搜索框约定。后续测试再断言修复版本必须满足固定的正确状态。
除了最终值,测试还比较每次完成后的完整快照。旧请求先失败时,最新请求仍应保持 loading;旧请求后失败时,最新成功不能被抹掉;最新请求失败后,旧成功也不能让页面重新显示成功。清理版重复执行同词回退和这些失败场景。
import test from 'node:test';
import assert from 'node:assert/strict';
import { controlledTransport, createSearch, scenario, requireRun } from './search-lab.mjs';
import { createCleanSearch } from './clean-search.mjs';
const race = [{ id: 2, value: 'cat@r2' }, { id: 1, value: 'ca@r1' }];
const aba = [{ id: 2, value: 'car@r2' }, { id: 3, value: 'cat@r3' },
{ id: 1, value: 'cat@r1' }];
const ordered = [{ id: 1, value: 'ca@r1' }, { id: 2, value: 'cat@r2' }];
const success = (query, value) => ({ query, value, error: null, status: 'success' });
const loading = query => ({ query, value: null, error: null, status: 'loading' });
const failure = (query, error) => ({ query, value: null, error, status: 'error' });
const cases = [
['race broken', 'broken', ['ca', 'cat'], race, success('cat', 'ca@r1')],
['race fixed', 'generation', ['ca', 'cat'], race, success('cat', 'cat@r2')],
['same query weak fix', 'query', ['cat', 'car', 'cat'], aba, success('cat', 'cat@r1')],
['same query fixed', 'generation', ['cat', 'car', 'cat'], aba, success('cat', 'cat@r3')],
['ordered broken', 'broken', ['ca', 'cat'], ordered, success('cat', 'cat@r2')],
['ordered fixed', 'generation', ['ca', 'cat'], ordered, success('cat', 'cat@r2')]
];
for (const [name, mode, queries, schedule, expected] of cases) {
test(name, async () => {
const result = await scenario(mode, queries, schedule, name);
assert.deepEqual(result.state, expected); // Literal contract, not another implementation.
});
}
for (const [mode, expected] of [
['broken', ['issued:1', 'issued:2', 'resolved:2', 'committed:2', 'resolved:1', 'committed:1']],
['generation', ['issued:1', 'issued:2', 'resolved:2', 'committed:2', 'resolved:1', 'discarded:1']]
]) test(`${mode} exact event order`, async () => {
const r = await scenario(mode, ['ca', 'cat'], race, mode);
assert.deepEqual(r.events.map(e => `${e.event}:${e.request_id}`), expected);
});
test('stale success cannot replace latest success at any settlement', async () => {
const r = await scenario('generation', ['cat', 'car', 'cat'], aba, 'snapshots');
assert.deepEqual(r.snapshots, [loading('cat'), success('cat', 'cat@r3'), success('cat', 'cat@r3')]);
});
const errorCases = [
['old failure arrives first', [{ id: 1, error: 'old failed' }, { id: 2, value: 'cat@r2' }],
[loading('cat'), success('cat', 'cat@r2')]],
['old failure arrives last', [{ id: 2, value: 'cat@r2' }, { id: 1, error: 'old failed' }],
[success('cat', 'cat@r2'), success('cat', 'cat@r2')]],
['latest failure survives old success', [{ id: 2, error: 'latest failed' }, { id: 1, value: 'ca@r1' }],
[failure('cat', 'latest failed'), failure('cat', 'latest failed')]]
];
for (const [name, schedule, expected] of errorCases) test(name, async () => {
const r = await scenario('generation', ['ca', 'cat'], schedule, name);
assert.deepEqual(r.snapshots, expected);
});
test('two instances have independent generations', async () => {
const ta = controlledTransport(), tb = controlledTransport();
const a = createSearch({ request: ta.request, runId: 'two', instanceId: 'a' });
const b = createSearch({ request: tb.request, runId: 'two', instanceId: 'b' });
const a1 = a.search('ca'), a2 = a.search('cat'), b1 = b.search('dog');
tb.settle(1, { value: 'dog@b1' }); await b1;
ta.settle(2, { value: 'cat@a2' }); await a2;
ta.settle(1, { value: 'ca@a1' }); await a1;
assert.deepEqual(a.read(), success('cat', 'cat@a2'));
assert.deepEqual(b.read(), success('dog', 'dog@b1'));
});
test('synchronous request throw becomes latest error', async () => {
const app = createSearch({ request: () => { throw new Error('sync failed'); }, runId: 'sync' });
await app.search('cat');
assert.deepEqual(app.read(), failure('cat', 'sync failed'));
});
test('evidence capture accepts only the complete expected run', async () => {
const r = await scenario('generation', ['ca', 'cat'], race, 'capture');
assert.equal(requireRun(r.events, 'capture', 'search', 2), r.events);
assert.throws(() => requireRun([], 'capture', 'search', 2), /UNKNOWN: no events/);
assert.throws(() => requireRun(r.events.slice(0, -1), 'capture', 'search', 2), /UNKNOWN: incomplete capture/);
assert.throws(() => requireRun(r.events.filter(e => e.seq !== 3), 'capture', 'search', 2), /UNKNOWN: missing/);
const mixed = r.events.map(e => ({ ...e })); mixed[2].run_id = 'another-run';
assert.throws(() => requireRun(mixed, 'capture', 'search', 2), /UNKNOWN: mixed identity/);
const otherInstance = r.events.map(e => ({ ...e })); otherInstance[2].instance_id = 'other';
assert.throws(() => requireRun(otherInstance, 'capture', 'search', 2), /UNKNOWN: mixed identity/);
});
for (const [name, queries, schedule, expected] of [
['clean same query', ['cat', 'car', 'cat'], aba,
[loading('cat'), success('cat', 'cat@r3'), success('cat', 'cat@r3')]],
...errorCases.map(([name, schedule, expected]) => [`clean ${name}`, ['ca', 'cat'], schedule, expected])
]) test(name, async () => {
const t = controlledTransport(), app = createCleanSearch(t.request);
const jobs = queries.map(q => app.search(q)), snapshots = [];
for (const step of schedule) {
t.settle(step.id, step); await jobs[step.id - 1]; snapshots.push(app.read());
}
await Promise.all(jobs);
assert.deepEqual(snapshots, expected);
});
node --test --test-reporter=spec search.test.mjs本次实际运行结果为 tests 19、pass 19、fail 0、cancelled 0、skipped 0、todo 0。测试包括 6 个基础场景、2 个固定事件序列、1 个逐次状态检查、3 个异步失败场景、1 个多实例场景、1 个同步抛错场景、1 个采集完整性测试和 4 个清理后回归场景。测试框架同时显示每项耗时,耗时不作为验收指标。
如果断言失败,先核对四个文件是否完整复制、命令是否在同一目录执行,以及 Node 版本是否与实测环境一致。事件顺序不匹配时检查 schedule 中的 id 和每步 await,不要加 sleep 让测试碰运气,也不要直接把 expected 改成当前错误输出。本文验证的是列出的状态契约和场景,19 项通过并不等于实际应用的全部行为都已验收。
11. 日志缺失时,结论先停在未知
2026-01-30 的一条社区反馈提到,根因尚未确定时就给出修复,会让使用者不得不回滚后继续调查。2026-05-15 的另一条日志讨论描述了文件存在但搜索未找到的情况。2026-08-04 的后续讨论回复也澄清了日志已经写入,按实际文件提供给 Agent 后可读取。这些都是带日期的历史案例,不能据此断言今天的版本仍有相同故障,也不能把帖子标题中的“没有写入”当成已核实结论。
遇到类似情况,先确认当前这次复现生成了什么文件或终端输出,再按确切位置读取;核对 run_id、实例和请求数是否对应刚才的操作。若文件确实没有生成,保留复现步骤和采集错误继续排查。若已经生成但未读到,可以将本轮必要记录明确提供给 Agent。不要把别轮日志拼起来,也不必为了这个例子改整个仓库的忽略规则。
本文的 requireRun 对空数组、截断末尾、漏掉中间 seq、混入另一个 run_id 都会抛出 UNKNOWN 错误。这四种情况已在测试中实际构造。即便它返回了完整日志,仍要核对 committed 或 discarded 对应的请求,以及每次完成后的状态;采集完整本身不证明修复正确。
12. 带回真实项目时,把验收问题问到写入点
完成离线练习后,可以让 Cursor 回到项目里找到实际结果、错误和加载状态的写入位置,沿用已经写清楚的产品约定。真实网络无法像本例一样只调用 settle 排定顺序时,可以使用项目已有的可控测试替身复现;再另做真实服务和界面的验收。若想引入取消请求或数据缓存,也应分别说明它们要解决什么,并保留失效结果不能提交的检查。
请基于本轮完整日志说明:
1. 哪个请求是最后发起的,哪一个旧请求试图写入?给出对应 seq。
2. 本次补丁在哪里统一保护成功、错误和加载状态?
3. ca→cat 的乱序、cat→car→cat 的同词回退,以及新旧请求失败各得到什么?
4. 哪些断言的预期在修复前已经确定?移除诊断代码后是否仍通过?
5. 哪些环节还没在真实应用验证?如果证据缺失,请列出下一步采集内容。最后保留复现输入、失败用例和修复后的固定断言,清理临时诊断代码。对这个搜索框,值得留下来的证据很具体:旧请求在最新代次已经变化后完成,写入被拒绝,当前结果和错误状态仍属于最后一次操作。
这篇文章实测了 Cursor Debug Mode 自动修复吗?
没有。实测的是 Node v24.19.0 的离线搜索控制器和 19 项测试;Cursor 的工作流程依据官方文档,真实客户端、模型和日志收集器没有运行。
为什么比较搜索词相等仍会出现回退?
cat→car→cat 会产生两个搜索词相同但身份不同的请求。旧 cat 最后返回时,字符串检查会放行它;本例用每个实例内递增的请求编号区分。
旧请求失败也需要检查代次吗?
需要。本例的结果、错误和状态统一受代次检查保护。旧失败不能覆盖最新成功,旧成功也不能掩盖最新失败。
按顺序完成的测试通过,能说明修复好了么?
不能。本文的原始错误实现也能在顺序完成时得到正确最终值,必须保留乱序和同词负例,以及每次完成后的状态断言。
run_id 和 discarded 是 Cursor 规定的字段吗?
不是。这些字段和事件由本文示例定义,用来说明本轮运行和状态写入的关系。真实 Cursor 的采集方式应以当前会话和官方文档为准。