Add file browser, tests, and bin entrypoint
This commit is contained in:
148
test/compact.test.js
Normal file
148
test/compact.test.js
Normal file
@@ -0,0 +1,148 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Tests for invalidateStaleReadResults in src/compact.js.
|
||||
*
|
||||
* Run with: node --test
|
||||
* (or) node --test test/compact.test.js
|
||||
*
|
||||
* These are pure unit tests: they build synthetic transcript arrays and never
|
||||
* call a model or touch the network. Importing compact.js also loads config.js
|
||||
* (dotenv + ROOT_DIR) which is harmless and needs no API key.
|
||||
*/
|
||||
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { invalidateStaleReadResults } from '../src/compact.js';
|
||||
|
||||
// A file body long enough that the replacement marker is strictly smaller.
|
||||
const BODY = Array.from({ length: 40 }, (_, i) => `line ${i} of the file body`).join('\n');
|
||||
|
||||
// Assistant message declaring a tool call.
|
||||
const call = (id, name, args) => ({
|
||||
role: 'assistant',
|
||||
tool_calls: [{ id, function: { name, arguments: JSON.stringify(args) } }],
|
||||
});
|
||||
|
||||
// A read_file tool result carrying a whole file body.
|
||||
const read = (id, filename, content = BODY) =>
|
||||
({ role: 'tool', tool_call_id: id, content });
|
||||
|
||||
// A patch tool result. `applied` is the array of mutation tags the real
|
||||
// applyOperations emits, e.g. ["PATCH in src/a.js"].
|
||||
const patchResult = (id, applied) =>
|
||||
({ role: 'tool', tool_call_id: id, content: JSON.stringify({ applied }) });
|
||||
|
||||
const isMarker = (content) => {
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
return typeof parsed.note === 'string' && parsed.note.includes('overwritten meanwhile');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
test('read before a later patch is invalidated', () => {
|
||||
const messages = [
|
||||
call('r1', 'read_file', { filename: 'src/a.js' }),
|
||||
read('r1', 'src/a.js'),
|
||||
call('p1', 'patch', { operations: [{ file: 'src/a.js', search: 'x', replace: 'y' }] }),
|
||||
patchResult('p1', ['PATCH in src/a.js']),
|
||||
];
|
||||
const n = invalidateStaleReadResults(messages);
|
||||
assert.equal(n, 1);
|
||||
assert.ok(isMarker(messages[1].content), 'read result replaced with marker');
|
||||
assert.equal(JSON.parse(messages[1].content).filename, 'src/a.js');
|
||||
});
|
||||
|
||||
test('read after the last patch is left intact (still matches disk)', () => {
|
||||
const messages = [
|
||||
call('p1', 'patch', { operations: [{ file: 'src/a.js', search: 'x', replace: 'y' }] }),
|
||||
patchResult('p1', ['PATCH in src/a.js']),
|
||||
call('r1', 'read_file', { filename: 'src/a.js' }),
|
||||
read('r1', 'src/a.js'),
|
||||
];
|
||||
assert.equal(invalidateStaleReadResults(messages), 0);
|
||||
assert.equal(messages[3].content, BODY);
|
||||
});
|
||||
|
||||
test('a file no patch ever wrote is left intact', () => {
|
||||
const messages = [
|
||||
call('r1', 'read_file', { filename: 'src/untouched.js' }),
|
||||
read('r1', 'src/untouched.js'),
|
||||
call('p1', 'patch', { operations: [{ file: 'src/a.js', search: 'x', replace: 'y' }] }),
|
||||
patchResult('p1', ['PATCH in src/a.js']),
|
||||
];
|
||||
assert.equal(invalidateStaleReadResults(messages), 0);
|
||||
assert.equal(messages[1].content, BODY);
|
||||
});
|
||||
|
||||
test('a failed patch (no applied entries) invalidates nothing', () => {
|
||||
const messages = [
|
||||
call('r1', 'read_file', { filename: 'src/a.js' }),
|
||||
read('r1', 'src/a.js'),
|
||||
call('p1', 'patch', { operations: [{ file: 'src/a.js', search: 'x', replace: 'y' }] }),
|
||||
patchResult('p1', []), // failure => nothing actually written
|
||||
];
|
||||
assert.equal(invalidateStaleReadResults(messages), 0);
|
||||
assert.equal(messages[1].content, BODY);
|
||||
});
|
||||
|
||||
test('partial failure: appliedSoFar still invalidates a written file', () => {
|
||||
const messages = [
|
||||
call('r1', 'read_file', { filename: 'src/a.js' }),
|
||||
read('r1', 'src/a.js'),
|
||||
call('p1', 'patch', { operations: [{ file: 'src/a.js', search: 'x', replace: 'y' }] }),
|
||||
{ role: 'tool', tool_call_id: 'p1', content: JSON.stringify({ appliedSoFar: ['CREATE/WRITE src/a.js'], error: 'boom' }) },
|
||||
];
|
||||
assert.equal(invalidateStaleReadResults(messages), 1);
|
||||
assert.ok(isMarker(messages[1].content));
|
||||
});
|
||||
|
||||
test('CREATE/WRITE tag is recognised', () => {
|
||||
const messages = [
|
||||
call('r1', 'read_file', { filename: 'src/new.js' }),
|
||||
read('r1', 'src/new.js'),
|
||||
call('p1', 'patch', { operations: [{ file: 'src/new.js', search: null, replace: 'hi' }] }),
|
||||
patchResult('p1', ['CREATE/WRITE src/new.js']),
|
||||
];
|
||||
assert.equal(invalidateStaleReadResults(messages), 1);
|
||||
assert.ok(isMarker(messages[1].content));
|
||||
});
|
||||
|
||||
test('path canonicalization: ./src/a.js matches src/a.js', () => {
|
||||
const messages = [
|
||||
call('r1', 'read_file', { filename: './src/a.js' }),
|
||||
read('r1', './src/a.js'),
|
||||
call('p1', 'patch', { operations: [{ file: 'src/a.js', search: 'x', replace: 'y' }] }),
|
||||
patchResult('p1', ['PATCH in src/a.js']),
|
||||
];
|
||||
assert.equal(invalidateStaleReadResults(messages), 1);
|
||||
assert.ok(isMarker(messages[1].content));
|
||||
});
|
||||
|
||||
test('size guard: a tiny read is never traded for a longer marker', () => {
|
||||
const messages = [
|
||||
call('r1', 'read_file', { filename: 'src/a.js' }),
|
||||
read('r1', 'src/a.js', 'tiny'),
|
||||
call('p1', 'patch', { operations: [{ file: 'src/a.js', search: 'x', replace: 'y' }] }),
|
||||
patchResult('p1', ['PATCH in src/a.js']),
|
||||
];
|
||||
assert.equal(invalidateStaleReadResults(messages), 0);
|
||||
assert.equal(messages[1].content, 'tiny');
|
||||
});
|
||||
|
||||
test('idempotent: a second pass changes nothing', () => {
|
||||
const messages = [
|
||||
call('r1', 'read_file', { filename: 'src/a.js' }),
|
||||
read('r1', 'src/a.js'),
|
||||
call('p1', 'patch', { operations: [{ file: 'src/a.js', search: 'x', replace: 'y' }] }),
|
||||
patchResult('p1', ['PATCH in src/a.js']),
|
||||
];
|
||||
assert.equal(invalidateStaleReadResults(messages), 1);
|
||||
const afterFirst = messages[1].content;
|
||||
assert.equal(invalidateStaleReadResults(messages), 0);
|
||||
assert.equal(messages[1].content, afterFirst);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user