From 78722406990f23c028f60676b351eeb9f365d529 Mon Sep 17 00:00:00 2001 From: iitzSeriZ Date: Sun, 27 Sep 2026 22:39:58 +0330 Subject: [PATCH 1/2] test(panels): make the panel life-cycle suites offline and sqlite-isolated Both installer panel suites fail on ubuntu-latest and pass on the authoring host, for two reasons that have nothing to do with the adapters they cover. Both are harness defects, and neither is fixed by changing production code. 1. sqlite3 leaked in from the runner. `sqlite: false` meant only "write no sqlite3 shim". The adapters decide with `command -v sqlite3` (rt_panel_rebecca_db_ready), and ubuntu-latest ships a real sqlite3, so the gate opened anyway, activation took the automatic path, and the manual-activation test read inactive outcome=auto ... (Automatic activation needs the sqlite3 command and Rebecca's SQLite database.) where it expected `manual`. Shadowing sqlite3 with a shim that fails cannot fix this: the gate asks whether the command exists, not whether it works. `sqlite: false` now removes every directory that holds a sqlite3 from the child's PATH, replacing each with a mirror of itself (the same entries, symlinked, minus sqlite3) so nothing else on the host stops resolving. A new test plants a real-looking sqlite3 -- plus a second tool, to prove the substituted directory still carries what it held -- and asserts that `command -v sqlite3` fails in the child while the other tool resolves. 2. The network leaked in from the payload. The payload was two designs. RT_TEMPLATES_AVAILABLE names seventeen, so rt_complete_install always judged the install short of what its version ships and downloaded the release pinned to that version. On a runner with network the download succeeded, the template store was rebuilt from the PUBLISHED v1.3.0 release, and verify then compared that release's artifact against this working tree's: Completing the Row-Template 1.3.0 installation (designs and installer files)... ok Installation completed: 17 design(s) available. FAIL canonical artifact does not match the selected template (row). The suites were therefore validating a published release against the branch, and doing it over the network -- which is the opposite of what they are for, and it made them depend on whatever v1.3.0 happened to contain. The payload is now the complete release (availableTemplateIds(), the same seventeen ids as RT_TEMPLATES_AVAILABLE), so rt_complete_install has nothing to complete and is never reached. curl and wget are denied in every fake host -- they record the attempt and fail -- so a regression fails the suite loudly instead of silently reaching the internet. The two life-cycle tests assert the recorded attempts are empty, so "this suite needs no network" is measured rather than assumed. Production behaviour is untouched: the installer, the adapters and the release tooling are byte-identical. The payload is a real release, and the only thing that changed is that the tests now hand the installer all of it. --- tests/helpers/panel-hosts.mjs | 109 +++++++++++++++++++++- tests/installer-panel-pasarguard.test.mjs | 24 ++++- tests/installer-panel-rebecca.test.mjs | 67 +++++++++++-- 3 files changed, 184 insertions(+), 16 deletions(-) diff --git a/tests/helpers/panel-hosts.mjs b/tests/helpers/panel-hosts.mjs index cca9706..579369a 100644 --- a/tests/helpers/panel-hosts.mjs +++ b/tests/helpers/panel-hosts.mjs @@ -12,7 +12,7 @@ * variables -- the same knobs a non-default APP_NAME would use, so no adapter * code is bypassed. * - * WHAT IS DOUBLED, AND WHY. Two external programs the adapters talk to: + * WHAT IS DOUBLED, AND WHY. Three external programs the adapters talk to: * * docker a shim that keeps "is the panel container running" in a state * directory, and on `compose up` loads the .env the way Docker @@ -22,15 +22,25 @@ * sqlite3 forwards to Python's sqlite3 module (as the 3X-UI suite does), so * Rebecca's database is a real SQLite file and every statement the * adapter runs is executed for real. + * curl denied, and the attempt recorded. These suites are offline; see + * wget NET_DENY_SHIM below for what used to leak in through them. * * Everything else -- the adapters, the transaction engine, the library -- is * the shipping code. + * + * `sqlite: false` means the host has NO sqlite3, and it is enforced rather than + * assumed: the child runs with a PATH from which every directory holding a + * sqlite3 has been removed (pathWithoutSqlite). The adapters gate on + * `command -v sqlite3`, so shadowing it with a shim that fails would still let + * the gate pass -- which is exactly how the runner's own sqlite3 changed the + * outcome of the manual-activation test. */ import { spawnSync } from 'node:child_process'; -import { chmodSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, readdirSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { chmodSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, readdirSync, existsSync, symlinkSync, rmSync } from 'node:fs'; import { createHash } from 'node:crypto'; -import { dirname, join, resolve } from 'node:path'; +import { delimiter, dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { build } from '../../tools/build.mjs'; @@ -209,6 +219,77 @@ finally: PYEOF `; +/* --- no network, and no sqlite3 the host happens to have -------------------- + * + * NETWORK. `rt_complete_install` downloads the release pinned to the INSTALLED + * version whenever it judges the install short of what that version ships. The + * panel suites used to build a two-design payload, and RT_TEMPLATES_AVAILABLE + * names seventeen, so the install was ALWAYS short: on a runner with network the + * download succeeded, the store was rebuilt from the PUBLISHED release, and the + * test then compared it against the LOCALLY built artifact. The payloads are + * complete now, so that download is not reached -- these shims are the guard + * rail. curl and wget are on PATH in every fake host, they record the attempt + * and they fail, so a regression fails the suite loudly instead of silently + * validating a published release against the working tree. + * + * SQLITE3. `rt_panel__db_ready` gates on `command -v sqlite3`, so a + * sqlite3 that merely FAILS is not enough -- the command must be unreachable. + * `sqlite: false` therefore scrubs every directory that holds a sqlite3 out of + * the child's PATH (see pathWithoutSqlite), rather than trusting that the host + * has none: ubuntu-latest ships a real one, which is what made the manual + * activation test pass on the authoring machine and fail on the runner. */ +const NET_DENY_SHIM = `#!/usr/bin/env bash +# Test double: this suite has no network. Record the attempt, then fail. +set -u +if [ -n "\${RT_TEST_NET_LOG:-}" ]; then + printf '%s %s\\n' "\${0##*/}" "$*" >> "$RT_TEST_NET_LOG" +fi +printf 'row-template tests: network access is denied in this suite (%s %s)\\n' "\${0##*/}" "$*" >&2 +exit 1 +`; + +/* A PATH that is the one given, minus every directory that holds a sqlite3 + executable. Those directories are replaced by a mirror of themselves -- the + same entries, symlinked, without sqlite3 -- so nothing else on the host stops + resolving. Cached per input, because mirroring /usr/bin is ~1200 links. */ +const mirrors = new Map(); +function mirrorWithoutSqlite(dir) { + const dest = join(tmpdir(), `row-nosqlite-${sha256(dir).slice(0, 12)}`); + if (!existsSync(dest)) { + mkdirSync(dest, { recursive: true }); + for (const entry of readdirSync(dir)) { + if (entry === 'sqlite3') continue; + try { symlinkSync(join(dir, entry), join(dest, entry)); } catch { /* unreadable entry: skip it */ } + } + process.on('exit', () => rmSync(dest, { recursive: true, force: true })); + } + return dest; +} + +export function pathWithoutSqlite(base = process.env.PATH || '') { + const hit = mirrors.get(base); + if (hit) return hit; + const out = []; + const seen = new Set(); + for (const dir of base.split(delimiter).filter(Boolean)) { + if (seen.has(dir)) continue; + seen.add(dir); + if (!existsSync(join(dir, 'sqlite3'))) { out.push(dir); continue; } + out.push(mirrorWithoutSqlite(dir)); + } + const path = out.join(delimiter); + mirrors.set(base, path); + return path; +} + +/* The network attempts a fake host recorded. Empty means the suite never tried: + assert on it, so "this test needs no network" is checked, not assumed. */ +export function netAttempts(base) { + const f = join(base, 'net.log'); + if (!existsSync(f)) return ''; + return readFileSync(f, 'utf8').trim(); +} + /* The transaction engine locks with flock and refuses to run without it. Git Bash on Windows has none, so -- exactly as tests/installer-transaction.test.mjs does -- a minimal double provides flock's contract (an exclusive lock on the @@ -242,6 +323,11 @@ function shims(base, { sqlite = true } = {}) { mkdirSync(bin, { recursive: true }); writeFileSync(join(bin, 'docker'), DOCKER_SHIM); chmodSync(join(bin, 'docker'), 0o755); + // Both downloaders rt_fetch_one can pick, denied in every host. + for (const prog of ['curl', 'wget']) { + writeFileSync(join(bin, prog), NET_DENY_SHIM); + chmodSync(join(bin, prog), 0o755); + } if (!HOST_HAS_FLOCK) { writeFileSync(join(bin, 'flock'), FLOCK_SHIM); chmodSync(join(bin, 'flock'), 0o755); @@ -318,7 +404,13 @@ export function pasarguardHost(base, { running = true, env = PG_ENV, compose = t const bin = shims(base, { sqlite: Boolean(db) }); return { app, dataDir, cliPath, docker, bin, db: dbPath, envFile: join(app, '.env'), - paths: { RT_PG_APP_DIR: app, RT_PG_DATA_DIR: dataDir, RT_PG_CLI: cliPath, RT_TEST_DOCKER: docker, RT_TEST_BIN: bin }, + paths: { + RT_PG_APP_DIR: app, RT_PG_DATA_DIR: dataDir, RT_PG_CLI: cliPath, + RT_TEST_DOCKER: docker, RT_TEST_BIN: bin, RT_TEST_NET_LOG: join(base, 'net.log'), + }, + // No sqlite3 shim means the host has no sqlite3 as far as the code under + // test is concerned -- including any sqlite3 the RUNNER ships. + env: db ? {} : { PATH: pathWithoutSqlite() }, }; } @@ -382,7 +474,14 @@ export function rebeccaHost(base, { running = true, sqlite = true, url, customDi const bin = shims(base, { sqlite }); return { app, dataDir, cliPath, docker, bin, db, envFile: join(app, '.env'), - paths: { RT_RB_APP_DIR: app, RT_RB_DATA_DIR: dataDir, RT_RB_CLI: cliPath, RT_TEST_DOCKER: docker, RT_TEST_BIN: bin }, + paths: { + RT_RB_APP_DIR: app, RT_RB_DATA_DIR: dataDir, RT_RB_CLI: cliPath, + RT_TEST_DOCKER: docker, RT_TEST_BIN: bin, RT_TEST_NET_LOG: join(base, 'net.log'), + }, + // `sqlite: false` must make sqlite3 UNAVAILABLE, not merely broken: the + // adapters gate on `command -v sqlite3`, so the runner's own sqlite3 has to + // be off PATH, not shadowed by a shim that exists and fails. + env: sqlite ? {} : { PATH: pathWithoutSqlite() }, }; } diff --git a/tests/installer-panel-pasarguard.test.mjs b/tests/installer-panel-pasarguard.test.mjs index 1a76fe3..156b37a 100644 --- a/tests/installer-panel-pasarguard.test.mjs +++ b/tests/installer-panel-pasarguard.test.mjs @@ -23,11 +23,18 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { - bashRun, makePayload, pasarguardHost, HOST_PREAMBLE, sha256, PG_ENV, + bashRun, makePayload, pasarguardHost, HOST_PREAMBLE, sha256, PG_ENV, netAttempts, } from './helpers/panel-hosts.mjs'; - +import { availableTemplateIds } from '../tools/templates.mjs'; + +/* A COMPLETE payload -- every design the release registry names, not a sample. + A payload short of them makes `rt_complete_install` fetch the release pinned + to the INSTALLED version, and on a runner with network that pulled the + PUBLISHED release into the store, which was then compared against this + working tree's artifact. The host denies the network and records any attempt + (tests/helpers/panel-hosts.mjs); the life-cycle test asserts it never tried. */ const PAYLOAD_DIR = mkdtempSync(join(tmpdir(), 'row-pg-payload-')); -const PAYLOAD = makePayload(PAYLOAD_DIR, { ids: ['row', 'editorial'] }); +const PAYLOAD = makePayload(PAYLOAD_DIR, { ids: availableTemplateIds() }); process.on('exit', () => rmSync(PAYLOAD_DIR, { recursive: true, force: true })); /* A Row-Template install on a PasarGuard host, up to a generated page -- the @@ -46,8 +53,9 @@ function withHost(opts, fn) { try { const host = pasarguardHost(base, opts); const rt = join(base, 'rt'); - const run = (lines) => bashRun([HOST_PREAMBLE, ...[].concat(lines)], - { paths: { ...host.paths, RT_ROOT: rt, RT_BIN: join(base, 'row-template'), PAYLOAD } }); + const run = (lines, env = {}) => bashRun([HOST_PREAMBLE, ...[].concat(lines)], + { paths: { ...host.paths, RT_ROOT: rt, RT_BIN: join(base, 'row-template'), PAYLOAD }, + env: { ...host.env, ...env } }); return fn({ base, host, rt, run }); } finally { rmSync(base, { recursive: true, force: true }); @@ -404,5 +412,11 @@ test('install, verify, rebrand, switch design, roll back and uninstall on a Pasa assert.equal(existsSync(rt), false, 'the install root is gone'); assert.equal(existsSync(join(base, 'row-template')), false, 'and the CLI'); assert.equal(existsSync(host.cliPath), true, 'PasarGuard itself is untouched'); + + /* The whole life cycle ran offline. curl and wget are denied in this host + and every attempt is recorded, so this is a measurement, not a promise: + the payload is complete, rt_complete_install never fires, and nothing the + suite does may depend on a published release. */ + assert.equal(netAttempts(base), '', 'the life cycle must not touch the network'); }); }); diff --git a/tests/installer-panel-rebecca.test.mjs b/tests/installer-panel-rebecca.test.mjs index 65deb52..c71758b 100644 --- a/tests/installer-panel-rebecca.test.mjs +++ b/tests/installer-panel-rebecca.test.mjs @@ -19,16 +19,23 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; +import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { delimiter, join } from 'node:path'; import { - bashRun, makePayload, rebeccaHost, rebeccaRow, HOST_PREAMBLE, posix, + bashRun, makePayload, rebeccaHost, rebeccaRow, HOST_PREAMBLE, posix, netAttempts, pathWithoutSqlite, } from './helpers/panel-hosts.mjs'; - +import { availableTemplateIds } from '../tools/templates.mjs'; + +/* A COMPLETE payload -- every design the release registry names, not a sample. + A payload short of them makes `rt_complete_install` fetch the release pinned + to the INSTALLED version, and on a runner with network that pulled the + PUBLISHED release into the store, which was then compared against this + working tree's artifact. The host denies the network and records any attempt + (tests/helpers/panel-hosts.mjs); the life-cycle test asserts it never tried. */ const PAYLOAD_DIR = mkdtempSync(join(tmpdir(), 'row-rb-payload-')); -const PAYLOAD = makePayload(PAYLOAD_DIR, { ids: ['row', 'editorial'] }); +const PAYLOAD = makePayload(PAYLOAD_DIR, { ids: availableTemplateIds() }); process.on('exit', () => rmSync(PAYLOAD_DIR, { recursive: true, force: true })); const SETUP = [ @@ -46,7 +53,8 @@ function withHost(opts, fn) { const host = rebeccaHost(base, opts); const rt = join(base, 'rt'); const run = (lines, env = {}) => bashRun([HOST_PREAMBLE, ...[].concat(lines)], - { paths: { ...host.paths, RT_ROOT: rt, RT_BIN: join(base, 'row-template'), PAYLOAD }, env }); + { paths: { ...host.paths, RT_ROOT: rt, RT_BIN: join(base, 'row-template'), PAYLOAD }, + env: { ...host.env, ...env } }); return fn({ base, host, rt, run }); } finally { rmSync(base, { recursive: true, force: true }); @@ -422,6 +430,47 @@ test('a failed panel refresh leaves sub.html and the placed page exactly as they /* --- manual activation --------------------------------------------------------- */ +/* The runner's own sqlite3 must not leak into `sqlite: false`. The adapters + gate on `command -v sqlite3`, so a sqlite3 that merely fails would still open + the automatic path -- and that is exactly what happened: ubuntu-latest ships + a real sqlite3, so activation reported "auto" on the runner while this + authoring host, which has none, reported "manual". */ +test('sqlite: false takes every sqlite3 off PATH, a real one included', () => { + const planted = mkdtempSync(join(tmpdir(), 'row-fake-sqlite-')); + try { + /* A stand-in for a runner that ships sqlite3, plus a second tool so the + substituted directory still has to carry everything else it held. */ + for (const name of ['sqlite3', 'rowmarker']) { + writeFileSync(join(planted, name), `#!/bin/sh\necho "${name} on the host"\n`); + chmodSync(join(planted, name), 0o755); + } + const scrubbed = pathWithoutSqlite([planted, process.env.PATH].join(delimiter)); + assert.equal(scrubbed.split(delimiter).includes(planted), false, + 'a directory holding a sqlite3 is not on the child PATH'); + + /* `rc=0; … || rc=$?` rather than `… ; echo $?`, because bashRun runs under + `set -e` and a bare failing command would end the script before the echo. */ + const seen = bashRun([ + 's=0; command -v sqlite3 >/dev/null 2>&1 || s=$?; echo "sqlite3=$s"', + 'm=0; command -v rowmarker >/dev/null 2>&1 || m=$?; echo "rowmarker=$m"', + ].join('\n'), { env: { PATH: scrubbed } }); + assert.match(seen.out, /sqlite3=1/, 'command -v sqlite3 fails -- the gate the adapters ask'); + assert.match(seen.out, /rowmarker=0/, 'and the rest of the host still resolves'); + + // And through the host the suite actually builds. + withHost({ sqlite: false }, ({ run }) => { + const g = run([ + 's=0; command -v sqlite3 >/dev/null 2>&1 || s=$?; echo "found=$s"', + 'rc=0; rt_panel_rebecca_db_ready || rc=$?; echo "db_ready=$rc"', + ].join('\n')); + assert.match(g.out, /found=1/, 'the fake host offers no sqlite3 at all'); + assert.match(g.out, /db_ready=1/, 'so the panel selection cannot be read here'); + }); + } finally { + rmSync(planted, { recursive: true, force: true }); + } +}); + test('without sqlite3, activation places the page and says exactly what to set', () => { withHost({ sqlite: false }, ({ host, run }) => { const before = last(host); @@ -473,5 +522,11 @@ test('install, verify, rebrand, switch design, roll back and uninstall on a Rebe assert.equal(existsSync(rt), false); assert.equal(existsSync(join(base, 'row-template')), false); assert.ok(readdirSync(host.dataDir).includes('db.sqlite3'), 'the database is still there'); + + /* The whole life cycle ran offline. curl and wget are denied in this host + and every attempt is recorded, so this is a measurement, not a promise: + the payload is complete, rt_complete_install never fires, and nothing the + suite does may depend on a published release. */ + assert.equal(netAttempts(base), '', 'the life cycle must not touch the network'); }); }); From 0b960129c9ae73efc1f5d263a7a1d347c929c74f Mon Sep 17 00:00:00 2001 From: iitzSeriZ Date: Sun, 27 Sep 2026 22:46:05 +0330 Subject: [PATCH 2/2] test(panels): pass bashRun an array, and stamp the sqlite3 mirrors Two defects in the previous commit, both in test code. Found by the Linux run: three failures down to one, and the one was this commit's own new test. bashRun spreads its first argument, so a string became one line per character: "set -Eeuo pipefail\ns\n=\n0\n;\n \ne\nc\nh\no\n \nA" The new sqlite3 test passed a joined string, so its script died under `set -e` -- the first one-character line is a command that does not exist -- before printing anything, and the assertion read an empty stdout (`actual: ''`). Every other caller passes an array. bashRun now accepts either shape, as the suites' own `run` helpers already do, so the footgun cannot recur; the test passes an array, and reports the exit code and stderr when it fails. A mirror is now stamped once it is complete. A run killed by a signal (a timeout, a cancelled job) never reaches the exit hook, and a half-built mirror would otherwise be reused on the next run -- failing, if `bash` were the entry that never got linked, in a way that has nothing to do with sqlite3. --- tests/helpers/panel-hosts.mjs | 23 +++++++++++++++++++---- tests/installer-panel-rebecca.test.mjs | 5 +++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/tests/helpers/panel-hosts.mjs b/tests/helpers/panel-hosts.mjs index 579369a..d585599 100644 --- a/tests/helpers/panel-hosts.mjs +++ b/tests/helpers/panel-hosts.mjs @@ -64,9 +64,14 @@ export const PYTHON = workingProgram(process.platform === 'win32' ? ['python', ' export function bashRun(lines, { paths = {}, env = {} } = {}) { const head = Object.entries(paths).map(([k, v]) => `${k}="$(cygpath -u ${sq(v)} 2>/dev/null || printf '%s' ${sq(v)})"; export ${k}`); + // A bare string is ONE line, not one line per character: `...lines` on a + // string spreads its characters, which turns the script into a column of + // one-character commands that dies under `set -e` before printing anything. + // Accept either shape, the way the suites' own `run` helpers do. + const body = [].concat(lines); const script = ['set -Eeuo pipefail', 'unset RT_TEMPLATE RT_RELEASE_URL RT_RELEASE_DIR RT_ASSUME_YES RT_PANEL RT_SMOKE_URL XUI_DB_FOLDER', - ...head, ...lines].join('\n'); + ...head, ...body].join('\n'); const r = spawnSync('bash', ['-c', script], { cwd: ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, env: { ...process.env, ...env }, }); @@ -251,16 +256,26 @@ exit 1 /* A PATH that is the one given, minus every directory that holds a sqlite3 executable. Those directories are replaced by a mirror of themselves -- the same entries, symlinked, without sqlite3 -- so nothing else on the host stops - resolving. Cached per input, because mirroring /usr/bin is ~1200 links. */ + resolving. Cached per input, because mirroring /usr/bin is ~1500 links. + + A mirror is only trusted once it is stamped. A run killed by a signal (a + timeout, a cancelled job) never reaches the exit hook below, and a half-built + mirror would otherwise be reused on the next run -- and, if `bash` happened to + be the entry that never got linked, fail in a way that has nothing to do with + sqlite3. */ const mirrors = new Map(); function mirrorWithoutSqlite(dir) { const dest = join(tmpdir(), `row-nosqlite-${sha256(dir).slice(0, 12)}`); - if (!existsSync(dest)) { + const stamp = join(dest, '.mirror-complete'); + if (!existsSync(stamp)) { + rmSync(dest, { recursive: true, force: true }); mkdirSync(dest, { recursive: true }); + let n = 0; for (const entry of readdirSync(dir)) { if (entry === 'sqlite3') continue; - try { symlinkSync(join(dir, entry), join(dest, entry)); } catch { /* unreadable entry: skip it */ } + try { symlinkSync(join(dir, entry), join(dest, entry)); n += 1; } catch { /* unreadable entry: skip it */ } } + writeFileSync(stamp, `${n}\n`); process.on('exit', () => rmSync(dest, { recursive: true, force: true })); } return dest; diff --git a/tests/installer-panel-rebecca.test.mjs b/tests/installer-panel-rebecca.test.mjs index c71758b..0568de2 100644 --- a/tests/installer-panel-rebecca.test.mjs +++ b/tests/installer-panel-rebecca.test.mjs @@ -453,8 +453,9 @@ test('sqlite: false takes every sqlite3 off PATH, a real one included', () => { const seen = bashRun([ 's=0; command -v sqlite3 >/dev/null 2>&1 || s=$?; echo "sqlite3=$s"', 'm=0; command -v rowmarker >/dev/null 2>&1 || m=$?; echo "rowmarker=$m"', - ].join('\n'), { env: { PATH: scrubbed } }); - assert.match(seen.out, /sqlite3=1/, 'command -v sqlite3 fails -- the gate the adapters ask'); + ], { env: { PATH: scrubbed } }); + assert.match(seen.out, /sqlite3=1/, + `command -v sqlite3 fails -- the gate the adapters ask\n${seen.code}\n${seen.err}`); assert.match(seen.out, /rowmarker=0/, 'and the rest of the host still resolves'); // And through the host the suite actually builds.