Recorded while implementing #6247 (PR #7255 — seeding packages/spec/liveness/qa.json and enforcing TestSuiteSchema at the os test load site). Not claiming or fixing here — filing per Prime Directive #10, and deliberately kept out of that PR's scope: the ruling there covers the ledger and the load site, and this is a behaviour change inside the assertion engine.
Finding. TestRunner.assert (packages/core/src/qa/runner.ts:171-177) handles contains like this:
case 'contains':
if (Array.isArray(actual)) {
if (!actual.includes(expected)) throw new Error(…);
} else if (typeof actual === 'string') {
if (!actual.includes(String(expected))) throw new Error(…);
}
break;
There is no else. When actual is anything else — undefined (the overwhelmingly common case: a typo'd field path, or a response shape that changed), null, a number, or an object — the branch falls through, throws nothing, and the assertion passes.
So a scenario asserting { field: "body.data.items", operator: "contains", expectedValue: "acme" } against a response that has no body.data.items at all reports ✅. The assertion that was supposed to be the test is the thing that silently disappears.
Why this one matters more than a normal bug. Every other unhandled shape in this engine fails LOUD, and that asymmetry is what makes this the odd one out:
- an action type with no adapter branch (
run_script) throws Unsupported action type in HttpAdapter (http-adapter.ts:38);
- an operator with no branch (
not_contains / gt / gte / lt / lte / error) throws Unknown assertion operator (runner.ts:186) — declared in TestAssertionTypeSchema, refused at runtime, which is annoying but honest;
is_null / not_null / equals / not_equals all compare unconditionally.
contains is the only path that can decide "no comparison applies here" and report success. A test framework whose failure mode is a false green is worse than one that is missing the feature — the whole point of the surface is to be believed by CI.
This is also the reason it is worth a separate card rather than a note: #6247's ledger records scenarios.setup's sub-keys with assertions → field / operator / expectedValue all live, and they are. The KEY is live; it is one VALUE of operator that is unsound. That distinction is real (the api.json type precedent) but it means no liveness verdict will ever surface this.
Options.
- Throw on the unhandled shape —
contains against a non-array/non-string actual is an assertion that cannot be evaluated, so fail it with a message naming the field, the operator and the actual type. Matches the default: branch's posture. Fails loudly on suites that are currently (falsely) green, which is the point, and is the only option that closes the false-green.
- Coerce and compare —
String(actual).includes(String(expected)). Makes contains total, but silently redefines it: undefined would then "contain" the substring "defi", which trades one false green for a weirder one. Not recommended.
- Leave it and document — weakest; a documented false green is still a false green.
Dedup. No open issue or PR in the three repos names the contains assertion or TestRunner.assert. Searched before filing.
Recorded while implementing #6247 (PR #7255 — seeding
packages/spec/liveness/qa.jsonand enforcingTestSuiteSchemaat theos testload site). Not claiming or fixing here — filing per Prime Directive #10, and deliberately kept out of that PR's scope: the ruling there covers the ledger and the load site, and this is a behaviour change inside the assertion engine.Finding.
TestRunner.assert(packages/core/src/qa/runner.ts:171-177) handlescontainslike this:There is no
else. Whenactualis anything else —undefined(the overwhelmingly common case: a typo'dfieldpath, or a response shape that changed),null, a number, or an object — the branch falls through, throws nothing, and the assertion passes.So a scenario asserting
{ field: "body.data.items", operator: "contains", expectedValue: "acme" }against a response that has nobody.data.itemsat all reports ✅. The assertion that was supposed to be the test is the thing that silently disappears.Why this one matters more than a normal bug. Every other unhandled shape in this engine fails LOUD, and that asymmetry is what makes this the odd one out:
run_script) throwsUnsupported action type in HttpAdapter(http-adapter.ts:38);not_contains/gt/gte/lt/lte/error) throwsUnknown assertion operator(runner.ts:186) — declared inTestAssertionTypeSchema, refused at runtime, which is annoying but honest;is_null/not_null/equals/not_equalsall compare unconditionally.containsis the only path that can decide "no comparison applies here" and report success. A test framework whose failure mode is a false green is worse than one that is missing the feature — the whole point of the surface is to be believed by CI.This is also the reason it is worth a separate card rather than a note: #6247's ledger records
scenarios.setup's sub-keys withassertions→field/operator/expectedValuealllive, and they are. The KEY is live; it is one VALUE ofoperatorthat is unsound. That distinction is real (theapi.jsontypeprecedent) but it means no liveness verdict will ever surface this.Options.
containsagainst a non-array/non-string actual is an assertion that cannot be evaluated, so fail it with a message naming the field, the operator and the actual type. Matches thedefault:branch's posture. Fails loudly on suites that are currently (falsely) green, which is the point, and is the only option that closes the false-green.String(actual).includes(String(expected)). Makescontainstotal, but silently redefines it:undefinedwould then "contain" the substring"defi", which trades one false green for a weirder one. Not recommended.Dedup. No open issue or PR in the three repos names the
containsassertion orTestRunner.assert. Searched before filing.