@reticlehq/test 2.4.0 → 2.5.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/flow-spec.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/input-mode.js +3 -3
- package/dist/junit.js +24 -7
- package/dist/matchers.js +3 -3
- package/dist/register.d.ts +0 -2
- package/dist/register.js +1 -3
- package/dist/resolve.js +3 -3
- package/dist/summary.js +6 -1
- package/dist/test-context.js +4 -4
- package/dist/types.d.ts +7 -0
- package/package.json +5 -4
package/dist/flow-spec.js
CHANGED
|
@@ -60,7 +60,7 @@ function errorSpec(name, code) {
|
|
|
60
60
|
* ERROR spec; enumeration of its siblings is never aborted.
|
|
61
61
|
*/
|
|
62
62
|
export async function flowsAsSpecs(source, opts) {
|
|
63
|
-
const store = typeof source
|
|
63
|
+
const store = 'string' === typeof source
|
|
64
64
|
? new FlowStore(opts?.fs ?? createNodeFileSystem(), source, opts?.clock ?? { now: () => 0 })
|
|
65
65
|
: source;
|
|
66
66
|
const names = await store.list();
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,6 @@ export type { SpecContext, SpecFn, ReticleSpec, SpecResult, RunSummary, ContextF
|
|
|
17
17
|
export { flowToSpec, flowsAsSpecs } from './flow-spec.js';
|
|
18
18
|
export type { FlowSpec, FlowSpecOptions, FlowsAsSpecsOptions, SpecRunResult } from './flow-spec.js';
|
|
19
19
|
export { assertSuccess, successToPredicate } from './success-assert.js';
|
|
20
|
-
export { registerFlowSpecs,
|
|
20
|
+
export { registerFlowSpecs, FlowMalformedError, SpecFailure } from './register.js';
|
|
21
21
|
export type { RegisterFn, RegisterFlowSpecsOptions } from './register.js';
|
|
22
22
|
export { SpecKind, SpecOutcome, SpecMessage, FLOW_LOAD_ERROR_PREFIX } from './constants.js';
|
package/dist/index.js
CHANGED
|
@@ -12,5 +12,5 @@ export { TestStatus, STATUS_GLYPH, SUMMARY_FOOTER_PREFIX, JUnit, DEFAULT_JUNIT_S
|
|
|
12
12
|
// flows under .reticle/flows become the runnable suite.
|
|
13
13
|
export { flowToSpec, flowsAsSpecs } from './flow-spec.js';
|
|
14
14
|
export { assertSuccess, successToPredicate } from './success-assert.js';
|
|
15
|
-
export { registerFlowSpecs,
|
|
15
|
+
export { registerFlowSpecs, FlowMalformedError, SpecFailure } from './register.js';
|
|
16
16
|
export { SpecKind, SpecOutcome, SpecMessage, FLOW_LOAD_ERROR_PREFIX } from './constants.js';
|
package/dist/input-mode.js
CHANGED
|
@@ -3,11 +3,11 @@ import { InputMode } from '@reticlehq/core';
|
|
|
3
3
|
import { ReticleSkip } from './skip.js';
|
|
4
4
|
import { SKIP_REASON_REAL_INPUT } from './constants.js';
|
|
5
5
|
function isRecord(value) {
|
|
6
|
-
return typeof value
|
|
6
|
+
return 'object' === typeof value && value !== null;
|
|
7
7
|
}
|
|
8
8
|
/** The act envelope's inputMode field, narrowed from the raw tool result. */
|
|
9
9
|
export function readInputMode(actResult) {
|
|
10
|
-
if (typeof actResult !== 'object' ||
|
|
10
|
+
if (typeof actResult !== 'object' || null === actResult)
|
|
11
11
|
return undefined;
|
|
12
12
|
const mode = actResult['inputMode'];
|
|
13
13
|
if (mode === InputMode.REAL || mode === InputMode.SYNTHETIC)
|
|
@@ -56,7 +56,7 @@ async function probeInputMode(invoke, sessionId) {
|
|
|
56
56
|
if (!isRecord(session))
|
|
57
57
|
continue;
|
|
58
58
|
if (sessionId === undefined || session['sessionId'] === sessionId) {
|
|
59
|
-
return session['realInputAvailable']
|
|
59
|
+
return true === session['realInputAvailable'] ? InputMode.REAL : InputMode.SYNTHETIC;
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
return InputMode.SYNTHETIC;
|
package/dist/junit.js
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
import { DEFAULT_JUNIT_SUITE_NAME, JUnit, TestStatus } from './constants.js';
|
|
2
2
|
import { summarize } from './summary.js';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* XML 1.0 section 2.2: every code point below U+0020 is illegal EXCEPT tab, newline and carriage
|
|
5
|
+
* return. Stated as codes rather than a regex character class, which cannot express control
|
|
6
|
+
* characters without tripping `no-control-regex` and needing the rule turned off to read it.
|
|
7
|
+
*
|
|
8
|
+
* Iterating code points also means an astral character (an emoji in a test name) survives intact;
|
|
9
|
+
* the regex worked on UTF-16 units.
|
|
6
10
|
*/
|
|
7
|
-
|
|
8
|
-
const
|
|
11
|
+
const XML_MIN_LEGAL_CODE = 0x20;
|
|
12
|
+
const XML_LEGAL_CONTROL_CODES = new Set([
|
|
13
|
+
0x09, // tab
|
|
14
|
+
0x0a, // newline
|
|
15
|
+
0x0d, // carriage return
|
|
16
|
+
]);
|
|
17
|
+
/** Drop the control characters no XML parser will accept. */
|
|
18
|
+
function stripXmlIllegal(value) {
|
|
19
|
+
let out = '';
|
|
20
|
+
for (const ch of value) {
|
|
21
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
22
|
+
if (code >= XML_MIN_LEGAL_CODE || XML_LEGAL_CONTROL_CODES.has(code))
|
|
23
|
+
out += ch;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
9
27
|
/**
|
|
10
28
|
* Strip characters illegal in XML 1.0 then escape the five XML-significant characters. Without the
|
|
11
29
|
* strip, ANSI colour codes in error output produce a document no parser will read — CI shows
|
|
12
30
|
* nothing instead of showing the failure.
|
|
13
31
|
*/
|
|
14
32
|
function escapeXml(value) {
|
|
15
|
-
return value
|
|
16
|
-
.replace(XML_ILLEGAL_CHARS, '')
|
|
33
|
+
return stripXmlIllegal(value)
|
|
17
34
|
.replace(/&/g, '&')
|
|
18
35
|
.replace(/</g, '<')
|
|
19
36
|
.replace(/>/g, '>')
|
|
@@ -23,7 +40,7 @@ function escapeXml(value) {
|
|
|
23
40
|
/** First line of text (for the attribute summary). Handles LF, CRLF, and bare CR. */
|
|
24
41
|
function firstLine(text) {
|
|
25
42
|
const match = /\r?\n|\r/.exec(text);
|
|
26
|
-
return
|
|
43
|
+
return null === match ? text : text.slice(0, match.index);
|
|
27
44
|
}
|
|
28
45
|
function seconds(ms) {
|
|
29
46
|
return (ms / 1000).toFixed(3);
|
package/dist/matchers.js
CHANGED
|
@@ -2,12 +2,12 @@ import { ReticleTool } from '@reticlehq/server';
|
|
|
2
2
|
import { ReticleAssertionError } from './skip.js';
|
|
3
3
|
import { CONSOLE_LEVEL_ERROR, DEFAULT_ASSERT_TIMEOUT_MS, PredicateKind } from './constants.js';
|
|
4
4
|
function asVerdict(value) {
|
|
5
|
-
if (typeof value !== 'object' ||
|
|
5
|
+
if (typeof value !== 'object' || null === value) {
|
|
6
6
|
return { pass: false, failureReason: 'assert returned a non-object result' };
|
|
7
7
|
}
|
|
8
8
|
const record = value;
|
|
9
|
-
const pass = record['pass']
|
|
10
|
-
const failureReason = typeof record['failureReason']
|
|
9
|
+
const pass = true === record['pass'];
|
|
10
|
+
const failureReason = 'string' === typeof record['failureReason'] ? record['failureReason'] : undefined;
|
|
11
11
|
return {
|
|
12
12
|
pass,
|
|
13
13
|
evidence: record['evidence'],
|
package/dist/register.d.ts
CHANGED
|
@@ -26,5 +26,3 @@ export interface RegisterFlowSpecsOptions extends FlowsAsSpecsOptions {
|
|
|
26
26
|
* vitest collects the registered cases.
|
|
27
27
|
*/
|
|
28
28
|
export declare function registerFlowSpecs(source: Parameters<typeof flowsAsSpecs>[0], getSession: () => Promise<FlowReplaySession> | FlowReplaySession, opts?: RegisterFlowSpecsOptions): Promise<void>;
|
|
29
|
-
/** Public alias matching the design's stated entrypoint name. */
|
|
30
|
-
export declare const reticleFlowsAsSpecs: typeof registerFlowSpecs;
|
package/dist/register.js
CHANGED
|
@@ -24,7 +24,7 @@ function renderEvidence(result) {
|
|
|
24
24
|
.filter((s) => s.drift !== undefined)
|
|
25
25
|
.map((s) => {
|
|
26
26
|
const nearest = s.drift?.nearest;
|
|
27
|
-
const fix =
|
|
27
|
+
const fix = null === nearest || nearest === undefined ? '' : ` (nearest: ${nearest})`;
|
|
28
28
|
return ` step ${String(s.step)}: ${s.drift?.reason ?? 'drift'}${fix}`;
|
|
29
29
|
});
|
|
30
30
|
const successLine = result.successResult.pass || result.successResult.failureReason === undefined
|
|
@@ -68,5 +68,3 @@ export async function registerFlowSpecs(source, getSession, opts) {
|
|
|
68
68
|
register(spec.name, specToCase(spec, getSession));
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
/** Public alias matching the design's stated entrypoint name. */
|
|
72
|
-
export const reticleFlowsAsSpecs = registerFlowSpecs;
|
package/dist/resolve.js
CHANGED
|
@@ -3,13 +3,13 @@ import { QueryBy } from '@reticlehq/core';
|
|
|
3
3
|
import { ReticleQueryEmptyError } from './skip.js';
|
|
4
4
|
import { NO_ELEMENT_FOR_TESTID } from './constants.js';
|
|
5
5
|
function asQueryEnvelope(value) {
|
|
6
|
-
if (typeof value !== 'object' ||
|
|
6
|
+
if (typeof value !== 'object' || null === value)
|
|
7
7
|
return {};
|
|
8
8
|
const record = value;
|
|
9
9
|
const elements = Array.isArray(record['elements'])
|
|
10
10
|
? record['elements']
|
|
11
11
|
: undefined;
|
|
12
|
-
const hint = typeof record['hint']
|
|
12
|
+
const hint = 'object' === typeof record['hint'] && record['hint'] !== null
|
|
13
13
|
? record['hint']
|
|
14
14
|
: undefined;
|
|
15
15
|
return {
|
|
@@ -30,7 +30,7 @@ export async function resolveTestid(invoke, testid, sessionId) {
|
|
|
30
30
|
};
|
|
31
31
|
const result = asQueryEnvelope(await invoke(ReticleTool.QUERY, args));
|
|
32
32
|
const first = result.elements?.[0]?.ref;
|
|
33
|
-
if (typeof first
|
|
33
|
+
if ('string' === typeof first && first.length > 0)
|
|
34
34
|
return first;
|
|
35
35
|
const present = result.hint?.presentTestids;
|
|
36
36
|
const detail = present !== undefined ? { evidence: { presentTestids: present } } : undefined;
|
package/dist/summary.js
CHANGED
|
@@ -11,7 +11,12 @@ export function summarize(results) {
|
|
|
11
11
|
else
|
|
12
12
|
skipped += 1;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
// `ok` is NOT merely "nothing failed". A suite whose every spec skipped — no browser, no real
|
|
15
|
+
// input, no session — has verified nothing, and reporting that as ok is the false green this
|
|
16
|
+
// product exists to prevent, in its own CI gate. An EMPTY suite stays ok: having recorded no
|
|
17
|
+
// flows yet is a different statement from having recorded flows and run none of them.
|
|
18
|
+
const total = results.length;
|
|
19
|
+
return { total, passed, failed, skipped, ok: 0 === failed && (0 === total || passed > 0) };
|
|
15
20
|
}
|
|
16
21
|
function resultLine(r) {
|
|
17
22
|
const head = `${STATUS_GLYPH[r.status]} ${r.name} (${String(r.durationMs)}ms)`;
|
package/dist/test-context.js
CHANGED
|
@@ -7,15 +7,15 @@ import { failFromVerdict } 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) {
|
|
10
|
-
const record = typeof value
|
|
10
|
+
const record = 'object' === typeof value && value !== null ? value : {};
|
|
11
11
|
const verdict = record['verdict'];
|
|
12
|
-
if (typeof verdict !== 'object' ||
|
|
12
|
+
if (typeof verdict !== 'object' || null === verdict) {
|
|
13
13
|
return { pass: false, failureReason: 'act_and_wait returned no verdict' };
|
|
14
14
|
}
|
|
15
15
|
const v = verdict;
|
|
16
|
-
const failureReason = typeof v['failureReason']
|
|
16
|
+
const failureReason = 'string' === typeof v['failureReason'] ? v['failureReason'] : undefined;
|
|
17
17
|
return {
|
|
18
|
-
pass: v['pass']
|
|
18
|
+
pass: true === v['pass'],
|
|
19
19
|
evidence: v['evidence'],
|
|
20
20
|
...(failureReason !== undefined ? { failureReason } : {}),
|
|
21
21
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -26,6 +26,13 @@ export interface RunSummary {
|
|
|
26
26
|
passed: number;
|
|
27
27
|
failed: number;
|
|
28
28
|
skipped: number;
|
|
29
|
+
/**
|
|
30
|
+
* The one field a CI gate should branch on: nothing failed AND something actually ran.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately not `failed === 0` — a suite where every spec skipped has verified nothing, and
|
|
33
|
+
* calling that a pass is exactly the false green this product exists to catch. An empty suite is
|
|
34
|
+
* still ok, because recording no flows yet is not the same as running none of the ones you have.
|
|
35
|
+
*/
|
|
29
36
|
ok: boolean;
|
|
30
37
|
}
|
|
31
38
|
/** Builds the per-spec `t` from the invoker — supplied by MATCHERS; the runner only calls it. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/test",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.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/
|
|
16
|
-
"@reticlehq/
|
|
15
|
+
"@reticlehq/core": "2.5.0",
|
|
16
|
+
"@reticlehq/server": "2.5.0"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"vitest": "^3.2.6"
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"build": "tsc -b",
|
|
53
53
|
"typecheck": "tsc -b",
|
|
54
54
|
"lint": "eslint src",
|
|
55
|
-
"test:unit": "vitest run src --passWithNoTests"
|
|
55
|
+
"test:unit": "vitest run src --passWithNoTests",
|
|
56
|
+
"test:coverage": "vitest run src --passWithNoTests --coverage.enabled --coverage.provider=v8 --coverage.reporter=text-summary --coverage.include='src/**/*.ts' --coverage.exclude='src/**/*.test.ts'"
|
|
56
57
|
}
|
|
57
58
|
}
|