From f19e05c87d32c4c4e7b04a10773a5028129379ed Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:23:22 +0000 Subject: [PATCH 1/2] T023, U-4: swb-session.js spells the model's two names at the call sites again (plan 038) submitSession had bound `headersOf` and `repairWith` from the model namespace (Copilot review, round 3 of #18), which respelled the two calls that tests/test_session_confinement.py pins in both write transports. Two nodes there went red in the composed run: test_the_renderer_presents_the_console_token_from_the_capability_probe test_the_renderer_repairs_a_stale_token_by_re_reading_it_never_by_reloading The fix shadows consoleHeaders and withConsoleRepair inside submitSession with the bound namespace, or with MODULE_TRANSPORT (the module-level pair, each reaching the current install at call time) when there is none. The calls read as the pinned spellings again and the bound behaviour is unchanged. No protected suite is edited. Lane: openXfactory-3 Co-Authored-By: Claude Sonnet 5.5 --- src/openxdox/web/views/swb-session.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/openxdox/web/views/swb-session.js b/src/openxdox/web/views/swb-session.js index df1f1d7..a68f6a9 100644 --- a/src/openxdox/web/views/swb-session.js +++ b/src/openxdox/web/views/swb-session.js @@ -311,16 +311,29 @@ export function sessionEndingReport(branch) { // mount reads the module-level install, which is the shape every affordance in // this file has always used; `firstEditTransport` passes its own, because its // returned transport outlives the call that built it (Copilot review, round 3). +// +// THE CALL SPELLINGS ARE PINNED. `tests/test_session_confinement.py` holds both +// write transports, this one and `swb-create.js`, to the model's own two names at +// their two call sites, so `submitSession` shadows those names with the pair it +// is to use instead of calling the pair through local aliases. The pinned text is +// deliberately not quoted in this comment: a needle quoted in its own source +// satisfies itself. `MODULE_TRANSPORT` is the module-level pair, each reaching +// the CURRENT install at call time exactly as the two consts above do, so with no +// `bound` the behaviour is the one the earlier ternaries gave. +const MODULE_TRANSPORT = { + consoleHeaders: (...args) => consoleHeaders(...args), + withConsoleRepair: (...args) => withConsoleRepair(...args), +}; + async function submitSession(affordance, body, fetcher, caps, repair, bound) { const doFetch = fetcher || fetch; - const headersOf = bound ? bound.consoleHeaders : consoleHeaders; - const repairWith = bound ? bound.withConsoleRepair : withConsoleRepair; + const { consoleHeaders, withConsoleRepair } = bound || MODULE_TRANSPORT; const send = async () => { const response = await doFetch(sessionRoute(affordance), { method: "POST", // the human-console header among them (FR-019's third clause): a write that // cannot present this serve's token is refused before its body is parsed - headers: headersOf(caps), + headers: consoleHeaders(caps), body: JSON.stringify(body), }); try { @@ -329,7 +342,7 @@ async function submitSession(affordance, body, fetcher, caps, repair, bound) { return { ok: false, message: "malformed response (HTTP " + response.status + ")" }; } }; - return repairWith(send, repair); + return withConsoleRepair(send, repair); } // ---- the doxBench Save transport (010-doxbench-editor-chat T080) ----------- From c3c6a470a0485e1c8774f10d4ede7aa3d594f1a9 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Tue, 6 Oct 2026 12:55:06 +0000 Subject: [PATCH 2/2] T023, U-4: commit the bound and unbound probes of submitSession's transport (Copilot's finding on #42, holder-ruled fix-now) One new file, tests/test_swb_session_transport.py, two node probes against the composed bundle, asserting on the request that reaches the injected fetcher: - BOUND: two firstEditTransport transports built from two models that differ in consoleHeaders and withConsoleRepair, both built before either is called; each request must carry its own model's header and go through its own model's repair policy. - UNBOUND: a mounted session form, submit pressed; the request must carry the mounted model's header and go through its repair policy. The pinned call spellings (tests/test_session_confinement.py) read the file as text, so they cannot tell the right model from the wrong one. With these two, an always-module-pair submitSession (M3) and a module pair wired to the wrong function (M4, M6) go red; the unfixed main passes both, so they describe the behaviour the respelling preserves. No protected suite is edited, and the file is outside the declared exclusion. Lane: openXfactory-3 Co-Authored-By: Claude Sonnet 5.5 --- tests/test_swb_session_transport.py | 205 ++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 tests/test_swb_session_transport.py diff --git a/tests/test_swb_session_transport.py b/tests/test_swb_session_transport.py new file mode 100644 index 0000000..9e70e5a --- /dev/null +++ b/tests/test_swb_session_transport.py @@ -0,0 +1,205 @@ +"""`swb-session.js`'s write transport, EXECUTED: which model's header and repair +policy a session request is sent through. + +WHY THIS FILE EXISTS. `submitSession` is the ONE request site of the session +column (see the comment above it in `views/swb-session.js`). It takes the two +model functions it needs, `consoleHeaders` and `withConsoleRepair`, from one of +two places: the model namespace its caller validated (`bound`, which is what +`firstEditTransport` passes, because the transport it returns outlives the call +that built it), or, with none, the module-level seam that reads the model the +current mount installed. + +`tests/test_session_confinement.py` pins how those two calls are SPELLED, and +only that: it reads the file as text. A pass there says the two names appear at +their call sites, not that the right model answers them. Copilot's review of the +change that restored those spellings (openXdox-code#42) saw the gap: always +selecting the module-level pair, or wiring either wrapper to the wrong function, +left every committed test green. These two probes close it. They run the shipped +module under node, out of a COMPOSED bundle, and assert on the request that +reaches the injected fetcher: + + * BOUND: two transports are built from two models that differ in the very two + functions under test, then both are called. Each request must carry its own + model's header and go through its own model's repair policy. A `submitSession` + that always read the module-level install would send both through the model + built LAST. + * UNBOUND: a mounted session form is submitted. The request must carry the + header the mounted model's `consoleHeaders` builds from the page's `caps`, and + go through that model's `withConsoleRepair`. A module-level pair that is wired + to the wrong function sends no header, or throws before it sends. + +WHAT THE HARNESS DOES NOT MEASURE. The outcome panel the column writes AFTER the +request needs the shell's `page-overlay` host, which a stub DOM does not provide, +so the form's click handler rejects after the fetcher has been called. The probe +records that rejection and asserts on the REQUEST only, which is what these +tests are about. + +A CREATED file, with no manifest row (RULED OQ-C), outside the declared +exclusion: it needs openDox's `web/` bundle and node, as +`tests/test_gate_loop_probes.py` does, and no openxFactory module. +""" + +from __future__ import annotations + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +import opendox_bundle + +NODE = shutil.which("node") + +_DOM_STUB = r""" +class Node { + constructor(tag) { + this.tag = tag; this.children = []; this.attrs = {}; this.listeners = {}; + this.className = ""; this._text = ""; this.type = ""; this.title = ""; + this.value = ""; this.checked = false; this.disabled = false; + this.hidden = false; + } + get textContent() { return this._text; } + set textContent(v) { this._text = String(v); this.children = []; } + set innerHTML(v) { + if (v !== "") throw new Error("innerHTML assigned " + v); + this.children = []; this._text = ""; + } + appendChild(c) { this.children.push(c); return c; } + append(...kids) { for (const k of kids) this.appendChild(k); } + setAttribute(k, v) { this.attrs[k] = String(v); } + addEventListener(t, fn) { (this.listeners[t] = this.listeners[t] || []).push(fn); } + focus() {} + click() { for (const fn of this.listeners.click || []) fn({}); } +} +globalThis.document = { createElement: (tag) => new Node(tag) }; +function flatten(node, out = []) { + out.push(node); + for (const c of node.children) flatten(c, out); + return out; +} +globalThis.buttons = (node) => flatten(node).filter((n) => n.tag === "button"); +globalThis.settle = () => new Promise((r) => setTimeout(r, 0)); +// The outcome panel needs a shell host this stub does not have (see the module +// docstring): the click handler rejects AFTER the request. Recorded, not fatal. +globalThis.rejections = []; +process.on("unhandledRejection", (e) => { globalThis.rejections.push(String(e)); }); +""" + + +@pytest.fixture +def views(tmp_path) -> Path: + """The COMPOSED bundle's `views/`, copied into `tmp_path` and marked as an + ES-module tree (a copy has no ancestor `package.json`, so node would read the + `export` syntax as CommonJS).""" + target = tmp_path / "web" + shutil.copytree(opendox_bundle.composed(), target) + (target / "package.json").write_text('{"type": "module"}\n', encoding="utf-8") + return target / "views" + + +def _run_node(body: str, views: Path) -> dict: + """Run an ES-module harness against the composed bundle and return the ONE + JSON object it prints last.""" + source = (_DOM_STUB + + f"\nconst VIEWS = {json.dumps(views.resolve().as_uri() + '/')};\n" + + body) + script = views.parent / "harness.mjs" + script.write_text(source, encoding="utf-8") + proc = subprocess.run([NODE, str(script)], capture_output=True, text=True, + timeout=120) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout.strip().splitlines()[-1]) + + +@pytest.mark.skipif(NODE is None, reason="node is not installed") +def test_js_each_built_transport_sends_through_its_own_models_header_and_repair( + views) -> None: + """BOUND. Two transports, two models that differ in exactly the two functions + `submitSession` takes from the namespace it was handed. BOTH are built before + EITHER is called, which is the interleaving a module-level read cannot survive: + the second build re-points the module-level install.""" + result = _run_node(""" +const column = await import(VIEWS + "swb-session.js"); +const model = await import(VIEWS + "staging-workbench-model.js"); +const repairs = []; +const make = (tag) => ({ + ...model, + consoleHeaders: (caps) => ({ "x-probe-model": tag, + "x-probe-token": String(caps.console_token) }), + withConsoleRepair: async (send, repair) => { repairs.push(tag); return send(); }, +}); +const sent = []; +const fetcher = async (route, opts) => { + sent.push({ route, method: opts.method, headers: opts.headers }); + return { ok: true, status: 200, json: async () => ({ ok: true }) }; +}; +const req = { key: { repository: "openxFactory", tile_kind: "staged", + tile_id: "t-1" }, + document: "d.md", content: "x" }; +const first = column.firstEditTransport({ + fetcher, caps: { console_token: "A" }, model: make("first") }); +const second = column.firstEditTransport({ + fetcher, caps: { console_token: "B" }, model: make("second") }); +await first(req); +await second(req); +console.log(JSON.stringify({ sent, repairs })); +""", views) + assert [s["method"] for s in result["sent"]] == ["POST", "POST"] + assert [s["headers"] for s in result["sent"]] == [ + {"x-probe-model": "first", "x-probe-token": "A"}, + {"x-probe-model": "second", "x-probe-token": "B"}, + ], "each request must carry the header ITS OWN model built, from its own caps" + assert result["repairs"] == ["first", "second"], ( + "each request must go through ITS OWN model's repair policy") + + +@pytest.mark.skipif(NODE is None, reason="node is not installed") +def test_js_a_mounted_form_sends_through_the_mounted_models_header_and_repair( + views) -> None: + """UNBOUND. A session form is mounted with a model whose `consoleHeaders` and + `withConsoleRepair` are markers, an affordance is opened and its submit + pressed. With no `bound` namespace, `submitSession` reaches the model through + the module-level pair, and the request must show both markers.""" + result = _run_node(""" +const column = await import(VIEWS + "swb-session.js"); +const model = await import(VIEWS + "staging-workbench-model.js"); +const repairs = []; +const mounted = { + ...model, + consoleHeaders: (caps) => ({ "x-probe-model": "mounted", + "x-probe-token": String(caps.console_token) }), + withConsoleRepair: async (send, repair) => { repairs.push("mounted"); return send(); }, +}; +const sent = []; +const fetcher = async (route, opts) => { + sent.push({ route, method: opts.method, headers: opts.headers }); + return { ok: true, status: 200, json: async () => ({ ok: true }) }; +}; +const host = document.createElement("div"); +const caps = { actions: { gate: true, session: true }, actor: "brett", + console_token: "tok1" }; +column.mountSessionAffordances(host, null, { + model: mounted, caps, fetcher, actor: "brett", + session: { scope: { kind: "staged", id: "t-1" }, + posture: { branch: "session/t-1", repository: "openxFactory" } }, +}); +const openers = buttons(host); +openers[0].click(); +await settle(); +const submits = buttons(host).filter( + (b) => !openers.includes(b) && b.textContent !== "cancel"); +for (const b of submits) b.click(); +await settle(); await settle(); +console.log(JSON.stringify({ openers: openers.length, submits: submits.length, + sent, repairs, rejections: globalThis.rejections })); +""", views) + assert result["openers"] >= 1 and result["submits"] == 1, result + assert [s["route"] for s in result["sent"]] == ["/actions/gate/edit-document"], result + assert result["sent"][0]["method"] == "POST" + assert result["sent"][0]["headers"] == { + "x-probe-model": "mounted", "x-probe-token": "tok1"}, ( + "the request must carry the header the mounted model's consoleHeaders built") + assert result["repairs"] == ["mounted"], ( + "the request must go through the mounted model's repair policy")