@reticlehq/test 2.0.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.
Files changed (74) hide show
  1. package/LICENSE +105 -0
  2. package/dist/boot.d.ts +25 -0
  3. package/dist/boot.d.ts.map +1 -0
  4. package/dist/boot.js +41 -0
  5. package/dist/boot.js.map +1 -0
  6. package/dist/clock.d.ts +10 -0
  7. package/dist/clock.d.ts.map +1 -0
  8. package/dist/clock.js +19 -0
  9. package/dist/clock.js.map +1 -0
  10. package/dist/constants.d.ts +87 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/constants.js +72 -0
  13. package/dist/constants.js.map +1 -0
  14. package/dist/flow-spec.d.ts +53 -0
  15. package/dist/flow-spec.d.ts.map +1 -0
  16. package/dist/flow-spec.js +79 -0
  17. package/dist/flow-spec.js.map +1 -0
  18. package/dist/index.d.ts +23 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +17 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/input-mode.d.ts +21 -0
  23. package/dist/input-mode.d.ts.map +1 -0
  24. package/dist/input-mode.js +64 -0
  25. package/dist/input-mode.js.map +1 -0
  26. package/dist/junit.d.ts +11 -0
  27. package/dist/junit.d.ts.map +1 -0
  28. package/dist/junit.js +43 -0
  29. package/dist/junit.js.map +1 -0
  30. package/dist/matchers.d.ts +26 -0
  31. package/dist/matchers.d.ts.map +1 -0
  32. package/dist/matchers.js +95 -0
  33. package/dist/matchers.js.map +1 -0
  34. package/dist/register.d.ts +31 -0
  35. package/dist/register.d.ts.map +1 -0
  36. package/dist/register.js +73 -0
  37. package/dist/register.js.map +1 -0
  38. package/dist/registry.d.ts +6 -0
  39. package/dist/registry.d.ts.map +1 -0
  40. package/dist/registry.js +13 -0
  41. package/dist/registry.js.map +1 -0
  42. package/dist/resolve.d.ts +8 -0
  43. package/dist/resolve.d.ts.map +1 -0
  44. package/dist/resolve.js +39 -0
  45. package/dist/resolve.js.map +1 -0
  46. package/dist/runner.d.ts +13 -0
  47. package/dist/runner.d.ts.map +1 -0
  48. package/dist/runner.js +52 -0
  49. package/dist/runner.js.map +1 -0
  50. package/dist/skip.d.ts +34 -0
  51. package/dist/skip.d.ts.map +1 -0
  52. package/dist/skip.js +44 -0
  53. package/dist/skip.js.map +1 -0
  54. package/dist/spec.d.ts +4 -0
  55. package/dist/spec.d.ts.map +1 -0
  56. package/dist/spec.js +6 -0
  57. package/dist/spec.js.map +1 -0
  58. package/dist/success-assert.d.ts +7 -0
  59. package/dist/success-assert.d.ts.map +1 -0
  60. package/dist/success-assert.js +7 -0
  61. package/dist/success-assert.js.map +1 -0
  62. package/dist/summary.d.ts +8 -0
  63. package/dist/summary.d.ts.map +1 -0
  64. package/dist/summary.js +34 -0
  65. package/dist/summary.js.map +1 -0
  66. package/dist/test-context.d.ts +34 -0
  67. package/dist/test-context.d.ts.map +1 -0
  68. package/dist/test-context.js +85 -0
  69. package/dist/test-context.js.map +1 -0
  70. package/dist/types.d.ts +45 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +2 -0
  73. package/dist/types.js.map +1 -0
  74. package/package.json +54 -0
@@ -0,0 +1,64 @@
1
+ import { ReticleTool } from '@reticlehq/server';
2
+ import { InputMode } from '@reticlehq/core';
3
+ import { ReticleSkip } from './skip.js';
4
+ import { SKIP_REASON_REAL_INPUT } from './constants.js';
5
+ function isRecord(value) {
6
+ return typeof value === 'object' && value !== null;
7
+ }
8
+ /** The act envelope's inputMode field, narrowed from the raw tool result. */
9
+ export function readInputMode(actResult) {
10
+ if (typeof actResult !== 'object' || actResult === null)
11
+ return undefined;
12
+ const mode = actResult['inputMode'];
13
+ if (mode === InputMode.REAL || mode === InputMode.SYNTHETIC)
14
+ return mode;
15
+ return undefined;
16
+ }
17
+ /**
18
+ * Tracks the most recent act's inputMode across a spec so `expectInputModeReal()` can read it
19
+ * without re-touching the page. The context records every act through `record`.
20
+ */
21
+ export class InputModeTracker {
22
+ last;
23
+ record(actResult) {
24
+ const mode = readInputMode(actResult);
25
+ if (mode !== undefined)
26
+ this.last = mode;
27
+ }
28
+ get() {
29
+ return this.last;
30
+ }
31
+ }
32
+ /**
33
+ * Assert real native input is active. Prefers the last act's reported mode (zero extra page
34
+ * mutation); if no act has run, issues a non-mutating SCROLL_INTO_VIEW probe and reads its mode.
35
+ * On 'synthetic' throws ReticleSkip(SKIP_REASON_REAL_INPUT) — the runner turns this into status:'skip'.
36
+ * It NEVER silently passes on synthetic.
37
+ */
38
+ export async function expectInputModeReal(invoke, tracker, sessionId) {
39
+ let mode = tracker.get();
40
+ if (mode === undefined) {
41
+ mode = await probeInputMode(invoke, sessionId);
42
+ }
43
+ if (mode !== InputMode.REAL) {
44
+ throw new ReticleSkip(SKIP_REASON_REAL_INPUT);
45
+ }
46
+ }
47
+ /**
48
+ * Read whether native real input is active for the session WITHOUT touching the page: reticle_sessions
49
+ * reports `realInputAvailable` per session (true when a CDP/launched provider matches the tab url).
50
+ * App-agnostic — no assumed testid.
51
+ */
52
+ async function probeInputMode(invoke, sessionId) {
53
+ const result = await invoke(ReticleTool.SESSIONS, {});
54
+ const sessions = isRecord(result) && Array.isArray(result['sessions']) ? result['sessions'] : [];
55
+ for (const session of sessions) {
56
+ if (!isRecord(session))
57
+ continue;
58
+ if (sessionId === undefined || session['sessionId'] === sessionId) {
59
+ return session['realInputAvailable'] === true ? InputMode.REAL : InputMode.SYNTHETIC;
60
+ }
61
+ }
62
+ return InputMode.SYNTHETIC;
63
+ }
64
+ //# sourceMappingURL=input-mode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input-mode.js","sourceRoot":"","sources":["../src/input-mode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAoB,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAExD,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AACrD,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,aAAa,CAAC,SAAkB;IAC9C,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC1E,MAAM,IAAI,GAAI,SAAqC,CAAC,WAAW,CAAC,CAAC;IACjE,IAAI,IAAI,KAAK,SAAS,CAAC,IAAI,IAAI,IAAI,KAAK,SAAS,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IACzE,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,MAAM,OAAO,gBAAgB;IACnB,IAAI,CAAwB;IAEpC,MAAM,CAAC,SAAkB;QACvB,MAAM,IAAI,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;QACtC,IAAI,IAAI,KAAK,SAAS;YAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAC3C,CAAC;IAED,GAAG;QACD,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,MAAmB,EACnB,OAAyB,EACzB,SAAkB;IAElB,IAAI,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IACzB,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,IAAI,GAAG,MAAM,cAAc,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACjD,CAAC;IACD,IAAI,IAAI,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC;QAC5B,MAAM,IAAI,WAAW,CAAC,sBAAsB,CAAC,CAAC;IAChD,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,cAAc,CAAC,MAAmB,EAAE,SAAkB;IACnE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACtD,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACjG,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,SAAS;QACjC,IAAI,SAAS,KAAK,SAAS,IAAI,OAAO,CAAC,WAAW,CAAC,KAAK,SAAS,EAAE,CAAC;YAClE,OAAO,OAAO,CAAC,oBAAoB,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC;QACvF,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC,SAAS,CAAC;AAC7B,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { FileSystemPort } from '@reticlehq/server';
2
+ import type { SpecResult } from './types.js';
3
+ /** Render results as a single JUnit `<testsuite>` document (CI consumable). */
4
+ export declare function toJUnitXml(results: readonly SpecResult[], opts?: {
5
+ suite?: string;
6
+ }): string;
7
+ /** Write the JUnit report through the injected filesystem seam (tests pass a fake adapter). */
8
+ export declare function writeJUnit(fs: FileSystemPort, path: string, results: readonly SpecResult[], opts?: {
9
+ suite?: string;
10
+ }): Promise<void>;
11
+ //# sourceMappingURL=junit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"junit.d.ts","sourceRoot":"","sources":["../src/junit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA+B7C,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,EAAE,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAU5F;AAED,+FAA+F;AAC/F,wBAAsB,UAAU,CAC9B,EAAE,EAAE,cAAc,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,SAAS,UAAU,EAAE,EAC9B,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACxB,OAAO,CAAC,IAAI,CAAC,CAEf"}
package/dist/junit.js ADDED
@@ -0,0 +1,43 @@
1
+ import { DEFAULT_JUNIT_SUITE_NAME, JUnit, TestStatus } from './constants.js';
2
+ import { summarize } from './summary.js';
3
+ /** Escape the five XML-significant characters in attribute values + text. */
4
+ function escapeXml(value) {
5
+ return value
6
+ .replace(/&/g, '&amp;')
7
+ .replace(/</g, '&lt;')
8
+ .replace(/>/g, '&gt;')
9
+ .replace(/"/g, '&quot;')
10
+ .replace(/'/g, '&apos;');
11
+ }
12
+ function seconds(ms) {
13
+ return (ms / 1000).toFixed(3);
14
+ }
15
+ function caseXml(r) {
16
+ const open = ` <${JUnit.CASE} ${JUnit.ATTR_NAME}="${escapeXml(r.name)}" ` +
17
+ `${JUnit.ATTR_TIME}="${seconds(r.durationMs)}">`;
18
+ if (r.status === TestStatus.FAIL) {
19
+ const msg = escapeXml(r.error ?? '');
20
+ return `${open}\n <${JUnit.FAILURE} ${JUnit.ATTR_MESSAGE}="${msg}"></${JUnit.FAILURE}>\n </${JUnit.CASE}>`;
21
+ }
22
+ if (r.status === TestStatus.SKIP) {
23
+ const msg = escapeXml(r.skipReason ?? '');
24
+ return `${open}\n <${JUnit.SKIPPED} ${JUnit.ATTR_MESSAGE}="${msg}"></${JUnit.SKIPPED}>\n </${JUnit.CASE}>`;
25
+ }
26
+ return `${open}</${JUnit.CASE}>`;
27
+ }
28
+ /** Render results as a single JUnit `<testsuite>` document (CI consumable). */
29
+ export function toJUnitXml(results, opts) {
30
+ const suite = escapeXml(opts?.suite ?? DEFAULT_JUNIT_SUITE_NAME);
31
+ const s = summarize(results);
32
+ const header = `<${JUnit.SUITE} ${JUnit.ATTR_NAME}="${suite}" ` +
33
+ `${JUnit.ATTR_TESTS}="${String(s.total)}" ` +
34
+ `${JUnit.ATTR_FAILURES}="${String(s.failed)}" ` +
35
+ `${JUnit.ATTR_SKIPPED}="${String(s.skipped)}">`;
36
+ const body = results.map(caseXml).join('\n');
37
+ return `<?xml version="1.0" encoding="UTF-8"?>\n${header}\n${body}\n</${JUnit.SUITE}>\n`;
38
+ }
39
+ /** Write the JUnit report through the injected filesystem seam (tests pass a fake adapter). */
40
+ export async function writeJUnit(fs, path, results, opts) {
41
+ await fs.writeFile(path, toJUnitXml(results, opts));
42
+ }
43
+ //# sourceMappingURL=junit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"junit.js","sourceRoot":"","sources":["../src/junit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import { type ToolInvoker } from '@reticlehq/server';
2
+ import type { ElementQuery, ElementState } from '@reticlehq/core';
3
+ /** The verdict envelope returned by reticle_assert / the `verdict` field of reticle_act_and_wait. */
4
+ export interface Verdict {
5
+ pass: boolean;
6
+ evidence?: unknown;
7
+ failureReason?: string;
8
+ }
9
+ /**
10
+ * Throw an ReticleAssertionError carrying the verdict's own evidence + failureReason. Used by every
11
+ * `expect*` matcher and by actAndWait, so the runner's single catch boundary marks fail with the
12
+ * predicate engine's structured diagnosis intact. `extraEvidence` lets actAndWait attach its trace.
13
+ */
14
+ export declare function failFromVerdict(verdict: Verdict, extraEvidence?: Record<string, unknown>): never;
15
+ export interface MatcherDeps {
16
+ invoke: ToolInvoker;
17
+ timeoutMs?: number;
18
+ sessionId?: string;
19
+ }
20
+ export declare function expectSignal(deps: MatcherDeps, name: string, dataMatches?: Record<string, unknown>): Promise<void>;
21
+ export declare function expectNet(deps: MatcherDeps, method: string, urlContains: string, status?: number): Promise<void>;
22
+ export declare function expectElement(deps: MatcherDeps, query: ElementQuery, state?: ElementState): Promise<void>;
23
+ export declare function expectText(deps: MatcherDeps, contains: string): Promise<void>;
24
+ export declare function expectAbsent(deps: MatcherDeps, query: ElementQuery): Promise<void>;
25
+ export declare function expectNoConsoleErrors(deps: MatcherDeps): Promise<void>;
26
+ //# sourceMappingURL=matchers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matchers.d.ts","sourceRoot":"","sources":["../src/matchers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAIlE,qGAAqG;AACrG,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAiBD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CAchG;AAkBD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,WAAW,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD,wBAAgB,YAAY,CAC1B,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACpC,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAgB,SAAS,CACvB,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAQf;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,YAAY,EACnB,KAAK,CAAC,EAAE,YAAY,GACnB,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG7E;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAOlF;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAOtE"}
@@ -0,0 +1,95 @@
1
+ import { ReticleTool } from '@reticlehq/server';
2
+ import { ReticleAssertionError } from './skip.js';
3
+ import { CONSOLE_LEVEL_ERROR, DEFAULT_ASSERT_TIMEOUT_MS, PredicateKind } from './constants.js';
4
+ function asVerdict(value) {
5
+ if (typeof value !== 'object' || value === null) {
6
+ return { pass: false, failureReason: 'assert returned a non-object result' };
7
+ }
8
+ const record = value;
9
+ const pass = record['pass'] === true;
10
+ const failureReason = typeof record['failureReason'] === 'string' ? record['failureReason'] : undefined;
11
+ return {
12
+ pass,
13
+ evidence: record['evidence'],
14
+ ...(failureReason !== undefined ? { failureReason } : {}),
15
+ };
16
+ }
17
+ /**
18
+ * Throw an ReticleAssertionError carrying the verdict's own evidence + failureReason. Used by every
19
+ * `expect*` matcher and by actAndWait, so the runner's single catch boundary marks fail with the
20
+ * predicate engine's structured diagnosis intact. `extraEvidence` lets actAndWait attach its trace.
21
+ */
22
+ export function failFromVerdict(verdict, extraEvidence) {
23
+ const message = verdict.failureReason ?? 'assertion failed';
24
+ // actAndWait passes extra context (the reaction trace); merge it alongside the verdict evidence.
25
+ const evidence = extraEvidence !== undefined
26
+ ? {
27
+ ...extraEvidence,
28
+ ...(verdict.evidence !== undefined ? { evidence: verdict.evidence } : {}),
29
+ }
30
+ : verdict.evidence;
31
+ throw new ReticleAssertionError(message, {
32
+ ...(evidence !== undefined ? { evidence } : {}),
33
+ ...(verdict.failureReason !== undefined ? { failureReason: verdict.failureReason } : {}),
34
+ });
35
+ }
36
+ /** Run reticle_assert for a predicate; resolve on pass, throw the structured failure otherwise. */
37
+ async function assertPredicate(invoke, predicate, timeoutMs, sessionId) {
38
+ const args = {
39
+ predicate,
40
+ timeout_ms: timeoutMs,
41
+ ...(sessionId !== undefined ? { sessionId } : {}),
42
+ };
43
+ const verdict = asVerdict(await invoke(ReticleTool.ASSERT, args));
44
+ if (!verdict.pass)
45
+ failFromVerdict(verdict);
46
+ }
47
+ function timeout(deps) {
48
+ return deps.timeoutMs ?? DEFAULT_ASSERT_TIMEOUT_MS;
49
+ }
50
+ export function expectSignal(deps, name, dataMatches) {
51
+ const predicate = {
52
+ kind: PredicateKind.SIGNAL,
53
+ name,
54
+ ...(dataMatches !== undefined ? { dataMatches } : {}),
55
+ };
56
+ return assertPredicate(deps.invoke, predicate, timeout(deps), deps.sessionId);
57
+ }
58
+ export function expectNet(deps, method, urlContains, status) {
59
+ const predicate = {
60
+ kind: PredicateKind.NET,
61
+ method,
62
+ urlContains,
63
+ ...(status !== undefined ? { status } : {}),
64
+ };
65
+ return assertPredicate(deps.invoke, predicate, timeout(deps), deps.sessionId);
66
+ }
67
+ export function expectElement(deps, query, state) {
68
+ const predicate = {
69
+ kind: PredicateKind.ELEMENT,
70
+ query,
71
+ ...(state !== undefined ? { state } : {}),
72
+ };
73
+ return assertPredicate(deps.invoke, predicate, timeout(deps), deps.sessionId);
74
+ }
75
+ export function expectText(deps, contains) {
76
+ const predicate = { kind: PredicateKind.TEXT, contains };
77
+ return assertPredicate(deps.invoke, predicate, timeout(deps), deps.sessionId);
78
+ }
79
+ export function expectAbsent(deps, query) {
80
+ const predicate = {
81
+ kind: PredicateKind.ELEMENT,
82
+ query,
83
+ absent: true,
84
+ };
85
+ return assertPredicate(deps.invoke, predicate, timeout(deps), deps.sessionId);
86
+ }
87
+ export function expectNoConsoleErrors(deps) {
88
+ const predicate = {
89
+ kind: PredicateKind.CONSOLE,
90
+ level: CONSOLE_LEVEL_ERROR,
91
+ absent: true,
92
+ };
93
+ return assertPredicate(deps.invoke, predicate, timeout(deps), deps.sessionId);
94
+ }
95
+ //# sourceMappingURL=matchers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matchers.js","sourceRoot":"","sources":["../src/matchers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import type { FlowErrorCode } from '@reticlehq/core';
2
+ import type { FlowReplaySession } from '@reticlehq/server';
3
+ import { flowsAsSpecs } from './flow-spec.js';
4
+ import type { FlowsAsSpecsOptions, SpecRunResult } from './flow-spec.js';
5
+ /** A loaded flow file was missing/malformed/badly-named — its spec throws this when run. */
6
+ export declare class FlowMalformedError extends Error {
7
+ readonly code: FlowErrorCode;
8
+ constructor(name: string, code: FlowErrorCode);
9
+ }
10
+ /** A RUNNABLE flow spec ran but failed — carries the verbatim replay + success evidence. */
11
+ export declare class SpecFailure extends Error {
12
+ readonly result: SpecRunResult;
13
+ constructor(name: string, result: SpecRunResult);
14
+ }
15
+ /** The injectable test-registrar — vitest's `it` in CI, a collector stub in unit tests. */
16
+ export type RegisterFn = (name: string, fn: () => Promise<void> | void) => void;
17
+ export interface RegisterFlowSpecsOptions extends FlowsAsSpecsOptions {
18
+ /** Defaults to vitest's `it`. Injected so registration can be unit-tested without nested vitest. */
19
+ register?: RegisterFn;
20
+ }
21
+ /**
22
+ * FLOW2SPEC — register one vitest test per flow under `source`. The agent-readable map (.reticle/flows)
23
+ * becomes the executable suite: a passing flow registers a green `it`, a malformed file registers a
24
+ * loudly-failing ERROR `it`, an empty dir registers nothing. `getSession` is the seam — the
25
+ * `reticle drive` launched session in CI, a fake in unit tests. MUST be awaited at module top level so
26
+ * vitest collects the registered cases.
27
+ */
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;
31
+ //# sourceMappingURL=register.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,KAAK,EAAY,mBAAmB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEnF,4FAA4F;AAC5F,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;gBACjB,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa;CAK9C;AAED,4FAA4F;AAC5F,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;gBACnB,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa;CAKhD;AAkBD,2FAA2F;AAC3F,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;AAEhF,MAAM,WAAW,wBAAyB,SAAQ,mBAAmB;IACnE,oGAAoG;IACpG,QAAQ,CAAC,EAAE,UAAU,CAAC;CACvB;AA6BD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC,EAC1C,UAAU,EAAE,MAAM,OAAO,CAAC,iBAAiB,CAAC,GAAG,iBAAiB,EAChE,IAAI,CAAC,EAAE,wBAAwB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,iEAAiE;AACjE,eAAO,MAAM,mBAAmB,0BAAoB,CAAC"}
@@ -0,0 +1,73 @@
1
+ import { FLOW_LOAD_ERROR_PREFIX, SpecKind, SpecOutcome } from './constants.js';
2
+ import { flowsAsSpecs } from './flow-spec.js';
3
+ /** A loaded flow file was missing/malformed/badly-named — its spec throws this when run. */
4
+ export class FlowMalformedError extends Error {
5
+ code;
6
+ constructor(name, code) {
7
+ super(`${FLOW_LOAD_ERROR_PREFIX}: '${name}' (${code})`);
8
+ this.name = 'FlowMalformedError';
9
+ this.code = code;
10
+ }
11
+ }
12
+ /** A RUNNABLE flow spec ran but failed — carries the verbatim replay + success evidence. */
13
+ export class SpecFailure extends Error {
14
+ result;
15
+ constructor(name, result) {
16
+ super(`${result.message ?? 'flow spec failed'}: ${name}\n${renderEvidence(result)}`);
17
+ this.name = 'SpecFailure';
18
+ this.result = result;
19
+ }
20
+ }
21
+ /** Render the structured drift/near-miss evidence into the Error message so a CI log self-explains. */
22
+ function renderEvidence(result) {
23
+ const driftLines = result.steps
24
+ .filter((s) => s.drift !== undefined)
25
+ .map((s) => {
26
+ const nearest = s.drift?.nearest;
27
+ const fix = nearest === null || nearest === undefined ? '' : ` (nearest: ${nearest})`;
28
+ return ` step ${String(s.step)}: ${s.drift?.reason ?? 'drift'}${fix}`;
29
+ });
30
+ const successLine = result.successResult.pass || result.successResult.failureReason === undefined
31
+ ? []
32
+ : [` success: ${result.successResult.failureReason}`];
33
+ return [...driftLines, ...successLine].join('\n');
34
+ }
35
+ /**
36
+ * Resolve the default registrar (vitest's `it`) lazily. vitest is an OPTIONAL peer dependency, so the
37
+ * public barrel (`@reticlehq/test`) must stay importable when vitest is absent — only callers that actually
38
+ * register flow specs without injecting their own `register` fn need it. A static top-level import
39
+ * would pull vitest into the eager module graph of every `import { reticleTest } from '@reticlehq/test'`.
40
+ */
41
+ async function defaultRegister() {
42
+ const vitest = await import('vitest');
43
+ return vitest.it;
44
+ }
45
+ /** Turn one FlowSpec into a (name, fn) registration. The fn throws structured evidence on failure. */
46
+ function specToCase(spec, getSession) {
47
+ return async () => {
48
+ if (spec.kind === SpecKind.ERROR && spec.loadError !== undefined) {
49
+ throw new FlowMalformedError(spec.name, spec.loadError.code);
50
+ }
51
+ const result = await spec.run(await getSession());
52
+ if (result.outcome === SpecOutcome.FAIL) {
53
+ throw new SpecFailure(spec.name, result);
54
+ }
55
+ };
56
+ }
57
+ /**
58
+ * FLOW2SPEC — register one vitest test per flow under `source`. The agent-readable map (.reticle/flows)
59
+ * becomes the executable suite: a passing flow registers a green `it`, a malformed file registers a
60
+ * loudly-failing ERROR `it`, an empty dir registers nothing. `getSession` is the seam — the
61
+ * `reticle drive` launched session in CI, a fake in unit tests. MUST be awaited at module top level so
62
+ * vitest collects the registered cases.
63
+ */
64
+ export async function registerFlowSpecs(source, getSession, opts) {
65
+ const register = opts?.register ?? (await defaultRegister());
66
+ const specs = await flowsAsSpecs(source, opts);
67
+ for (const spec of specs) {
68
+ register(spec.name, specToCase(spec, getSession));
69
+ }
70
+ }
71
+ /** Public alias matching the design's stated entrypoint name. */
72
+ export const reticleFlowsAsSpecs = registerFlowSpecs;
73
+ //# sourceMappingURL=register.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { ReticleSpec } from './types.js';
2
+ export declare function register(spec: ReticleSpec): void;
3
+ /** A frozen snapshot copy so a run that started can ignore later (re-entrant) registrations. */
4
+ export declare function getRegistered(): readonly ReticleSpec[];
5
+ export declare function clearRegistry(): void;
6
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAK9C,wBAAgB,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAEhD;AAED,gGAAgG;AAChG,wBAAgB,aAAa,IAAI,SAAS,WAAW,EAAE,CAEtD;AAED,wBAAgB,aAAa,IAAI,IAAI,CAEpC"}
@@ -0,0 +1,13 @@
1
+ /** Module-level registration list. A list (not a map) so duplicate names are kept, not collapsed. */
2
+ const specs = [];
3
+ export function register(spec) {
4
+ specs.push(spec);
5
+ }
6
+ /** A frozen snapshot copy so a run that started can ignore later (re-entrant) registrations. */
7
+ export function getRegistered() {
8
+ return [...specs];
9
+ }
10
+ export function clearRegistry() {
11
+ specs.length = 0;
12
+ }
13
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAEA,qGAAqG;AACrG,MAAM,KAAK,GAAkB,EAAE,CAAC;AAEhC,MAAM,UAAU,QAAQ,CAAC,IAAiB;IACxC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnB,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,aAAa;IAC3B,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;AACnB,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { type ToolInvoker } from '@reticlehq/server';
2
+ /**
3
+ * The single testid -> ref chokepoint. Queries by testid; returns the first ref. On zero matches
4
+ * throws ReticleQueryEmptyError naming the testid, carrying the query's presentTestids as evidence so
5
+ * the runner surfaces "you meant one of these" instead of a blank failure.
6
+ */
7
+ export declare function resolveTestid(invoke: ToolInvoker, testid: string, sessionId?: string): Promise<string>;
8
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AA2BlE;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAajB"}
@@ -0,0 +1,39 @@
1
+ import { ReticleTool } from '@reticlehq/server';
2
+ import { QueryBy } from '@reticlehq/core';
3
+ import { ReticleQueryEmptyError } from './skip.js';
4
+ import { NO_ELEMENT_FOR_TESTID } from './constants.js';
5
+ function asQueryEnvelope(value) {
6
+ if (typeof value !== 'object' || value === null)
7
+ return {};
8
+ const record = value;
9
+ const elements = Array.isArray(record['elements'])
10
+ ? record['elements']
11
+ : undefined;
12
+ const hint = typeof record['hint'] === 'object' && record['hint'] !== null
13
+ ? record['hint']
14
+ : undefined;
15
+ return {
16
+ ...(elements !== undefined ? { elements } : {}),
17
+ ...(hint !== undefined ? { hint } : {}),
18
+ };
19
+ }
20
+ /**
21
+ * The single testid -> ref chokepoint. Queries by testid; returns the first ref. On zero matches
22
+ * throws ReticleQueryEmptyError naming the testid, carrying the query's presentTestids as evidence so
23
+ * the runner surfaces "you meant one of these" instead of a blank failure.
24
+ */
25
+ export async function resolveTestid(invoke, testid, sessionId) {
26
+ const args = {
27
+ by: QueryBy.TESTID,
28
+ value: testid,
29
+ ...(sessionId !== undefined ? { sessionId } : {}),
30
+ };
31
+ const result = asQueryEnvelope(await invoke(ReticleTool.QUERY, args));
32
+ const first = result.elements?.[0]?.ref;
33
+ if (typeof first === 'string' && first.length > 0)
34
+ return first;
35
+ const present = result.hint?.presentTestids;
36
+ const detail = present !== undefined ? { evidence: { presentTestids: present } } : undefined;
37
+ throw new ReticleQueryEmptyError(`${NO_ELEMENT_FOR_TESTID} ${testid}`, detail);
38
+ }
39
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAoB,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAQvD,SAAS,eAAe,CAAC,KAAc;IACrC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IAC3D,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAChD,CAAC,CAAE,MAAM,CAAC,UAAU,CAAyB;QAC7C,CAAC,CAAC,SAAS,CAAC;IACd,MAAM,IAAI,GACR,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,IAAI;QAC3D,CAAC,CAAE,MAAM,CAAC,MAAM,CAAkC;QAClD,CAAC,CAAC,SAAS,CAAC;IAChB,OAAO;QACL,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACxC,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAAmB,EACnB,MAAc,EACd,SAAkB;IAElB,MAAM,IAAI,GAA4B;QACpC,EAAE,EAAE,OAAO,CAAC,MAAM;QAClB,KAAK,EAAE,MAAM;QACb,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClD,CAAC;IACF,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;IACtE,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC;IACxC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAEhE,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,cAAc,CAAC;IAC5C,MAAM,MAAM,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAC7F,MAAM,IAAI,sBAAsB,CAAC,GAAG,qBAAqB,IAAI,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC;AACjF,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { ReticleSpec, RunSummary, RunnerOptions, SpecResult } from './types.js';
2
+ /** Run one spec to a result. Never throws — a spec failure is captured, not propagated. */
3
+ export declare function runOne(spec: ReticleSpec, opts: RunnerOptions): Promise<SpecResult>;
4
+ /**
5
+ * Run all specs sequentially, in registration order, isolating each through runOne so one
6
+ * failure never aborts the rest. Snapshots the spec list at call time (re-entrant registration
7
+ * during a run is ignored for that run). Returns the JSON result + summary; prints if a sink is set.
8
+ */
9
+ export declare function runSpecs(opts: RunnerOptions): Promise<{
10
+ results: SpecResult[];
11
+ summary: RunSummary;
12
+ }>;
13
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAWrF,2FAA2F;AAC3F,wBAAsB,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAmBxF;AAED;;;;GAIG;AACH,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC;IAAE,OAAO,EAAE,UAAU,EAAE,CAAC;IAAC,OAAO,EAAE,UAAU,CAAA;CAAE,CAAC,CAWzD"}
package/dist/runner.js ADDED
@@ -0,0 +1,52 @@
1
+ import { TestStatus } from './constants.js';
2
+ import { isSkip } from './skip.js';
3
+ import { getRegistered } from './registry.js';
4
+ import { summarize, printSummary } from './summary.js';
5
+ /** Classify a thrown value: skip (sentinel) vs fail (everything else). */
6
+ function classify(error) {
7
+ if (isSkip(error)) {
8
+ return { status: TestStatus.SKIP, skipReason: error.reason };
9
+ }
10
+ const message = error instanceof Error ? error.message : String(error);
11
+ return { status: TestStatus.FAIL, error: message };
12
+ }
13
+ /** Run one spec to a result. Never throws — a spec failure is captured, not propagated. */
14
+ export async function runOne(spec, opts) {
15
+ const start = opts.now();
16
+ let outcome;
17
+ try {
18
+ const t = opts.buildContext(opts.invoke);
19
+ await spec.fn(t);
20
+ outcome = { status: TestStatus.PASS };
21
+ }
22
+ catch (error) {
23
+ outcome = classify(error);
24
+ }
25
+ const durationMs = opts.now() - start;
26
+ // exactOptionalPropertyTypes: only spread an optional field when it is actually present.
27
+ return {
28
+ name: spec.name,
29
+ status: outcome.status,
30
+ durationMs,
31
+ ...(outcome.error !== undefined ? { error: outcome.error } : {}),
32
+ ...(outcome.skipReason !== undefined ? { skipReason: outcome.skipReason } : {}),
33
+ };
34
+ }
35
+ /**
36
+ * Run all specs sequentially, in registration order, isolating each through runOne so one
37
+ * failure never aborts the rest. Snapshots the spec list at call time (re-entrant registration
38
+ * during a run is ignored for that run). Returns the JSON result + summary; prints if a sink is set.
39
+ */
40
+ export async function runSpecs(opts) {
41
+ const specs = opts.specs ?? getRegistered();
42
+ const results = [];
43
+ for (const spec of specs) {
44
+ results.push(await runOne(spec, opts));
45
+ }
46
+ const summary = summarize(results);
47
+ if (opts.print !== undefined) {
48
+ printSummary(summary, results, opts.print);
49
+ }
50
+ return { results, summary };
51
+ }
52
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAGvD,0EAA0E;AAC1E,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAClB,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC;IAC/D,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AACrD,CAAC;AAED,2FAA2F;AAC3F,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,IAAiB,EAAE,IAAmB;IACjE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,IAAI,OAA4D,CAAC;IACjE,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACzC,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACjB,OAAO,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,IAAI,EAAE,CAAC;IACxC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC;IACD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;IACtC,yFAAyF;IACzF,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,UAAU;QACV,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,GAAG,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,IAAmB;IAEnB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,aAAa,EAAE,CAAC;IAC5C,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,IAAI,CAAC,MAAM,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IACzC,CAAC;IACD,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC7B,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAC9B,CAAC"}
package/dist/skip.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The skip sentinel. MATCHERS' `t.expectInputModeReal()` throws this when the active input mode
3
+ * is not 'real', so the runner reports `status:'skip'` (with the reason) instead of a silent pass.
4
+ */
5
+ export declare class ReticleSkip extends Error {
6
+ readonly reason: string;
7
+ constructor(reason: string);
8
+ }
9
+ /** Structured diagnosis attached to a matcher failure so the runner surfaces the tool's own evidence. */
10
+ export interface AssertionDetail {
11
+ /** The predicate engine's near-miss / matched-element / found-entries evidence. */
12
+ evidence?: unknown;
13
+ /** The tool's own failureReason, preserved verbatim (the Error message may add framing). */
14
+ failureReason?: string;
15
+ }
16
+ /**
17
+ * A failed matcher (e.g. an `reticle_assert` returning `{ pass:false, failureReason }`). Carries the
18
+ * reason as its message plus the tool's structured `evidence`/`failureReason`; the runner treats it
19
+ * as an ordinary fail (the message becomes SpecResult.error).
20
+ */
21
+ export declare class ReticleAssertionError extends Error {
22
+ readonly evidence?: unknown;
23
+ readonly failureReason?: string;
24
+ constructor(message: string, detail?: AssertionDetail);
25
+ }
26
+ /**
27
+ * Raised when a testid resolves to zero elements (the act/fill chokepoint). A subclass so a runner
28
+ * can special-case "unknown testid" if it wants, but it is still an ordinary fail.
29
+ */
30
+ export declare class ReticleQueryEmptyError extends ReticleAssertionError {
31
+ constructor(message: string, detail?: AssertionDetail);
32
+ }
33
+ export declare function isSkip(error: unknown): error is ReticleSkip;
34
+ //# sourceMappingURL=skip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skip.d.ts","sourceRoot":"","sources":["../src/skip.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,MAAM,EAAE,MAAM;CAK3B;AAED,yGAAyG;AACzG,MAAM,WAAW,eAAe;IAC9B,mFAAmF;IACnF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4FAA4F;IAC5F,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;gBAEpB,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,eAAe;CAOtD;AAED;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,qBAAqB;gBACnD,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,eAAe;CAItD;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAE3D"}