@telorun/assert 0.3.1 → 0.5.1

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/README.md CHANGED
@@ -1,6 +1,21 @@
1
- # ⚡ Telo
2
-
3
- Runtime for declarative backends.
1
+ <p align="center">
2
+ <img src="./assets/telo.png" alt="Telo" width="200" />
3
+ </p>
4
+
5
+ <h1 align="center">Telo</h1>
6
+
7
+ <p align="center">Runtime for declarative backends.</p>
8
+
9
+ <p align="center">
10
+ <a href="https://github.com/telorun/telo/actions/workflows/test.yml"><img alt="Tests" src="https://github.com/telorun/telo/actions/workflows/test.yml/badge.svg" /></a>
11
+ <a href="https://www.npmjs.com/package/@telorun/cli"><img alt="node" src="https://img.shields.io/node/v/@telorun/cli" /></a>
12
+ <br />
13
+ <a href="https://github.com/telorun/telo/commits/main"><img alt="Last commit" src="https://img.shields.io/github/last-commit/telorun/telo" /></a>
14
+ <a href="https://github.com/telorun/telo/issues"><img alt="Issues" src="https://img.shields.io/github/issues/telorun/telo" /></a>
15
+ <a href="https://github.com/telorun/telo/pulls"><img alt="Pull requests" src="https://img.shields.io/github/issues-pr/telorun/telo" /></a>
16
+ <br />
17
+ <img alt="Changesets" src="https://img.shields.io/badge/maintained%20with-changesets-176de3" />
18
+ </p>
4
19
 
5
20
  Telo is an execution engine (Micro-Kernel) that runs logic defined entirely in YAML manifests. Instead of writing imperative backend code, you define your routes, databases, schemas, and AI workflows as atomic, interconnected YAML documents. Telo takes those manifests and runs them.
6
21
 
@@ -0,0 +1,12 @@
1
+ import type { ResourceContext } from "@telorun/sdk";
2
+ /**
3
+ * Build the small ANSI color helpers used by every assert kind. Returns
4
+ * pass-through (uncolored) helpers when stderr isn't a TTY so piped /
5
+ * redirected output stays clean.
6
+ */
7
+ export declare function createColors(ctx: ResourceContext): {
8
+ bold: (t: string) => string;
9
+ red: (t: string) => string;
10
+ green: (t: string) => string;
11
+ dim: (t: string) => string;
12
+ };
package/dist/colors.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Build the small ANSI color helpers used by every assert kind. Returns
3
+ * pass-through (uncolored) helpers when stderr isn't a TTY so piped /
4
+ * redirected output stays clean.
5
+ */
6
+ export function createColors(ctx) {
7
+ const useColor = ctx.stderr.isTTY ?? false;
8
+ const c = (code, text) => useColor ? `\x1b[${code}m${text}\x1b[0m` : text;
9
+ return {
10
+ bold: (t) => c("1", t),
11
+ red: (t) => c("31", t),
12
+ green: (t) => c("32", t),
13
+ dim: (t) => c("2", t),
14
+ };
15
+ }
@@ -0,0 +1,16 @@
1
+ import { ResourceContext } from "@telorun/sdk";
2
+ import { Static } from "@sinclair/typebox";
3
+ export declare const schema: import("@sinclair/typebox").TObject<{
4
+ metadata: import("@sinclair/typebox").TObject<{
5
+ name: import("@sinclair/typebox").TString;
6
+ }>;
7
+ }>;
8
+ type AssertManifest = Static<typeof schema>;
9
+ interface ContainsInput {
10
+ actual: unknown;
11
+ value: unknown;
12
+ }
13
+ export declare function create(manifest: AssertManifest, ctx: ResourceContext): Promise<{
14
+ invoke: (input: ContainsInput) => boolean;
15
+ }>;
16
+ export {};
@@ -0,0 +1,53 @@
1
+ import { InvokeError } from "@telorun/sdk";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { createColors } from "./colors.js";
4
+ import { deepEquals } from "./deep-equals.js";
5
+ export const schema = Type.Object({
6
+ metadata: Type.Object({
7
+ name: Type.String(),
8
+ }),
9
+ });
10
+ export async function create(manifest, ctx) {
11
+ const { bold, red, green, dim } = createColors(ctx);
12
+ const name = manifest.metadata.name;
13
+ return {
14
+ invoke: (input) => {
15
+ const { actual, value } = input ?? {};
16
+ let ok = false;
17
+ let kind = "";
18
+ if (typeof actual === "string") {
19
+ if (typeof value !== "string") {
20
+ const message = `actual is a string but value is ${typeof value}; expected substring`;
21
+ ctx.stderr.write(bold(red(`Assert.Contains.${name}: assertion failed`)) +
22
+ "\n" +
23
+ ` ${red("✗")} ${message}\n`);
24
+ throw new InvokeError("ERR_ASSERTION_FAILED", `Assert.Contains "${name}": ${message}`);
25
+ }
26
+ kind = "substring";
27
+ ok = actual.includes(value);
28
+ }
29
+ else if (Array.isArray(actual)) {
30
+ kind = "element";
31
+ ok = actual.some((item) => deepEquals(item, value));
32
+ }
33
+ else {
34
+ const message = `actual must be string or array; got ${typeof actual}`;
35
+ ctx.stderr.write(bold(red(`Assert.Contains.${name}: assertion failed`)) +
36
+ "\n" +
37
+ ` ${red("✗")} ${message}\n`);
38
+ throw new InvokeError("ERR_ASSERTION_FAILED", `Assert.Contains "${name}": ${message}`);
39
+ }
40
+ if (ok) {
41
+ ctx.stdout.write(bold(green(`Assert.Contains.${name}: assertion passed`)) +
42
+ "\n" +
43
+ ` ${green("✓")} ${dim(JSON.stringify(actual))} ${dim("⊇")} ${dim(JSON.stringify(value))}\n`);
44
+ return true;
45
+ }
46
+ const message = `${JSON.stringify(actual)} does not contain ${kind} ${JSON.stringify(value)}`;
47
+ ctx.stderr.write(bold(red(`Assert.Contains.${name}: assertion failed`)) +
48
+ "\n" +
49
+ ` ${red("✗")} ${message}\n`);
50
+ throw new InvokeError("ERR_ASSERTION_FAILED", `Assert.Contains "${name}": ${message}`);
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Deep equality for the JSON-shaped values Telo stages typically pass
3
+ * around: primitives, plain objects (proto === Object.prototype or null),
4
+ * and arrays. Non-plain objects (Date, Map, Set, RegExp, class instances)
5
+ * are NOT structurally compared — only `Object.is`-equal instances pass.
6
+ *
7
+ * This is intentional: structurally comparing the empty `Object.keys()`
8
+ * of two distinct `new Date(…)` instances would silently return true,
9
+ * letting `Assert.Equals` pass for values that are clearly different.
10
+ * If a consumer genuinely needs Date / Map / Set equality, they should
11
+ * serialize first (e.g. `date.toISOString()`) and compare the strings.
12
+ */
13
+ export declare function deepEquals(a: unknown, b: unknown): boolean;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Deep equality for the JSON-shaped values Telo stages typically pass
3
+ * around: primitives, plain objects (proto === Object.prototype or null),
4
+ * and arrays. Non-plain objects (Date, Map, Set, RegExp, class instances)
5
+ * are NOT structurally compared — only `Object.is`-equal instances pass.
6
+ *
7
+ * This is intentional: structurally comparing the empty `Object.keys()`
8
+ * of two distinct `new Date(…)` instances would silently return true,
9
+ * letting `Assert.Equals` pass for values that are clearly different.
10
+ * If a consumer genuinely needs Date / Map / Set equality, they should
11
+ * serialize first (e.g. `date.toISOString()`) and compare the strings.
12
+ */
13
+ export function deepEquals(a, b) {
14
+ if (Object.is(a, b))
15
+ return true;
16
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
17
+ return false;
18
+ if (Array.isArray(a) || Array.isArray(b)) {
19
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
20
+ return false;
21
+ for (let i = 0; i < a.length; i++) {
22
+ if (!deepEquals(a[i], b[i]))
23
+ return false;
24
+ }
25
+ return true;
26
+ }
27
+ // Refuse structural compare for non-plain objects. Identity was already
28
+ // checked at the top; reaching here with a non-plain object means a !== b
29
+ // and we cannot meaningfully recurse into "fields" — empty keys would
30
+ // produce false positives.
31
+ if (!isPlainObject(a) || !isPlainObject(b))
32
+ return false;
33
+ const ao = a;
34
+ const bo = b;
35
+ const aKeys = Object.keys(ao);
36
+ const bKeys = Object.keys(bo);
37
+ if (aKeys.length !== bKeys.length)
38
+ return false;
39
+ for (const k of aKeys) {
40
+ if (!Object.prototype.hasOwnProperty.call(bo, k) || !deepEquals(ao[k], bo[k]))
41
+ return false;
42
+ }
43
+ return true;
44
+ }
45
+ function isPlainObject(v) {
46
+ const proto = Object.getPrototypeOf(v);
47
+ return proto === Object.prototype || proto === null;
48
+ }
@@ -0,0 +1,16 @@
1
+ import { ResourceContext } from "@telorun/sdk";
2
+ import { Static } from "@sinclair/typebox";
3
+ export declare const schema: import("@sinclair/typebox").TObject<{
4
+ metadata: import("@sinclair/typebox").TObject<{
5
+ name: import("@sinclair/typebox").TString;
6
+ }>;
7
+ }>;
8
+ type AssertManifest = Static<typeof schema>;
9
+ interface EqualsInput {
10
+ actual: unknown;
11
+ expected: unknown;
12
+ }
13
+ export declare function create(manifest: AssertManifest, ctx: ResourceContext): Promise<{
14
+ invoke: (input: EqualsInput) => boolean;
15
+ }>;
16
+ export {};
package/dist/equals.js ADDED
@@ -0,0 +1,29 @@
1
+ import { InvokeError } from "@telorun/sdk";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { createColors } from "./colors.js";
4
+ import { deepEquals } from "./deep-equals.js";
5
+ export const schema = Type.Object({
6
+ metadata: Type.Object({
7
+ name: Type.String(),
8
+ }),
9
+ });
10
+ export async function create(manifest, ctx) {
11
+ const { bold, red, green, dim } = createColors(ctx);
12
+ const name = manifest.metadata.name;
13
+ return {
14
+ invoke: (input) => {
15
+ const { actual, expected } = input ?? {};
16
+ if (deepEquals(actual, expected)) {
17
+ ctx.stdout.write(bold(green(`Assert.Equals.${name}: assertion passed`)) +
18
+ "\n" +
19
+ ` ${green("✓")} ${dim(JSON.stringify(actual))}\n`);
20
+ return true;
21
+ }
22
+ const message = `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`;
23
+ ctx.stderr.write(bold(red(`Assert.Equals.${name}: assertion failed`)) +
24
+ "\n" +
25
+ ` ${red("✗")} ${message}\n`);
26
+ throw new InvokeError("ERR_ASSERTION_FAILED", `Assert.Equals "${name}": ${message}`);
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,17 @@
1
+ import { ResourceContext } from "@telorun/sdk";
2
+ import { Static } from "@sinclair/typebox";
3
+ export declare const schema: import("@sinclair/typebox").TObject<{
4
+ metadata: import("@sinclair/typebox").TObject<{
5
+ name: import("@sinclair/typebox").TString;
6
+ }>;
7
+ }>;
8
+ type AssertManifest = Static<typeof schema>;
9
+ interface MatchesInput {
10
+ actual: unknown;
11
+ pattern: string;
12
+ flags?: string;
13
+ }
14
+ export declare function create(manifest: AssertManifest, ctx: ResourceContext): Promise<{
15
+ invoke: (input: MatchesInput) => boolean;
16
+ }>;
17
+ export {};
@@ -0,0 +1,45 @@
1
+ import { InvokeError } from "@telorun/sdk";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { createColors } from "./colors.js";
4
+ export const schema = Type.Object({
5
+ metadata: Type.Object({
6
+ name: Type.String(),
7
+ }),
8
+ });
9
+ export async function create(manifest, ctx) {
10
+ const { bold, red, green, dim } = createColors(ctx);
11
+ const name = manifest.metadata.name;
12
+ return {
13
+ invoke: (input) => {
14
+ const { actual, pattern, flags } = input ?? {};
15
+ if (typeof pattern !== "string") {
16
+ throw new InvokeError("ERR_INVALID_CONFIG", `Assert.Matches "${name}": 'pattern' must be a string`);
17
+ }
18
+ let regex;
19
+ try {
20
+ regex = new RegExp(pattern, flags ?? "");
21
+ }
22
+ catch (err) {
23
+ throw new InvokeError("ERR_INVALID_CONFIG", `Assert.Matches "${name}": invalid pattern — ${err instanceof Error ? err.message : String(err)}`);
24
+ }
25
+ if (typeof actual !== "string") {
26
+ const message = `actual must be a string; got ${typeof actual}`;
27
+ ctx.stderr.write(bold(red(`Assert.Matches.${name}: assertion failed`)) +
28
+ "\n" +
29
+ ` ${red("✗")} ${message}\n`);
30
+ throw new InvokeError("ERR_ASSERTION_FAILED", `Assert.Matches "${name}": ${message}`);
31
+ }
32
+ if (regex.test(actual)) {
33
+ ctx.stdout.write(bold(green(`Assert.Matches.${name}: assertion passed`)) +
34
+ "\n" +
35
+ ` ${green("✓")} ${dim(JSON.stringify(actual))} ${dim("~")} ${dim(regex.toString())}\n`);
36
+ return true;
37
+ }
38
+ const message = `${JSON.stringify(actual)} does not match ${regex.toString()}`;
39
+ ctx.stderr.write(bold(red(`Assert.Matches.${name}: assertion failed`)) +
40
+ "\n" +
41
+ ` ${red("✗")} ${message}\n`);
42
+ throw new InvokeError("ERR_ASSERTION_FAILED", `Assert.Matches "${name}": ${message}`);
43
+ },
44
+ };
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/assert",
3
- "version": "0.3.1",
3
+ "version": "0.5.1",
4
4
  "description": "Telo Assert module - Assertion resource kinds for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -23,7 +23,10 @@
23
23
  "./schema": "./src/schema.ts",
24
24
  "./events": "./src/events.ts",
25
25
  "./module-context": "./src/module-context.ts",
26
- "./manifest": "./src/manifest.ts"
26
+ "./manifest": "./src/manifest.ts",
27
+ "./equals": "./src/equals.ts",
28
+ "./matches": "./src/matches.ts",
29
+ "./contains": "./src/contains.ts"
27
30
  },
28
31
  "files": [
29
32
  "dist",
@@ -31,7 +34,7 @@
31
34
  ],
32
35
  "dependencies": {
33
36
  "@sinclair/typebox": "^0.34.48",
34
- "@telorun/analyzer": "0.6.0",
37
+ "@telorun/analyzer": "0.6.1",
35
38
  "@telorun/sdk": "0.7.0"
36
39
  },
37
40
  "devDependencies": {
package/src/colors.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { ResourceContext } from "@telorun/sdk";
2
+
3
+ /**
4
+ * Build the small ANSI color helpers used by every assert kind. Returns
5
+ * pass-through (uncolored) helpers when stderr isn't a TTY so piped /
6
+ * redirected output stays clean.
7
+ */
8
+ export function createColors(ctx: ResourceContext) {
9
+ const useColor = (ctx.stderr as { isTTY?: boolean }).isTTY ?? false;
10
+ const c = (code: string, text: string) =>
11
+ useColor ? `\x1b[${code}m${text}\x1b[0m` : text;
12
+ return {
13
+ bold: (t: string) => c("1", t),
14
+ red: (t: string) => c("31", t),
15
+ green: (t: string) => c("32", t),
16
+ dim: (t: string) => c("2", t),
17
+ };
18
+ }
@@ -0,0 +1,74 @@
1
+ import { InvokeError, ResourceContext } from "@telorun/sdk";
2
+ import { Static, Type } from "@sinclair/typebox";
3
+ import { createColors } from "./colors.js";
4
+ import { deepEquals } from "./deep-equals.js";
5
+
6
+ export const schema = Type.Object({
7
+ metadata: Type.Object({
8
+ name: Type.String(),
9
+ }),
10
+ });
11
+
12
+ type AssertManifest = Static<typeof schema>;
13
+
14
+ interface ContainsInput {
15
+ actual: unknown;
16
+ value: unknown;
17
+ }
18
+
19
+ export async function create(manifest: AssertManifest, ctx: ResourceContext) {
20
+ const { bold, red, green, dim } = createColors(ctx);
21
+ const name = manifest.metadata.name;
22
+
23
+ return {
24
+ invoke: (input: ContainsInput) => {
25
+ const { actual, value } = input ?? ({} as ContainsInput);
26
+
27
+ let ok = false;
28
+ let kind = "";
29
+ if (typeof actual === "string") {
30
+ if (typeof value !== "string") {
31
+ const message = `actual is a string but value is ${typeof value}; expected substring`;
32
+ ctx.stderr.write(
33
+ bold(red(`Assert.Contains.${name}: assertion failed`)) +
34
+ "\n" +
35
+ ` ${red("✗")} ${message}\n`,
36
+ );
37
+ throw new InvokeError(
38
+ "ERR_ASSERTION_FAILED",
39
+ `Assert.Contains "${name}": ${message}`,
40
+ );
41
+ }
42
+ kind = "substring";
43
+ ok = actual.includes(value);
44
+ } else if (Array.isArray(actual)) {
45
+ kind = "element";
46
+ ok = actual.some((item) => deepEquals(item, value));
47
+ } else {
48
+ const message = `actual must be string or array; got ${typeof actual}`;
49
+ ctx.stderr.write(
50
+ bold(red(`Assert.Contains.${name}: assertion failed`)) +
51
+ "\n" +
52
+ ` ${red("✗")} ${message}\n`,
53
+ );
54
+ throw new InvokeError("ERR_ASSERTION_FAILED", `Assert.Contains "${name}": ${message}`);
55
+ }
56
+
57
+ if (ok) {
58
+ ctx.stdout.write(
59
+ bold(green(`Assert.Contains.${name}: assertion passed`)) +
60
+ "\n" +
61
+ ` ${green("✓")} ${dim(JSON.stringify(actual))} ${dim("⊇")} ${dim(JSON.stringify(value))}\n`,
62
+ );
63
+ return true;
64
+ }
65
+ const message = `${JSON.stringify(actual)} does not contain ${kind} ${JSON.stringify(value)}`;
66
+ ctx.stderr.write(
67
+ bold(red(`Assert.Contains.${name}: assertion failed`)) +
68
+ "\n" +
69
+ ` ${red("✗")} ${message}\n`,
70
+ );
71
+ throw new InvokeError("ERR_ASSERTION_FAILED", `Assert.Contains "${name}": ${message}`);
72
+ },
73
+ };
74
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Deep equality for the JSON-shaped values Telo stages typically pass
3
+ * around: primitives, plain objects (proto === Object.prototype or null),
4
+ * and arrays. Non-plain objects (Date, Map, Set, RegExp, class instances)
5
+ * are NOT structurally compared — only `Object.is`-equal instances pass.
6
+ *
7
+ * This is intentional: structurally comparing the empty `Object.keys()`
8
+ * of two distinct `new Date(…)` instances would silently return true,
9
+ * letting `Assert.Equals` pass for values that are clearly different.
10
+ * If a consumer genuinely needs Date / Map / Set equality, they should
11
+ * serialize first (e.g. `date.toISOString()`) and compare the strings.
12
+ */
13
+ export function deepEquals(a: unknown, b: unknown): boolean {
14
+ if (Object.is(a, b)) return true;
15
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
16
+
17
+ if (Array.isArray(a) || Array.isArray(b)) {
18
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
19
+ for (let i = 0; i < a.length; i++) {
20
+ if (!deepEquals(a[i], b[i])) return false;
21
+ }
22
+ return true;
23
+ }
24
+
25
+ // Refuse structural compare for non-plain objects. Identity was already
26
+ // checked at the top; reaching here with a non-plain object means a !== b
27
+ // and we cannot meaningfully recurse into "fields" — empty keys would
28
+ // produce false positives.
29
+ if (!isPlainObject(a) || !isPlainObject(b)) return false;
30
+
31
+ const ao = a as Record<string, unknown>;
32
+ const bo = b as Record<string, unknown>;
33
+ const aKeys = Object.keys(ao);
34
+ const bKeys = Object.keys(bo);
35
+ if (aKeys.length !== bKeys.length) return false;
36
+ for (const k of aKeys) {
37
+ if (!Object.prototype.hasOwnProperty.call(bo, k) || !deepEquals(ao[k], bo[k])) return false;
38
+ }
39
+ return true;
40
+ }
41
+
42
+ function isPlainObject(v: object): boolean {
43
+ const proto = Object.getPrototypeOf(v);
44
+ return proto === Object.prototype || proto === null;
45
+ }
package/src/equals.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { InvokeError, ResourceContext } from "@telorun/sdk";
2
+ import { Static, Type } from "@sinclair/typebox";
3
+ import { createColors } from "./colors.js";
4
+ import { deepEquals } from "./deep-equals.js";
5
+
6
+ export const schema = Type.Object({
7
+ metadata: Type.Object({
8
+ name: Type.String(),
9
+ }),
10
+ });
11
+
12
+ type AssertManifest = Static<typeof schema>;
13
+
14
+ interface EqualsInput {
15
+ actual: unknown;
16
+ expected: unknown;
17
+ }
18
+
19
+ export async function create(manifest: AssertManifest, ctx: ResourceContext) {
20
+ const { bold, red, green, dim } = createColors(ctx);
21
+ const name = manifest.metadata.name;
22
+
23
+ return {
24
+ invoke: (input: EqualsInput) => {
25
+ const { actual, expected } = input ?? ({} as EqualsInput);
26
+ if (deepEquals(actual, expected)) {
27
+ ctx.stdout.write(
28
+ bold(green(`Assert.Equals.${name}: assertion passed`)) +
29
+ "\n" +
30
+ ` ${green("✓")} ${dim(JSON.stringify(actual))}\n`,
31
+ );
32
+ return true;
33
+ }
34
+ const message = `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`;
35
+ ctx.stderr.write(
36
+ bold(red(`Assert.Equals.${name}: assertion failed`)) +
37
+ "\n" +
38
+ ` ${red("✗")} ${message}\n`,
39
+ );
40
+ throw new InvokeError("ERR_ASSERTION_FAILED", `Assert.Equals "${name}": ${message}`);
41
+ },
42
+ };
43
+ }
package/src/matches.ts ADDED
@@ -0,0 +1,69 @@
1
+ import { InvokeError, ResourceContext } from "@telorun/sdk";
2
+ import { Static, Type } from "@sinclair/typebox";
3
+ import { createColors } from "./colors.js";
4
+
5
+ export const schema = Type.Object({
6
+ metadata: Type.Object({
7
+ name: Type.String(),
8
+ }),
9
+ });
10
+
11
+ type AssertManifest = Static<typeof schema>;
12
+
13
+ interface MatchesInput {
14
+ actual: unknown;
15
+ pattern: string;
16
+ flags?: string;
17
+ }
18
+
19
+ export async function create(manifest: AssertManifest, ctx: ResourceContext) {
20
+ const { bold, red, green, dim } = createColors(ctx);
21
+ const name = manifest.metadata.name;
22
+
23
+ return {
24
+ invoke: (input: MatchesInput) => {
25
+ const { actual, pattern, flags } = input ?? ({} as MatchesInput);
26
+
27
+ if (typeof pattern !== "string") {
28
+ throw new InvokeError(
29
+ "ERR_INVALID_CONFIG",
30
+ `Assert.Matches "${name}": 'pattern' must be a string`,
31
+ );
32
+ }
33
+ let regex: RegExp;
34
+ try {
35
+ regex = new RegExp(pattern, flags ?? "");
36
+ } catch (err) {
37
+ throw new InvokeError(
38
+ "ERR_INVALID_CONFIG",
39
+ `Assert.Matches "${name}": invalid pattern — ${err instanceof Error ? err.message : String(err)}`,
40
+ );
41
+ }
42
+
43
+ if (typeof actual !== "string") {
44
+ const message = `actual must be a string; got ${typeof actual}`;
45
+ ctx.stderr.write(
46
+ bold(red(`Assert.Matches.${name}: assertion failed`)) +
47
+ "\n" +
48
+ ` ${red("✗")} ${message}\n`,
49
+ );
50
+ throw new InvokeError("ERR_ASSERTION_FAILED", `Assert.Matches "${name}": ${message}`);
51
+ }
52
+ if (regex.test(actual)) {
53
+ ctx.stdout.write(
54
+ bold(green(`Assert.Matches.${name}: assertion passed`)) +
55
+ "\n" +
56
+ ` ${green("✓")} ${dim(JSON.stringify(actual))} ${dim("~")} ${dim(regex.toString())}\n`,
57
+ );
58
+ return true;
59
+ }
60
+ const message = `${JSON.stringify(actual)} does not match ${regex.toString()}`;
61
+ ctx.stderr.write(
62
+ bold(red(`Assert.Matches.${name}: assertion failed`)) +
63
+ "\n" +
64
+ ` ${red("✗")} ${message}\n`,
65
+ );
66
+ throw new InvokeError("ERR_ASSERTION_FAILED", `Assert.Matches "${name}": ${message}`);
67
+ },
68
+ };
69
+ }