Update test script in package.json and enhance patch_files.js with chroot support and improved path handling for file operations.
This commit is contained in:
@@ -49,7 +49,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node cli.js",
|
"start": "node cli.js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "node tests/run-tests.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
626
tests/run-tests.js
Normal file
626
tests/run-tests.js
Normal file
@@ -0,0 +1,626 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { promises as fs } from 'node:fs';
|
||||||
|
import fsSync from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { run as runPatch } from '../tools/patch_files.js';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const repoRoot = path.resolve(__dirname, '..');
|
||||||
|
const sandboxRoot = path.resolve(repoRoot, 'tmp', 'patch-tests');
|
||||||
|
|
||||||
|
async function rimraf(dir) {
|
||||||
|
await fs.rm(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureDir(dir) {
|
||||||
|
await fs.mkdir(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeFiles(baseDir, filesMap) {
|
||||||
|
for (const [rel, content] of Object.entries(filesMap || {})) {
|
||||||
|
const filePath = path.resolve(baseDir, rel);
|
||||||
|
await ensureDir(path.dirname(filePath));
|
||||||
|
await fs.writeFile(filePath, content, 'utf8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readFileIfExists(filePath) {
|
||||||
|
try {
|
||||||
|
return await fs.readFile(filePath, 'utf8');
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPatch(lines, useCRLF = false) {
|
||||||
|
const eol = useCRLF ? '\r\n' : '\n';
|
||||||
|
return lines.join(eol);
|
||||||
|
}
|
||||||
|
|
||||||
|
function begin() { return '*** Begin Patch'; }
|
||||||
|
function end() { return '*** End Patch'; }
|
||||||
|
function addFile(p) { return `*** Add File: ${p}`; }
|
||||||
|
function updateFile(p) { return `*** Update File: ${p}`; }
|
||||||
|
function moveTo(p) { return `*** Move to: ${p}`; }
|
||||||
|
function endOfFile() { return '*** End of File'; }
|
||||||
|
|
||||||
|
function k(line) { return ` ${line}`; }
|
||||||
|
function d(line) { return `-${line}`; }
|
||||||
|
function a(line) { return `+${line}`; }
|
||||||
|
function at(atLine = '') { return atLine ? `@@ ${atLine}` : '@@'; }
|
||||||
|
|
||||||
|
function slugify(name) {
|
||||||
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectEqual(actual, expected, label) {
|
||||||
|
if (actual !== expected) {
|
||||||
|
const ellipsize = (s) => (s.length > 400 ? s.slice(0, 400) + '…' : s);
|
||||||
|
throw new Error(`${label} mismatch.\nExpected:\n${ellipsize(expected ?? '<<undefined>>')}\nActual:\n${ellipsize(actual ?? '<<undefined>>')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCase(index, testCase) {
|
||||||
|
const idx = String(index + 1).padStart(2, '0');
|
||||||
|
const caseDir = path.resolve(sandboxRoot, `${idx}-${slugify(testCase.name)}`);
|
||||||
|
await rimraf(caseDir);
|
||||||
|
await ensureDir(caseDir);
|
||||||
|
|
||||||
|
// Setup initial files
|
||||||
|
await writeFiles(caseDir, typeof testCase.before === 'function' ? await testCase.before({ dir: caseDir }) : (testCase.before || {}));
|
||||||
|
|
||||||
|
const patchText = await testCase.patch({ dir: caseDir });
|
||||||
|
|
||||||
|
let threw = false;
|
||||||
|
let errorMessage = '';
|
||||||
|
try {
|
||||||
|
const result = await runPatch({ patch: patchText });
|
||||||
|
if (testCase.expect?.resultEquals) {
|
||||||
|
expectEqual(result, testCase.expect.resultEquals, 'Tool result');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
threw = true;
|
||||||
|
errorMessage = err?.message || String(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testCase.expect?.error) {
|
||||||
|
if (!threw) {
|
||||||
|
throw new Error(`Expected error matching ${testCase.expect.error} but call succeeded`);
|
||||||
|
}
|
||||||
|
const re = typeof testCase.expect.error === 'string' ? new RegExp(testCase.expect.error.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) : testCase.expect.error;
|
||||||
|
if (!re.test(errorMessage)) {
|
||||||
|
throw new Error(`Error did not match.\nExpected: ${re}\nActual: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
} else if (threw) {
|
||||||
|
throw new Error(`Unexpected error: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testCase.expect?.files) {
|
||||||
|
for (const [rel, expectedContent] of Object.entries(testCase.expect.files)) {
|
||||||
|
const filePath = path.resolve(caseDir, rel);
|
||||||
|
const content = await readFileIfExists(filePath);
|
||||||
|
if (content === undefined) {
|
||||||
|
throw new Error(`Expected file missing: ${rel}`);
|
||||||
|
}
|
||||||
|
expectEqual(content, expectedContent, `Content of ${rel}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testCase.expect?.exists) {
|
||||||
|
for (const rel of testCase.expect.exists) {
|
||||||
|
const filePath = path.resolve(caseDir, rel);
|
||||||
|
if (!fsSync.existsSync(filePath)) {
|
||||||
|
throw new Error(`Expected path to exist: ${rel}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testCase.expect?.notExists) {
|
||||||
|
for (const rel of testCase.expect.notExists) {
|
||||||
|
const filePath = path.resolve(caseDir, rel);
|
||||||
|
if (fsSync.existsSync(filePath)) {
|
||||||
|
throw new Error(`Expected path NOT to exist: ${rel}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cases() {
|
||||||
|
const list = [];
|
||||||
|
|
||||||
|
// 1. Add simple file
|
||||||
|
list.push({
|
||||||
|
name: 'add simple file',
|
||||||
|
before: {},
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
addFile(path.resolve(dir, 'file1.txt')),
|
||||||
|
a('hello'),
|
||||||
|
a('world'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: {
|
||||||
|
files: { 'file1.txt': 'hello\nworld' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Add nested directories
|
||||||
|
list.push({
|
||||||
|
name: 'add nested directories',
|
||||||
|
before: {},
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
addFile(path.resolve(dir, 'a/b/c.txt')),
|
||||||
|
a('alpha'),
|
||||||
|
a('beta'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: {
|
||||||
|
files: { 'a/b/c.txt': 'alpha\nbeta' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Update simple replacement
|
||||||
|
list.push({
|
||||||
|
name: 'update simple replacement',
|
||||||
|
before: { 'greet.txt': 'line1\nreplace me\nline3' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'greet.txt')),
|
||||||
|
k('line1'),
|
||||||
|
d('replace me'),
|
||||||
|
a('replaced'),
|
||||||
|
k('line3'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: {
|
||||||
|
files: { 'greet.txt': 'line1\nreplaced\nline3' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Update multiple chunks
|
||||||
|
list.push({
|
||||||
|
name: 'update multiple chunks',
|
||||||
|
before: { 'multi.txt': 'l1\nl2\nl3\nl4\nl5\nl6\nl7' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'multi.txt')),
|
||||||
|
k('l1'),
|
||||||
|
k('l2'),
|
||||||
|
d('l3'),
|
||||||
|
a('L3'),
|
||||||
|
k('l4'),
|
||||||
|
k('l5'),
|
||||||
|
d('l6'),
|
||||||
|
a('L6'),
|
||||||
|
k('l7'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: {
|
||||||
|
files: { 'multi.txt': 'l1\nl2\nL3\nl4\nl5\nL6\nl7' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Insert at beginning
|
||||||
|
list.push({
|
||||||
|
name: 'insert at beginning',
|
||||||
|
before: { 'begin.txt': 'B1\nB2' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'begin.txt')),
|
||||||
|
a('A0'),
|
||||||
|
k('B1'),
|
||||||
|
k('B2'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { files: { 'begin.txt': 'A0\nB1\nB2' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. Insert at end with EOF marker
|
||||||
|
list.push({
|
||||||
|
name: 'insert at end with EOF',
|
||||||
|
before: { 'end.txt': 'E1\nE2' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'end.txt')),
|
||||||
|
k('E1'),
|
||||||
|
k('E2'),
|
||||||
|
a('E3'),
|
||||||
|
endOfFile(),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { files: { 'end.txt': 'E1\nE2\nE3' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. Delete a line
|
||||||
|
list.push({
|
||||||
|
name: 'delete a line',
|
||||||
|
before: { 'delete.txt': 'X\nY\nZ' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'delete.txt')),
|
||||||
|
k('X'),
|
||||||
|
d('Y'),
|
||||||
|
k('Z'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { files: { 'delete.txt': 'X\nZ' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 8. Whitespace rstrip match
|
||||||
|
list.push({
|
||||||
|
name: 'rstrip whitespace match',
|
||||||
|
before: { 'ws.txt': 'foo \nbar' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'ws.txt')),
|
||||||
|
k('foo'), // matches 'foo ' via rstrip
|
||||||
|
k('bar'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { files: { 'ws.txt': 'foo \nbar' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 9. Trim match
|
||||||
|
list.push({
|
||||||
|
name: 'trim whitespace match',
|
||||||
|
before: { 'trim.txt': ' alpha \n beta' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'trim.txt')),
|
||||||
|
k('alpha'), // matches ' alpha ' via trim
|
||||||
|
k(' beta'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { files: { 'trim.txt': ' alpha \n beta' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 10. Use def_str to anchor (do not duplicate the anchor line in context)
|
||||||
|
list.push({
|
||||||
|
name: 'def_str anchor',
|
||||||
|
before: { 'code.js': 'function a() {}\nfunction greet() {\n return 1;\n}\nfunction z() {}' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'code.js')),
|
||||||
|
at('function greet() {'),
|
||||||
|
// context starts at the next line after the def_str match
|
||||||
|
d(' return 1;'),
|
||||||
|
a(' return 2;'),
|
||||||
|
k('}'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { files: { 'code.js': 'function a() {}\nfunction greet() {\n return 2;\n}\nfunction z() {}' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 11. Bare @@ marker allowed
|
||||||
|
list.push({
|
||||||
|
name: 'bare @@ marker',
|
||||||
|
before: { 'marker.txt': 'L1\nL2\nL3' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'marker.txt')),
|
||||||
|
at(),
|
||||||
|
k('L1'),
|
||||||
|
d('L2'),
|
||||||
|
a('X2'),
|
||||||
|
k('L3'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { files: { 'marker.txt': 'L1\nX2\nL3' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 12. Move/rename file with content change
|
||||||
|
list.push({
|
||||||
|
name: 'move and change',
|
||||||
|
before: { 'mv.txt': 'A\nB' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'mv.txt')),
|
||||||
|
moveTo(path.resolve(dir, 'moved/mv.txt')),
|
||||||
|
k('A'),
|
||||||
|
d('B'),
|
||||||
|
a('C'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { files: { 'moved/mv.txt': 'A\nC' }, notExists: ['mv.txt'] }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 13. Delete file
|
||||||
|
list.push({
|
||||||
|
name: 'delete file',
|
||||||
|
before: { 'del/me.txt': 'bye' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
`*** Delete File: ${path.resolve(dir, 'del/me.txt')}`,
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { notExists: ['del/me.txt'] }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 14. Combined add/update/delete
|
||||||
|
list.push({
|
||||||
|
name: 'combined operations',
|
||||||
|
before: { 'combo/u.txt': 'X\nY', 'combo/d.txt': 'gone' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'combo/u.txt')),
|
||||||
|
k('X'),
|
||||||
|
a('Z'),
|
||||||
|
k('Y'),
|
||||||
|
`*** Delete File: ${path.resolve(dir, 'combo/d.txt')}`,
|
||||||
|
addFile(path.resolve(dir, 'combo/a.txt')),
|
||||||
|
a('new'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { files: { 'combo/u.txt': 'X\nZ\nY', 'combo/a.txt': 'new' }, notExists: ['combo/d.txt'] }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 15. Add with CRLF patch
|
||||||
|
list.push({
|
||||||
|
name: 'add with CRLF patch',
|
||||||
|
before: {},
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
addFile(path.resolve(dir, 'crlf/add.txt')),
|
||||||
|
a('one'),
|
||||||
|
a('two'),
|
||||||
|
end(),
|
||||||
|
], true),
|
||||||
|
expect: { files: { 'crlf/add.txt': 'one\ntwo' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 16. Update with CRLF patch
|
||||||
|
list.push({
|
||||||
|
name: 'update with CRLF patch',
|
||||||
|
before: { 'crlf/up.txt': 'A\nB' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'crlf/up.txt')),
|
||||||
|
k('A'),
|
||||||
|
d('B'),
|
||||||
|
a('C'),
|
||||||
|
end(),
|
||||||
|
], true),
|
||||||
|
expect: { files: { 'crlf/up.txt': 'A\nC' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 17. Ambiguous content resolved by def_str
|
||||||
|
list.push({
|
||||||
|
name: 'ambiguous resolved by def_str',
|
||||||
|
before: { 'amb.js': 'function target() {\n let x = 1;\n}\nfunction target() {\n let x = 2;\n}' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'amb.js')),
|
||||||
|
at('function target() {'),
|
||||||
|
k('function target() {'),
|
||||||
|
d(' let x = 2;'),
|
||||||
|
a(' let x = 42;'),
|
||||||
|
k('}'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { files: { 'amb.js': 'function target() {\n let x = 1;\n}\nfunction target() {\n let x = 42;\n}' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 18. Update missing file -> error
|
||||||
|
list.push({
|
||||||
|
name: 'update missing file error',
|
||||||
|
before: {},
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'missing.txt')),
|
||||||
|
k('x'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { error: /Patch error: Update File Error - missing file:/ }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 19. Delete missing file -> error
|
||||||
|
list.push({
|
||||||
|
name: 'delete missing file error',
|
||||||
|
before: {},
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
`*** Delete File: ${path.resolve(dir, 'nope.txt')}`,
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { error: /Patch error: Delete File Error - missing file:/ }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 20. Add existing file -> error
|
||||||
|
list.push({
|
||||||
|
name: 'add existing file error',
|
||||||
|
before: { 'exists.txt': 'already' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
addFile(path.resolve(dir, 'exists.txt')),
|
||||||
|
a('new'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { error: /Patch error: Add File Error - file already exists:/ }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 21. Duplicate update -> error
|
||||||
|
list.push({
|
||||||
|
name: 'duplicate update error',
|
||||||
|
before: { 'dup.txt': 'X' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'dup.txt')),
|
||||||
|
k('X'),
|
||||||
|
updateFile(path.resolve(dir, 'dup.txt')),
|
||||||
|
k('X'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { error: /Patch error: Duplicate update for file:/ }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 22. Invalid line in update section -> error
|
||||||
|
list.push({
|
||||||
|
name: 'invalid line in section error',
|
||||||
|
before: { 'bad.txt': 'A\nB' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'bad.txt')),
|
||||||
|
k('A'),
|
||||||
|
'?weird',
|
||||||
|
k('B'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { error: /Patch error: Invalid Line:/ }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 23. Missing end sentinel -> error
|
||||||
|
list.push({
|
||||||
|
name: 'missing end sentinel error',
|
||||||
|
before: {},
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
addFile(path.resolve(dir, 'x.txt')),
|
||||||
|
a('x'),
|
||||||
|
// Intentionally no end sentinel here; we'll strip it below
|
||||||
|
]).replace(/\*\*\* End Patch$/, ''),
|
||||||
|
expect: { error: /Patch error: Invalid patch text - missing sentinels/ }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 24. Unknown line while parsing -> error
|
||||||
|
list.push({
|
||||||
|
name: 'unknown line while parsing error',
|
||||||
|
before: {},
|
||||||
|
patch: async () => buildPatch([
|
||||||
|
begin(),
|
||||||
|
'some random line',
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { error: /Patch error: Unknown line while parsing:/ }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 25. Add empty file (no + lines)
|
||||||
|
list.push({
|
||||||
|
name: 'add empty file',
|
||||||
|
before: {},
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
addFile(path.resolve(dir, 'empty.txt')),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { files: { 'empty.txt': '' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 26. Replace whole file contents
|
||||||
|
list.push({
|
||||||
|
name: 'replace whole file',
|
||||||
|
before: { 'whole.txt': 'a\nb\nc' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'whole.txt')),
|
||||||
|
d('a'),
|
||||||
|
d('b'),
|
||||||
|
d('c'),
|
||||||
|
a('x'),
|
||||||
|
a('y'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { files: { 'whole.txt': 'x\ny' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 27. Multiple file updates
|
||||||
|
list.push({
|
||||||
|
name: 'multi-file updates',
|
||||||
|
before: { 'm1.txt': '1', 'm2.txt': 'A' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'm1.txt')),
|
||||||
|
d('1'),
|
||||||
|
a('2'),
|
||||||
|
updateFile(path.resolve(dir, 'm2.txt')),
|
||||||
|
d('A'),
|
||||||
|
a('B'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { files: { 'm1.txt': '2', 'm2.txt': 'B' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 28. Rename only (no content changes)
|
||||||
|
list.push({
|
||||||
|
name: 'rename only',
|
||||||
|
before: { 'r/from.txt': 'same' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'r/from.txt')),
|
||||||
|
moveTo(path.resolve(dir, 'r/to.txt')),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { files: { 'r/to.txt': 'same' }, notExists: ['r/from.txt'] }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 29. EOF context matching at end
|
||||||
|
list.push({
|
||||||
|
name: 'EOF context matching',
|
||||||
|
before: { 'eof.txt': 'first\nsecond\nthird' },
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
updateFile(path.resolve(dir, 'eof.txt')),
|
||||||
|
k('second'),
|
||||||
|
a('inserted'),
|
||||||
|
k('third'),
|
||||||
|
endOfFile(),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { files: { 'eof.txt': 'first\nsecond\ninserted\nthird' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 30. Add multiple files in single patch
|
||||||
|
list.push({
|
||||||
|
name: 'add multiple files',
|
||||||
|
before: {},
|
||||||
|
patch: async ({ dir }) => buildPatch([
|
||||||
|
begin(),
|
||||||
|
addFile(path.resolve(dir, 'multi/a.txt')),
|
||||||
|
a('A'),
|
||||||
|
addFile(path.resolve(dir, 'multi/b.txt')),
|
||||||
|
a('B'),
|
||||||
|
addFile(path.resolve(dir, 'multi/c.txt')),
|
||||||
|
a('C'),
|
||||||
|
end(),
|
||||||
|
]),
|
||||||
|
expect: { files: { 'multi/a.txt': 'A', 'multi/b.txt': 'B', 'multi/c.txt': 'C' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const all = cases();
|
||||||
|
await ensureDir(sandboxRoot);
|
||||||
|
const results = [];
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < all.length; i++) {
|
||||||
|
const tc = all[i];
|
||||||
|
const label = `${String(i + 1).padStart(2, '0')} ${tc.name}`;
|
||||||
|
try {
|
||||||
|
await runCase(i, tc);
|
||||||
|
console.log(`✓ ${label}`);
|
||||||
|
results.push({ name: tc.name, ok: true });
|
||||||
|
passed++;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`✗ ${label}`);
|
||||||
|
console.error(String(err?.stack || err));
|
||||||
|
results.push({ name: tc.name, ok: false, error: String(err?.message || err) });
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log(`Passed: ${passed}, Failed: ${failed}, Total: ${all.length}`);
|
||||||
|
if (failed > 0) process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('Fatal error in test runner:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -1,19 +1,14 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
const desc = `
|
||||||
* A self-contained JavaScript utility for applying human-readable
|
This is a custom utility that makes it more convenient to add, remove, move, or edit code files. 'apply_patch' effectively allows you to execute a diff/patch against a file,
|
||||||
* "pseudo-diff" patch files to a collection of text files.
|
but the format of the diff specification is unique to this task, so pay careful attention to these instructions.
|
||||||
*
|
To use the 'apply_patch' command, you should pass a message of the following structure as "input":
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
*** Begin Patch
|
*** Begin Patch
|
||||||
[YOUR_PATH]
|
[YOUR_PATH]
|
||||||
*** End Patch
|
*** End Patch
|
||||||
|
|
||||||
|
|
||||||
Where [YOUR_PATCH] is the actual content of your patch, specified in the following V4A diff format.
|
Where [YOUR_PATCH] is the actual content of your patch, specified in the following V4A diff format.
|
||||||
|
|
||||||
*** [ACTION] File: [path/to/file] -> ACTION can be one of Add, Update, or Delete.
|
*** [ACTION] File: [path/to/file] -> ACTION can be one of Add, Update, or Delete.
|
||||||
@@ -24,7 +19,7 @@ For each snippet of code that needs to be changed, repeat the following:
|
|||||||
[context_after] -> See below for further instructions on context.
|
[context_after] -> See below for further instructions on context.
|
||||||
|
|
||||||
For instructions on [context_before] and [context_after]:
|
For instructions on [context_before] and [context_after]:
|
||||||
- By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first change’s [context_after] lines in the second change’s [context_before] lines.
|
- By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first changes [context_after] lines in the second changes [context_before] lines.
|
||||||
- If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have:
|
- If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have:
|
||||||
@@ class BaseClass
|
@@ class BaseClass
|
||||||
[3 lines of pre-context]
|
[3 lines of pre-context]
|
||||||
@@ -32,7 +27,7 @@ For instructions on [context_before] and [context_after]:
|
|||||||
+ [new_code]
|
+ [new_code]
|
||||||
[3 lines of post-context]
|
[3 lines of post-context]
|
||||||
|
|
||||||
- If a code block is repeated so many times in a class or function such that even a single @@ statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple `@@` statements to jump to the right context. For instance:
|
- If a code block is repeated so many times in a class or function such that even a single @@ statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple '@@' statements to jump to the right context. For instance:
|
||||||
|
|
||||||
@@ class BaseClass
|
@@ class BaseClass
|
||||||
@@ def method():
|
@@ def method():
|
||||||
@@ -42,12 +37,7 @@ For instructions on [context_before] and [context_after]:
|
|||||||
[3 lines of post-context]
|
[3 lines of post-context]
|
||||||
|
|
||||||
Note, then, that we do not use line numbers in this diff format, as the context is enough to uniquely identify code. An example of a message that you might pass as "input" to this function, in order to apply a patch, is shown below.
|
Note, then, that we do not use line numbers in this diff format, as the context is enough to uniquely identify code. An example of a message that you might pass as "input" to this function, in order to apply a patch, is shown below.
|
||||||
|
`;
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
// --------------------------------------------------------------------------- //
|
// --------------------------------------------------------------------------- //
|
||||||
// Domain objects
|
// Domain objects
|
||||||
// --------------------------------------------------------------------------- //
|
// --------------------------------------------------------------------------- //
|
||||||
@@ -109,16 +99,55 @@ class Patch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------- //
|
||||||
|
// Path utilities for chroot functionality
|
||||||
|
// --------------------------------------------------------------------------- //
|
||||||
|
function normalizePath(path) {
|
||||||
|
return path.replace(/\\/g, '/').replace(/\/+/g, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinPaths(...paths) {
|
||||||
|
return normalizePath(paths.filter(p => p).join('/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePath(chroot, filepath) {
|
||||||
|
if (!chroot) return filepath;
|
||||||
|
|
||||||
|
// Remove leading slash from filepath if present
|
||||||
|
const cleanFilepath = filepath.startsWith('/') ? filepath.substring(1) : filepath;
|
||||||
|
|
||||||
|
// Join chroot and filepath
|
||||||
|
const resolved = joinPaths(chroot, cleanFilepath);
|
||||||
|
|
||||||
|
// Ensure it starts with /
|
||||||
|
return resolved.startsWith('/') ? resolved : '/' + resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unresolvePath(chroot, filepath) {
|
||||||
|
if (!chroot) return filepath;
|
||||||
|
|
||||||
|
const chrootPath = chroot.startsWith('/') ? chroot : '/' + chroot;
|
||||||
|
const cleanFilepath = filepath.startsWith('/') ? filepath : '/' + filepath;
|
||||||
|
|
||||||
|
if (cleanFilepath.startsWith(chrootPath)) {
|
||||||
|
const relativePath = cleanFilepath.substring(chrootPath.length);
|
||||||
|
return relativePath.startsWith('/') ? relativePath : '/' + relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath;
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------- //
|
// --------------------------------------------------------------------------- //
|
||||||
// Patch text parser
|
// Patch text parser
|
||||||
// --------------------------------------------------------------------------- //
|
// --------------------------------------------------------------------------- //
|
||||||
class Parser {
|
class Parser {
|
||||||
constructor(current_files, lines) {
|
constructor(current_files, lines, chroot = null) {
|
||||||
this.current_files = current_files;
|
this.current_files = current_files;
|
||||||
this.lines = lines;
|
this.lines = lines;
|
||||||
this.index = 0;
|
this.index = 0;
|
||||||
this.patch = new Patch();
|
this.patch = new Patch();
|
||||||
this.fuzz = 0;
|
this.fuzz = 0;
|
||||||
|
this.chroot = chroot;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------- low-level helpers -------------------------------------- //
|
// ------------- low-level helpers -------------------------------------- //
|
||||||
@@ -186,43 +215,52 @@ class Parser {
|
|||||||
// ---------- UPDATE ---------- //
|
// ---------- UPDATE ---------- //
|
||||||
let path = this.read_str("*** Update File: ");
|
let path = this.read_str("*** Update File: ");
|
||||||
if (path) {
|
if (path) {
|
||||||
if (path in this.patch.actions) {
|
// Resolve path with chroot
|
||||||
throw new DiffError(`Duplicate update for file: ${path}`);
|
const resolvedPath = resolvePath(this.chroot, path);
|
||||||
|
|
||||||
|
if (resolvedPath in this.patch.actions) {
|
||||||
|
throw new DiffError(`Duplicate update for file: ${resolvedPath}`);
|
||||||
}
|
}
|
||||||
const move_to = this.read_str("*** Move to: ");
|
const move_to = this.read_str("*** Move to: ");
|
||||||
if (!(path in this.current_files)) {
|
if (!(resolvedPath in this.current_files)) {
|
||||||
throw new DiffError(`Update File Error - missing file: ${path}`);
|
throw new DiffError(`Update File Error - missing file: ${resolvedPath}`);
|
||||||
}
|
}
|
||||||
const text = this.current_files[path];
|
const text = this.current_files[resolvedPath];
|
||||||
const action = this._parse_update_file(text);
|
const action = this._parse_update_file(text);
|
||||||
action.move_path = move_to || null;
|
action.move_path = move_to ? resolvePath(this.chroot, move_to) : null;
|
||||||
this.patch.actions[path] = action;
|
this.patch.actions[resolvedPath] = action;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- DELETE ---------- //
|
// ---------- DELETE ---------- //
|
||||||
path = this.read_str("*** Delete File: ");
|
path = this.read_str("*** Delete File: ");
|
||||||
if (path) {
|
if (path) {
|
||||||
if (path in this.patch.actions) {
|
// Resolve path with chroot
|
||||||
throw new DiffError(`Duplicate delete for file: ${path}`);
|
const resolvedPath = resolvePath(this.chroot, path);
|
||||||
|
|
||||||
|
if (resolvedPath in this.patch.actions) {
|
||||||
|
throw new DiffError(`Duplicate delete for file: ${resolvedPath}`);
|
||||||
}
|
}
|
||||||
if (!(path in this.current_files)) {
|
if (!(resolvedPath in this.current_files)) {
|
||||||
throw new DiffError(`Delete File Error - missing file: ${path}`);
|
throw new DiffError(`Delete File Error - missing file: ${resolvedPath}`);
|
||||||
}
|
}
|
||||||
this.patch.actions[path] = new PatchAction(ActionType.DELETE);
|
this.patch.actions[resolvedPath] = new PatchAction(ActionType.DELETE);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- ADD ---------- //
|
// ---------- ADD ---------- //
|
||||||
path = this.read_str("*** Add File: ");
|
path = this.read_str("*** Add File: ");
|
||||||
if (path) {
|
if (path) {
|
||||||
if (path in this.patch.actions) {
|
// Resolve path with chroot
|
||||||
throw new DiffError(`Duplicate add for file: ${path}`);
|
const resolvedPath = resolvePath(this.chroot, path);
|
||||||
|
|
||||||
|
if (resolvedPath in this.patch.actions) {
|
||||||
|
throw new DiffError(`Duplicate add for file: ${resolvedPath}`);
|
||||||
}
|
}
|
||||||
if (path in this.current_files) {
|
if (resolvedPath in this.current_files) {
|
||||||
throw new DiffError(`Add File Error - file already exists: ${path}`);
|
throw new DiffError(`Add File Error - file already exists: ${resolvedPath}`);
|
||||||
}
|
}
|
||||||
this.patch.actions[path] = this._parse_add_file();
|
this.patch.actions[resolvedPath] = this._parse_add_file();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,7 +550,7 @@ function _get_updated_file(text, action, path) {
|
|||||||
return dest_lines.join("\n");
|
return dest_lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
function patch_to_commit(patch, orig) {
|
function patch_to_commit(patch, orig, chroot = null) {
|
||||||
const commit = new Commit();
|
const commit = new Commit();
|
||||||
for (const [path, action] of Object.entries(patch.actions)) {
|
for (const [path, action] of Object.entries(patch.actions)) {
|
||||||
if (action.type === ActionType.DELETE) {
|
if (action.type === ActionType.DELETE) {
|
||||||
@@ -534,11 +572,12 @@ function patch_to_commit(patch, orig) {
|
|||||||
);
|
);
|
||||||
} else if (action.type === ActionType.UPDATE) {
|
} else if (action.type === ActionType.UPDATE) {
|
||||||
const new_content = _get_updated_file(orig[path], action, path);
|
const new_content = _get_updated_file(orig[path], action, path);
|
||||||
|
const move_path = action.move_path ? unresolvePath(chroot, action.move_path) : null;
|
||||||
commit.changes[path] = new FileChange(
|
commit.changes[path] = new FileChange(
|
||||||
ActionType.UPDATE,
|
ActionType.UPDATE,
|
||||||
orig[path],
|
orig[path],
|
||||||
new_content,
|
new_content,
|
||||||
action.move_path
|
move_path
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -548,7 +587,7 @@ function patch_to_commit(patch, orig) {
|
|||||||
// --------------------------------------------------------------------------- //
|
// --------------------------------------------------------------------------- //
|
||||||
// User-facing helpers
|
// User-facing helpers
|
||||||
// --------------------------------------------------------------------------- //
|
// --------------------------------------------------------------------------- //
|
||||||
function text_to_patch(text, orig) {
|
function text_to_patch(text, orig, chroot = null) {
|
||||||
// Handle different line ending scenarios
|
// Handle different line ending scenarios
|
||||||
let lines = text.split(/\r?\n/);
|
let lines = text.split(/\r?\n/);
|
||||||
|
|
||||||
@@ -574,13 +613,13 @@ function text_to_patch(text, orig) {
|
|||||||
throw new DiffError("Invalid patch text - missing sentinels");
|
throw new DiffError("Invalid patch text - missing sentinels");
|
||||||
}
|
}
|
||||||
|
|
||||||
const parser = new Parser(orig, lines);
|
const parser = new Parser(orig, lines, chroot);
|
||||||
parser.index = 1;
|
parser.index = 1;
|
||||||
parser.parse();
|
parser.parse();
|
||||||
return [parser.patch, parser.fuzz];
|
return [parser.patch, parser.fuzz];
|
||||||
}
|
}
|
||||||
|
|
||||||
function identify_files_needed(text) {
|
function identify_files_needed(text, chroot = null) {
|
||||||
// Handle line splitting consistently
|
// Handle line splitting consistently
|
||||||
let lines = text.split(/\r?\n/);
|
let lines = text.split(/\r?\n/);
|
||||||
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
||||||
@@ -589,16 +628,16 @@ function identify_files_needed(text) {
|
|||||||
|
|
||||||
const updateFiles = lines
|
const updateFiles = lines
|
||||||
.filter(line => line.startsWith("*** Update File: "))
|
.filter(line => line.startsWith("*** Update File: "))
|
||||||
.map(line => line.substring("*** Update File: ".length));
|
.map(line => resolvePath(chroot, line.substring("*** Update File: ".length)));
|
||||||
|
|
||||||
const deleteFiles = lines
|
const deleteFiles = lines
|
||||||
.filter(line => line.startsWith("*** Delete File: "))
|
.filter(line => line.startsWith("*** Delete File: "))
|
||||||
.map(line => line.substring("*** Delete File: ".length));
|
.map(line => resolvePath(chroot, line.substring("*** Delete File: ".length)));
|
||||||
|
|
||||||
return [...updateFiles, ...deleteFiles];
|
return [...updateFiles, ...deleteFiles];
|
||||||
}
|
}
|
||||||
|
|
||||||
function identify_files_added(text) {
|
function identify_files_added(text, chroot = null) {
|
||||||
// Handle line splitting consistently
|
// Handle line splitting consistently
|
||||||
let lines = text.split(/\r?\n/);
|
let lines = text.split(/\r?\n/);
|
||||||
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
||||||
@@ -607,7 +646,7 @@ function identify_files_added(text) {
|
|||||||
|
|
||||||
return lines
|
return lines
|
||||||
.filter(line => line.startsWith("*** Add File: "))
|
.filter(line => line.startsWith("*** Add File: "))
|
||||||
.map(line => line.substring("*** Add File: ".length));
|
.map(line => resolvePath(chroot, line.substring("*** Add File: ".length)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------- //
|
// --------------------------------------------------------------------------- //
|
||||||
@@ -621,7 +660,7 @@ function load_files(paths, open_fn) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function apply_commit(commit, write_fn, remove_fn) {
|
function apply_commit(commit, write_fn, remove_fn, chroot = null) {
|
||||||
for (const [path, change] of Object.entries(commit.changes)) {
|
for (const [path, change] of Object.entries(commit.changes)) {
|
||||||
if (change.type === ActionType.DELETE) {
|
if (change.type === ActionType.DELETE) {
|
||||||
remove_fn(path);
|
remove_fn(path);
|
||||||
@@ -634,7 +673,7 @@ function apply_commit(commit, write_fn, remove_fn) {
|
|||||||
if (change.new_content === null) {
|
if (change.new_content === null) {
|
||||||
throw new DiffError(`UPDATE change for ${path} has no new content`);
|
throw new DiffError(`UPDATE change for ${path} has no new content`);
|
||||||
}
|
}
|
||||||
const target = change.move_path || path;
|
const target = change.move_path ? resolvePath(chroot, change.move_path) : path;
|
||||||
write_fn(target, change.new_content);
|
write_fn(target, change.new_content);
|
||||||
if (change.move_path) {
|
if (change.move_path) {
|
||||||
remove_fn(path);
|
remove_fn(path);
|
||||||
@@ -643,15 +682,15 @@ function apply_commit(commit, write_fn, remove_fn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function process_patch(text, open_fn, write_fn, remove_fn) {
|
function process_patch(text, open_fn, write_fn, remove_fn, chroot = null) {
|
||||||
if (!text.startsWith("*** Begin Patch")) {
|
if (!text.startsWith("*** Begin Patch")) {
|
||||||
throw new DiffError("Patch text must start with *** Begin Patch");
|
throw new DiffError("Patch text must start with *** Begin Patch");
|
||||||
}
|
}
|
||||||
const paths = identify_files_needed(text);
|
const paths = identify_files_needed(text, chroot);
|
||||||
const orig = load_files(paths, open_fn);
|
const orig = load_files(paths, open_fn);
|
||||||
const [patch, _fuzz] = text_to_patch(text, orig);
|
const [patch, _fuzz] = text_to_patch(text, orig, chroot);
|
||||||
const commit = patch_to_commit(patch, orig);
|
const commit = patch_to_commit(patch, orig, chroot);
|
||||||
apply_commit(commit, write_fn, remove_fn);
|
apply_commit(commit, write_fn, remove_fn, chroot);
|
||||||
return "Done!";
|
return "Done!";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,13 +722,13 @@ function remove_file(filepath) {
|
|||||||
export default {
|
export default {
|
||||||
type: "function",
|
type: "function",
|
||||||
name: "patch_files",
|
name: "patch_files",
|
||||||
description: "Apply a unified diff patch to files within a chroot directory, with option to reverse the patch",
|
description: "Apply a unified diff patch " + desc,
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
patch: {
|
patch: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "The unidiff patch string to apply.",
|
description: "The patch string to apply.",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: ["patch"],
|
required: ["patch"],
|
||||||
@@ -706,7 +745,8 @@ export async function run(args) {
|
|||||||
args.patch,
|
args.patch,
|
||||||
open_file,
|
open_file,
|
||||||
write_file,
|
write_file,
|
||||||
remove_file
|
remove_file,
|
||||||
|
'/home/seb/src/aiTools/tmp'
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -715,4 +755,4 @@ export async function run(args) {
|
|||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user