/** * The motion page tools, and the one rule none of them may relax. * * Motion is the first feature in PIG with a row-level access rule: a `private` * template belongs to its owner and to a platform admin, and to nobody else. * Every other read in this process is book-wide, so the habit of the codebase * is against this clause rather than for it — which is exactly why it is pinned * here rather than left to a review. * * Piggy cannot enforce ownership because it does not reliably know who is * asking: the dock publishes a route and the relay checks a capability, and * neither reaches the query. So the query is closed instead. The tests below * assert the closed form under every filter combination a model can send, * because the plausible-but-wrong version of this code is one where the clause * is present in the unfiltered read and lost in a branch. * * The unit suite runs in CI BEFORE the migration step, against a database with * no tables, so nothing here may execute a query. The WHERE clause is rendered * with `PgDialect` rather than run. */ import assert from 'node:assert/strict'; import test from 'node:test'; import { MOTION_KINDS } from '@pig/core'; import type { Database } from '@pig/db'; import { PgDialect } from 'drizzle-orm/pg-core'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { assertPigToolBoundary } from '../src/chat'; import { createPagePigTools, motionLibraryWhere } from '../src/page-tools'; /** Schema and SQL-shape checks only: no query is executed. */ const db = {} as Database; const dialect = new PgDialect(); function renderedWhere(filter: Parameters[0]): { text: string; params: unknown[]; } { const query = dialect.sqlToQuery(motionLibraryWhere(filter)); return { text: query.sql, params: query.params }; } function tool(route: '/motion' | '/motion/library' | '/motion/engagements') { const [only, ...rest] = createPagePigTools(db, route); assert.ok(only, `${route} has a tool`); // One tool per page: a second is a second thing to choose wrongly, and a // wrong choice costs one of four turns. assert.equal(rest.length, 0); return only; } // --------------------------------------------------------------------------- // The visibility invariant. Do not relax this to "unless the user owns it". // --------------------------------------------------------------------------- test('the library query filters to shared templates UNCONDITIONALLY, under every filter', () => { const filters: Parameters[0][] = [ {}, { kind: 'proposal' }, { stage: 'poc' }, { query: 'sovereign' }, { kind: 'playbook', stage: 'deployment', query: 'inference' }, // The nulls a schema-abiding model sends for "no filter" must not read as // "no visibility filter either". { kind: null, stage: null, query: null }, ]; for (const filter of filters) { const { params } = renderedWhere(filter); assert.ok( params.includes('shared'), `visibility = 'shared' is missing for ${JSON.stringify(filter)}`, ); // Bound to the column, not merely present somewhere in the statement. assert.match(renderedWhere(filter).text, /"visibility" = \$\d+/); } }); test('no filter a model can send widens the library beyond shared — private is not a parameter', () => { const library = tool('/motion/library'); const accepts = (input: unknown) => library.inputSchema.safeParse(input).success; // `.strict()`, so every one of these is refused rather than ignored. A tool // that silently drops an unknown key teaches a model to keep trying. assert.equal(accepts({ visibility: 'private' }), false); assert.equal(accepts({ ownerUserId: '20000000-0000-4000-8000-000000000002' }), false); assert.equal(accepts({ includePrivate: true }), false); assert.equal(accepts({ all: '1' }), false); assert.equal(accepts({ kind: 'proposal' }), true); }); test('a private template is invisible even when its title is the search term', () => { // The query filter is an AND alongside the visibility clause, never an OR // beside it: an `or(...)` at the top level would make any matching title // satisfy the whole WHERE and return the private row. const { text, params } = renderedWhere({ query: 'Halcyon' }); const [visibility] = text.split('and'); assert.ok(visibility?.includes('"visibility"'), text); assert.ok(text.startsWith('('), text); assert.match(text, /^\("[a-z_]+"\."visibility" = \$1 and /); assert.equal(params[0], 'shared'); }); test('archived templates are excluded from every library read', () => { // Archiving is how deletion works here, so a query that ignores it hands the // model practice somebody deliberately withdrew. assert.match(renderedWhere({}).text, /"archived_at" is null/); assert.match(renderedWhere({ kind: 'case_study' }).text, /"archived_at" is null/); }); test('LIKE wildcards in the model-supplied query are escaped, not honoured', () => { // Unescaped, `%` matches every shared template and the model is handed the // first eight as though they answered the question. assert.deepEqual(renderedWhere({ query: '%' }).params, ['shared', '%\\%%', '%\\%%', '%\\%%']); }); // --------------------------------------------------------------------------- // The boundary // --------------------------------------------------------------------------- test('every motion page tool sits inside the PIG tool boundary', () => { const tools = [tool('/motion'), tool('/motion/library'), tool('/motion/engagements')]; assert.deepEqual(tools.map((entry) => entry.name), [ 'pig_get_motion_summary', 'pig_search_motion_library', 'pig_get_engagement', ]); // The assertion the chat provider runs on every request: a name that fails it // takes the whole conversation down rather than one tool. assert.doesNotThrow(() => assertPigToolBoundary(tools)); for (const entry of tools) { assert.ok(entry.description.length > 40, `${entry.name} has a usable description`); } }); test('the library and engagement tools say out loud that private drafts are not read', () => { // The description is the only place the model learns the limit, and "I found // nothing" is a materially different answer from "I cannot see private // drafts" to someone looking at their own. assert.match(tool('/motion/library').description, /[Pp]rivate drafts are never searched/); assert.match(tool('/motion').description, /[Pp]rivate drafts are not visible/); }); // --------------------------------------------------------------------------- // The input bounds // --------------------------------------------------------------------------- test('the motion summary takes no input at all', () => { const summary = tool('/motion'); assert.equal(summary.inputSchema.safeParse({}).success, true); assert.equal(summary.inputSchema.safeParse({ stage: 'poc' }).success, false); }); test('the library filters accept only ontology values, and a bounded query', () => { const library = tool('/motion/library'); const accepts = (input: unknown) => library.inputSchema.safeParse(input).success; for (const kind of MOTION_KINDS) assert.equal(accepts({ kind }), true); // A kind the model invented reaches the database as a cast error rather than // a miss, so it is refused at the schema. assert.equal(accepts({ kind: 'battlecard' }), false); assert.equal(accepts({ stage: 'closed_won' }), true); assert.equal(accepts({ stage: 'negotiation' }), false); // Trimmed before the length check, so trailing whitespace cannot smuggle a // one-character query past the floor and match the whole library. assert.equal(accepts({ query: ' a ' }), false); assert.equal(accepts({ query: 'x'.repeat(64) }), true); assert.equal(accepts({ query: 'x'.repeat(65) }), false); assert.equal(accepts({ query: 'x'.repeat(4000) }), false); }); test('the engagement query is bounded and refuses anything it does not name', () => { const engagement = tool('/motion/engagements'); const accepts = (input: unknown) => engagement.inputSchema.safeParse(input).success; assert.equal(accepts({}), true); assert.equal(accepts({ query: 'Halcyon' }), true); assert.equal(accepts({ query: 'a' }), false); assert.equal(accepts({ query: 'x'.repeat(65) }), false); assert.equal(accepts({ engagementId: '20000000-0000-4000-8000-000000000002' }), false); assert.equal(accepts({ limit: 500 }), false); }); /** * What the model is actually sent, rather than what the zod reads like. * * `zodToJsonSchema(..., { target: 'openAi' })` — the exact call both inference * paths make — emits an optional field as REQUIRED and nullable, so a * schema-abiding model sends `null` for every filter it does not want and * `.optional()` would reject the call. A `.describe()` applied after the * wrapper is dropped from the emitted schema entirely. */ test('an omitted motion filter arrives as the null the emitted schema asks for', () => { const library = tool('/motion/library'); assert.equal( library.inputSchema.safeParse({ kind: null, stage: null, query: null }).success, true, ); assert.equal(tool('/motion/engagements').inputSchema.safeParse({ query: null }).success, true); const emitted = zodToJsonSchema(library.inputSchema, { $refStrategy: 'none', target: 'openAi', }) as { properties?: Record; required?: string[] }; assert.deepEqual(emitted.required, ['kind', 'stage', 'query']); for (const [parameter, shape] of Object.entries(emitted.properties ?? {})) { assert.ok( shape.description && shape.description.length > 10, `${parameter} reaches the model with no description`, ); } });