/** * The write tools, taken all the way through a real transaction. * * `test/write-tools.test.ts` proves the negative — that a change nobody agreed * to never opens a transaction — against a fake handle. It cannot prove the * positive, because the interesting part of an applied write is what the * database ends up holding: whether the row is really there, and whether the * audit trail says Piggy wrote it. That needs Postgres. * * It needs its own Postgres, too. These cases INSERT, and the development * database is a book people are looking at — an activity that appears in * somebody's feed because a test ran is exactly the kind of thing a CRM must * never do. So the URL is supplied separately and `pig_combined` is refused by * name. * * docker exec pig-ux-db psql -U pig -d postgres -c "CREATE DATABASE pig_a2_scratch" * DATABASE_URL=postgres://pig:pig@localhost:54330/pig_a2_scratch pnpm -F @pig/db run migrate * PIGGY_WRITE_DATABASE_URL=postgres://pig:pig@localhost:54330/pig_a2_scratch \ * pnpm -F @pig/piggy run test:e2e */ import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import test, { after, before } from 'node:test'; import type { ExtensionContext } from '@earendil-works/pi-coding-agent'; import type { Principal } from '@pig/api/src/lib/auth'; import { accounts, activities, createDatabase, demandDeals, users, type Database, } from '@pig/db'; import { and, eq, like } from 'drizzle-orm'; import { createPigWriteTools, type PigWriteDetails } from '../src/write-tools'; const databaseUrl = process.env.PIGGY_WRITE_DATABASE_URL; // A skipped suite that says why beats one that silently passes: these are the // only cases in the repo that watch a write land. if (!databaseUrl) { test.skip('the write-tool E2E needs PIGGY_WRITE_DATABASE_URL pointing at a scratch database'); } if (databaseUrl?.includes('pig_combined')) { throw new Error('The write-tool E2E must never run against the development book.'); } const db: Database = createDatabase({ url: databaseUrl ?? 'postgres://unused', max: 2 }); const ctx = {} as ExtensionContext; const marker = `PIGGY-A2-${randomUUID()}`; const fixture = { userId: '', accountId: '', dealId: '' }; function seller(): Principal { return { userId: fixture.userId, email: `${marker}@example.test`, name: 'Dana Okonjo', isPlatformAdmin: false, teams: [ { team: 'demand', role: 'member' }, { team: 'supply', role: 'member' }, ], via: 'jwt', scopes: ['read', 'write'], }; } function tools(mode: 'confirm' | 'auto', decision: 'apply' | 'reject') { return createPigWriteTools({ db, principal: seller(), mode, propose: async () => decision, }); } function named(list: ReturnType, name: string) { const found = list.find((candidate) => candidate.name === name); assert.ok(found, `${name} is missing`); return found; } function detailsOf(result: { details: unknown }): PigWriteDetails { return result.details as PigWriteDetails; } before(async () => { if (!databaseUrl) return; const [user] = await db .insert(users) .values({ email: `${marker}@example.test`, name: 'Dana Okonjo', authSubject: randomUUID() }) .returning({ id: users.id }); assert.ok(user); fixture.userId = user.id; const [account] = await db .insert(accounts) .values({ name: `${marker} Northwind Robotics`, side: 'demand' }) .returning({ id: accounts.id }); assert.ok(account); fixture.accountId = account.id; const [deal] = await db .insert(demandDeals) .values({ accountId: account.id, name: `${marker} H200 reserved`, stage: 'proposal' }) .returning({ id: demandDeals.id }); assert.ok(deal); fixture.dealId = deal.id; }); after(async () => { if (!databaseUrl) return; // Activities and deals cascade from the account; the user does not. if (fixture.accountId) await db.delete(accounts).where(eq(accounts.id, fixture.accountId)); if (fixture.userId) await db.delete(users).where(eq(users.id, fixture.userId)); // Closed explicitly: an open pool keeps the event loop alive, and a suite // that passes but never exits looks exactly like one that hangs. await db.$client.end({ timeout: 5 }); }); test('an approved activity is written, and marked as Piggy’s', { skip: !databaseUrl }, async () => { const result = await named(tools('confirm', 'apply'), 'pig_log_activity').execute( 'call-1', { type: 'call', subject: 'Pricing call with procurement', body: 'They want H200 pricing before the board meets.', accountId: fixture.accountId, }, undefined, undefined, ctx, ); assert.equal(detailsOf(result).status, 'applied'); const written = await db .select() .from(activities) .where(eq(activities.accountId, fixture.accountId)); assert.equal(written.length, 1); const [row] = written; assert.ok(row); assert.equal(row.subject, 'Pricing call with procurement'); assert.equal(row.actorUserId, fixture.userId, 'the write is attributed to the caller'); // The row IS its own audit event, so the provenance rides on the external id. assert.match(row.externalId ?? '', /^piggy:/); const piggyRows = await db .select() .from(activities) .where(and(eq(activities.accountId, fixture.accountId), like(activities.externalId, 'piggy:%'))); assert.equal(piggyRows.length, 1, 'every write Piggy made is selectable by that prefix'); }); test('a rejected change leaves the book exactly as it was', { skip: !databaseUrl }, async () => { const before = await db.select().from(demandDeals).where(eq(demandDeals.id, fixture.dealId)); const activitiesBefore = await db .select() .from(activities) .where(eq(activities.demandDealId, fixture.dealId)); const result = await named(tools('confirm', 'reject'), 'pig_update_deal_stage').execute( 'call-2', { dealType: 'demand', dealId: fixture.dealId, stage: 'procurement', reason: 'Legal cleared the MSA this morning.', }, undefined, undefined, ctx, ); assert.equal(detailsOf(result).status, 'declined'); const after = await db.select().from(demandDeals).where(eq(demandDeals.id, fixture.dealId)); assert.equal(after[0]?.stage, before[0]?.stage, 'the stage did not move'); const activitiesAfter = await db .select() .from(activities) .where(eq(activities.demandDealId, fixture.dealId)); assert.equal(activitiesAfter.length, activitiesBefore.length, 'no audit row was written'); }); test('an approved stage change carries Piggy in its audit row', { skip: !databaseUrl }, async () => { const result = await named(tools('confirm', 'apply'), 'pig_update_deal_stage').execute( 'call-3', { dealType: 'demand', dealId: fixture.dealId, stage: 'procurement', reason: 'Legal cleared the MSA this morning.', }, undefined, undefined, ctx, ); assert.equal(detailsOf(result).status, 'applied'); const [deal] = await db.select().from(demandDeals).where(eq(demandDeals.id, fixture.dealId)); assert.equal(deal?.stage, 'procurement'); const [audit] = await db .select() .from(activities) .where(and(eq(activities.demandDealId, fixture.dealId), eq(activities.type, 'stage_change'))); assert.ok(audit, 'the mutation convention wrote its audit row'); assert.equal(audit.subject, 'proposal → procurement'); assert.equal(audit.actorUserId, fixture.userId, 'still the caller, never an elevated principal'); // `actorAgent` on the column stays null because the request really did // authenticate as a person; the provenance goes where the caller legitimately // controls the content. assert.equal(audit.meta?.actorAgent, 'piggy'); assert.equal(audit.meta?.piggyTool, 'pig_update_deal_stage'); assert.equal(audit.meta?.piggyReason, 'Legal cleared the MSA this morning.'); assert.match(audit.body ?? '', /Recorded by Piggy \(pig_update_deal_stage\) on behalf of Dana/); }); test('a task becomes a calendar entry the user owns', { skip: !databaseUrl }, async () => { const result = await named(tools('auto', 'apply'), 'pig_create_task').execute( 'call-4', { title: 'Send the H200 quote', startsAt: '2026-09-01', accountId: fixture.accountId, }, undefined, undefined, ctx, ); const details = detailsOf(result); assert.equal(details.status, 'applied'); assert.ok(details.recordId); });