aura-basetools 0.13.3 → 0.15.0
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 +14 -16
- package/bin/aura-mcp.mjs +38 -41
- package/bin/aura.mjs +16 -37
- package/lib/ops.mjs +106 -37
- package/lib/transaction-store.mjs +3 -82
- package/locator-lab/args.mjs +3 -3
- package/locator-lab/locator-api.mjs +10 -172
- package/locator-lab/write-path.mjs +14 -73
- package/package.json +1 -1
- package/test/standalone-test.mjs +6 -64
- package/test/write-test.mjs +3 -18
|
@@ -2577,35 +2577,6 @@ function locateByHandle(lines, hunk, parsed, ctx) {
|
|
|
2577
2577
|
})
|
|
2578
2578
|
}
|
|
2579
2579
|
|
|
2580
|
-
/**
|
|
2581
|
-
* `intent` 与哪些字段**不能同时出现**(T16)。
|
|
2582
|
-
*
|
|
2583
|
-
* `intent` 是**自成一体的一层地址**:它由引擎决定落到哪个精确区间。若同时给出任何别的地址,
|
|
2584
|
-
* 谁说了算就没有正确答案——挑一个执行就是一次静默换语义(调用方写的两个地址指向不同地方时,
|
|
2585
|
-
* 改错的那次不会有任何提示)。所以参数面直接拒绝,而不是挑一个。
|
|
2586
|
-
*
|
|
2587
|
-
* 名单与 {@link planHunks} 的地址判据同源:`target` / `lines`(含别名 `range`)/ `start_line` /
|
|
2588
|
-
* `end_line` / `line_cue` / `anchor` / `old` / `context` / `handle`。
|
|
2589
|
-
* `near` / `expect` / `replace` **不在**名单里:前两个是修饰,第三个是动作。
|
|
2590
|
-
*
|
|
2591
|
-
* 「在场」的判据逐字段照搬它自己那一层的:字符串看非空(`anchor` 看长度、其余看 trim),
|
|
2592
|
-
* 坐标看 `!== undefined && !== null`。判据写宽或写窄都会让某个形状在参数面与引擎里得到两种结论。
|
|
2593
|
-
*
|
|
2594
|
-
* @returns 冲突的字段名(按上面的固定顺序);没有冲突时为空数组
|
|
2595
|
-
*/
|
|
2596
|
-
function intentAddressConflicts(hunk) {
|
|
2597
|
-
const clashes = []
|
|
2598
|
-
if (typeof hunk.target === 'string' && hunk.target.trim().length > 0) clashes.push('target')
|
|
2599
|
-
for (const key of ['lines', 'range', 'start_line', 'end_line']) {
|
|
2600
|
-
if (hunk[key] !== undefined && hunk[key] !== null) clashes.push(key)
|
|
2601
|
-
}
|
|
2602
|
-
if (typeof hunk.line_cue === 'string' && hunk.line_cue.trim().length > 0) clashes.push('line_cue')
|
|
2603
|
-
if (typeof hunk.anchor === 'string' && hunk.anchor.length > 0) clashes.push('anchor')
|
|
2604
|
-
if (oldAddressOf(hunk) !== null) clashes.push('old')
|
|
2605
|
-
if (typeof hunk.context === 'string' && hunk.context.trim().length > 0) clashes.push('context')
|
|
2606
|
-
if (typeof hunk.handle === 'string' && hunk.handle.trim().length > 0) clashes.push('handle')
|
|
2607
|
-
return clashes
|
|
2608
|
-
}
|
|
2609
2580
|
|
|
2610
2581
|
/**
|
|
2611
2582
|
* 规划所有 hunk:逐个定位 → expect 核对 → 重叠检查 → 倒序拼接。
|
|
@@ -2661,7 +2632,7 @@ export function planHunks(content, hunks, filePath = null, options = {}) {
|
|
|
2661
2632
|
*
|
|
2662
2633
|
* 它不是第六种地址,而是 `old` 的一种作用范围;因此只认 `old` + 动作字段(`replace` /
|
|
2663
2634
|
* `expect` / `eol`)。把 `target`/`lines`/`context`/`near` 混进来意味着"既想全改又想收敛到一处",
|
|
2664
|
-
* 没有正确答案 ——
|
|
2635
|
+
* 没有正确答案 —— 参数面直接拒绝,与地址字段互斥同一条理由。
|
|
2665
2636
|
*/
|
|
2666
2637
|
const replaceAll = hunk.replace_all
|
|
2667
2638
|
if (replaceAll !== undefined && typeof replaceAll !== 'boolean') {
|
|
@@ -2673,7 +2644,7 @@ export function planHunks(content, hunks, filePath = null, options = {}) {
|
|
|
2673
2644
|
if (replaceAll === true) {
|
|
2674
2645
|
const clashes = []
|
|
2675
2646
|
if (hasTarget) clashes.push('target')
|
|
2676
|
-
|
|
2647
|
+
|
|
2677
2648
|
for (const key of ['lines', 'range', 'start_line', 'end_line']) {
|
|
2678
2649
|
if (hunk[key] !== undefined && hunk[key] !== null) clashes.push(key)
|
|
2679
2650
|
}
|
|
@@ -2708,23 +2679,6 @@ export function planHunks(content, hunks, filePath = null, options = {}) {
|
|
|
2708
2679
|
)
|
|
2709
2680
|
}
|
|
2710
2681
|
}
|
|
2711
|
-
/**
|
|
2712
|
-
* 意图地址(T16):`intent` 是**自成一体**的一层地址,与其余地址字段互斥。
|
|
2713
|
-
*
|
|
2714
|
-
* 冲突在**定位之前**就拒(参数面没成形,与 `PATCH_NO_ADDRESS` 同族),而不是在引擎里挑一个:
|
|
2715
|
-
* 调用方写了两个地址说明它自己也没定,工具替它选一次就是一次看不见的换语义。
|
|
2716
|
-
* `near` / `expect` / `replace` 不在此列——前两个是修饰,第三个是动作。
|
|
2717
|
-
*/
|
|
2718
|
-
const hasIntent = typeof hunk.intent === 'string' && hunk.intent.trim().length > 0
|
|
2719
|
-
if (hasIntent) {
|
|
2720
|
-
const clashed = intentAddressConflicts(hunk)
|
|
2721
|
-
if (clashed.length > 0) {
|
|
2722
|
-
throw new PatchError(
|
|
2723
|
-
`\`intent\` is a complete address by itself — it cannot be combined with ${clashed.join(', ')} (near / expect / replace are fine); drop either intent or those fields — nothing was written`,
|
|
2724
|
-
'PATCH_INTENT_CONFLICT',
|
|
2725
|
-
)
|
|
2726
|
-
}
|
|
2727
|
-
}
|
|
2728
2682
|
/** 句柄的形状在这里判(不对就抛错);`{handle, old}` 那一支的 `old` 仍然要命中区间内部。 */
|
|
2729
2683
|
const parsedHandle = handleAddressOf(hunk)
|
|
2730
2684
|
/**
|
|
@@ -2752,9 +2706,9 @@ export function planHunks(content, hunks, filePath = null, options = {}) {
|
|
|
2752
2706
|
'PATCH_NO_ADDRESS',
|
|
2753
2707
|
)
|
|
2754
2708
|
}
|
|
2755
|
-
if (oldText === null && parsedHandle === null && !hasTarget && range === undefined && !hasCue && !hasAnchor
|
|
2709
|
+
if (oldText === null && parsedHandle === null && !hasTarget && range === undefined && !hasCue && !hasAnchor) {
|
|
2756
2710
|
throw new PatchError(
|
|
2757
|
-
gaveLineAddress(hunk) ? LINE_ADDRESS_HINT : 'each hunk needs one of: old,
|
|
2711
|
+
gaveLineAddress(hunk) ? LINE_ADDRESS_HINT : 'each hunk needs one of: old, target, lines, line_cue, anchor',
|
|
2758
2712
|
'PATCH_NO_ADDRESS',
|
|
2759
2713
|
)
|
|
2760
2714
|
}
|
|
@@ -2776,29 +2730,16 @@ export function planHunks(content, hunks, filePath = null, options = {}) {
|
|
|
2776
2730
|
? locateAllByOld(lines, hunk, oldText, filePath === null ? 'this file' : filePath, handleCtx)
|
|
2777
2731
|
: [locateByOld(lines, hunk, oldText, filePath === null ? 'this file' : filePath, handleCtx)]
|
|
2778
2732
|
} else {
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
? {
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
...(filePath === null ? {} : { filePath }),
|
|
2790
|
-
...(options.allowLoneCr === true ? { allowLoneCr: true } : {}),
|
|
2791
|
-
}
|
|
2792
|
-
: {
|
|
2793
|
-
...(hasTarget ? { target: hunk.target } : {}),
|
|
2794
|
-
...(range === undefined ? {} : { lines: range }),
|
|
2795
|
-
...(hunk.line_cue === undefined ? {} : { lineCue: hunk.line_cue }),
|
|
2796
|
-
...(hasTarget || range !== undefined || hunk.line_cue !== undefined ? {} : { anchor: hunk.anchor }),
|
|
2797
|
-
...(hunk.near === undefined ? {} : { near: hunk.near }),
|
|
2798
|
-
...(hunk.expect === undefined || hunk.expect === null ? {} : { assert: hunk.expect }),
|
|
2799
|
-
...(filePath === null ? {} : { filePath }),
|
|
2800
|
-
...(options.allowLoneCr === true ? { allowLoneCr: true } : {}),
|
|
2801
|
-
}
|
|
2733
|
+
const request = {
|
|
2734
|
+
...(hasTarget ? { target: hunk.target } : {}),
|
|
2735
|
+
...(range === undefined ? {} : { lines: range }),
|
|
2736
|
+
...(hunk.line_cue === undefined ? {} : { lineCue: hunk.line_cue }),
|
|
2737
|
+
...(hasTarget || range !== undefined || hunk.line_cue !== undefined ? {} : { anchor: hunk.anchor }),
|
|
2738
|
+
...(hunk.near === undefined ? {} : { near: hunk.near }),
|
|
2739
|
+
...(hunk.expect === undefined || hunk.expect === null ? {} : { assert: hunk.expect }),
|
|
2740
|
+
...(filePath === null ? {} : { filePath }),
|
|
2741
|
+
...(options.allowLoneCr === true ? { allowLoneCr: true } : {}),
|
|
2742
|
+
}
|
|
2802
2743
|
const found = locate(content, request)
|
|
2803
2744
|
if (!found.ok) {
|
|
2804
2745
|
throw new PatchError(found.message, found.code, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aura-basetools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Byte-exact read, edit and write tools for coding agents. Standalone CLI and MCP server; no DSH required. 面向 coding agent 的逐字节读写工具;独立 CLI / MCP,可选 DSH 适配器。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/test/standalone-test.mjs
CHANGED
|
@@ -179,7 +179,7 @@ say('B2 能力清单:capabilities 从引擎常量派生')
|
|
|
179
179
|
check(caps.ok === true && caps.langsCount === caps.langs?.length, `langsCount=${caps.langsCount}`, JSON.stringify(caps.langs?.length))
|
|
180
180
|
check(caps.langsCount === 65, '语言表 65 种(与 LANGS 同源)', String(caps.langsCount))
|
|
181
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('
|
|
182
|
+
check(editTool?.addressKinds?.includes('target') && editTool.addressKinds.includes('handle'), '编辑地址档含 target/handle', JSON.stringify(editTool?.addressKinds))
|
|
183
183
|
check(editTool?.options?.includes('replace_all'), '编辑选项含 replace_all', JSON.stringify(editTool?.options))
|
|
184
184
|
check(editTool?.options?.includes('require_observation'), '编辑选项含 require_observation', JSON.stringify(editTool?.options))
|
|
185
185
|
check(caps.limits?.multiTargetsMax === 32 && caps.limits?.defaultPruneBudget === 8192 && caps.limits?.contentBudget === 7492, '限制与常量一致', JSON.stringify(caps.limits))
|
|
@@ -385,7 +385,7 @@ say('F MCP:initialize / tools/list / tools/call')
|
|
|
385
385
|
})()
|
|
386
386
|
if (manifest !== null) check(byId.get(1)?.result?.serverInfo?.version === manifest.version, `serverInfo.version 跟包一致(${manifest.version})`, String(byId.get(1)?.result?.serverInfo?.version))
|
|
387
387
|
const tools = byId.get(2)?.result?.tools ?? []
|
|
388
|
-
check(tools.length ===
|
|
388
|
+
check(tools.length === 3 && 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/list 三个工具(${tools.map((t) => t.name).join(',')}`)
|
|
389
389
|
const readEnv = JSON.parse(byId.get(3)?.result?.content?.[0]?.text ?? '{}')
|
|
390
390
|
check(readEnv.ok === true && Array.isArray(readEnv.symbols ?? []), 'aura_basetools_read 经 MCP 可用', JSON.stringify(readEnv).slice(0, 120))
|
|
391
391
|
const editEnv = JSON.parse(byId.get(4)?.result?.content?.[0]?.text ?? '{}')
|
|
@@ -1537,49 +1537,9 @@ say('N provider:mode / symlink / hardlink / range / 上限 / cache')
|
|
|
1537
1537
|
}
|
|
1538
1538
|
}
|
|
1539
1539
|
|
|
1540
|
-
say('O
|
|
1540
|
+
say('O 写失败回滚与锁(0.14.0 已移除公开 undo/transactions)')
|
|
1541
1541
|
{
|
|
1542
|
-
|
|
1543
|
-
const txOriginal = 'one\ntwo\n'
|
|
1544
|
-
fs.writeFileSync(txFile, txOriginal)
|
|
1545
|
-
const txEdit = json(run(['basetools', 'edit', '--file', txFile, '--lines', '1', '--replace', 'ONE', '--transaction']))
|
|
1546
|
-
check(txEdit.ok === true && typeof txEdit.result?.transaction?.id === 'string', 'O1 单目标编辑返回 transaction id', JSON.stringify(txEdit.result?.transaction ?? txEdit.code))
|
|
1547
|
-
check(fs.readFileSync(txFile, 'utf8') === 'ONE\ntwo\n', 'O1 编辑已落盘')
|
|
1548
|
-
const txId = txEdit.result?.transaction?.id
|
|
1549
|
-
const txUndo = json(run(['basetools', 'undo', '--id', String(txId)]))
|
|
1550
|
-
check(txUndo.ok === true && txUndo.status === 'undone', 'O2 undo 成功', JSON.stringify(txUndo))
|
|
1551
|
-
check(fs.readFileSync(txFile, 'utf8') === txOriginal, 'O2 undo 恢复原始字节')
|
|
1552
|
-
|
|
1553
|
-
const a = path.join(tmp, 'tx-a.txt')
|
|
1554
|
-
const b = path.join(tmp, 'tx-b.txt')
|
|
1555
|
-
fs.writeFileSync(a, 'alpha\n')
|
|
1556
|
-
fs.writeFileSync(b, 'beta\n')
|
|
1557
|
-
const targets = [
|
|
1558
|
-
{ file_path: a, hunks: [{ lines: [1, 1], replace: 'ALPHA' }] },
|
|
1559
|
-
{ file_path: b, hunks: [{ lines: [1, 1], replace: 'BETA' }] },
|
|
1560
|
-
]
|
|
1561
|
-
const multi = json(run(['basetools', 'edit', '--targets', JSON.stringify(targets), '--transaction']))
|
|
1562
|
-
check(multi.ok === true && typeof multi.transaction?.id === 'string', 'O3 多目标编辑返回 transaction id', JSON.stringify(multi.transaction ?? multi.code))
|
|
1563
|
-
check(fs.readFileSync(a, 'utf8') === 'ALPHA\n' && fs.readFileSync(b, 'utf8') === 'BETA\n', 'O3 两个文件都已落盘')
|
|
1564
|
-
const multiUndo = json(run(['basetools', 'undo', '--id', String(multi.transaction?.id)]))
|
|
1565
|
-
check(multiUndo.ok === true && multiUndo.restored === 2, 'O4 多目标 undo 恢复两个文件', JSON.stringify(multiUndo))
|
|
1566
|
-
check(fs.readFileSync(a, 'utf8') === 'alpha\n' && fs.readFileSync(b, 'utf8') === 'beta\n', 'O4 两个文件都回到原始字节')
|
|
1567
|
-
|
|
1568
|
-
const list = json(run(['basetools', 'transactions']))
|
|
1569
|
-
check(list.ok === true && Array.isArray(list.transactions) && list.transactions.length > 0, 'O5 transactions 列出当前工作区事务')
|
|
1570
|
-
|
|
1571
|
-
// 事务后又被改过 ⇒ 默认拒绝 undo,force 才覆盖
|
|
1572
|
-
const guardFile = path.join(tmp, 'tx-guard.txt')
|
|
1573
|
-
fs.writeFileSync(guardFile, 'v0\n')
|
|
1574
|
-
const guardEdit = json(run(['basetools', 'edit', '--file', guardFile, '--lines', '1', '--replace', 'v1', '--transaction']))
|
|
1575
|
-
const guardId = guardEdit.result?.transaction?.id
|
|
1576
|
-
fs.writeFileSync(guardFile, 'external\n')
|
|
1577
|
-
const blocked = json(run(['basetools', 'undo', '--id', String(guardId)]))
|
|
1578
|
-
check(blocked.ok === false && blocked.code === 'TRANSACTION_STALE', 'O6 事务后文件被外部改过 ⇒ undo 默认拒绝', JSON.stringify({ code: blocked.code, error: blocked.error }))
|
|
1579
|
-
const forced = json(run(['basetools', 'undo', '--id', String(guardId), '--force']))
|
|
1580
|
-
check(forced.ok === true && fs.readFileSync(guardFile, 'utf8') === 'v0\n', 'O6 force 后仍可恢复')
|
|
1581
|
-
|
|
1582
|
-
// O7 多目标:一个成功、一个写失败 ⇒ 成功那个也必须回滚
|
|
1542
|
+
// O7 多目标:一个成功、一个写失败 ⇒ 成功那个也必须回滚(内部事务日志仍保留)
|
|
1583
1543
|
const okFile = path.join(tmp, 'tx-rollback-ok.txt')
|
|
1584
1544
|
const hardBase = path.join(tmp, 'tx-rollback-hard-base.txt')
|
|
1585
1545
|
const hardFile = path.join(tmp, 'tx-rollback-hard.txt')
|
|
@@ -1594,31 +1554,13 @@ say('O 写事务:journal / undo / 多目标回滚')
|
|
|
1594
1554
|
{ file_path: okFile, hunks: [{ lines: [1, 1], replace: 'ALPHA' }] },
|
|
1595
1555
|
{ file_path: hardFile, hunks: [{ lines: [1, 1], replace: 'BETA' }] },
|
|
1596
1556
|
]
|
|
1597
|
-
const rollback = json(run(['basetools', 'edit', '--targets', JSON.stringify(rollbackTargets)
|
|
1598
|
-
check(rollback.ok === false && rollback.
|
|
1557
|
+
const rollback = json(run(['basetools', 'edit', '--targets', JSON.stringify(rollbackTargets)]))
|
|
1558
|
+
check(rollback.ok === false && rollback.rollback?.status === 'rolled_back' && rollback.rolled_back === true, 'O7 多目标部分失败 ⇒ 自动回滚', JSON.stringify({ ok: rollback.ok, rollback: rollback.rollback }))
|
|
1599
1559
|
check(rollback.rollback?.restored === 1 && fs.readFileSync(okFile, 'utf8') === 'alpha\n', 'O7 已落地的那个文件被恢复')
|
|
1600
1560
|
check(fs.readFileSync(hardBase, 'utf8') === 'beta\n' && fs.readFileSync(hardFile, 'utf8') === 'beta\n', 'O7 硬链接目标未被改动')
|
|
1601
|
-
// 回执必须等于**回滚后的最终磁盘状态**,不能继续报 landed/applied。
|
|
1602
1561
|
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
1562
|
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
1563
|
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 }))
|
|
1622
1564
|
}
|
|
1623
1565
|
|
|
1624
1566
|
// O8 锁:同一工作区已有写事务时,第二个编辑必须超时拒绝
|
package/test/write-test.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 覆盖:
|
|
5
5
|
* A 新建:真正建盘、回执 operation=create、dry-run 不建盘、事务可撤销(撤销后文件消失)
|
|
6
6
|
* B 覆写:整份替换、BOM 与文件既有行尾保留、有无尾换行精确、no-overwrite、dry-run、
|
|
7
|
-
*
|
|
7
|
+
* `--content @file`(0.14.0 移除公开事务/undo)
|
|
8
8
|
* C MCP:aura_basetools_write 出现在 tools/list,create/overwrite/no_overwrite/dry_run/坏参数都走同一引擎
|
|
9
9
|
* D 能力清单:commands 与 tools 都包含 write
|
|
10
10
|
*
|
|
@@ -55,7 +55,7 @@ process.on('exit', () => {
|
|
|
55
55
|
})
|
|
56
56
|
|
|
57
57
|
// ── A 新建 ────────────────────────────────────────────────────────────────
|
|
58
|
-
say('A 新建:整份建盘,dry-run
|
|
58
|
+
say('A 新建:整份建盘,dry-run 不建盘')
|
|
59
59
|
{
|
|
60
60
|
const created = json(run(['basetools', 'write', '--file', 'created.txt', '--content', 'alpha\nbeta\n']))
|
|
61
61
|
check(created.ok === true && created.result?.operation === 'create', 'A1 新建回执 operation=create', JSON.stringify(created).slice(0, 160))
|
|
@@ -66,13 +66,6 @@ say('A 新建:整份建盘,dry-run 不建盘,事务可撤销')
|
|
|
66
66
|
check(dry.ok === true && dry.result?.operation === 'create' && dry.result?.dryRun === true && dry.result?.bytes === 1, 'A4 新建 dry-run 回执给 create/bytes,但不落盘', JSON.stringify(dry).slice(0, 160))
|
|
67
67
|
check(fs.existsSync(file('dry-new.txt')) === false, 'A5 新建 dry-run 之后文件仍不存在')
|
|
68
68
|
|
|
69
|
-
const tx = json(run(['basetools', 'write', '--file', 'tx-created.txt', '--content', 'gone\n', '--transaction']))
|
|
70
|
-
const txId = tx.result?.transaction?.id
|
|
71
|
-
check(tx.ok === true && typeof txId === 'string' && tx.result.transaction.status === 'committed', 'A6 --transaction 返回 committed 事务', JSON.stringify(tx.result?.transaction))
|
|
72
|
-
const undone = json(run(['basetools', 'undo', '--id', txId]))
|
|
73
|
-
check(undone.ok === true && undone.status === 'undone', 'A7 undo 成功', JSON.stringify(undone).slice(0, 160))
|
|
74
|
-
check(fs.existsSync(file('tx-created.txt')) === false, 'A8 撤销“新建”后文件消失(不留下空文件)')
|
|
75
|
-
|
|
76
69
|
const missingFile = json(run(['basetools', 'write', '--content', 'x']))
|
|
77
70
|
const missingContent = json(run(['basetools', 'write', '--file', 'x.txt']))
|
|
78
71
|
check(missingFile.ok === false && missingFile.code === 'USAGE', 'A9 缺 --file ⇒ USAGE', JSON.stringify(missingFile).slice(0, 140))
|
|
@@ -119,14 +112,6 @@ say('B 覆写:整份替换,BOM / 既有行尾 / 尾换行精确')
|
|
|
119
112
|
const newNoOverwrite = json(run(['basetools', 'write', '--file', 'brand-new.txt', '--content', 'ok\n', '--no-overwrite']))
|
|
120
113
|
check(newNoOverwrite.ok === true, 'B11 no-overwrite 对不存在的文件照常创建')
|
|
121
114
|
|
|
122
|
-
fs.writeFileSync(file('tx-restore.txt'), Buffer.concat([BOM, Buffer.from('orig\r\nbytes\r\n', 'utf8')]))
|
|
123
|
-
prime('tx-restore.txt')
|
|
124
|
-
const original = fs.readFileSync(file('tx-restore.txt'))
|
|
125
|
-
const tx = json(run(['basetools', 'write', '--file', 'tx-restore.txt', '--content', 'changed\n', '--transaction']))
|
|
126
|
-
check(!fs.readFileSync(file('tx-restore.txt')).equals(original), 'B12 覆写真的改了盘')
|
|
127
|
-
const undone = json(run(['basetools', 'undo', '--id', tx.result?.transaction?.id]))
|
|
128
|
-
check(undone.ok === true && fs.readFileSync(file('tx-restore.txt')).equals(original), 'B13 undo 逐字节恢复覆写前的原文(含 BOM/CRLF)')
|
|
129
|
-
|
|
130
115
|
fs.writeFileSync(file('content-source.txt'), 'from @file\nsecond line\n')
|
|
131
116
|
const atFile = json(run(['basetools', 'write', '--file', 'from-at.txt', '--content', '@' + file('content-source.txt')]))
|
|
132
117
|
check(atFile.ok === true && fs.readFileSync(file('from-at.txt'), 'utf8') === fs.readFileSync(file('content-source.txt'), 'utf8'), 'B14 --content @file 读入整份内容')
|
|
@@ -225,7 +210,7 @@ say('C MCP:aura_basetools_write 声明面与调用面')
|
|
|
225
210
|
|
|
226
211
|
const tools = byId.get(2)?.result?.tools ?? []
|
|
227
212
|
const writeTool = tools.find((tool) => tool.name === 'aura_basetools_write')
|
|
228
|
-
check(tools.length ===
|
|
213
|
+
check(tools.length === 3 && writeTool !== undefined, `C1 tools/list 三个工具且含 aura_basetools_write(${tools.map((tool) => tool.name).join(',')})`)
|
|
229
214
|
check(JSON.stringify(writeTool?.inputSchema?.required) === JSON.stringify(['file_path', 'content']), 'C2 aura_basetools_write schema 必填 file_path + content', JSON.stringify(writeTool?.inputSchema?.required))
|
|
230
215
|
check(envOf(3).ok === true && envOf(3).result?.operation === 'create', 'C3 经 MCP 新建成功', JSON.stringify(envOf(3)).slice(0, 160))
|
|
231
216
|
check(fs.readFileSync(file('mcp-created.txt'), 'utf8') === 'mcp\ncreate\n', 'C4 MCP 新建的内容真的落盘')
|