Skip to content

test: support Pi 1.0.3 HTML renderer lookups - #28

Merged
peterOC26 merged 2 commits into
mainfrom
fm/fm-pi-103-calm-test-p1
Oct 5, 2026
Merged

peterOC26 merged 2 commits into
mainfrom
fm/fm-pi-103-calm-test-p1

Conversation

@peterOC26

Copy link
Copy Markdown
Owner

Intent

"q298a", answering this option: "merge PR 27 now, accepting the failing check "Behavior portable serial 6", and start a small firstmate job to fix that test for Pi 1.0.3". Later: "also we should probably update our pi as well".

Context the ask refers to. CI job "Behavior portable serial 6" fails on tests/fm-calm-pi-extension.test.sh with "not ok - Pi calm renderer and lifecycle contract failed: file:///tmp/fm-calm-pi-extension.*/renderer/[eval1]:546", reported by the pipeline as "grep disappeared from /export calm.html HTML while calm mode was on". .github/workflows/ci.yml installs the newest @earendil-works/pi-coding-agent without a version; CI used 1.0.3 (released 2026-10-05 08:37 UTC) on #27, while main last passed on 2026-10-03 with 1.0.0. The test passes locally on main and on that PR with the VPS's Pi 0.99.2. Failing run: https://github.com/peterOC26/firstmate/actions/runs/37285235172.

What Changed

  • Update Calm and branch-outcome HTML renderer test fixtures to support both legacy getToolDefinition and Pi 1.0.3’s getToolRenderers lookup.
  • Document Pi 0.99.2 and 1.0.3 verification of Calm export rendering and queue retention.

Risk Assessment

✅ Low: The change only touches test fixtures and verification notes. Each fixture now supplies both getToolDefinition (used by Pi 0.99.2) and getToolRenderers (used by 1.0.3). I checked the Pi 1.0.3 package: it expects the same object shape with renderCall/renderResult, so the fixtures behave the same on both versions without changing any product code.

Testing

Focused fixtures and real Pi CLI scenarios passed on both versions. Reproduced the original failure, proved the branch assertion detects unwanted export rendering, and captured HTML, screenshots, and transcripts. Initial dependency-layout and Chromium socket-path setup issues were resolved before successful reruns; scratch files were cleaned up.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
Export a session while Calm is enabled on Pi 0.99.2 and 1.0.3; grep, find, and watcher tool data remain in the rendered export ✅ pass live Pi 1.0.3 and Pi 0.99.2 Calm export screenshots; validation-transcript.txt
Remap submit to Alt+S and press Enter on an export command; neither version exports or reveals hidden branch outcomes ✅ pass live branch-0992/non-submit.txt, branch-103/non-submit.txt, and validation-transcript.txt
Submit branch-outcome export with Alt+S on both versions; Pi uses stock HTML fallback, preserves results, and restores Calm hiding afterward ✅ pass live Pi 1.0.3 branch export screenshot; branch-0992/export.html, branch-103/export.html, and validation-transcript.txt
Queue a follow-up in a real Pi session; both versions expose every queue-retention capability Calm requires ✅ pass live pi-0992-queue-retention.log and pi-103-queue-retention.log

Pi 1.0.3 Calm export preserves grep and find

Evidence: Actual Pi 1.0.3 Calm export

Source: Actual Pi 1.0.3 Calm export

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Session Export</title>
  <style>
    :root {
      --accent: #800080;
      --borderAccent: #800080;
      --customMessageLabel: #800080;
      --mdCode: #800080;
      --mdListBullet: #800080;
      --syntaxType: #800080;
      --border: #000080;
      --mdLink: #000080;
      --syntaxKeyword: #000080;
      --syntaxVariable: #008080;
      --success: #008000;
      --mdCodeBlock: #008000;
      --toolDiffAdded: #008000;
      --bashMode: #008000;
      --syntaxNumber: #800080;
      --error: #800000;
      --toolDiffRemoved: #800000;
      --warning: #808000;
      --mdHeading: #808000;
      --syntaxFunction: #808000;
      --syntaxString: #008000;
      --thinkingMinimal: #000080;
      --thinkingLow: #000080;
      --thinkingMedium: #008080;
      --thinkingHigh: #800080;
      --thinkingXhigh: #ff00ff;
      --thinkingMax: #800000;
      --text: #e5e5e7;
      --userMessageText: #e5e5e7;
      --customMessageText: #727273;
      --toolTitle: #e5e5e7;
      --syntaxOperator: #727273;
      --syntaxPunctuation: #727273;
      --muted: #727273;
      --dim: #727273;
      --thinkingText: #727273;
      --toolOutput: #727273;
      --mdLinkUrl: #727273;
      --mdQuote: #727273;
      --mdQuoteBorder: #727273;
      --mdHr: #727273;
      --mdCodeBlockBorder: #727273;
      --toolDiffContext: #727273;
      --syntaxComment: #727273;
      --scrollbarTrack: #727273;
      --scrollbarThumb: #727273;
      --searchMatchText: #727273;
      --borderMuted: #727273;
      --thinkingOff: #727273;
      --selectedBg: #000000;
      --searchMatchBg: #000000;
      --userMessageBg: #000000;
      --customMessageBg: #000000;
      --toolPendingBg: #000000;
      --toolSuccessBg: #000000;
      --toolErrorBg: #000000;
      --exportPageBg: rgb(0, 0, 0);
      --exportCardBg: rgb(0, 0, 0);
      --exportInfoBg: rgb(20, 15, 0);
      --body-bg: rgb(0, 0, 0);
      --container-bg: rgb(0, 0, 0);
      --info-bg: rgb(20, 15, 0);
    }

    * { margin: 0; padding: 0; box-sizing: border-box; }

    :root {
      --line-height: 18px; /* 12px font * 1.5 */
      --sidebar-width: 400px;
      --sidebar-min-width: 240px;
      --sidebar-max-width: 840px;
      --sidebar-resizer-width: 6px;
    }

    body {
      font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
      font-size: 12px;
      line-height: var(--line-height);
      color: var(--text);
      background: var(--body-bg);
    }

    body.sidebar-resizing {
      cursor: col-resize;
      user-select: none;
    }

    #app {
      display: flex;
      min-height: 100vh;
    }

    /* Sidebar */
    #sidebar {
      width: var(--sidebar-width);
      min-width: var(--sidebar-width);
      max-width: var(--sidebar-width);
      background: var(--container-bg);
      flex-shrink: 0;
      display: flex;
      flex-direction: column;
      position: sticky;
      top: 0;
      height: 100vh;
      border-right: 1px solid var(--dim);
    }

    #sidebar-resizer {
      width: var(--sidebar-resizer-width);
      flex-shrink: 0;
      position: sticky;
      top: 0;
      height: 100vh;
      cursor: col-resize;
      touch-action: none;
      background: transparent;
      border-right: 1px solid transparent;
    }

    #sidebar-resizer:hover,
    body.sidebar-resizing #sidebar-resizer {
      background: var(--selectedBg);
      border-right-color: var(--dim);
    }

    .sidebar-header {
      padding: 8px 12px;
      flex-shrink: 0;
    }

    .sidebar-controls {
      padding: 8px 8px 4px 8px;
    }

    .sidebar-search {
      width: 100%;
      box-sizing: border-box;
      padding: 4px 8px;
      font-size: 11px;
      font-family: inherit;
      background: var(--body-bg);
      color: var(--text);
      border: 1px solid var(--dim);
      border-radius: 3px;
    }

    .sidebar-filters {
      display: flex;
      padding: 4px 8px 8px 8px;
      gap: 4px;
      align-items: center;
      flex-wrap: wrap;
    }

    .sidebar-search:focus {
      outline: none;
      border-color: var(--accent);
    }

    .sidebar-search::placeholder {
      color: var(--muted);
    }

    .filter-btn {
      padding: 3px 8px;
      font-size: 10px;
      font-family: inherit;
      background: transparent;
      color: var(--muted);
      border: 1px solid var(--dim);
      border-radius: 3px;
      cursor: pointer;
    }

    .filter-btn:hover {
      color: var(--text);
      border-color: var(--text);
    }

    .filter-btn.active {
      background: var(--accent);
      color: var(--body-bg);
      border-color: var(--accent);
    }

    .sidebar-close {
      display: none;
      padding: 3px 8px;
      font-size: 12px;
      font-family: inherit;
      background: transparent;
      color: var(--muted);
      border: 1px solid var(--dim);
      border-radius: 3px;
      cursor: pointer;
      margin-left: auto;
    }

    .sidebar-close:hover {
      color: var(--text);
      border-color: var(--text);
    }

    .tree-container {
      flex: 1;
      overflow: auto;
      padding: 4px 0;
    }

    .tree-node {
      padding: 0 8px;
      cursor: pointer;
      display: flex;
      align-items: baseline;
      font-size: 11px;
      line-height: 13px;
      white-space: nowrap;
    }

    .tree-node:hover {
      background: var(--selectedBg);
    }

    .tree-node.active {
      background: var(--selectedBg);
    }

    .tree-node.active .tree-content {
      font-weight: bold;
    }

    .tree-node.in-path {
      background: color-mix(in srgb, var(--accent) 10%, transparent);
    }

    .tree-node:not(.in-path) {
      opacity: 0.5;
    }

    .tree-node:not(.in-path):hover {
      opacity: 1;
    }

    .tree-prefix {
      color: var(--muted);
      flex-shrink: 0;
      font-family: monospace;
      white-space: pre;
    }

    .tree-marker {
      color: var(--accent);
      flex-shrink: 0;
    }

    .tree-content {
      color: var(--text);
    }

    .tree-role-user {
      color: var(--accent);
    }

    .tree-role-skill {
      color: var(--customMessageLabel);
    }

    .tree-role-assistant {
      color: var(--success);
    }

    .tree-role-tool {
      color: var(--muted);
    }

    .tree-muted {
      color: var(--muted);
    }

    .tree-error {
      color: var(--error);
    }

    .tree-compaction {
      color: var(--borderAccent);
    }

    .tree-branch-summary {
      color: var(--warning);
    }

    .tree-custom-message {
      color: var(--customMessageLabel);
    }

    .tree-status {
      padding: 4px 12px;
      font-size: 10px;
      color: var(--muted);
      flex-shrink: 0;
    }

    /* Main content */
    #content {
      flex: 1;
      min-width: 0;
      overflow-y: auto;
      padding: var(--line-height) calc(var(--line-height) * 2);
      display: flex;
      flex-direction: column;
      align-items: center;
    }

    #content > * {
      width: 100%;
      max-width: 800px;
    }

    /* Help bar */
    .help-bar {
      font-size: 11px;
      color: var(--warning);
      margin-bottom: var(--line-height);
      display: flex;
      align-items: center;
      justify-content: space-between;
      flex-wrap: wrap;
      gap: 12px;
    }

    .help-hint {
      flex: 1 1 240px;
    }

    .help-actions {
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: 8px;
    }

    .header-toggle-btn,
    .download-json-btn {
      font-size: 10px;
      padding: 2px 8px;
      background: var(--container-bg);
      border: 1px solid var(--border);
      border-radius: 3px;
      color: var(--text);
      cursor: pointer;
      font-family: inherit;
    }

    .header-toggle-btn:hover,
    .download-json-btn:hover {
      background: var(--hover);
      border-color: var(--borderAccent);
    }

    .header-toggle-btn[aria-pressed="true"] {
      color: var(--accent);
      border-color: var(--accent);
      font-weight: bold;
    }

    /* Header */
    .header {
      background: var(--container-bg);
      border-radius: 4px;
      paddin

... [289842 bytes truncated] ...

hare:v1:sidebar-width';
      const MIN_CONTENT_WIDTH = 320;

      function isMobileLayout() {
        return window.matchMedia('(max-width: 900px)').matches;
      }

      function getSidebarBounds() {
        const rootStyles = getComputedStyle(document.documentElement);
        const minWidth = parseFloat(rootStyles.getPropertyValue('--sidebar-min-width')) || 240;
        const maxWidth = parseFloat(rootStyles.getPropertyValue('--sidebar-max-width')) || 720;
        const viewportMaxWidth = window.innerWidth - MIN_CONTENT_WIDTH;
        return {
          minWidth,
          maxWidth: Math.max(minWidth, Math.min(maxWidth, viewportMaxWidth))
        };
      }

      function clampSidebarWidth(width) {
        const { minWidth, maxWidth } = getSidebarBounds();
        return Math.max(minWidth, Math.min(maxWidth, width));
      }

      function applySidebarWidth(width) {
        document.documentElement.style.setProperty('--sidebar-width', `${Math.round(clampSidebarWidth(width))}px`);
      }

      function loadSidebarWidth() {
        try {
          const raw = localStorage.getItem(SIDEBAR_WIDTH_STORAGE_KEY);
          if (raw === null) return null;
          const width = Number(raw);
          return Number.isFinite(width) ? width : null;
        } catch {
          return null;
        }
      }

      function saveSidebarWidth(width) {
        try {
          localStorage.setItem(SIDEBAR_WIDTH_STORAGE_KEY, String(Math.round(clampSidebarWidth(width))));
        } catch {
          // Ignore storage failures (e.g. private browsing restrictions)
        }
      }

      function setupSidebarResize() {
        const savedWidth = loadSidebarWidth();
        if (savedWidth !== null) {
          applySidebarWidth(savedWidth);
        }

        if (!sidebarResizer) return;

        let cleanupDrag = null;

        const stopDrag = (pointerId) => {
          if (cleanupDrag) {
            cleanupDrag(pointerId);
            cleanupDrag = null;
          }
        };

        sidebarResizer.addEventListener('pointerdown', (e) => {
          if (isMobileLayout()) return;

          e.preventDefault();
          const startX = e.clientX;
          const startWidth = sidebar.getBoundingClientRect().width;
          document.body.classList.add('sidebar-resizing');
          sidebarResizer.setPointerCapture?.(e.pointerId);

          const onPointerMove = (event) => {
            applySidebarWidth(startWidth + (event.clientX - startX));
          };

          cleanupDrag = (pointerIdToRelease) => {
            document.body.classList.remove('sidebar-resizing');
            sidebarResizer.releasePointerCapture?.(pointerIdToRelease);
            window.removeEventListener('pointermove', onPointerMove);
            window.removeEventListener('pointerup', onPointerUp);
            window.removeEventListener('pointercancel', onPointerCancel);
            saveSidebarWidth(sidebar.getBoundingClientRect().width);
          };

          const onPointerUp = (event) => stopDrag(event.pointerId);
          const onPointerCancel = (event) => stopDrag(event.pointerId);

          window.addEventListener('pointermove', onPointerMove);
          window.addEventListener('pointerup', onPointerUp);
          window.addEventListener('pointercancel', onPointerCancel);
        });

        sidebarResizer.addEventListener('dblclick', () => {
          if (isMobileLayout()) return;
          applySidebarWidth(400);
          saveSidebarWidth(400);
        });

        window.addEventListener('resize', () => {
          if (isMobileLayout()) return;
          applySidebarWidth(sidebar.getBoundingClientRect().width);
        });
      }

      setupSidebarResize();

      hamburger.addEventListener('click', () => {
        sidebar.classList.add('open');
        overlay.classList.add('open');
        hamburger.style.display = 'none';
      });

      const closeSidebar = () => {
        sidebar.classList.remove('open');
        overlay.classList.remove('open');
        hamburger.style.display = '';
      };

      overlay.addEventListener('click', closeSidebar);
      document.getElementById('sidebar-close').addEventListener('click', closeSidebar);

      // Toggle states
      let thinkingExpanded = true;
      let toolOutputsExpanded = false;
      let showHiddenMessages = false;

      function setHiddenMessagesVisible(visible) {
        showHiddenMessages = visible;
        document.body.classList.toggle('show-hidden-messages', visible);
        const button = document.querySelector('[data-action="toggle-hidden-messages"]');
        if (button) {
          button.setAttribute('aria-pressed', String(visible));
          button.textContent = visible ? 'Hide hidden messages' : 'Show hidden messages';
        }
      }

      function setThinkingExpanded(expanded) {
        thinkingExpanded = expanded;
        document.querySelector('[data-action="toggle-thinking"]')?.setAttribute('aria-pressed', String(expanded));
        document.querySelectorAll('.thinking-text').forEach(el => {
          el.style.display = thinkingExpanded ? '' : 'none';
        });
        document.querySelectorAll('.thinking-collapsed').forEach(el => {
          el.style.display = thinkingExpanded ? 'none' : 'block';
        });
      }

      function setToolOutputsExpanded(expanded) {
        toolOutputsExpanded = expanded;
        document.querySelector('[data-action="toggle-tools"]')?.setAttribute('aria-pressed', String(expanded));
        document.querySelectorAll('.tool-output.expandable').forEach(el => {
          el.classList.toggle('expanded', toolOutputsExpanded);
        });
        document.querySelectorAll('.compaction').forEach(el => {
          el.classList.toggle('expanded', toolOutputsExpanded);
        });
        document.querySelectorAll('.skill-invocation').forEach(el => {
          el.classList.toggle('expanded', toolOutputsExpanded);
        });
      }

      const attachHeaderHandlers = () => {
        document.querySelector('[data-action="toggle-thinking"]')?.addEventListener('click', () => {
          setThinkingExpanded(!thinkingExpanded);
        });
        document.querySelector('[data-action="toggle-tools"]')?.addEventListener('click', () => {
          setToolOutputsExpanded(!toolOutputsExpanded);
        });
        document.querySelector('[data-action="toggle-hidden-messages"]')?.addEventListener('click', () => {
          setHiddenMessagesVisible(!showHiddenMessages);
        });
      };

      const isEditableTarget = (element) => {
        if (!element) return false;
        const tagName = element.tagName;
        if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT' || tagName === 'BUTTON') {
          return true;
        }
        return element.isContentEditable || Boolean(element.closest?.('[contenteditable="true"]'));
      };

      // Keyboard shortcuts
      document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape') {
          searchInput.value = '';
          searchQuery = '';
          navigateTo(leafId, 'bottom');
        }

        if (e.ctrlKey || e.metaKey || e.altKey || isEditableTarget(document.activeElement)) {
          return;
        }

        const key = e.key.toLowerCase();
        if (key === 't') {
          e.preventDefault();
          setThinkingExpanded(!thinkingExpanded);
        } else if (key === 'o') {
          e.preventDefault();
          setToolOutputsExpanded(!toolOutputsExpanded);
        } else if (key === 'h') {
          e.preventDefault();
          setHiddenMessagesVisible(!showHiddenMessages);
        }
      });

      // Initial render
      // If URL has targetId, scroll to that specific message; otherwise stay at top
      if (leafId) {
        if (urlTargetId && byId.has(urlTargetId)) {
          // Deep link: navigate to leaf and scroll to target message
          navigateTo(leafId, 'target', urlTargetId);
        } else {
          navigateTo(leafId, 'none');
        }
      } else if (entries.length > 0) {
        // Fallback: use last entry if no leafId
        navigateTo(entries[entries.length - 1].id, 'none');
      }
    })();

  </script>
</body>
</html>
Evidence: Live results, original failure reproduction, and mutation evidence

Source: Live results, original failure reproduction, and mutation evidence

=== pi-0992-before.log ===
ok - Pi calm centralizes transcript visibility, preserves execution/export data, keeps Pi's stock working row visible while no run is active, and persists its choice across session starts


=== pi-103-before.log ===
not ok - Pi calm renderer and lifecycle contract failed: file://~/.no-mistakes/worktrees/7f0ec18181b6/01M45RE5DJ7B3MS5TT4S6FF568/.test-live/tmp/fm-calm-pi-extension.b8yj7m/renderer/[eval1]:547
      throw new Error(`${name} disappeared from ${command} HTML while calm mode was on`);
            ^

Error: grep disappeared from /export calm.html HTML while calm mode was on
    at assertStockHtmlRendering (file://~/.no-mistakes/worktrees/7f0ec18181b6/01M45RE5DJ7B3MS5TT4S6FF568/.test-live/tmp/fm-calm-pi-extension.b8yj7m/renderer/[eval1]:547:13)
    at file://~/.no-mistakes/worktrees/7f0ec18181b6/01M45RE5DJ7B3MS5TT4S6FF568/.test-live/tmp/fm-calm-pi-extension.b8yj7m/renderer/[eval1]:554:7
    at process.processTicksAndRejections (node:internal/process/task_queues:103:5)

Node.js v22.23.2


=== pi-0992-calm.log ===
ok - Pi calm centralizes transcript visibility, preserves execution/export data, keeps Pi's stock working row visible while no run is active, and persists its choice across session starts
ok - Pi calm native E2E replaces the stock working row with a moving, resize-clamped working ship that freezes and resumes across two working periods in one Pi session, clears on abort, keeps captain turns visible, hides exact operational user rows without changing persistence, restores stock rendering Calm-off, survives restart, and preserves export plus Ctrl+O behavior


=== pi-103-calm.log ===
ok - Pi calm centralizes transcript visibility, preserves execution/export data, keeps Pi's stock working row visible while no run is active, and persists its choice across session starts
ok - Pi calm native E2E replaces the stock working row with a moving, resize-clamped working ship that freezes and resumes across two working periods in one Pi session, clears on abort, keeps captain turns visible, hides exact operational user rows without changing persistence, restores stock rendering Calm-off, survives restart, and preserves export plus Ctrl+O behavior


=== pi-0992-branch.log ===
ok - fm_branch_outcomes hides through ToolExecutionComponent while Calm-off and HTML export stay stock


=== pi-103-branch.log ===
ok - fm_branch_outcomes hides through ToolExecutionComponent while Calm-off and HTML export stay stock


=== pi-103-branch-mutation-before.log ===
ok - fm_branch_outcomes hides through ToolExecutionComponent while Calm-off and HTML export stay stock


=== pi-103-branch-mutation-after.log ===
not ok - Pi outcomes rendering consumers must preserve stock behavior: file://~/.no-mistakes/worktrees/7f0ec18181b6/01M45RE5DJ7B3MS5TT4S6FF568/.test-live/tmp/fm-pi-branch-extension.0EYrNT/stock-render-consumers/[eval1]:130
  throw new Error("stock export rendering did not delegate to Pi's structured fallback");
        ^

Error: stock export rendering did not delegate to Pi's structured fallback
    at file://~/.no-mistakes/worktrees/7f0ec18181b6/01M45RE5DJ7B3MS5TT4S6FF568/.test-live/tmp/fm-pi-branch-extension.0EYrNT/stock-render-consumers/[eval1]:130:9
    at process.processTicksAndRejections (node:internal/process/task_queues:103:5)

Node.js v22.23.2: expected exit 0, got 1


=== pi-0992-branch-live.log ===
Pi 0992: stock branch outputs visible; Calm hides; non-submit Enter neither exports nor reveals; Alt+S exports persisted outcomes with stock HTML fallback; Calm hiding restores; toggle off reveals outcomes.


=== pi-103-branch-live.log ===
Pi 103: stock branch outputs visible; Calm hides; non-submit Enter neither exports nor reveals; Alt+S exports persisted outcomes with stock HTML fallback; Calm hiding restores; toggle off reveals outcomes.


=== pi-0992-queue-retention.log ===
FM_TEST_BEGIN 2026-10-05T10:18:25Z tests/fm-calm-pi-queue-retention-live-e2e.test.sh family=live-harness-optin expected_gate_skip=live-capability
ok - Pi 0.99.2 exposes every queue-retention member Calm preflights before hiding queued Firstmate rows
FM_TEST_END 2026-10-05T10:18:27Z tests/fm-calm-pi-queue-retention-live-e2e.test.sh exit=0 duration_ms=2292 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=2407
FM_TEST_SUMMARY_FAMILY family=live-harness-optin count=1 duration_ms=2292 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-calm-pi-queue-retention-live-e2e.test.sh duration_ms=2292


=== pi-103-queue-retention.log ===
FM_TEST_BEGIN 2026-10-05T10:16:03Z tests/fm-calm-pi-queue-retention-live-e2e.test.sh family=live-harness-optin expected_gate_skip=live-capability
ok - Pi 1.0.3 exposes every queue-retention member Calm preflights before hiding queued Firstmate rows
FM_TEST_END 2026-10-05T10:16:05Z tests/fm-calm-pi-queue-retention-live-e2e.test.sh exit=0 duration_ms=1696 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=1790
FM_TEST_SUMMARY_FAMILY family=live-harness-optin count=1 duration_ms=1696 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-calm-pi-queue-retention-live-e2e.test.sh duration_ms=1696


=== pi-0992-screenshot.log ===
{"html":"~/.no-mistakes/evidence/01M45RE5DJ7B3MS5TT4S6FF568/pi-0992/calm-export.html","screenshot":"~/.no-mistakes/evidence/01M45RE5DJ7B3MS5TT4S6FF568/pi-0992-calm.png","visible":{"marker":"CALM_EXPORT_GREP","text":"sample.txt:1:CALM_EXPORT_GREP","tools":["grep","find"]}}


=== pi-103-screenshot.log ===
{"html":"~/.no-mistakes/evidence/01M45RE5DJ7B3MS5TT4S6FF568/pi-103/calm-export.html","screenshot":"~/.no-mistakes/evidence/01M45RE5DJ7B3MS5TT4S6FF568/pi-103-calm.png","visible":{"marker":"CALM_EXPORT_GREP","text":"sample.txt:1:CALM_EXPORT_GREP","tools":["grep","find"]}}


=== pi-103-branch-screenshot.log ===
{"html":"~/.no-mistakes/evidence/01M45RE5DJ7B3MS5TT4S6FF568/branch-103/export.html","screenshot":"~/.no-mistakes/evidence/01M45RE5DJ7B3MS5TT4S6FF568/pi-103-branch.png","visible":{"marker":"LIVE_BRANCH_OUTCOME_ONE","text":"LIVE_BRANCH_OUTCOME_ONE","tools":["fm_branch_outcomes","fm_branch_outcomes"]}}
Evidence: Validation setup and preserved-driver explanation

Source: Validation setup and preserved-driver explanation

Pi 0.99.2 was selected from /usr/bin/pi, and Pi 1.0.3 from a disposable worktree-local npm installation. No production installation was changed.
calm-focused.sh executes exactly test_rendering_and_session_lifecycle and test_interactive_terminal_e2e from tests/fm-calm-pi-extension.test.sh. Its only added behavior copies generated artifacts before standard cleanup.
branch-focused.sh executes exactly test_outcomes_tool_uses_stock_execution_and_export_consumers from tests/fm-pi-branch-extension.test.sh. Other function definitions are copied unchanged, and tests/lib.sh is sourced from this worktree.
calm-before.sh removes the two new lookup callbacks and executes only test_rendering_and_session_lifecycle. It passes on 0.99.2 and reproduces the reported grep failure on 1.0.3.
branch-mutated.sh overrides the registered tool definition's renderCall and renderResult in the fixture to emit UNEXPECTED_CUSTOM_EXPORT. The unchanged fallback assertion rejects this injected defect on 1.0.3. branch-mutated-before.sh additionally removes the two new lookup callbacks, causing the same defect to be missed. Expected mutation failures demonstrate test sensitivity, not product failures.
branch-live.py starts the real Pi CLI on a private named fm-lab-branch-* tmux socket at 180x44, loads unmodified Calm and branch extensions from a scratch project, and opens generated session history. It drives /calm, /export with remapped submit keys, and /calm again. No model or external provider response is required. Its finally block kills the private server.
The existing Calm interactive E2E uses a deterministic local provider with the real Pi terminal lifecycle. Production credentials are not accessed.
Two initial setup issues were resolved before successful reruns: npm's hoisted dependencies required a scratch package-local node_modules symlink; Chromium needed its incidental temporary socket files under /tmp because the worktree TMPDIR pathname was too long.
Chromium screenshots load the real exported HTML and its JavaScript, locate a visible tool marker in the conversation, scroll it into view, and capture the viewport.
Worktree test fixtures, package installations, browser profiles, and copied drivers were removed after validation. Evidence copies remain here.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ tests/fm-pi-branch-extension.test.sh:5282 - This change misses one other test that uses the same renamed Pi lookup. fm-pi-branch-extension.test.sh:5281-5282 still passes only getToolDefinition to createToolHtmlRenderer. Pi's tool-renderer wraps the lookup in try/catch and returns undefined on any error (see dist/core/export-html/tool-renderer.js in renderCall and renderResult). Under Pi 1.0.3, which this change says looks up renderers through getToolRenderers, the missing function throws a TypeError. The renderer catches it, so actualCall and actualResult are always undefined. The assertion at about line 5287 expects undefined ("stock export rendering did not delegate to Pi's structured fallback"), so it now passes without testing anything: a regression that made the fm_branch_outcomes renderer emit HTML during Calm stock export would go undetected on CI's unpinned Pi. Fix: add getToolRenderers: () =&gt; stockDefinition and getToolRenderers: () =&gt; actualDefinition next to the existing getToolDefinition entries at lines 5281 and 5282, matching the fixture change in tests/fm-calm-pi-extension.test.sh:1668,1700.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
Export a session while Calm is enabled on Pi 0.99.2 and 1.0.3; grep, find, and watcher tool data remain in the rendered export ✅ pass live Pi 1.0.3 and Pi 0.99.2 Calm export screenshots; validation-transcript.txt
Remap submit to Alt+S and press Enter on an export command; neither version exports or reveals hidden branch outcomes ✅ pass live branch-0992/non-submit.txt, branch-103/non-submit.txt, and validation-transcript.txt
Submit branch-outcome export with Alt+S on both versions; Pi uses stock HTML fallback, preserves results, and restores Calm hiding afterward ✅ pass live Pi 1.0.3 branch export screenshot; branch-0992/export.html, branch-103/export.html, and validation-transcript.txt
Queue a follow-up in a real Pi session; both versions expose every queue-retention capability Calm requires ✅ pass live pi-0992-queue-retention.log and pi-103-queue-retention.log
  • pi --version, node --version, and chromium --version; installed Pi 1.0.3 only in disposable worktree-local scratch storage.
  • bash .test-live/calm-focused.sh on both versions, selecting test_rendering_and_session_lifecycle and test_interactive_terminal_e2e from tests/fm-calm-pi-extension.test.sh.
  • bash .test-live/branch-focused.sh on both versions, selecting test_outcomes_tool_uses_stock_execution_and_export_consumers from tests/fm-pi-branch-extension.test.sh.
  • bash .test-live/calm-before.sh: legacy version passed; Pi 1.0.3 reproduced the exact reported grep-export failure with the new callbacks removed.
  • bash .test-live/branch-mutated-before.sh and bash .test-live/branch-mutated.sh: the old fixture missed injected custom export rendering; the corrected fixture rejected it.
  • python3 .test-live/branch-live.py 0992 /usr/bin/pi and python3 .test-live/branch-live.py 103 &#34;$PWD/.test-live/pi-103/node_modules/.bin/pi&#34;; real CLI sessions used private 180x44 terminals.
  • bash bin/fm-test-run.sh --jobs 1 tests/fm-calm-pi-queue-retention-live-e2e.test.sh with each matching Pi CLI.
  • node .test-live/capture-export.mjs captured Chromium screenshots of both Calm exports and the Pi 1.0.3 branch export; visually inspected screenshots.
  • Removed task-owned scratch installations, fixtures, and browser profiles; git status --short confirmed a clean worktree.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@peterOC26
peterOC26 merged commit f1f1a6b into main Oct 5, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant