morphit/apps/web/scripts/order-completion-semantics-smoke.ts

132 lines
6.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env tsx
/*
* order-completion-semantics — v1.5.5 (t155) guard.
*
* Ken's report, in one line: he and his counterparty completed a trade — BLURT
* sent, Payment Receipt in the chat, both parties reviewed each other — and the
* order still read "Live". It stayed searchable in the orderbook, still offered
* "Cancel this order", still sat under Active orders, still showed "(Live)" in
* the chat inbox, and the Paid pill counted 0.
*
* ROOT CAUSE: `broadcastOrderComplete` was called ONLY from my/orders'
* auto-complete + manual-complete paths. The button labelled "Mark complete /
* review" and the chat panel headed "Mark this trade complete" both went
* through LeaveFeedbackForm, which posted the REVIEW and nothing else. The
* completion half of both labels was never implemented.
*
* The whole downstream cluster follows from the order's status, so it all fixes
* itself once the op is actually broadcast (the orderbook already filters
* status='live'). That makes this one op the load-bearing piece of the batch —
* hence a smoke.
*
* WHAT IS PINNED
* 1. LeaveFeedbackForm broadcasts order_complete when (and only when) the
* caller asserts the user owns the cited order.
* 2. It names the reviewed subject as the counterparty, so BOTH sides get
* trade credit (a taker owns no order and would otherwise read "0 trades"
* forever).
* 3. The completion is best-effort AFTER the review — the review is already
* irreversible on-chain, so a failed completion must not report the submit
* as failed and invite a duplicate review.
* 4. my/orders passes completeOwnedOrder (every order there is the user's).
* 5. The chat gates it on `orderIsMine` — a chat may be about the PEER's
* order, and completing is owner-only.
* 6. The client never names ITSELF as counterparty (the handler rejects
* counterparty_is_self, which would cost the user the whole completion).
*/
import { readFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const WEB = resolve(HERE, '..');
const FORM = resolve(WEB, 'src/lib/components/LeaveFeedbackForm.svelte');
const CHAT = resolve(WEB, 'src/lib/components/ConversationView.svelte');
const MYORDERS = resolve(WEB, 'src/routes/[lang]/my/orders/+page.svelte');
const OPS = resolve(WEB, 'src/lib/blurt/ops/order.ts');
let pass = 0;
let fail = 0;
function check(name: string, ok: boolean, detail = ''): void {
if (ok) {
pass++;
console.log(`${name}`);
} else {
fail++;
console.log(`${name}${detail ? `: ${detail}` : ''}`);
}
}
/** Whitespace-flattened source — assertions must survive reformatting. */
function flat(p: string): string {
return readFileSync(p, 'utf8').replace(/\s+/g, ' ');
}
const form = flat(FORM);
const chat = flat(CHAT);
const myOrders = flat(MYORDERS);
const ops = flat(OPS);
// ── 1. the form completes the order ─────────────────────────────────
check(
'LeaveFeedbackForm broadcasts order_complete for an owned order',
/if \(completeOwnedOrder\) \{ try \{ await broadcastOrderComplete\(/.test(form),
'submitting a review on your OWN order must also mark it complete — otherwise a settled trade stays Live, stays in the orderbook, keeps its Cancel button and counts 0 under Paid (Ken hit exactly this)'
);
// ── 2. it names the counterparty ────────────────────────────────────
check(
'the reviewed subject is named as the counterparty (both sides credited)',
/await broadcastOrderComplete\(state\.live, orderPermlink, subject\)/.test(form),
'without the counterparty only the OWNER is credited a trade; the taker owns no order and would read "0 trades" forever'
);
// ── 3. best-effort, and AFTER the review ────────────────────────────
check(
'the completion cannot fail the review (best-effort, caught)',
/if \(completeOwnedOrder\) \{ try \{ await broadcastOrderComplete\([^)]*\);.*?\} catch/.test(form),
'the review is already irreversible on-chain; a failed completion must not report the submit as failed and invite a duplicate review'
);
check(
'the completion runs AFTER the review broadcast',
form.indexOf('await broadcastFeedback(') !== -1 &&
form.indexOf('await broadcastFeedback(') < form.indexOf('await broadcastOrderComplete('),
'the review is what the user typed — it must land first'
);
// ── 4. my/orders opts in ────────────────────────────────────────────
check(
'my/orders passes completeOwnedOrder (every order there is the users own)',
/<LeaveFeedbackForm[^>]*completeOwnedOrder=\{true\}/.test(myOrders),
'the button says "Mark complete / review" — it must do both halves'
);
// ── 5. the chat gates on ownership ──────────────────────────────────
check(
'chat gates completion on orderIsMine (a chat may be about the PEERs order)',
/<LeaveFeedbackForm[^>]*completeOwnedOrder=\{orderIsMine\}/.test(chat),
'completing is owner-only; in the other direction it is the peers job'
);
check(
'orderIsMine is derived from the resolved order OWNER, not the peer',
/const orderIsMine = \$derived\(orderOwner !== null && orderOwner === me\)/.test(chat),
'ownership must come from the order record, never assumed from who opened the chat'
);
// ── 6. never name yourself ──────────────────────────────────────────
check(
'broadcastOrderComplete refuses to name the signer as counterparty',
/counterparty !== account/.test(ops) && /\{ permlink, counterparty \} : \{ permlink \}/.test(ops),
'the handler rejects counterparty_is_self outright, which would cost the user the whole completion and leave the listing live'
);
console.log('\n' + '─'.repeat(58));
if (fail === 0) {
console.log(`✓ all ${pass} order-completion-semantics scenarios passed`);
process.exit(0);
} else {
console.log(`${fail} of ${pass + fail} scenarios FAILED`);
process.exit(1);
}