opencode-dux 1.0.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/LICENSE +21 -0
- package/README.md +452 -0
- package/dist/agents/descriptions.d.ts +6 -0
- package/dist/agents/designer.d.ts +2 -0
- package/dist/agents/explorer.d.ts +2 -0
- package/dist/agents/fixer.d.ts +2 -0
- package/dist/agents/index.d.ts +22 -0
- package/dist/agents/interpreter.d.ts +2 -0
- package/dist/agents/librarian.d.ts +2 -0
- package/dist/agents/oracle.d.ts +2 -0
- package/dist/agents/orchestrator.d.ts +27 -0
- package/dist/agents/overrides.d.ts +18 -0
- package/dist/agents/prompt-blocks.d.ts +97 -0
- package/dist/agents/steward.d.ts +3 -0
- package/dist/cli/config-io.d.ts +24 -0
- package/dist/cli/config-manager.d.ts +4 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +1006 -0
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/mcps.d.ts +13 -0
- package/dist/cli/model-key-normalization.d.ts +1 -0
- package/dist/cli/paths.d.ts +35 -0
- package/dist/cli/providers.d.ts +137 -0
- package/dist/cli/skills.d.ts +22 -0
- package/dist/cli/system.d.ts +5 -0
- package/dist/cli/types.d.ts +38 -0
- package/dist/config/constants.d.ts +12 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/loader.d.ts +40 -0
- package/dist/config/runtime-preset.d.ts +12 -0
- package/dist/config/schema.d.ts +281 -0
- package/dist/config/utils.d.ts +10 -0
- package/dist/discovery/local/types.d.ts +79 -0
- package/dist/discovery/local.d.ts +73 -0
- package/dist/discovery/mcp-servers.d.ts +88 -0
- package/dist/discovery/skills.d.ts +94 -0
- package/dist/hooks/apply-patch/codec.d.ts +7 -0
- package/dist/hooks/apply-patch/errors.d.ts +25 -0
- package/dist/hooks/apply-patch/execution-context.d.ts +27 -0
- package/dist/hooks/apply-patch/index.d.ts +15 -0
- package/dist/hooks/apply-patch/matching.d.ts +26 -0
- package/dist/hooks/apply-patch/operations.d.ts +3 -0
- package/dist/hooks/apply-patch/patch.d.ts +2 -0
- package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
- package/dist/hooks/apply-patch/resolution.d.ts +19 -0
- package/dist/hooks/apply-patch/rewrite.d.ts +7 -0
- package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
- package/dist/hooks/apply-patch/types.d.ts +80 -0
- package/dist/hooks/auto-update-checker/cache.d.ts +11 -0
- package/dist/hooks/auto-update-checker/checker.d.ts +32 -0
- package/dist/hooks/auto-update-checker/constants.d.ts +11 -0
- package/dist/hooks/auto-update-checker/index.d.ts +18 -0
- package/dist/hooks/auto-update-checker/types.d.ts +22 -0
- package/dist/hooks/chat-headers.d.ts +16 -0
- package/dist/hooks/context-pressure-reminder/index.d.ts +33 -0
- package/dist/hooks/delegate-task-retry/guidance.d.ts +2 -0
- package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
- package/dist/hooks/delegate-task-retry/index.d.ts +4 -0
- package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
- package/dist/hooks/filter-available-skills/index.d.ts +32 -0
- package/dist/hooks/foreground-fallback/index.d.ts +72 -0
- package/dist/hooks/image-hook.d.ts +5 -0
- package/dist/hooks/index.d.ts +14 -0
- package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
- package/dist/hooks/json-error-recovery/index.d.ts +1 -0
- package/dist/hooks/phase-reminder/index.d.ts +26 -0
- package/dist/hooks/post-file-tool-nudge/index.d.ts +19 -0
- package/dist/hooks/task-session-manager/index.d.ts +52 -0
- package/dist/hooks/todo-continuation/index.d.ts +53 -0
- package/dist/hooks/todo-continuation/todo-hygiene.d.ts +35 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +31782 -0
- package/dist/mcp/context7.d.ts +6 -0
- package/dist/mcp/grep-app.d.ts +6 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/types.d.ts +12 -0
- package/dist/mcp/websearch.d.ts +9 -0
- package/dist/skills/registry.d.ts +29 -0
- package/dist/subscriptions/accounts-store.d.ts +57 -0
- package/dist/subscriptions/index.d.ts +13 -0
- package/dist/subscriptions/neuralwatt-scraper.d.ts +14 -0
- package/dist/subscriptions/opencode-go-scraper.d.ts +27 -0
- package/dist/subscriptions/types.d.ts +115 -0
- package/dist/subscriptions/usage-service.d.ts +74 -0
- package/dist/tools/ast-grep/cli.d.ts +15 -0
- package/dist/tools/ast-grep/constants.d.ts +25 -0
- package/dist/tools/ast-grep/downloader.d.ts +5 -0
- package/dist/tools/ast-grep/index.d.ts +10 -0
- package/dist/tools/ast-grep/tools.d.ts +3 -0
- package/dist/tools/ast-grep/types.d.ts +30 -0
- package/dist/tools/ast-grep/utils.d.ts +4 -0
- package/dist/tools/delegate.d.ts +14 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/preset-manager.d.ts +27 -0
- package/dist/tools/smartfetch/binary.d.ts +3 -0
- package/dist/tools/smartfetch/cache.d.ts +6 -0
- package/dist/tools/smartfetch/constants.d.ts +12 -0
- package/dist/tools/smartfetch/index.d.ts +3 -0
- package/dist/tools/smartfetch/network.d.ts +38 -0
- package/dist/tools/smartfetch/secondary-model.d.ts +28 -0
- package/dist/tools/smartfetch/tool.d.ts +3 -0
- package/dist/tools/smartfetch/types.d.ts +122 -0
- package/dist/tools/smartfetch/utils.d.ts +18 -0
- package/dist/tui-state.d.ts +168 -0
- package/dist/tui.d.ts +37 -0
- package/dist/tui.js +1896 -0
- package/dist/utils/agent-variant.d.ts +63 -0
- package/dist/utils/compat.d.ts +30 -0
- package/dist/utils/env.d.ts +1 -0
- package/dist/utils/index.d.ts +9 -0
- package/dist/utils/internal-initiator.d.ts +6 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/polling.d.ts +21 -0
- package/dist/utils/session-manager.d.ts +55 -0
- package/dist/utils/session.d.ts +90 -0
- package/dist/utils/subagent-depth.d.ts +35 -0
- package/dist/utils/system-collapse.d.ts +6 -0
- package/dist/utils/task.d.ts +4 -0
- package/dist/utils/zip-extractor.d.ts +1 -0
- package/index.ts +1 -0
- package/opencode-dux.schema.json +634 -0
- package/package.json +103 -0
- package/src/agents/descriptions.ts +55 -0
- package/src/agents/designer.test.ts +86 -0
- package/src/agents/designer.ts +154 -0
- package/src/agents/display-name.test.ts +186 -0
- package/src/agents/explorer.test.ts +79 -0
- package/src/agents/explorer.ts +144 -0
- package/src/agents/fixer.test.ts +79 -0
- package/src/agents/fixer.ts +145 -0
- package/src/agents/index.test.ts +472 -0
- package/src/agents/index.ts +248 -0
- package/src/agents/interpreter.ts +136 -0
- package/src/agents/librarian.test.ts +80 -0
- package/src/agents/librarian.ts +145 -0
- package/src/agents/oracle.test.ts +89 -0
- package/src/agents/oracle.ts +184 -0
- package/src/agents/orchestrator.test.ts +116 -0
- package/src/agents/orchestrator.ts +574 -0
- package/src/agents/overrides.ts +95 -0
- package/src/agents/prompt-blocks.test.ts +114 -0
- package/src/agents/prompt-blocks.ts +640 -0
- package/src/agents/steward.ts +146 -0
- package/src/cli/config-io.test.ts +536 -0
- package/src/cli/config-io.ts +473 -0
- package/src/cli/config-manager.test.ts +141 -0
- package/src/cli/config-manager.ts +4 -0
- package/src/cli/index.ts +88 -0
- package/src/cli/install.ts +282 -0
- package/src/cli/mcps.test.ts +62 -0
- package/src/cli/mcps.ts +39 -0
- package/src/cli/model-key-normalization.test.ts +21 -0
- package/src/cli/model-key-normalization.ts +60 -0
- package/src/cli/paths.test.ts +167 -0
- package/src/cli/paths.ts +144 -0
- package/src/cli/providers.test.ts +118 -0
- package/src/cli/providers.ts +141 -0
- package/src/cli/skills.test.ts +111 -0
- package/src/cli/skills.ts +103 -0
- package/src/cli/system.test.ts +91 -0
- package/src/cli/system.ts +180 -0
- package/src/cli/types.ts +43 -0
- package/src/config/constants.ts +58 -0
- package/src/config/index.ts +4 -0
- package/src/config/loader.test.ts +1194 -0
- package/src/config/loader.ts +269 -0
- package/src/config/model-resolution.test.ts +176 -0
- package/src/config/runtime-preset.test.ts +61 -0
- package/src/config/runtime-preset.ts +37 -0
- package/src/config/schema.ts +248 -0
- package/src/config/utils.test.ts +41 -0
- package/src/config/utils.ts +23 -0
- package/src/discovery/local/types.ts +85 -0
- package/src/discovery/local.ts +322 -0
- package/src/discovery/mcp-servers.ts +804 -0
- package/src/discovery/skills.ts +959 -0
- package/src/hooks/apply-patch/codec.test.ts +184 -0
- package/src/hooks/apply-patch/codec.ts +352 -0
- package/src/hooks/apply-patch/errors.ts +117 -0
- package/src/hooks/apply-patch/execution-context.ts +432 -0
- package/src/hooks/apply-patch/hook.test.ts +768 -0
- package/src/hooks/apply-patch/index.ts +126 -0
- package/src/hooks/apply-patch/matching.test.ts +215 -0
- package/src/hooks/apply-patch/matching.ts +586 -0
- package/src/hooks/apply-patch/operations.test.ts +1535 -0
- package/src/hooks/apply-patch/operations.ts +3 -0
- package/src/hooks/apply-patch/patch.ts +9 -0
- package/src/hooks/apply-patch/prepared-changes.ts +400 -0
- package/src/hooks/apply-patch/resolution.test.ts +420 -0
- package/src/hooks/apply-patch/resolution.ts +437 -0
- package/src/hooks/apply-patch/rewrite.ts +496 -0
- package/src/hooks/apply-patch/test-helpers.ts +52 -0
- package/src/hooks/apply-patch/types.ts +111 -0
- package/src/hooks/auto-update-checker/cache.test.ts +179 -0
- package/src/hooks/auto-update-checker/cache.ts +188 -0
- package/src/hooks/auto-update-checker/checker.test.ts +159 -0
- package/src/hooks/auto-update-checker/checker.ts +308 -0
- package/src/hooks/auto-update-checker/constants.ts +33 -0
- package/src/hooks/auto-update-checker/index.test.ts +282 -0
- package/src/hooks/auto-update-checker/index.ts +225 -0
- package/src/hooks/auto-update-checker/types.ts +26 -0
- package/src/hooks/chat-headers.test.ts +236 -0
- package/src/hooks/chat-headers.ts +97 -0
- package/src/hooks/context-pressure-reminder/index.test.ts +179 -0
- package/src/hooks/context-pressure-reminder/index.ts +137 -0
- package/src/hooks/delegate-task-retry/guidance.ts +41 -0
- package/src/hooks/delegate-task-retry/hook.ts +23 -0
- package/src/hooks/delegate-task-retry/index.test.ts +38 -0
- package/src/hooks/delegate-task-retry/index.ts +7 -0
- package/src/hooks/delegate-task-retry/patterns.ts +79 -0
- package/src/hooks/filter-available-skills/index.test.ts +297 -0
- package/src/hooks/filter-available-skills/index.ts +160 -0
- package/src/hooks/foreground-fallback/index.test.ts +624 -0
- package/src/hooks/foreground-fallback/index.ts +374 -0
- package/src/hooks/image-hook.ts +6 -0
- package/src/hooks/index.ts +17 -0
- package/src/hooks/json-error-recovery/hook.ts +73 -0
- package/src/hooks/json-error-recovery/index.test.ts +111 -0
- package/src/hooks/json-error-recovery/index.ts +6 -0
- package/src/hooks/phase-reminder/index.test.ts +74 -0
- package/src/hooks/phase-reminder/index.ts +85 -0
- package/src/hooks/post-file-tool-nudge/index.test.ts +94 -0
- package/src/hooks/post-file-tool-nudge/index.ts +63 -0
- package/src/hooks/task-session-manager/index.test.ts +833 -0
- package/src/hooks/task-session-manager/index.ts +434 -0
- package/src/hooks/todo-continuation/index.test.ts +3026 -0
- package/src/hooks/todo-continuation/index.ts +878 -0
- package/src/hooks/todo-continuation/todo-hygiene.test.ts +204 -0
- package/src/hooks/todo-continuation/todo-hygiene.ts +207 -0
- package/src/index.ts +1672 -0
- package/src/mcp/context7.ts +14 -0
- package/src/mcp/grep-app.ts +11 -0
- package/src/mcp/index.test.ts +96 -0
- package/src/mcp/index.ts +66 -0
- package/src/mcp/types.ts +16 -0
- package/src/mcp/websearch.ts +47 -0
- package/src/skills/codemap/README.md +60 -0
- package/src/skills/codemap/SKILL.md +174 -0
- package/src/skills/codemap/scripts/codemap.mjs +483 -0
- package/src/skills/codemap/scripts/codemap.test.ts +129 -0
- package/src/skills/registry.ts +218 -0
- package/src/skills/simplify/README.md +19 -0
- package/src/skills/simplify/SKILL.md +138 -0
- package/src/subscriptions/accounts-store.test.ts +236 -0
- package/src/subscriptions/accounts-store.ts +184 -0
- package/src/subscriptions/index.ts +30 -0
- package/src/subscriptions/neuralwatt-scraper.ts +108 -0
- package/src/subscriptions/opencode-go-scraper.ts +301 -0
- package/src/subscriptions/types.ts +145 -0
- package/src/subscriptions/usage-service.test.ts +202 -0
- package/src/subscriptions/usage-service.ts +651 -0
- package/src/tools/ast-grep/cli.ts +257 -0
- package/src/tools/ast-grep/constants.ts +214 -0
- package/src/tools/ast-grep/downloader.ts +131 -0
- package/src/tools/ast-grep/index.ts +24 -0
- package/src/tools/ast-grep/tools.ts +117 -0
- package/src/tools/ast-grep/types.ts +51 -0
- package/src/tools/ast-grep/utils.ts +126 -0
- package/src/tools/delegate-handoff.test.ts +18 -0
- package/src/tools/delegate.ts +508 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/preset-manager.test.ts +795 -0
- package/src/tools/preset-manager.ts +332 -0
- package/src/tools/smartfetch/binary.ts +58 -0
- package/src/tools/smartfetch/cache.test.ts +34 -0
- package/src/tools/smartfetch/cache.ts +112 -0
- package/src/tools/smartfetch/constants.ts +29 -0
- package/src/tools/smartfetch/index.ts +8 -0
- package/src/tools/smartfetch/network.test.ts +178 -0
- package/src/tools/smartfetch/network.ts +614 -0
- package/src/tools/smartfetch/secondary-model.test.ts +85 -0
- package/src/tools/smartfetch/secondary-model.ts +276 -0
- package/src/tools/smartfetch/tool.test.ts +60 -0
- package/src/tools/smartfetch/tool.ts +832 -0
- package/src/tools/smartfetch/types.ts +135 -0
- package/src/tools/smartfetch/utils.test.ts +24 -0
- package/src/tools/smartfetch/utils.ts +456 -0
- package/src/tui-state.test.ts +867 -0
- package/src/tui-state.ts +1255 -0
- package/src/tui.test.ts +336 -0
- package/src/tui.ts +1539 -0
- package/src/utils/agent-variant.test.ts +244 -0
- package/src/utils/agent-variant.ts +187 -0
- package/src/utils/compat.ts +91 -0
- package/src/utils/env.ts +12 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/internal-initiator.ts +28 -0
- package/src/utils/logger.test.ts +220 -0
- package/src/utils/logger.ts +136 -0
- package/src/utils/polling.test.ts +191 -0
- package/src/utils/polling.ts +67 -0
- package/src/utils/session-manager.test.ts +173 -0
- package/src/utils/session-manager.ts +356 -0
- package/src/utils/session.test.ts +110 -0
- package/src/utils/session.ts +389 -0
- package/src/utils/subagent-depth.test.ts +170 -0
- package/src/utils/subagent-depth.ts +75 -0
- package/src/utils/system-collapse.test.ts +86 -0
- package/src/utils/system-collapse.ts +24 -0
- package/src/utils/task.test.ts +24 -0
- package/src/utils/task.ts +20 -0
- package/src/utils/zip-extractor.ts +102 -0
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { chmod, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { platform } from 'node:os';
|
|
5
|
+
|
|
6
|
+
import { parsePatch } from './codec';
|
|
7
|
+
import { createApplyPatchHook } from './index';
|
|
8
|
+
import { applyPreparedChanges, preparePatchChanges } from './operations';
|
|
9
|
+
import { createTempDir, DEFAULT_OPTIONS, writeFixture } from './test-helpers';
|
|
10
|
+
|
|
11
|
+
function createHook() {
|
|
12
|
+
return createApplyPatchHook({
|
|
13
|
+
client: {} as never,
|
|
14
|
+
directory: '/tmp/hook-root',
|
|
15
|
+
worktree: '/tmp/hook-root',
|
|
16
|
+
} as never);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('apply-patch/hook', () => {
|
|
20
|
+
test('ignores tools other than apply_patch', async () => {
|
|
21
|
+
const hook = createHook();
|
|
22
|
+
const patchText = '*** Begin Patch\n*** End Patch';
|
|
23
|
+
const output = { args: { patchText } };
|
|
24
|
+
|
|
25
|
+
await hook['tool.execute.before']({ tool: 'read' }, output);
|
|
26
|
+
|
|
27
|
+
expect(output.args.patchText).toBe(patchText);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('blocks an unrecoverable patch as verification before native execution', async () => {
|
|
31
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
32
|
+
await writeFixture(root, 'sample.txt', 'alpha\nbeta\ngamma\n');
|
|
33
|
+
const hook = createHook();
|
|
34
|
+
const patchText = `*** Begin Patch
|
|
35
|
+
*** Update File: sample.txt
|
|
36
|
+
@@
|
|
37
|
+
-missing
|
|
38
|
+
+omega
|
|
39
|
+
*** End Patch`;
|
|
40
|
+
const output = { args: { patchText } };
|
|
41
|
+
|
|
42
|
+
await expect(
|
|
43
|
+
hook['tool.execute.before'](
|
|
44
|
+
{ tool: 'apply_patch', directory: root },
|
|
45
|
+
output,
|
|
46
|
+
),
|
|
47
|
+
).rejects.toThrow(
|
|
48
|
+
'apply_patch verification failed: Failed to find expected lines',
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(output.args.patchText).toBe(patchText);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('normalizes an exact patch wrapped in a heredoc before native execution', async () => {
|
|
55
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
56
|
+
await writeFixture(
|
|
57
|
+
root,
|
|
58
|
+
'sample.txt',
|
|
59
|
+
'line-01\nexact-top\nexact-old\nexact-bottom\nline-05\n',
|
|
60
|
+
);
|
|
61
|
+
const hook = createHook();
|
|
62
|
+
const cleanPatchText = `*** Begin Patch
|
|
63
|
+
*** Update File: sample.txt
|
|
64
|
+
@@ exact-top
|
|
65
|
+
-exact-old
|
|
66
|
+
+exact-new
|
|
67
|
+
exact-bottom
|
|
68
|
+
*** End Patch`;
|
|
69
|
+
const output = {
|
|
70
|
+
args: {
|
|
71
|
+
patchText: `cat <<'PATCH'
|
|
72
|
+
${cleanPatchText}
|
|
73
|
+
PATCH`,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
await hook['tool.execute.before'](
|
|
78
|
+
{ tool: 'apply_patch', directory: root },
|
|
79
|
+
output,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(output.args.patchText).toBe(cleanPatchText);
|
|
83
|
+
|
|
84
|
+
const changes = await preparePatchChanges(
|
|
85
|
+
root,
|
|
86
|
+
output.args.patchText as string,
|
|
87
|
+
DEFAULT_OPTIONS,
|
|
88
|
+
);
|
|
89
|
+
await applyPreparedChanges(changes);
|
|
90
|
+
expect(await readFile(path.join(root, 'sample.txt'), 'utf-8')).toBe(
|
|
91
|
+
'line-01\nexact-top\nexact-new\nexact-bottom\nline-05\n',
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('normalizes absolute paths inside root before native execution', async () => {
|
|
96
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
97
|
+
const absolutePath = path.join(root, 'sample.txt');
|
|
98
|
+
await writeFixture(root, 'sample.txt', 'alpha\nbeta\n');
|
|
99
|
+
const hook = createHook();
|
|
100
|
+
const patchText = `*** Begin Patch
|
|
101
|
+
*** Update File: ${absolutePath}
|
|
102
|
+
@@
|
|
103
|
+
-alpha
|
|
104
|
+
+omega
|
|
105
|
+
*** End Patch`;
|
|
106
|
+
const output = { args: { patchText } };
|
|
107
|
+
|
|
108
|
+
await hook['tool.execute.before'](
|
|
109
|
+
{ tool: 'apply_patch', directory: root },
|
|
110
|
+
output,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(parsePatch(output.args.patchText as string).hunks[0]).toMatchObject({
|
|
114
|
+
type: 'update',
|
|
115
|
+
path: 'sample.txt',
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('passes through an absolute target outside root/worktree before native execution', async () => {
|
|
120
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
121
|
+
const outsideDir = await createTempDir('apply-patch-hook-outside-');
|
|
122
|
+
const outsidePath = path.join(outsideDir, 'outside.txt');
|
|
123
|
+
await writeFile(outsidePath, 'outside\n', 'utf-8');
|
|
124
|
+
const hook = createApplyPatchHook({
|
|
125
|
+
client: {} as never,
|
|
126
|
+
directory: root,
|
|
127
|
+
worktree: root,
|
|
128
|
+
} as never);
|
|
129
|
+
const patchText = `*** Begin Patch
|
|
130
|
+
*** Update File: ${outsidePath}
|
|
131
|
+
@@
|
|
132
|
+
-outside
|
|
133
|
+
+changed
|
|
134
|
+
*** End Patch`;
|
|
135
|
+
const output = { args: { patchText } };
|
|
136
|
+
|
|
137
|
+
await expect(
|
|
138
|
+
hook['tool.execute.before'](
|
|
139
|
+
{ tool: 'apply_patch', directory: root },
|
|
140
|
+
output,
|
|
141
|
+
),
|
|
142
|
+
).resolves.toBeUndefined();
|
|
143
|
+
|
|
144
|
+
expect(output.args.patchText).toBe(patchText);
|
|
145
|
+
expect(await readFile(outsidePath, 'utf-8')).toBe('outside\n');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('passes through mixed stale inside and absolute outside patch without partial rewrite', async () => {
|
|
149
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
150
|
+
const outsideDir = await createTempDir('apply-patch-hook-outside-');
|
|
151
|
+
const outsidePath = path.join(outsideDir, 'outside.txt');
|
|
152
|
+
await writeFixture(root, 'sample.txt', 'prefix\nstale-value\nsuffix\n');
|
|
153
|
+
await writeFile(outsidePath, 'outside\n', 'utf-8');
|
|
154
|
+
const hook = createApplyPatchHook({
|
|
155
|
+
client: {} as never,
|
|
156
|
+
directory: root,
|
|
157
|
+
worktree: root,
|
|
158
|
+
} as never);
|
|
159
|
+
const patchText = `*** Begin Patch
|
|
160
|
+
*** Update File: sample.txt
|
|
161
|
+
@@
|
|
162
|
+
prefix
|
|
163
|
+
-old-value
|
|
164
|
+
+new-value
|
|
165
|
+
suffix
|
|
166
|
+
*** Update File: ${outsidePath}
|
|
167
|
+
@@
|
|
168
|
+
-outside
|
|
169
|
+
+changed
|
|
170
|
+
*** End Patch`;
|
|
171
|
+
const output = { args: { patchText } };
|
|
172
|
+
|
|
173
|
+
await expect(
|
|
174
|
+
hook['tool.execute.before'](
|
|
175
|
+
{ tool: 'apply_patch', directory: root },
|
|
176
|
+
output,
|
|
177
|
+
),
|
|
178
|
+
).resolves.toBeUndefined();
|
|
179
|
+
|
|
180
|
+
expect(output.args.patchText).toBe(patchText);
|
|
181
|
+
expect(await readFile(path.join(root, 'sample.txt'), 'utf-8')).toBe(
|
|
182
|
+
'prefix\nstale-value\nsuffix\n',
|
|
183
|
+
);
|
|
184
|
+
expect(await readFile(outsidePath, 'utf-8')).toBe('outside\n');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('rewrites a stale prefix patch and remains applicable', async () => {
|
|
188
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
189
|
+
await writeFixture(
|
|
190
|
+
root,
|
|
191
|
+
'sample.txt',
|
|
192
|
+
'top\nA\nB-stale\nC\nD\nE\nbottom\n',
|
|
193
|
+
);
|
|
194
|
+
const hook = createHook();
|
|
195
|
+
const patchText = `*** Begin Patch
|
|
196
|
+
*** Update File: sample.txt
|
|
197
|
+
@@ top
|
|
198
|
+
A
|
|
199
|
+
-B
|
|
200
|
+
-C
|
|
201
|
+
-D
|
|
202
|
+
-E
|
|
203
|
+
+B
|
|
204
|
+
+C
|
|
205
|
+
+D
|
|
206
|
+
+X
|
|
207
|
+
*** End Patch`;
|
|
208
|
+
const output = { args: { patchText } };
|
|
209
|
+
|
|
210
|
+
await hook['tool.execute.before'](
|
|
211
|
+
{ tool: 'apply_patch', directory: root },
|
|
212
|
+
output,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const rewritten = parsePatch(output.args.patchText as string).hunks[0];
|
|
216
|
+
expect(rewritten.type).toBe('update');
|
|
217
|
+
expect(
|
|
218
|
+
rewritten.type === 'update' && rewritten.chunks[0]?.old_lines,
|
|
219
|
+
).toEqual(['A', 'B-stale', 'C', 'D', 'E']);
|
|
220
|
+
|
|
221
|
+
const changes = await preparePatchChanges(
|
|
222
|
+
root,
|
|
223
|
+
output.args.patchText as string,
|
|
224
|
+
DEFAULT_OPTIONS,
|
|
225
|
+
);
|
|
226
|
+
await applyPreparedChanges(changes);
|
|
227
|
+
expect(await readFile(path.join(root, 'sample.txt'), 'utf-8')).toBe(
|
|
228
|
+
'top\nA\nB\nC\nD\nX\nbottom\n',
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('does not alter new_lines during rewrite', async () => {
|
|
233
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
234
|
+
await writeFixture(
|
|
235
|
+
root,
|
|
236
|
+
'sample.txt',
|
|
237
|
+
'top\nprefix\nstale-value\nsuffix\nbottom\n',
|
|
238
|
+
);
|
|
239
|
+
const hook = createHook();
|
|
240
|
+
const patchText = `*** Begin Patch
|
|
241
|
+
*** Update File: sample.txt
|
|
242
|
+
@@ top
|
|
243
|
+
prefix
|
|
244
|
+
-old-value
|
|
245
|
+
+ \tverbatim "" Ω
|
|
246
|
+
suffix
|
|
247
|
+
*** End Patch`;
|
|
248
|
+
const expected = parsePatch(patchText).hunks[0];
|
|
249
|
+
const output = { args: { patchText } };
|
|
250
|
+
|
|
251
|
+
await hook['tool.execute.before'](
|
|
252
|
+
{ tool: 'apply_patch', directory: root },
|
|
253
|
+
output,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const rewritten = parsePatch(output.args.patchText as string).hunks[0];
|
|
257
|
+
expect(expected.type).toBe('update');
|
|
258
|
+
expect(rewritten.type).toBe('update');
|
|
259
|
+
expect(
|
|
260
|
+
expected.type === 'update' && rewritten.type === 'update'
|
|
261
|
+
? rewritten.chunks[0]?.new_lines
|
|
262
|
+
: undefined,
|
|
263
|
+
).toEqual(expected.type === 'update' ? expected.chunks[0]?.new_lines : []);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('rewrites a unicode-only stale patch and remains applicable', async () => {
|
|
267
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
268
|
+
await writeFixture(root, 'sample.txt', 'const title = “Hola”;\n');
|
|
269
|
+
const hook = createHook();
|
|
270
|
+
const patchText = `*** Begin Patch
|
|
271
|
+
*** Update File: sample.txt
|
|
272
|
+
@@
|
|
273
|
+
-const title = "Hola";
|
|
274
|
+
+const title = "Hola mundo";
|
|
275
|
+
*** End Patch`;
|
|
276
|
+
const output = { args: { patchText } };
|
|
277
|
+
|
|
278
|
+
await hook['tool.execute.before'](
|
|
279
|
+
{ tool: 'apply_patch', directory: root },
|
|
280
|
+
output,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const rewritten = parsePatch(output.args.patchText as string).hunks[0];
|
|
284
|
+
expect(rewritten.type).toBe('update');
|
|
285
|
+
expect(
|
|
286
|
+
rewritten.type === 'update' ? rewritten.chunks[0]?.old_lines : undefined,
|
|
287
|
+
).toEqual(['const title = “Hola”;']);
|
|
288
|
+
|
|
289
|
+
const changes = await preparePatchChanges(
|
|
290
|
+
root,
|
|
291
|
+
output.args.patchText as string,
|
|
292
|
+
DEFAULT_OPTIONS,
|
|
293
|
+
);
|
|
294
|
+
await applyPreparedChanges(changes);
|
|
295
|
+
expect(await readFile(path.join(root, 'sample.txt'), 'utf-8')).toBe(
|
|
296
|
+
'const title = "Hola mundo";\n',
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('rewrites a trim-end stale patch and remains applicable', async () => {
|
|
301
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
302
|
+
await writeFixture(root, 'sample.txt', 'alpha \n');
|
|
303
|
+
const hook = createHook();
|
|
304
|
+
const patchText = `*** Begin Patch
|
|
305
|
+
*** Update File: sample.txt
|
|
306
|
+
@@
|
|
307
|
+
-alpha
|
|
308
|
+
+omega
|
|
309
|
+
*** End Patch`;
|
|
310
|
+
const output = { args: { patchText } };
|
|
311
|
+
|
|
312
|
+
await hook['tool.execute.before'](
|
|
313
|
+
{ tool: 'apply_patch', directory: root },
|
|
314
|
+
output,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const rewritten = parsePatch(output.args.patchText as string).hunks[0];
|
|
318
|
+
expect(rewritten.type).toBe('update');
|
|
319
|
+
expect(
|
|
320
|
+
rewritten.type === 'update' ? rewritten.chunks[0]?.old_lines : undefined,
|
|
321
|
+
).toEqual(['alpha ']);
|
|
322
|
+
|
|
323
|
+
const changes = await preparePatchChanges(
|
|
324
|
+
root,
|
|
325
|
+
output.args.patchText as string,
|
|
326
|
+
DEFAULT_OPTIONS,
|
|
327
|
+
);
|
|
328
|
+
await applyPreparedChanges(changes);
|
|
329
|
+
expect(await readFile(path.join(root, 'sample.txt'), 'utf-8')).toBe(
|
|
330
|
+
'omega\n',
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('blocks a trim-only stale patch as verification', async () => {
|
|
335
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
336
|
+
await writeFixture(root, 'sample.txt', ' alpha \n');
|
|
337
|
+
const hook = createHook();
|
|
338
|
+
const patchText = `*** Begin Patch
|
|
339
|
+
*** Update File: sample.txt
|
|
340
|
+
@@
|
|
341
|
+
-alpha
|
|
342
|
+
+omega
|
|
343
|
+
*** End Patch`;
|
|
344
|
+
const output = { args: { patchText } };
|
|
345
|
+
|
|
346
|
+
await expect(
|
|
347
|
+
hook['tool.execute.before'](
|
|
348
|
+
{ tool: 'apply_patch', directory: root },
|
|
349
|
+
output,
|
|
350
|
+
),
|
|
351
|
+
).rejects.toThrow(
|
|
352
|
+
'apply_patch verification failed: Failed to find expected lines',
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
expect(output.args.patchText).toBe(patchText);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('blocks a malformed @@ at runtime before native execution', async () => {
|
|
359
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
360
|
+
await writeFixture(root, 'sample.txt', 'alpha\nbeta\n');
|
|
361
|
+
const hook = createHook();
|
|
362
|
+
const patchText = `*** Begin Patch
|
|
363
|
+
*** Update File: sample.txt
|
|
364
|
+
@@
|
|
365
|
+
alpha
|
|
366
|
+
garbage
|
|
367
|
+
-beta
|
|
368
|
+
+BETA
|
|
369
|
+
*** End Patch`;
|
|
370
|
+
const output = { args: { patchText } };
|
|
371
|
+
|
|
372
|
+
await expect(
|
|
373
|
+
hook['tool.execute.before'](
|
|
374
|
+
{ tool: 'apply_patch', directory: root },
|
|
375
|
+
output,
|
|
376
|
+
),
|
|
377
|
+
).rejects.toThrow(
|
|
378
|
+
'apply_patch validation failed: Invalid patch format: unexpected line in patch chunk: garbage',
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
expect(output.args.patchText).toBe(patchText);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('blocks a malformed Add File at runtime before native execution', async () => {
|
|
385
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
386
|
+
const hook = createHook();
|
|
387
|
+
const patchText = `*** Begin Patch
|
|
388
|
+
*** Add File: added.txt
|
|
389
|
+
+fresh
|
|
390
|
+
garbage
|
|
391
|
+
*** End Patch`;
|
|
392
|
+
const output = { args: { patchText } };
|
|
393
|
+
|
|
394
|
+
await expect(
|
|
395
|
+
hook['tool.execute.before'](
|
|
396
|
+
{ tool: 'apply_patch', directory: root },
|
|
397
|
+
output,
|
|
398
|
+
),
|
|
399
|
+
).rejects.toThrow(
|
|
400
|
+
'apply_patch validation failed: Invalid patch format: unexpected line in Add File body: garbage',
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
expect(output.args.patchText).toBe(patchText);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const testGuardError = platform() === 'win32' ? test.skip : test;
|
|
407
|
+
testGuardError('blocks internal guard errors before native execution', async () => {
|
|
408
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
409
|
+
const lockedDir = path.join(root, 'locked');
|
|
410
|
+
await mkdir(lockedDir, { recursive: true });
|
|
411
|
+
await chmod(lockedDir, 0o000);
|
|
412
|
+
const hook = createHook();
|
|
413
|
+
const patchText = `*** Begin Patch
|
|
414
|
+
*** Add File: locked/child.txt
|
|
415
|
+
+fresh
|
|
416
|
+
*** End Patch`;
|
|
417
|
+
const output = { args: { patchText } };
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
await expect(
|
|
421
|
+
hook['tool.execute.before'](
|
|
422
|
+
{ tool: 'apply_patch', directory: root },
|
|
423
|
+
output,
|
|
424
|
+
),
|
|
425
|
+
).rejects.toThrow('apply_patch internal error:');
|
|
426
|
+
|
|
427
|
+
expect(output.args.patchText).toBe(patchText);
|
|
428
|
+
} finally {
|
|
429
|
+
await chmod(lockedDir, 0o755);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test('blocks a dangerous indented case as verification', async () => {
|
|
434
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
435
|
+
await writeFixture(
|
|
436
|
+
root,
|
|
437
|
+
'sample.yml',
|
|
438
|
+
'root:\n child:\n enabled: false\nnext: true\n',
|
|
439
|
+
);
|
|
440
|
+
const hook = createHook();
|
|
441
|
+
const patchText = `*** Begin Patch
|
|
442
|
+
*** Update File: sample.yml
|
|
443
|
+
@@
|
|
444
|
+
-enabled: false
|
|
445
|
+
+enabled: true
|
|
446
|
+
*** End Patch`;
|
|
447
|
+
const output = { args: { patchText } };
|
|
448
|
+
|
|
449
|
+
await expect(
|
|
450
|
+
hook['tool.execute.before'](
|
|
451
|
+
{ tool: 'apply_patch', directory: root },
|
|
452
|
+
output,
|
|
453
|
+
),
|
|
454
|
+
).rejects.toThrow(
|
|
455
|
+
'apply_patch verification failed: Failed to find expected lines',
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
expect(output.args.patchText).toBe(patchText);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test('rewrites anchored insertion to avoid native EOF handling', async () => {
|
|
462
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
463
|
+
await writeFixture(
|
|
464
|
+
root,
|
|
465
|
+
'sample.txt',
|
|
466
|
+
'top\nanchor-insert\nafter-anchor\nend\n',
|
|
467
|
+
);
|
|
468
|
+
const hook = createHook();
|
|
469
|
+
const patchText = `*** Begin Patch
|
|
470
|
+
*** Update File: sample.txt
|
|
471
|
+
@@ anchor-insert
|
|
472
|
+
+middle-inserted
|
|
473
|
+
*** End Patch`;
|
|
474
|
+
const output = { args: { patchText } };
|
|
475
|
+
|
|
476
|
+
await hook['tool.execute.before'](
|
|
477
|
+
{ tool: 'apply_patch', directory: root },
|
|
478
|
+
output,
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
const changes = await preparePatchChanges(
|
|
482
|
+
root,
|
|
483
|
+
output.args.patchText as string,
|
|
484
|
+
DEFAULT_OPTIONS,
|
|
485
|
+
);
|
|
486
|
+
await applyPreparedChanges(changes);
|
|
487
|
+
expect(await readFile(path.join(root, 'sample.txt'), 'utf-8')).toBe(
|
|
488
|
+
'top\nanchor-insert\nmiddle-inserted\nafter-anchor\nend\n',
|
|
489
|
+
);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test('blocks a pure insertion when the anchor is missing', async () => {
|
|
493
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
494
|
+
await writeFixture(root, 'sample.txt', 'top\nafter-anchor\nend\n');
|
|
495
|
+
const hook = createHook();
|
|
496
|
+
const patchText = `*** Begin Patch
|
|
497
|
+
*** Update File: sample.txt
|
|
498
|
+
@@ anchor-insert
|
|
499
|
+
+middle-inserted
|
|
500
|
+
*** End Patch`;
|
|
501
|
+
const output = { args: { patchText } };
|
|
502
|
+
|
|
503
|
+
await expect(
|
|
504
|
+
hook['tool.execute.before'](
|
|
505
|
+
{ tool: 'apply_patch', directory: root },
|
|
506
|
+
output,
|
|
507
|
+
),
|
|
508
|
+
).rejects.toThrow(
|
|
509
|
+
'apply_patch verification failed: Failed to find insertion anchor',
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
expect(output.args.patchText).toBe(patchText);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test('blocks a pure insertion when the anchor is ambiguous', async () => {
|
|
516
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
517
|
+
await writeFixture(
|
|
518
|
+
root,
|
|
519
|
+
'sample.txt',
|
|
520
|
+
'top\nanchor-insert\nafter-first\nsplit\nanchor-insert\nafter-second\nend\n',
|
|
521
|
+
);
|
|
522
|
+
const hook = createHook();
|
|
523
|
+
const patchText = `*** Begin Patch
|
|
524
|
+
*** Update File: sample.txt
|
|
525
|
+
@@ anchor-insert
|
|
526
|
+
+middle-inserted
|
|
527
|
+
*** End Patch`;
|
|
528
|
+
const output = { args: { patchText } };
|
|
529
|
+
|
|
530
|
+
await expect(
|
|
531
|
+
hook['tool.execute.before'](
|
|
532
|
+
{ tool: 'apply_patch', directory: root },
|
|
533
|
+
output,
|
|
534
|
+
),
|
|
535
|
+
).rejects.toThrow(
|
|
536
|
+
'apply_patch verification failed: Insertion anchor was ambiguous',
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
expect(output.args.patchText).toBe(patchText);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test('blocks real patch ambiguity before native execution', async () => {
|
|
543
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
544
|
+
await writeFixture(
|
|
545
|
+
root,
|
|
546
|
+
'sample.txt',
|
|
547
|
+
'left\nstale-one\nright\nseparator\nleft\nstale-two\nright\n',
|
|
548
|
+
);
|
|
549
|
+
const hook = createHook();
|
|
550
|
+
const patchText = `*** Begin Patch
|
|
551
|
+
*** Update File: sample.txt
|
|
552
|
+
@@
|
|
553
|
+
left
|
|
554
|
+
-old
|
|
555
|
+
+new
|
|
556
|
+
right
|
|
557
|
+
*** End Patch`;
|
|
558
|
+
const output = { args: { patchText } };
|
|
559
|
+
|
|
560
|
+
await expect(
|
|
561
|
+
hook['tool.execute.before'](
|
|
562
|
+
{ tool: 'apply_patch', directory: root },
|
|
563
|
+
output,
|
|
564
|
+
),
|
|
565
|
+
).rejects.toThrow('apply_patch verification failed:');
|
|
566
|
+
|
|
567
|
+
expect(output.args.patchText).toBe(patchText);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test('rewrites only the update hunk in a patch with add + update', async () => {
|
|
571
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
572
|
+
await writeFixture(
|
|
573
|
+
root,
|
|
574
|
+
'sample.txt',
|
|
575
|
+
'top\nprefix\nstale-value\nsuffix\n',
|
|
576
|
+
);
|
|
577
|
+
const hook = createHook();
|
|
578
|
+
const patchText = `*** Begin Patch
|
|
579
|
+
*** Add File: added.txt
|
|
580
|
+
+fresh
|
|
581
|
+
*** Update File: sample.txt
|
|
582
|
+
@@ top
|
|
583
|
+
prefix
|
|
584
|
+
-old-value
|
|
585
|
+
+new-value
|
|
586
|
+
suffix
|
|
587
|
+
*** End Patch`;
|
|
588
|
+
const output = { args: { patchText } };
|
|
589
|
+
|
|
590
|
+
await hook['tool.execute.before'](
|
|
591
|
+
{ tool: 'apply_patch', directory: root },
|
|
592
|
+
output,
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
const rewritten = parsePatch(output.args.patchText as string);
|
|
596
|
+
expect(rewritten.hunks[0]).toEqual({
|
|
597
|
+
type: 'add',
|
|
598
|
+
path: 'added.txt',
|
|
599
|
+
contents: 'fresh',
|
|
600
|
+
});
|
|
601
|
+
expect(rewritten.hunks[1]).toEqual({
|
|
602
|
+
type: 'update',
|
|
603
|
+
path: 'sample.txt',
|
|
604
|
+
chunks: [
|
|
605
|
+
{
|
|
606
|
+
old_lines: ['prefix', 'stale-value', 'suffix'],
|
|
607
|
+
new_lines: ['prefix', 'new-value', 'suffix'],
|
|
608
|
+
change_context: 'top',
|
|
609
|
+
is_end_of_file: undefined,
|
|
610
|
+
},
|
|
611
|
+
],
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
const changes = await preparePatchChanges(
|
|
615
|
+
root,
|
|
616
|
+
output.args.patchText as string,
|
|
617
|
+
DEFAULT_OPTIONS,
|
|
618
|
+
);
|
|
619
|
+
await applyPreparedChanges(changes);
|
|
620
|
+
expect(await readFile(path.join(root, 'sample.txt'), 'utf-8')).toBe(
|
|
621
|
+
'top\nprefix\nnew-value\nsuffix\n',
|
|
622
|
+
);
|
|
623
|
+
expect(await readFile(path.join(root, 'added.txt'), 'utf-8')).toBe(
|
|
624
|
+
'fresh\n',
|
|
625
|
+
);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test('passes through sibling-directory targets outside root/worktree before native execution', async () => {
|
|
629
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
630
|
+
const outside = path.join(path.dirname(root), 'outside.txt');
|
|
631
|
+
await writeFile(outside, 'outside\n', 'utf-8');
|
|
632
|
+
const hook = createHook();
|
|
633
|
+
const patchText = `*** Begin Patch
|
|
634
|
+
*** Update File: ../outside.txt
|
|
635
|
+
@@
|
|
636
|
+
-outside
|
|
637
|
+
+changed
|
|
638
|
+
*** End Patch`;
|
|
639
|
+
const output = { args: { patchText } };
|
|
640
|
+
|
|
641
|
+
await expect(
|
|
642
|
+
hook['tool.execute.before'](
|
|
643
|
+
{ tool: 'apply_patch', directory: root },
|
|
644
|
+
output,
|
|
645
|
+
),
|
|
646
|
+
).resolves.toBeUndefined();
|
|
647
|
+
|
|
648
|
+
expect(output.args.patchText).toBe(patchText);
|
|
649
|
+
expect(await readFile(outside, 'utf-8')).toBe('outside\n');
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test('normalizes an absolute path inside worktree even when it is outside root', async () => {
|
|
653
|
+
const worktree = await createTempDir('apply-patch-worktree-');
|
|
654
|
+
const root = path.join(worktree, 'subdir');
|
|
655
|
+
await mkdir(root, { recursive: true });
|
|
656
|
+
const siblingPath = path.join(worktree, 'shared.txt');
|
|
657
|
+
const hook = createApplyPatchHook({
|
|
658
|
+
client: {} as never,
|
|
659
|
+
directory: root,
|
|
660
|
+
worktree,
|
|
661
|
+
} as never);
|
|
662
|
+
const patchText = `*** Begin Patch
|
|
663
|
+
*** Add File: ${siblingPath}
|
|
664
|
+
+fresh
|
|
665
|
+
*** End Patch`;
|
|
666
|
+
const output = { args: { patchText } };
|
|
667
|
+
|
|
668
|
+
await expect(
|
|
669
|
+
hook['tool.execute.before'](
|
|
670
|
+
{ tool: 'apply_patch', directory: root },
|
|
671
|
+
output,
|
|
672
|
+
),
|
|
673
|
+
).resolves.toBeUndefined();
|
|
674
|
+
|
|
675
|
+
expect(parsePatch(output.args.patchText as string).hunks[0]).toMatchObject({
|
|
676
|
+
type: 'add',
|
|
677
|
+
path: '../shared.txt',
|
|
678
|
+
contents: 'fresh',
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
test('passes through mixed patches with outside paths without partial rewrite', async () => {
|
|
683
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
684
|
+
const outsideDir = await createTempDir('apply-patch-hook-outside-');
|
|
685
|
+
await writeFixture(root, 'sample.txt', 'prefix\nstale-value\nsuffix\n');
|
|
686
|
+
await writeFixture(outsideDir, 'outside.txt', 'legacy\n');
|
|
687
|
+
const hook = createHook();
|
|
688
|
+
const outsideAdded = path.join(path.dirname(root), 'outside-added.txt');
|
|
689
|
+
const patchText = `*** Begin Patch
|
|
690
|
+
*** Add File: ../outside-added.txt
|
|
691
|
+
+fresh
|
|
692
|
+
*** Update File: sample.txt
|
|
693
|
+
@@
|
|
694
|
+
prefix
|
|
695
|
+
-old-value
|
|
696
|
+
+new-value
|
|
697
|
+
suffix
|
|
698
|
+
*** Delete File: ../${path.basename(outsideDir)}/outside.txt
|
|
699
|
+
*** End Patch`;
|
|
700
|
+
const output = { args: { patchText } };
|
|
701
|
+
|
|
702
|
+
await expect(
|
|
703
|
+
hook['tool.execute.before'](
|
|
704
|
+
{ tool: 'apply_patch', directory: root },
|
|
705
|
+
output,
|
|
706
|
+
),
|
|
707
|
+
).resolves.toBeUndefined();
|
|
708
|
+
|
|
709
|
+
expect(output.args.patchText).toBe(patchText);
|
|
710
|
+
expect(await readFile(path.join(root, 'sample.txt'), 'utf-8')).toBe(
|
|
711
|
+
'prefix\nstale-value\nsuffix\n',
|
|
712
|
+
);
|
|
713
|
+
expect(await stat(outsideAdded).catch(() => null)).toBeNull();
|
|
714
|
+
expect(await readFile(path.join(outsideDir, 'outside.txt'), 'utf-8')).toBe(
|
|
715
|
+
'legacy\n',
|
|
716
|
+
);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
test('keeps normal behavior for patches entirely inside root/worktree', async () => {
|
|
720
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
721
|
+
await writeFixture(root, 'sample.txt', 'alpha\nbeta\n');
|
|
722
|
+
const hook = createHook();
|
|
723
|
+
const patchText = `*** Begin Patch
|
|
724
|
+
*** Update File: sample.txt
|
|
725
|
+
@@
|
|
726
|
+
-alpha
|
|
727
|
+
+omega
|
|
728
|
+
beta
|
|
729
|
+
*** End Patch`;
|
|
730
|
+
const output = { args: { patchText } };
|
|
731
|
+
|
|
732
|
+
await expect(
|
|
733
|
+
hook['tool.execute.before'](
|
|
734
|
+
{ tool: 'apply_patch', directory: root },
|
|
735
|
+
output,
|
|
736
|
+
),
|
|
737
|
+
).resolves.toBeUndefined();
|
|
738
|
+
|
|
739
|
+
expect(output.args.patchText).toBe(patchText);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
test('does not expose the tool.execute.after hook', () => {
|
|
743
|
+
const hook = createHook() as Record<string, unknown>;
|
|
744
|
+
|
|
745
|
+
expect(hook['tool.execute.after']).toBeUndefined();
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
test('does not alter an exact patch', async () => {
|
|
749
|
+
const root = await createTempDir('apply-patch-hook-');
|
|
750
|
+
await writeFixture(root, 'sample.txt', 'alpha\nbeta\n');
|
|
751
|
+
const hook = createHook();
|
|
752
|
+
const patchText = `*** Begin Patch
|
|
753
|
+
*** Update File: sample.txt
|
|
754
|
+
@@
|
|
755
|
+
-alpha
|
|
756
|
+
+omega
|
|
757
|
+
beta
|
|
758
|
+
*** End Patch`;
|
|
759
|
+
const output = { args: { patchText } };
|
|
760
|
+
|
|
761
|
+
await hook['tool.execute.before'](
|
|
762
|
+
{ tool: 'apply_patch', directory: root },
|
|
763
|
+
output,
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
expect(output.args.patchText).toBe(patchText);
|
|
767
|
+
});
|
|
768
|
+
});
|