__tests__/role-requirement.test.js
Rama main · réplica de lectura del repositorio original.
/*
* A ROLE MAY SAY IT CANNOT RUN JUST ANYWHERE — and the projection must not eat that.
*
* capcut-video-editor needs CDP drag-interception: the cluster's browser loads the CapCut editor fine
* and then cannot drop a clip on the timeline. The requirement went into the role's data, the ring
* can route on it, the gate in server.js reads it — and the automation still started on the cluster.
*
* Because getRole() does not return the role, it returns a PROJECTION of it onto a fixed list of
* fields: site, group, label, description, tools, prompt. Anything else disappears without a word. So
* roles.get() handed back a role with no requirement, deviceNeedOf saw null, and the gate never fired.
*
* save() had the same fixed list, which is worse: opening the role in the console and saving it would
* have dropped the requirement out of the data itself.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createRequire } from 'module';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const require = createRequire(import.meta.url);
let BASE;
let userRoles;
beforeAll(() => {
// DIR is resolved at module load from PROFILE_DIR, so it must be set before the require.
BASE = mkdtempSync(join(tmpdir(), 'gb-roles-'));
process.env.PROFILE_DIR = BASE;
mkdirSync(join(BASE, 'roles'), { recursive: true });
userRoles = require('../src/userRoles.js');
writeFileSync(join(BASE, 'roles', 'capcut-video-editor.json'), JSON.stringify({
id: 'capcut-video-editor', site: 'capcut.com', group: 'Video',
label: 'CapCut - Video editor', description: 'Edits a short vertical video.',
tools: ['look', 'read', 'open', 'click', 'type', 'scroll', 'note', 'finish', 'act'],
prompt: 'Open CapCut, import the clip, put it on the timeline, trim it, export it.',
require: { cdp: true, features: ['drag_xy'] },
source: 'user',
}, null, 2));
writeFileSync(join(BASE, 'roles', 'plain-role.json'), JSON.stringify({
id: 'plain-role', label: 'Plain', description: 'Runs anywhere.',
tools: null, prompt: 'Do the ordinary thing, anywhere it happens to run.', source: 'user',
}, null, 2));
});
afterAll(() => { try { rmSync(BASE, { recursive: true, force: true }); } catch { /* temp */ } });
describe('the requirement survives being read', () => {
it('getRole carries require — the field the projection used to drop', () => {
const r = userRoles.getRole('capcut-video-editor');
expect(r, 'the role should be found').toBeTruthy();
expect(r.require).toEqual({ cdp: true, features: ['drag_xy'] });
});
it('a role with no requirement reads as null, so ordinary flows are unaffected', () => {
expect(userRoles.getRole('plain-role').require).toBeNull();
});
it('listRoles shows it too, so a picker can say where a role runs', () => {
const row = userRoles.listRoles().find((x) => x.name === 'capcut-video-editor');
expect(row.require).toEqual({ cdp: true, features: ['drag_xy'] });
});
});
describe('the requirement survives being saved', () => {
it('editing the role does not wipe it out of the data', () => {
// The dangerous half: a read that drops the field is a bug, a WRITE that drops it is data loss.
const r = userRoles.getRole('capcut-video-editor');
userRoles.save({
id: 'capcut-video-editor', label: r.label, site: r.site, group: r.group,
description: 'edited description', tools: r.tools, prompt: r.prompt, require: r.require,
}, r.tools);
const onDisk = JSON.parse(readFileSync(join(BASE, 'roles', 'capcut-video-editor.json'), 'utf8'));
expect(onDisk.description).toBe('edited description');
expect(onDisk.require).toEqual({ cdp: true, features: ['drag_xy'] });
});
});
describe('only requirements the ring understands', () => {
it('unknown keys and unknown platforms are dropped, not passed through', () => {
// A requirement no device could ever meet is indistinguishable from "no device is online", so a
// typo in the data must not become one.
expect(userRoles.normRequire({ cdp: true, features: ['drag_xy'], nonsense: 1 }))
.toEqual({ cdp: true, features: ['drag_xy'] });
expect(userRoles.normRequire({ platform: 'toaster', cdp: true })).toEqual({ cdp: true });
expect(userRoles.normRequire({ platform: 'desktop' })).toEqual({ platform: 'desktop' });
});
it('nothing to require reads as null rather than an empty object', () => {
// An empty object is truthy, and the gate branches on truthiness: {} would have made every role
// a device role and stopped every flow.
expect(userRoles.normRequire({})).toBeNull();
expect(userRoles.normRequire(null)).toBeNull();
expect(userRoles.normRequire({ cdp: false })).toBeNull();
});
});