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>
This commit is contained in:
@@ -0,0 +1,725 @@
|
||||
/**
|
||||
* 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',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user