aura-basetools 0.7.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +223 -13
- package/bin/aura-mcp.mjs +169 -27
- package/bin/aura.mjs +257 -37
- package/lib/observation-store.mjs +150 -17
- package/lib/ops.mjs +183 -19
- package/lib/transaction-store.mjs +1 -1
- package/lib/workspace-lock.mjs +25 -4
- package/locator-lab/anchor-core.mjs +1 -1
- package/locator-lab/args.mjs +103 -6
- package/locator-lab/locator-api.mjs +401 -22
- package/locator-lab/read-core.mjs +634 -563
- package/locator-lab/repo-map.mjs +1 -1
- package/locator-lab/struct-core.mjs +119 -38
- package/locator-lab/write-path.mjs +663 -112
- package/package.json +8 -8
- package/test/argument-surface-test.mjs +119 -0
- package/test/doctor-test.mjs +73 -0
- package/test/observation-concurrency-test.mjs +90 -0
- package/test/standalone-test.mjs +202 -128
- package/test/write-test.mjs +294 -0
package/test/standalone-test.mjs
CHANGED
|
@@ -101,8 +101,8 @@ const layoutRoundTrip = (binPath, label) => {
|
|
|
101
101
|
fs.writeFileSync(file, 'function alpha() {\n const a = 1\n return a\n}\n')
|
|
102
102
|
const env2 = { ...process.env, AURA_CACHE_DIR: path.join(work, 'cache') }
|
|
103
103
|
const spawn = (args) => spawnSync(process.execPath, [binPath, ...args], { cwd: work, encoding: 'utf8', env: env2 })
|
|
104
|
-
const read = spawn(['read', '--file', file, '--mode', 'map'])
|
|
105
|
-
const edited = spawn(['edit', '--file', file, '--hunks', JSON.stringify([{ lines: [2, 2], replace: ' const a = 42' }])])
|
|
104
|
+
const read = spawn(['basetools', 'read', '--file', file, '--mode', 'map'])
|
|
105
|
+
const edited = spawn(['basetools', 'edit', '--file', file, '--hunks', JSON.stringify([{ lines: [2, 2], replace: ' const a = 42' }])])
|
|
106
106
|
const text = fs.readFileSync(file, 'utf8')
|
|
107
107
|
fs.rmSync(work, { recursive: true, force: true })
|
|
108
108
|
const parse = (res) => {
|
|
@@ -140,15 +140,15 @@ const countBom = (b) => {
|
|
|
140
140
|
say('A 读:取景与令牌')
|
|
141
141
|
{
|
|
142
142
|
const f = write('a.js', utf8('function alpha() {\n const a = 1\n return a\n}\n\nfunction beta() {\n const b = 2\n}\n'))
|
|
143
|
-
const map = json(run(['read', '--file', f.path, '--mode', 'map']))
|
|
143
|
+
const map = json(run(['basetools', 'read', '--file', f.path, '--mode', 'map']))
|
|
144
144
|
// 信封里 `symbols` 是**个数**,渲染好的目录在 `text` —— 断言照实际形状写(我第一版写成数组,当场红)
|
|
145
145
|
check(map.ok === true && typeof map.text === 'string' && map.symbols >= 2, 'map 返回符号目录(text + 计数)', JSON.stringify(map).slice(0, 120))
|
|
146
146
|
check(String(map.text).includes('beta'), 'map 里有 beta')
|
|
147
|
-
const around = json(run(['read', '--file', f.path, '--mode', 'around', '--around', 'beta']))
|
|
147
|
+
const around = json(run(['basetools', 'read', '--file', f.path, '--mode', 'around', '--around', 'beta']))
|
|
148
148
|
check(around.ok === true && String(around.text ?? '').includes('beta'), 'around 窗口里有 beta')
|
|
149
|
-
const full = json(run(['read', '--file', f.path, '--mode', 'full']))
|
|
149
|
+
const full = json(run(['basetools', 'read', '--file', f.path, '--mode', 'full']))
|
|
150
150
|
check(typeof full.version === 'string' && full.version.length > 0, `full 给出令牌 ${full.version}`)
|
|
151
|
-
const changed = json(run(['read', '--file', f.path, '--mode', 'changed', '--since', String(full.version)]))
|
|
151
|
+
const changed = json(run(['basetools', 'read', '--file', f.path, '--mode', 'changed', '--since', String(full.version)]))
|
|
152
152
|
check(changed.ok === true && String(changed.note ?? '').includes('no change'), '立即 changed = 无变化', JSON.stringify(changed).slice(0, 120))
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -156,14 +156,61 @@ say('A 读:取景与令牌')
|
|
|
156
156
|
say('B 改:按符号 + 回读证据')
|
|
157
157
|
{
|
|
158
158
|
const f = write('b.js', utf8('function alpha() {\n const a = 1\n return a\n}\n'))
|
|
159
|
-
const res = run(['edit', '--file', f.path, '--hunks', JSON.stringify([{ target: 'alpha', replace: 'function alpha() {\n return 42\n}' }])])
|
|
159
|
+
const res = run(['basetools', 'edit', '--file', f.path, '--hunks', JSON.stringify([{ target: 'alpha', replace: 'function alpha() {\n return 42\n}' }])])
|
|
160
160
|
const env2 = json(res)
|
|
161
161
|
check(env2.ok === true, '按符号改成功', JSON.stringify(env2).slice(0, 160))
|
|
162
162
|
check(String(fs.readFileSync(f.path, 'utf8')).includes('return 42'), '磁盘上真的改了')
|
|
163
163
|
check(env2.result?.write_verify?.level === 'bytes', `回读证据 level=${env2.result?.write_verify?.level}`)
|
|
164
|
-
const dry = json(run(['edit', '--file', f.path, '--hunks', JSON.stringify([{ target: 'alpha', replace: 'x' }]), '--dry-run']))
|
|
164
|
+
const dry = json(run(['basetools', 'edit', '--file', f.path, '--hunks', JSON.stringify([{ target: 'alpha', replace: 'x' }]), '--dry-run']))
|
|
165
165
|
check(dry.ok === true && dry.result?.dryRun === true, 'dry_run 不落盘')
|
|
166
166
|
check(String(fs.readFileSync(f.path, 'utf8')).includes('return 42'), 'dry_run 之后磁盘没变')
|
|
167
|
+
const beforePlan = fs.readFileSync(f.path, 'utf8')
|
|
168
|
+
const plan = json(run(['basetools', 'edit', '--file', f.path, '--hunks', JSON.stringify([{ target: 'alpha', replace: 'x' }]), '--plan']))
|
|
169
|
+
check(plan.ok === true && plan.result?.dryRun === true, '--plan 是 --dry-run 的同义旗标(回执照常给 applied)', JSON.stringify(plan).slice(0, 120))
|
|
170
|
+
check(fs.readFileSync(f.path, 'utf8') === beforePlan, '--plan 之后磁盘逐字节没变')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── B2 能力清单:运行时可查(0.9.9) ─────────────────────────────────────
|
|
174
|
+
say('B2 能力清单:capabilities 从引擎常量派生')
|
|
175
|
+
{
|
|
176
|
+
const caps = json(run(['basetools', 'capabilities']))
|
|
177
|
+
const readTool = caps.tools?.find((tool) => tool.name === 'aura_basetools_read')
|
|
178
|
+
const editTool = caps.tools?.find((tool) => tool.name === 'aura_basetools_edit')
|
|
179
|
+
check(caps.ok === true && caps.langsCount === caps.langs?.length, `langsCount=${caps.langsCount}`, JSON.stringify(caps.langs?.length))
|
|
180
|
+
check(caps.langsCount === 65, '语言表 65 种(与 LANGS 同源)', String(caps.langsCount))
|
|
181
|
+
check(readTool?.modes?.includes('auto') && readTool.modes.includes('map') && readTool.modes.includes('changed'), '读模式含 auto/map/changed', JSON.stringify(readTool?.modes))
|
|
182
|
+
check(editTool?.addressKinds?.includes('intent') && editTool.addressKinds.includes('target') && editTool.addressKinds.includes('handle'), '编辑地址档含 intent/target/handle', JSON.stringify(editTool?.addressKinds))
|
|
183
|
+
check(editTool?.options?.includes('replace_all'), '编辑选项含 replace_all', JSON.stringify(editTool?.options))
|
|
184
|
+
check(editTool?.options?.includes('require_observation'), '编辑选项含 require_observation', JSON.stringify(editTool?.options))
|
|
185
|
+
check(caps.limits?.multiTargetsMax === 32 && caps.limits?.defaultPruneBudget === 8192 && caps.limits?.contentBudget === 7492, '限制与常量一致', JSON.stringify(caps.limits))
|
|
186
|
+
check(caps.commands?.includes('basetools doctor') && caps.commands.includes('basetools capabilities') && caps.commands.includes('basetools write'), '命令表含 doctor/capabilities/write', JSON.stringify(caps.commands))
|
|
187
|
+
const writeTool = caps.tools?.find((tool) => tool.name === 'aura_basetools_write')
|
|
188
|
+
check(writeTool?.writeSafety?.dsh?.includes('observation-gate') && !writeTool.writeSafety.dsh.includes('transaction'), 'write 能力按入口分:DSH 有观察闸、无事务', JSON.stringify(writeTool?.writeSafety))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── B3 平铺 CLI 的空 replace = 删除(0.9.10 评审发现) ─────────────────────
|
|
192
|
+
say('B3 平铺 CLI:--replace "" 是删除,不是缺参数')
|
|
193
|
+
{
|
|
194
|
+
const f = write('del.txt', 'a\nb\n')
|
|
195
|
+
const res = run(['basetools', 'edit', '--file', f.path, '--lines', '1,1', '--replace', ''])
|
|
196
|
+
const env = json(res)
|
|
197
|
+
check(res.status === 0 && env.ok === true, '空 replace 被当成删除(exit 0)', JSON.stringify(env).slice(0, 140))
|
|
198
|
+
check(fs.readFileSync(f.path, 'utf8') === 'b\n', '第一行被删除,剩余字节不动', JSON.stringify(fs.readFileSync(f.path, 'utf8')))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── B4 分页读:内置 Read 的 offset/limit 形状(0.10.0) ───────────────────
|
|
202
|
+
say('B4 分页读:offset/limit 与 lines 互斥')
|
|
203
|
+
{
|
|
204
|
+
const f = write('page.txt', Array.from({ length: 10 }, (_, i) => `line ${i + 1}`).join('\n'))
|
|
205
|
+
const page = json(run(['basetools', 'read', '--file', f.path, '--offset', '2', '--limit', '3']))
|
|
206
|
+
check(page.ok === true && page.mode === 'lines' && page.totalLines === 10 && page.shown === '3-5', 'offset=2 limit=3 ⇒ 第 3-5 行', JSON.stringify({ mode: page.mode, totalLines: page.totalLines, shown: page.shown }))
|
|
207
|
+
check(page.text.includes('3: line 3') && page.text.includes('5: line 5') && !page.text.includes('6: line 6'), '正文只含 3-5 行', JSON.stringify(page.text))
|
|
208
|
+
const conflict = json(run(['basetools', 'read', '--file', f.path, '--lines', '1,2', '--offset', '0', '--limit', '2']))
|
|
209
|
+
check(conflict.ok === false && conflict.code === 'READ_BAD_ARGS', 'lines 与 offset/limit 同给 ⇒ 拒绝', JSON.stringify(conflict).slice(0, 160))
|
|
210
|
+
const oob = json(run(['basetools', 'read', '--file', f.path, '--offset', '99', '--limit', '2']))
|
|
211
|
+
check(oob.ok === false && oob.code === 'READ_BAD_ARGS', 'offset 越界 ⇒ 拒绝', JSON.stringify(oob).slice(0, 160))
|
|
212
|
+
const modeOffset = json(run(['basetools', 'read', '--file', f.path, '--mode', 'map', '--offset', '0', '--limit', '2']))
|
|
213
|
+
check(modeOffset.ok === false && modeOffset.code === 'READ_BAD_ARGS', '显式 mode 与 offset/limit 同给 ⇒ 拒绝(不静默换成 lines)', JSON.stringify(modeOffset).slice(0, 160))
|
|
167
214
|
}
|
|
168
215
|
|
|
169
216
|
// ── C 字节保真 ────────────────────────────────────────────────────────────
|
|
@@ -183,7 +230,7 @@ say('C 字节保真:改第 4 行,其余字节不动')
|
|
|
183
230
|
}
|
|
184
231
|
for (const [name, buf] of Object.entries(shapes)) {
|
|
185
232
|
const f = write(name, buf)
|
|
186
|
-
const res = run(['edit', '--file', f.path, '--hunks', JSON.stringify([{ lines: [4, 4], replace: 'EDITED' }])])
|
|
233
|
+
const res = run(['basetools', 'edit', '--file', f.path, '--hunks', JSON.stringify([{ lines: [4, 4], replace: 'EDITED' }])])
|
|
187
234
|
const env2 = json(res)
|
|
188
235
|
if (env2.ok !== true) {
|
|
189
236
|
check(false, `${name} 编辑成功`, JSON.stringify(env2).slice(0, 140))
|
|
@@ -208,14 +255,14 @@ say('C 字节保真:改第 4 行,其余字节不动')
|
|
|
208
255
|
say('D 陈旧闸门:读之后区域被外部改过 ⇒ 拒绝')
|
|
209
256
|
{
|
|
210
257
|
const f = write('d.md', utf8('one\ntwo\nthree\nfour\n'))
|
|
211
|
-
json(run(['read', '--file', f.path, '--mode', 'full'])) // 建"上次观测"(落盘缓存)
|
|
258
|
+
json(run(['basetools', 'read', '--file', f.path, '--mode', 'full'])) // 建"上次观测"(落盘缓存)
|
|
212
259
|
fs.writeFileSync(f.path, 'one\nTWO-BY-SOMEONE-ELSE\nthree\nfour\n') // 外部改第 2 行
|
|
213
|
-
const res = run(['edit', '--file', f.path, '--hunks', JSON.stringify([{ lines: [2, 2], replace: 'mine' }])])
|
|
260
|
+
const res = run(['basetools', 'edit', '--file', f.path, '--hunks', JSON.stringify([{ lines: [2, 2], replace: 'mine' }])])
|
|
214
261
|
const env2 = json(res)
|
|
215
262
|
check(env2.ok === false && env2.code === 'PATCH_STALE_REGION', `被拒绝(code=${env2.code})`, JSON.stringify(env2).slice(0, 160))
|
|
216
263
|
check(fs.readFileSync(f.path, 'utf8').includes('TWO-BY-SOMEONE-ELSE'), '磁盘保留外部改动')
|
|
217
264
|
// 带 expect 的老路不受影响:改第 4 行(未被动过)
|
|
218
|
-
const ok = json(run(['edit', '--file', f.path, '--hunks', JSON.stringify([{ lines: [4, 4], expect: 'four', replace: 'FOUR' }])]))
|
|
265
|
+
const ok = json(run(['basetools', 'edit', '--file', f.path, '--hunks', JSON.stringify([{ lines: [4, 4], expect: 'four', replace: 'FOUR' }])]))
|
|
219
266
|
check(ok.ok === true, '未被动过的区域照常可编辑', JSON.stringify(ok).slice(0, 140))
|
|
220
267
|
}
|
|
221
268
|
|
|
@@ -225,14 +272,14 @@ say('D 陈旧闸门:读之后区域被外部改过 ⇒ 拒绝')
|
|
|
225
272
|
say('E 建文件:create 可单独用(与 DSH 壳同一条规则)')
|
|
226
273
|
{
|
|
227
274
|
const p = path.join(tmp, 'made.md')
|
|
228
|
-
const res = run(['edit', '--file', p, '--create', 'hello\n'])
|
|
275
|
+
const res = run(['basetools', 'edit', '--file', p, '--create', 'hello\n'])
|
|
229
276
|
const env2 = json(res)
|
|
230
277
|
check(env2.ok === true && env2.result?.operation === 'create', `不给 --hunks 也能建文件 operation=${env2.result?.operation}`, JSON.stringify(env2).slice(0, 140))
|
|
231
278
|
check(fs.readFileSync(p, 'utf8') === 'hello\n', '内容是逐字节写下的')
|
|
232
279
|
// 兼容:显式给 `--hunks '[]'` 仍然可用(旧脚本不该因此变红)
|
|
233
280
|
const p2 = path.join(tmp, 'made2.md')
|
|
234
|
-
check(json(run(['edit', '--file', p2, '--hunks', '[]', '--create', 'hi\n'])).ok === true, "显式 `--hunks '[]' --create` 仍可用(向后兼容)")
|
|
235
|
-
const again = json(run(['edit', '--file', p, '--create', 'other\n']))
|
|
281
|
+
check(json(run(['basetools', 'edit', '--file', p2, '--hunks', '[]', '--create', 'hi\n'])).ok === true, "显式 `--hunks '[]' --create` 仍可用(向后兼容)")
|
|
282
|
+
const again = json(run(['basetools', 'edit', '--file', p, '--create', 'other\n']))
|
|
236
283
|
check(again.ok === false, '已存在时不覆盖(create 只用于不存在)', JSON.stringify(again).slice(0, 140))
|
|
237
284
|
}
|
|
238
285
|
|
|
@@ -244,21 +291,21 @@ say('E1 --create 的取值:front matter / 含换行 / 后面还跟旗标')
|
|
|
244
291
|
{
|
|
245
292
|
const fm = '---\ntitle: x\n---\n\nbody\n'
|
|
246
293
|
const p = path.join(tmp, 'frontmatter.md')
|
|
247
|
-
const res = json(run(['edit', '--file', p, '--create', fm]))
|
|
294
|
+
const res = json(run(['basetools', 'edit', '--file', p, '--create', fm]))
|
|
248
295
|
check(res.ok === true && res.result?.operation === 'create', '值以 `---` 开头也能建文件', JSON.stringify(res).slice(0, 140))
|
|
249
296
|
check(fs.readFileSync(p, 'utf8') === fm, '完整内容逐字节落下(换行与 `--` 都没丢)', JSON.stringify(fs.readFileSync(p, 'utf8')))
|
|
250
297
|
// 值后面还跟着另一个旗标:最容易被误判成“没带值”的形状。
|
|
251
298
|
// 注意**不能**用 `json()` 断这一条:`json()` 取的是 stdout 最后一行,而 `--pretty` 是多行输出
|
|
252
299
|
// (我第一版就是这么写错的——产品是对的,断言在量错东西)。这里直接看退出码与磁盘。
|
|
253
300
|
const p2 = path.join(tmp, 'frontmatter2.md')
|
|
254
|
-
const withFlag = run(['edit', '--file', p2, '--create', fm, '--pretty'])
|
|
301
|
+
const withFlag = run(['basetools', 'edit', '--file', p2, '--create', fm, '--pretty'])
|
|
255
302
|
check(withFlag.status === 0, `值后面再跟一个旗标仍然正确(exit=${withFlag.status})`, String(withFlag.stderr).slice(0, 90))
|
|
256
303
|
check(fs.readFileSync(p2, 'utf8') === fm, '第二种形状下内容也完整')
|
|
257
304
|
// `@文件` 形式:长内容绕开命令行引号(推荐写法,帮助文本里也写了)
|
|
258
305
|
const bodyFile = path.join(tmp, 'body.txt')
|
|
259
306
|
fs.writeFileSync(bodyFile, fm)
|
|
260
307
|
const p3 = path.join(tmp, 'fromfile.md')
|
|
261
|
-
check(json(run(['edit', '--file', p3, '--create', `@${bodyFile}`])).ok === true, '`--create @文件` 可用')
|
|
308
|
+
check(json(run(['basetools', 'edit', '--file', p3, '--create', `@${bodyFile}`])).ok === true, '`--create @文件` 可用')
|
|
262
309
|
check(fs.readFileSync(p3, 'utf8') === fm, '`@文件` 形式下内容也完整')
|
|
263
310
|
}
|
|
264
311
|
|
|
@@ -268,16 +315,16 @@ say('E1 --create 的取值:front matter / 含换行 / 后面还跟旗标')
|
|
|
268
315
|
say('E3 --hunks-file:前导 BOM 不让它失败')
|
|
269
316
|
{
|
|
270
317
|
const f = write('bomhunks.md', utf8('alpha\nbravo\n'))
|
|
271
|
-
json(run(['read', '--file', f.path, '--mode', 'full']))
|
|
318
|
+
json(run(['basetools', 'read', '--file', f.path, '--mode', 'full']))
|
|
272
319
|
const hunksPath = path.join(tmp, 'hunks-bom.json')
|
|
273
320
|
fs.writeFileSync(hunksPath, Buffer.concat([BOM, utf8('[{"lines":[2,2],"replace":"BRAVO"}]')]))
|
|
274
|
-
const res = json(run(['edit', '--file', f.path, '--hunks-file', hunksPath]))
|
|
321
|
+
const res = json(run(['basetools', 'edit', '--file', f.path, '--hunks-file', hunksPath]))
|
|
275
322
|
check(res.ok === true, '带 BOM 的 hunks 文件照样能编辑', JSON.stringify(res).slice(0, 140))
|
|
276
323
|
check(fs.readFileSync(f.path, 'utf8') === 'alpha\nBRAVO\n', '落盘正确', JSON.stringify(fs.readFileSync(f.path, 'utf8')))
|
|
277
324
|
// 真坏 JSON 时要报**实际来源**(旧版一律报 `--hunks`,把排查引到错方向)
|
|
278
325
|
const badPath = path.join(tmp, 'hunks-bad.json')
|
|
279
326
|
fs.writeFileSync(badPath, '{ not json')
|
|
280
|
-
const bad = json(run(['edit', '--file', f.path, '--hunks-file', badPath]))
|
|
327
|
+
const bad = json(run(['basetools', 'edit', '--file', f.path, '--hunks-file', badPath]))
|
|
281
328
|
check(bad.ok === false && String(bad.error).includes('--hunks-file'), '坏 JSON 报的是实际来源(--hunks-file)', String(bad.error).slice(0, 110))
|
|
282
329
|
}
|
|
283
330
|
|
|
@@ -288,17 +335,17 @@ say('E2 平铺旗标:edit --lines / --replace')
|
|
|
288
335
|
{
|
|
289
336
|
const f = write('flat.md', utf8('alpha\nbravo\ncharlie\n'))
|
|
290
337
|
// 先 read 建立观测(区域新鲜度闸门依赖它);本套件没有 readFull 助手,read 也要走 run。
|
|
291
|
-
const seen = json(run(['read', '--file', f.path, '--mode', 'full']))
|
|
338
|
+
const seen = json(run(['basetools', 'read', '--file', f.path, '--mode', 'full']))
|
|
292
339
|
check(seen.ok === true, 'E2 先 read 建立观测', JSON.stringify(seen).slice(0, 120))
|
|
293
|
-
const flat = json(run(['edit', '--file', f.path, '--lines', '3', '--replace', 'CHARLIE', '--expect', 'charlie']))
|
|
340
|
+
const flat = json(run(['basetools', 'edit', '--file', f.path, '--lines', '3', '--replace', 'CHARLIE', '--expect', 'charlie']))
|
|
294
341
|
check(flat.ok === true, '平铺旗标可以改一行', JSON.stringify(flat).slice(0, 160))
|
|
295
342
|
check(fs.readFileSync(f.path, 'utf8') === 'alpha\nbravo\nCHARLIE\n', '落盘正确', JSON.stringify(fs.readFileSync(f.path, 'utf8')))
|
|
296
343
|
// 半成品(只有 --replace、没有地址)必须继续报“需要 hunks”,不得被当成一个缺地址的 hunk
|
|
297
|
-
const half = json(run(['edit', '--file', f.path, '--replace', 'X']))
|
|
344
|
+
const half = json(run(['basetools', 'edit', '--file', f.path, '--replace', 'X']))
|
|
298
345
|
check(half.ok === false && half.code === 'USAGE', '只有 --replace ⇒ USAGE(不给假地址)', `${half.code}: ${String(half.error).slice(0, 70)}`)
|
|
299
346
|
// 两种形状同时给:`--hunks` 优先(显式数组是更强的意图表达)
|
|
300
|
-
json(run(['read', '--file', f.path, '--mode', 'full']))
|
|
301
|
-
const both = json(run(['edit', '--file', f.path, '--lines', '1', '--replace', 'ZZ', '--hunks', '[{"lines":[2,2],"replace":"BRAVO"}]']))
|
|
347
|
+
json(run(['basetools', 'read', '--file', f.path, '--mode', 'full']))
|
|
348
|
+
const both = json(run(['basetools', 'edit', '--file', f.path, '--lines', '1', '--replace', 'ZZ', '--hunks', '[{"lines":[2,2],"replace":"BRAVO"}]']))
|
|
302
349
|
check(both.ok === true && both.result?.applied?.[0]?.line === 2, '--hunks 与旗标同时给 ⇒ 以 --hunks 为准', JSON.stringify(both).slice(0, 140))
|
|
303
350
|
check(fs.readFileSync(f.path, 'utf8') === 'alpha\nBRAVO\nCHARLIE\n', '两种形状同给时只有 --hunks 那一条生效', JSON.stringify(fs.readFileSync(f.path, 'utf8')))
|
|
304
351
|
}
|
|
@@ -311,12 +358,12 @@ say('F MCP:initialize / tools/list / tools/call')
|
|
|
311
358
|
{ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-06-18', capabilities: {} } },
|
|
312
359
|
{ jsonrpc: '2.0', method: 'notifications/initialized' },
|
|
313
360
|
{ jsonrpc: '2.0', id: 2, method: 'tools/list' },
|
|
314
|
-
{ jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: '
|
|
361
|
+
{ jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: { file_path: f.path, mode: 'map' } } },
|
|
315
362
|
{
|
|
316
363
|
jsonrpc: '2.0',
|
|
317
364
|
id: 4,
|
|
318
365
|
method: 'tools/call',
|
|
319
|
-
params: { name: '
|
|
366
|
+
params: { name: 'aura_basetools_edit', arguments: { file_path: f.path, hunks: [{ lines: [4, 4], replace: 'EDITED' }] } },
|
|
320
367
|
},
|
|
321
368
|
]
|
|
322
369
|
const res = spawnSync(process.execPath, [mcpBin], { input: `${frames.map((x) => JSON.stringify(x)).join('\n')}\n`, encoding: 'utf8', env })
|
|
@@ -338,11 +385,11 @@ say('F MCP:initialize / tools/list / tools/call')
|
|
|
338
385
|
})()
|
|
339
386
|
if (manifest !== null) check(byId.get(1)?.result?.serverInfo?.version === manifest.version, `serverInfo.version 跟包一致(${manifest.version})`, String(byId.get(1)?.result?.serverInfo?.version))
|
|
340
387
|
const tools = byId.get(2)?.result?.tools ?? []
|
|
341
|
-
check(tools.length ===
|
|
388
|
+
check(tools.length === 5 && tools.some((t) => t.name === 'aura_basetools_read') && tools.some((t) => t.name === 'aura_basetools_edit') && tools.some((t) => t.name === 'aura_basetools_write') && tools.some((t) => t.name === 'aura_basetools_undo') && tools.some((t) => t.name === 'aura_basetools_transactions'), `tools/list 五个工具(${tools.map((t) => t.name).join(',')}`)
|
|
342
389
|
const readEnv = JSON.parse(byId.get(3)?.result?.content?.[0]?.text ?? '{}')
|
|
343
|
-
check(readEnv.ok === true && Array.isArray(readEnv.symbols ?? []), '
|
|
390
|
+
check(readEnv.ok === true && Array.isArray(readEnv.symbols ?? []), 'aura_basetools_read 经 MCP 可用', JSON.stringify(readEnv).slice(0, 120))
|
|
344
391
|
const editEnv = JSON.parse(byId.get(4)?.result?.content?.[0]?.text ?? '{}')
|
|
345
|
-
check(editEnv.ok === true, '
|
|
392
|
+
check(editEnv.ok === true, 'aura_basetools_edit 经 MCP 可用', JSON.stringify(editEnv).slice(0, 140))
|
|
346
393
|
check(fs.readFileSync(f.path, 'utf8').includes('EDITED'), 'MCP 路径真的落盘了')
|
|
347
394
|
check(replies.every((r) => r.jsonrpc === '2.0'), '每条回包都是 JSON-RPC 2.0')
|
|
348
395
|
}
|
|
@@ -356,19 +403,19 @@ say('G MCP:坏参数 / 未知工具 / 服务存活')
|
|
|
356
403
|
const frames = [
|
|
357
404
|
{ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {} } },
|
|
358
405
|
{ jsonrpc: '2.0', method: 'notifications/initialized' },
|
|
359
|
-
{ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: '
|
|
360
|
-
{ jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: '
|
|
361
|
-
{ jsonrpc: '2.0', id: 4, method: 'tools/call', params: { name: '
|
|
362
|
-
{ jsonrpc: '2.0', id: 5, method: 'tools/call', params: { name: '
|
|
406
|
+
{ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: { file: f.path, mode: 'map' } } },
|
|
407
|
+
{ jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: { file_path: f.path, mode: 'outline' } } },
|
|
408
|
+
{ jsonrpc: '2.0', id: 4, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: { file_path: path.join(tmp, 'nope.md'), mode: 'map' } } },
|
|
409
|
+
{ jsonrpc: '2.0', id: 5, method: 'tools/call', params: { name: 'aura_basetools_edit', arguments: { file_path: f.path, hunks: 'nope' } } },
|
|
363
410
|
{ jsonrpc: '2.0', id: 6, method: 'tools/call', params: { name: 'aura_nope', arguments: {} } },
|
|
364
|
-
{ jsonrpc: '2.0', id: 7, method: 'tools/call', params: { name: '
|
|
411
|
+
{ jsonrpc: '2.0', id: 7, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: { file_path: f.path, mode: 'map' } } },
|
|
365
412
|
]
|
|
366
413
|
const res = spawnSync(process.execPath, [mcpBin], { input: `${frames.map((x) => JSON.stringify(x)).join('\n')}\n`, encoding: 'utf8', env })
|
|
367
414
|
const replies = res.stdout.trim().split('\n').filter(Boolean).map((l) => JSON.parse(l))
|
|
368
415
|
const byId = new Map(replies.map((r) => [r.id, r]))
|
|
369
416
|
const envOf = (id) => JSON.parse(byId.get(id)?.result?.content?.[0]?.text ?? '{}')
|
|
370
417
|
check(replies.length === 7, `7 条请求全部有回包(收到 ${replies.length})`)
|
|
371
|
-
check(envOf(2).code === '
|
|
418
|
+
check(envOf(2).code === 'ARGS_UNKNOWN_KEY', `参数名写错 → ARGS_UNKNOWN_KEY(${envOf(2).code})`)
|
|
372
419
|
check(envOf(3).code === 'READ_BAD_MODE', `mode 写错 → READ_BAD_MODE(${envOf(3).code})`)
|
|
373
420
|
check(envOf(4).code === 'FS_NOT_FOUND', `文件不存在 → FS_NOT_FOUND(${envOf(4).code})`)
|
|
374
421
|
check(envOf(5).code === 'BAD_ARGS', `hunks 不是数组 → BAD_ARGS(${envOf(5).code})`)
|
|
@@ -383,7 +430,7 @@ say('G MCP:坏参数 / 未知工具 / 服务存活')
|
|
|
383
430
|
say('H MCP:参数形状宽容度')
|
|
384
431
|
{
|
|
385
432
|
const f = write('mcp-shapes.md', utf8('l1\nl2\nl3\nl4\n'))
|
|
386
|
-
const call = (id, args) => ({ jsonrpc: '2.0', id, method: 'tools/call', params: { name: '
|
|
433
|
+
const call = (id, args) => ({ jsonrpc: '2.0', id, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: args } })
|
|
387
434
|
const frames = [
|
|
388
435
|
{ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {} } },
|
|
389
436
|
{ jsonrpc: '2.0', method: 'notifications/initialized' },
|
|
@@ -392,9 +439,9 @@ say('H MCP:参数形状宽容度')
|
|
|
392
439
|
call(4, { file_path: f.path, lines: 2 }),
|
|
393
440
|
call(5, { file_path: f.path, lines: { start: 1, end: 2 } }),
|
|
394
441
|
// arguments 整体是 JSON 字符串(实测有客户端这么传)
|
|
395
|
-
{ jsonrpc: '2.0', id: 6, method: 'tools/call', params: { name: '
|
|
442
|
+
{ jsonrpc: '2.0', id: 6, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: JSON.stringify({ file_path: f.path, start_line: 1 }) } },
|
|
396
443
|
// arguments 是解不开的字符串:必须回结构化错误,不能挂死
|
|
397
|
-
{ jsonrpc: '2.0', id: 7, method: 'tools/call', params: { name: '
|
|
444
|
+
{ jsonrpc: '2.0', id: 7, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: '不是 JSON' } },
|
|
398
445
|
]
|
|
399
446
|
const res = spawnSync(process.execPath, [mcpBin], { input: `${frames.map((x) => JSON.stringify(x)).join('\n')}\n`, encoding: 'utf8', env })
|
|
400
447
|
const replies = res.stdout.trim().split('\n').filter(Boolean).map((l) => JSON.parse(l))
|
|
@@ -417,7 +464,7 @@ say('I MCP:hunk 行区间的宽容度')
|
|
|
417
464
|
jsonrpc: '2.0',
|
|
418
465
|
id,
|
|
419
466
|
method: 'tools/call',
|
|
420
|
-
params: { name: '
|
|
467
|
+
params: { name: 'aura_basetools_edit', arguments: { file_path: f.path, hunks: [hunk] } },
|
|
421
468
|
})
|
|
422
469
|
const frames = [
|
|
423
470
|
{ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {} } },
|
|
@@ -552,45 +599,45 @@ const normalizeGolden = (text, dir) => {
|
|
|
552
599
|
* `changed` 之前要先拿到令牌。**采集与比对共用这一份定义** ⇒ 黄金集不可能与用例漂开。
|
|
553
600
|
*/
|
|
554
601
|
const CLI_GOLDEN_CALLS = [
|
|
555
|
-
{ name: 'read-map', file: 'scene.js', args: () => ['read', '--file', 'scene.js', '--mode', 'map'] },
|
|
556
|
-
{ name: 'read-digest', file: 'scene.js', args: () => ['read', '--file', 'scene.js', '--mode', 'digest'] },
|
|
557
|
-
{ name: 'read-full', file: 'scene.js', args: () => ['read', '--file', 'scene.js', '--mode', 'full'] },
|
|
558
|
-
{ name: 'read-around', file: 'scene.js', args: () => ['read', '--file', 'scene.js', '--mode', 'around', '--around', 'beta'] },
|
|
559
|
-
{ name: 'read-lines', file: 'scene.js', args: () => ['read', '--file', 'scene.js', '--mode', 'lines', '--lines', '1,3'] },
|
|
560
|
-
{ name: 'read-search', file: 'scene.js', args: () => ['read', '--file', 'scene.js', '--mode', 'search', '--search', 'return'] },
|
|
602
|
+
{ name: 'read-map', file: 'scene.js', args: () => ['basetools', 'read', '--file', 'scene.js', '--mode', 'map'] },
|
|
603
|
+
{ name: 'read-digest', file: 'scene.js', args: () => ['basetools', 'read', '--file', 'scene.js', '--mode', 'digest'] },
|
|
604
|
+
{ name: 'read-full', file: 'scene.js', args: () => ['basetools', 'read', '--file', 'scene.js', '--mode', 'full'] },
|
|
605
|
+
{ name: 'read-around', file: 'scene.js', args: () => ['basetools', 'read', '--file', 'scene.js', '--mode', 'around', '--around', 'beta'] },
|
|
606
|
+
{ name: 'read-lines', file: 'scene.js', args: () => ['basetools', 'read', '--file', 'scene.js', '--mode', 'lines', '--lines', '1,3'] },
|
|
607
|
+
{ name: 'read-search', file: 'scene.js', args: () => ['basetools', 'read', '--file', 'scene.js', '--mode', 'search', '--search', 'return'] },
|
|
561
608
|
{
|
|
562
609
|
name: 'read-changed',
|
|
563
610
|
file: 'scene.js',
|
|
564
611
|
// 令牌是内容指纹(`<hash>:<字节数>`):现场读一次取回来,不在黄金集里钉一个手抄的令牌。
|
|
565
|
-
prepare: (spawn) => JSON.parse(spawn(['read', '--file', 'scene.js', '--mode', 'full']).stdout).version,
|
|
566
|
-
args: (token) => ['read', '--file', 'scene.js', '--mode', 'changed', '--since', String(token)],
|
|
612
|
+
prepare: (spawn) => JSON.parse(spawn(['basetools', 'read', '--file', 'scene.js', '--mode', 'full']).stdout).version,
|
|
613
|
+
args: (token) => ['basetools', 'read', '--file', 'scene.js', '--mode', 'changed', '--since', String(token)],
|
|
567
614
|
},
|
|
568
615
|
{
|
|
569
616
|
name: 'edit-target',
|
|
570
617
|
file: 'w.js',
|
|
571
|
-
prepare: (spawn) => void spawn(['read', '--file', 'w.js', '--mode', 'full']),
|
|
572
|
-
args: () => ['edit', '--file', 'w.js', '--hunks', JSON.stringify([{ target: 'alpha', replace: 'function alpha() {\n return 42\n}' }])],
|
|
618
|
+
prepare: (spawn) => void spawn(['basetools', 'read', '--file', 'w.js', '--mode', 'full']),
|
|
619
|
+
args: () => ['basetools', 'edit', '--file', 'w.js', '--hunks', JSON.stringify([{ target: 'alpha', replace: 'function alpha() {\n return 42\n}' }])],
|
|
573
620
|
},
|
|
574
621
|
{
|
|
575
622
|
name: 'edit-lines',
|
|
576
623
|
file: 'w.js',
|
|
577
|
-
prepare: (spawn) => void spawn(['read', '--file', 'w.js', '--mode', 'full']),
|
|
578
|
-
args: () => ['edit', '--file', 'w.js', '--hunks', JSON.stringify([{ lines: [2, 2], replace: ' const a = 42' }])],
|
|
624
|
+
prepare: (spawn) => void spawn(['basetools', 'read', '--file', 'w.js', '--mode', 'full']),
|
|
625
|
+
args: () => ['basetools', 'edit', '--file', 'w.js', '--hunks', JSON.stringify([{ lines: [2, 2], replace: ' const a = 42' }])],
|
|
579
626
|
},
|
|
580
627
|
{
|
|
581
628
|
name: 'edit-line-cue',
|
|
582
629
|
file: 'w.js',
|
|
583
|
-
prepare: (spawn) => void spawn(['read', '--file', 'w.js', '--mode', 'full']),
|
|
584
|
-
args: () => ['edit', '--file', 'w.js', '--hunks', JSON.stringify([{ line_cue: 'beta', replace: 'function beta() { /* cue */' }])],
|
|
630
|
+
prepare: (spawn) => void spawn(['basetools', 'read', '--file', 'w.js', '--mode', 'full']),
|
|
631
|
+
args: () => ['basetools', 'edit', '--file', 'w.js', '--hunks', JSON.stringify([{ line_cue: 'beta', replace: 'function beta() { /* cue */' }])],
|
|
585
632
|
},
|
|
586
633
|
{
|
|
587
634
|
name: 'edit-anchor',
|
|
588
635
|
file: 'w.js',
|
|
589
|
-
prepare: (spawn) => void spawn(['read', '--file', 'w.js', '--mode', 'full']),
|
|
590
|
-
args: () => ['edit', '--file', 'w.js', '--hunks', JSON.stringify([{ anchor: ' const b = 2', replace: ' const b = 22' }])],
|
|
636
|
+
prepare: (spawn) => void spawn(['basetools', 'read', '--file', 'w.js', '--mode', 'full']),
|
|
637
|
+
args: () => ['basetools', 'edit', '--file', 'w.js', '--hunks', JSON.stringify([{ anchor: ' const b = 2', replace: ' const b = 22' }])],
|
|
591
638
|
},
|
|
592
639
|
// `create` 没有“读一次”这回事:文件本来就还不存在。
|
|
593
|
-
{ name: 'edit-create', file: null, args: () => ['edit', '--file', 'made.md', '--create', 'hello\n'] },
|
|
640
|
+
{ name: 'edit-create', file: null, args: () => ['basetools', 'edit', '--file', 'made.md', '--create', 'hello\n'] },
|
|
594
641
|
]
|
|
595
642
|
|
|
596
643
|
/** 跑一次黄金集用例(干净的目录 + 干净的缓存 ⇒ 同一份输入恒定给同一份回执)。 */
|
|
@@ -617,54 +664,55 @@ const runGoldenCall = (call) => {
|
|
|
617
664
|
const CLI_GOLDEN = {
|
|
618
665
|
"read-map": {
|
|
619
666
|
"exit": 0,
|
|
620
|
-
"stdout": "{\"ok\":true,\"mode\":\"map\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"symbols\":4,\"shown\":4,\"byKind\":{\"function\":2,\"variable\":2},\"text\":\"10 lines · 4 symbols (function:2 variable:2)\\nexported (0):\\ninternal:\\n function alpha 1-4\\n function beta 6-9\",\"omitted\":\"2 nested locals omitted (inside a function — reach them with mode=lines, not target=)\",\"next\":\"mode=around around=<name> · mode=lines lines=[a,b] · mode=digest for signatures\"}\n"
|
|
667
|
+
"stdout": "{\"ok\":true,\"mode\":\"map\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"symbols\":4,\"shown\":4,\"byKind\":{\"function\":2,\"variable\":2},\"text\":\"10 lines · 4 symbols (function:2 variable:2)\\nexported (0):\\ninternal:\\n function alpha 1-4\\n function beta 6-9\",\"omitted\":\"2 nested locals omitted (inside a function — reach them with mode=lines, not target=)\",\"next\":\"mode=around around=<name> · mode=lines lines=[a,b] · mode=digest for signatures\",\"metrics\":{\"mode\":\"map\",\"lines\":10,\"chars\":154,\"symbols\":4,\"truncated\":false}}\n"
|
|
621
668
|
},
|
|
622
669
|
"read-digest": {
|
|
623
670
|
"exit": 0,
|
|
624
|
-
"stdout": "{\"ok\":true,\"mode\":\"digest\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"symbols\":4,\"exported\":[],\"text\":\"10 lines · 4 symbols · 0 control-flow\\nexported: (none)\\nimports: (none)\\nexternal calls: (none)\\n--- signatures ---\\n1-4 ( 4 lines) function alpha()\\n6-9 ( 4 lines) function beta()\",\"next\":\"mode=around around=<name> to see one function\"}\n"
|
|
671
|
+
"stdout": "{\"ok\":true,\"mode\":\"digest\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"symbols\":4,\"exported\":[],\"text\":\"10 lines · 4 symbols · 0 control-flow\\nexported: (none)\\nimports: (none)\\nexternal calls: (none)\\n--- signatures ---\\n1-4 ( 4 lines) function alpha()\\n6-9 ( 4 lines) function beta()\",\"next\":\"mode=around around=<name> to see one function\",\"metrics\":{\"mode\":\"digest\",\"lines\":10,\"chars\":177,\"symbols\":4,\"truncated\":false}}\n"
|
|
625
672
|
},
|
|
626
673
|
"read-full": {
|
|
627
674
|
"exit": 0,
|
|
628
|
-
"stdout": "{\"ok\":true,\"mode\":\"full\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"center\":1,\"label\":\"full file\",\"relocatable\":[{\"line\":1,\"target\":\"alpha\",\"lines\":[1,4]}],\"shown\":\"1-10\",\"text\":\"1: function alpha() {\\n2: const a = 1\\n3: return a\\n4: }\\n5: \\n6: function beta() {\\n7: const b = 2\\n8: return b\\n9: }\\n10: \"}\n"
|
|
675
|
+
"stdout": "{\"ok\":true,\"mode\":\"full\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"center\":1,\"label\":\"full file\",\"relocatable\":[{\"line\":1,\"target\":\"alpha\",\"lines\":[1,4]}],\"shown\":\"1-10\",\"text\":\"1: function alpha() {\\n2: const a = 1\\n3: return a\\n4: }\\n5: \\n6: function beta() {\\n7: const b = 2\\n8: return b\\n9: }\\n10: \",\"metrics\":{\"mode\":\"full\",\"lines\":10,\"chars\":123,\"truncated\":false}}\n"
|
|
629
676
|
},
|
|
630
677
|
"read-around": {
|
|
631
678
|
"exit": 0,
|
|
632
|
-
"stdout": "{\"ok\":true,\"mode\":\"around\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"center\":6,\"label\":\"around symbol function:beta (6-9)\",\"relocatable\":[{\"line\":1,\"target\":\"alpha\",\"lines\":[1,4]}],\"shown\":\"1-10\",\"text\":\"1: function alpha() {\\n2: const a = 1\\n3: return a\\n4: }\\n5: \\n6: function beta() {\\n7: const b = 2\\n8: return b\\n9: }\\n10: \"}\n"
|
|
679
|
+
"stdout": "{\"ok\":true,\"mode\":\"around\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"center\":6,\"label\":\"around symbol function:beta (6-9)\",\"relocatable\":[{\"line\":1,\"target\":\"alpha\",\"lines\":[1,4]}],\"shown\":\"1-10\",\"text\":\"1: function alpha() {\\n2: const a = 1\\n3: return a\\n4: }\\n5: \\n6: function beta() {\\n7: const b = 2\\n8: return b\\n9: }\\n10: \",\"metrics\":{\"mode\":\"around\",\"lines\":10,\"chars\":123,\"truncated\":false}}\n"
|
|
633
680
|
},
|
|
634
681
|
"read-lines": {
|
|
635
682
|
"exit": 0,
|
|
636
|
-
"stdout": "{\"ok\":true,\"mode\":\"lines\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"center\":1,\"label\":\"requested range\",\"relocatable\":[{\"line\":1,\"target\":\"alpha\",\"lines\":[1,4]}],\"shown\":\"1-3\",\"handle\":\"h<ID>a8d61e88:1-3\",\"text\":\"1: function alpha() { <h<ID>a8d61e88:1-3>\\n2: const a = 1\\n3: return a\"}\n"
|
|
683
|
+
"stdout": "{\"ok\":true,\"mode\":\"lines\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"center\":1,\"label\":\"requested range\",\"relocatable\":[{\"line\":1,\"target\":\"alpha\",\"lines\":[1,4]}],\"shown\":\"1-3\",\"handle\":\"h<ID>a8d61e88:1-3\",\"text\":\"1: function alpha() { <h<ID>a8d61e88:1-3>\\n2: const a = 1\\n3: return a\",\"metrics\":{\"mode\":\"lines\",\"lines\":10,\"chars\":73,\"truncated\":false}}\n"
|
|
637
684
|
},
|
|
638
685
|
"read-search": {
|
|
639
686
|
"exit": 0,
|
|
640
|
-
"stdout": "{\"ok\":true,\"mode\":\"search\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"matches\":2,\"handles\":[{\"line\":3,\"handle\":\"h<ID>82986ddc:3-3\"},{\"line\":8,\"handle\":\"h<ID>85987295:8-8\"}],\"relocatable\":[{\"line\":3,\"target\":\"alpha\",\"lines\":[1,4]},{\"line\":8,\"target\":\"beta\",\"lines\":[6,9]}],\"text\":\"3: return a <h<ID>82986ddc:3-3>\\n8: return b <h<ID>85987295:8-8>\"}\n"
|
|
687
|
+
"stdout": "{\"ok\":true,\"mode\":\"search\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"matches\":2,\"handles\":[{\"line\":3,\"handle\":\"h<ID>82986ddc:3-3\"},{\"line\":8,\"handle\":\"h<ID>85987295:8-8\"}],\"relocatable\":[{\"line\":3,\"target\":\"alpha\",\"lines\":[1,4]},{\"line\":8,\"target\":\"beta\",\"lines\":[6,9]}],\"text\":\"3: return a <h<ID>82986ddc:3-3>\\n8: return b <h<ID>85987295:8-8>\",\"metrics\":{\"mode\":\"search\",\"lines\":10,\"chars\":65,\"truncated\":false}}\n"
|
|
641
688
|
},
|
|
642
689
|
"read-changed": {
|
|
643
690
|
"exit": 0,
|
|
644
|
-
"stdout": "{\"ok\":true,\"mode\":\"changed\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"note\":\"no change since that version\",\"text\":\"no change since that version\"}\n"
|
|
691
|
+
"stdout": "{\"ok\":true,\"mode\":\"changed\",\"totalLines\":10,\"contentBudget\":7492,\"version\":\"531e7692:92\",\"note\":\"no change since that version\",\"text\":\"no change since that version\",\"metrics\":{\"mode\":\"changed\",\"lines\":10,\"chars\":28,\"truncated\":false}}\n"
|
|
645
692
|
},
|
|
646
693
|
"edit-target": {
|
|
647
694
|
"exit": 0,
|
|
648
|
-
"stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR
|
|
695
|
+
"stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR>/w.js\",\"applied\":[{\"line\":1,\"tier\":\"symbol\",\"byNear\":false,\"actual\":\"function alpha() {\\n const a = 1\\n return a\\n}\",\"replaced\":\"function alpha() {\\n return 42\\n}\",\"afterLine\":1,\"afterEndLine\":3,\"delta\":-1,\"handle\":\"h<ID>370ea564:1-3\"}],\"dryRun\":false,\"write_verify\":{\"level\":\"bytes\",\"bytes\":79}}}\n"
|
|
649
696
|
},
|
|
650
697
|
"edit-lines": {
|
|
651
698
|
"exit": 0,
|
|
652
|
-
"stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR
|
|
699
|
+
"stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR>/w.js\",\"applied\":[{\"line\":2,\"tier\":\"lines\",\"byNear\":false,\"actual\":\" const a = 1\",\"replaced\":\" const a = 42\",\"afterLine\":2,\"afterEndLine\":2,\"delta\":0,\"handle\":\"h<ID>9edee6fa:2-2\"}],\"dryRun\":false,\"write_verify\":{\"level\":\"bytes\",\"bytes\":93}}}\n"
|
|
653
700
|
},
|
|
654
701
|
"edit-line-cue": {
|
|
655
702
|
"exit": 0,
|
|
656
|
-
"stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR
|
|
703
|
+
"stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR>/w.js\",\"applied\":[{\"line\":6,\"tier\":\"lineCue\",\"byNear\":false,\"actual\":\"function beta() {\",\"replaced\":\"function beta() { /* cue */\",\"afterLine\":6,\"afterEndLine\":6,\"delta\":0,\"handle\":\"h<ID>dfda3c32:6-6\"}],\"dryRun\":false,\"write_verify\":{\"level\":\"bytes\",\"bytes\":102}}}\n"
|
|
657
704
|
},
|
|
658
705
|
"edit-anchor": {
|
|
659
706
|
"exit": 0,
|
|
660
|
-
"stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR
|
|
707
|
+
"stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR>/w.js\",\"applied\":[{\"line\":7,\"tier\":\"exact\",\"byNear\":false,\"actual\":\" const b = 2\",\"replaced\":\" const b = 22\",\"afterLine\":7,\"afterEndLine\":7,\"delta\":0,\"handle\":\"h<ID>6f2dcb63:7-7\"}],\"dryRun\":false,\"write_verify\":{\"level\":\"bytes\",\"bytes\":93}}}\n"
|
|
661
708
|
},
|
|
662
709
|
"edit-create": {
|
|
663
710
|
"exit": 0,
|
|
664
|
-
"stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR
|
|
711
|
+
"stdout": "{\"ok\":true,\"result\":{\"path\":\"<DIR>/made.md\",\"operation\":\"create\",\"applied\":[{\"line\":1,\"tier\":\"create\",\"actual\":\"\",\"replaced\":\"hello\\n\"}],\"dryRun\":false,\"write_verify\":{\"level\":\"bytes\",\"bytes\":6}}}\n"
|
|
665
712
|
}
|
|
666
713
|
}
|
|
667
714
|
|
|
715
|
+
|
|
668
716
|
if (process.argv.includes('--capture-cli-goldens')) {
|
|
669
717
|
const captured = {}
|
|
670
718
|
for (const call of CLI_GOLDEN_CALLS) {
|
|
@@ -705,6 +753,7 @@ say('MT-G 多目标读:CLI --targets(逐项/顺序/不连坐/汇总行/退
|
|
|
705
753
|
scene.write('a.js', MT_SOURCE)
|
|
706
754
|
scene.write('b.md', 'one\ntwo\nthree\n')
|
|
707
755
|
const res = scene.run([
|
|
756
|
+
'basetools',
|
|
708
757
|
'read',
|
|
709
758
|
'--targets',
|
|
710
759
|
JSON.stringify([
|
|
@@ -721,8 +770,8 @@ say('MT-G 多目标读:CLI --targets(逐项/顺序/不连坐/汇总行/退
|
|
|
721
770
|
JSON.stringify(batch.entries?.map((e) => e.file_path)),
|
|
722
771
|
)
|
|
723
772
|
// ① 每项与“单独一次单目标读”的同一字段值相同:拿整个信封逐字节比
|
|
724
|
-
const soloA = json(scene.run(['read', '--file', 'a.js', '--mode', 'digest']))
|
|
725
|
-
const soloB = json(scene.run(['read', '--file', 'b.md', '--mode', 'lines', '--lines', '1,2']))
|
|
773
|
+
const soloA = json(scene.run(['basetools', 'read', '--file', 'a.js', '--mode', 'digest']))
|
|
774
|
+
const soloB = json(scene.run(['basetools', 'read', '--file', 'b.md', '--mode', 'lines', '--lines', '1,2']))
|
|
726
775
|
const entryA = JSON.stringify(batch.entries?.[0]?.envelope)
|
|
727
776
|
const entryB = JSON.stringify(batch.entries?.[1]?.envelope)
|
|
728
777
|
check(entryA === JSON.stringify(soloA), '① A 项信封与单独读逐字节相同', byteDiff(entryA, JSON.stringify(soloA)) ?? '')
|
|
@@ -730,6 +779,7 @@ say('MT-G 多目标读:CLI --targets(逐项/顺序/不连坐/汇总行/退
|
|
|
730
779
|
// ② 一项目标不存在:整批 ok=false,另一项照常可读(不连坐)
|
|
731
780
|
const partial = json(
|
|
732
781
|
scene.run([
|
|
782
|
+
'basetools',
|
|
733
783
|
'read',
|
|
734
784
|
'--targets',
|
|
735
785
|
JSON.stringify([
|
|
@@ -743,7 +793,7 @@ say('MT-G 多目标读:CLI --targets(逐项/顺序/不连坐/汇总行/退
|
|
|
743
793
|
check(partial.entries?.[1]?.ok === false && partial.entries?.[1]?.code === 'FS_NOT_FOUND', `② 缺的那项报 FS_NOT_FOUND(${partial.entries?.[1]?.code})`)
|
|
744
794
|
check(partial.summary === 'targets: 2 | ok 1 | not_found 1', `③ 汇总行 ${JSON.stringify(partial.summary)}`)
|
|
745
795
|
check(partial.counts?.ok === 1 && partial.counts?.failed === 1, `③ counts ok=1 failed=1(${JSON.stringify(partial.counts)})`)
|
|
746
|
-
const missingOnly = scene.run(['read', '--targets', JSON.stringify([{ file_path: 'nope.md', mode: 'full' }])])
|
|
796
|
+
const missingOnly = scene.run(['basetools', 'read', '--targets', JSON.stringify([{ file_path: 'nope.md', mode: 'full' }])])
|
|
747
797
|
check(missingOnly.status === 2 && json(missingOnly).ok === false, `④ 逐项失败 ⇒ 走结果通道、退出码 2(实测 ${missingOnly.status})`)
|
|
748
798
|
}
|
|
749
799
|
|
|
@@ -754,11 +804,12 @@ say('MT-H 多目标写:CLI --targets(真落盘/零写入/同文件合并/dry
|
|
|
754
804
|
const scene = mtScene('h')
|
|
755
805
|
scene.write('c.md', 'one\ntwo\n')
|
|
756
806
|
scene.write('d.md', 'alpha\nbravo\n')
|
|
757
|
-
scene.run(['read', '--file', 'c.md', '--mode', 'full'])
|
|
758
|
-
scene.run(['read', '--file', 'd.md', '--mode', 'full'])
|
|
807
|
+
scene.run(['basetools', 'read', '--file', 'c.md', '--mode', 'full'])
|
|
808
|
+
scene.run(['basetools', 'read', '--file', 'd.md', '--mode', 'full'])
|
|
759
809
|
const beforeC = fs.readFileSync(path.join(scene.dir, 'c.md'))
|
|
760
810
|
const beforeD = fs.readFileSync(path.join(scene.dir, 'd.md'))
|
|
761
811
|
const res = scene.run([
|
|
812
|
+
'basetools',
|
|
762
813
|
'edit',
|
|
763
814
|
'--targets',
|
|
764
815
|
JSON.stringify([
|
|
@@ -775,8 +826,8 @@ say('MT-H 多目标写:CLI --targets(真落盘/零写入/同文件合并/dry
|
|
|
775
826
|
// ① 逐项回执与单目标同形:字段集合相同(同一个渲染,不是另写一套)
|
|
776
827
|
const soloScene = mtScene('h-solo')
|
|
777
828
|
soloScene.write('solo.md', 'one\ntwo\n')
|
|
778
|
-
soloScene.run(['read', '--file', 'solo.md', '--mode', 'full'])
|
|
779
|
-
const solo = json(soloScene.run(['edit', '--file', 'solo.md', '--hunks', JSON.stringify([{ lines: [1, 1], replace: 'ONE' }])]))
|
|
829
|
+
soloScene.run(['basetools', 'read', '--file', 'solo.md', '--mode', 'full'])
|
|
830
|
+
const solo = json(soloScene.run(['basetools', 'edit', '--file', 'solo.md', '--hunks', JSON.stringify([{ lines: [1, 1], replace: 'ONE' }])]))
|
|
780
831
|
const keysOf = (o) => Object.keys(o ?? {}).sort().join(',')
|
|
781
832
|
check(keysOf(batch.edits?.[0]?.receipt) === keysOf(solo.result), '① 逐项回执与单目标同形(字段集合相同)', `${keysOf(batch.edits?.[0]?.receipt)} vs ${keysOf(solo.result)}`)
|
|
782
833
|
|
|
@@ -784,11 +835,12 @@ say('MT-H 多目标写:CLI --targets(真落盘/零写入/同文件合并/dry
|
|
|
784
835
|
const zero = mtScene('h-zero')
|
|
785
836
|
zero.write('c.md', 'one\ntwo\n')
|
|
786
837
|
zero.write('d.md', 'alpha\nbravo\n')
|
|
787
|
-
zero.run(['read', '--file', 'c.md', '--mode', 'full'])
|
|
788
|
-
zero.run(['read', '--file', 'd.md', '--mode', 'full'])
|
|
838
|
+
zero.run(['basetools', 'read', '--file', 'c.md', '--mode', 'full'])
|
|
839
|
+
zero.run(['basetools', 'read', '--file', 'd.md', '--mode', 'full'])
|
|
789
840
|
const keepC = fs.readFileSync(path.join(zero.dir, 'c.md'))
|
|
790
841
|
const keepD = fs.readFileSync(path.join(zero.dir, 'd.md'))
|
|
791
842
|
const failed = zero.run([
|
|
843
|
+
'basetools',
|
|
792
844
|
'edit',
|
|
793
845
|
'--targets',
|
|
794
846
|
JSON.stringify([
|
|
@@ -812,9 +864,10 @@ say('MT-H 多目标写:CLI --targets(真落盘/零写入/同文件合并/dry
|
|
|
812
864
|
// ③ 同文件两个目标 ⇒ 合并成一次提交
|
|
813
865
|
const merged = mtScene('h-merge')
|
|
814
866
|
merged.write('m.md', 'one\ntwo\nthree\n')
|
|
815
|
-
merged.run(['read', '--file', 'm.md', '--mode', 'full'])
|
|
867
|
+
merged.run(['basetools', 'read', '--file', 'm.md', '--mode', 'full'])
|
|
816
868
|
const mergeEnv = json(
|
|
817
869
|
merged.run([
|
|
870
|
+
'basetools',
|
|
818
871
|
'edit',
|
|
819
872
|
'--targets',
|
|
820
873
|
JSON.stringify([
|
|
@@ -830,10 +883,10 @@ say('MT-H 多目标写:CLI --targets(真落盘/零写入/同文件合并/dry
|
|
|
830
883
|
// ④ dry-run 不落盘
|
|
831
884
|
const dry = mtScene('h-dry')
|
|
832
885
|
dry.write('c.md', 'one\ntwo\n')
|
|
833
|
-
dry.run(['read', '--file', 'c.md', '--mode', 'full'])
|
|
886
|
+
dry.run(['basetools', 'read', '--file', 'c.md', '--mode', 'full'])
|
|
834
887
|
const keep = fs.readFileSync(path.join(dry.dir, 'c.md'))
|
|
835
888
|
const dryEnv = json(
|
|
836
|
-
dry.run(['edit', '--dry-run', '--targets', JSON.stringify([{ file_path: 'c.md', hunks: [{ lines: [1, 1], replace: 'ONE' }] }])]),
|
|
889
|
+
dry.run(['basetools', 'edit', '--dry-run', '--targets', JSON.stringify([{ file_path: 'c.md', hunks: [{ lines: [1, 1], replace: 'ONE' }] }])]),
|
|
837
890
|
)
|
|
838
891
|
check(dryEnv.ok === true && dryEnv.edits?.[0]?.outcome === 'dry_run', `④ dryRun 的回执是 dry_run(${dryEnv.edits?.[0]?.outcome})`, JSON.stringify(dryEnv).slice(0, 160))
|
|
839
892
|
check(fs.readFileSync(path.join(dry.dir, 'c.md')).equals(keep), '④ dryRun 不落盘(逐字节不变)')
|
|
@@ -965,42 +1018,42 @@ say('MT-I 形状与边界:参数面拒绝(exit 1),判据只有在引擎
|
|
|
965
1018
|
scene.write('a.js', MT_SOURCE)
|
|
966
1019
|
const one = JSON.stringify([{ file_path: 'a.js', mode: 'digest' }])
|
|
967
1020
|
// ① 两种形状同时给 ⇒ 参数面拒绝(读侧与写侧各一次)
|
|
968
|
-
const readBoth = scene.run(['read', '--file', 'a.js', '--targets', one])
|
|
1021
|
+
const readBoth = scene.run(['basetools', 'read', '--file', 'a.js', '--targets', one])
|
|
969
1022
|
const readBothEnv = json(readBoth)
|
|
970
1023
|
check(readBoth.status === 1, `① read:--file 与 --targets 同时给 ⇒ exit 1(实测 ${readBoth.status})`, String(readBothEnv.error).slice(0, 120))
|
|
971
1024
|
check(readBothEnv.ok === false && readBothEnv.shape === 'ARGS_SHAPE_EXCLUSIVE', `① 读侧点名互斥判据(shape=${readBothEnv.shape})`, JSON.stringify(readBothEnv).slice(0, 160))
|
|
972
|
-
const editBoth = scene.run(['edit', '--file', 'a.js', '--targets', JSON.stringify([{ file_path: 'a.js', hunks: [{ lines: [1, 1], replace: 'X' }] }])])
|
|
1025
|
+
const editBoth = scene.run(['basetools', 'edit', '--file', 'a.js', '--targets', JSON.stringify([{ file_path: 'a.js', hunks: [{ lines: [1, 1], replace: 'X' }] }])])
|
|
973
1026
|
const editBothEnv = json(editBoth)
|
|
974
1027
|
check(editBoth.status === 1 && editBothEnv.shape === 'ARGS_SHAPE_EXCLUSIVE', `① edit:同样被拒(exit=${editBoth.status},shape=${editBothEnv.shape})`, String(editBothEnv.error).slice(0, 120))
|
|
975
1028
|
check(fs.readFileSync(path.join(scene.dir, 'a.js'), 'utf8') === MT_SOURCE, '① 被拒的写没有碰磁盘(逐字节不变)')
|
|
976
1029
|
|
|
977
1030
|
// ② 超 32 项
|
|
978
1031
|
const tooMany = Array.from({ length: 33 }, () => ({ file_path: 'a.js', mode: 'digest' }))
|
|
979
|
-
const over = scene.run(['read', '--targets', JSON.stringify(tooMany)])
|
|
1032
|
+
const over = scene.run(['basetools', 'read', '--targets', JSON.stringify(tooMany)])
|
|
980
1033
|
const overEnv = json(over)
|
|
981
1034
|
check(over.status === 1, `② 33 项 ⇒ exit 1(实测 ${over.status})`)
|
|
982
1035
|
check(overEnv.shape === 'ARGS_TARGETS_LIMIT' && overEnv.got === 33, `② 报出实得数(got=${overEnv.got},shape=${overEnv.shape})`, String(overEnv.error).slice(0, 120))
|
|
983
1036
|
|
|
984
1037
|
// ③ 空数组不是“成功读了 0 个”
|
|
985
|
-
const empty = scene.run(['read', '--targets', '[]'])
|
|
1038
|
+
const empty = scene.run(['basetools', 'read', '--targets', '[]'])
|
|
986
1039
|
const emptyEnv = json(empty)
|
|
987
1040
|
check(empty.status === 1 && emptyEnv.shape === 'ARGS_TARGETS_SHAPE', `③ 空数组 ⇒ 拒绝(exit=${empty.status},shape=${emptyEnv.shape})`, String(emptyEnv.error).slice(0, 120))
|
|
988
1041
|
|
|
989
1042
|
// ④ 不是 JSON:说清来源与接受的形状
|
|
990
|
-
const notJson = json(scene.run(['read', '--targets', '[{file_path: a.js}]']))
|
|
1043
|
+
const notJson = json(scene.run(['basetools', 'read', '--targets', '[{file_path: a.js}]']))
|
|
991
1044
|
check(notJson.code === 'USAGE' && String(notJson.error).includes('--targets') && String(notJson.error).includes('JSON'), '④ 字面 JSON 坏了 ⇒ 说清来源与形状', String(notJson.error).slice(0, 140))
|
|
992
|
-
const missingSpec = json(scene.run(['read', '--targets', '@nope.json']))
|
|
1045
|
+
const missingSpec = json(scene.run(['basetools', 'read', '--targets', '@nope.json']))
|
|
993
1046
|
check(missingSpec.code === 'USAGE' && String(missingSpec.error).includes('@nope.json'), '④ @文件 读不到 ⇒ 报的是实际来源', String(missingSpec.error).slice(0, 140))
|
|
994
|
-
const bare = json(scene.run(['read', '--targets']))
|
|
1047
|
+
const bare = json(scene.run(['basetools', 'read', '--targets']))
|
|
995
1048
|
check(bare.code === 'USAGE', `④ \`--targets\` 没带值 ⇒ USAGE(收到 ${JSON.stringify(String(bare.error).slice(0, 60))})`)
|
|
996
1049
|
// 与 `--hunks-file` 同一条理由:PowerShell 5.1 默认写 BOM,剥掉即可
|
|
997
1050
|
fs.writeFileSync(path.join(scene.dir, 'spec.json'), Buffer.concat([BOM, Buffer.from(one, 'utf8')]))
|
|
998
|
-
const bomSpec = scene.run(['read', '--targets', '@spec.json'])
|
|
1051
|
+
const bomSpec = scene.run(['basetools', 'read', '--targets', '@spec.json'])
|
|
999
1052
|
check(bomSpec.status === 0 && json(bomSpec).ok === true, `④ \`@文件\` 带 BOM 照样能用(exit=${bomSpec.status})`, String(bomSpec.stderr).slice(0, 100))
|
|
1000
1053
|
|
|
1001
1054
|
// ⑤ 顶层不认识的键不许静默忽略(`dryrun` 拼错而照写盘是最坏的一种)
|
|
1002
1055
|
const unknown = json(
|
|
1003
|
-
scene.run(['edit', '--targets', JSON.stringify({ targets: [{ file_path: 'a.js', hunks: [{ lines: [1, 1], replace: 'X' }] }], dryrun: true })]),
|
|
1056
|
+
scene.run(['basetools', 'edit', '--targets', JSON.stringify({ targets: [{ file_path: 'a.js', hunks: [{ lines: [1, 1], replace: 'X' }] }], dryrun: true })]),
|
|
1004
1057
|
)
|
|
1005
1058
|
check(unknown.code === 'USAGE' && unknown.shape === 'ARGS_UNKNOWN_KEY', `⑤ 未知顶层键 ⇒ 拒绝(shape=${unknown.shape})`, String(unknown.error).slice(0, 140))
|
|
1006
1059
|
check(fs.readFileSync(path.join(scene.dir, 'a.js'), 'utf8') === MT_SOURCE, '⑤ 被拒的写没有碰磁盘(逐字节不变)')
|
|
@@ -1018,11 +1071,11 @@ say('MT-K MCP:targets 的声明、逐项回执、汇总行与既有形状')
|
|
|
1018
1071
|
{ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {} } },
|
|
1019
1072
|
{ jsonrpc: '2.0', method: 'notifications/initialized' },
|
|
1020
1073
|
{ jsonrpc: '2.0', id: 2, method: 'tools/list' },
|
|
1021
|
-
call(3, '
|
|
1022
|
-
call(4, '
|
|
1023
|
-
call(5, '
|
|
1024
|
-
call(6, '
|
|
1025
|
-
call(7, '
|
|
1074
|
+
call(3, 'aura_basetools_read', { targets: [{ file_path: 'a.js', mode: 'digest' }, { file_path: 'b.md', mode: 'lines', lines: [1, 2] }] }),
|
|
1075
|
+
call(4, 'aura_basetools_read', { targets: [{ file_path: 'a.js', mode: 'digest' }, { file_path: 'nope.md', mode: 'full' }] }),
|
|
1076
|
+
call(5, 'aura_basetools_edit', { targets: [{ file_path: 'c.md', hunks: [{ lines: [1, 1], replace: 'ALPHA' }] }] }),
|
|
1077
|
+
call(6, 'aura_basetools_edit', { file_path: 'c.md', targets: [{ file_path: 'c.md', hunks: [{ lines: [2, 2], replace: 'B' }] }] }),
|
|
1078
|
+
call(7, 'aura_basetools_read', { file: 'a.js', mode: 'digest' }),
|
|
1026
1079
|
])
|
|
1027
1080
|
const replies = res.stdout.trim().split('\n').filter(Boolean).map((l) => JSON.parse(l))
|
|
1028
1081
|
const byId = new Map(replies.map((r) => [r.id, r]))
|
|
@@ -1030,9 +1083,9 @@ say('MT-K MCP:targets 的声明、逐项回执、汇总行与既有形状')
|
|
|
1030
1083
|
const tools = byId.get(2)?.result?.tools ?? []
|
|
1031
1084
|
const schemaOf = (name) => tools.find((t) => t.name === name)?.inputSchema ?? {}
|
|
1032
1085
|
check(
|
|
1033
|
-
schemaOf('
|
|
1086
|
+
schemaOf('aura_basetools_read').properties?.targets?.type === 'array' && schemaOf('aura_basetools_edit').properties?.targets?.type === 'array',
|
|
1034
1087
|
'K 两个工具的 inputSchema 都声明了 targets 数组',
|
|
1035
|
-
JSON.stringify({ read: schemaOf('
|
|
1088
|
+
JSON.stringify({ read: schemaOf('aura_basetools_read').properties?.targets, edit: schemaOf('aura_basetools_edit').properties?.targets }).slice(0, 160),
|
|
1036
1089
|
)
|
|
1037
1090
|
const batch = envOf(3)
|
|
1038
1091
|
check(batch.ok === true && batch.entries?.length === 2, 'K file_path 缺席 + targets 在场 ⇒ 不再 BAD_ARGS,逐项照给', JSON.stringify(batch).slice(0, 180))
|
|
@@ -1043,10 +1096,10 @@ say('MT-K MCP:targets 的声明、逐项回执、汇总行与既有形状')
|
|
|
1043
1096
|
check(partial.summary === 'targets: 2 | ok 1 | not_found 1', `K 缺一项时的汇总行 ${JSON.stringify(partial.summary)}`)
|
|
1044
1097
|
check(byId.get(4)?.result?.isError === true, `K 有失败 ⇒ isError=true(${byId.get(4)?.result?.isError})`)
|
|
1045
1098
|
const editBatch = envOf(5)
|
|
1046
|
-
check(editBatch.ok === true && editBatch.edits?.[0]?.landed === true, 'K
|
|
1099
|
+
check(editBatch.ok === true && editBatch.edits?.[0]?.landed === true, 'K aura_basetools_edit 带 targets 走引擎批入口', JSON.stringify(editBatch).slice(0, 180))
|
|
1047
1100
|
check(fs.readFileSync(path.join(scene.dir, 'c.md'), 'utf8') === 'ALPHA\nbravo\n', 'K MCP 批编辑真落盘', JSON.stringify(fs.readFileSync(path.join(scene.dir, 'c.md'), 'utf8')))
|
|
1048
1101
|
check(envOf(6).code === 'ARGS_SHAPE_EXCLUSIVE', `K file_path 与 targets 同时给 ⇒ 互斥判据(${envOf(6).code})`, String(envOf(6).error).slice(0, 140))
|
|
1049
|
-
check(envOf(7).code === '
|
|
1102
|
+
check(envOf(7).code === 'ARGS_UNKNOWN_KEY', `K 两种形状都没给仍被未知键守卫点名(${envOf(7).code})`, String(envOf(7).error).slice(0, 140))
|
|
1050
1103
|
check(res.stderr.trim() === '', 'K 没靠 stderr 兑底(不用抛出当回包)', res.stderr.trim().slice(0, 80))
|
|
1051
1104
|
}
|
|
1052
1105
|
|
|
@@ -1088,7 +1141,7 @@ say('MT-L 句柄寻址:同一份柄、同一次改动 —— 三面同 tier
|
|
|
1088
1141
|
}
|
|
1089
1142
|
const bytesOf = (p) => fs.readFileSync(p)
|
|
1090
1143
|
const tierOf = (env) => String(env?.result?.applied?.[0]?.tier ?? env?.applied?.[0]?.tier ?? '')
|
|
1091
|
-
const readHandle = (scene) => json(scene.run(['read', '--file', 'a.js', '--mode', 'lines', '--lines', '1,4'])).handle
|
|
1144
|
+
const readHandle = (scene) => json(scene.run(['basetools', 'read', '--file', 'a.js', '--mode', 'lines', '--lines', '1,4'])).handle
|
|
1092
1145
|
/** 逐字节只有第 n 行变:行数、每行的行尾字节、其余每一行都要一模一样(返回 null = 通过)。 */
|
|
1093
1146
|
const onlyLineChanged = (before, after, lineNo) => {
|
|
1094
1147
|
const a = split(String(before))
|
|
@@ -1103,20 +1156,20 @@ say('MT-L 句柄寻址:同一份柄、同一次改动 —— 三面同 tier
|
|
|
1103
1156
|
const declScene = mtScene('l-decl')
|
|
1104
1157
|
declScene.write('a.js', MT_SOURCE)
|
|
1105
1158
|
const decl = mcpReplies(
|
|
1106
|
-
declScene.mcp([...MCP_HELLO, { jsonrpc: '2.0', id: 2, method: 'tools/list' }, mcpCall(3, '
|
|
1159
|
+
declScene.mcp([...MCP_HELLO, { jsonrpc: '2.0', id: 2, method: 'tools/list' }, mcpCall(3, 'aura_basetools_read', { file_path: 'a.js', mode: 'lines', lines: [1, 4] })]),
|
|
1107
1160
|
)
|
|
1108
|
-
const editTool = decl.tools.find((t) => t.name === '
|
|
1161
|
+
const editTool = decl.tools.find((t) => t.name === 'aura_basetools_edit')
|
|
1109
1162
|
const hunkProps = Object.keys(editTool?.inputSchema?.properties?.hunks?.items?.properties ?? {})
|
|
1110
1163
|
const missing = ['handle', 'line', 'position'].filter((key) => !hunkProps.includes(key))
|
|
1111
1164
|
check(
|
|
1112
1165
|
missing.length === 0,
|
|
1113
|
-
'L① MCP 声明面:
|
|
1166
|
+
'L① MCP 声明面:aura_basetools_edit 的 hunks.items.properties 收得下 handle / line / position',
|
|
1114
1167
|
`缺 ${missing.join(',') || '(无)'};实得 ${hunkProps.join(',')}`,
|
|
1115
1168
|
)
|
|
1116
1169
|
const hunkRequired = editTool?.inputSchema?.properties?.hunks?.items?.required ?? []
|
|
1117
1170
|
check(JSON.stringify(hunkRequired) === '["replace"]', 'L① required 仍是 ["replace"](三个从属键一个都不进去)', JSON.stringify(hunkRequired))
|
|
1118
1171
|
|
|
1119
|
-
// ② MCP 真改一次:柄从上面那次 `
|
|
1172
|
+
// ② MCP 真改一次:柄从上面那次 `aura_basetools_read` 拿;正文行尾的 <h…> 与结构化 handle 必须同值
|
|
1120
1173
|
const mcpRead = decl.env(3)
|
|
1121
1174
|
const mcpHandle = mcpRead.handle
|
|
1122
1175
|
const marker = (String(mcpRead.text).match(HANDLE_RE) ?? [null])[0]
|
|
@@ -1127,7 +1180,7 @@ say('MT-L 句柄寻址:同一份柄、同一次改动 —— 三面同 tier
|
|
|
1127
1180
|
)
|
|
1128
1181
|
const mcpBefore = bytesOf(path.join(declScene.dir, 'a.js'))
|
|
1129
1182
|
const mcpEdit = mcpReplies(
|
|
1130
|
-
declScene.mcp([...MCP_HELLO, mcpCall(4, '
|
|
1183
|
+
declScene.mcp([...MCP_HELLO, mcpCall(4, 'aura_basetools_edit', { file_path: 'a.js', hunks: [{ handle: mcpHandle, line: 2, replace: LINE2 }] })]),
|
|
1131
1184
|
).env(4)
|
|
1132
1185
|
const mcpAfter = bytesOf(path.join(declScene.dir, 'a.js'))
|
|
1133
1186
|
check(
|
|
@@ -1142,7 +1195,7 @@ say('MT-L 句柄寻址:同一份柄、同一次改动 —— 三面同 tier
|
|
|
1142
1195
|
const cliScene = mtScene('l-cli')
|
|
1143
1196
|
cliScene.write('a.js', MT_SOURCE)
|
|
1144
1197
|
const cliHandle = readHandle(cliScene)
|
|
1145
|
-
const cliRun = cliScene.run(['edit', '--file', 'a.js', '--handle', String(cliHandle), '--line', '2', '--replace', LINE2])
|
|
1198
|
+
const cliRun = cliScene.run(['basetools', 'edit', '--file', 'a.js', '--handle', String(cliHandle), '--line', '2', '--replace', LINE2])
|
|
1146
1199
|
const cliEdit = json(cliRun)
|
|
1147
1200
|
check(
|
|
1148
1201
|
cliRun.status === 0 && cliEdit.ok === true,
|
|
@@ -1156,7 +1209,7 @@ say('MT-L 句柄寻址:同一份柄、同一次改动 —— 三面同 tier
|
|
|
1156
1209
|
const subScene = mtScene('l-sub')
|
|
1157
1210
|
subScene.write('a.js', MT_SOURCE)
|
|
1158
1211
|
const subBefore = bytesOf(path.join(subScene.dir, 'a.js'))
|
|
1159
|
-
const subRun = subScene.run(['edit', '--file', 'a.js', '--line', '2', '--replace', 'X'])
|
|
1212
|
+
const subRun = subScene.run(['basetools', 'edit', '--file', 'a.js', '--line', '2', '--replace', 'X'])
|
|
1160
1213
|
const subEnv = json(subRun)
|
|
1161
1214
|
check(
|
|
1162
1215
|
subRun.status === 1 && subEnv.code === 'USAGE',
|
|
@@ -1168,7 +1221,7 @@ say('MT-L 句柄寻址:同一份柄、同一次改动 —— 三面同 tier
|
|
|
1168
1221
|
// `--position after`:在区间**边界**插入,区间本身一个字节不动
|
|
1169
1222
|
const posScene = mtScene('l-pos')
|
|
1170
1223
|
posScene.write('a.js', MT_SOURCE)
|
|
1171
|
-
const posRun = posScene.run(['edit', '--file', 'a.js', '--handle', String(readHandle(posScene)), '--position', 'after', '--replace', 'POST'])
|
|
1224
|
+
const posRun = posScene.run(['basetools', 'edit', '--file', 'a.js', '--handle', String(readHandle(posScene)), '--position', 'after', '--replace', 'POST'])
|
|
1172
1225
|
const posEnv = json(posRun)
|
|
1173
1226
|
const posLines = split(bytesOf(path.join(posScene.dir, 'a.js')).toString('utf8'))
|
|
1174
1227
|
const origLines = split(MT_SOURCE)
|
|
@@ -1192,16 +1245,16 @@ say('MT-L 句柄寻址:同一份柄、同一次改动 —— 三面同 tier
|
|
|
1192
1245
|
const reset = () => fs.writeFileSync(triFile, Buffer.from(MT_SOURCE, 'utf8'))
|
|
1193
1246
|
reset()
|
|
1194
1247
|
const triCliHandle = readHandle(triScene)
|
|
1195
|
-
const triCli = json(triScene.run(['edit', '--file', 'a.js', '--handle', String(triCliHandle), '--line', '2', '--replace', LINE2]))
|
|
1248
|
+
const triCli = json(triScene.run(['basetools', 'edit', '--file', 'a.js', '--handle', String(triCliHandle), '--line', '2', '--replace', LINE2]))
|
|
1196
1249
|
const triCliDisk = bytesOf(triFile)
|
|
1197
1250
|
|
|
1198
1251
|
// 回到同一份字节:柄是「账本键 + 区间文本 + 行区间」的函数 ⇒ 重读一次的柄必须逐字符相同
|
|
1199
1252
|
reset()
|
|
1200
1253
|
const triMcpRead = mcpReplies(
|
|
1201
|
-
triScene.mcp([...MCP_HELLO, mcpCall(3, '
|
|
1254
|
+
triScene.mcp([...MCP_HELLO, mcpCall(3, 'aura_basetools_read', { file_path: 'a.js', mode: 'lines', lines: [1, 4] })]),
|
|
1202
1255
|
).env(3)
|
|
1203
1256
|
const triMcp = mcpReplies(
|
|
1204
|
-
triScene.mcp([...MCP_HELLO, mcpCall(4, '
|
|
1257
|
+
triScene.mcp([...MCP_HELLO, mcpCall(4, 'aura_basetools_edit', { file_path: 'a.js', hunks: [{ handle: triMcpRead.handle, line: 2, replace: LINE2 }] })]),
|
|
1205
1258
|
).env(4)
|
|
1206
1259
|
const triMcpDisk = bytesOf(triFile)
|
|
1207
1260
|
|
|
@@ -1265,8 +1318,8 @@ say('MT-M 修复地址:"改错了能自纠"那一半在独立面上看不看
|
|
|
1265
1318
|
.map((m) => { try { return JSON.parse(m[1]) } catch { return null } })
|
|
1266
1319
|
.filter((o) => o !== null && typeof o.handle === 'string')
|
|
1267
1320
|
|
|
1268
|
-
const notFoundRun = (scene) => scene.run(['edit', '--file', 'a.js', '--hunks', JSON.stringify([{ old: NF_QUOTE, replace: '// nope' }])])
|
|
1269
|
-
const retryRun = (scene, handle) => scene.run(['edit', '--file', 'a.js', '--handle', handle, '--lines', `${NF_REGION.start},${NF_REGION.end}`, '--replace', NF_NEW.join('\n')])
|
|
1321
|
+
const notFoundRun = (scene) => scene.run(['basetools', 'edit', '--file', 'a.js', '--hunks', JSON.stringify([{ old: NF_QUOTE, replace: '// nope' }])])
|
|
1322
|
+
const retryRun = (scene, handle) => scene.run(['basetools', 'edit', '--file', 'a.js', '--handle', handle, '--lines', `${NF_REGION.start},${NF_REGION.end}`, '--replace', NF_NEW.join('\n')])
|
|
1270
1323
|
|
|
1271
1324
|
// ── ① CLI:一次注定找不到的编辑 ──
|
|
1272
1325
|
const cliScene = mtScene('m-cli')
|
|
@@ -1312,7 +1365,7 @@ say('MT-M 修复地址:"改错了能自纠"那一半在独立面上看不看
|
|
|
1312
1365
|
}
|
|
1313
1366
|
const mcpScene = mtScene('m-mcp')
|
|
1314
1367
|
mcpScene.write('a.js', NF_SRC)
|
|
1315
|
-
const mcpFailRes = mcpScene.mcp([...MCP_HELLO_M, mcpCallM(3, '
|
|
1368
|
+
const mcpFailRes = mcpScene.mcp([...MCP_HELLO_M, mcpCallM(3, 'aura_basetools_edit', { file_path: 'a.js', hunks: [{ old: NF_QUOTE, replace: '// nope' }] })])
|
|
1316
1369
|
const mcpFail = envOfM(mcpFailRes, 3)
|
|
1317
1370
|
const mcpRetry = nearHandles(textOf(mcpFail))
|
|
1318
1371
|
check(
|
|
@@ -1322,7 +1375,7 @@ say('MT-M 修复地址:"改错了能自纠"那一半在独立面上看不看
|
|
|
1322
1375
|
)
|
|
1323
1376
|
const mcpRetryRes = mcpScene.mcp([
|
|
1324
1377
|
...MCP_HELLO_M,
|
|
1325
|
-
mcpCallM(3, '
|
|
1378
|
+
mcpCallM(3, 'aura_basetools_edit', { file_path: 'a.js', hunks: [{ handle: mcpRetry[0]?.handle, lines: [NF_REGION.start, NF_REGION.end], replace: NF_NEW.join('\n') }] }),
|
|
1326
1379
|
])
|
|
1327
1380
|
const mcpRetryEnv = envOfM(mcpRetryRes, 3)
|
|
1328
1381
|
const mcpDisk = fs.readFileSync(path.join(mcpScene.dir, 'a.js')).toString('utf8')
|
|
@@ -1374,7 +1427,7 @@ say('MT-M 修复地址:"改错了能自纠"那一半在独立面上看不看
|
|
|
1374
1427
|
const AMB_SRC = ['const head = 0', ...Array.from({ length: 10 }, (_, i) => `const pad${String(i + 1).padStart(2, '0')} = ${i + 1}`), 'const dup = value * 2', ...Array.from({ length: 7 }, (_, i) => `const pad${String(i + 11).padStart(2, '0')} = ${i + 11}`), 'const dup = value * 2', ...Array.from({ length: 2 }, (_, i) => `const pad${String(i + 18).padStart(2, '0')} = ${i + 18}`), 'const dup = value * 2', 'const tail = 0'].join('\n') + '\n'
|
|
1375
1428
|
const ambScene = mtScene('m-amb')
|
|
1376
1429
|
ambScene.write('a.js', AMB_SRC)
|
|
1377
|
-
const ambFail = json(ambScene.run(['edit', '--file', 'a.js', '--hunks', JSON.stringify([{ old: 'const dup = value * 2', replace: 'const dup = 0' }])]))
|
|
1430
|
+
const ambFail = json(ambScene.run(['basetools', 'edit', '--file', 'a.js', '--hunks', JSON.stringify([{ old: 'const dup = value * 2', replace: 'const dup = 0' }])]))
|
|
1378
1431
|
const ambRetry = nearHandles(textOf(ambFail))
|
|
1379
1432
|
// 候选在哪个键上:引擎的错误对象把细节挂在 `error` 下(DSH 壳再把它抬成外套字段)。
|
|
1380
1433
|
// CLI 收到的是 `{ok,error,code}`(`error` 是**消息字符串**)⇒ 细节只能从 `result`/外套里找;
|
|
@@ -1390,7 +1443,7 @@ say('MT-M 修复地址:"改错了能自纠"那一半在独立面上看不看
|
|
|
1390
1443
|
// 逐行都真能用:拿第三个候选的地址改一次(刻意不取第一个 "" 就近挑会蒙对)
|
|
1391
1444
|
const ambPick = ambRetry[2]
|
|
1392
1445
|
const ambBeforeDisk = fs.readFileSync(path.join(ambScene.dir, 'a.js')).toString('utf8')
|
|
1393
|
-
const ambUse = json(ambScene.run(['edit', '--file', 'a.js', '--handle', ambPick.handle, '--replace', 'const dup = 99']))
|
|
1446
|
+
const ambUse = json(ambScene.run(['basetools', 'edit', '--file', 'a.js', '--handle', ambPick.handle, '--replace', 'const dup = 99']))
|
|
1394
1447
|
const ambAfterDisk = fs.readFileSync(path.join(ambScene.dir, 'a.js')).toString('utf8')
|
|
1395
1448
|
check(
|
|
1396
1449
|
ambUse.ok === true && ambAfterDisk !== ambBeforeDisk && ambAfterDisk.split('const dup = 99').length === 2,
|
|
@@ -1489,11 +1542,11 @@ say('O 写事务:journal / undo / 多目标回滚')
|
|
|
1489
1542
|
const txFile = path.join(tmp, 'tx-single.txt')
|
|
1490
1543
|
const txOriginal = 'one\ntwo\n'
|
|
1491
1544
|
fs.writeFileSync(txFile, txOriginal)
|
|
1492
|
-
const txEdit = json(run(['edit', '--file', txFile, '--lines', '1', '--replace', 'ONE', '--transaction']))
|
|
1545
|
+
const txEdit = json(run(['basetools', 'edit', '--file', txFile, '--lines', '1', '--replace', 'ONE', '--transaction']))
|
|
1493
1546
|
check(txEdit.ok === true && typeof txEdit.result?.transaction?.id === 'string', 'O1 单目标编辑返回 transaction id', JSON.stringify(txEdit.result?.transaction ?? txEdit.code))
|
|
1494
1547
|
check(fs.readFileSync(txFile, 'utf8') === 'ONE\ntwo\n', 'O1 编辑已落盘')
|
|
1495
1548
|
const txId = txEdit.result?.transaction?.id
|
|
1496
|
-
const txUndo = json(run(['undo', '--id', String(txId)]))
|
|
1549
|
+
const txUndo = json(run(['basetools', 'undo', '--id', String(txId)]))
|
|
1497
1550
|
check(txUndo.ok === true && txUndo.status === 'undone', 'O2 undo 成功', JSON.stringify(txUndo))
|
|
1498
1551
|
check(fs.readFileSync(txFile, 'utf8') === txOriginal, 'O2 undo 恢复原始字节')
|
|
1499
1552
|
|
|
@@ -1505,25 +1558,25 @@ say('O 写事务:journal / undo / 多目标回滚')
|
|
|
1505
1558
|
{ file_path: a, hunks: [{ lines: [1, 1], replace: 'ALPHA' }] },
|
|
1506
1559
|
{ file_path: b, hunks: [{ lines: [1, 1], replace: 'BETA' }] },
|
|
1507
1560
|
]
|
|
1508
|
-
const multi = json(run(['edit', '--targets', JSON.stringify(targets), '--transaction']))
|
|
1561
|
+
const multi = json(run(['basetools', 'edit', '--targets', JSON.stringify(targets), '--transaction']))
|
|
1509
1562
|
check(multi.ok === true && typeof multi.transaction?.id === 'string', 'O3 多目标编辑返回 transaction id', JSON.stringify(multi.transaction ?? multi.code))
|
|
1510
1563
|
check(fs.readFileSync(a, 'utf8') === 'ALPHA\n' && fs.readFileSync(b, 'utf8') === 'BETA\n', 'O3 两个文件都已落盘')
|
|
1511
|
-
const multiUndo = json(run(['undo', '--id', String(multi.transaction?.id)]))
|
|
1564
|
+
const multiUndo = json(run(['basetools', 'undo', '--id', String(multi.transaction?.id)]))
|
|
1512
1565
|
check(multiUndo.ok === true && multiUndo.restored === 2, 'O4 多目标 undo 恢复两个文件', JSON.stringify(multiUndo))
|
|
1513
1566
|
check(fs.readFileSync(a, 'utf8') === 'alpha\n' && fs.readFileSync(b, 'utf8') === 'beta\n', 'O4 两个文件都回到原始字节')
|
|
1514
1567
|
|
|
1515
|
-
const list = json(run(['transactions']))
|
|
1568
|
+
const list = json(run(['basetools', 'transactions']))
|
|
1516
1569
|
check(list.ok === true && Array.isArray(list.transactions) && list.transactions.length > 0, 'O5 transactions 列出当前工作区事务')
|
|
1517
1570
|
|
|
1518
1571
|
// 事务后又被改过 ⇒ 默认拒绝 undo,force 才覆盖
|
|
1519
1572
|
const guardFile = path.join(tmp, 'tx-guard.txt')
|
|
1520
1573
|
fs.writeFileSync(guardFile, 'v0\n')
|
|
1521
|
-
const guardEdit = json(run(['edit', '--file', guardFile, '--lines', '1', '--replace', 'v1', '--transaction']))
|
|
1574
|
+
const guardEdit = json(run(['basetools', 'edit', '--file', guardFile, '--lines', '1', '--replace', 'v1', '--transaction']))
|
|
1522
1575
|
const guardId = guardEdit.result?.transaction?.id
|
|
1523
1576
|
fs.writeFileSync(guardFile, 'external\n')
|
|
1524
|
-
const blocked = json(run(['undo', '--id', String(guardId)]))
|
|
1577
|
+
const blocked = json(run(['basetools', 'undo', '--id', String(guardId)]))
|
|
1525
1578
|
check(blocked.ok === false && blocked.code === 'TRANSACTION_STALE', 'O6 事务后文件被外部改过 ⇒ undo 默认拒绝', JSON.stringify({ code: blocked.code, error: blocked.error }))
|
|
1526
|
-
const forced = json(run(['undo', '--id', String(guardId), '--force']))
|
|
1579
|
+
const forced = json(run(['basetools', 'undo', '--id', String(guardId), '--force']))
|
|
1527
1580
|
check(forced.ok === true && fs.readFileSync(guardFile, 'utf8') === 'v0\n', 'O6 force 后仍可恢复')
|
|
1528
1581
|
|
|
1529
1582
|
// O7 多目标:一个成功、一个写失败 ⇒ 成功那个也必须回滚
|
|
@@ -1541,10 +1594,31 @@ say('O 写事务:journal / undo / 多目标回滚')
|
|
|
1541
1594
|
{ file_path: okFile, hunks: [{ lines: [1, 1], replace: 'ALPHA' }] },
|
|
1542
1595
|
{ file_path: hardFile, hunks: [{ lines: [1, 1], replace: 'BETA' }] },
|
|
1543
1596
|
]
|
|
1544
|
-
const rollback = json(run(['edit', '--targets', JSON.stringify(rollbackTargets), '--transaction']))
|
|
1597
|
+
const rollback = json(run(['basetools', 'edit', '--targets', JSON.stringify(rollbackTargets), '--transaction']))
|
|
1545
1598
|
check(rollback.ok === false && rollback.transaction?.status === 'rolled_back', 'O7 多目标部分失败 ⇒ transaction rolled_back', JSON.stringify({ ok: rollback.ok, tx: rollback.transaction, rollback: rollback.rollback }))
|
|
1546
1599
|
check(rollback.rollback?.restored === 1 && fs.readFileSync(okFile, 'utf8') === 'alpha\n', 'O7 已落地的那个文件被恢复')
|
|
1547
1600
|
check(fs.readFileSync(hardBase, 'utf8') === 'beta\n' && fs.readFileSync(hardFile, 'utf8') === 'beta\n', 'O7 硬链接目标未被改动')
|
|
1601
|
+
// 回执必须等于**回滚后的最终磁盘状态**,不能继续报 landed/applied。
|
|
1602
|
+
check(rollback.applied === 0 && rollback.landed_paths?.length === 0 && rollback.unchanged === true, 'O7 回滚后 applied=0 / landed_paths=[] / unchanged=true', JSON.stringify({ applied: rollback.applied, landed_paths: rollback.landed_paths, unchanged: rollback.unchanged }))
|
|
1603
|
+
check(rollback.rolled_back === true && rollback.edits?.filter((e) => e.outcome === 'rolled_back').every((e) => e.landed === false), 'O7 逐项 outcome=rolled_back 且 landed=false', JSON.stringify(rollback.edits?.map((e) => ({ outcome: e.outcome, landed: e.landed }))))
|
|
1604
|
+
check(!/applied [1-9]|landed [1-9]/.test(rollback.summary ?? ''), 'O7 汇总行不再出现 applied N / landed N', rollback.summary)
|
|
1605
|
+
|
|
1606
|
+
// 不带 --transaction 也必须能看出“已回滚”:回执事实不依赖是否索要事务 id。
|
|
1607
|
+
// 用一组**新文件**:上一轮回滚后观测账本里还留着“写下去的那版”,同一文件立刻重试会先被
|
|
1608
|
+
// 区域闸拦下(那是另一条保护,别把它混进回执断言)。
|
|
1609
|
+
const okFile2 = path.join(tmp, 'tx-rollback-ok2.txt')
|
|
1610
|
+
const hardBase2 = path.join(tmp, 'tx-rollback-hard-base2.txt')
|
|
1611
|
+
const hardFile2 = path.join(tmp, 'tx-rollback-hard2.txt')
|
|
1612
|
+
fs.writeFileSync(okFile2, 'alpha\n')
|
|
1613
|
+
fs.writeFileSync(hardBase2, 'beta\n')
|
|
1614
|
+
fs.linkSync(hardBase2, hardFile2)
|
|
1615
|
+
const rollbackTargets2 = [
|
|
1616
|
+
{ file_path: okFile2, hunks: [{ lines: [1, 1], replace: 'ALPHA' }] },
|
|
1617
|
+
{ file_path: hardFile2, hunks: [{ lines: [1, 1], replace: 'BETA' }] },
|
|
1618
|
+
]
|
|
1619
|
+
const rollbackNoTx = json(run(['basetools', 'edit', '--targets', JSON.stringify(rollbackTargets2)]))
|
|
1620
|
+
check(rollbackNoTx.ok === false && rollbackNoTx.rollback?.status === 'rolled_back' && rollbackNoTx.rolled_back === true, 'O7 include_transaction=false 仍返回 rollback/rolled_back', JSON.stringify({ rollback: rollbackNoTx.rollback, rolled_back: rollbackNoTx.rolled_back }))
|
|
1621
|
+
check(rollbackNoTx.applied === 0 && rollbackNoTx.landed_paths?.length === 0 && fs.readFileSync(okFile2, 'utf8') === 'alpha\n', 'O7 不索要事务 id 时回执仍与最终磁盘一致', JSON.stringify({ applied: rollbackNoTx.applied, landed_paths: rollbackNoTx.landed_paths }))
|
|
1548
1622
|
}
|
|
1549
1623
|
|
|
1550
1624
|
// O8 锁:同一工作区已有写事务时,第二个编辑必须超时拒绝
|
|
@@ -1554,7 +1628,7 @@ say('O 写事务:journal / undo / 多目标回滚')
|
|
|
1554
1628
|
process.env.AURA_CACHE_DIR = cache
|
|
1555
1629
|
try {
|
|
1556
1630
|
await withWorkspaceLock(tmp, async () => {
|
|
1557
|
-
const res = spawnSync(process.execPath, [bin, 'edit', '--file', lockFile, '--lines', '1', '--replace', 'LOCK'], {
|
|
1631
|
+
const res = spawnSync(process.execPath, [bin, 'basetools', 'edit', '--file', lockFile, '--lines', '1', '--replace', 'LOCK'], {
|
|
1558
1632
|
cwd: tmp,
|
|
1559
1633
|
encoding: 'utf8',
|
|
1560
1634
|
env: { ...env, AURA_LOCK_TIMEOUT_MS: '200' },
|