@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 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 === 'string'
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, reticleFlowsAsSpecs, FlowMalformedError, SpecFailure, } from './register.js';
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, reticleFlowsAsSpecs, FlowMalformedError, SpecFailure, } from './register.js';
15
+ export { registerFlowSpecs, FlowMalformedError, SpecFailure } from './register.js';
16
16
  export { SpecKind, SpecOutcome, SpecMessage, FLOW_LOAD_ERROR_PREFIX } from './constants.js';
@@ -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 === 'object' && value !== null;
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' || actResult === null)
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'] === true ? InputMode.REAL : InputMode.SYNTHETIC;
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
- * Characters illegal in XML 1.0 per section 2.2: U+0000-U+0008, U+000B, U+000C, U+000E-U+001F.
5
- * Tab (U+0009), newline (U+000A), and carriage return (U+000D) are the only control chars allowed.
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
- // eslint-disable-next-line no-control-regex -- intentional: these are the exact chars to strip
8
- const XML_ILLEGAL_CHARS = /[\x00-\x08\x0B\x0C\x0E-\x1F]/g;
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, '&lt;')
19
36
  .replace(/>/g, '&gt;')
@@ -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 match === null ? text : text.slice(0, match.index);
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' || value === null) {
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'] === true;
10
- const failureReason = typeof record['failureReason'] === 'string' ? record['failureReason'] : undefined;
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'],
@@ -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 = nearest === null || nearest === undefined ? '' : ` (nearest: ${nearest})`;
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' || value === null)
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'] === 'object' && record['hint'] !== null
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 === 'string' && first.length > 0)
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
- return { total: results.length, passed, failed, skipped, ok: failed === 0 };
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)`;
@@ -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 === 'object' && value !== null ? value : {};
10
+ const record = 'object' === typeof value && value !== null ? value : {};
11
11
  const verdict = record['verdict'];
12
- if (typeof verdict !== 'object' || verdict === null) {
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'] === 'string' ? v['failureReason'] : undefined;
16
+ const failureReason = 'string' === typeof v['failureReason'] ? v['failureReason'] : undefined;
17
17
  return {
18
- pass: v['pass'] === true,
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.4.0",
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/server": "2.4.0",
16
- "@reticlehq/core": "2.4.0"
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
  }