Files
builder/scripts/test-edit-read-errors.mjs
Nariman Jelveh 93ac98a37a Only tell the model a file is missing when it is
edit and multi_edit read the file before matching, and reported ANY read
failure as "File not found … Use the 'write' tool to create new files." A
network blip or a folder at that path therefore invited the model to
recreate an existing file from memory with 'write', silently dropping
whatever it misremembered.

Use the not-found message only for a genuine not-found error; otherwise say
why the read failed, that nothing was changed, and to retry rather than
recreate the file.
2026-10-04 13:10:56 -07:00

64 lines
3.1 KiB
JavaScript

import fs from 'node:fs';
// ---- Regression guard: a failed read is only "not found" when it is ----------
// edit and multi_edit read the file before matching. ANY read failure used to be
// reported as "File not found … Use the 'write' tool to create new files." — a
// network blip on an existing file then invited the model to recreate it from
// memory, silently dropping whatever it misremembered. Only a genuinely missing
// file may point at 'write'; anything else must say the file was left as is.
//
// Runs the real tools against a stubbed filesystem, with the real
// isNotFoundError from app.js.
let failures = 0;
function check(name, cond, detail) {
if (cond) console.log('ok - ' + name);
else { console.error('FAIL - ' + name + (detail ? '\n ' + detail : '')); failures++; }
}
const read = (p) => fs.readFileSync(new URL(p, import.meta.url), 'utf8');
const app = read('../src/js/app.js');
const a = app.indexOf('function isNotFoundError(error) {');
const b = app.indexOf('\n}\n', a);
const isNotFoundError = new Function(app.slice(a, b + 2) + '\nreturn isNotFoundError;')();
const win = {
tools: [],
assertPathInProject: (p) => p,
withFileLock: (_p, fn) => fn(),
applyFileEdit: (c, o, n) => c.replace(o, n),
writeFileVerified: async () => {},
};
let readImpl;
const puter = { fs: { read: (...args) => readImpl(...args) } };
for (const file of ['../src/tools/fs/edit.js', '../src/tools/fs/multi_edit.js']) {
new Function('window', 'puter', 'isNotFoundError', read(file))(win, puter, isNotFoundError);
}
const tool = (name) => win.tools.find((t) => t.function.name === name);
const runs = {
edit: () => tool('edit').exec({ path: '/u/app/index.html', old_content: 'a', new_content: 'b' }, {}),
multi_edit: () => tool('multi_edit').exec({ path: '/u/app/index.html', edits: [{ old_content: 'a', new_content: 'b' }] }, {}),
};
for (const [name, run] of Object.entries(runs)) {
readImpl = async () => { throw { code: 'subject_does_not_exist', message: 'Subject does not exist' }; };
let err = null;
try { await run(); } catch (e) { err = e; }
check(`${name}: a missing file still points at 'write'`, err && /File not found/.test(err.message) && /'write'/.test(err.message), err && err.message);
readImpl = async () => { throw new TypeError('Failed to fetch'); };
err = null;
try { await run(); } catch (e) { err = e; }
check(`${name}: a network failure is not reported as a missing file`, err && !/File not found/.test(err.message), err && err.message);
check(`${name}: …says why, that nothing changed, and not to recreate it`,
err && /Failed to fetch/.test(err.message) && /nothing was changed/.test(err.message) && /do not recreate/.test(err.message), err && err.message);
readImpl = async () => ({ text: async () => 'a' });
let res = null;
try { res = await run(); } catch (e) { err = e; }
check(`${name}: a readable file is edited as before`, res && res.success === true, err && err.message);
}
if (failures) { console.error(`\n${failures} check(s) FAILED`); process.exit(1); }
console.log('\nAll edit read-error checks passed.');