"""#6345 — collapsed summary-card rendering for process-wakeup messages. Behavioral coverage for the three ui.js helpers behind the card: ``_parseProcessWakeupBody`` (client inverse of ``format_wakeup_prompt``), ``_processWakeupInfo`` (server ``_wakeup_meta`` merged over the client parse), and ``_processWakeupCardHtml`` (the ``
`` card markup). Structural integration with the render loop is pinned by tests/test_process_wakeup_rendering.py. """ import json import re import shutil import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] UI_JS_PATH = ROOT / "static" / "ui.js" STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8") NODE = shutil.which("node") pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH") _DRIVER = r""" const fs = require('fs'); const src = fs.readFileSync(process.argv[1], 'utf8'); function extractFunc(name){ const start = src.indexOf('function ' + name); if(start === -1) throw new Error(name + ' not found'); const params = src.indexOf('(', start); let depth = 0, close = -1; for(let i=params; i/g,'>').replace(/"/g,'"'); } function li(name, size){ return ''; } function t(key, ...args){ let out = key; if(args.length) out += ':' + args.join(','); return out; } eval(extractFunc('_parseProcessWakeupBody')); eval(extractFunc('_processWakeupInfo')); eval(extractFunc('_processWakeupCardHtml')); const okBody = '[IMPORTANT: Background process proc_1 completed (exit_code=0).\nCommand: npm run build\nOutput:\nall good]'; const failBody = '[IMPORTANT: Background process proc_2 completed (exit_code=3).\nCommand: pytest -q\nOutput:\n1 failed]'; const signalBody = '[IMPORTANT: Background process proc_3 completed (exit_code=-9).\nCommand: sleep 999\nOutput:\n]'; const watchBody = '[IMPORTANT: Background process w1 matched watch pattern "ERROR.*timeout".\nCommand: tail -f app.log\nMatched output:\nERROR request timeout\n(3 earlier matches were suppressed by rate limit)]'; const htmlBody = '[IMPORTANT: Background process p completed (exit_code=0).\nCommand: echo ""\nOutput:\nbold]'; // Finding 1: leading indentation + trailing blank lines must survive. const wsBody = '[IMPORTANT: Background process p completed (exit_code=0).\nCommand: build\nOutput:\n indented line\n\n]'; // Finding 2: output that legitimately ends with the suppression phrasing must // be preserved intact (not lifted into a suppression field and dropped). const supLikeBody = '[IMPORTANT: Background process w9 matched watch pattern "ERR".\nCommand: tail\nMatched output:\nreal log\n(3 earlier matches were suppressed by rate limit)]'; const okInfo = _processWakeupInfo({}, okBody); const failInfo = _processWakeupInfo({}, failBody); const signalInfo = _processWakeupInfo({}, signalBody); const watchInfo = _processWakeupInfo({}, watchBody); const htmlInfo = _processWakeupInfo({}, htmlBody); const wsInfo = _processWakeupInfo({}, wsBody); const supLikeInfo = _processWakeupInfo({}, supLikeBody); const metaOnlyInfo = _processWakeupInfo( {_wakeup_meta: {type: 'completion', task_id: 'srv_1', command: 'cargo test', exit_code: 1}}, 'some future format the client parser does not know' ); const metaOverParse = _processWakeupInfo( {_wakeup_meta: {type: 'completion', task_id: 'authoritative', command: 'npm run build', exit_code: 0}}, okBody ); const extras = {timeHtml: '14:32', filesHtml: '', footHtml: '
'}; process.stdout.write(JSON.stringify({ okInfo, failInfo, watchInfo, metaOnlyInfo, metaOverParseTaskId: metaOverParse.taskId, unparseableIsNull: _processWakeupInfo({}, 'plain text') === null, emptyIsNull: _processWakeupInfo({content: ''}, '') === null, wsInfoOutput: wsInfo.output, supLikeInfo, okCard: _processWakeupCardHtml(okInfo, okBody, extras), failCard: _processWakeupCardHtml(failInfo, failBody, extras), signalCard: _processWakeupCardHtml(signalInfo, signalBody, extras), watchCard: _processWakeupCardHtml(watchInfo, watchBody, extras), htmlCard: _processWakeupCardHtml(htmlInfo, htmlBody, extras), wsCard: _processWakeupCardHtml(wsInfo, wsBody, extras), supLikeCard: _processWakeupCardHtml(supLikeInfo, supLikeBody, extras), metaOnlyCard: _processWakeupCardHtml(metaOnlyInfo, 'some future format the client parser does not know', extras), })); """ def _run_driver(): assert NODE is not None proc = subprocess.run( [NODE, "-e", _DRIVER, str(UI_JS_PATH)], text=True, capture_output=True, timeout=30, check=False, ) assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout) def test_client_parser_mirrors_the_two_structured_wakeup_shapes(): result = _run_driver() ok = result["okInfo"] assert ok["type"] == "completion" assert ok["taskId"] == "proc_1" assert ok["command"] == "npm run build" assert ok["exitCode"] == "0" assert ok["output"] == "all good" watch = result["watchInfo"] assert watch["type"] == "watch_match" assert watch["pattern"] == "ERROR.*timeout" assert watch["command"] == "tail -f app.log" # The suppression note is part of the output tail, never a separate field. assert watch["output"] == "ERROR request timeout\n(3 earlier matches were suppressed by rate limit)" assert "suppressed" not in watch assert result["unparseableIsNull"] is True assert result["emptyIsNull"] is True def test_output_whitespace_is_preserved_byte_for_byte(): # #6350 finding 1: leading indentation and trailing blank lines must not be # trimmed away in the rendered
.
    result = _run_driver()
    assert result["wsInfoOutput"] == "    indented line\n\n"
    ws_card = result["wsCard"]
    assert "
    indented line\n\n
" in ws_card def test_output_that_looks_like_suppression_metadata_is_kept_in_full(): # #6350 finding 2: adversarial output ending with the suppression phrasing # is preserved verbatim, with no suppression field inferred/deleted. result = _run_driver() sup = result["supLikeInfo"] assert sup["output"] == "real log\n(3 earlier matches were suppressed by rate limit)" assert "suppressed" not in sup assert "(3 earlier matches were suppressed by rate limit)" in result["supLikeCard"] def test_server_meta_is_authoritative_and_covers_unparseable_bodies(): result = _run_driver() meta_only = result["metaOnlyInfo"] assert meta_only["taskId"] == "srv_1" assert meta_only["command"] == "cargo test" assert meta_only["exitCode"] == 1 assert meta_only["output"] is None assert result["metaOverParseTaskId"] == "authoritative" # With no parsable output section the detail falls back to the raw body. assert "some future format" in result["metaOnlyCard"] def test_card_markup_collapsed_by_default_with_exit_chip(): result = _run_driver() ok_card = result["okCard"] assert ok_card.startswith('
') assert "open" not in ok_card.split(">", 1)[0] assert 'class="process-wakeup-chip ok"' in ok_card assert "exit 0" in ok_card assert "npm run build" in ok_card assert "[IMPORTANT" not in ok_card assert 'data-icon="chevron-right"' in ok_card assert 'data-icon="terminal"' in ok_card assert "process_wakeup_label" in ok_card assert '14:32' in ok_card fail_card = result["failCard"] assert 'class="process-wakeup-chip fail"' in fail_card assert "exit 3" in fail_card # Signal-killed processes report negative returncodes; still a failure. signal_card = result["signalCard"] assert 'class="process-wakeup-chip fail"' in signal_card assert "exit -9" in signal_card watch_card = result["watchCard"] assert 'class="process-wakeup-chip watch"' in watch_card assert "ERROR.*timeout" in watch_card # No separate suppression line anymore; the note rides in the output pre. assert "process_wakeup_suppressed" not in watch_card # Finding 4: the full pattern is rendered in the expanded detail (a # dedicated pattern row), not only inside the collapsed chip's hover title. assert "process-wakeup-pattern-row" in watch_card # Pattern appears twice: once in the collapsed chip, once in the detail row. assert watch_card.count("ERROR.*timeout") >= 2 def test_card_escapes_command_and_output(): result = _run_driver() html_card = result["htmlCard"] assert "