@reticlehq/test 2.13.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/junit.js +30 -6
- package/dist/matchers.d.ts +26 -1
- package/dist/matchers.js +29 -2
- package/dist/test-context.js +9 -2
- package/package.json +3 -3
package/dist/junit.js
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { DEFAULT_JUNIT_SUITE_NAME, JUnit, TestStatus } from './constants.js';
|
|
2
2
|
import { summarize } from './summary.js';
|
|
3
3
|
/**
|
|
4
|
-
* XML 1.0 section 2.2
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* XML 1.0 section 2.2, the whole `Char` production:
|
|
5
|
+
*
|
|
6
|
+
* #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
|
|
7
|
+
*
|
|
8
|
+
* Stated as codes rather than a regex character class, which cannot express control characters
|
|
9
|
+
* without tripping `no-control-regex` and needing the rule turned off to read it.
|
|
7
10
|
*
|
|
8
11
|
* Iterating code points also means an astral character (an emoji in a test name) survives intact;
|
|
9
12
|
* the regex worked on UTF-16 units.
|
|
13
|
+
*
|
|
14
|
+
* Only the LOWER bound used to be enforced, which left the production's two upper holes open:
|
|
15
|
+
* the surrogate block, and the noncharacters U+FFFE and U+FFFF. A test name or an assertion message
|
|
16
|
+
* carrying U+FFFF produced a document a real parser rejects outright ("disallowed character"), which
|
|
17
|
+
* is the same outcome the strip exists to prevent: CI shows nothing instead of showing the failure.
|
|
18
|
+
* A lone surrogate is the milder case, since writing the file as UTF-8 turns it into U+FFFD, but it
|
|
19
|
+
* is illegal by the same clause and dropping it keeps the rule one thing rather than two.
|
|
10
20
|
*/
|
|
11
21
|
const XML_MIN_LEGAL_CODE = 0x20;
|
|
12
22
|
const XML_LEGAL_CONTROL_CODES = new Set([
|
|
@@ -14,12 +24,26 @@ const XML_LEGAL_CONTROL_CODES = new Set([
|
|
|
14
24
|
0x0a, // newline
|
|
15
25
|
0x0d, // carriage return
|
|
16
26
|
]);
|
|
17
|
-
/**
|
|
27
|
+
/** The surrogate block: legal only as a PAIR, which iteration has already resolved to one code point. */
|
|
28
|
+
const XML_SURROGATE_FIRST = 0xd800;
|
|
29
|
+
const XML_SURROGATE_LAST = 0xdfff;
|
|
30
|
+
/** U+FFFE and U+FFFF are noncharacters; the production stops the BMP at U+FFFD. */
|
|
31
|
+
const XML_BMP_LAST_LEGAL = 0xfffd;
|
|
32
|
+
const XML_ASTRAL_FIRST = 0x10000;
|
|
33
|
+
function isXmlLegal(code) {
|
|
34
|
+
if (XML_LEGAL_CONTROL_CODES.has(code))
|
|
35
|
+
return true;
|
|
36
|
+
if (code < XML_MIN_LEGAL_CODE)
|
|
37
|
+
return false;
|
|
38
|
+
if (code >= XML_SURROGATE_FIRST && code <= XML_SURROGATE_LAST)
|
|
39
|
+
return false;
|
|
40
|
+
return code <= XML_BMP_LAST_LEGAL || code >= XML_ASTRAL_FIRST;
|
|
41
|
+
}
|
|
42
|
+
/** Drop the characters no XML parser will accept. */
|
|
18
43
|
function stripXmlIllegal(value) {
|
|
19
44
|
let out = '';
|
|
20
45
|
for (const ch of value) {
|
|
21
|
-
|
|
22
|
-
if (code >= XML_MIN_LEGAL_CODE || XML_LEGAL_CONTROL_CODES.has(code))
|
|
46
|
+
if (isXmlLegal(ch.codePointAt(0) ?? 0))
|
|
23
47
|
out += ch;
|
|
24
48
|
}
|
|
25
49
|
return out;
|
package/dist/matchers.d.ts
CHANGED
|
@@ -1,11 +1,36 @@
|
|
|
1
1
|
import { type ToolInvoker } from '@reticlehq/server';
|
|
2
|
-
import type
|
|
2
|
+
import { type ElementQuery, type ElementState } from '@reticlehq/core';
|
|
3
3
|
/** The verdict envelope returned by reticle_assert / the `verdict` field of reticle_act_and_wait. */
|
|
4
4
|
export interface Verdict {
|
|
5
|
+
/** The RAW predicate match, before the honesty layer has had its say. */
|
|
5
6
|
pass: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* `"yes" | "no" | "unknown"`, when the daemon reported one. `act_and_wait`'s schema calls this
|
|
9
|
+
* "THE field to gate on", and it is allowed to disagree with `pass`: a UI that rendered success
|
|
10
|
+
* over a POST that returned 500 comes back `pass: true, verified: "no"`, which is the flagship
|
|
11
|
+
* verdict of the product rather than an edge case.
|
|
12
|
+
*/
|
|
13
|
+
verified?: string;
|
|
14
|
+
/** One sentence naming the deciding evidence behind `verified`. */
|
|
15
|
+
because?: string;
|
|
6
16
|
evidence?: unknown;
|
|
7
17
|
failureReason?: string;
|
|
8
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Did this verdict PROVE the thing the spec asserted?
|
|
21
|
+
*
|
|
22
|
+
* Only `verified: "no"` overrides `pass`. That is the case where the daemon looked at the same
|
|
23
|
+
* evidence and overturned a raw match, and reading `pass` alone made the one verdict this product
|
|
24
|
+
* exists to produce, a contradicted green, the one a spec reported as passing.
|
|
25
|
+
*
|
|
26
|
+
* `unknown` deliberately still follows `pass`. The tool's own schema says "unknown is NOT failure,
|
|
27
|
+
* it means the evidence could not decide, which calls for a better check rather than a code
|
|
28
|
+
* change", and whether a CI spec should go red, green or skipped on that is a product decision
|
|
29
|
+
* rather than something to settle inside a false-green fix.
|
|
30
|
+
*
|
|
31
|
+
* A daemon too old to send the field falls through to `pass`, unchanged.
|
|
32
|
+
*/
|
|
33
|
+
export declare function proved(verdict: Verdict): boolean;
|
|
9
34
|
/**
|
|
10
35
|
* Throw an ReticleAssertionError carrying the verdict's own evidence + failureReason. Used by every
|
|
11
36
|
* `expect*` matcher and by actAndWait, so the runner's single catch boundary marks fail with the
|
package/dist/matchers.js
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { ReticleTool } from '@reticlehq/server';
|
|
2
|
+
import { Verified } from '@reticlehq/core';
|
|
2
3
|
import { ReticleAssertionError } from './skip.js';
|
|
3
4
|
import { CONSOLE_LEVEL_ERROR, DEFAULT_ASSERT_TIMEOUT_MS, PredicateKind } from './constants.js';
|
|
5
|
+
/**
|
|
6
|
+
* Did this verdict PROVE the thing the spec asserted?
|
|
7
|
+
*
|
|
8
|
+
* Only `verified: "no"` overrides `pass`. That is the case where the daemon looked at the same
|
|
9
|
+
* evidence and overturned a raw match, and reading `pass` alone made the one verdict this product
|
|
10
|
+
* exists to produce, a contradicted green, the one a spec reported as passing.
|
|
11
|
+
*
|
|
12
|
+
* `unknown` deliberately still follows `pass`. The tool's own schema says "unknown is NOT failure,
|
|
13
|
+
* it means the evidence could not decide, which calls for a better check rather than a code
|
|
14
|
+
* change", and whether a CI spec should go red, green or skipped on that is a product decision
|
|
15
|
+
* rather than something to settle inside a false-green fix.
|
|
16
|
+
*
|
|
17
|
+
* A daemon too old to send the field falls through to `pass`, unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export function proved(verdict) {
|
|
20
|
+
if (Verified.NO === verdict.verified)
|
|
21
|
+
return false;
|
|
22
|
+
return verdict.pass;
|
|
23
|
+
}
|
|
4
24
|
function asVerdict(value) {
|
|
5
25
|
if (typeof value !== 'object' || null === value) {
|
|
6
26
|
return { pass: false, failureReason: 'assert returned a non-object result' };
|
|
@@ -8,9 +28,13 @@ function asVerdict(value) {
|
|
|
8
28
|
const record = value;
|
|
9
29
|
const pass = true === record['pass'];
|
|
10
30
|
const failureReason = 'string' === typeof record['failureReason'] ? record['failureReason'] : undefined;
|
|
31
|
+
const verified = 'string' === typeof record['verified'] ? record['verified'] : undefined;
|
|
32
|
+
const because = 'string' === typeof record['because'] ? record['because'] : undefined;
|
|
11
33
|
return {
|
|
12
34
|
pass,
|
|
13
35
|
evidence: record['evidence'],
|
|
36
|
+
...(verified !== undefined ? { verified } : {}),
|
|
37
|
+
...(because !== undefined ? { because } : {}),
|
|
14
38
|
...(failureReason !== undefined ? { failureReason } : {}),
|
|
15
39
|
};
|
|
16
40
|
}
|
|
@@ -20,7 +44,10 @@ function asVerdict(value) {
|
|
|
20
44
|
* predicate engine's structured diagnosis intact. `extraEvidence` lets actAndWait attach its trace.
|
|
21
45
|
*/
|
|
22
46
|
export function failFromVerdict(verdict, extraEvidence) {
|
|
23
|
-
|
|
47
|
+
// `because` before the generic fallback: an overturned verdict passed its own predicate, so it has
|
|
48
|
+
// no `failureReason` to offer, and "assertion failed" would hide the contradiction that is the
|
|
49
|
+
// entire finding.
|
|
50
|
+
const message = verdict.failureReason ?? verdict.because ?? 'assertion failed';
|
|
24
51
|
// actAndWait passes extra context (the reaction trace); merge it alongside the verdict evidence.
|
|
25
52
|
const evidence = extraEvidence !== undefined
|
|
26
53
|
? {
|
|
@@ -41,7 +68,7 @@ async function assertPredicate(invoke, predicate, timeoutMs, sessionId) {
|
|
|
41
68
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
42
69
|
};
|
|
43
70
|
const verdict = asVerdict(await invoke(ReticleTool.ASSERT, args));
|
|
44
|
-
if (!verdict
|
|
71
|
+
if (!proved(verdict))
|
|
45
72
|
failFromVerdict(verdict);
|
|
46
73
|
}
|
|
47
74
|
function timeout(deps) {
|
package/dist/test-context.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ActionType } from '@reticlehq/core';
|
|
|
3
3
|
import { resolveTestid } from './resolve.js';
|
|
4
4
|
import { buildClock } from './clock.js';
|
|
5
5
|
import { InputModeTracker, expectInputModeReal } from './input-mode.js';
|
|
6
|
-
import { failFromVerdict } from './matchers.js';
|
|
6
|
+
import { failFromVerdict, proved } from './matchers.js';
|
|
7
7
|
import { expectAbsent, expectElement, expectNet, expectNoConsoleErrors, expectSignal, expectText, } from './matchers.js';
|
|
8
8
|
import { DEFAULT_ASSERT_TIMEOUT_MS } from './constants.js';
|
|
9
9
|
function asVerdict(value) {
|
|
@@ -14,9 +14,16 @@ function asVerdict(value) {
|
|
|
14
14
|
}
|
|
15
15
|
const v = verdict;
|
|
16
16
|
const failureReason = 'string' === typeof v['failureReason'] ? v['failureReason'] : undefined;
|
|
17
|
+
// `verified` and `because` sit at the TOP level of the act_and_wait result, beside `verdict`
|
|
18
|
+
// rather than inside it: the nested object carries the raw predicate match and nothing else. That
|
|
19
|
+
// split is why reading `verdict.pass` alone could not see an overturn.
|
|
20
|
+
const verified = 'string' === typeof record['verified'] ? record['verified'] : undefined;
|
|
21
|
+
const because = 'string' === typeof record['because'] ? record['because'] : undefined;
|
|
17
22
|
return {
|
|
18
23
|
pass: true === v['pass'],
|
|
19
24
|
evidence: v['evidence'],
|
|
25
|
+
...(verified !== undefined ? { verified } : {}),
|
|
26
|
+
...(because !== undefined ? { because } : {}),
|
|
20
27
|
...(failureReason !== undefined ? { failureReason } : {}),
|
|
21
28
|
};
|
|
22
29
|
}
|
|
@@ -60,7 +67,7 @@ export function createTestContext(invoke, options = {}) {
|
|
|
60
67
|
};
|
|
61
68
|
const raw = await invoke(ReticleTool.ACT_AND_WAIT, waitArgs);
|
|
62
69
|
const verdict = asVerdict(raw);
|
|
63
|
-
if (!verdict
|
|
70
|
+
if (!proved(verdict)) {
|
|
64
71
|
const trace = raw['trace'];
|
|
65
72
|
failFromVerdict(verdict, trace !== undefined ? { trace } : undefined);
|
|
66
73
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/test",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0",
|
|
4
4
|
"description": "Reticle test runner: reticleTest spec registry + a runner that drives Reticle tools without MCP/stdio.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@reticlehq/core": "2.
|
|
16
|
-
"@reticlehq/server": "2.
|
|
15
|
+
"@reticlehq/core": "2.14.0",
|
|
16
|
+
"@reticlehq/server": "2.14.0"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"vitest": "^3.2.6"
|