__tests__/herald-setup.test.js
Rama main · réplica de lectura del repositorio original.
/**
* Herald setup roles — the intelligence of the multi-step create flows. A role's prompt is its
* brain, so these pin the guardrails that a vague prompt would drop: the Facebook page walk must
* name the profile-switcher bottom sheet and choose "Create a new Page" NEVER "Use existing profile"
* (which would convert the owner's account); the group walk must default to Public and invite nobody;
* every setup walk must STOP on a human-only step rather than fake it, and act through the gate.
*/
import { describe, it, expect } from 'vitest';
import { get, ROLES } from '../src/roles.js';
const SETUP = ['herald.facebook.setup', 'herald.facebook.group.setup', 'herald.linkedin.setup', 'herald.reddit.setup'];
describe('every Herald setup role shares the safety spine', () => {
it('is a WRITER (has act) so nothing is created without the owner approving', () => {
for (const name of SETUP) expect(get(name).tools, name).toContain('act');
});
it('STOPS on a human-only step rather than faking past it', () => {
for (const name of SETUP) {
const p = get(name).prompt;
expect(p, name).toMatch(/STOP/);
expect(p, name).toMatch(/CAPTCHA|verification|phone/i);
expect(p, name).toMatch(/never (guess past it|fake it)/i);
}
});
it('is decisive, not an explorer — the wandering failure is named and bounded', () => {
for (const name of SETUP) expect(get(name).prompt, name).toMatch(/TWO looks|not an explorer|decisive/i);
});
});
describe('the Facebook PAGE walk knows the real flow', () => {
const p = get('herald.facebook.setup').prompt;
it('names the profile switcher and its bottom sheet — the landmark from the real UI', () => {
expect(p).toMatch(/RED DOT/);
expect(p).toMatch(/BOTTOM SHEET/i);
expect(p).toMatch(/Create Facebook Page/);
});
it('chooses "Create a new Page" and REFUSES "Use your existing profile" — never convert the owner', () => {
expect(p).toMatch(/Create a new Page/);
expect(p).toMatch(/DO NOT choose "Use your existing profile"/);
expect(p).toMatch(/destructive|converts?|turns the OWNER/i);
});
it('fills name, category and bio from the goal EXACTLY, and never posts or boosts in setup', () => {
expect(p).toMatch(/NAME/);
expect(p).toMatch(/CATEGORY/i);
expect(p).toMatch(/BIO|description/i);
expect(p).toMatch(/EXACTLY/);
expect(p).toMatch(/do NOT post yet|do NOT run ads/i);
});
it('has a desktop fallback so a non-mobile layout is not a dead end', () => {
expect(p).toMatch(/pages\/create/);
});
it('treats the Create act as the FINISH LINE — the welcome tour confirms it, never re-create', () => {
// The live CaseHandoff run created the page, then wandered looking for the URL, landed back on
// /pages/create and started a SECOND create. These guards are the fix.
expect(p).toMatch(/THE PAGE EXISTS/);
expect(p).toMatch(/finish line/i);
expect(p).toMatch(/tour IS YOUR CONFIRMATION|welcome tour that confirms/i);
expect(p).toMatch(/CREATE ONCE|never (create )?twice|duplicate pages/i);
expect(p).toMatch(/never (search for the page|go looking)|do not (go looking|reopen)/i);
});
});
describe('the Facebook GROUP walk builds a community, not a page', () => {
const p = get('herald.facebook.group.setup').prompt;
it('distinguishes a GROUP (community) from a PAGE (broadcast)', () => {
expect(p).toMatch(/COMMUNITY/i);
expect(p).toMatch(/distinct from a PAGE|not a PAGE/i);
});
it('defaults to PUBLIC and never makes it secret unless told', () => {
expect(p).toMatch(/Public/);
expect(p).toMatch(/never make it Secret\/Private unless/i);
});
it('invites NOBODY this run — no spamming the owner\'s friends', () => {
expect(p).toMatch(/invite NOBODY|invite no one/i);
});
});
describe('the setup/operate split is intact', () => {
it('setup roles CREATE (page/group), operate roles POST — never the same role doing both', () => {
expect(get('herald.facebook.setup').prompt).toMatch(/CREATE ONE FACEBOOK PAGE/);
expect(get('herald.facebook.operate').prompt).toMatch(/PUBLISH ONE PREPARED POST/);
expect(get('herald.facebook.group.operate').prompt).toMatch(/PUBLISH ONE PREPARED POST inside a brand's Facebook GROUP/);
});
it('all twelve herald roles are registered and grouped under Herald (setup/onboard/operate + insights + two groups)', () => {
const herald = Object.entries(ROLES).filter(([k]) => k.startsWith('herald.'));
expect(herald).toHaveLength(12);
for (const [, r] of herald) expect(r.group).toBe('Herald');
});
it('insights is READ-ONLY (no act) — it only looks', () => {
expect(get('herald.facebook.insights').tools).not.toContain('act');
expect(get('herald.facebook.insights').prompt).toMatch(/only LOOK|read-only|never post/i);
});
it('group engage is value-first and acts as the OWNER (Facebook groups are personal, not pages)', () => {
const p = get('herald.facebook.groups.engage').prompt;
expect(get('herald.facebook.groups.engage').tools).toContain('act');
expect(p).toMatch(/NEVER A PITCH|DO NOT post it/i);
expect(p).toMatch(/NO product mention|NO link|worse than silence/i);
expect(p).toMatch(/as the OWNER|owner's own (personal )?profile/i);
expect(p).toMatch(/NOT the brand's page|never pretending to be it|not as a page/i);
// Replies in the POST's own language — the multilingual advantage, made an explicit rule.
expect(p).toMatch(/REPLY IN THE POST'S OWN LANGUAGE|Read the language of the post/i);
expect(p).toMatch(/Dutch.*Dutch|Spanish|meet each person in theirs/i);
});
it('group find joins to belong, never mass-joins or posts', () => {
const p = get('herald.facebook.groups.find').prompt;
expect(p).toMatch(/to belong|never to advertise/i);
expect(p).toMatch(/AT MOST a few|never mass-join/i);
expect(p).toMatch(/Do NOT post|joining only/i);
});
it('group find answers a private group\'s join questions HONESTLY as a vendor — never faking a professional role', () => {
// A private group gates joins with questions ("your role in the trade?"). The owner is a vendor,
// not a professional; answering as one would be a lie that gets the account banned.
const p = get('herald.facebook.groups.find').prompt;
expect(p).toMatch(/JOIN QUESTIONS|answer.*questions/i);
expect(p).toMatch(/AS A VENDOR|you build\/run the brand/i);
expect(p).toMatch(/NEVER claim to be a professional|do NOT fake/i);
expect(p).toMatch(/pending/i);
});
it('herald.facebook.onboard is the FILL role — separate from create (setup) and posting (operate)', () => {
const o = get('herald.facebook.onboard');
expect(o.prompt).toMatch(/FILL ONE EMPTY FACEBOOK PAGE/);
expect(o.tools).toContain('make_brand_image');
expect(o.tools).toContain('upload_image');
});
it('onboard knows the real FB-page edit landmarks — the Edit hub, and the SECOND click (Upload photo) that reveals the file field', () => {
// The live CaseHandoff onboard opened "Acties voor profielfoto" but never clicked "Foto uploaden",
// so the file field never appeared and it looped. These are the fix, from the real UI.
const p = get('herald.facebook.onboard').prompt;
expect(p).toMatch(/EDIT HUB|Edit profile view|Profiel bewerken/i);
expect(p).toMatch(/Foto uploaden|Upload photo/);
expect(p).toMatch(/second (step|click)|reveals the file field/i);
expect(p).toMatch(/Bio bewerken|Edit bio/i); // the About lives here too
});
it('the photo dialog has its OWN save — never the page-level "Wijzigingen opslaan"', () => {
// The live run uploaded the profile but clicked "Wijzigingen opslaan" (page save) instead of the
// photo dialog\'s "Opslaan", so the picture never set while the cover (which used its own save) did.
const p = get('herald.facebook.onboard').prompt;
expect(p).toMatch(/PHOTO DIALOG/);
expect(p).toMatch(/NOT .*Wijzigingen opslaan|not .*Save changes/i);
expect(p).toMatch(/Opslaan.*Save.*Apply|dialog's OWN/i);
});
it('the first post uses the on-page composer, not the Planner or a plugins URL', () => {
const p = get('herald.facebook.onboard').prompt;
expect(p).toMatch(/Deel een gedachte|Schrijf iets|Write something|Create post/);
expect(p).toMatch(/NOT use the Planner|do NOT open a plugins|not .*Opmerking plaatsen/i);
});
it('after typing the post, the only next action is Publish — never re-click the text field', () => {
// The live run typed the post then clicked the text field over and over instead of publishing.
const p = get('herald.facebook.onboard').prompt;
expect(p).toMatch(/ONLY NEXT ACTION IS TO PUBLISH|Do NOT click the text box again/i);
expect(p).toMatch(/Plaatsen|Posten|Publiceren/);
expect(p).toMatch(/TWO DIFFERENT elements|distinct BUTTON/i);
});
});
describe('the setup roles work in ANY language, not just English', () => {
it('Facebook page + group setup match controls by meaning, with examples-not-a-list, in any language', () => {
for (const name of ['herald.facebook.setup', 'herald.facebook.group.setup']) {
const p = get(name).prompt;
expect(p, name).toMatch(/ANY LANGUAGE/);
expect(p, name).toMatch(/WHAT IT DOES|by their function|by MEANING/i);
expect(p, name).toMatch(/examples?[^.]*not a list|illustrations,? not a list/i);
expect(p, name).toMatch(/never loop waiting for an English label/i);
}
});
it('the desktop page-create form is described as ONE screen (name, category, create) — no phantom Next', () => {
const p = get('herald.facebook.setup').prompt;
expect(p).toMatch(/ONE-SCREEN form/);
expect(p).toMatch(/no Next on this screen|do not hunt for them/i);
});
});
describe('the desktop create form handles the category autocomplete and cannot loop', () => {
const p = get('herald.facebook.setup').prompt;
it('says the category is an autocomplete — type once, then CLICK a suggestion, never retype', () => {
expect(p).toMatch(/CATEGORY IS AN AUTOCOMPLETE/);
expect(p).toMatch(/CLICK the closest suggestion/);
expect(p).toMatch(/submit=true/);
expect(p).toMatch(/AT MOST twice/);
});
it('has a HARD anti-loop: never type the same field more than twice', () => {
expect(p).toMatch(/HARD ANTI-LOOP/);
expect(p).toMatch(/never type into the SAME field more than twice/);
expect(p).toMatch(/waiting for you to CLICK/);
});
});