Behavioural regression guard for the Transparent Stream / Worklog reload path. Drives the real buildToolCard + _anchorSceneToolCallFromRow from static/ui.js via node and pins that a tool card reconstructed from a persisted activity_scene_v1 row keeps: - the full terminal output with a working Show-more expand (data-full), and - the diff body (diff-block) for patch/edit snippets. #4622 was reported as a regression after v0.51.547-.560, but that window RESTORED this activity (#4539/#4587); independent repro on master was negative. This is the permanent guard so the restore path can't silently lose full output / expand / diff again. Test-only; no product change. RED-proven: dropping the payload.result/output recovery in _anchorSceneToolCallFromRow makes the two restored-card tests fail. Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
This commit is contained in:
1 parent
d3de2cbc1d
commit
2621df98eb
1 file changed
+191
@@ -0,0 +1,191 @@
|
||||
"""Behavioural regression guard for issue #4622: in the Transparent Stream /
|
||||
Worklog view, restored (settled / reloaded) tool-call cards must keep showing
|
||||
the FULL terminal output with a working expand control, and patch/edit cards
|
||||
must still render their diff body.
|
||||
|
||||
This drives the ACTUAL ``buildToolCard`` and ``_anchorSceneToolCallFromRow``
|
||||
from ``static/ui.js`` via node (not a regex over the source). The reload path
|
||||
rebuilds a tool card from a persisted ``activity_scene_v1`` row through
|
||||
``_anchorSceneToolCallFromRow`` and then ``buildToolCard``; this pins that the
|
||||
reconstructed card:
|
||||
|
||||
* produces a "Show more" expand button for output over the 800-char cap, with
|
||||
the FULL output carried in ``data-full=`` (so the inline ``_toggleToolDiff``
|
||||
expand handler can reveal it), and
|
||||
* renders a ``diff-block`` for patch/edit snippets.
|
||||
|
||||
#4622 was reported as a regression after the v0.51.547-.560 wave (which actually
|
||||
*restored* this activity via #4539/#4587). Independent reproduction on master was
|
||||
negative; this test is the permanent guard so the restore path cannot silently
|
||||
lose full output / expand / diff again.
|
||||
"""
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).parent.parent.resolve()
|
||||
UI_JS_PATH = REPO_ROOT / "static" / "ui.js"
|
||||
|
||||
NODE = shutil.which("node")
|
||||
pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||||
|
||||
|
||||
_DRIVER_SRC = r"""
|
||||
const fs = require('fs');
|
||||
const src = fs.readFileSync(process.argv[2], 'utf8');
|
||||
|
||||
function extractFunc(name) {
|
||||
const re = new RegExp('function\\s+' + name + '\\s*\\(');
|
||||
const start = src.search(re);
|
||||
if (start < 0) throw new Error(name + ' not found');
|
||||
let i = src.indexOf('{', start);
|
||||
let depth = 1; i++;
|
||||
while (depth > 0 && i < src.length) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}') depth--;
|
||||
i++;
|
||||
}
|
||||
return src.slice(start, i);
|
||||
}
|
||||
|
||||
// Minimal DOM stub. buildToolCard does createElement('div') then assigns
|
||||
// row.innerHTML = `...` as a string; we capture that string and assert on it.
|
||||
function makeEl() {
|
||||
return {
|
||||
_class: '', innerHTML: '', _attrs: {},
|
||||
get className(){return this._class;}, set className(v){this._class=v;},
|
||||
dataset: {},
|
||||
setAttribute(k,v){this._attrs[k]=String(v);},
|
||||
getAttribute(k){return this._attrs[k];},
|
||||
removeAttribute(k){delete this._attrs[k];},
|
||||
appendChild(){}, querySelector(){return null;}, closest(){return null;},
|
||||
};
|
||||
}
|
||||
global.document = { createElement: () => makeEl() };
|
||||
global.window = {};
|
||||
|
||||
// Escaping + trivial label/icon helpers that buildToolCard references but which
|
||||
// are orthogonal to the output/diff behaviour under test.
|
||||
global.esc = (s)=>String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
global.li = (n)=>'<i data-lucide="'+n+'"></i>';
|
||||
global.toolIcon = ()=>'<i></i>';
|
||||
global._toolActionLabelText = (tc)=>String(tc&&tc.name||'tool');
|
||||
global._toolDisplayName = (tc)=>String(tc&&tc.name||'tool');
|
||||
global._toolDisclosureIdentity = ()=>'';
|
||||
global._toolCardPreviewText = ()=>'';
|
||||
global._formatToolArgPreview = ()=>'';
|
||||
global._toolDetailLeadText = ()=>'';
|
||||
global._toolDetailLeadLabel = ()=>'Shell';
|
||||
global._isMemorySave = ()=>false;
|
||||
global._isSkillUpdate = ()=>false;
|
||||
|
||||
// Real functions under test (extracted from the live source):
|
||||
for (const fn of ['_snippetLooksLikeDiff','_colorDiffLines','_toolActionKind',
|
||||
'_toolCardAllowsDetail','buildToolCard','_anchorSceneToolCallFromRow']) {
|
||||
eval(extractFunc(fn));
|
||||
}
|
||||
|
||||
const mode = process.argv[3];
|
||||
const payload = JSON.parse(process.argv[4]);
|
||||
let html;
|
||||
if (mode === 'direct') {
|
||||
// Build a card directly from a live tool-call object.
|
||||
html = buildToolCard(payload).innerHTML;
|
||||
} else if (mode === 'restored') {
|
||||
// Simulate the reload/settled path: reconstruct the tool call from a persisted
|
||||
// activity_scene_v1 row, then build the card from THAT.
|
||||
const tc = _anchorSceneToolCallFromRow(payload, {settled:true});
|
||||
const card = buildToolCard(tc);
|
||||
html = card.innerHTML;
|
||||
process.stdout.write(JSON.stringify({
|
||||
html,
|
||||
snippet_len: (tc.snippet||'').length,
|
||||
}));
|
||||
return;
|
||||
} else {
|
||||
throw new Error('unknown mode ' + mode);
|
||||
}
|
||||
process.stdout.write(JSON.stringify({ html, snippet_len: (payload.snippet||'').length }));
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def driver_path(tmp_path_factory):
|
||||
p = tmp_path_factory.mktemp("tool_card_restore_driver") / "driver.js"
|
||||
p.write_text(_DRIVER_SRC, encoding="utf-8")
|
||||
return str(p)
|
||||
|
||||
|
||||
def _run(driver_path, mode, payload):
|
||||
result = subprocess.run(
|
||||
[NODE, driver_path, str(UI_JS_PATH), mode, json.dumps(payload)],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed: {result.stderr}")
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
# A terminal output well over buildToolCard's 800-char display cap.
|
||||
LONG_SHELL_OUTPUT = "result line {}\n".format("x" * 20) * 120 # ~> 800 chars
|
||||
DIFF_SNIPPET = "@@ -1,3 +1,4 @@\n context\n-old line\n+new line\n+added line\n"
|
||||
|
||||
|
||||
class TestRestoredToolCardKeepsFullOutputAndExpand:
|
||||
"""#4622 symptom 1: restored terminal cards must keep full output + expand."""
|
||||
|
||||
def test_long_output_direct_card_has_expand_with_full_snippet(self, driver_path):
|
||||
tc = {"name": "terminal", "args": {"command": "ls -R /"},
|
||||
"snippet": LONG_SHELL_OUTPUT, "done": True}
|
||||
out = _run(driver_path, "direct", tc)
|
||||
assert "tool-card-more" in out["html"], (
|
||||
"a terminal output over the display cap must render a Show-more expand button"
|
||||
)
|
||||
# The inline _toggleToolDiff handler reveals data-full; it must be
|
||||
# present (it carries the full output, not the truncated preview).
|
||||
assert "data-full=" in out["html"]
|
||||
|
||||
def test_restored_scene_terminal_card_stays_expandable(self, driver_path):
|
||||
"""The reload/settled path reconstructs the card from a persisted scene
|
||||
row; it must still be expandable with the full output recovered."""
|
||||
scene_row = {
|
||||
"role": "tool", "status": "completed",
|
||||
"tool": {"name": "terminal", "args": {"command": "ls -R /"}},
|
||||
"payload": {"output": LONG_SHELL_OUTPUT},
|
||||
}
|
||||
out = _run(driver_path, "restored", scene_row)
|
||||
assert out["snippet_len"] >= len(LONG_SHELL_OUTPUT) - 4, (
|
||||
"restored tool call must recover the FULL output snippet from the "
|
||||
f"persisted scene row (got {out['snippet_len']} vs {len(LONG_SHELL_OUTPUT)})"
|
||||
)
|
||||
assert "tool-card-more" in out["html"], (
|
||||
"restored terminal card must still render the Show-more expand control"
|
||||
)
|
||||
|
||||
|
||||
class TestRestoredPatchCardRendersDiff:
|
||||
"""#4622 symptom 2: restored patch/edit cards must render the diff body."""
|
||||
|
||||
def test_direct_patch_card_renders_diff_block(self, driver_path):
|
||||
tc = {"name": "patch", "args": {"path": "foo.py"},
|
||||
"snippet": DIFF_SNIPPET, "is_diff": True, "done": True}
|
||||
out = _run(driver_path, "direct", tc)
|
||||
assert "diff-block" in out["html"]
|
||||
|
||||
def test_restored_scene_patch_card_renders_diff_block(self, driver_path):
|
||||
scene_row = {
|
||||
"role": "tool", "status": "completed",
|
||||
"tool": {"name": "patch", "args": {"path": "foo.py"}},
|
||||
"payload": {"output": DIFF_SNIPPET},
|
||||
}
|
||||
out = _run(driver_path, "restored", scene_row)
|
||||
assert out["snippet_len"] >= len(DIFF_SNIPPET) - 4, (
|
||||
"restored patch tool call must recover the full diff snippet"
|
||||
)
|
||||
assert "diff-block" in out["html"], (
|
||||
"restored patch card must render the diff body (diff-block) from the "
|
||||
"persisted scene snippet"
|
||||
)
|
||||
Reference in new issue
Block a user