Files
pig/apps/api/test/motion.test.ts
T
karti 516685526c Add Motion: the go-to-market operating system on top of the ledger
The ledger answers which contracted capacity is sold, to whom, at what
margin. It says nothing about the motion — the repeatable practice that
turns a customer conversation into a scoped deployment, and turns that
deployment into something the next one reuses.

Motion is deliberately not a parallel entity tree. DEMAND_STAGES already
is the motion, so Motion binds reusable artefacts to the stages of a
demand deal that already exists: an engagement hangs off one deal,
cascade deleted, one per deal by unique constraint.

Nine closed kinds, each declaring which stages it serves, and a starter
library of twelve templates covering all eight open stages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:27:03 -07:00

726 lines
27 KiB
TypeScript

/**
* Motion — the two rules that make this a system, and the one departure that
* makes it a risk.
*
* The departure first. `permissions.ts` states that every read endpoint returns
* the whole book because no row-level filter exists anywhere in the query
* layer; motion templates are the first exception, and an exception that is
* only enforced by the UI is not an exception, it is a leak with a nice screen
* in front of it. So the first suite renders the actual predicate to SQL and
* asserts on it. It looks like a test of an implementation detail and is not:
* the WHERE clause IS the access policy, and the two ways of getting it wrong —
* dropping the owner comparison, or widening it with `OR owner IS NULL` — both
* produce a query that returns rows and reports nothing.
*
* The rest drive the exported mutation definitions against a scripted
* transaction, in the register of `activities.test.ts`. The fake answers
* queries in call order and records what ran; it is deliberately not a
* database, because a fake that pretends to run SQL is a fake that will one day
* assert a broken query works.
*/
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import { getTableName, isSQLWrapper, isTable, type SQL } from 'drizzle-orm';
import { PgDialect } from 'drizzle-orm/pg-core';
import { motionScoreBasisPoints } from '@pig/core';
import type { Database, MotionTemplate } from '@pig/db';
import { Hono } from 'hono';
import { AuthError, type Principal } from '../src/lib/auth';
import { executeMutation, MutationError, type ApiEnv } from '../src/lib/mutation';
import {
createMotionRoutes,
motionArtifactCreateDefinition,
motionArtifactPromoteDefinition,
motionEngagementCreateDefinition,
motionEngagementUpdateDefinition,
motionScoreDefinition,
motionTemplateCreateDefinition,
motionTemplatePublishDefinition,
motionTemplateUpdateDefinition,
motionTemplateVersionDefinition,
} from '../src/routes/motion';
import { motionSlug, visibleTemplates } from '../src/services/motion';
import { onTeam, principal } from './helpers/principal';
const OWNER = '00000000-0000-4000-8000-000000000001';
const OTHER = '00000000-0000-4000-8000-0000000000aa';
const DEAL_ID = '00000000-0000-4000-8000-0000000000d1';
const ACCOUNT_ID = '00000000-0000-4000-8000-0000000000ac';
const ENGAGEMENT_ID = '00000000-0000-4000-8000-0000000000e1';
const ARTIFACT_ID = '00000000-0000-4000-8000-0000000000f1';
const TEMPLATE_ID = '00000000-0000-4000-8000-0000000000b1';
const NOW = new Date('2026-08-17T09:00:00.000Z');
/** A demand member: `motion:write`, no `motion:publish`. */
const member = principal(onTeam('demand', 'member'));
/** A demand lead: both, which is the point of the split. */
const lead = principal(onTeam('demand', 'lead'));
// --------------------------------------------------------------- the fixtures
function template(overrides: Partial<MotionTemplate> = {}): MotionTemplate {
return {
id: TEMPLATE_ID,
kind: 'poc',
slug: 'poc-plan',
version: 1,
title: 'POC plan',
summary: 'What a proof of concept must show.',
body: '# POC plan',
fields: null,
stage: 'poc',
visibility: 'private',
ownerUserId: OWNER,
supersedesId: null,
originArtifactId: null,
isSystem: false,
usageCount: 0,
archivedAt: null,
createdAt: NOW,
updatedAt: NOW,
...overrides,
};
}
const deal = { id: DEAL_ID, accountId: ACCOUNT_ID, name: 'DEMO — Northwind training' };
const engagement = { id: ENGAGEMENT_ID, demandDealId: DEAL_ID, status: 'open' };
function artifact(overrides: Record<string, unknown> = {}) {
return {
id: ARTIFACT_ID,
engagementId: ENGAGEMENT_ID,
templateId: null,
kind: 'poc',
stage: 'poc',
title: 'Northwind POC plan',
body: '# What we proved',
fields: null,
status: 'final',
authoredByUserId: OWNER,
promotedTemplateId: null,
archivedAt: null,
createdAt: NOW,
updatedAt: NOW,
...overrides,
};
}
// ------------------------------------------------------------------ the fake
interface Recorded {
events: string[];
inserted: { table: string; row: Record<string, unknown> }[];
updated: { table: string; values: Record<string, unknown> }[];
}
/**
* A transaction that answers selects from a script, in call order, and records
* every write with the table it landed in. Order-dependence is the price of not
* pretending to be Postgres, and it is what makes "no UPDATE ran" assertable.
*/
function database(script: unknown[][]): { db: Database; log: Recorded } {
const log: Recorded = { events: [], inserted: [], updated: [] };
const results = [...script];
const next = (): unknown[] => results.shift() ?? [];
const selectChain = {
from: () => selectChain,
innerJoin: () => selectChain,
leftJoin: () => selectChain,
where: () => selectChain,
orderBy: () => selectChain,
groupBy: () => selectChain,
// `limit` returns the chain rather than a promise so that `.for('update')`
// can follow it, as it does on every query in this feature that allocates
// a version number. The chain is a thenable, so `await` still ends it.
limit: () => selectChain,
for: () => selectChain,
then: (resolve: (rows: unknown[]) => unknown) => resolve(next()),
};
const name = (table: unknown): string => (isTable(table) ? getTableName(table) : 'unknown');
const tx = {
select: () => {
log.events.push('select');
return selectChain;
},
insert: (table: unknown) => ({
values: (row: Record<string, unknown>) => {
log.events.push(`insert:${name(table)}`);
log.inserted.push({ table: name(table), row });
const written = [{ id: `${name(table)}-${log.inserted.length}`, ...row }];
return {
returning: async () => written,
then: (resolve: (value: unknown) => unknown) => resolve(undefined),
};
},
}),
update: (table: unknown) => ({
set: (values: Record<string, unknown>) => ({
where: () => {
log.events.push(`update:${name(table)}`);
log.updated.push({ table: name(table), values });
return Object.assign(Promise.resolve(undefined), {
returning: async () => [{ id: `${name(table)}-updated`, ...values }],
});
},
}),
}),
};
return {
db: {
transaction: async (work: (t: unknown) => Promise<unknown>) => {
log.events.push('transaction');
return work(tx);
},
// The same scripted chain outside a transaction, so a read handler can be
// driven through the mounted routes rather than only its service.
select: tx.select,
} as unknown as Database,
log,
};
}
/** The motion routes with a principal already resolved, as `createApp` mounts them. */
function mounted(db: Database, who: Principal): Hono<ApiEnv> {
const app = new Hono<ApiEnv>();
app.use('*', async (c, next) => {
c.set('principal', who);
await next();
});
app.route('/', createMotionRoutes(db));
return app;
}
function rendered(userId: string, isPlatformAdmin = false) {
const predicate = visibleTemplates({ userId, isPlatformAdmin });
return predicate ? new PgDialect().sqlToQuery(predicate) : null;
}
// ------------------------------------------------------------------- the tests
describe('the private library filter is a WHERE clause, not an affordance', () => {
it('matches shared rows for everyone and private rows only against the viewer id', () => {
const query = rendered(OWNER);
assert.ok(query, 'a member must be filtered at all');
assert.match(query.sql, /"visibility" = \$1/);
assert.match(query.sql, /"owner_user_id" = \$3/);
assert.deepEqual(query.params, ['shared', 'private', OWNER]);
});
it('never widens to `owner_user_id IS NULL` — an orphaned private draft belongs to nobody', () => {
const query = rendered(OWNER);
// `ON DELETE SET NULL` on the owner column can produce a private row with
// no owner. Matching it here would publish every departed colleague's
// drafts to the whole workspace, and the query would look like a fix for
// rows that had "gone missing".
assert.ok(query);
assert.doesNotMatch(query.sql, /owner_user_id" is null/i);
});
it('is deliberately absent for a platform admin — an admin CAN read a private template', () => {
// Not a hole. An unfiltered query here is the decision: someone has to be
// able to answer "what is in this workspace" during an audit or a
// departure. If this ever starts returning a predicate, that was a choice
// somebody made, and this test is where they say so.
assert.equal(rendered(OTHER, true), null);
});
});
describe('a slug is the identity of a lineage', () => {
it('strips the combining mark NFKD leaves behind rather than hyphenating through a word', () => {
// `normalize('NFKD')` splits "é" into "e" plus a combining acute, and the
// `[^a-z0-9]+` rule that follows turns that mark into a separator — so
// without the strip, "Café strategy" starts the lineage `caf-e-strategy`
// and the next person authoring the same title cannot find it.
assert.equal(motionSlug('Café strategy'), 'cafe-strategy');
assert.equal(motionSlug('Proposal Blocks'), 'proposal-blocks');
// Nothing latin survives, and an empty slug would violate the NOT NULL.
assert.equal(motionSlug('日本語'), 'untitled');
});
});
describe('a used template is never edited in place', () => {
it('refuses a PATCH once usage_count is above zero, and names the versions endpoint', async () => {
const { db, log } = database([[template({ usageCount: 3 })]]);
await assert.rejects(
executeMutation(
db,
principal(onTeam('demand', 'member')),
async () => ({ body: '# Rewritten' }),
motionTemplateUpdateDefinition(),
{ id: TEMPLATE_ID },
),
(error: unknown) =>
error instanceof MutationError &&
error.code === 'template_in_use' &&
error.status === 409 &&
error.message.includes('/versions'),
);
assert.deepEqual(log.updated, [], 'a live engagement must not have its template move underneath it');
});
it('edits freely while nobody has instantiated it — an unused template is still a draft', async () => {
const { db, log } = database([[template({ usageCount: 0 })]]);
await executeMutation(
db,
member,
async () => ({ title: 'POC plan, tightened' }),
motionTemplateUpdateDefinition(),
{ id: TEMPLATE_ID },
);
assert.equal(log.updated.length, 1);
assert.equal(log.updated[0]?.table, 'motion_templates');
assert.equal(log.updated[0]?.values.title, 'POC plan, tightened');
});
it('refuses an edit to somebody else\'s template even when it is shared — publishing is not donating', async () => {
const { db, log } = database([[template({ visibility: 'shared', ownerUserId: OTHER })]]);
await assert.rejects(
executeMutation(
db,
member,
async () => ({ body: '# Mine now' }),
motionTemplateUpdateDefinition(),
{ id: TEMPLATE_ID },
),
(error: unknown) => error instanceof AuthError && error.code === 'not_owner',
);
assert.deepEqual(log.updated, []);
});
});
describe('publishing is a lead\'s judgement, and the owner\'s', () => {
it('refuses a member without motion:publish before the row is read at all', async () => {
const { db, log } = database([[template()]]);
await assert.rejects(
executeMutation(db, member, async () => ({}), motionTemplatePublishDefinition(), {
id: TEMPLATE_ID,
}),
(error: unknown) => error instanceof AuthError && error.code === 'insufficient_permission',
);
assert.deepEqual(log.events, [], 'permission precedes the transaction, so nothing was queried');
});
it('refuses a lead flipping somebody else\'s private template to shared', async () => {
const { db, log } = database([[template({ ownerUserId: OTHER })]]);
await assert.rejects(
executeMutation(db, lead, async () => ({}), motionTemplatePublishDefinition(), {
id: TEMPLATE_ID,
}),
(error: unknown) => error instanceof AuthError && error.code === 'not_owner',
);
assert.deepEqual(log.updated, []);
});
it('gates creating straight into the shared library on the same capability', async () => {
// The publish endpoint is not the only door into the shared library, so a
// member who simply POSTs `visibility: 'shared'` must be refused too.
const { db, log } = database([]);
await assert.rejects(
executeMutation(
db,
member,
async () => ({
kind: 'poc',
title: 'POC plan',
summary: 'What a POC must show.',
body: '# POC',
stage: 'poc',
visibility: 'shared',
}),
motionTemplateCreateDefinition(),
),
(error: unknown) => error instanceof AuthError && error.code === 'insufficient_permission',
);
assert.deepEqual(log.inserted, []);
});
it('creates a private template owned by the author, because an unowned private row is unreadable', async () => {
const { db, log } = database([]);
await executeMutation(
db,
member,
async () => ({
kind: 'poc',
title: 'POC plan',
summary: 'What a POC must show.',
body: '# POC',
stage: 'poc',
}),
motionTemplateCreateDefinition(),
);
const written = log.inserted.find((row) => row.table === 'motion_templates');
assert.equal(written?.row.visibility, 'private');
assert.equal(written?.row.ownerUserId, member.userId);
assert.equal(written?.row.version, 1);
assert.equal(written?.row.slug, 'poc-plan');
});
});
describe('promotion is the loop', () => {
function promote(db: Database, input: Record<string, unknown> = {}) {
return executeMutation(
db,
lead,
async () => input,
motionArtifactPromoteDefinition(),
{ id: ARTIFACT_ID },
);
}
it('refuses to promote an artifact that is already in the library', async () => {
const { db, log } = database([[artifact({ promotedTemplateId: TEMPLATE_ID })]]);
await assert.rejects(
promote(db),
(error: unknown) =>
error instanceof MutationError &&
error.code === 'already_promoted' &&
error.status === 409,
);
assert.deepEqual(log.inserted, [], 'a second promotion would fork the lineage silently');
});
it('refuses to promote a draft — the library is what the next deployment copies', async () => {
const { db, log } = database([[artifact({ status: 'draft' })]]);
await assert.rejects(
promote(db),
(error: unknown) =>
error instanceof MutationError &&
error.code === 'artifact_not_final' &&
error.status === 409,
);
assert.deepEqual(log.inserted, []);
});
it('writes a shared version pointing back at the artifact that proved it', async () => {
const { db, log } = database([[artifact()], [{ engagement, deal }], []]);
const result = (await promote(db, { slug: 'poc-plan' })) as {
template: { version: number; visibility: string; originArtifactId: string };
};
assert.equal(result.template.version, 1);
assert.equal(result.template.visibility, 'shared');
assert.equal(result.template.originArtifactId, ARTIFACT_ID);
assert.equal(
log.updated.find((row) => row.table === 'engagement_artifacts')?.values.promotedTemplateId,
'motion_templates-1',
);
});
it('chains supersedes_id across three promotions of one lineage', async () => {
let previous: { id: string; version: number } | null = null;
const chain: { id: string; version: number; supersedesId: string | null }[] = [];
for (let round = 0; round < 3; round += 1) {
// The lineage is read twice and on purpose: once unfiltered for the
// version number, once through the visibility filter for the row the new
// version may claim to supersede.
const lineage = previous ? [{ ...template(), ...previous, slug: 'poc-plan' }] : [];
const { db } = database([
[artifact({ id: `${ARTIFACT_ID}-${round}` })],
[{ engagement, deal }],
lineage,
lineage,
]);
const result = (await promote(db, { slug: 'poc-plan' })) as {
template: { id: string; version: number; supersedesId: string | null };
};
chain.push({
id: result.template.id,
version: result.template.version,
supersedesId: result.template.supersedesId,
});
previous = { id: result.template.id, version: result.template.version };
}
assert.deepEqual(
chain.map((row) => row.version),
[1, 2, 3],
);
assert.equal(chain[0]?.supersedesId, null, 'the first version supersedes nothing');
assert.equal(chain[1]?.supersedesId, chain[0]?.id);
assert.equal(chain[2]?.supersedesId, chain[1]?.id);
});
it('lands the audit row on the account of the deal that proved it', async () => {
const { db, log } = database([[artifact()], [{ engagement, deal }], []]);
await promote(db, { slug: 'poc-plan' });
const activity = log.inserted.find((row) => row.table === 'activities')?.row;
assert.equal(activity?.accountId, ACCOUNT_ID);
assert.equal(activity?.demandDealId, DEAL_ID);
assert.match(String(activity?.subject), /^Promoted to the library:/);
});
});
describe('a lineage nobody can read never supplies content to one everybody can', () => {
/**
* The version number and the predecessor row are two different questions, and
* answering both from one unfiltered query is the shape of the bug these pin.
* Both suites script a lineage whose newest row is private to somebody else:
* the unfiltered read finds v4, the filtered read finds nothing.
*/
const PRIVATE_SUMMARY = 'Unreleased pricing: 40% floor, Northwind only.';
/** Alice's v4, which the unfiltered read finds and the filtered read must not. */
const hidden = () =>
template({
id: '00000000-0000-4000-8000-0000000000c4',
version: 4,
ownerUserId: OTHER,
summary: PRIVATE_SUMMARY,
});
it('promotes past a private newest version without copying its summary or naming its id', async () => {
const { db, log } = database([
[artifact()],
[{ engagement, deal }],
[hidden()],
[],
]);
await executeMutation(db, lead, async () => ({ slug: 'poc-plan' }), motionArtifactPromoteDefinition(), {
id: ARTIFACT_ID,
});
const written = log.inserted.find((row) => row.table === 'motion_templates')?.row;
// v5, because the unique constraint is on the whole lineage and a private
// fork still consumes a number.
assert.equal(written?.version, 5);
// But nothing else from that row. The summary fell back to the artifact's
// own title, and the new version supersedes nothing it cannot show.
assert.notEqual(written?.summary, PRIVATE_SUMMARY);
assert.equal(written?.summary, artifact().title);
assert.equal(written?.supersedesId, null);
});
it('forks a shared template without disclosing that private versions of it exist', async () => {
const source = template({ id: TEMPLATE_ID, visibility: 'shared', ownerUserId: OTHER });
const { db, log } = database([[source], [hidden()], []]);
await executeMutation(db, member, async () => ({}), motionTemplateVersionDefinition(), {
id: TEMPLATE_ID,
});
const written = log.inserted.find((row) => row.table === 'motion_templates')?.row;
assert.equal(written?.version, 5, 'the number is allocated against the whole lineage');
assert.equal(
written?.supersedesId,
source.id,
'the edge points at the row that was actually forked, not at a private row the forker cannot fetch',
);
});
it('refuses to promote an artifact whose source template is unreadable, rather than 404ing on the artifact', async () => {
const { db, log } = database([
[artifact({ templateId: TEMPLATE_ID })],
[{ engagement, deal }],
[],
]);
await assert.rejects(
executeMutation(db, lead, async () => ({}), motionArtifactPromoteDefinition(), {
id: ARTIFACT_ID,
}),
(error: unknown) =>
error instanceof MutationError &&
error.code === 'source_template_unreadable' &&
error.status === 409 &&
error.message.includes('slug'),
);
assert.deepEqual(log.inserted, []);
});
});
describe('a template id somebody sent is read before it is stored', () => {
it('refuses a playbook the setter cannot read, so a private id cannot go book-wide on an engagement', async () => {
const { db, log } = database([[{ engagement, deal }], []]);
await assert.rejects(
executeMutation(
db,
member,
async () => ({ playbookTemplateId: TEMPLATE_ID }),
motionEngagementUpdateDefinition(),
{ id: ENGAGEMENT_ID },
),
(error: unknown) => error instanceof MutationError && error.status === 404,
);
assert.deepEqual(log.updated, [], 'an unreadable id must not reach the foreign key either');
});
});
describe('a UI hint that disagrees with its endpoint is the hint that is wrong', () => {
it('does not offer publish to a read-only key, which the endpoint would refuse for scope', async () => {
const readOnlyLead = principal({ ...onTeam('demand', 'lead'), via: 'api_key', scopes: ['read'] });
const mine = template({ ownerUserId: readOnlyLead.userId });
const { db } = database([[mine], [mine]]);
const response = await mounted(db, readOnlyLead).request(`/api/motion/templates/${TEMPLATE_ID}`);
const body = (await response.json()) as { canPublish: boolean };
assert.equal(response.status, 200);
assert.equal(
body.canPublish,
false,
'the button would 403 insufficient_scope, and a button that cannot work must not be offered',
);
});
it('still offers it to the same lead on a full-scope session', async () => {
const mine = template({ ownerUserId: lead.userId });
const { db } = database([[mine], [mine]]);
const response = await mounted(db, lead).request(`/api/motion/templates/${TEMPLATE_ID}`);
const body = (await response.json()) as { canPublish: boolean };
assert.equal(body.canPublish, true);
});
});
describe('a slug collision is the caller\'s to fix, not a 500', () => {
it('answers 409 naming the versions endpoint when the title derives a slug already in use', async () => {
// "Proposal Blocks" is one of the nine shipped starter templates, so this
// is the first thing a new author trips over rather than an edge case.
const { db, log } = database([[{ version: 1 }]]);
await assert.rejects(
executeMutation(
db,
member,
async () => ({
kind: 'proposal',
title: 'Proposal Blocks',
summary: 'Reusable proposal language.',
body: '# Blocks',
stage: 'proposal',
}),
motionTemplateCreateDefinition(),
),
(error: unknown) =>
error instanceof MutationError &&
error.code === 'template_slug_exists' &&
error.status === 409 &&
error.message.includes('/versions'),
);
assert.deepEqual(log.inserted, []);
});
});
describe('instantiating a template', () => {
it('increments usage_count in the same transaction that writes the artifact', async () => {
const { db, log } = database([
[{ engagement, deal }],
[template({ usageCount: 2, visibility: 'shared', ownerUserId: OTHER })],
]);
await executeMutation(
db,
member,
async () => ({ templateId: TEMPLATE_ID }),
motionArtifactCreateDefinition(),
{ id: ENGAGEMENT_ID },
);
// One transaction, both writes: a count that could be committed without the
// artifact would close the template to edits for a use that never happened.
assert.equal(log.updated[0]?.table, 'motion_templates');
// And the count is incremented by the database, not by JS arithmetic on the
// row that was read a moment ago. Two people instantiating one template
// under READ COMMITTED both read 2 and both write 3, so a use is lost — and
// `usage_count` is the only thing holding §7a shut. Asserting the rendered
// SQL is what makes the difference between the two visible at all: the
// wrong version writes the literal 3 and passes every behavioural test.
const increment = log.updated[0]?.values.usageCount;
assert.ok(
isSQLWrapper(increment),
'usage_count must be written as an expression, never as a number computed in JS',
);
assert.match(new PgDialect().sqlToQuery(increment as SQL).sql, /"usage_count" \+ \$?1/);
assert.equal(log.inserted[0]?.table, 'engagement_artifacts');
assert.equal(log.inserted[0]?.row.templateId, TEMPLATE_ID);
// The body is copied, not referenced — editing the artifact must not reach
// back into the library.
assert.equal(log.inserted[0]?.row.body, template().body);
});
});
describe('one engagement per deal', () => {
it('answers 409 with the id of the engagement that already exists', async () => {
const { db, log } = database([[deal], [engagement]]);
await assert.rejects(
executeMutation(
db,
member,
async () => ({ demandDealId: DEAL_ID }),
motionEngagementCreateDefinition(),
),
(error: unknown) =>
error instanceof MutationError &&
error.code === 'engagement_exists' &&
error.status === 409 &&
error.message.includes(ENGAGEMENT_ID),
);
assert.deepEqual(log.inserted, []);
});
});
describe('qualification scores', () => {
it('computes the score from the dimensions rather than believing the body', async () => {
const { db, log } = database([[{ engagement, deal }]]);
const dimensions = [
{ id: 'budget', weight: 30, score: 4 },
{ id: 'urgency', weight: 20, score: 2 },
{ id: 'fit', weight: 50, score: 3 },
];
await executeMutation(
db,
member,
async () => ({ dimensions }),
motionScoreDefinition(),
{ id: ENGAGEMENT_ID },
);
const written = log.inserted.find((row) => row.table === 'qualification_scores')?.row;
assert.equal(written?.basisPoints, motionScoreBasisPoints(dimensions));
assert.equal(written?.band, 'Strategic');
assert.equal(typeof written?.basisPoints, 'number');
assert.ok(Number.isInteger(written?.basisPoints), 'a score is an integer, exactly as money is');
});
it('refuses a posted basisPoints outright — a score somebody can send is a score somebody can fix', async () => {
const { db } = database([[{ engagement, deal }]]);
await assert.rejects(
executeMutation(
db,
member,
async () => ({ dimensions: [{ id: 'fit', weight: 1, score: 1 }], basisPoints: 10_000 }),
motionScoreDefinition(),
{ id: ENGAGEMENT_ID },
),
(error: unknown) => error instanceof MutationError && error.code === 'invalid_request',
);
});
});