Files
builder/scripts/test-worker-tools.mjs
Reynaldi Chernando 0bc29c6a75 improve worker editing performance (#124)
* improve worker editing performance

* fixes
2026-10-28 00:07:17 +07:00

345 lines
20 KiB
JavaScript

import fs from 'node:fs';
// ---- Regression guard for the worker tools' project sandbox ----------------
// Deployed Puter workers are ACCOUNT-level resources keyed only by name and
// shared by every project on the account, so all four worker tools are scoped
// to the project. On the write side: delete_worker must only ever delete a
// worker THIS project owns, and create_worker — whose backing call redeploys an
// existing name — must never take over a name another project holds. On the read
// side: list_workers and get_worker must not hand another project's worker names,
// live URLs and source paths (which embed its chat id) to this project's model,
// where they persist in the conversation and can get wired into this app.
// Ownership is the rule the rest of the app already uses
// (WorkerOwnership.ownedWorkers: the deployed record's source file lives inside
// the project dir). This test evaluates the REAL tool files with the REAL
// path-sandbox helpers (helpers.js) and the REAL ownership module against a fake
// puter.workers, and drives every accept/refuse branch.
// Mirrors scripts/test-search-files.mjs.
let failures = 0;
function check(name, cond) {
if (cond) console.log('ok - ' + name);
else { console.error('FAIL - ' + name); failures++; }
}
async function rejects(promise, substr) {
try { await promise; return false; }
catch (e) {
const msg = (e && e.message) ? e.message : String(e);
return substr ? msg.includes(substr) : true;
}
}
// --- Real path-sandbox helpers (helpers.js) + real ownership module ---------
const HELP = fs.readFileSync(new URL('../src/js/helpers.js', import.meta.url), 'utf8');
const a = HELP.indexOf('window.normalizePosixPath =');
const b = HELP.indexOf('// ---- Per-path file-write serialization');
if (a < 0 || b < 0 || b <= a) throw new Error('could not extract the path-sandbox helper block');
const WO_SRC = fs.readFileSync(new URL('../src/js/worker-ownership.js', import.meta.url), 'utf8');
const CREATE = fs.readFileSync(new URL('../src/tools/workers/create_worker.js', import.meta.url), 'utf8');
const DELETE = fs.readFileSync(new URL('../src/tools/workers/delete_worker.js', import.meta.url), 'utf8');
const LIST = fs.readFileSync(new URL('../src/tools/workers/list_workers.js', import.meta.url), 'utf8');
const GET = fs.readFileSync(new URL('../src/tools/workers/get_worker.js', import.meta.url), 'utf8');
// --- Fake puter.workers / puter.fs -------------------------------------------
let WORKERS = [];
let LIST_THROWS = false;
const calls = { created: [], deleted: [], mkdir: [], writes: [], got: [], listed: 0 };
function resetCalls() { for (const k of Object.keys(calls)) calls[k] = Array.isArray(calls[k]) ? [] : 0; }
const puter = {
workers: {
list: async () => { calls.listed++; if (LIST_THROWS) throw new Error('network down'); return WORKERS.slice(); },
create: async (name, path, opts) => { calls.created.push({ name, path, opts }); return { success: true, url: `https://${name}.puter.work/` }; },
delete: async (name) => { calls.deleted.push(name); },
get: async (name) => {
calls.got.push(name);
const w = WORKERS.find(x => x.name === name);
return w ? { ...w, file_uid: 'uid-' + name } : null;
},
},
fs: { mkdir: async (p) => { calls.mkdir.push(p); } },
};
const win = {
tools: [],
withFileLock: (p, fn) => Promise.resolve().then(fn),
writeFileVerified: async (p, d) => { calls.writes.push({ p, d }); },
};
new Function('window', 'puter', HELP.slice(a, b) + '\n' + WO_SRC + '\n' + CREATE + '\n' + DELETE + '\n' + LIST + '\n' + GET)(win, puter);
const create = win.tools.find(t => t.function && t.function.name === 'create_worker');
const del = win.tools.find(t => t.function && t.function.name === 'delete_worker');
const list = win.tools.find(t => t.function && t.function.name === 'list_workers');
const get = win.tools.find(t => t.function && t.function.name === 'get_worker');
if (!create || !del || !list || !get) throw new Error('worker tools did not register');
const WO = win.WorkerOwnership;
const ROOT = '/nj/AppData/app-uid/chat_abc';
const OTHER = '/nj/AppData/app-uid/chat_xyz';
const st = { appDir: ROOT };
const own = (name) => ({ name, url: `https://${name}.puter.work/`, file_path: `${ROOT}/workers/${name}.js` });
const theirs = (name) => ({ name, url: `https://${name}.puter.work/`, file_path: `${OTHER}/workers/${name}.js` });
// === Pure name lookup ========================================================
{
const list = [own('api'), theirs('Api'), theirs('other')];
check('matchesByName: case-insensitive, exact first',
WO.matchesByName(list, 'api').map(w => w.name).join(',') === 'api,Api');
check('matchesByName: no hit → []', WO.matchesByName(list, 'nope').length === 0);
check('matchesByName: junk input → []',
WO.matchesByName(null, 'api').length === 0 && WO.matchesByName(list, '').length === 0);
check('findByName: exact match wins', WO.findByName(list, 'Api').name === 'Api');
check('findByName: unique case-variant is accepted', WO.findByName(list, 'OTHER').name === 'other');
const twins = [theirs('ab'), theirs('AB')];
check('findByName: ambiguous case-variants are NOT guessed at', WO.findByName(twins, 'Ab') === null);
check('findByName: …but an exact match among them is returned', WO.findByName(twins, 'AB').name === 'AB');
check('findByName: no hit → null', WO.findByName(twins, 'zz') === null);
}
// === delete_worker ===========================================================
WORKERS = [own('api'), theirs('billing'), { name: 'legacy', url: 'https://legacy.puter.work/' }];
resetCalls();
{
const r = await del.exec({ name: 'api' }, st);
check('delete: own worker is deleted', calls.deleted.join(',') === 'api' && r.success === true && r.name === 'api');
}
resetCalls();
check("delete: another project's worker is refused (model-directed error)",
await rejects(del.exec({ name: 'billing' }, st), 'does not belong to this project'));
check('delete: refusal deletes nothing', calls.deleted.length === 0);
resetCalls();
check('delete: unknown name → not found', await rejects(del.exec({ name: 'ghost' }, st), 'not found'));
check('delete: a record with no source path is not deletable (cannot prove ownership)',
await rejects(del.exec({ name: 'legacy' }, st), 'does not belong'));
check('delete: nothing deleted across the refusals', calls.deleted.length === 0);
resetCalls();
{
WORKERS = [own('api')];
const r = await del.exec({ name: 'API' }, st);
check('delete: unique case-variant resolves to the real record and deletes by its real name',
calls.deleted.join(',') === 'api' && r.name === 'api');
}
resetCalls();
check('delete: empty name is refused', await rejects(del.exec({ name: ' ' }, st), 'non-empty'));
check('delete: no project dir (no state, no global) → refused for safety',
await rejects(del.exec({ name: 'api' }, {}), 'refused for safety'));
LIST_THROWS = true;
check('delete: a failing worker lookup fails CLOSED', await rejects(del.exec({ name: 'api' }, st)) && calls.deleted.length === 0);
LIST_THROWS = false;
// === create_worker ===========================================================
WORKERS = [own('api'), theirs('billing'), theirs('Reports')];
resetCalls();
{
const r = await create.exec({ name: 'fresh', code: 'router.get("/", () => 1);' }, st);
// Worker names are shared by everyone on Puter, so a new worker gets a
// random suffix (issue #35: "guestbook" was another user's).
check('create: a new worker is named after the request plus a suffix',
calls.created.length === 1 && r.success === true && /^fresh-[a-z0-9]{6}$/.test(r.name), r.name);
check('create: …and the result says to use that name', /Builder named this worker 'fresh-[a-z0-9]{6}'/.test(r.note || ''), r.note);
check('create: source lands at <project>/workers/<name>.js',
calls.writes[0] && calls.writes[0].p === `${ROOT}/workers/${r.name}.js` && r.file_path === calls.writes[0].p);
check('create: deployed from that file under that name, sandboxed',
calls.created[0].name === r.name && calls.created[0].path === `${ROOT}/workers/${r.name}.js`
&& calls.created[0].opts && calls.created[0].opts.sandbox === true);
check('create: workers/ dir is created under the project', calls.mkdir[0] === `${ROOT}/workers`);
check('create: the account list is consulted before writing', calls.listed === 1);
}
resetCalls();
{
const r = await create.exec({ name: 'api', code: 'x' }, st);
check("create: this project's OWN worker by that name is redeployed, not suffixed",
calls.created.length === 1 && r.name === 'api' && calls.created[0].name === 'api' && !r.note);
}
resetCalls();
{
const a = await create.exec({ name: 'notes', code: 'x' }, st);
WORKERS = [own('api'), own(a.name), theirs('billing'), theirs('Reports')];
const b = await create.exec({ name: 'Notes', code: 'y' }, st);
check('create: asking again for the same name redeploys the worker the first call made',
b.name === a.name && calls.created.length === 2 && calls.created[1].name === a.name, a.name + ' / ' + b.name);
check("create: …and that name is the project's own, not another project's",
WO.requestedName(OTHER, 'notes') !== a.name);
WORKERS = [own('api'), own('chat-server'), theirs('billing'), theirs('Reports')];
const c = await create.exec({ name: 'chat', code: 'x' }, st);
check("create: a request for 'chat' never touches this project's 'chat-server'",
c.name !== 'chat-server' && /^chat-[a-z0-9]{6}$/.test(c.name) && calls.created[2].name === c.name, c.name);
const long1 = await create.exec({ name: 'x'.repeat(40) + 'one', code: 'x' }, st);
const long2 = await create.exec({ name: 'x'.repeat(40) + 'two', code: 'x' }, st);
check('create: two long names that only differ past the cut get different workers', long1.name !== long2.name, long1.name);
WORKERS = [own('api'), theirs('billing'), theirs('Reports')];
}
resetCalls();
{
// The name this request always gets is held by another project of the
// account: a random suffix instead, written nowhere first.
const usual = WO.requestedName(ROOT, 'inbox');
WORKERS = [own('api'), theirs(usual)];
const r = await create.exec({ name: 'inbox', code: 'x' }, st);
check("create: a name another project holds gets a random suffix instead",
r.name !== usual && /^inbox-[a-z0-9]{6}$/.test(r.name) && calls.writes.length === 1 && calls.writes[0].p === `${ROOT}/workers/${r.name}.js`, r.name);
WORKERS = [own('api'), theirs('billing'), theirs('Reports')];
}
resetCalls();
{
// ...or by another Puter user, whose 'sandbox-<name>' app has no owner
// we can see. A sandbox app of our own is ours to reuse.
const usual = WO.requestedName(ROOT, 'guestbook');
puter.apps = { get: async (n) => {
if (n === 'sandbox-' + usual) return { name: n };
throw { code: 'entity_not_found' };
} };
puter.getUser = async () => ({ uuid: 'me' });
const r = await create.exec({ name: 'guestbook', code: 'x' }, st);
check("create: a name another Puter user holds gets a random suffix, before anything is written",
r.name !== usual && /^guestbook-[a-z0-9]{6}$/.test(r.name) && calls.writes.length === 1 && calls.writes[0].p.endsWith(`/${r.name}.js`), r.name);
puter.apps = { get: async (n) => ({ name: n, owner: { uuid: 'me' } }) };
resetCalls();
const mine = await create.exec({ name: 'guestbook', code: 'x' }, st);
check('create: …while a sandbox app of our own keeps the usual name', mine.name === usual, mine.name);
delete puter.apps;
delete puter.getUser;
}
resetCalls();
{
const r = await create.exec({ name: 'billing', code: 'x' }, st);
check("create: a name held by another project gets a new worker of this project's own",
/^billing-[a-z0-9]{6}$/.test(r.name) && calls.created.length === 1 && calls.created[0].name === r.name, r.name);
}
resetCalls();
{
const r = await create.exec({ name: 'reports', code: 'x' }, st);
check("create: so does a case-variant of another project's name",
/^reports-[a-z0-9]{6}$/.test(r.name) && calls.created[0].name === r.name, r.name);
}
resetCalls();
{
const r = await create.exec({ name: 'A'.repeat(50), code: 'x' }, st);
check('create: a long name is shortened so the published copy ("-live") keeps the whole suffix',
/^a{38}-[a-z0-9]{6}$/.test(r.name) && (r.name.slice(0, 45) + '-live').includes(r.name.slice(-6)), r.name);
}
resetCalls();
{
const realCreate = puter.workers.create;
puter.workers.create = async () => { throw new TypeError("Cannot read properties of undefined (reading 'uuid')"); };
check("create: puter.js's crash on another user's name is explained to the model",
await rejects(create.exec({ name: 'guestbook', code: 'x' }, st), 'already taken by another Puter user'));
puter.workers.create = realCreate;
}
resetCalls();
check('create: a name that escapes the project dir is refused before any IO',
await rejects(create.exec({ name: '../../escape', code: 'x' }, st), 'Invalid worker name') && calls.listed === 0 && calls.writes.length === 0);
resetCalls();
// "../index" stays INSIDE the project (<root>/index.js), so the path sandbox
// alone let it through and the worker source overwrote the app's entry file.
check('create: a name that resolves inside the project but outside workers/ is refused',
await rejects(create.exec({ name: '../index', code: 'x' }, st), 'Invalid worker name') && calls.listed === 0 && calls.writes.length === 0);
check('create: a name with a slash is refused',
await rejects(create.exec({ name: 'api/v1', code: 'x' }, st), 'Invalid worker name'));
check('create: empty name is refused', await rejects(create.exec({ name: '', code: 'x' }, st), 'non-empty'));
check('create: no project dir → refused for safety', await rejects(create.exec({ name: 'w', code: 'x' }, {}), 'refused for safety'));
resetCalls();
LIST_THROWS = true;
check('create: a failing worker lookup fails CLOSED (nothing written)',
await rejects(create.exec({ name: 'fresh2', code: 'x' }, st)) && calls.writes.length === 0 && calls.created.length === 0);
LIST_THROWS = false;
// === list_workers ============================================================
WORKERS = [own('api'), own('notes'), theirs('billing'), { name: 'legacy', url: 'https://legacy.puter.work/' }];
resetCalls();
{
const r = await list.exec({}, st);
const names = r.workers.map(w => w.name).sort().join(',');
check("list: only this project's workers are listed", r.success === true && names === 'api,notes');
check("list: another project's worker is not listed", !r.workers.some(w => w.name === 'billing'));
check('list: a record with no source path is not claimed', !r.workers.some(w => w.name === 'legacy'));
check('list: each entry still carries name/url/file_path', r.workers.every(w => w.name && w.url && w.file_path));
}
resetCalls();
{
WORKERS = [theirs('billing')];
const r = await list.exec({}, st);
check('list: a project with no workers of its own lists nothing', r.success === true && r.workers.length === 0);
}
check('list: no project dir → refused for safety', await rejects(list.exec({}, {}), 'refused for safety'));
LIST_THROWS = true;
check('list: a failing account lookup fails CLOSED', await rejects(list.exec({}, st)));
LIST_THROWS = false;
// === get_worker ==============================================================
WORKERS = [own('api'), theirs('billing'), { name: 'legacy', url: 'https://legacy.puter.work/' }];
resetCalls();
{
const r = await get.exec({ name: 'api' }, st);
check("get: this project's own worker is returned", r.success === true && r.name === 'api' && !!r.url);
check('get: details come from workers.get (file_uid present)', r.file_uid === 'uid-api');
}
resetCalls();
{
const r = await get.exec({ name: 'billing' }, st);
check("get: another project's worker reports not found", r.success === false && /not found/i.test(r.error));
check("get: and its details are never fetched", calls.got.length === 0);
check("get: nothing about it leaks into the result", !JSON.stringify(r).includes('puter.work'));
}
resetCalls();
{
const r = await get.exec({ name: 'legacy' }, st);
check('get: a record with no source path is not claimed', r.success === false && calls.got.length === 0);
}
resetCalls();
{
const r = await get.exec({ name: 'ghost' }, st);
check('get: an unknown name reports not found', r.success === false);
}
resetCalls();
{
WORKERS = [own('api')];
const r = await get.exec({ name: 'API' }, st);
check('get: a unique case-variant resolves to the real record', r.success === true && r.name === 'api');
}
check('get: empty name is refused', await rejects(get.exec({ name: ' ' }, st), 'non-empty'));
check('get: no project dir → refused for safety', await rejects(get.exec({ name: 'api' }, {}), 'refused for safety'));
resetCalls();
LIST_THROWS = true;
check('get: a failing account lookup fails CLOSED', await rejects(get.exec({ name: 'api' }, st)) && calls.got.length === 0);
LIST_THROWS = false;
// === A duplicated chat's history follows the copy's worker names =============
{
const use = (id, name, input) => ({ role: 'assistant', content: { type: 'tool_use', id, name, input } });
const result = (id, obj) => ({ role: 'user', content: { type: 'tool_result', tool_use_id: id, content: JSON.stringify(obj) } });
const history = [
use('c1', 'create_worker', { name: 'guestbook', code: 'x' }),
result('c1', { success: true, name: 'guestbook-ab12cd', url: 'https://guestbook-ab12cd.puter.work',
note: "Builder named this worker 'guestbook-ab12cd' so its name is unique on Puter. Use this name and url from now on." }),
use('s1', 'set_secret', { worker: 'guestbook-ab12cd', name: 'KEY', value: 'v', live: false }),
result('s1', { success: true, worker: 'guestbook-ab12cd', name: 'KEY', live: false }),
use('e1', 'edit', { path: '/x/workers/guestbook-ab12cd.js', old_content: 'a', new_content: 'b' }),
result('e1', { success: true, path: '/x/workers/guestbook-ab12cd.js', worker: { name: 'guestbook-ab12cd', url: 'https://guestbook-ab12cd.puter.work', live: true } }),
];
const out = WO.rewriteHistory(history, [{ oldName: 'guestbook-ab12cd', newName: 'guestbook-ab12cd-x7k2p1',
oldUrl: 'https://guestbook-ab12cd.puter.work', newUrl: 'https://guestbook-ab12cd-x7k2p1.puter.work' }]);
const parsed = (i) => JSON.parse(out[i].content.content);
check("duplicate: create_worker's result and its note name the copy's worker",
parsed(1).name === 'guestbook-ab12cd-x7k2p1' && parsed(1).note.includes("'guestbook-ab12cd-x7k2p1'") && !parsed(1).note.includes("'guestbook-ab12cd'"), parsed(1).note);
check("duplicate: set_secret's input and result name the copy's worker",
out[2].content.input.worker === 'guestbook-ab12cd-x7k2p1' && parsed(3).worker === 'guestbook-ab12cd-x7k2p1');
check("duplicate: a file save's worker field names the copy's worker",
parsed(5).worker.name === 'guestbook-ab12cd-x7k2p1' && parsed(5).worker.url === 'https://guestbook-ab12cd-x7k2p1.puter.work', JSON.stringify(parsed(5)));
}
// === Wiring: the guards stay hooked up ========================================
check('delete_worker consults WorkerOwnership.ownedWorkers', DELETE.includes('WorkerOwnership.ownedWorkers('));
check('create_worker consults WorkerOwnership.ownedWorkers', CREATE.includes('WO.ownedWorkers(all, root)'));
check('list_workers consults WorkerOwnership.ownedWorkers', LIST.includes('WorkerOwnership.ownedWorkers('));
check('get_worker consults WorkerOwnership.ownedWorkers', GET.includes('WorkerOwnership.ownedWorkers('));
check('all four tools scope to the turn-captured project dir',
[DELETE, CREATE, LIST, GET].every(src => src.includes('window.projectRootDir(state)')));
// The descriptions must tell the model the scope, or it will keep asking for
// workers it cannot see and mis-read the refusals.
check('the read tools describe themselves as project-scoped',
/THIS project/.test(LIST) && /THIS project/.test(GET));
if (failures) {
console.error(`\n${failures} check(s) FAILED`);
process.exit(1);
}
console.log('\nAll worker-tool sandbox checks passed');