@namzu/sdk 3.0.0 → 3.2.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/CHANGELOG.md +223 -0
- package/dist/advisory/__tests__/consultation-context.test.d.ts +2 -0
- package/dist/advisory/__tests__/consultation-context.test.d.ts.map +1 -0
- package/dist/advisory/__tests__/consultation-context.test.js +124 -0
- package/dist/advisory/__tests__/consultation-context.test.js.map +1 -0
- package/dist/advisory/context.d.ts +25 -0
- package/dist/advisory/context.d.ts.map +1 -1
- package/dist/advisory/context.js +18 -0
- package/dist/advisory/context.js.map +1 -1
- package/dist/advisory/executor.d.ts.map +1 -1
- package/dist/advisory/executor.js +25 -3
- package/dist/advisory/executor.js.map +1 -1
- package/dist/compaction/__tests__/context-reducer.test.d.ts +2 -0
- package/dist/compaction/__tests__/context-reducer.test.d.ts.map +1 -0
- package/dist/compaction/__tests__/context-reducer.test.js +197 -0
- package/dist/compaction/__tests__/context-reducer.test.js.map +1 -0
- package/dist/compaction/factory.d.ts +7 -0
- package/dist/compaction/factory.d.ts.map +1 -1
- package/dist/compaction/factory.js +7 -0
- package/dist/compaction/factory.js.map +1 -1
- package/dist/compaction/index.d.ts +2 -0
- package/dist/compaction/index.d.ts.map +1 -1
- package/dist/compaction/index.js +1 -0
- package/dist/compaction/index.js.map +1 -1
- package/dist/compaction/interface.d.ts +13 -0
- package/dist/compaction/interface.d.ts.map +1 -1
- package/dist/compaction/managers/null.d.ts +3 -0
- package/dist/compaction/managers/null.d.ts.map +1 -1
- package/dist/compaction/managers/null.js +3 -0
- package/dist/compaction/managers/null.js.map +1 -1
- package/dist/compaction/managers/slidingWindow.d.ts +6 -0
- package/dist/compaction/managers/slidingWindow.d.ts.map +1 -1
- package/dist/compaction/managers/slidingWindow.js +6 -0
- package/dist/compaction/managers/slidingWindow.js.map +1 -1
- package/dist/compaction/managers/structured.d.ts +10 -0
- package/dist/compaction/managers/structured.d.ts.map +1 -1
- package/dist/compaction/managers/structured.js +10 -0
- package/dist/compaction/managers/structured.js.map +1 -1
- package/dist/compaction/reducer.d.ts +86 -0
- package/dist/compaction/reducer.d.ts.map +1 -0
- package/dist/compaction/reducer.js +77 -0
- package/dist/compaction/reducer.js.map +1 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts +2 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts.map +1 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.js +54 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.js.map +1 -0
- package/dist/connector/builtins/http.d.ts.map +1 -1
- package/dist/connector/builtins/http.js +24 -2
- package/dist/connector/builtins/http.js.map +1 -1
- package/dist/connector/builtins/http.test.js +18 -2
- package/dist/connector/builtins/http.test.js.map +1 -1
- package/dist/connector/index.d.ts +2 -2
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts +2 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts.map +1 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js +214 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js.map +1 -0
- package/dist/connector/mcp/client.d.ts +52 -1
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +86 -0
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/discovery.d.ts +12 -1
- package/dist/connector/mcp/discovery.d.ts.map +1 -1
- package/dist/connector/mcp/discovery.js +19 -4
- package/dist/connector/mcp/discovery.js.map +1 -1
- package/dist/connector/mcp/index.d.ts +2 -2
- package/dist/connector/mcp/index.d.ts.map +1 -1
- package/dist/connector/mcp/index.js +1 -1
- package/dist/connector/mcp/index.js.map +1 -1
- package/dist/connector/mcp/server.d.ts +42 -1
- package/dist/connector/mcp/server.d.ts.map +1 -1
- package/dist/connector/mcp/server.js +77 -4
- package/dist/connector/mcp/server.js.map +1 -1
- package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts +2 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts.map +1 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.js +58 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.js.map +1 -0
- package/dist/plugin/__tests__/discovery-scopes.test.d.ts +2 -0
- package/dist/plugin/__tests__/discovery-scopes.test.d.ts.map +1 -0
- package/dist/plugin/__tests__/discovery-scopes.test.js +97 -0
- package/dist/plugin/__tests__/discovery-scopes.test.js.map +1 -0
- package/dist/plugin/__tests__/enable-contributions.test.js +5 -1
- package/dist/plugin/__tests__/enable-contributions.test.js.map +1 -1
- package/dist/plugin/__tests__/mcp-admission.test.d.ts +2 -0
- package/dist/plugin/__tests__/mcp-admission.test.d.ts.map +1 -0
- package/dist/plugin/__tests__/mcp-admission.test.js +192 -0
- package/dist/plugin/__tests__/mcp-admission.test.js.map +1 -0
- package/dist/plugin/lifecycle.d.ts +41 -0
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +29 -1
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/loader.d.ts +39 -3
- package/dist/plugin/loader.d.ts.map +1 -1
- package/dist/plugin/loader.js +37 -4
- package/dist/plugin/loader.js.map +1 -1
- package/dist/public-runtime.d.ts +5 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +11 -3
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -2
- package/dist/public-types.d.ts.map +1 -1
- package/dist/rag/__tests__/namespace-isolation.test.d.ts +2 -0
- package/dist/rag/__tests__/namespace-isolation.test.d.ts.map +1 -0
- package/dist/rag/__tests__/namespace-isolation.test.js +80 -0
- package/dist/rag/__tests__/namespace-isolation.test.js.map +1 -0
- package/dist/rag/ingestion.d.ts.map +1 -1
- package/dist/rag/ingestion.js +1 -0
- package/dist/rag/ingestion.js.map +1 -1
- package/dist/rag/retriever.d.ts.map +1 -1
- package/dist/rag/retriever.js +2 -0
- package/dist/rag/retriever.js.map +1 -1
- package/dist/rag/vector-store.d.ts.map +1 -1
- package/dist/rag/vector-store.js +6 -0
- package/dist/rag/vector-store.js.map +1 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +113 -109
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/runtime/query/__tests__/capability-negotiation.test.js +59 -0
- package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -1
- package/dist/runtime/query/__tests__/per-step-skills.test.d.ts +10 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.js +122 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.js.map +1 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.js +153 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.js.map +1 -0
- package/dist/runtime/query/__tests__/resume-run.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/resume-run.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/resume-run.test.js +211 -0
- package/dist/runtime/query/__tests__/resume-run.test.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +1 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +10 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +10 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +26 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +118 -34
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js +96 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +95 -5
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js +180 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/resume-run.d.ts +70 -0
- package/dist/runtime/query/resume-run.d.ts.map +1 -0
- package/dist/runtime/query/resume-run.js +46 -0
- package/dist/runtime/query/resume-run.js.map +1 -0
- package/dist/telemetry/__tests__/model-call-span.test.d.ts +2 -0
- package/dist/telemetry/__tests__/model-call-span.test.d.ts.map +1 -0
- package/dist/telemetry/__tests__/model-call-span.test.js +147 -0
- package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -0
- package/dist/telemetry/__tests__/span-closure.test.d.ts +2 -0
- package/dist/telemetry/__tests__/span-closure.test.d.ts.map +1 -0
- package/dist/telemetry/__tests__/span-closure.test.js +124 -0
- package/dist/telemetry/__tests__/span-closure.test.js.map +1 -0
- package/dist/tools/advisory/index.js +1 -1
- package/dist/tools/advisory/index.js.map +1 -1
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js +78 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js.map +1 -0
- package/dist/tools/builtins/__tests__/edit.test.js +242 -1
- package/dist/tools/builtins/__tests__/edit.test.js.map +1 -1
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.js +187 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.js.map +1 -0
- package/dist/tools/builtins/atomic-write-file.d.ts +1 -1
- package/dist/tools/builtins/atomic-write-file.d.ts.map +1 -1
- package/dist/tools/builtins/atomic-write-file.js +31 -2
- package/dist/tools/builtins/atomic-write-file.js.map +1 -1
- package/dist/tools/builtins/content-fingerprint.d.ts +23 -0
- package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -0
- package/dist/tools/builtins/content-fingerprint.js +30 -0
- package/dist/tools/builtins/content-fingerprint.js.map +1 -0
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +172 -31
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +5 -2
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/write-file.d.ts.map +1 -1
- package/dist/tools/builtins/write-file.js +75 -27
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/coordinator/__tests__/ask-user-question.test.js +55 -0
- package/dist/tools/coordinator/__tests__/ask-user-question.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.js +126 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +75 -5
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/coordinator/plan-dependencies.d.ts +43 -0
- package/dist/tools/coordinator/plan-dependencies.d.ts.map +1 -0
- package/dist/tools/coordinator/plan-dependencies.js +148 -0
- package/dist/tools/coordinator/plan-dependencies.js.map +1 -0
- package/dist/types/agent/supervisor.d.ts +15 -0
- package/dist/types/agent/supervisor.d.ts.map +1 -1
- package/dist/types/connector/core.d.ts +34 -0
- package/dist/types/connector/core.d.ts.map +1 -1
- package/dist/types/connector/definition.d.ts +10 -0
- package/dist/types/connector/definition.d.ts.map +1 -1
- package/dist/types/connector/mcp.d.ts +13 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/rag/retrieval.d.ts +16 -0
- package/dist/types/rag/retrieval.d.ts.map +1 -1
- package/dist/types/rag/storage.d.ts +9 -0
- package/dist/types/rag/storage.d.ts.map +1 -1
- package/dist/types/rag/vector.d.ts +11 -0
- package/dist/types/rag/vector.d.ts.map +1 -1
- package/dist/types/router/task-router.d.ts +19 -0
- package/dist/types/router/task-router.d.ts.map +1 -1
- package/dist/types/run/prepare-step.d.ts +56 -3
- package/dist/types/run/prepare-step.d.ts.map +1 -1
- package/dist/types/tool/index.d.ts +17 -1
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/toolset/index.d.ts +22 -0
- package/dist/types/toolset/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/advisory/__tests__/consultation-context.test.ts +191 -0
- package/src/advisory/context.ts +32 -0
- package/src/advisory/executor.ts +30 -3
- package/src/compaction/__tests__/context-reducer.test.ts +239 -0
- package/src/compaction/factory.ts +7 -0
- package/src/compaction/index.ts +8 -0
- package/src/compaction/interface.ts +13 -0
- package/src/compaction/managers/null.ts +3 -0
- package/src/compaction/managers/slidingWindow.ts +6 -0
- package/src/compaction/managers/structured.ts +10 -0
- package/src/compaction/reducer.ts +154 -0
- package/src/connector/builtins/__tests__/oauth2-auth.test.ts +73 -0
- package/src/connector/builtins/http.test.ts +28 -2
- package/src/connector/builtins/http.ts +26 -2
- package/src/connector/index.ts +6 -2
- package/src/connector/mcp/__tests__/prompts-and-lifecycle.test.ts +286 -0
- package/src/connector/mcp/client.ts +95 -0
- package/src/connector/mcp/discovery.ts +19 -4
- package/src/connector/mcp/index.ts +6 -2
- package/src/connector/mcp/server.ts +101 -3
- package/src/manager/agent/__tests__/depth-limit-authority.test.ts +74 -0
- package/src/plugin/__tests__/discovery-scopes.test.ts +133 -0
- package/src/plugin/__tests__/enable-contributions.test.ts +5 -1
- package/src/plugin/__tests__/mcp-admission.test.ts +242 -0
- package/src/plugin/lifecycle.ts +57 -1
- package/src/plugin/loader.ts +57 -3
- package/src/public-runtime.ts +16 -1
- package/src/public-types.ts +5 -0
- package/src/rag/__tests__/namespace-isolation.test.ts +109 -0
- package/src/rag/ingestion.ts +1 -0
- package/src/rag/retriever.ts +2 -0
- package/src/rag/vector-store.ts +5 -0
- package/src/registry/tool/execute.ts +123 -119
- package/src/runtime/query/__tests__/capability-negotiation.test.ts +69 -0
- package/src/runtime/query/__tests__/per-step-skills.test.ts +154 -0
- package/src/runtime/query/__tests__/per-step-tool-choice.test.ts +180 -0
- package/src/runtime/query/__tests__/resume-run.test.ts +262 -0
- package/src/runtime/query/executor.ts +9 -1
- package/src/runtime/query/index.ts +39 -0
- package/src/runtime/query/iteration/index.ts +130 -34
- package/src/runtime/query/iteration/phases/compaction-model-routing.test.ts +125 -0
- package/src/runtime/query/iteration/phases/compaction.ts +106 -5
- package/src/runtime/query/iteration/phases/context-reducer-dispatch.test.ts +238 -0
- package/src/runtime/query/iteration/phases/context.ts +11 -0
- package/src/runtime/query/resume-run.ts +93 -0
- package/src/telemetry/__tests__/model-call-span.test.ts +189 -0
- package/src/telemetry/__tests__/span-closure.test.ts +153 -0
- package/src/tools/advisory/index.ts +1 -1
- package/src/tools/builtins/__tests__/edit-commits-atomically.test.ts +103 -0
- package/src/tools/builtins/__tests__/edit.test.ts +324 -1
- package/src/tools/builtins/__tests__/stale-file-refusal.test.ts +258 -0
- package/src/tools/builtins/atomic-write-file.ts +30 -2
- package/src/tools/builtins/content-fingerprint.ts +33 -0
- package/src/tools/builtins/edit.ts +188 -36
- package/src/tools/builtins/read-file.ts +5 -2
- package/src/tools/builtins/write-file.ts +79 -28
- package/src/tools/coordinator/__tests__/ask-user-question.test.ts +68 -0
- package/src/tools/coordinator/__tests__/plan-dependencies.test.ts +186 -0
- package/src/tools/coordinator/index.ts +116 -42
- package/src/tools/coordinator/plan-dependencies.ts +175 -0
- package/src/types/agent/supervisor.ts +15 -0
- package/src/types/connector/core.ts +34 -0
- package/src/types/connector/definition.ts +10 -0
- package/src/types/connector/mcp.ts +14 -0
- package/src/types/rag/retrieval.ts +16 -0
- package/src/types/rag/storage.ts +9 -0
- package/src/types/rag/vector.ts +11 -0
- package/src/types/router/task-router.ts +19 -0
- package/src/types/run/prepare-step.ts +58 -3
- package/src/types/tool/index.ts +17 -1
- package/src/types/toolset/index.ts +22 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A span that never ends is a trace that never closes, and the export is
|
|
6
|
+
* incomplete for exactly the run that failed.
|
|
7
|
+
*
|
|
8
|
+
* Both sites had the same shape: an `end()` call at every exit the author
|
|
9
|
+
* could see. The iteration loop had seventeen of them and the tool executor
|
|
10
|
+
* three plus a `finally` that opened below them. That is a rule every future
|
|
11
|
+
* edit has to remember, and it was already broken — anything throwing between
|
|
12
|
+
* the span's creation and the `try` left it open, and a generator abandoned
|
|
13
|
+
* by its consumer reached no exit at all.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const ended: string[] = []
|
|
17
|
+
const started: string[] = []
|
|
18
|
+
|
|
19
|
+
function fakeSpan(name: string) {
|
|
20
|
+
return {
|
|
21
|
+
setAttributes: () => undefined,
|
|
22
|
+
setAttribute: () => undefined,
|
|
23
|
+
setStatus: () => undefined,
|
|
24
|
+
recordException: () => undefined,
|
|
25
|
+
addEvent: () => undefined,
|
|
26
|
+
end: () => ended.push(name),
|
|
27
|
+
spanContext: () => ({ traceId: 't', spanId: 's', traceFlags: 1 }),
|
|
28
|
+
isRecording: () => true,
|
|
29
|
+
updateName: () => undefined,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
vi.mock('../runtime-accessors.js', () => ({
|
|
34
|
+
getTracer: () => ({
|
|
35
|
+
startSpan: (name: string) => {
|
|
36
|
+
started.push(name)
|
|
37
|
+
return fakeSpan(name)
|
|
38
|
+
},
|
|
39
|
+
startActiveSpan: (
|
|
40
|
+
name: string,
|
|
41
|
+
_opts: unknown,
|
|
42
|
+
_ctx: unknown,
|
|
43
|
+
fn: (span: ReturnType<typeof fakeSpan>) => unknown,
|
|
44
|
+
) => {
|
|
45
|
+
started.push(name)
|
|
46
|
+
return fn(fakeSpan(name))
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
getMeter: () => ({
|
|
50
|
+
createCounter: () => ({ add: () => undefined }),
|
|
51
|
+
createHistogram: () => ({ record: () => undefined }),
|
|
52
|
+
}),
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
function toolContext() {
|
|
56
|
+
return {
|
|
57
|
+
runId: 'run_span',
|
|
58
|
+
workingDirectory: '.',
|
|
59
|
+
abortSignal: new AbortController().signal,
|
|
60
|
+
env: {},
|
|
61
|
+
log: () => undefined,
|
|
62
|
+
} as never
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
started.length = 0
|
|
67
|
+
ended.length = 0
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
vi.clearAllMocks()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('a tool span closes however the call leaves', () => {
|
|
75
|
+
it('closes on the ordinary path', async () => {
|
|
76
|
+
const { ToolRegistry } = await import('../../registry/tool/execute.js')
|
|
77
|
+
const tools = new ToolRegistry()
|
|
78
|
+
tools.register({
|
|
79
|
+
name: 'echo',
|
|
80
|
+
description: 'echo',
|
|
81
|
+
inputSchema: z.object({}),
|
|
82
|
+
execute: async () => ({ success: true, output: 'ok' }),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
await tools.execute('echo', {}, toolContext())
|
|
86
|
+
|
|
87
|
+
expect(started).toHaveLength(1)
|
|
88
|
+
expect(ended).toEqual(started)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('closes when the tool throws', async () => {
|
|
92
|
+
const { ToolRegistry } = await import('../../registry/tool/execute.js')
|
|
93
|
+
const tools = new ToolRegistry()
|
|
94
|
+
tools.register({
|
|
95
|
+
name: 'boom',
|
|
96
|
+
description: 'boom',
|
|
97
|
+
inputSchema: z.object({}),
|
|
98
|
+
execute: async () => {
|
|
99
|
+
throw new Error('kaboom')
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
await tools.execute('boom', {}, toolContext())
|
|
104
|
+
|
|
105
|
+
expect(ended).toEqual(started)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('closes when input validation refuses the call', async () => {
|
|
109
|
+
const { ToolRegistry } = await import('../../registry/tool/execute.js')
|
|
110
|
+
const tools = new ToolRegistry()
|
|
111
|
+
tools.register({
|
|
112
|
+
name: 'strict',
|
|
113
|
+
description: 'strict',
|
|
114
|
+
inputSchema: z.object({ required: z.string() }),
|
|
115
|
+
execute: async () => ({ success: true, output: 'ok' }),
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await tools.execute('strict', { wrong: 1 }, toolContext())
|
|
119
|
+
|
|
120
|
+
expect(ended).toEqual(started)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('closes when the tool is not active', async () => {
|
|
124
|
+
const { ToolRegistry } = await import('../../registry/tool/execute.js')
|
|
125
|
+
const tools = new ToolRegistry()
|
|
126
|
+
tools.register(
|
|
127
|
+
{
|
|
128
|
+
name: 'later',
|
|
129
|
+
description: 'later',
|
|
130
|
+
inputSchema: z.object({}),
|
|
131
|
+
execute: async () => ({ success: true, output: 'ok' }),
|
|
132
|
+
},
|
|
133
|
+
'deferred',
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
await tools.execute('later', {}, toolContext())
|
|
137
|
+
|
|
138
|
+
expect(ended).toEqual(started)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('closes when the registry does not hold the name at all', async () => {
|
|
142
|
+
const { ToolRegistry } = await import('../../registry/tool/execute.js')
|
|
143
|
+
const tools = new ToolRegistry()
|
|
144
|
+
|
|
145
|
+
// `getOrThrow` sat OUTSIDE the try that owned the finally, so this
|
|
146
|
+
// path — the one where the model invents a tool name — opened a span
|
|
147
|
+
// and never closed it.
|
|
148
|
+
await expect(tools.execute('nonexistent', {}, toolContext())).rejects.toThrow()
|
|
149
|
+
|
|
150
|
+
expect(started).toHaveLength(1)
|
|
151
|
+
expect(ended).toEqual(started)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
@@ -104,7 +104,7 @@ export function buildAdvisoryTools(opts: AdvisoryToolsOptions): ToolDefinition[]
|
|
|
104
104
|
const executionResult = await advisoryCtx.executor.consult(
|
|
105
105
|
advisor,
|
|
106
106
|
{ advisorId: advisor.id, question, domain, urgency, includeContext: include_context },
|
|
107
|
-
|
|
107
|
+
advisoryCtx.callContext(),
|
|
108
108
|
)
|
|
109
109
|
|
|
110
110
|
advisoryCtx.recordCall({
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import type { ToolContext } from '../../../types/tool/index.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* That the atomic writer works is tested next door. This tests the thing
|
|
10
|
+
* that actually regressed: whether the TOOL goes through it.
|
|
11
|
+
*
|
|
12
|
+
* `edit` and `write` were reverted to a bare `writeFile`, and the existing
|
|
13
|
+
* atomicity test kept passing because it called `atomicWriteFile` directly —
|
|
14
|
+
* proving the module, never the caller. A write that fails partway then
|
|
15
|
+
* truncates the user's file, which is the one outcome `edit` exists to
|
|
16
|
+
* avoid.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const atomicWriteFile = vi.fn(async () => {})
|
|
20
|
+
vi.mock('../atomic-write-file.js', () => ({ atomicWriteFile }))
|
|
21
|
+
|
|
22
|
+
function makeContext(workingDirectory: string): ToolContext {
|
|
23
|
+
return {
|
|
24
|
+
runId: 'run_atomic' as ToolContext['runId'],
|
|
25
|
+
workingDirectory,
|
|
26
|
+
abortSignal: new AbortController().signal,
|
|
27
|
+
env: {},
|
|
28
|
+
log: () => {},
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('the file-mutating tools commit through the atomic writer', () => {
|
|
33
|
+
it('edit routes its local write through it', async () => {
|
|
34
|
+
const { EditTool } = await import('../edit.js')
|
|
35
|
+
atomicWriteFile.mockClear()
|
|
36
|
+
|
|
37
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-atomic-tool-'))
|
|
38
|
+
writeFileSync(join(dir, 'doc.md'), 'alpha\nbeta\n')
|
|
39
|
+
|
|
40
|
+
const result = await EditTool.execute(
|
|
41
|
+
{ path: 'doc.md', old_string: 'beta', new_string: 'gamma', replace_all: false },
|
|
42
|
+
makeContext(dir),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
expect(result.success).toBe(true)
|
|
46
|
+
expect(atomicWriteFile).toHaveBeenCalledTimes(1)
|
|
47
|
+
expect(atomicWriteFile).toHaveBeenCalledWith(resolve(dir, 'doc.md'), 'alpha\ngamma\n')
|
|
48
|
+
// The mock swallowed the write, so the file is untouched — which is
|
|
49
|
+
// itself the proof that nothing else wrote it.
|
|
50
|
+
expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\nbeta\n')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('write routes its local write through it', async () => {
|
|
54
|
+
const { WriteFileTool } = await import('../write-file.js')
|
|
55
|
+
atomicWriteFile.mockClear()
|
|
56
|
+
|
|
57
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-atomic-tool-'))
|
|
58
|
+
|
|
59
|
+
const result = await WriteFileTool.execute(
|
|
60
|
+
{ path: 'out.md', content: 'a complete body' },
|
|
61
|
+
makeContext(dir),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
expect(result.success).toBe(true)
|
|
65
|
+
expect(atomicWriteFile).toHaveBeenCalledTimes(1)
|
|
66
|
+
expect(atomicWriteFile).toHaveBeenCalledWith(resolve(dir, 'out.md'), 'a complete body')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('edit does not use it for a sandboxed write, which the sandbox owns', async () => {
|
|
70
|
+
const { EditTool } = await import('../edit.js')
|
|
71
|
+
atomicWriteFile.mockClear()
|
|
72
|
+
|
|
73
|
+
let body = 'alpha\nbeta\n'
|
|
74
|
+
const sandbox = {
|
|
75
|
+
id: 'sbx',
|
|
76
|
+
status: 'ready',
|
|
77
|
+
rootDir: '/workspace',
|
|
78
|
+
environment: 'basic',
|
|
79
|
+
async readFile() {
|
|
80
|
+
return Buffer.from(body)
|
|
81
|
+
},
|
|
82
|
+
async writeFile(_path: string, content: string | Buffer) {
|
|
83
|
+
body = content.toString()
|
|
84
|
+
},
|
|
85
|
+
async exec() {
|
|
86
|
+
throw new Error('not used')
|
|
87
|
+
},
|
|
88
|
+
async listFiles() {
|
|
89
|
+
return []
|
|
90
|
+
},
|
|
91
|
+
async destroy() {},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const result = await EditTool.execute(
|
|
95
|
+
{ path: 'doc.md', old_string: 'beta', new_string: 'gamma', replace_all: false },
|
|
96
|
+
{ ...makeContext('/workspace'), sandbox } as ToolContext,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
expect(result.success).toBe(true)
|
|
100
|
+
expect(body).toBe('alpha\ngamma\n')
|
|
101
|
+
expect(atomicWriteFile).not.toHaveBeenCalled()
|
|
102
|
+
})
|
|
103
|
+
})
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { describe, expect, it } from 'vitest'
|
|
5
|
+
import { ToolRegistry } from '../../../registry/tool/execute.js'
|
|
6
|
+
import type { Sandbox } from '../../../types/sandbox/index.js'
|
|
5
7
|
import type { ToolContext } from '../../../types/tool/index.js'
|
|
8
|
+
import { atomicWriteFile } from '../atomic-write-file.js'
|
|
6
9
|
import { EditTool } from '../edit.js'
|
|
7
10
|
|
|
8
11
|
function makeContext(workingDirectory: string): ToolContext {
|
|
@@ -16,6 +19,326 @@ function makeContext(workingDirectory: string): ToolContext {
|
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
describe('EditTool', () => {
|
|
22
|
+
it('publishes one closed canonical replacement contract', () => {
|
|
23
|
+
const schema = EditTool.modelInputSchema
|
|
24
|
+
expect(schema).toEqual({
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
path: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Path to the file to edit. Must not be empty.',
|
|
30
|
+
},
|
|
31
|
+
old_string: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description:
|
|
34
|
+
'Exact unique text from the file, without read-tool line-number prefixes. Must not be empty.',
|
|
35
|
+
},
|
|
36
|
+
new_string: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description:
|
|
39
|
+
'Exact replacement text. May be empty to delete old_string. Keep under 12000 characters.',
|
|
40
|
+
},
|
|
41
|
+
replace_all: {
|
|
42
|
+
type: 'boolean',
|
|
43
|
+
description: 'Replace every occurrence instead of requiring one unique match.',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
required: ['path', 'old_string', 'new_string'],
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
expect(
|
|
51
|
+
EditTool.inputSchema.safeParse({
|
|
52
|
+
path: 'doc.md',
|
|
53
|
+
old_string: 'old',
|
|
54
|
+
new_string: 'new',
|
|
55
|
+
replace_all: true,
|
|
56
|
+
}).success,
|
|
57
|
+
).toBe(true)
|
|
58
|
+
|
|
59
|
+
// The MODEL schema above is the closed canonical one. The HOST schema
|
|
60
|
+
// is wider on purpose — aliases and line insertion are declared — and
|
|
61
|
+
// the point of `.strict()` is that wider still is not open.
|
|
62
|
+
for (const supported of [
|
|
63
|
+
{ path: 'doc.md', oldStr: 'old', newStr: 'new' },
|
|
64
|
+
{ path: 'doc.md', insertLine: 'end', new_string: 'append' },
|
|
65
|
+
{ path: 'doc.md', old_string: 'old', new_string: 'new', newStr: 'legacy' },
|
|
66
|
+
]) {
|
|
67
|
+
expect(EditTool.inputSchema.safeParse(supported).success, JSON.stringify(supported)).toBe(
|
|
68
|
+
true,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const unknown of [
|
|
73
|
+
{ path: 'doc.md', old_str: 'old', new_string: 'new' },
|
|
74
|
+
{ path: 'doc.md', old_string: 'old', new_string: 'new', replaceAll: true },
|
|
75
|
+
]) {
|
|
76
|
+
expect(EditTool.inputSchema.safeParse(unknown).success, JSON.stringify(unknown)).toBe(false)
|
|
77
|
+
}
|
|
78
|
+
expect(
|
|
79
|
+
EditTool.inputSchema.safeParse({
|
|
80
|
+
path: ' ',
|
|
81
|
+
old_string: 'old',
|
|
82
|
+
new_string: 'new',
|
|
83
|
+
}).success,
|
|
84
|
+
).toBe(false)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('replaces one exact unique string', async () => {
|
|
88
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
|
|
89
|
+
writeFileSync(join(dir, 'doc.md'), 'alpha\nbeta\n')
|
|
90
|
+
|
|
91
|
+
const result = await EditTool.execute(
|
|
92
|
+
{
|
|
93
|
+
path: 'doc.md',
|
|
94
|
+
old_string: 'beta',
|
|
95
|
+
new_string: 'gamma',
|
|
96
|
+
replace_all: false,
|
|
97
|
+
},
|
|
98
|
+
makeContext(dir),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
expect(result.success).toBe(true)
|
|
102
|
+
expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\ngamma\n')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('validates a path without rewriting edge whitespace', async () => {
|
|
106
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
|
|
107
|
+
writeFileSync(join(dir, ' doc.md '), 'alpha\n')
|
|
108
|
+
|
|
109
|
+
const result = await EditTool.execute(
|
|
110
|
+
{
|
|
111
|
+
path: ' doc.md ',
|
|
112
|
+
old_string: 'alpha',
|
|
113
|
+
new_string: 'beta',
|
|
114
|
+
replace_all: false,
|
|
115
|
+
},
|
|
116
|
+
makeContext(dir),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
expect(result.success).toBe(true)
|
|
120
|
+
expect(readFileSync(join(dir, ' doc.md '), 'utf-8')).toBe('beta\n')
|
|
121
|
+
expect(existsSync(join(dir, 'doc.md'))).toBe(false)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('allows deletion with an empty new_string', async () => {
|
|
125
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
|
|
126
|
+
writeFileSync(join(dir, 'doc.md'), 'alpha\nremove me\nomega\n')
|
|
127
|
+
|
|
128
|
+
const result = await EditTool.execute(
|
|
129
|
+
{
|
|
130
|
+
path: 'doc.md',
|
|
131
|
+
old_string: 'remove me\n',
|
|
132
|
+
new_string: '',
|
|
133
|
+
replace_all: false,
|
|
134
|
+
},
|
|
135
|
+
makeContext(dir),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
expect(result.success).toBe(true)
|
|
139
|
+
expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\nomega\n')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('replaces every exact occurrence only when replace_all is true', async () => {
|
|
143
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
|
|
144
|
+
writeFileSync(join(dir, 'doc.md'), 'alpha alpha alpha')
|
|
145
|
+
|
|
146
|
+
const result = await EditTool.execute(
|
|
147
|
+
{
|
|
148
|
+
path: 'doc.md',
|
|
149
|
+
old_string: 'alpha',
|
|
150
|
+
new_string: 'beta',
|
|
151
|
+
replace_all: true,
|
|
152
|
+
},
|
|
153
|
+
makeContext(dir),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
expect(result.success).toBe(true)
|
|
157
|
+
expect(result.data).toMatchObject({ replacements: 3 })
|
|
158
|
+
expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('beta beta beta')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('refuses ambiguous exact matches instead of guessing', async () => {
|
|
162
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
|
|
163
|
+
writeFileSync(join(dir, 'doc.md'), 'alpha\nalpha\n')
|
|
164
|
+
|
|
165
|
+
const result = await EditTool.execute(
|
|
166
|
+
{
|
|
167
|
+
path: 'doc.md',
|
|
168
|
+
old_string: 'alpha',
|
|
169
|
+
new_string: 'beta',
|
|
170
|
+
replace_all: false,
|
|
171
|
+
},
|
|
172
|
+
makeContext(dir),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
expect(result.success).toBe(false)
|
|
176
|
+
expect(result.error).toContain('is not unique')
|
|
177
|
+
expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\nalpha\n')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('normalizes consistent CRLF/LF boundaries without fuzzy matching', async () => {
|
|
181
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
|
|
182
|
+
writeFileSync(join(dir, 'doc.md'), 'alpha\r\nbeta\r\nomega\r\n')
|
|
183
|
+
|
|
184
|
+
const result = await EditTool.execute(
|
|
185
|
+
{
|
|
186
|
+
path: 'doc.md',
|
|
187
|
+
old_string: 'alpha\nbeta',
|
|
188
|
+
new_string: 'gamma\ndelta',
|
|
189
|
+
replace_all: false,
|
|
190
|
+
},
|
|
191
|
+
makeContext(dir),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
expect(result.success).toBe(true)
|
|
195
|
+
expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('gamma\r\ndelta\r\nomega\r\n')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('rejects a field it does not accept, even when execute is called directly', async () => {
|
|
199
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
|
|
200
|
+
writeFileSync(join(dir, 'doc.md'), 'alpha\n')
|
|
201
|
+
|
|
202
|
+
// Without `.strict()` zod STRIPS an unrecognised key, so each of these
|
|
203
|
+
// would run as if the caller had never written it — a misspelling
|
|
204
|
+
// becomes a silent no-op rather than an error anyone can act on.
|
|
205
|
+
for (const input of [
|
|
206
|
+
{ path: 'doc.md', old_str: 'alpha', new_string: 'beta' },
|
|
207
|
+
{ path: 'doc.md', old_string: 'alpha', new_string: 'beta', overwrite: true },
|
|
208
|
+
{ path: 'doc.md', old_string: 'alpha', new_string: 'beta', replaceAll: true },
|
|
209
|
+
]) {
|
|
210
|
+
const result = await EditTool.execute(input as never, makeContext(dir))
|
|
211
|
+
expect(result.success, JSON.stringify(input)).toBe(false)
|
|
212
|
+
expect(result.error).toContain('Invalid edit input')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\n')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('accepts the host aliases the schema does declare', async () => {
|
|
219
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
|
|
220
|
+
writeFileSync(join(dir, 'doc.md'), 'alpha\n')
|
|
221
|
+
|
|
222
|
+
// The contract is closed, not narrow. `oldStr`/`newStr` and
|
|
223
|
+
// `insertLine` are in the schema for hosts that speak those names, so
|
|
224
|
+
// strictness must reject the unknown without rejecting these.
|
|
225
|
+
const result = await EditTool.execute(
|
|
226
|
+
{ path: 'doc.md', oldStr: 'alpha', newStr: 'beta' } as never,
|
|
227
|
+
makeContext(dir),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
expect(result.success).toBe(true)
|
|
231
|
+
expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('beta\n')
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('returns one canonical recovery shape after registry validation fails', async () => {
|
|
235
|
+
const registry = new ToolRegistry()
|
|
236
|
+
registry.register(EditTool)
|
|
237
|
+
|
|
238
|
+
const result = await registry.execute(
|
|
239
|
+
'edit',
|
|
240
|
+
{ insertLine: '"end"', newStr: 'content' },
|
|
241
|
+
makeContext('/tmp'),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
expect(result.success).toBe(false)
|
|
245
|
+
expect(result.error).toContain('Validation failed for "edit":')
|
|
246
|
+
expect(result.error).toContain('Required: path: string')
|
|
247
|
+
expect(result.error).toContain(
|
|
248
|
+
'{"path":"file.md","old_string":"exact unique text","new_string":"replacement text"}',
|
|
249
|
+
)
|
|
250
|
+
expect(result.error).not.toContain('Accepted shapes')
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('serializes independent EditTool read-modify-write cycles for one sandbox path', async () => {
|
|
254
|
+
let body = Buffer.from('alpha')
|
|
255
|
+
let reads = 0
|
|
256
|
+
let writes = 0
|
|
257
|
+
let releaseFirstWrite = () => {}
|
|
258
|
+
let markFirstWriteStarted = () => {}
|
|
259
|
+
const firstWriteStarted = new Promise<void>((resolve) => {
|
|
260
|
+
markFirstWriteStarted = resolve
|
|
261
|
+
})
|
|
262
|
+
const firstWriteGate = new Promise<void>((resolve) => {
|
|
263
|
+
releaseFirstWrite = resolve
|
|
264
|
+
})
|
|
265
|
+
const sandbox = {
|
|
266
|
+
id: 'sbx_edit_lock',
|
|
267
|
+
status: 'ready',
|
|
268
|
+
rootDir: '/workspace',
|
|
269
|
+
environment: 'basic',
|
|
270
|
+
async readFile() {
|
|
271
|
+
reads += 1
|
|
272
|
+
return Buffer.from(body)
|
|
273
|
+
},
|
|
274
|
+
async writeFile(_path: string, content: string | Buffer) {
|
|
275
|
+
writes += 1
|
|
276
|
+
if (writes === 1) {
|
|
277
|
+
markFirstWriteStarted()
|
|
278
|
+
await firstWriteGate
|
|
279
|
+
}
|
|
280
|
+
body = Buffer.isBuffer(content) ? Buffer.from(content) : Buffer.from(content)
|
|
281
|
+
},
|
|
282
|
+
async exec() {
|
|
283
|
+
throw new Error('not used')
|
|
284
|
+
},
|
|
285
|
+
async listFiles() {
|
|
286
|
+
return []
|
|
287
|
+
},
|
|
288
|
+
async destroy() {},
|
|
289
|
+
} as Sandbox
|
|
290
|
+
const firstContext = { ...makeContext('/workspace'), sandbox }
|
|
291
|
+
const secondContext = { ...makeContext('/workspace'), sandbox }
|
|
292
|
+
|
|
293
|
+
const first = EditTool.execute(
|
|
294
|
+
{
|
|
295
|
+
path: 'doc.md',
|
|
296
|
+
old_string: 'alpha',
|
|
297
|
+
new_string: 'beta',
|
|
298
|
+
replace_all: false,
|
|
299
|
+
},
|
|
300
|
+
firstContext,
|
|
301
|
+
)
|
|
302
|
+
await firstWriteStarted
|
|
303
|
+
const second = EditTool.execute(
|
|
304
|
+
{
|
|
305
|
+
path: 'doc.md',
|
|
306
|
+
old_string: 'beta',
|
|
307
|
+
new_string: 'gamma',
|
|
308
|
+
replace_all: false,
|
|
309
|
+
},
|
|
310
|
+
secondContext,
|
|
311
|
+
)
|
|
312
|
+
await Promise.resolve()
|
|
313
|
+
expect(reads).toBe(1)
|
|
314
|
+
|
|
315
|
+
releaseFirstWrite()
|
|
316
|
+
const results = await Promise.all([first, second])
|
|
317
|
+
expect(results.every((result) => result.success)).toBe(true)
|
|
318
|
+
expect(reads).toBe(2)
|
|
319
|
+
expect(writes).toBe(2)
|
|
320
|
+
expect(body.toString('utf-8')).toBe('gamma')
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('leaves the original intact when an atomic local write fails before commit', async () => {
|
|
324
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-atomic-write-'))
|
|
325
|
+
const filePath = join(dir, 'doc.md')
|
|
326
|
+
writeFileSync(filePath, 'original')
|
|
327
|
+
|
|
328
|
+
await expect(
|
|
329
|
+
atomicWriteFile(filePath, 'replacement', {
|
|
330
|
+
beforeCommit: async () => {
|
|
331
|
+
throw new Error('injected pre-commit failure')
|
|
332
|
+
},
|
|
333
|
+
}),
|
|
334
|
+
).rejects.toThrow('injected pre-commit failure')
|
|
335
|
+
|
|
336
|
+
expect(readFileSync(filePath, 'utf-8')).toBe('original')
|
|
337
|
+
expect(readdirSync(dir).filter((entry) => entry.includes('.namzu-'))).toEqual([])
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
// ── the alias + insertion surface this build adds on top ──────────
|
|
341
|
+
|
|
19
342
|
it('accepts oldStr/newStr aliases for string replacement', async () => {
|
|
20
343
|
const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
|
|
21
344
|
writeFileSync(join(dir, 'doc.md'), 'alpha\nbeta\n')
|