@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,43 @@
1
+ /**
2
+ * Test helper that mimics deprecated executeWithAsyncHost API
3
+ *
4
+ * Uses Sandbox under the hood for consistency with production code.
5
+ * This helper exists solely for test compatibility and should NOT be used in production.
6
+ *
7
+ * @deprecated Use Sandbox.configure() + Sandbox.getInstance() + sandbox.execute() instead
8
+ */
9
+
10
+ import { Sandbox } from './index.js';
11
+ import type {
12
+ SandboxGlobals,
13
+ SandboxResult,
14
+ Logger
15
+ } from '../types.js';
16
+ import type { BridgeInstaller } from './bridges/types.js';
17
+
18
+ /**
19
+ * Execute code in a sandbox (test compatibility layer)
20
+ *
21
+ * Ensures the sandbox is configured before executing.
22
+ *
23
+ * @deprecated Use Sandbox.configure() + Sandbox.getInstance() + sandbox.execute() instead
24
+ */
25
+ export async function executeWithAsyncHost(
26
+ code: string,
27
+ asyncGlobals: SandboxGlobals,
28
+ options: {
29
+ timeoutMs?: number;
30
+ memoryLimitBytes?: number;
31
+ logger?: Logger;
32
+ bridgeInstallers?: BridgeInstaller[];
33
+ } = {}
34
+ ): Promise<SandboxResult> {
35
+ // In tests, configure if not already configured, otherwise just get the instance
36
+ let sandbox: Sandbox;
37
+ try {
38
+ sandbox = await Sandbox.getInstance();
39
+ } catch {
40
+ sandbox = await Sandbox.configure(options);
41
+ }
42
+ return sandbox.execute(code, asyncGlobals, options);
43
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Test utilities for mocking fetch responses
3
+ *
4
+ * Provides mock implementations of fetch to avoid external network dependencies
5
+ */
6
+
7
+ export interface MockFetchResponse {
8
+ status?: number;
9
+ statusText?: string;
10
+ headers?: Record<string, string>;
11
+ body?: string | Uint8Array;
12
+ delay?: number;
13
+ }
14
+
15
+ export class MockFetchError extends Error {
16
+ constructor(message: string, public status?: number) {
17
+ super(message);
18
+ this.name = 'MockFetchError';
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Create a mock fetch implementation for testing
24
+ */
25
+ export function createMockFetch(responses: Map<string, MockFetchResponse> = new Map()) {
26
+ return async function mockFetch(url: string, init?: RequestInit): Promise<Response> {
27
+ const urlKey = url.split('?')[0] ?? url; // Strip query params
28
+ const mock = responses.get(urlKey);
29
+
30
+ if (!mock) {
31
+ throw new MockFetchError(`No mock response configured for: ${urlKey}`, 404);
32
+ }
33
+
34
+ // Simulate delay if specified
35
+ if (mock.delay) {
36
+ await new Promise(resolve => setTimeout(resolve, mock.delay));
37
+ }
38
+
39
+ // Simulate abort
40
+ if (init?.signal) {
41
+ if (init.signal.aborted) {
42
+ throw new DOMException('Aborted', 'AbortError');
43
+ }
44
+ }
45
+
46
+ const rawBody = mock.body !== undefined ? mock.body : JSON.stringify({ mock: true });
47
+ const headers = new Headers(mock.headers);
48
+
49
+ if (typeof rawBody === 'string') {
50
+ headers.set('content-type', 'application/json');
51
+ }
52
+
53
+ // Convert Uint8Array to ArrayBuffer for Response compatibility
54
+ const body = rawBody instanceof Uint8Array ? rawBody.buffer as ArrayBuffer : rawBody;
55
+
56
+ return new Response(body, {
57
+ status: mock.status ?? 200,
58
+ statusText: mock.statusText ?? 'OK',
59
+ headers
60
+ });
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Setup common mock responses for httpbin.org endpoints
66
+ */
67
+ export function createHttpbinMocks(): Map<string, MockFetchResponse> {
68
+ const mocks = new Map<string, MockFetchResponse>();
69
+
70
+ // GET https://httpbin.org/get
71
+ mocks.set('https://httpbin.org/get', {
72
+ status: 200,
73
+ headers: { 'content-type': 'application/json' },
74
+ body: JSON.stringify({
75
+ args: {},
76
+ url: 'https://httpbin.org/get',
77
+ headers: { 'User-Agent': 'Test' },
78
+ origin: '127.0.0.1'
79
+ })
80
+ });
81
+
82
+ // POST https://httpbin.org/uuid
83
+ mocks.set('https://httpbin.org/uuid', {
84
+ status: 200,
85
+ headers: { 'content-type': 'application/json' },
86
+ body: JSON.stringify({
87
+ uuid: '550e8400-e29b-41d4-a716-446655440000'
88
+ })
89
+ });
90
+
91
+ // GET https://httpbin.org/json
92
+ mocks.set('https://httpbin.org/json', {
93
+ status: 200,
94
+ headers: { 'content-type': 'application/json' },
95
+ body: JSON.stringify({
96
+ slideshow: { author: 'Test User' }
97
+ })
98
+ });
99
+
100
+ // GET https://httpbin.org/stream-bytes/N
101
+ mocks.set('https://httpbin.org/stream-bytes/10', {
102
+ status: 200,
103
+ headers: { 'content-type': 'application/octet-stream' },
104
+ body: new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
105
+ });
106
+
107
+ mocks.set('https://httpbin.org/stream-bytes/50', {
108
+ status: 200,
109
+ headers: { 'content-type': 'application/octet-stream' },
110
+ body: new Uint8Array(Array.from({ length: 50 }, (_, i) => i % 256))
111
+ });
112
+
113
+ mocks.set('https://httpbin.org/stream-bytes/10000', {
114
+ status: 200,
115
+ headers: { 'content-type': 'application/octet-stream' },
116
+ body: new Uint8Array(Array.from({ length: 10000 }, (_, i) => i % 256))
117
+ });
118
+
119
+ // GET https://httpbin.org/delay/N
120
+ const createDelayMock = (delayMs: number): MockFetchResponse => ({
121
+ status: 200,
122
+ headers: { 'content-type': 'application/json' },
123
+ delay: delayMs,
124
+ body: JSON.stringify({ delay: delayMs })
125
+ });
126
+
127
+ mocks.set('https://httpbin.org/delay/5', createDelayMock(5000));
128
+ mocks.set('https://httpbin.org/delay/10', createDelayMock(10000));
129
+
130
+ return mocks;
131
+ }
132
+
133
+ /**
134
+ * Create a bridge installer that mocks fetch
135
+ */
136
+ import type { BridgeInstaller } from './bridges/types.js';
137
+ import { installFetch } from './bridges/fetch.js';
138
+
139
+ export function createMockFetchBridgeInstaller(): BridgeInstaller {
140
+ const mockFetch = createMockFetch(createHttpbinMocks());
141
+
142
+ return (ctx) => {
143
+ installFetch(ctx);
144
+
145
+ // Replace global fetch with mock
146
+ const originalFetch = globalThis.fetch;
147
+ globalThis.fetch = mockFetch as any;
148
+
149
+ // Return cleanup that restores original fetch
150
+ return () => {
151
+ globalThis.fetch = originalFetch;
152
+ };
153
+ };
154
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Test the exact user code that was failing
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
6
+ import { executeWithAsyncHost } from './test-helper.js';
7
+ import { createHttpbinMocks } from './test-mocks.js';
8
+
9
+ describe('User Stream Test', () => {
10
+ let mockFetch: any;
11
+
12
+ beforeEach(() => {
13
+ const mocks = createHttpbinMocks();
14
+ mockFetch = vi.fn(async (url: string, _init?: RequestInit) => {
15
+ const urlKey = url.split('?')[0];
16
+ const mock = mocks.get(urlKey);
17
+ if (!mock) {
18
+ throw new Error(`No mock for: ${urlKey}`);
19
+ }
20
+ if (mock.delay) {
21
+ await new Promise(resolve => setTimeout(resolve, mock.delay));
22
+ }
23
+ return new Response(mock.body, {
24
+ status: mock.status ?? 200,
25
+ statusText: mock.statusText ?? 'OK',
26
+ headers: new Headers(mock.headers)
27
+ });
28
+ });
29
+ globalThis.fetch = mockFetch;
30
+ });
31
+
32
+ afterEach(() => {
33
+ vi.restoreAllMocks();
34
+ });
35
+
36
+ it('should handle streaming response with TextDecoder', async () => {
37
+ const code = `
38
+ async function main() {
39
+ const res = await fetch('https://httpbin.org/get');
40
+
41
+ const reader = res.body.getReader();
42
+ const decoder = new TextDecoder();
43
+ let result = '';
44
+
45
+ while (true) {
46
+ const { done, value } = await reader.read();
47
+ if (done) break;
48
+ const text = decoder.decode(value, { stream: true });
49
+ result += text;
50
+ }
51
+
52
+ return {
53
+ status: res.status,
54
+ headers: Object.fromEntries(res.headers.entries()),
55
+ body: result
56
+ };
57
+ }
58
+
59
+ return await main();
60
+ `;
61
+
62
+ const result = await executeWithAsyncHost(
63
+ code,
64
+ {},
65
+ {
66
+ timeoutMs: 10000
67
+ }
68
+ );
69
+
70
+ expect(result.success).toBe(true);
71
+ expect(result.result.status).toBe(200);
72
+ expect(mockFetch).toHaveBeenCalledWith('https://httpbin.org/get', expect.objectContaining({
73
+ signal: expect.any(AbortSignal)
74
+ }));
75
+ expect(Object.keys(result.result.headers).length).toBeGreaterThan(0);
76
+ });
77
+ });
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Tests for YAML frontmatter parser
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { parseFrontmatter } from './frontmatter.js';
7
+
8
+ describe('parseFrontmatter', () => {
9
+ it('should parse basic frontmatter with required fields', () => {
10
+ const content = `---
11
+ name: data-cleanup
12
+ description: Normalize and deduplicate data across tables.
13
+ ---
14
+
15
+ # Data Cleanup Skill
16
+
17
+ Instructions here...`;
18
+
19
+ const result = parseFrontmatter(content);
20
+ expect(result).not.toBeNull();
21
+ expect(result!.name).toBe('data-cleanup');
22
+ expect(result!.description).toBe('Normalize and deduplicate data across tables.');
23
+ });
24
+
25
+ it('should parse frontmatter with all optional fields', () => {
26
+ const content = `---
27
+ name: pdf-processing
28
+ description: Extracts text and tables from PDF files, fills forms, merges documents.
29
+ license: Apache-2.0
30
+ compatibility: Requires poppler and ghostscript
31
+ allowed-tools: Bash(pdftotext:*) Read
32
+ metadata:
33
+ author: example-org
34
+ version: "1.0"
35
+ ---
36
+
37
+ # PDF Processing`;
38
+
39
+ const result = parseFrontmatter(content);
40
+ expect(result).not.toBeNull();
41
+ expect(result!.name).toBe('pdf-processing');
42
+ expect(result!.description).toBe(
43
+ 'Extracts text and tables from PDF files, fills forms, merges documents.'
44
+ );
45
+ expect(result!.license).toBe('Apache-2.0');
46
+ expect(result!.compatibility).toBe('Requires poppler and ghostscript');
47
+ expect(result!.allowedTools).toBe('Bash(pdftotext:*) Read');
48
+ expect(result!.metadata).toEqual({ author: 'example-org', version: '1.0' });
49
+ });
50
+
51
+ it('should return null for content without frontmatter', () => {
52
+ const content = `# Just a Markdown File
53
+
54
+ No frontmatter here.`;
55
+
56
+ expect(parseFrontmatter(content)).toBeNull();
57
+ });
58
+
59
+ it('should return null for unclosed frontmatter', () => {
60
+ const content = `---
61
+ name: broken
62
+ description: No closing delimiter`;
63
+
64
+ expect(parseFrontmatter(content)).toBeNull();
65
+ });
66
+
67
+ it('should return null for frontmatter missing required name', () => {
68
+ const content = `---
69
+ description: Has description but no name
70
+ ---`;
71
+
72
+ expect(parseFrontmatter(content)).toBeNull();
73
+ });
74
+
75
+ it('should return null for frontmatter missing required description', () => {
76
+ const content = `---
77
+ name: has-name
78
+ ---`;
79
+
80
+ expect(parseFrontmatter(content)).toBeNull();
81
+ });
82
+
83
+ it('should handle quoted values', () => {
84
+ const content = `---
85
+ name: my-skill
86
+ description: "A skill with quoted description"
87
+ ---`;
88
+
89
+ const result = parseFrontmatter(content);
90
+ expect(result).not.toBeNull();
91
+ expect(result!.description).toBe('A skill with quoted description');
92
+ });
93
+
94
+ it('should handle single-quoted values', () => {
95
+ const content = `---
96
+ name: my-skill
97
+ description: 'Single quoted description'
98
+ ---`;
99
+
100
+ const result = parseFrontmatter(content);
101
+ expect(result).not.toBeNull();
102
+ expect(result!.description).toBe('Single quoted description');
103
+ });
104
+
105
+ it('should skip comments in frontmatter', () => {
106
+ const content = `---
107
+ # This is a comment
108
+ name: my-skill
109
+ description: A skill
110
+ # Another comment
111
+ ---`;
112
+
113
+ const result = parseFrontmatter(content);
114
+ expect(result).not.toBeNull();
115
+ expect(result!.name).toBe('my-skill');
116
+ });
117
+
118
+ it('should skip empty lines in frontmatter', () => {
119
+ const content = `---
120
+ name: my-skill
121
+
122
+ description: A skill
123
+
124
+ ---`;
125
+
126
+ const result = parseFrontmatter(content);
127
+ expect(result).not.toBeNull();
128
+ expect(result!.name).toBe('my-skill');
129
+ expect(result!.description).toBe('A skill');
130
+ });
131
+
132
+ it('should handle metadata with tab indentation', () => {
133
+ const content = `---
134
+ name: my-skill
135
+ description: A skill
136
+ metadata:
137
+ \tauthor: test-org
138
+ \tversion: "2.0"
139
+ ---`;
140
+
141
+ const result = parseFrontmatter(content);
142
+ expect(result).not.toBeNull();
143
+ expect(result!.metadata).toEqual({ author: 'test-org', version: '2.0' });
144
+ });
145
+ });
146
+
147
+ describe('parseFrontmatter — block scalars', () => {
148
+ it('should join folded scalar (>) lines with spaces', () => {
149
+ const content = `---
150
+ name: my-skill
151
+ description: >
152
+ This is a long
153
+ description that
154
+ spans multiple lines.
155
+ ---
156
+
157
+ # Skill`;
158
+
159
+ const result = parseFrontmatter(content);
160
+ expect(result).not.toBeNull();
161
+ expect(result!.description).toBe(
162
+ 'This is a long description that spans multiple lines.'
163
+ );
164
+ });
165
+
166
+ it('should preserve newlines in literal scalar (|)', () => {
167
+ const content = `---
168
+ name: my-skill
169
+ description: |
170
+ Line one.
171
+ Line two.
172
+ Line three.
173
+ ---
174
+
175
+ # Skill`;
176
+
177
+ const result = parseFrontmatter(content);
178
+ expect(result).not.toBeNull();
179
+ expect(result!.description).toBe('Line one.\nLine two.\nLine three.');
180
+ });
181
+
182
+ it('should handle mixed single-line and block scalar fields', () => {
183
+ const content = `---
184
+ name: mixed-skill
185
+ description: >
186
+ A multi-line
187
+ folded description.
188
+ license: MIT
189
+ compatibility: |
190
+ Requires Node 18+
191
+ and npm 9+
192
+ ---
193
+
194
+ # Mixed`;
195
+
196
+ const result = parseFrontmatter(content);
197
+ expect(result).not.toBeNull();
198
+ expect(result!.name).toBe('mixed-skill');
199
+ expect(result!.description).toBe('A multi-line folded description.');
200
+ expect(result!.license).toBe('MIT');
201
+ expect(result!.compatibility).toBe('Requires Node 18+\nand npm 9+');
202
+ });
203
+
204
+ it('should still parse existing single-line frontmatter unchanged', () => {
205
+ const content = `---
206
+ name: simple
207
+ description: A simple single-line description.
208
+ license: Apache-2.0
209
+ ---`;
210
+
211
+ const result = parseFrontmatter(content);
212
+ expect(result).not.toBeNull();
213
+ expect(result!.name).toBe('simple');
214
+ expect(result!.description).toBe('A simple single-line description.');
215
+ expect(result!.license).toBe('Apache-2.0');
216
+ });
217
+
218
+ it('should preserve empty lines within a literal block scalar', () => {
219
+ const content = `---
220
+ name: my-skill
221
+ description: |
222
+ First paragraph.
223
+
224
+ Second paragraph.
225
+ ---`;
226
+
227
+ const result = parseFrontmatter(content);
228
+ expect(result).not.toBeNull();
229
+ expect(result!.description).toBe('First paragraph.\n\nSecond paragraph.');
230
+ });
231
+
232
+ it('should treat empty lines in folded scalar as paragraph breaks', () => {
233
+ const content = `---
234
+ name: my-skill
235
+ description: >
236
+ First paragraph
237
+ continues here.
238
+
239
+ Second paragraph
240
+ also continues.
241
+ ---`;
242
+
243
+ const result = parseFrontmatter(content);
244
+ expect(result).not.toBeNull();
245
+ expect(result!.description).toBe(
246
+ 'First paragraph continues here.\n\nSecond paragraph also continues.'
247
+ );
248
+ });
249
+
250
+ it('should handle a block scalar with only one indented line', () => {
251
+ const content = `---
252
+ name: my-skill
253
+ description: >
254
+ Just one line.
255
+ ---`;
256
+
257
+ const result = parseFrontmatter(content);
258
+ expect(result).not.toBeNull();
259
+ expect(result!.description).toBe('Just one line.');
260
+ });
261
+
262
+ it('should handle literal scalar with only one indented line', () => {
263
+ const content = `---
264
+ name: my-skill
265
+ description: |
266
+ Single literal line.
267
+ ---`;
268
+
269
+ const result = parseFrontmatter(content);
270
+ expect(result).not.toBeNull();
271
+ expect(result!.description).toBe('Single literal line.');
272
+ });
273
+
274
+ it('should work with block scalar followed by metadata', () => {
275
+ const content = `---
276
+ name: my-skill
277
+ description: >
278
+ A folded
279
+ description here.
280
+ metadata:
281
+ author: test-org
282
+ version: "1.0"
283
+ ---`;
284
+
285
+ const result = parseFrontmatter(content);
286
+ expect(result).not.toBeNull();
287
+ expect(result!.description).toBe('A folded description here.');
288
+ expect(result!.metadata).toEqual({ author: 'test-org', version: '1.0' });
289
+ });
290
+
291
+ it('should handle block scalar with tab-indented content', () => {
292
+ const content = `---
293
+ name: my-skill
294
+ description: |
295
+ \tTab indented line one.
296
+ \tTab indented line two.
297
+ ---`;
298
+
299
+ const result = parseFrontmatter(content);
300
+ expect(result).not.toBeNull();
301
+ expect(result!.description).toBe(
302
+ 'Tab indented line one.\nTab indented line two.'
303
+ );
304
+ });
305
+ });