/* WITNESS · order-visibility (W-076) — full blackbox walk           — QRATE-UAT-1
 *
 * WHAT THE CANARY PROMISES, from its own step headers (11 steps):
 *   02 PATRON  POST /diner/orders
 *   03 WAITER  Order visible to waiter in <= 30s  (an explicit SLO)
 *   04 PATRON  POST 2x same item with DISTINCT SPICE
 *   05 WAITER  2 distinct order_items rows, each with its OWN spice
 *   08/09 PATRON  Patron A and Patron B order the same Entree
 *   10 WAITER  Both patrons' orders visible (<= 30s)
 * Steps 01/06/07/11 are token minting, fixture creation and teardown - machine plumbing with
 * no screen, so they are not walkable and I do not pretend to walk them.
 *
 * WHY THIS ROW EXISTS: my earlier pass got 4 of 11 and reported order-visible=false. STR-1197
 * later showed WHY: the waiter account held no table assignments, so the board was empty for a
 * reason that had nothing to do with order visibility. That was my setup, not the product. This
 * walk fixes it by taking a table the waiter DEMONSTRABLY has on his own board.
 *
 * SUBSTITUTION, stated: Anant + the real waiter daniel@, because no canary fixture has a waiter
 * credential this container holds. Same substitution the session-close and cooldown rows use.
 *
 * THE SLO IS THE POINT. I measure elapsed seconds from pressing Place order to the dish
 * appearing on the waiter's board, and I report the number whether it passes or fails.
 */
import { test } from "@playwright/test";
import { journal, look, roleContext, closeRole, signInWaiter, arriveAsDiner,
         account, openCourse, DWELL, PHONE, DESK } from "../lib-witness.mjs";

const ANANT = "097c229c-a1cb-4ba7-b603-800e46936133";
const SLO_S = 30;

const tap = async (page, sel) => {
  const l = typeof sel === "string" ? page.locator(sel).first() : sel;
  if (!(await l.count())) return false;
  await l.click({ timeout: 12000 }).catch(async () => { await l.evaluate(e => e.click()); });
  return true;
};

/* THE NEEDLE IS THE ORACLE, AND AN EMPTY NEEDLE IS A CHECK THAT CANNOT FAIL.
 * A previous pass on this row read the dish name from the tile's innerText, got back "",
 * and then searched the waiter's board for "" - which matches instantly, on any page, always.
 * It reported 10.8s and 11.1s as delivery times. Those numbers measured how long my own
 * harness took to look, not how long the product took to deliver, and I withdrew the green.
 * So: try five independent sources, record WHICH one produced the name, and refuse to run
 * the timing check at all unless the result is a real word. */
async function dishName(page, tile) {
  // STRIP THE UI VERB. An accessible label is written for a SCREEN READER, so it names the
  // ACTION as well as the thing: "View Express Lunch - Mirch Masala". The waiter's board shows
  // only "Express Lunch - Mirch Masala". Searching the board for the button's label therefore
  // hunts a string that can never appear - which is exactly how this walk reported "the order
  // never arrived" on 2026-09-05 while its own error text quoted the order sitting on the board.
  const clean = t => (t || "").replace(/\s+/g, " ").replace(/\$[\d.,]+/g, "")
                       .replace(/^\s*(view|open|add|order|choose|select|see)\b[\s:-]*/i, "")
                       .replace(/\b(add|to order|from)\b/gi, "").trim().slice(0, 44);
  let n = clean(await tile.innerText().catch(() => ""));
  if (n.length >= 4) return { name: n, from: "tile text" };
  n = clean(await tile.evaluate(e => e.getAttribute("aria-label") || e.getAttribute("title")
        || (e.querySelector("img") || {}).alt || "").catch(() => ""));
  if (n.length >= 4) return { name: n, from: "tile aria/alt" };
  n = clean(await page.locator('[data-testid="m2-composition"] h1, [data-testid="m2-composition"] h2, '
        + '[data-testid="m2-composition"] h3, [role="dialog"] h1, [role="dialog"] h2, [role="dialog"] h3')
        .first().innerText().catch(() => ""));
  if (n.length >= 4) return { name: n, from: "sheet heading" };
  n = clean(await page.evaluate(() => {                       // biggest text on the open sheet
    const sh = document.querySelector('[data-testid="m2-composition"], [role="dialog"]');
    if (!sh) return "";
    let best = "", bestPx = 0;
    for (const e of sh.querySelectorAll("*")) {
      if (e.children.length) continue;
      const px = parseFloat(getComputedStyle(e).fontSize) || 0;
      const t = (e.textContent || "").trim();
      if (t.length > 3 && px > bestPx) { bestPx = px; best = t; }
    }
    return best;
  }).catch(() => ""));
  if (n.length >= 4) return { name: n, from: "largest text on sheet" };
  return { name: "", from: "NOTHING - all five sources empty" };
}

/* ── THE ORACLE IS A NEW LINE ON THE BOARD, NOT A NAME I GUESSED ──────────────────
 * Twice on 2026-09-05 this walk reported "the order never reached the waiter" while its
 * own error text quoted the order sitting on the board. Both times the fault was the same:
 * I built a search string on the DINER's side and hunted for it on the WAITER's side.
 *   1st: the tile's accessible label -> "View Express Lunch - Mirch Masala" (a button verb)
 *   2nd: the basket container's text -> "The Cart Ordered Express Lunch - Mirch Masal"
 *        (a panel heading and a tab label glued to the dish)
 * Every source I pick drags neighbouring UI text with it, and a needle that is one word
 * wrong can never match - so the walk reports a product failure that is really mine.
 *
 * A person does not do this. A waiter watches the board and sees a NEW TICKET appear.
 * So: snapshot the lines before the order, poll for a line that was not there before, and
 * report the seconds plus what that new line actually says. No name is needed to detect it,
 * and the board then tells me the dish's real name for every later step. The check still
 * fails honestly - if no new line appears, nothing appeared. */
const ageless = t => (t || "").replace(/\s+/g, " ")
  .replace(/\b(just now|\d+\s*[smhd] ago|\d+\s*(sec|min|hour)s? ago)\b/gi, "")
  .replace(/\s+/g, " ").trim();

async function boardSnapshot(w) {
  const b = await boardLines(w);
  return { raw: b.cards, keys: b.cards.map(ageless), scope: b.scope };
}

/** Poll until a line appears that was NOT on the board before. Returns seconds + the line. */
async function waitForNewLine(w, before, t0, deadlineS) {
  for (;;) {
    const now = await boardSnapshot(w);
    const fresh = now.raw.filter((_, i) => !before.keys.includes(now.keys[i]));
    if (fresh.length)
      return { seconds: +((Date.now() - t0) / 1000).toFixed(1), line: fresh[0],
               fresh, scope: now.scope, total: now.raw.length };
    const elapsed = (Date.now() - t0) / 1000;
    if (elapsed > deadlineS)
      return { seconds: null, elapsed, line: null, fresh: [], scope: now.scope,
               total: now.raw.length, board: now.raw.join(" | ").slice(0, 400) };
    await w.waitForTimeout(2000);
  }
}

/* The diner's own basket line - a second, independent source for the dish's real name. */
async function cartLineName(page, hint) {
  const tokens = hint.split(/\s+/).filter(w => w.length > 3).slice(0, 3);
  const name = await page.evaluate(toks => {
    const nodes = [...document.querySelectorAll('[data-testid^="cart-"], [data-testid^="m2-cart"], '
      + '[data-testid^="order-line"], [data-testid^="cancel-item-"]')]
      .map(e => e.closest("li,div,article") || e);
    const texts = nodes.map(e => (e.innerText || "").replace(/\s+/g, " ").trim())
                       .filter(t => t && t.length < 220);
    if (!texts.length) return "";
    const hit = toks.length
      ? texts.find(t => toks.some(w => t.toLowerCase().includes(w.toLowerCase()))) : texts[0];
    if (!hit) return "";
    return (hit.split(/\s*[·|\n]\s*|\s{2,}/)[0] || hit)
      .replace(/^\d+\s*[x×]\s*/i, "").replace(/\$[\d.,]+/g, "").trim().slice(0, 44);
  }, tokens).catch(() => "");
  return { name: (name || "").trim() };
}

/* Read the waiter's ORDER LINES, not the whole page. The board also renders a menu and
 * table names, so a page-wide match can find the dish somewhere that is not an order. */
async function boardLines(w) {
  await w.getByTestId("tab-orders").click({ timeout: 8000 }).catch(() => {});
  const cards = await w.evaluate(() =>
    [...document.querySelectorAll('[data-testid^="order-card-"]')]
      .map(e => (e.innerText || "").replace(/\s+/g, " ").trim()));
  if (cards.length) return { cards, scope: "order cards" };
  const body = (await w.locator("body").innerText().catch(() => "")).replace(/\s+/g, " ");
  return { cards: body ? [body] : [], scope: "whole page (no order-card testids found)" };
}
const countLines = (cards, needle) =>
  cards.filter(t => new RegExp(needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i").test(t)).length;

/** Open a dish that actually offers a spice choice, pick one, add it. Returns what was chosen. */
async function orderWithSpice(page, wantIdx, who) {
  // Food, not drinks: mosaic_v2 arrives on Beverages and only 1 of Anant's 31 beverages has
  // options at all - which is why an earlier pass wrongly reported "no way to choose a spice".
  // The course testids are CAPITALISED and use the menu's own words:
  //   m2-section-Beverages / m2-section-Appetizers / m2-section-Entrees / m2-section-Desserts
  // My first attempt asked for lowercase "mains"/"entrees", matched nothing, and so never left
  // Drinks - where almost nothing has options. That produced "no dish offers a spice choice",
  // which is the SAME false negative I already retracted once on this row (a lassi has no heat
  // setting, correctly). Probe result: Starters 4 of 6 and Mains 3 of 6 carry a 6-step scale.
  for (const course of ["Entrees", "Appetizers"]) {
    const r = await openCourse(page, course);
    if (r.ok && r.tiles) break;
  }
  const tiles = page.locator('[data-testid^="m2-tile-"]');
  const n = await tiles.count();
  if (!n) throw new Error(`${who}: no food dishes on screen to order.`);
  // Find a dish that offers a spice scale. The tile opens the composition sheet - m2-text-<id>
  // does NOT, it times out and looks exactly like a missing control (my own prior false finding).
  let picked = null;
  for (let i = 0; i < Math.min(n, 12); i++) {
    await tiles.nth(i).evaluate(e => e.click());
    await page.waitForTimeout(DWELL);
    const spice = await page.locator('[data-testid^="m2-cmp-spice-"]').count();
    const got = await dishName(page, tiles.nth(i));
    if (spice) { picked = { i, name: got.name, nameFrom: got.from, spice }; break; }
    await tap(page, '[data-testid="composition-close"], [data-testid="m2-composition-close"]');
    await page.waitForTimeout(1500);
  }
  if (!picked) throw new Error(`${who}: none of the first dishes offered a spice choice, so the `
    + `"same item at two spice levels" half of this journey cannot be driven here.`);
  const opts = page.locator('[data-testid^="m2-cmp-spice-"]');
  const cnt = await opts.count();
  const idx = Math.min(wantIdx, cnt - 1);
  const chosen = (await opts.nth(idx).innerText()).replace(/\s+/g, " ").trim();
  await opts.nth(idx).evaluate(e => e.click());
  await page.waitForTimeout(1500);
  // HEAT IS NOT THE ONLY REQUIRED CHOICE. These dishes also make you pick a PROTEIN - the
  // `m2-cmp-opt-<id>` entries, which carry the price. Choose only the heat and Add stays greyed
  // out reading "Add to order ($0.00)", nothing enters the basket, and the walk then reports
  // "no Place order button" as though the app were broken. It is a half-filled form.
  const proteins = page.locator('[data-testid^="m2-cmp-opt-"]');
  const np = await proteins.count();
  let protein = null;
  if (np) {
    protein = (await proteins.first().innerText()).replace(/\s+/g, " ").trim().slice(0, 40);
    await proteins.first().evaluate(e => e.click());
    await page.waitForTimeout(1800);
  }
  const add = page.locator('[data-testid="composition-add"]').first();
  if (!(await add.count())) throw new Error(`${who}: opened "${picked.name}" but found no Add.`);
  // PROVE the form is satisfied before pressing: a disabled button or a $0.00 price means a
  // required question is still unanswered, and pressing it would prove nothing.
  const st = await add.evaluate(e => ({
    disabled: e.disabled === true || e.getAttribute("aria-disabled") === "true"
              || (e.className || "").toLowerCase().includes("disabled"),
    label: (e.innerText || "").replace(/\s+/g, " ").trim() }));
  if (st.disabled || /\$0\.00/.test(st.label))
    throw new Error(`${who}: the dish still will not add - button reads "${st.label}" `
      + `(disabled=${st.disabled}) after choosing heat "${chosen}"`
      + `${protein ? ` and protein "${protein}"` : " and it offered no protein choice"}. `
      + `A required question is still unanswered, so this is my form-filling, not the product.`);
  const before = (await page.locator('[data-testid="m2-dock-cart-count"]').first()
                    .innerText().catch(() => "0")).trim() || "0";
  await tap(page, add);
  await page.waitForTimeout(DWELL);
  const after = (await page.locator('[data-testid="m2-dock-cart-count"]').first()
                   .innerText().catch(() => "0")).trim() || "0";
  if (Number(after) <= Number(before))
    throw new Error(`${who}: pressed "${st.label}" but the basket stayed at ${after}. `
      + `Nothing was added, so there is nothing to order.`);
  await tap(page, '[data-testid="composition-close"], [data-testid="m2-composition-close"]');
  await page.waitForTimeout(1500);
  // CROSS-CHECK the name against a SECOND, independent artefact: the diner's own basket line.
  // The basket renders the dish the way the order carries it, which is the way the waiter's
  // board will render it too. A tile label is one step further from the order than that.
  await tap(page, '[data-testid="m2-dock-trigger"]');
  await tap(page, '[data-testid="m2-dock-cart"]');
  await page.waitForTimeout(2500);
  const cart = await cartLineName(page, picked.name);
  if (cart.name && cart.name.length >= 4 && cart.name.toLowerCase() !== picked.name.toLowerCase()) {
    picked.nameFrom = `${picked.nameFrom} -> corrected by the diner's own basket line`;
    picked.name = cart.name;
  }
  return { dish: picked.name, nameFrom: picked.nameFrom, spice: chosen, scale: cnt, protein, price: st.label };
}

async function placeOrder(page, who) {
  await tap(page, '[data-testid="m2-dock-trigger"]');
  await tap(page, '[data-testid="m2-dock-cart"]');
  await page.waitForTimeout(DWELL);
  const btn = page.locator('[data-testid="order-now-btn"]').first();
  if (!(await btn.count())) throw new Error(`${who}: no Place order button - nothing was sent.`);
  const label = (await btn.innerText()).replace(/\s+/g, " ").trim();
  const t0 = Date.now();
  await tap(page, btn);
  await page.waitForTimeout(DWELL);
  return { label, t0 };
}

/** Poll the waiter's board for a needle. Returns the SECONDS it took, or null past the deadline. */
async function waitForOnBoard(w, needle, t0, deadlineS) {
  for (;;) {
    const elapsed = (Date.now() - t0) / 1000;
    if (elapsed > deadlineS) {
      const b = await boardLines(w);
      return { seconds: null, elapsed, body: b.cards.join(" | ").slice(0, 400), scope: b.scope };
    }
    const b = await boardLines(w);
    if (countLines(b.cards, needle))
      return { seconds: +((Date.now() - t0) / 1000).toFixed(1),
               body: b.cards.join(" | ").slice(0, 400), scope: b.scope };
    await w.waitForTimeout(3000);
  }
}

/* THE CONTROL. A stopwatch that starts on something already on the screen always reads zero.
 * Before I time a delivery I prove the needle is ABSENT from the waiter's board, so the
 * appearance I later measure is genuinely this order arriving and not a leftover, a menu
 * entry, or a word that was on the page the whole time. If the needle is already there the
 * measurement is void and I say so instead of reporting a fast number. */
async function waitForLineIncrease(w, needle, t0, deadlineS, baseline) {
  for (;;) {
    const elapsed = (Date.now() - t0) / 1000;
    const b = await boardLines(w);
    const n = countLines(b.cards, needle);
    if (n > baseline)
      return { seconds: +((Date.now() - t0) / 1000).toFixed(1), lines: n, scope: b.scope,
               body: b.cards.join(" | ").slice(0, 400) };
    if (elapsed > deadlineS)
      return { seconds: null, elapsed, lines: n, scope: b.scope,
               body: b.cards.join(" | ").slice(0, 400) };
    await w.waitForTimeout(3000);
  }
}

async function assertNeedleUsable(w, needle, who) {
  if (!needle || needle.trim().length < 4)
    throw new Error(`${who}: I could not read the dish's NAME (got "${needle}"), so the only thing `
      + `I could search the waiter's board for would be an empty string - which matches instantly `
      + `and would report a delivery time that is really just my own lookup. Refusing to time it. `
      + `This is my harness, not the product.`);
  const b = await boardLines(w);
  const n = countLines(b.cards, needle);
  if (n) throw new Error(`${who}: "${needle}" is ALREADY on the waiter's board (${n} line(s)) before `
      + `this order was placed, so any arrival I then measured would be meaningless. Void, not green.`);
  return { scope: b.scope, linesBefore: b.cards.length };
}

test("order-visibility", async ({ browser }) => {
  const jr = journal("order-visibility");
  jr.target({ app: "patron x2 + waiter", restaurantId: ANANT,
              substitution: "Anant + daniel@ - no canary fixture has a waiter credential here" });
  jr.note("_The canary's promise in one line: a diner's order reaches the waiter within 30 seconds, "
        + "two of the same dish at different spice levels stay two separate lines each carrying its "
        + "own spice, and two different diners at one table are both visible. I measure the seconds "
        + "and report them either way._");
  jr.note("_My earlier pass reported order-visible=false. STR-1197 showed the waiter account held "
        + "no table assignments, so the board was empty for an unrelated reason. This walk takes a "
        + "table the waiter demonstrably HAS._");

  const cW = await roleContext(browser, jr, "waiter", DESK);
  const w = await cW.newPage();
  const c1 = await roleContext(browser, jr, "diner-1", PHONE);
  const p1 = await c1.newPage();
  const c2 = await roleContext(browser, jr, "diner-2", PHONE);
  const p2 = await c2.newPage();
  let verdict = "INCOMPLETE", why = "";
  const rec = {};
  let TABLE = null;

  try {
    await jr.step("WAITER", "Daniel signs in - which tables are actually his?", async id => {
      const acc = account("waiter");
      await signInWaiter(w, acc.email, acc.password);
      await w.getByTestId("tab-tables").click({ timeout: 20000 });
      await w.waitForTimeout(DWELL);
      const cards = await w.evaluate(() => [...document.querySelectorAll('[data-testid^="table-card-"]')]
        .map(e => ({ n: e.getAttribute("data-testid").replace("table-card-", ""),
                     t: (e.innerText || "").replace(/\s+/g, " ").trim() })));
      await look(w, jr, "waiter", id, "the tables that belong to this waiter");
      jr.note(`> **Tables on Daniel's own board:** `
            + (cards.map(c => `${c.n} ("${c.t.slice(0, 34)}")`).join(" · ") || "**NONE**"));
      if (!cards.length)
        throw new Error(`this waiter has no tables at all, so an order can never appear on his `
          + `board and "order not visible" would say nothing about order visibility. That is the `
          + `STR-1197 confound, not a finding about this journey.`);
      const free = cards.find(c => /no guests/i.test(c.t)) || cards[0];
      TABLE = free.n; rec.tables = cards;
      return `He has ${cards.length} table(s). I will seat the diners on table ${TABLE} `
           + `("${free.t.slice(0, 60)}").`;
    });

    await jr.step("PATRON", `A diner sits at table ${TABLE} and orders a dish at one spice level`, async id => {
      await arriveAsDiner(p1, ANANT, TABLE, "mosaic_v2");
      await p1.waitForTimeout(DWELL);
      rec.o1 = await orderWithSpice(p1, 1, "Diner one");
      const before = await boardSnapshot(w);
      rec.before = before;
      jr.note(`> **Diner one ordered:** "${rec.o1.dish}" at spice \`${rec.o1.spice}\` `
            + `(the dish offers a ${rec.o1.scale}-step scale)`);
      jr.note(`> **Control before the stopwatch starts:** the waiter's board holds `
            + `**${before.raw.length}** order line(s) right now (read as ${before.scope}). I am not `
            + `searching for a name I made up on the diner's side - twice that produced a false `
            + `"never arrived". I watch for a line that was not there before, which is what a `
            + `waiter actually does.`);
      const r = await placeOrder(p1, "Diner one");
      rec.t0 = r.t0;
      await look(p1, jr, "diner-1", id, "order placed");
      return `Ordered "${rec.o1.dish}" at spice "${rec.o1.spice}" and pressed "${r.label}".`;
    });

    await jr.step("WAITER", `Does it reach him within ${SLO_S}s? (the canary's own SLO)`, async id => {
      const hit = await waitForNewLine(w, rec.before, rec.t0, SLO_S + 20);
      rec.sec = hit.seconds;
      rec.newLine = hit.line;
      // The board just named the dish for me. THAT is the authoritative name from here on.
      if (hit.line) {
        const m = hit.line.match(/([A-Z][\w'&.-]*(?:\s+[A-Z\d][\w'&.-]*){1,5})/);
        rec.boardDish = (m ? m[1] : hit.line).replace(/\b(New|Cancelled|Guest|Enter in POS)\b/g, "")
                          .replace(/\s+/g, " ").trim().slice(0, 40);
      }
      await look(w, jr, "waiter", id, "the waiter's order board");
      jr.note(`> **The line that appeared:** \`${(hit.line || "none").slice(0, 150)}\``);
      jr.note(`> **Board lines:** ${rec.before.raw.length} before → ${hit.total} after`);
      jr.note(`> **Seconds from Place order to the dish appearing on the waiter's board:** `
            + `${hit.seconds === null ? `**never - still absent after ${Math.round(hit.elapsed)}s**` : `${hit.seconds}s`} `
            + `· the canary's SLO is ${SLO_S}s`);
      if (hit.seconds === null)
        throw new Error(`no NEW order line appeared on the waiter's board in `
          + `${Math.round(hit.elapsed)}s - it held ${rec.before.raw.length} line(s) before and `
          + `${hit.total} after, on the table HE owns, so the STR-1197 confound does not apply. `
          + `Board: ${(hit.board || "").slice(0, 240)}`);
      if (hit.seconds > SLO_S)
        return `It arrived, but in ${hit.seconds}s - OUTSIDE the ${SLO_S}s the canary asserts.`;
      return `It reached him in ${hit.seconds}s, inside the ${SLO_S}s SLO.`;
    });

    await jr.step("PATRON", "The same dish again, at a DIFFERENT spice level", async id => {
      rec.o2 = await orderWithSpice(p1, 4, "Diner one");
      jr.note(`> **Second order of the same dish:** "${rec.o2.dish}" at spice \`${rec.o2.spice}\` `
            + `(first was \`${rec.o1.spice}\`)`);
      if (rec.o2.spice === rec.o1.spice)
        throw new Error(`I could not pick a DIFFERENT spice - both orders came out "${rec.o1.spice}", `
          + `so this step cannot test what it is meant to test. My harness.`);
      rec.before2 = await boardSnapshot(w);
      const r = await placeOrder(p1, "Diner one");
      rec.t1 = r.t0;
      await look(p1, jr, "diner-1", id, "second order placed at a different spice");
      return `Ordered the same dish again at "${rec.o2.spice}" (first was "${rec.o1.spice}").`;
    });

    await jr.step("WAITER", "Two separate lines, each carrying its own spice", async id => {
      await w.waitForTimeout(12000);
      await w.getByTestId("tab-orders").click({ timeout: 10000 }).catch(() => {});
      await w.waitForTimeout(DWELL);
      const hit2 = await waitForNewLine(w, rec.before2, rec.t1, SLO_S + 20);
      rec.sec1b = hit2.seconds;
      const b = await boardLines(w);
      const t = await look(w, jr, "waiter", id, "both lines on the board");
      const s1 = rec.o1.spice.split(" ")[0], s2 = rec.o2.spice.split(" ")[0];
      jr.note(`> **Second line appeared after:** ${hit2.seconds === null ? "**never**" : hit2.seconds + "s"}`
            + ` · \`${(hit2.line || "none").slice(0, 130)}\``);
      // Scope the spice check to the lines carrying THIS DISH, named the way the BOARD names it.
      const key = (rec.boardDish || "").trim();
      if (!key) throw new Error(`the board never gave me a dish name to scope the spice check to. `
        + `My harness - I cannot check "each line carries its own spice" without one.`);
      const mine = b.cards.filter(c => new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i").test(c));
      const has1 = mine.some(c => new RegExp(s1, "i").test(c));
      const has2 = mine.some(c => new RegExp(s2, "i").test(c));
      const dishCount = mine.length;
      jr.note(`> **On the waiter's board:** ${b.cards.length} order line(s) total, `
            + `${dishCount} carrying "${key}" — the name the BOARD uses (counted as ${b.scope}, `
            + `not as words on the page) · `
            + `spice "${s1}" on one of those lines: ${has1 ? "yes" : "**no**"} · `
            + `spice "${s2}": ${has2 ? "yes" : "**no**"}`);
      rec.two = { dishCount, has1, has2 };
      if (dishCount < 2)
        throw new Error(`only ${dishCount} line(s) for this dish on the waiter's board - the two `
          + `orders did not stay two distinct rows. Lines: ${b.cards.join(" | ").slice(0, 240)}`);
      if (!(has1 && has2))
        throw new Error(`both lines are there but the waiter cannot see which spice each one is: `
          + `"${s1}" visible=${has1}, "${s2}" visible=${has2}. A kitchen cannot cook this order. `
          + `Board: ${t.slice(0, 240)}`);
      return `Two lines, and the board shows both "${s1}" and "${s2}".`;
    });

    await jr.step("PATRON", "A SECOND diner at the same table orders too", async id => {
      await arriveAsDiner(p2, ANANT, TABLE, "mosaic_v2");
      await p2.waitForTimeout(DWELL);
      rec.o3 = await orderWithSpice(p2, 2, "Diner two");
      rec.before3 = await boardSnapshot(w);
      jr.note(`> **Control before diner two's stopwatch:** the board holds `
            + `**${rec.before3.raw.length}** line(s). Diner two's order must add one that was `
            + `not there before - a same-dish order that changes nothing is not an arrival.`);
      const r = await placeOrder(p2, "Diner two");
      rec.t2 = r.t0;
      await look(p2, jr, "diner-2", id, "second diner's order");
      return `Diner two ordered "${rec.o3.dish}" at "${rec.o3.spice}".`;
    });

    await jr.step("WAITER", `Both diners' orders visible to him (<= ${SLO_S}s)`, async id => {
      const hit = await waitForNewLine(w, rec.before3, rec.t2, SLO_S + 20);
      const cards = hit.total;
      const t = await look(w, jr, "waiter", id, "both diners on one board");
      jr.note(`> **Second diner's order reached the board in:** `
            + `${hit.seconds === null ? "**never**" : hit.seconds + "s"} · `
            + `board lines ${rec.before3.raw.length} → ${cards} · `
            + `the new line: \`${(hit.line || "none").slice(0, 130)}\``);
      rec.sec2 = hit.seconds; rec.cards = cards;
      if (hit.seconds === null)
        throw new Error(`the second diner's order never appeared: the board held `
          + `${rec.before3.raw.length} line(s) before and ${cards} after `
          + `${Math.round(hit.elapsed)}s. Board: ${(hit.board || "").slice(0, 240)}`);
      return `Both diners' orders are on his board; the second took ${hit.seconds}s. `
           + `${cards} order card(s) on screen.`;
    });

    verdict = "HONEST_GREEN";
    why = `Every human-walkable step of this journey holds on the deployed dev apps: the order `
        + `reached the waiter in ${rec.sec}s and the second diner's in ${rec.sec2}s (SLO ${SLO_S}s), `
        + `two orders of one dish stayed two lines each showing its own spice, and both diners were `
        + `visible. The 4 machine steps (token minting, fixture create/delete) have no screen and `
        + `were not walked.`;
  } catch (e) {
    why = e.message; verdict = "HONEST_RED";
  } finally {
    for (const [pg, nm] of [[p1, "diner-1"], [p2, "diner-2"]]) {
      try {
        await tap(pg, '[data-testid="m2-dock-trigger"]');
        await tap(pg, '[data-testid="m2-dock-cart"]');
        await pg.waitForTimeout(2500);
        for (let i = 0; i < 6; i++) {
          const c = pg.locator('[data-testid^="cancel-item-"]').first();
          if (!(await c.count())) break;
          await c.click({ timeout: 8000 }).catch(() => {});
          await pg.waitForTimeout(2500);
        }
      } catch {}
    }
    jr.note(`> **Cleanup:** both diners cancelled their own items where the control was still `
          + `offered, so table ${TABLE} is left as I found it.`);
    await closeRole(c1, jr, "diner-1");
    await closeRole(c2, jr, "diner-2");
    await closeRole(cW, jr, "waiter");
    jr.finish(verdict, why);
  }
});
