@liquidmetal-ai/precip 1.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 (78) hide show
  1. package/.prettierrc +9 -0
  2. package/CHANGELOG.md +8 -0
  3. package/eslint.config.mjs +28 -0
  4. package/package.json +53 -0
  5. package/src/engine/agent.ts +478 -0
  6. package/src/engine/llm-provider.test.ts +275 -0
  7. package/src/engine/llm-provider.ts +330 -0
  8. package/src/engine/stream-parser.ts +170 -0
  9. package/src/index.ts +142 -0
  10. package/src/mounts/mount-manager.test.ts +516 -0
  11. package/src/mounts/mount-manager.ts +327 -0
  12. package/src/mounts/mount-registry.ts +196 -0
  13. package/src/mounts/zod-to-string.test.ts +154 -0
  14. package/src/mounts/zod-to-string.ts +213 -0
  15. package/src/presets/agent-tools.ts +57 -0
  16. package/src/presets/index.ts +5 -0
  17. package/src/sandbox/README.md +1321 -0
  18. package/src/sandbox/bridges/README.md +571 -0
  19. package/src/sandbox/bridges/actor.test.ts +229 -0
  20. package/src/sandbox/bridges/actor.ts +195 -0
  21. package/src/sandbox/bridges/bridge-fixes.test.ts +614 -0
  22. package/src/sandbox/bridges/bucket.test.ts +300 -0
  23. package/src/sandbox/bridges/cleanup-reproduction.test.ts +225 -0
  24. package/src/sandbox/bridges/console-multiple.test.ts +187 -0
  25. package/src/sandbox/bridges/console.test.ts +157 -0
  26. package/src/sandbox/bridges/console.ts +122 -0
  27. package/src/sandbox/bridges/fetch.ts +93 -0
  28. package/src/sandbox/bridges/index.ts +78 -0
  29. package/src/sandbox/bridges/readable-stream.ts +323 -0
  30. package/src/sandbox/bridges/response.test.ts +154 -0
  31. package/src/sandbox/bridges/response.ts +123 -0
  32. package/src/sandbox/bridges/review-fixes.test.ts +331 -0
  33. package/src/sandbox/bridges/search.test.ts +475 -0
  34. package/src/sandbox/bridges/search.ts +264 -0
  35. package/src/sandbox/bridges/shared/body-methods.ts +93 -0
  36. package/src/sandbox/bridges/shared/cleanup.ts +112 -0
  37. package/src/sandbox/bridges/shared/convert.ts +76 -0
  38. package/src/sandbox/bridges/shared/headers.ts +181 -0
  39. package/src/sandbox/bridges/shared/index.ts +36 -0
  40. package/src/sandbox/bridges/shared/json-helpers.ts +77 -0
  41. package/src/sandbox/bridges/shared/path-parser.ts +109 -0
  42. package/src/sandbox/bridges/shared/promise-helper.ts +108 -0
  43. package/src/sandbox/bridges/shared/registry-setup.ts +84 -0
  44. package/src/sandbox/bridges/shared/response-object.ts +280 -0
  45. package/src/sandbox/bridges/shared/result-builder.ts +130 -0
  46. package/src/sandbox/bridges/shared/scope-helpers.ts +44 -0
  47. package/src/sandbox/bridges/shared/stream-reader.ts +90 -0
  48. package/src/sandbox/bridges/storage-bridge.test.ts +893 -0
  49. package/src/sandbox/bridges/storage.ts +421 -0
  50. package/src/sandbox/bridges/text-decoder.ts +190 -0
  51. package/src/sandbox/bridges/text-encoder.ts +102 -0
  52. package/src/sandbox/bridges/types.ts +39 -0
  53. package/src/sandbox/bridges/utils.ts +123 -0
  54. package/src/sandbox/index.ts +6 -0
  55. package/src/sandbox/quickjs-wasm.d.ts +9 -0
  56. package/src/sandbox/sandbox.test.ts +191 -0
  57. package/src/sandbox/sandbox.ts +831 -0
  58. package/src/sandbox/test-helper.ts +43 -0
  59. package/src/sandbox/test-mocks.ts +154 -0
  60. package/src/sandbox/user-stream.test.ts +77 -0
  61. package/src/skills/frontmatter.test.ts +305 -0
  62. package/src/skills/frontmatter.ts +200 -0
  63. package/src/skills/index.ts +9 -0
  64. package/src/skills/skills-loader.test.ts +237 -0
  65. package/src/skills/skills-loader.ts +200 -0
  66. package/src/tools/actor-storage-tools.ts +250 -0
  67. package/src/tools/code-tools.test.ts +199 -0
  68. package/src/tools/code-tools.ts +444 -0
  69. package/src/tools/file-tools.ts +206 -0
  70. package/src/tools/registry.ts +125 -0
  71. package/src/tools/script-tools.ts +145 -0
  72. package/src/tools/smartbucket-tools.ts +203 -0
  73. package/src/tools/sql-tools.ts +213 -0
  74. package/src/tools/tool-factory.ts +119 -0
  75. package/src/types.ts +512 -0
  76. package/tsconfig.eslint.json +5 -0
  77. package/tsconfig.json +15 -0
  78. package/vitest.config.ts +33 -0
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Mount Manager - Routes path prefixes to Raindrop resources
3
+ *
4
+ * Supports mounting Buckets, SmartBuckets, KvCache, SqlDatabase, and Actors with path prefixes.
5
+ * All mounts require an explicit type field - no automatic type detection.
6
+ *
7
+ * Examples:
8
+ * - @user-data/profile.json → USER_BUCKET.get("profile.json")
9
+ * - @logs/2024/jan.txt → LOG_BUCKET.get("2024/jan.txt")
10
+ * - @knowledge/query → SMART_BUCKET.search({input: "query"})
11
+ * - sql:analytics → ANALYTICS_DB
12
+ * - kv:cache → CACHE_KV
13
+ * - actor:session → SESSION_ACTOR (accessed via actor.get('session', 'instance-name'))
14
+ */
15
+
16
+ import type {
17
+ Logger,
18
+ MountConfig,
19
+ MountInfo,
20
+ ActorMountInfo,
21
+ ParsedPath,
22
+ MountProvider
23
+ } from '../types.js';
24
+ import { zodToTypeString } from './zod-to-string.js';
25
+ import { createMountInfo, registerStandardMountTypes } from './mount-registry.js';
26
+
27
+ // Initialize standard mount types on module load
28
+ registerStandardMountTypes();
29
+
30
+ export class MountManager implements MountProvider {
31
+ private mounts = new Map<string, MountInfo>();
32
+ private logger?: Logger;
33
+
34
+ constructor(config: MountConfig = {}, logger?: Logger) {
35
+ this.logger = logger;
36
+
37
+ for (const [name, input] of Object.entries(config)) {
38
+ this.mountConfig(name, input);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Mount a configuration (discriminated union with explicit type)
44
+ */
45
+ private mountConfig(name: string, config: MountConfig[string]): void {
46
+ // Use registry to create mount info
47
+ const info = createMountInfo(name, config);
48
+ this.mounts.set(name, info);
49
+ }
50
+
51
+ /**
52
+ * Unmount a resource
53
+ */
54
+ unmount(name: string): boolean {
55
+ return this.mounts.delete(name);
56
+ }
57
+
58
+ /**
59
+ * Get mount by name
60
+ */
61
+ getMount(name: string): MountInfo | undefined {
62
+ return this.mounts.get(name);
63
+ }
64
+
65
+ /**
66
+ * Get all mounts
67
+ */
68
+ getAllMounts(): Map<string, MountInfo> {
69
+ return new Map(this.mounts);
70
+ }
71
+
72
+ /**
73
+ * Parse a path to extract mount name and resource path
74
+ *
75
+ * Format: /mount-name/path/to/resource
76
+ * Examples:
77
+ * /uploads/images/photo.jpg → { mountName: 'uploads', path: 'images/photo.jpg' }
78
+ * /cache/user:123 → { mountName: 'cache', path: 'user:123' }
79
+ * /analytics → { mountName: 'analytics', path: '' }
80
+ */
81
+ parsePath(fullPath: string): ParsedPath {
82
+ // Must start with /
83
+ if (!fullPath.startsWith('/')) {
84
+ throw new Error(`Invalid path: ${fullPath}. Paths must start with /`);
85
+ }
86
+
87
+ const withoutSlash = fullPath.slice(1);
88
+
89
+ if (withoutSlash === '') {
90
+ throw new Error(`Invalid path: ${fullPath}. Mount name required.`);
91
+ }
92
+
93
+ const slashIndex = withoutSlash.indexOf('/');
94
+
95
+ if (slashIndex === -1) {
96
+ // Just /mount-name with no path
97
+ return { mountName: withoutSlash, path: '', fullPath };
98
+ }
99
+
100
+ const mountName = withoutSlash.slice(0, slashIndex);
101
+ const path = withoutSlash.slice(slashIndex + 1);
102
+
103
+ return { mountName, path, fullPath };
104
+ }
105
+
106
+ /**
107
+ * Resolve a path to a mount and resource path
108
+ */
109
+ resolvePath(fullPath: string): { mount: MountInfo; path: string } {
110
+ const parsed = this.parsePath(fullPath);
111
+ const mount = this.mounts.get(parsed.mountName);
112
+
113
+ if (!mount) {
114
+ throw new Error(
115
+ `Mount not found: ${parsed.mountName}. Available mounts: ${Array.from(this.mounts.keys()).join(', ')}`
116
+ );
117
+ }
118
+
119
+ return { mount, path: parsed.path };
120
+ }
121
+
122
+ /**
123
+ * List all mounts by type
124
+ */
125
+ getMountsByType<T extends MountInfo['type']>(type: T): MountInfo[] {
126
+ return Array.from(this.mounts.values()).filter(m => m.type === type);
127
+ }
128
+
129
+ /**
130
+ * Get all actor mounts with their schemas
131
+ */
132
+ getActorMounts(): Map<string, ActorMountInfo> {
133
+ const actorMounts = new Map<string, ActorMountInfo>();
134
+ for (const [name, mount] of this.mounts) {
135
+ if (mount.type === 'actor') {
136
+ actorMounts.set(name, mount);
137
+ }
138
+ }
139
+ return actorMounts;
140
+ }
141
+
142
+ /**
143
+ * Create sandbox globals from mounts
144
+ * Returns globals that can be injected into QuickJS sandbox
145
+ */
146
+ createSandboxGlobals(): Record<string, any> {
147
+ const globals: Record<string, any> = {};
148
+
149
+ // Create bucket global with all bucket and smartbucket mounts
150
+ const bucketMounts = [
151
+ ...this.getMountsByType('bucket'),
152
+ ...this.getMountsByType('smartbucket')
153
+ ];
154
+ if (bucketMounts.length > 0) {
155
+ globals.bucket = {
156
+ // These will be replaced with actual async functions by the tool
157
+ _mounts: bucketMounts.map(m => ({ name: m.name, type: m.type })),
158
+ _type: 'bucket'
159
+ };
160
+ }
161
+
162
+ // Create kv global with all KV mounts
163
+ const kvMounts = this.getMountsByType('kv');
164
+ if (kvMounts.length > 0) {
165
+ globals.kv = {
166
+ _mounts: kvMounts.map(m => m.name),
167
+ _type: 'kv'
168
+ };
169
+ }
170
+
171
+ // Create db global with all database mounts
172
+ const dbMounts = this.getMountsByType('database');
173
+ if (dbMounts.length > 0) {
174
+ globals.db = {
175
+ _mounts: dbMounts.map(m => m.name),
176
+ _type: 'database'
177
+ };
178
+ }
179
+
180
+ // Create actor global placeholder (will be replaced by actor bridge)
181
+ const actorMounts = this.getActorMounts();
182
+ if (actorMounts.size > 0) {
183
+ globals.actor = {
184
+ _mounts: Array.from(actorMounts.entries()).map(([name, mount]) => ({
185
+ name,
186
+ methods: Object.keys(mount.methods)
187
+ })),
188
+ _type: 'actor'
189
+ };
190
+ }
191
+
192
+ // Create actor-storage placeholder (will be handled by storage bridge)
193
+ const actorStorageMounts = this.getMountsByType('actor-storage');
194
+ if (actorStorageMounts.length > 0) {
195
+ globals.actorStorage = {
196
+ _mounts: actorStorageMounts.map(m => m.name),
197
+ _type: 'actor-storage'
198
+ };
199
+ }
200
+
201
+ return globals;
202
+ }
203
+
204
+ /**
205
+ * Generate a formatted description of all mounts for inclusion in system prompts
206
+ * Returns a structured string that describes available resources
207
+ */
208
+ getMountsDescription(): string {
209
+ if (this.mounts.size === 0) {
210
+ return '';
211
+ }
212
+
213
+ const lines: string[] = [];
214
+
215
+ // Header
216
+ lines.push('');
217
+ lines.push('AVAILABLE STORAGE:');
218
+ lines.push('');
219
+ lines.push('All paths start with /<mount-name>/. Use these functions:');
220
+ lines.push('');
221
+
222
+ // Group by type
223
+ const bucketMounts = this.getMountsByType('bucket');
224
+ const smartbucketMounts = this.getMountsByType('smartbucket');
225
+ const kvMounts = this.getMountsByType('kv');
226
+ const dbMounts = this.getMountsByType('database');
227
+ const actorMounts = Array.from(this.getActorMounts().values());
228
+ const actorStorageMounts = this.getMountsByType('actor-storage');
229
+
230
+ // File storage (buckets and smartbuckets)
231
+ const allFileMounts = [...bucketMounts, ...smartbucketMounts];
232
+ if (allFileMounts.length > 0) {
233
+ lines.push('## File Storage');
234
+ lines.push('Durable object storage for files and binary data.');
235
+ lines.push('read() returns Response-like with .body (stream), .size, .uploaded');
236
+ lines.push('write() accepts optional { contentType, customMetadata }');
237
+ lines.push('');
238
+ for (const mount of allFileMounts) {
239
+ const desc = mount.description ? ` - ${mount.description}` : '';
240
+ const mode = mount.mode === 'rw' ? ' (read-write)' : ' (read-only)';
241
+ const isSmartBucket = mount.type === 'smartbucket';
242
+ lines.push(` /${mount.name}/...${desc}${mode}${isSmartBucket ? ' [searchable]' : ''}`);
243
+ }
244
+ lines.push('');
245
+ }
246
+
247
+ // KV storage
248
+ if (kvMounts.length > 0) {
249
+ lines.push('## Key-Value Cache');
250
+ lines.push('Ephemeral key-value storage with optional expiration. Eventually consistent.');
251
+ lines.push('write() accepts optional { ttl, expiration, metadata }');
252
+ lines.push('');
253
+ for (const mount of kvMounts) {
254
+ const desc = mount.description ? ` - ${mount.description}` : '';
255
+ const mode = mount.mode === 'rw' ? ' (read-write)' : ' (read-only)';
256
+ lines.push(` /${mount.name}/<key>${desc}${mode}`);
257
+ }
258
+ lines.push('');
259
+ }
260
+
261
+ // Actor storage
262
+ if (actorStorageMounts.length > 0) {
263
+ lines.push('## Actor State');
264
+ lines.push('Strongly consistent, transactional storage local to this actor instance.');
265
+ lines.push('list() supports range queries: { start, end, startAfter, reverse, limit }');
266
+ lines.push('');
267
+ for (const mount of actorStorageMounts) {
268
+ const desc = mount.description ? ` - ${mount.description}` : '';
269
+ const mode = mount.mode === 'rw' ? ' (read-write)' : ' (read-only)';
270
+ lines.push(` /${mount.name}/<key>${desc}${mode}`);
271
+ }
272
+ lines.push('');
273
+ }
274
+
275
+ // Databases
276
+ if (dbMounts.length > 0) {
277
+ lines.push('## Databases');
278
+ lines.push('Use query() and execute() with these mounts:');
279
+ lines.push('');
280
+ for (const mount of dbMounts) {
281
+ const desc = mount.description ? ` - ${mount.description}` : '';
282
+ const mode = mount.mode === 'rw' ? ' (read-write)' : ' (read-only)';
283
+ lines.push(` /${mount.name}${desc}${mode}`);
284
+ }
285
+ lines.push('');
286
+ }
287
+
288
+ // Actors
289
+ if (actorMounts.length > 0) {
290
+ lines.push('## Actors');
291
+ lines.push('Use actor(path, instanceId) to get a stub, then call methods.');
292
+ lines.push('');
293
+
294
+ for (const mount of actorMounts) {
295
+ const desc = mount.description ? ` - ${mount.description}` : '';
296
+ lines.push(`### /${mount.name}${desc}`);
297
+ lines.push('');
298
+ lines.push('Methods:');
299
+
300
+ for (const [methodName, schema] of Object.entries(mount.methods)) {
301
+ // Format tuple params as a function signature using .describe() for names
302
+ const tupleItems = (schema.params as any)._def?.items ?? [];
303
+ const paramsStr = tupleItems.length === 0
304
+ ? ''
305
+ : tupleItems.map((item: any, i: number) => {
306
+ const name = item.description ?? `arg${i}`;
307
+ return `${name}: ${zodToTypeString(item)}`;
308
+ }).join(', ');
309
+ const returnsStr = zodToTypeString(schema.returns);
310
+ lines.push(` ${methodName}(${paramsStr}) → ${returnsStr}`);
311
+ lines.push(` ${schema.description}`);
312
+ }
313
+
314
+ lines.push('');
315
+ lines.push('Example:');
316
+ lines.push(` const stub = await actor("/${mount.name}", "<instance-id>");`);
317
+ const firstMethod = Object.keys(mount.methods)[0];
318
+ if (firstMethod) {
319
+ lines.push(` const result = await stub.${firstMethod}(...);`);
320
+ }
321
+ lines.push('');
322
+ }
323
+ }
324
+
325
+ return lines.join('\n');
326
+ }
327
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Mount Registry - Extensible mount type system
3
+ *
4
+ * Replaces repetitive switch statements with a registration-based pattern.
5
+ * Makes it easy to add new mount types without modifying core code.
6
+ */
7
+
8
+ import type {
9
+ BucketMountConfig,
10
+ SmartBucketMountConfig,
11
+ KvMountConfig,
12
+ SqlMountConfig,
13
+ ActorMountConfig,
14
+ ActorStorageMountConfig,
15
+ BucketMountInfo,
16
+ SmartBucketMountInfo,
17
+ KvMountInfo,
18
+ SqlMountInfo,
19
+ ActorMountInfo,
20
+ ActorStorageMountInfo,
21
+ MountConfigType,
22
+ MountInfo,
23
+ MountType,
24
+ MountMode
25
+ } from '../types.js';
26
+
27
+ // Internal base interfaces (not exported)
28
+ interface BaseMountConfig {
29
+ description?: string;
30
+ mode?: MountMode;
31
+ }
32
+
33
+ interface BaseMountInfo extends BaseMountConfig {
34
+ name: string;
35
+ }
36
+
37
+ /**
38
+ * Handler for a specific mount type
39
+ */
40
+ interface MountHandler<TConfig extends BaseMountConfig, TInfo extends BaseMountInfo> {
41
+ /** Validate the mount configuration */
42
+ validate?: (config: TConfig) => void;
43
+
44
+ /** Create a MountInfo from a name and config */
45
+ createInfo: (name: string, config: TConfig) => TInfo;
46
+ }
47
+
48
+ const mountHandlers = new Map<MountType, MountHandler<any, any>>();
49
+
50
+ /**
51
+ * Register a mount type handler
52
+ */
53
+ export function registerMountType<TConfig extends BaseMountConfig & { type: MountType }, TInfo extends BaseMountInfo>(
54
+ type: MountType,
55
+ handler: MountHandler<TConfig, TInfo>
56
+ ): void {
57
+ mountHandlers.set(type, handler);
58
+ }
59
+
60
+ /**
61
+ * Register standard mount types
62
+ */
63
+ export function registerStandardMountTypes(): void {
64
+ // Bucket
65
+ registerMountType<BucketMountConfig, BucketMountInfo>('bucket', {
66
+ validate: (config) => {
67
+ if (!config.resource) {
68
+ throw new Error(`Bucket mount requires a resource`);
69
+ }
70
+ },
71
+ createInfo: (name, config) => ({
72
+ name,
73
+ type: 'bucket',
74
+ resource: config.resource,
75
+ description: config.description,
76
+ mode: config.mode
77
+ })
78
+ });
79
+
80
+ // SmartBucket
81
+ registerMountType<SmartBucketMountConfig, SmartBucketMountInfo>('smartbucket', {
82
+ validate: (config) => {
83
+ if (!config.resource) {
84
+ throw new Error(`SmartBucket mount requires a resource`);
85
+ }
86
+ },
87
+ createInfo: (name, config) => ({
88
+ name,
89
+ type: 'smartbucket',
90
+ resource: config.resource,
91
+ description: config.description,
92
+ mode: config.mode
93
+ })
94
+ });
95
+
96
+ // KvCache
97
+ registerMountType<KvMountConfig, KvMountInfo>('kv', {
98
+ validate: (config) => {
99
+ if (!config.resource) {
100
+ throw new Error(`KvCache mount requires a resource`);
101
+ }
102
+ },
103
+ createInfo: (name, config) => ({
104
+ name,
105
+ type: 'kv',
106
+ resource: config.resource,
107
+ description: config.description,
108
+ mode: config.mode
109
+ })
110
+ });
111
+
112
+ // SqlDatabase
113
+ registerMountType<SqlMountConfig, SqlMountInfo>('database', {
114
+ validate: (config) => {
115
+ if (!config.resource) {
116
+ throw new Error(`Database mount requires a resource`);
117
+ }
118
+ },
119
+ createInfo: (name, config) => ({
120
+ name,
121
+ type: 'database',
122
+ resource: config.resource,
123
+ description: config.description,
124
+ mode: config.mode
125
+ })
126
+ });
127
+
128
+ // Actor
129
+ registerMountType<ActorMountConfig, ActorMountInfo>('actor', {
130
+ validate: (config) => {
131
+ if (!config.resource) {
132
+ throw new Error(`Actor mount requires a resource`);
133
+ }
134
+ if (!config.methods || Object.keys(config.methods).length === 0) {
135
+ throw new Error(`Actor mount requires at least one method schema`);
136
+ }
137
+ },
138
+ createInfo: (name, config) => ({
139
+ name,
140
+ type: 'actor',
141
+ resource: config.resource,
142
+ description: config.description,
143
+ mode: config.mode,
144
+ methods: config.methods
145
+ })
146
+ });
147
+
148
+ // ActorStorage
149
+ registerMountType<ActorStorageMountConfig, ActorStorageMountInfo>('actor-storage', {
150
+ validate: (config) => {
151
+ if (!config.resource) {
152
+ throw new Error(`ActorStorage mount requires a resource`);
153
+ }
154
+ },
155
+ createInfo: (name, config) => ({
156
+ name,
157
+ type: 'actor-storage',
158
+ resource: config.resource,
159
+ description: config.description,
160
+ mode: config.mode
161
+ })
162
+ });
163
+ }
164
+
165
+ /**
166
+ * Create a MountInfo from a name and config
167
+ */
168
+ export function createMountInfo(name: string, config: MountConfigType): MountInfo {
169
+ const handler = mountHandlers.get(config.type);
170
+
171
+ if (!handler) {
172
+ throw new Error(`Unknown mount type: ${config.type}`);
173
+ }
174
+
175
+ // Validate if handler provides validation
176
+ if (handler.validate) {
177
+ handler.validate(config);
178
+ }
179
+
180
+ // Create the mount info
181
+ return handler.createInfo(name, config);
182
+ }
183
+
184
+ /**
185
+ * Get all registered mount types
186
+ */
187
+ export function getRegisteredMountTypes(): MountType[] {
188
+ return Array.from(mountHandlers.keys());
189
+ }
190
+
191
+ /**
192
+ * Check if a mount type is registered
193
+ */
194
+ export function isMountTypeRegistered(type: MountType): boolean {
195
+ return mountHandlers.has(type);
196
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Tests for zodToTypeString utility
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { z } from 'zod';
7
+ import { zodToTypeString } from './zod-to-string.js';
8
+
9
+ describe('zodToTypeString', () => {
10
+ describe('primitives', () => {
11
+ it('should convert z.string() to "string"', () => {
12
+ expect(zodToTypeString(z.string())).toBe('string');
13
+ });
14
+
15
+ it('should convert z.number() to "number"', () => {
16
+ expect(zodToTypeString(z.number())).toBe('number');
17
+ });
18
+
19
+ it('should convert z.boolean() to "boolean"', () => {
20
+ expect(zodToTypeString(z.boolean())).toBe('boolean');
21
+ });
22
+
23
+ it('should convert z.null() to "null"', () => {
24
+ expect(zodToTypeString(z.null())).toBe('null');
25
+ });
26
+
27
+ it('should convert z.undefined() to "undefined"', () => {
28
+ expect(zodToTypeString(z.undefined())).toBe('undefined');
29
+ });
30
+ });
31
+
32
+ describe('literals and enums', () => {
33
+ it('should convert z.literal("hello") to "\'hello\'"', () => {
34
+ expect(zodToTypeString(z.literal('hello'))).toBe("'hello'");
35
+ });
36
+
37
+ it('should convert z.literal(42) to "42"', () => {
38
+ expect(zodToTypeString(z.literal(42))).toBe('42');
39
+ });
40
+
41
+ it('should convert z.enum to union of literals', () => {
42
+ const result = zodToTypeString(z.enum(['light', 'dark']));
43
+ expect(result).toBe("'light' | 'dark'");
44
+ });
45
+ });
46
+
47
+ describe('arrays', () => {
48
+ it('should convert z.array(z.string()) to "Array<string>"', () => {
49
+ expect(zodToTypeString(z.array(z.string()))).toBe('Array<string>');
50
+ });
51
+
52
+ it('should convert z.array(z.number()) to "Array<number>"', () => {
53
+ expect(zodToTypeString(z.array(z.number()))).toBe('Array<number>');
54
+ });
55
+ });
56
+
57
+ describe('objects', () => {
58
+ it('should convert empty z.object({}) to "{}"', () => {
59
+ expect(zodToTypeString(z.object({}))).toBe('{}');
60
+ });
61
+
62
+ it('should convert simple object', () => {
63
+ const schema = z.object({ name: z.string() });
64
+ expect(zodToTypeString(schema)).toBe('{ name: string }');
65
+ });
66
+
67
+ it('should convert object with multiple properties', () => {
68
+ const schema = z.object({
69
+ name: z.string(),
70
+ age: z.number()
71
+ });
72
+ expect(zodToTypeString(schema)).toBe('{ name: string, age: number }');
73
+ });
74
+
75
+ it('should mark optional properties', () => {
76
+ const schema = z.object({
77
+ name: z.string(),
78
+ age: z.number().optional()
79
+ });
80
+ expect(zodToTypeString(schema)).toBe('{ name: string, age?: number }');
81
+ });
82
+ });
83
+
84
+ describe('unions', () => {
85
+ it('should convert z.union to pipe-separated types', () => {
86
+ const schema = z.union([z.string(), z.number()]);
87
+ expect(zodToTypeString(schema)).toBe('string | number');
88
+ });
89
+ });
90
+
91
+ describe('optional and nullable', () => {
92
+ it('should unwrap optional', () => {
93
+ expect(zodToTypeString(z.string().optional())).toBe('string');
94
+ });
95
+
96
+ it('should show nullable as union with null', () => {
97
+ expect(zodToTypeString(z.string().nullable())).toBe('string | null');
98
+ });
99
+ });
100
+
101
+ describe('records and maps', () => {
102
+ it('should convert z.record to Record<string, T>', () => {
103
+ expect(zodToTypeString(z.record(z.number()))).toBe('Record<string, number>');
104
+ });
105
+ });
106
+
107
+ describe('tuples', () => {
108
+ it('should convert z.tuple to bracketed types', () => {
109
+ const schema = z.tuple([z.string(), z.number()]);
110
+ expect(zodToTypeString(schema)).toBe('[string, number]');
111
+ });
112
+ });
113
+
114
+ describe('complex nested types', () => {
115
+ it('should handle nested objects', () => {
116
+ const schema = z.object({
117
+ user: z.object({
118
+ name: z.string()
119
+ })
120
+ });
121
+ const result = zodToTypeString(schema);
122
+ expect(result).toContain('user');
123
+ });
124
+
125
+ it('should handle array of objects', () => {
126
+ const schema = z.array(z.object({ id: z.string() }));
127
+ expect(zodToTypeString(schema)).toBe('Array<{ id: string }>');
128
+ });
129
+ });
130
+
131
+ describe('depth limiting', () => {
132
+ it('should abbreviate deeply nested types', () => {
133
+ // Create a deeply nested structure
134
+ const deep = z.object({
135
+ a: z.object({
136
+ b: z.object({
137
+ c: z.object({
138
+ d: z.object({
139
+ e: z.object({
140
+ f: z.string()
141
+ })
142
+ })
143
+ })
144
+ })
145
+ })
146
+ });
147
+
148
+ const result = zodToTypeString(deep);
149
+ // Should not throw and should return something reasonable
150
+ expect(result).toBeDefined();
151
+ expect(result.length).toBeLessThan(500); // Shouldn't explode
152
+ });
153
+ });
154
+ });