__tests__/people.test.js
Rama main · réplica de lectura del repositorio original.
/**
* People memory across posts and outcome learning: exchanges land on the right person, promises /
* asks / commercial signals are picked out, the drafter's profile reads right and excludes the current
* post, outcomes classify as-is / edited / rewritten and keep lessons, leads list. Temp PROFILE_DIR.
*/
import { describe, it, expect, beforeAll } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
let people;
beforeAll(async () => { process.env.PROFILE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'ppl-')); people = await import('../src/people.js'); });
const me = 'Wesley Stoep';
const isMe = (n) => String(n.author || '').toLowerCase() === me.toLowerCase();
const node = (id, author, text, extra = {}) => ({ id, author, text, isReply: !!extra.replyTo, replyTo: extra.replyTo || '', when: extra.when || '', i: Number(id.replace(/\D/g, '')) });
function entriesOf(branch) { return branch.map((n) => ({ node: n, branch, isMe, needsReply: !isMe(n) && (!n.isReply || n.replyTo === me) })); }
describe('people memory', () => {
it('remembers exchanges per person with promises, asks and commercial signals, across posts', () => {
const b1 = [node('c1', 'Ilya Elbert', 'Which stack do you use for auth? Entra was brutal'), node('c2', me, "I'll put together a walkthrough on the Entra part", { replyTo: 'Ilya Elbert' }), node('c3', 'Ilya Elbert', 'i was so angry at entra with all the policies lol', { replyTo: me })];
const touched = people.remember('facebook', { postId: 'p1', postText: 'Vibe coding post', nodes: b1 }, entriesOf(b1), { now: 1000, urlOf: (n) => 'https://fb/' + n.id });
expect(touched).toEqual(['Ilya Elbert']);
const b2 = [node('c9', 'Ilya Elbert', 'what does it cost to have you build this for us?')];
people.remember('facebook', { postId: 'p2', postText: 'Second post', nodes: b2 }, entriesOf(b2), { now: 2000 });
const r = people.load('facebook', 'Ilya Elbert');
expect(r.exchanges.map((x) => x.who)).toEqual(['them', 'you', 'them', 'them']);
expect(Object.keys(r.posts)).toEqual(['p1', 'p2']);
expect(r.promises[0].text).toMatch(/put together a walkthrough/);
expect(r.asks[0].text).toMatch(/Which stack/);
expect(r.signals[0].text).toMatch(/what does it cost/); expect(r.signals[0].postId).toBe('p2');
expect(people.isLead('facebook', 'Ilya Elbert')).toBe(true);
// c3 came after the owner's c2 in the same branch: he came back — the outcome a reply is for
expect(r.outcomes.repliedBack).toBe(1);
expect(people.worthOf(r)).toBeGreaterThanOrEqual(60); expect(people.worthOf(null)).toBe(0);
// a second pass over the same branch adds nothing twice
people.remember('facebook', { postId: 'p1', postText: 'Vibe coding post', nodes: b1 }, entriesOf(b1), { now: 3000 });
expect(people.load('facebook', 'Ilya Elbert').exchanges.length).toBe(4);
});
it('leaves side conversations out and starts a record only for people the owner talks with', () => {
const b = [node('d1', 'Peter', 'nice game'), node('d2', 'Dennis', 'meh', { replyTo: 'Peter' })];
const entries = [{ node: b[0], branch: b, isMe, needsReply: true }, { node: b[1], branch: b, isMe, needsReply: false }];
people.remember('facebook', { postId: 'p3', postText: 'x', nodes: b }, entries, { now: 4000 });
expect(people.load('facebook', 'Peter').exchanges.length).toBe(1);
// Dennis talked to Peter, not to the owner — but the branch holds no owner reply, so Dennis is "them" on a root-less…
// …no: he is a reply not to the owner in a branch without the owner: not remembered
expect(people.load('facebook', 'Dennis')).toBeNull();
});
it('gives the drafter a profile that excludes the current post and carries the promise and the signal', () => {
const p = people.profileOf('facebook', 'Ilya Elbert', { exceptPostId: 'p2' });
expect(p).toMatch(/PERSON MEMORY — Ilya Elbert: 4 exchange/); expect(p).toMatch(/"Vibe coding post"/); expect(p).not.toMatch(/"Second post"/);
expect(p).toMatch(/You promised them/); expect(p).toMatch(/COMMERCIAL SIGNAL/); expect(p).toMatch(/never re-promise/);
expect(people.profileOf('facebook', 'Nobody')).toBe('');
});
it('classifies outcomes and keeps rewrites as lessons the drafter reads', () => {
expect(people.recordOutcome('facebook', 'Ilya Elbert', { draft: 'Thanks Ilya, that is fair.', posted: 'Thanks Ilya, that is fair.' }).kind).toBe('asIs');
expect(people.recordOutcome('facebook', 'Ilya Elbert', { draft: 'Thanks Ilya, that is a fair point about Entra and the keys.', posted: 'Thanks Ilya, that is a fair point about Entra and keys!' }).kind).toBe('edited');
expect(people.recordOutcome('facebook', 'Ilya Elbert', { draft: 'Great question! Absolutely, I would be happy to elaborate on that.', posted: 'ha yeah entra is a pain' }).kind).toBe('rewritten');
const o = people.outcomes(); expect(o).toMatchObject({ drafted: 3, asIs: 1, edited: 1, rewritten: 1, asIsRate: 33 });
expect(people.editLessons()).toMatch(/drafted: "Great question!/); expect(people.editLessons()).toMatch(/posted: "ha yeah entra/);
expect(people.load('facebook', 'Ilya Elbert').outcomes).toMatchObject({ drafted: 3, asIs: 1, edited: 1, rewritten: 1, repliedBack: 1 });
});
it("on someone else's post, their post and the owner's root comment land on their record", () => {
const b = [node('e0', me, 'super clean stack'), node('e1', 'Marta', 'thanks! which db?', { replyTo: me })];
const entries = b.map((n) => ({ node: n, branch: b, isMe, needsReply: !isMe(n) }));
people.remember('facebook', { postId: 'p9', postText: 'my auth stack is entra + supabase', postAuthor: 'Marta', nodes: b }, entries, { now: 9000 });
const r = people.load('facebook', 'Marta');
expect(r.exchanges.map((x) => [x.who, x.kind || '', x.text.slice(0, 12)])).toEqual([['them', 'post', 'my auth stac'], ['you', '', 'super clean '], ['them', '', 'thanks! whic']]);
expect(r.posts.p9.theirs).toBe(true);
expect(people.profileOf('facebook', 'Marta')).toMatch(/Marta POSTED: my auth stack/);
expect(people.profileOf('facebook', 'Marta')).toMatch(/YOU: super clean stack/);
expect(r.outcomes.repliedBack).toBe(1); // she answered the owner's comment
});
it('lists people newest first and leads only on request', () => {
const all = people.list('facebook'); expect(all.map((p) => p.name)).toEqual(['Marta', 'Peter', 'Ilya Elbert']);
const leads = people.list('facebook', { leadsOnly: true }); expect(leads.map((p) => p.name)).toEqual(['Ilya Elbert']); expect(leads[0].signals[0]).toMatch(/cost/);
});
});