/** * The write tools, up to but not through the transaction. * * What these cases pin is the promise the approval flow makes: that a change * the user has not agreed to leaves the database exactly as it was. So the * database here is a fake whose only real job is to COUNT how many transactions * were opened, because "nothing was written" is not a claim about a row — it is * a claim that no write was ever attempted, and a row check would pass just as * happily against a write that failed for some other reason. * * `e2e/write-tools.test.ts` takes the applied path through a real Postgres and * reads the audit row back. This file deliberately never reaches one: the unit * suite runs in CI before the migration step, against a database with no * tables. */ import assert from 'node:assert/strict'; import test from 'node:test'; import type { AgentToolResult, ExtensionContext } from '@earendil-works/pi-coding-agent'; import { PIGGY_ALWAYS_CONFIRM_KINDS, isGuardedKind, requiresApproval, type PiggyApprovalDecision, type PiggyProposedChange, } from '@pig/core'; import type { Principal } from '@pig/api/src/lib/auth'; import type { Database } from '@pig/db'; import { getTableName, type Table } from 'drizzle-orm'; import { createPigWriteTools, type PigWriteDetails } from '../src/write-tools'; const ctx = {} as ExtensionContext; const ACCOUNT_ID = '11111111-1111-4111-8111-111111111111'; const DEAL_ID = '22222222-2222-4222-8222-222222222222'; /** A member of both pipelines: the ordinary GTM user, not an admin. */ function seller(overrides: Partial = {}): Principal { return { userId: '33333333-3333-4333-8333-333333333333', email: 'dana@primeintellect.ai', name: 'Dana Okonjo', isPlatformAdmin: false, teams: [ { team: 'demand', role: 'member' }, { team: 'supply', role: 'member' }, ], via: 'jwt', scopes: ['read', 'write'], ...overrides, }; } interface FakeDatabase { db: Database; /** Transactions opened. `executeMutation` opens exactly one per write. */ transactions: number; } /** * Reads answer from a fixed table of rows; writes are counted and refused. * * The refusal matters as much as the count: a test that let a write "succeed" * against a fake would be asserting on the fake. Anything that gets as far as * opening a transaction here fails loudly. */ function fakeDatabase(rows: Record[]>): FakeDatabase { const state: FakeDatabase = { transactions: 0, db: undefined as unknown as Database }; const selection = (table: Table) => ({ where: () => ({ limit: async () => rows[getTableName(table)] ?? [], }), }); // The shape drizzle exposes is far wider than the four calls these tools // make, so the cast is to the handle rather than to `any` at each call site. state.db = { select: () => ({ from: (table: Table) => selection(table) }), transaction: async () => { state.transactions += 1; throw new Error('the fake database refuses to write'); }, } as unknown as Database; return state; } function tool(tools: ReturnType, name: string) { const found = tools.find((candidate) => candidate.name === name); assert.ok(found, `${name} is not among ${tools.map((t) => t.name).join(', ')}`); return found; } function detailsOf(result: { details: unknown }): PigWriteDetails { return result.details as PigWriteDetails; } function textOf(result: AgentToolResult): string { const [first] = result.content; return first?.type === 'text' ? first.text : ''; } test('read_only mode offers no write tool at all', () => { const { db } = fakeDatabase({}); const tools = createPigWriteTools({ db, principal: seller(), mode: 'read_only', propose: async () => 'apply', }); assert.deepEqual(tools, [], 'a read-only session must not be told writes are possible'); }); test('the write surface is exactly five pig_ tools, each teachable to the model', () => { const { db } = fakeDatabase({}); const tools = createPigWriteTools({ db, principal: seller(), mode: 'confirm', propose: async () => 'apply', }); assert.deepEqual( tools.map((candidate) => candidate.name).sort(), [ 'pig_create_contact', 'pig_create_task', 'pig_log_activity', 'pig_update_deal_stage', 'pig_update_record_fields', ], 'the write surface is closed, and grows only by decision', ); for (const candidate of tools) { // Without a snippet the tool is absent from the system prompt's tool list. assert.ok(candidate.promptSnippet, `${candidate.name} has no promptSnippet`); assert.ok(candidate.promptGuidelines?.length, `${candidate.name} teaches the model nothing`); } }); test('a confirm-mode write proposes first and touches nothing until it is answered', async () => { const state = fakeDatabase({ accounts: [{ name: 'Northwind Robotics' }], }); const proposed: Omit[] = []; let released: ((decision: PiggyApprovalDecision) => void) | undefined; const tools = createPigWriteTools({ db: state.db, principal: seller(), mode: 'confirm', propose: async (change) => { proposed.push(change); // Held open, so the assertions below run at the exact moment a user is // still looking at the card: the point at which nothing may have been // written yet. return new Promise((resolve) => { released = resolve; }); }, }); const running = tool(tools, 'pig_log_activity').execute( 'call-1', { type: 'call', subject: 'Pricing call with procurement', body: 'They want H200 pricing before the board meets.', accountId: ACCOUNT_ID, }, undefined, undefined, ctx, ); // Let the proposal be raised, then look at the world before answering. await new Promise((resolve) => setImmediate(resolve)); assert.equal(proposed.length, 1, 'the change was proposed'); assert.equal(state.transactions, 0, 'no transaction was opened while the user was deciding'); const [change] = proposed; assert.ok(change); assert.equal(change.tool, 'pig_log_activity'); assert.equal(change.kind, 'activity'); assert.equal(change.summary, 'Log a call on Northwind Robotics'); assert.equal(change.record?.label, 'Northwind Robotics', 'the card names the record, not a uuid'); assert.deepEqual( change.fields.map((field) => field.label), ['Type', 'Subject', 'Note'], 'the card shows the change field by field', ); assert.ok(released, 'propose was never called'); released('reject'); const result = await running; assert.equal(state.transactions, 0, 'a rejected change never reaches the database'); assert.equal(detailsOf(result).status, 'declined'); assert.match( textOf(result), /NOT SAVED/, 'the model is told plainly that nothing was written', ); assert.match(textOf(result), /declined/i); }); test('a stage change shows the value it is replacing, because a diff needs both', async () => { const state = fakeDatabase({ demand_deals: [{ name: 'Northwind — H200 reserved', stage: 'proposal' }], }); const proposed: Omit[] = []; const tools = createPigWriteTools({ db: state.db, principal: seller(), mode: 'confirm', propose: async (change) => { proposed.push(change); return 'reject'; }, }); await tool(tools, 'pig_update_deal_stage').execute( 'call-2', { dealType: 'demand', dealId: DEAL_ID, stage: 'procurement', reason: 'Legal cleared the MSA this morning.', }, undefined, undefined, ctx, ); const [change] = proposed; assert.ok(change); assert.deepEqual(change.fields[0], { label: 'Stage', value: 'Procurement', previous: 'Proposal', }); assert.equal(state.transactions, 0); }); test('auto mode writes without asking, because none of these kinds is guarded', async () => { const state = fakeDatabase({ accounts: [{ name: 'Northwind Robotics' }] }); let asked = 0; const tools = createPigWriteTools({ db: state.db, principal: seller(), mode: 'auto', propose: async () => { asked += 1; return 'apply'; }, }); // The fake refuses every write, which is the point: what is asserted is that // the tool got as far as opening a transaction with nobody asked. await assert.rejects( () => tool(tools, 'pig_log_activity').execute( 'call-3', { type: 'note', subject: 'Left a voicemail', accountId: ACCOUNT_ID }, undefined, undefined, ctx, ), /refuses to write/, ); assert.equal(asked, 0, 'auto mode does not ask for an ordinary activity'); assert.equal(state.transactions, 1, 'auto mode goes straight to the write'); }); test('a capability failure is reported to the model, not thrown into the stream', async () => { const state = fakeDatabase({ accounts: [{ name: 'Northwind Robotics' }] }); const tools = createPigWriteTools({ db: state.db, // A read-only credential in a session the user put into auto mode. The // permission is the user's own, so this is an answer, not a fault. principal: seller({ scopes: ['read'] }), mode: 'auto', propose: async () => 'apply', }); const result = await tool(tools, 'pig_log_activity').execute( 'call-4', { type: 'note', subject: 'Left a voicemail', accountId: ACCOUNT_ID }, undefined, undefined, ctx, ); assert.equal(state.transactions, 0, 'permission is checked before any transaction opens'); assert.equal(detailsOf(result).status, 'refused'); assert.equal(detailsOf(result).reason, 'insufficient_scope'); assert.match(textOf(result), /NOT SAVED/); assert.match(textOf(result), /permission/i); }); test('a capability the user lacks on this team is an answer, not a crash', async () => { const state = fakeDatabase({ demand_deals: [{ name: 'Northwind — H200 reserved', stage: 'proposal' }], }); const tools = createPigWriteTools({ db: state.db, // Supply-side only. `updateDemandDealMutationDefinition` requires // `deal:write` on `demand`, so this is the everyday case of a person being // asked to move somebody else's deal — not a misconfiguration. principal: seller({ teams: [{ team: 'supply', role: 'member' }] }), mode: 'auto', propose: async () => 'apply', }); const result = await tool(tools, 'pig_update_deal_stage').execute( 'call-8', { dealType: 'demand', dealId: DEAL_ID, stage: 'procurement', reason: 'They asked me to move it.', }, undefined, undefined, ctx, ); assert.equal(state.transactions, 0, 'permission is checked before any transaction opens'); assert.equal(detailsOf(result).status, 'refused'); assert.equal(detailsOf(result).reason, 'insufficient_permission'); // Thrown, this would end the turn on the user's own permissions, which reads // to them as Piggy being broken rather than as PIG saying no. assert.match(textOf(result), /NOT SAVED/); assert.match(textOf(result), /deal:write/); assert.match(textOf(result), /do not retry it/); }); test('every kind the write surface proposes is one auto mode may apply', async () => { const state = fakeDatabase({ accounts: [{ name: 'Northwind Robotics' }], demand_deals: [{ name: 'Northwind — H200 reserved', stage: 'proposal' }], }); const kinds = new Map(); const tools = createPigWriteTools({ db: state.db, principal: seller(), mode: 'confirm', propose: async (change) => { kinds.set(change.tool, change.kind); return 'reject'; }, }); // One call per tool, in confirm mode, so each one has to raise a card and // name the kind it belongs to. const calls: [string, Record][] = [ ['pig_log_activity', { type: 'note', subject: 'Left a voicemail', accountId: ACCOUNT_ID }], [ 'pig_create_contact', { accountId: ACCOUNT_ID, fullName: 'Marta Reyes', role: 'staff', title: 'VP Infrastructure' }, ], [ 'pig_update_deal_stage', { dealType: 'demand', dealId: DEAL_ID, stage: 'procurement', reason: 'Legal cleared it.' }, ], [ 'pig_update_record_fields', { recordType: 'account', recordId: ACCOUNT_ID, reason: 'Corrected on the call.', country: 'Germany' }, ], ['pig_create_task', { title: 'Send the H200 quote', startsAt: '2026-09-01', accountId: ACCOUNT_ID }], ]; for (const [name, params] of calls) { await tool(tools, name).execute('call-kind', params, undefined, undefined, ctx); } assert.deepEqual( Object.fromEntries([...kinds].sort()), { pig_create_contact: 'contact', pig_create_task: 'task', pig_log_activity: 'activity', pig_update_deal_stage: 'deal', pig_update_record_fields: 'record', }, 'every write tool proposes a kind, and the kind is what the policy is read against', ); assert.equal(state.transactions, 0, 'the whole sweep was declined, so nothing was written'); // `requiresApproval` is the single source of truth for the policy, so the // claim "auto mode writes these without asking" is checked against it rather // than restated here. A kind added to `PIGGY_ALWAYS_CONFIRM_KINDS` that a // tool already uses would flip one of these and fail loudly. for (const kind of kinds.values()) { assert.equal(isGuardedKind(kind), false, `${kind} is a guarded kind`); assert.equal(requiresApproval('auto', kind), false); assert.equal(requiresApproval('confirm', kind), true); assert.equal(requiresApproval('read_only', kind), true); } }); test('contracts, commitments, allocations and compliance stop even in auto mode', () => { // No tool in `write-tools.ts` creates one of these today, and that is the // point: the policy is stated once, in the protocol, so a tool added later // inherits it rather than having to remember it. This is the assertion that // makes `requiresApproval` the single source of truth rather than a comment. assert.deepEqual( [...PIGGY_ALWAYS_CONFIRM_KINDS], ['contract', 'commitment', 'allocation', 'compliance'], ); for (const kind of PIGGY_ALWAYS_CONFIRM_KINDS) { assert.equal(isGuardedKind(kind), true); assert.equal(requiresApproval('auto', kind), true, `${kind} slipped through auto mode`); assert.equal(requiresApproval('confirm', kind), true); assert.equal(requiresApproval('read_only', kind), true); } // And an unguarded kind is only free in auto mode, never in the other two. assert.equal(requiresApproval('auto', 'activity'), false); assert.equal(requiresApproval('confirm', 'activity'), true); }); test('an activity with nothing to attach to is refused before it is proposed', async () => { const state = fakeDatabase({}); let asked = 0; const tools = createPigWriteTools({ db: state.db, principal: seller(), mode: 'confirm', propose: async () => { asked += 1; return 'apply'; }, }); const result = await tool(tools, 'pig_log_activity').execute( 'call-5', { type: 'note', subject: 'Nobody in particular' }, undefined, undefined, ctx, ); assert.equal(asked, 0, 'the user is not asked to approve a change that cannot be made'); assert.equal(state.transactions, 0); assert.equal(detailsOf(result).status, 'refused'); assert.equal(detailsOf(result).reason, 'no_target'); }); test('a field that does not belong to the record type is named, not silently dropped', async () => { const state = fakeDatabase({ accounts: [{ name: 'Northwind Robotics' }] }); const tools = createPigWriteTools({ db: state.db, principal: seller(), mode: 'confirm', propose: async () => 'apply', }); const result = await tool(tools, 'pig_update_record_fields').execute( 'call-6', { recordType: 'account', recordId: ACCOUNT_ID, reason: 'Correcting after the call.', probability: 0.4, }, undefined, undefined, ctx, ); assert.equal(state.transactions, 0); assert.equal(detailsOf(result).reason, 'field_not_applicable'); assert.match(textOf(result), /probability/); }); test('an unanswered proposal expires as a rejection rather than holding the turn open', async () => { const state = fakeDatabase({ accounts: [{ name: 'Northwind Robotics' }] }); const tools = createPigWriteTools({ db: state.db, principal: seller(), mode: 'confirm', // The user closed the tab. Nothing will ever resolve this. propose: () => new Promise(() => {}), }); const abort = new AbortController(); const running = tool(tools, 'pig_log_activity').execute( 'call-7', { type: 'note', subject: 'Left a voicemail', accountId: ACCOUNT_ID }, abort.signal, undefined, ctx, ); // The five-minute deadline is the backstop; an aborted turn must settle at // once rather than waiting it out, because the connection is billed either // way and nobody is reading the answer. abort.abort(); const result = await running; assert.equal(state.transactions, 0); assert.equal(detailsOf(result).status, 'declined'); });