glirastes 0.3.1 → 0.4.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/CHANGELOG.md +49 -1
- package/README.md +11 -3
- package/dist/cjs/server/adapters/nestjs/module/ai-tools.service.d.ts +2 -2
- package/dist/cjs/server/core/index.d.ts +1 -1
- package/dist/cjs/server/core/zod-compact.d.ts +1 -1
- package/dist/cjs/server/core/zod-compact.js +78 -55
- package/dist/cjs/server/testing/edge-cases.js +52 -101
- package/dist/cjs/server/testing/schema-consistency.js +5 -26
- package/dist/cjs/server/testing/zod-introspect.d.ts +38 -0
- package/dist/cjs/server/testing/zod-introspect.js +124 -0
- package/dist/cjs/types.d.ts +1 -1
- package/dist/cjs/types.js +1 -1
- package/dist/server/adapters/nestjs/module/ai-tools.service.d.ts +2 -2
- package/dist/server/core/index.d.ts +1 -1
- package/dist/server/core/zod-compact.d.ts +1 -1
- package/dist/server/core/zod-compact.js +78 -55
- package/dist/server/core/zod-compact.js.map +1 -1
- package/dist/server/testing/edge-cases.js +46 -95
- package/dist/server/testing/edge-cases.js.map +1 -1
- package/dist/server/testing/schema-consistency.js +5 -26
- package/dist/server/testing/schema-consistency.js.map +1 -1
- package/dist/server/testing/zod-introspect.d.ts +38 -0
- package/dist/server/testing/zod-introspect.js +113 -0
- package/dist/server/testing/zod-introspect.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/skills/integrating-glirastes-nextjs/SKILL.md +9 -3
- package/skills/maintaining-glirastes-tools/SKILL.md +6 -2
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Zod schema introspection helpers (Zod v4).
|
|
4
|
+
*
|
|
5
|
+
* Centralizes all access to Zod's internal `_zod.def` representation so the
|
|
6
|
+
* test-suite generators (`edge-cases.ts`, `schema-consistency.ts`) don't each
|
|
7
|
+
* reach into internals independently. If a future Zod version changes its
|
|
8
|
+
* internal shape, only this file — covered by `zod-introspect.test.ts` — needs
|
|
9
|
+
* updating, and the unit tests will catch a silent regression (introspection
|
|
10
|
+
* returning nothing) instead of letting it ship as a green-but-empty suite.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.getSchemaType = getSchemaType;
|
|
14
|
+
exports.isOptional = isOptional;
|
|
15
|
+
exports.unwrap = unwrap;
|
|
16
|
+
exports.getBaseType = getBaseType;
|
|
17
|
+
exports.getObjectShape = getObjectShape;
|
|
18
|
+
exports.getEnumValues = getEnumValues;
|
|
19
|
+
exports.getArrayElement = getArrayElement;
|
|
20
|
+
exports.getArrayMax = getArrayMax;
|
|
21
|
+
exports.hasStringFormat = hasStringFormat;
|
|
22
|
+
exports.isUuidString = isUuidString;
|
|
23
|
+
/** Read Zod v4's internal schema definition (`schema._zod.def`). */
|
|
24
|
+
function getDef(schema) {
|
|
25
|
+
return schema?._zod?.def;
|
|
26
|
+
}
|
|
27
|
+
/** Wrapper types that hold their inner schema in `def.innerType`. */
|
|
28
|
+
const WRAPPER_TYPES = new Set([
|
|
29
|
+
'optional',
|
|
30
|
+
'nullable',
|
|
31
|
+
'default',
|
|
32
|
+
'prefault',
|
|
33
|
+
'catch',
|
|
34
|
+
'readonly',
|
|
35
|
+
'nonoptional',
|
|
36
|
+
]);
|
|
37
|
+
/** The normalized Zod v4 type tag (e.g. 'object', 'string', 'array', 'enum'). */
|
|
38
|
+
function getSchemaType(schema) {
|
|
39
|
+
return getDef(schema)?.type;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* True if the field is optional, nullable, or has a default — i.e. an input
|
|
43
|
+
* is not strictly required to provide a concrete value. Matches the historical
|
|
44
|
+
* behaviour of the edge-case generator, which skips the "rejects missing"
|
|
45
|
+
* boundary test for any of these.
|
|
46
|
+
*/
|
|
47
|
+
function isOptional(schema) {
|
|
48
|
+
const type = getSchemaType(schema);
|
|
49
|
+
return (type === 'optional' ||
|
|
50
|
+
type === 'default' ||
|
|
51
|
+
type === 'prefault' ||
|
|
52
|
+
type === 'nullable');
|
|
53
|
+
}
|
|
54
|
+
/** Strip optional/nullable/default/pipe wrappers to the base schema. */
|
|
55
|
+
function unwrap(schema) {
|
|
56
|
+
const def = getDef(schema);
|
|
57
|
+
const type = def?.type;
|
|
58
|
+
if (WRAPPER_TYPES.has(type))
|
|
59
|
+
return unwrap(def.innerType);
|
|
60
|
+
if (type === 'pipe')
|
|
61
|
+
return unwrap(def.in);
|
|
62
|
+
return schema;
|
|
63
|
+
}
|
|
64
|
+
/** The base Zod type tag after unwrapping (e.g. an optional string → 'string'). */
|
|
65
|
+
function getBaseType(schema) {
|
|
66
|
+
return getSchemaType(unwrap(schema));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The field map of an object schema, unwrapping optional/default/pipe wrappers.
|
|
70
|
+
* Returns null for non-object schemas.
|
|
71
|
+
*/
|
|
72
|
+
function getObjectShape(schema) {
|
|
73
|
+
const def = getDef(schema);
|
|
74
|
+
if (!def)
|
|
75
|
+
return null;
|
|
76
|
+
if (def.type === 'object') {
|
|
77
|
+
return def.shape;
|
|
78
|
+
}
|
|
79
|
+
if (WRAPPER_TYPES.has(def.type) && def.innerType) {
|
|
80
|
+
return getObjectShape(def.innerType);
|
|
81
|
+
}
|
|
82
|
+
if (def.type === 'pipe' && def.in) {
|
|
83
|
+
return getObjectShape(def.in);
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/** The string members of an enum schema (after unwrapping). */
|
|
88
|
+
function getEnumValues(schema) {
|
|
89
|
+
const def = getDef(unwrap(schema));
|
|
90
|
+
if (def?.type !== 'enum')
|
|
91
|
+
return [];
|
|
92
|
+
return Object.values(def.entries).filter((v) => typeof v === 'string');
|
|
93
|
+
}
|
|
94
|
+
/** The element schema of an array (after unwrapping), or null. */
|
|
95
|
+
function getArrayElement(schema) {
|
|
96
|
+
const def = getDef(unwrap(schema));
|
|
97
|
+
return def?.type === 'array' ? def.element : null;
|
|
98
|
+
}
|
|
99
|
+
/** The `.max(n)` length constraint on an array schema, or null. */
|
|
100
|
+
function getArrayMax(schema) {
|
|
101
|
+
const def = getDef(unwrap(schema));
|
|
102
|
+
if (def?.type !== 'array')
|
|
103
|
+
return null;
|
|
104
|
+
for (const check of def.checks ?? []) {
|
|
105
|
+
const cd = check?._zod?.def;
|
|
106
|
+
if (cd?.check === 'max_length')
|
|
107
|
+
return cd.maximum;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
/** True if a string schema carries the given format check (e.g. 'uuid'). */
|
|
112
|
+
function hasStringFormat(schema, format) {
|
|
113
|
+
const def = getDef(unwrap(schema));
|
|
114
|
+
if (def?.type !== 'string')
|
|
115
|
+
return false;
|
|
116
|
+
return (def.checks ?? []).some((check) => {
|
|
117
|
+
const cd = check?._zod?.def;
|
|
118
|
+
return cd?.check === 'string_format' && cd?.format === format;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** True if a string schema is a UUID. */
|
|
122
|
+
function isUuidString(schema) {
|
|
123
|
+
return hasStringFormat(schema, 'uuid');
|
|
124
|
+
}
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -76,7 +76,7 @@ export interface UiActionTemplate {
|
|
|
76
76
|
*
|
|
77
77
|
* @param extraSchemas - Additional Zod schemas for app-specific action types
|
|
78
78
|
*/
|
|
79
|
-
export declare function createUiActionSchema(extraSchemas?: z.ZodTypeAny[]): z.ZodUnion<[z.
|
|
79
|
+
export declare function createUiActionSchema(extraSchemas?: z.ZodTypeAny[]): z.ZodUnion<[z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, ...z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>[]]>;
|
|
80
80
|
export type UiPatternType = 'filter-and-navigate' | 'open-detail' | 'open-dialog' | 'refresh' | 'toast';
|
|
81
81
|
export interface FilterAndNavigatePattern {
|
|
82
82
|
type: 'filter-and-navigate';
|
package/dist/cjs/types.js
CHANGED
|
@@ -29,7 +29,7 @@ function createUiActionSchema(extraSchemas = []) {
|
|
|
29
29
|
.object({
|
|
30
30
|
type: zod_1.z.literal('run-client-action'),
|
|
31
31
|
actionId: zod_1.z.string().min(1),
|
|
32
|
-
payload: zod_1.z.record(zod_1.z.unknown()).optional(),
|
|
32
|
+
payload: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
|
|
33
33
|
})
|
|
34
34
|
.strict();
|
|
35
35
|
const navigateSchema = zod_1.z
|
|
@@ -14,14 +14,14 @@ export declare class AiToolsService {
|
|
|
14
14
|
buildAiTools(context: ToolContext, options?: ToolToAiToolOptions): Promise<Record<string, {
|
|
15
15
|
_toolId: string;
|
|
16
16
|
description: string;
|
|
17
|
-
parameters: import("zod").ZodType<
|
|
17
|
+
parameters: import("zod").ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
|
|
18
18
|
needsApproval: ((input: unknown) => Promise<boolean>) | undefined;
|
|
19
19
|
execute: (input: unknown) => Promise<unknown>;
|
|
20
20
|
}>>;
|
|
21
21
|
buildAiToolsForIntent(intent: string, context: ToolContext, options?: ToolToAiToolOptions): Promise<Record<string, {
|
|
22
22
|
_toolId: string;
|
|
23
23
|
description: string;
|
|
24
|
-
parameters: import("zod").ZodType<
|
|
24
|
+
parameters: import("zod").ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
|
|
25
25
|
needsApproval: ((input: unknown) => Promise<boolean>) | undefined;
|
|
26
26
|
execute: (input: unknown) => Promise<unknown>;
|
|
27
27
|
}>>;
|
|
@@ -117,7 +117,7 @@ export interface ToolToAiToolOptions {
|
|
|
117
117
|
export declare function toolToAiTool(tool: Tool, context: ToolContext, toolName?: string, options?: ToolToAiToolOptions): {
|
|
118
118
|
_toolId: string;
|
|
119
119
|
description: string;
|
|
120
|
-
parameters: z.ZodType<
|
|
120
|
+
parameters: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
121
121
|
needsApproval: ((input: unknown) => Promise<boolean>) | undefined;
|
|
122
122
|
execute: (input: unknown) => Promise<unknown>;
|
|
123
123
|
};
|
|
@@ -9,7 +9,7 @@ import { z } from 'zod';
|
|
|
9
9
|
* zodToCompactDescription(z.object({
|
|
10
10
|
* tasks: z.array(z.object({ id: z.string(), title: z.string() })),
|
|
11
11
|
* total: z.number(),
|
|
12
|
-
* }))
|
|
12
|
+
* }), 5)
|
|
13
13
|
* // → '{ tasks: Array<{ id: string, title: string }>, total: number }'
|
|
14
14
|
*
|
|
15
15
|
* @param schema - Any Zod schema
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* zodToCompactDescription(z.object({
|
|
9
9
|
* tasks: z.array(z.object({ id: z.string(), title: z.string() })),
|
|
10
10
|
* total: z.number(),
|
|
11
|
-
* }))
|
|
11
|
+
* }), 5)
|
|
12
12
|
* // → '{ tasks: Array<{ id: string, title: string }>, total: number }'
|
|
13
13
|
*
|
|
14
14
|
* @param schema - Any Zod schema
|
|
@@ -17,45 +17,49 @@
|
|
|
17
17
|
export function zodToCompactDescription(schema, maxDepth = 3) {
|
|
18
18
|
return describeSchema(schema, 0, maxDepth);
|
|
19
19
|
}
|
|
20
|
+
/** Read Zod v4's internal schema definition (`schema._zod.def`). */
|
|
21
|
+
function getDef(schema) {
|
|
22
|
+
return schema?._zod?.def;
|
|
23
|
+
}
|
|
20
24
|
function describeSchema(schema, depth, maxDepth) {
|
|
21
25
|
if (depth >= maxDepth)
|
|
22
26
|
return '...';
|
|
23
|
-
const def = schema
|
|
24
|
-
const
|
|
25
|
-
switch (
|
|
26
|
-
case '
|
|
27
|
+
const def = getDef(schema);
|
|
28
|
+
const type = def?.type;
|
|
29
|
+
switch (type) {
|
|
30
|
+
case 'string':
|
|
27
31
|
return 'string';
|
|
28
|
-
case '
|
|
32
|
+
case 'number':
|
|
33
|
+
case 'bigint':
|
|
29
34
|
return 'number';
|
|
30
|
-
case '
|
|
35
|
+
case 'boolean':
|
|
31
36
|
return 'boolean';
|
|
32
|
-
case '
|
|
37
|
+
case 'date':
|
|
33
38
|
return 'string'; // Dates serialize as strings in JSON
|
|
34
|
-
case '
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
case 'literal':
|
|
40
|
+
// Zod v4 literals hold an array of allowed values.
|
|
41
|
+
return def.values
|
|
42
|
+
.map((v) => JSON.stringify(v))
|
|
43
|
+
.join(' | ');
|
|
44
|
+
case 'null':
|
|
37
45
|
return 'null';
|
|
38
|
-
case '
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
case 'undefined':
|
|
47
|
+
case 'void':
|
|
48
|
+
return type;
|
|
49
|
+
case 'any':
|
|
41
50
|
return 'any';
|
|
42
|
-
case '
|
|
51
|
+
case 'unknown':
|
|
43
52
|
return 'unknown';
|
|
44
|
-
case '
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
case '
|
|
51
|
-
return
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
.join(' | ');
|
|
55
|
-
case 'ZodArray':
|
|
56
|
-
return `Array<${describeSchema(def.type, depth + 1, maxDepth)}>`;
|
|
57
|
-
case 'ZodObject': {
|
|
58
|
-
const shape = def.shape();
|
|
53
|
+
case 'enum': {
|
|
54
|
+
// Zod v4 stores enum members in an `entries` record (covers both string
|
|
55
|
+
// enums and native enums). Emit the string values only.
|
|
56
|
+
const values = Object.values(def.entries).filter((v) => typeof v === 'string');
|
|
57
|
+
return values.map((v) => `"${v}"`).join(' | ');
|
|
58
|
+
}
|
|
59
|
+
case 'array':
|
|
60
|
+
return `Array<${describeSchema(def.element, depth + 1, maxDepth)}>`;
|
|
61
|
+
case 'object': {
|
|
62
|
+
const shape = def.shape;
|
|
59
63
|
const entries = Object.entries(shape);
|
|
60
64
|
if (entries.length === 0)
|
|
61
65
|
return '{}';
|
|
@@ -68,60 +72,79 @@ function describeSchema(schema, depth, maxDepth) {
|
|
|
68
72
|
});
|
|
69
73
|
return `{ ${fields.join(', ')} }`;
|
|
70
74
|
}
|
|
71
|
-
case '
|
|
75
|
+
case 'record':
|
|
72
76
|
return `Record<string, ${describeSchema(def.valueType, depth + 1, maxDepth)}>`;
|
|
73
|
-
case '
|
|
74
|
-
|
|
77
|
+
case 'union': {
|
|
78
|
+
// Covers both unions and discriminated unions in Zod v4.
|
|
75
79
|
const options = def.options;
|
|
76
80
|
return options
|
|
77
81
|
.map((o) => describeSchema(o, depth + 1, maxDepth))
|
|
78
82
|
.join(' | ');
|
|
79
83
|
}
|
|
80
|
-
case '
|
|
84
|
+
case 'tuple': {
|
|
81
85
|
const items = def.items;
|
|
82
86
|
return `[${items.map((i) => describeSchema(i, depth + 1, maxDepth)).join(', ')}]`;
|
|
83
87
|
}
|
|
84
|
-
case '
|
|
88
|
+
case 'nullable':
|
|
85
89
|
return `${describeSchema(def.innerType, depth, maxDepth)} | null`;
|
|
86
|
-
case '
|
|
87
|
-
case '
|
|
90
|
+
case 'optional':
|
|
91
|
+
case 'default':
|
|
92
|
+
case 'prefault':
|
|
93
|
+
case 'catch':
|
|
94
|
+
case 'readonly':
|
|
95
|
+
case 'nonoptional':
|
|
88
96
|
return describeSchema(def.innerType, depth, maxDepth);
|
|
89
|
-
case '
|
|
90
|
-
|
|
97
|
+
case 'pipe':
|
|
98
|
+
// Transforms / refinements become pipes in Zod v4; describe the input
|
|
99
|
+
// side, matching the pre-v4 behaviour of describing the base schema.
|
|
100
|
+
return describeSchema(def.in, depth, maxDepth);
|
|
101
|
+
case 'lazy':
|
|
102
|
+
return describeSchema(def.getter(), depth, maxDepth);
|
|
91
103
|
default:
|
|
92
104
|
return 'unknown';
|
|
93
105
|
}
|
|
94
106
|
}
|
|
95
|
-
/** Unwrap
|
|
107
|
+
/** Unwrap optional / nullable / default / pipe wrappers to the inner schema. */
|
|
96
108
|
function unwrap(schema) {
|
|
97
|
-
const def = schema
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
const def = getDef(schema);
|
|
110
|
+
const type = def?.type;
|
|
111
|
+
if (type === 'optional' ||
|
|
112
|
+
type === 'nullable' ||
|
|
113
|
+
type === 'default' ||
|
|
114
|
+
type === 'prefault' ||
|
|
115
|
+
type === 'catch' ||
|
|
116
|
+
type === 'readonly' ||
|
|
117
|
+
type === 'nonoptional') {
|
|
102
118
|
return unwrap(def.innerType);
|
|
103
119
|
}
|
|
104
|
-
if (
|
|
105
|
-
return unwrap(def.
|
|
120
|
+
if (type === 'pipe') {
|
|
121
|
+
return unwrap(def.in);
|
|
106
122
|
}
|
|
107
123
|
return schema;
|
|
108
124
|
}
|
|
109
125
|
function isOptional(schema) {
|
|
110
|
-
const def = schema
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
126
|
+
const def = getDef(schema);
|
|
127
|
+
const type = def?.type;
|
|
128
|
+
if (type === 'optional' || type === 'default' || type === 'prefault') {
|
|
113
129
|
return true;
|
|
114
|
-
|
|
130
|
+
}
|
|
131
|
+
if (type === 'nullable' || type === 'readonly' || type === 'catch') {
|
|
115
132
|
return isOptional(def.innerType);
|
|
133
|
+
}
|
|
116
134
|
return false;
|
|
117
135
|
}
|
|
118
136
|
function isNullable(schema) {
|
|
119
|
-
const def = schema
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
137
|
+
const def = getDef(schema);
|
|
138
|
+
const type = def?.type;
|
|
139
|
+
if (type === 'nullable')
|
|
122
140
|
return true;
|
|
123
|
-
if (
|
|
141
|
+
if (type === 'optional' ||
|
|
142
|
+
type === 'default' ||
|
|
143
|
+
type === 'prefault' ||
|
|
144
|
+
type === 'readonly' ||
|
|
145
|
+
type === 'catch') {
|
|
124
146
|
return isNullable(def.innerType);
|
|
147
|
+
}
|
|
125
148
|
return false;
|
|
126
149
|
}
|
|
127
150
|
//# sourceMappingURL=zod-compact.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zod-compact.js","sourceRoot":"","sources":["../../../src/server/core/zod-compact.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAoB,EACpB,QAAQ,GAAG,CAAC;IAEZ,OAAO,cAAc,CAAC,MAAM,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC;AAC7C,CAAC;AAED,SAAS,cAAc,CACrB,MAAoB,EACpB,KAAa,EACb,QAAgB;IAEhB,IAAI,KAAK,IAAI,QAAQ;QAAE,OAAO,KAAK,CAAC;IAEpC,MAAM,GAAG,
|
|
1
|
+
{"version":3,"file":"zod-compact.js","sourceRoot":"","sources":["../../../src/server/core/zod-compact.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { getArrayElement, getArrayMax, getBaseType, getEnumValues, getObjectShape, isOptional, isUuidString, unwrap, } from './zod-introspect.js';
|
|
2
3
|
/**
|
|
3
4
|
* Auto-generates edge case tests from tool Zod schemas + custom cases.
|
|
4
5
|
*
|
|
@@ -86,59 +87,26 @@ export function runEdgeCaseTest(toolRegistry, options) {
|
|
|
86
87
|
*/
|
|
87
88
|
function hasTestableFields(shape) {
|
|
88
89
|
for (const [, fieldSchema] of Object.entries(shape)) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return true; // Required field → generates "rejects missing" test
|
|
90
|
+
if (!isOptional(fieldSchema))
|
|
91
|
+
return true; // Required field → "rejects missing" test
|
|
92
92
|
const inner = unwrap(fieldSchema);
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
93
|
+
const type = getBaseType(inner);
|
|
94
|
+
if (type === 'enum' && getEnumValues(inner).length > 0)
|
|
95
|
+
return true;
|
|
96
|
+
if (type === 'array' && getArrayMax(inner) !== null)
|
|
97
|
+
return true;
|
|
98
|
+
if (type === 'string' && isUuidString(inner))
|
|
95
99
|
return true;
|
|
96
|
-
if (typeName === 'ZodArray') {
|
|
97
|
-
const checks = inner._def.checks ?? [];
|
|
98
|
-
if (checks.some((c) => c.kind === 'max'))
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
if (typeName === 'ZodString') {
|
|
102
|
-
const checks = inner._def.checks ?? [];
|
|
103
|
-
if (checks.some((c) => c.kind === 'uuid'))
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
100
|
}
|
|
107
101
|
return false;
|
|
108
102
|
}
|
|
109
|
-
function getObjectShape(schema) {
|
|
110
|
-
const def = schema._def;
|
|
111
|
-
if (def.typeName === 'ZodObject' && typeof def.shape === 'function') {
|
|
112
|
-
return def.shape();
|
|
113
|
-
}
|
|
114
|
-
// Unwrap ZodEffects (e.g. .refine(), .transform())
|
|
115
|
-
if (def.typeName === 'ZodEffects' && def.innerType) {
|
|
116
|
-
return getObjectShape(def.innerType);
|
|
117
|
-
}
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
function isOptional(schema) {
|
|
121
|
-
const name = schema._def.typeName;
|
|
122
|
-
return name === 'ZodOptional' || name === 'ZodNullable' || name === 'ZodDefault';
|
|
123
|
-
}
|
|
124
|
-
function unwrap(schema) {
|
|
125
|
-
const name = schema._def.typeName;
|
|
126
|
-
if ((name === 'ZodOptional' || name === 'ZodNullable' || name === 'ZodDefault') &&
|
|
127
|
-
schema._def.innerType) {
|
|
128
|
-
return unwrap(schema._def.innerType);
|
|
129
|
-
}
|
|
130
|
-
if (name === 'ZodEffects' && schema._def.innerType) {
|
|
131
|
-
return unwrap(schema._def.innerType);
|
|
132
|
-
}
|
|
133
|
-
return schema;
|
|
134
|
-
}
|
|
135
103
|
function generateShapeTests(shape, parentSchema) {
|
|
136
104
|
// Build a valid base object with placeholder values
|
|
137
105
|
const baseInput = buildMinimalValidInput(shape);
|
|
138
106
|
for (const [fieldName, fieldSchema] of Object.entries(shape)) {
|
|
139
107
|
const optional = isOptional(fieldSchema);
|
|
140
108
|
const inner = unwrap(fieldSchema);
|
|
141
|
-
const
|
|
109
|
+
const type = getBaseType(inner);
|
|
142
110
|
// Required field: test missing
|
|
143
111
|
if (!optional) {
|
|
144
112
|
it(`rejects missing ${fieldName} (required)`, () => {
|
|
@@ -149,9 +117,9 @@ function generateShapeTests(shape, parentSchema) {
|
|
|
149
117
|
});
|
|
150
118
|
}
|
|
151
119
|
// Enum: test each valid value + invalid
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
for (const val of
|
|
120
|
+
const enumValues = type === 'enum' ? getEnumValues(inner) : [];
|
|
121
|
+
if (enumValues.length > 0) {
|
|
122
|
+
for (const val of enumValues) {
|
|
155
123
|
it(`accepts valid ${fieldName} enum: ${val}`, () => {
|
|
156
124
|
const result = parentSchema.safeParse({ ...baseInput, [fieldName]: val });
|
|
157
125
|
expect(result.success).toBe(true);
|
|
@@ -163,11 +131,9 @@ function generateShapeTests(shape, parentSchema) {
|
|
|
163
131
|
});
|
|
164
132
|
}
|
|
165
133
|
// Array with max: test at limit and over limit
|
|
166
|
-
if (
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
if (maxCheck && typeof maxCheck.value === 'number') {
|
|
170
|
-
const max = maxCheck.value;
|
|
134
|
+
if (type === 'array') {
|
|
135
|
+
const max = getArrayMax(inner);
|
|
136
|
+
if (max !== null) {
|
|
171
137
|
const arrayFillValue = getArrayElementPlaceholder(inner);
|
|
172
138
|
it(`accepts ${fieldName} at max(${max})`, () => {
|
|
173
139
|
const result = parentSchema.safeParse({
|
|
@@ -186,31 +152,24 @@ function generateShapeTests(shape, parentSchema) {
|
|
|
186
152
|
}
|
|
187
153
|
}
|
|
188
154
|
// String with UUID check
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const result = parentSchema.safeParse({ ...baseInput, [fieldName]: 'not-a-uuid' });
|
|
195
|
-
expect(result.success).toBe(false);
|
|
196
|
-
});
|
|
197
|
-
}
|
|
155
|
+
if (type === 'string' && isUuidString(inner)) {
|
|
156
|
+
it(`rejects invalid ${fieldName} UUID: "not-a-uuid"`, () => {
|
|
157
|
+
const result = parentSchema.safeParse({ ...baseInput, [fieldName]: 'not-a-uuid' });
|
|
158
|
+
expect(result.success).toBe(false);
|
|
159
|
+
});
|
|
198
160
|
}
|
|
199
161
|
}
|
|
200
162
|
}
|
|
201
163
|
function getArrayElementPlaceholder(arraySchema) {
|
|
202
|
-
const elementSchema = arraySchema
|
|
164
|
+
const elementSchema = getArrayElement(arraySchema);
|
|
203
165
|
if (elementSchema) {
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return '00000000-0000-0000-0000-000000000000';
|
|
210
|
-
}
|
|
211
|
-
return 'test-id';
|
|
166
|
+
const elType = getBaseType(elementSchema);
|
|
167
|
+
if (elType === 'string') {
|
|
168
|
+
return isUuidString(elementSchema)
|
|
169
|
+
? '00000000-0000-0000-0000-000000000000'
|
|
170
|
+
: 'test-id';
|
|
212
171
|
}
|
|
213
|
-
if (elType === '
|
|
172
|
+
if (elType === 'number')
|
|
214
173
|
return 1;
|
|
215
174
|
}
|
|
216
175
|
return 'test-id';
|
|
@@ -221,41 +180,33 @@ function buildMinimalValidInput(shape) {
|
|
|
221
180
|
if (isOptional(schema))
|
|
222
181
|
continue;
|
|
223
182
|
const inner = unwrap(schema);
|
|
224
|
-
const
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
input[name] = 'test-value';
|
|
232
|
-
}
|
|
183
|
+
const type = getBaseType(inner);
|
|
184
|
+
if (type === 'string') {
|
|
185
|
+
input[name] = isUuidString(inner)
|
|
186
|
+
? '00000000-0000-0000-0000-000000000000'
|
|
187
|
+
: 'test-value';
|
|
233
188
|
}
|
|
234
|
-
else if (
|
|
189
|
+
else if (type === 'number') {
|
|
235
190
|
input[name] = 1;
|
|
236
191
|
}
|
|
237
|
-
else if (
|
|
192
|
+
else if (type === 'boolean') {
|
|
238
193
|
input[name] = false;
|
|
239
194
|
}
|
|
240
|
-
else if (
|
|
241
|
-
|
|
195
|
+
else if (type === 'enum') {
|
|
196
|
+
const values = getEnumValues(inner);
|
|
197
|
+
input[name] = values.length > 0 ? values[0] : 'test';
|
|
242
198
|
}
|
|
243
|
-
else if (
|
|
199
|
+
else if (type === 'array') {
|
|
244
200
|
// Introspect array element type to generate valid placeholders
|
|
245
|
-
const elementSchema = inner
|
|
201
|
+
const elementSchema = getArrayElement(inner);
|
|
246
202
|
if (elementSchema) {
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
input[name] = ['00000000-0000-0000-0000-000000000000'];
|
|
253
|
-
}
|
|
254
|
-
else {
|
|
255
|
-
input[name] = ['test-id'];
|
|
256
|
-
}
|
|
203
|
+
const elType = getBaseType(elementSchema);
|
|
204
|
+
if (elType === 'string') {
|
|
205
|
+
input[name] = isUuidString(elementSchema)
|
|
206
|
+
? ['00000000-0000-0000-0000-000000000000']
|
|
207
|
+
: ['test-id'];
|
|
257
208
|
}
|
|
258
|
-
else if (elType === '
|
|
209
|
+
else if (elType === 'number') {
|
|
259
210
|
input[name] = [1];
|
|
260
211
|
}
|
|
261
212
|
else {
|