@jigging/agent-method 0.0.0 → 0.1.0-alpha.3
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/AGENTS.md +98 -0
- package/FLOW.contract.json +241 -0
- package/FLOW.meta.json +8 -0
- package/FLOW.ts +3 -0
- package/LICENSE +373 -0
- package/README.md +369 -0
- package/THIRD_PARTY_NOTICES +9 -0
- package/contracts/acp-public-updates.json +75 -0
- package/contracts/agent-commands.json +88 -0
- package/contracts/agent-replies.json +202 -0
- package/contracts/http-request/contract.json +37 -0
- package/dist/api.d.ts +11 -0
- package/dist/api.js +164 -0
- package/dist/conversation.d.ts +68 -0
- package/dist/conversation.js +346 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +8 -0
- package/dist/flow.d.ts +3 -0
- package/dist/flow.js +2784 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +220 -0
- package/dist/json.d.ts +21 -0
- package/dist/json.js +409 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +180 -0
- package/dist/skills.d.ts +3 -0
- package/dist/skills.js +132 -0
- package/dist/values.d.ts +11 -0
- package/dist/values.js +65 -0
- package/justfile +32 -0
- package/licenses/flow.LICENSE +202 -0
- package/package.json +44 -4
- package/settings.schema.json +12 -0
- package/skills/answer-check/SKILL.md +5 -0
- package/src/api.ts +191 -0
- package/src/conversation.ts +387 -0
- package/src/errors.ts +11 -0
- package/src/flow.ts +66 -0
- package/src/index.ts +325 -0
- package/src/json.ts +406 -0
- package/src/schema.ts +230 -0
- package/src/skills.ts +138 -0
- package/src/values.ts +77 -0
- package/test/api.test.ts +326 -0
- package/test/conversation-fixture.ts +70 -0
- package/test/conversation.test.ts +305 -0
- package/test/json.test.ts +88 -0
- package/test/method.test.ts +308 -0
- package/test/pack.test.ts +81 -0
- package/test/result.test.ts +103 -0
- package/test/skills-flow.test.ts +252 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
/** Synthetic Agent host, real public SDK subprocess. No native-client qualification. */
|
|
4
|
+
async function exercise(mode: string) {
|
|
5
|
+
const process = Bun.spawn([Bun.which('bun')!, `${import.meta.dir}/conversation-fixture.ts`], {
|
|
6
|
+
stdin: 'pipe',
|
|
7
|
+
stdout: 'pipe',
|
|
8
|
+
stderr: 'pipe',
|
|
9
|
+
})
|
|
10
|
+
const errors = new Response(process.stderr).text()
|
|
11
|
+
const timer = setTimeout(() => process.kill(), 5_000)
|
|
12
|
+
const send = (frame: unknown) => {
|
|
13
|
+
process.stdin.write(JSON.stringify(frame) + '\n')
|
|
14
|
+
process.stdin.flush()
|
|
15
|
+
}
|
|
16
|
+
const ok = (request: any, result: unknown) => send({ jsonrpc: '2.0', id: request.id, result })
|
|
17
|
+
const fail = (request: any, code: string) =>
|
|
18
|
+
send({ jsonrpc: '2.0', id: request.id, error: { code: -32000, message: code, data: { code } } })
|
|
19
|
+
const queues: any[] = []
|
|
20
|
+
let read: any
|
|
21
|
+
let eventRead: any
|
|
22
|
+
const eventQueue = [
|
|
23
|
+
{ item: { sequence: 1, value: { sessionUpdate: 'plan', entries: [], turn: 0 } } },
|
|
24
|
+
{
|
|
25
|
+
item: {
|
|
26
|
+
sequence: 2,
|
|
27
|
+
value: {
|
|
28
|
+
sessionUpdate: 'agent_message_chunk',
|
|
29
|
+
content: { type: 'text', text: 'visible draft' },
|
|
30
|
+
turn: 0,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
let call: any
|
|
36
|
+
let sequence = 0
|
|
37
|
+
let creates = 0
|
|
38
|
+
let closed = false
|
|
39
|
+
let final: any
|
|
40
|
+
let nativeSettled = false
|
|
41
|
+
const operations: any[] = []
|
|
42
|
+
const item = (value: unknown) => {
|
|
43
|
+
queues.push({ item: { sequence: ++sequence, value } })
|
|
44
|
+
if (read) {
|
|
45
|
+
ok(read, queues.shift())
|
|
46
|
+
read = undefined
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const terminal = (turn: number, type = 'result') =>
|
|
50
|
+
item(
|
|
51
|
+
type === 'result'
|
|
52
|
+
? { type, turn, result: { outcome: 'done', output: { text: `answer ${turn}` } } }
|
|
53
|
+
: { type, turn },
|
|
54
|
+
)
|
|
55
|
+
send({
|
|
56
|
+
jsonrpc: '2.0',
|
|
57
|
+
id: 'root',
|
|
58
|
+
method: 'flow/run',
|
|
59
|
+
params: {
|
|
60
|
+
protocol: 'run/0',
|
|
61
|
+
input: mode,
|
|
62
|
+
settings: {},
|
|
63
|
+
attachments: {},
|
|
64
|
+
scratch: '/tmp/unused',
|
|
65
|
+
deadlineUnixMs: Date.now() + 4_000,
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
try {
|
|
69
|
+
let buffer = ''
|
|
70
|
+
const decoder = new TextDecoder()
|
|
71
|
+
for await (const bytes of process.stdout) {
|
|
72
|
+
buffer += decoder.decode(bytes, { stream: true })
|
|
73
|
+
while (buffer.includes('\n')) {
|
|
74
|
+
const split = buffer.indexOf('\n')
|
|
75
|
+
const request = JSON.parse(buffer.slice(0, split))
|
|
76
|
+
buffer = buffer.slice(split + 1)
|
|
77
|
+
operations.push(request)
|
|
78
|
+
if (request.id === 'root') {
|
|
79
|
+
final = request
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
const p = request.params
|
|
83
|
+
if (request.method === 'channel/create') {
|
|
84
|
+
creates++
|
|
85
|
+
if (mode === 'partial-allocation' && creates === 2) {
|
|
86
|
+
fail(request, 'RESOURCE_EXHAUSTED')
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
const prefix = creates === 1 ? 'commands' : creates === 2 ? 'replies' : 'events'
|
|
90
|
+
expect(p.contract).toEqual({ slot: 'agent', channel: prefix })
|
|
91
|
+
ok(request, {
|
|
92
|
+
send: { endpoint: `${prefix}:s`, direction: 'send', delivery: 'direct' },
|
|
93
|
+
receive: {
|
|
94
|
+
endpoint: `${prefix}:r`,
|
|
95
|
+
direction: 'receive',
|
|
96
|
+
delivery: 'direct',
|
|
97
|
+
startSequence: 1,
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
} else if (request.method === 'flow/call') {
|
|
101
|
+
call = request
|
|
102
|
+
expect(p.channels).toEqual({
|
|
103
|
+
commands: 'commands:r',
|
|
104
|
+
replies: 'replies:s',
|
|
105
|
+
...(mode.startsWith('observer') ? { events: 'events:s' } : {}),
|
|
106
|
+
})
|
|
107
|
+
expect(p.input.session).toEqual(
|
|
108
|
+
['retained', 'missing-session', 'invalid-session'].includes(mode)
|
|
109
|
+
? { retain: true }
|
|
110
|
+
: undefined,
|
|
111
|
+
)
|
|
112
|
+
if (mode === 'unavailable' || mode === 'observer-unavailable') {
|
|
113
|
+
nativeSettled = true
|
|
114
|
+
fail(call, 'UNAVAILABLE')
|
|
115
|
+
} else if (mode === 'root-cancel' || mode === 'observer-root-cancel') {
|
|
116
|
+
send({ jsonrpc: '2.0', method: 'request/cancel', params: { requestId: 'root' } })
|
|
117
|
+
} else if (mode === 'abandoned') {
|
|
118
|
+
// No result: callback returns while a real invocation remains live.
|
|
119
|
+
} else if (mode === 'malformed')
|
|
120
|
+
item({ type: 'result', turn: 7, result: { outcome: 'done', output: null } })
|
|
121
|
+
else terminal(0)
|
|
122
|
+
} else if (request.method === 'channel/next') {
|
|
123
|
+
if (p.endpoint === 'events:r') {
|
|
124
|
+
if (mode === 'observer-lagged') fail(request, 'LAGGED')
|
|
125
|
+
else if (mode === 'observer-disposal') fail(request, 'CANCELLED')
|
|
126
|
+
else if (eventQueue.length) ok(request, eventQueue.shift())
|
|
127
|
+
else if (nativeSettled) ok(request, { end: { lastSequence: 2 } })
|
|
128
|
+
else eventRead = request
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
read = request
|
|
132
|
+
if (queues.length) {
|
|
133
|
+
ok(read, queues.shift())
|
|
134
|
+
read = undefined
|
|
135
|
+
}
|
|
136
|
+
} else if (request.method === 'channel/send') {
|
|
137
|
+
ok(request, null)
|
|
138
|
+
const command = p.value
|
|
139
|
+
item({ type: 'accepted', command: command.type, turn: command.turn })
|
|
140
|
+
if (command.type === 'prompt') {
|
|
141
|
+
if (mode === 'late-error') {
|
|
142
|
+
nativeSettled = true
|
|
143
|
+
fail(call, 'EXECUTION_FAILED')
|
|
144
|
+
} else if (!['interrupt', 'completion-race'].includes(mode)) terminal(command.turn)
|
|
145
|
+
} else if (command.type === 'interrupt')
|
|
146
|
+
terminal(command.turn, mode === 'interrupt' ? 'cancelled' : 'result')
|
|
147
|
+
} else if (request.method === 'channel/close') {
|
|
148
|
+
expect(p.endpoint).toBe('commands:s')
|
|
149
|
+
closed = true
|
|
150
|
+
ok(request, null)
|
|
151
|
+
if (call && !nativeSettled) {
|
|
152
|
+
nativeSettled = true
|
|
153
|
+
if (eventRead) {
|
|
154
|
+
ok(eventRead, { end: { lastSequence: 2 } })
|
|
155
|
+
eventRead = undefined
|
|
156
|
+
}
|
|
157
|
+
if (p.error) fail(call, 'EXECUTION_FAILED')
|
|
158
|
+
else
|
|
159
|
+
ok(call, {
|
|
160
|
+
outcome: 'done',
|
|
161
|
+
output: {
|
|
162
|
+
turns: mode === 'callback-error' ? 1 : 2,
|
|
163
|
+
...(['retained', 'invalid-session', 'unsolicited-session'].includes(mode)
|
|
164
|
+
? {
|
|
165
|
+
session: {
|
|
166
|
+
status: 'retained',
|
|
167
|
+
reference:
|
|
168
|
+
mode === 'invalid-session'
|
|
169
|
+
? 'x'.repeat(36)
|
|
170
|
+
: '013579ab-cdef-4567-89ab-0123456789ab',
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
: {}),
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
} else if (request.method === 'channel/release') {
|
|
178
|
+
if (p.endpoint === 'events:r') {
|
|
179
|
+
ok(
|
|
180
|
+
request,
|
|
181
|
+
mode === 'observer-disposal'
|
|
182
|
+
? { status: 'failed', code: 'LAGGED' }
|
|
183
|
+
: { status: 'released' },
|
|
184
|
+
)
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
expect(['replies:r', ...(mode === 'partial-allocation' ? ['commands:r'] : [])]).toContain(
|
|
188
|
+
p.endpoint,
|
|
189
|
+
)
|
|
190
|
+
if (call) expect(nativeSettled).toBe(true)
|
|
191
|
+
if (mode === 'disposal-error') ok(request, { status: 'failed', code: 'LAGGED' })
|
|
192
|
+
else ok(request, { status: 'released' })
|
|
193
|
+
} else if (request.method === 'request/cancel') {
|
|
194
|
+
if (eventRead && eventRead.id === p.requestId) {
|
|
195
|
+
fail(eventRead, 'CANCELLED')
|
|
196
|
+
eventRead = undefined
|
|
197
|
+
} else if (read && read.id === p.requestId) {
|
|
198
|
+
fail(read, 'CANCELLED')
|
|
199
|
+
read = undefined
|
|
200
|
+
} else if (
|
|
201
|
+
(mode === 'root-cancel' || mode === 'observer-root-cancel') &&
|
|
202
|
+
call &&
|
|
203
|
+
call.id === p.requestId
|
|
204
|
+
) {
|
|
205
|
+
nativeSettled = true
|
|
206
|
+
fail(call, 'CANCELLED')
|
|
207
|
+
} else if (
|
|
208
|
+
operations.some(
|
|
209
|
+
(operation) => operation.id === p.requestId && operation.method === 'channel/next',
|
|
210
|
+
)
|
|
211
|
+
) {
|
|
212
|
+
// Cancellation may arrive after this fixture already answered the read.
|
|
213
|
+
} else
|
|
214
|
+
throw Error('Helper cancelled its invocation waiter instead of awaiting settlement')
|
|
215
|
+
} else throw Error(`unexpected method ${request.method}`)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
expect(await process.exited, await errors).toBe(0)
|
|
219
|
+
if (mode === 'root-cancel' || mode === 'observer-root-cancel') {
|
|
220
|
+
expect(final?.error?.data?.code).toBe('CANCELLED')
|
|
221
|
+
return { output: null, operations }
|
|
222
|
+
}
|
|
223
|
+
expect(final?.result?.outcome).toBe('done')
|
|
224
|
+
expect(closed).toBe(true)
|
|
225
|
+
return { output: final.result.output, operations }
|
|
226
|
+
} finally {
|
|
227
|
+
clearTimeout(timer)
|
|
228
|
+
process.kill()
|
|
229
|
+
await process.exited
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (const mode of ['normal', 'interrupt', 'completion-race']) {
|
|
234
|
+
test(`public conversation helper: ${mode}`, async () => {
|
|
235
|
+
const { output } = await exercise(mode)
|
|
236
|
+
expect(output.turns).toHaveLength(2)
|
|
237
|
+
expect(output.value.second.type).toBe(mode === 'interrupt' ? 'cancelled' : 'result')
|
|
238
|
+
expect(output.settlement).toEqual({ outcome: 'done', output: { turns: 2 } })
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
test('helper filters live public events without caller channel coordination', async () => {
|
|
242
|
+
const { output } = await exercise('observer')
|
|
243
|
+
expect(output.displayed).toEqual(['visible draft'])
|
|
244
|
+
expect(output.observation).toEqual({ status: 'complete' })
|
|
245
|
+
expect(output.settlement).toEqual({ outcome: 'done', output: { turns: 2 } })
|
|
246
|
+
})
|
|
247
|
+
for (const mode of ['observer-throws', 'observer-async', 'observer-lagged', 'observer-disposal']) {
|
|
248
|
+
test(`${mode} leaves successful Agent settlement intact with explicit observation loss`, async () => {
|
|
249
|
+
const { output } = await exercise(mode)
|
|
250
|
+
expect(output.observation.status).toBe('incomplete')
|
|
251
|
+
expect(output.observation.errors.length).toBeGreaterThan(0)
|
|
252
|
+
expect(output.turns).toHaveLength(2)
|
|
253
|
+
expect(output.settlement).toEqual({ outcome: 'done', output: { turns: 2 } })
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
test('public conversation helper keeps requested retention on final settlement only', async () => {
|
|
257
|
+
const { output } = await exercise('retained')
|
|
258
|
+
expect(output.turns).toHaveLength(2)
|
|
259
|
+
expect(output.turns.every((turn: any) => turn.result.output.session === undefined)).toBe(true)
|
|
260
|
+
expect(output.settlement).toEqual({
|
|
261
|
+
outcome: 'done',
|
|
262
|
+
output: {
|
|
263
|
+
turns: 2,
|
|
264
|
+
session: { status: 'retained', reference: '013579ab-cdef-4567-89ab-0123456789ab' },
|
|
265
|
+
},
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
for (const mode of ['missing-session', 'invalid-session', 'unsolicited-session']) {
|
|
269
|
+
test(`public conversation helper refuses ${mode} while retaining settled turns`, async () => {
|
|
270
|
+
const { output } = await exercise(mode)
|
|
271
|
+
expect(output.errors).toContain('Agent omitted matching conversation settlement')
|
|
272
|
+
expect(output.turns).toHaveLength(2)
|
|
273
|
+
expect(output.settlement.output.turns).toBe(2)
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const mode of [
|
|
278
|
+
'unavailable',
|
|
279
|
+
'late-error',
|
|
280
|
+
'malformed',
|
|
281
|
+
'partial-allocation',
|
|
282
|
+
'callback-error',
|
|
283
|
+
'abandoned',
|
|
284
|
+
'disposal-error',
|
|
285
|
+
]) {
|
|
286
|
+
test(`public conversation helper retains honest ${mode}`, async () => {
|
|
287
|
+
const { output } = await exercise(mode)
|
|
288
|
+
expect(output.errors.length).toBeGreaterThan(0)
|
|
289
|
+
expect(output.turns).toHaveLength(
|
|
290
|
+
mode === 'disposal-error' ? 2 : ['late-error', 'callback-error'].includes(mode) ? 1 : 0,
|
|
291
|
+
)
|
|
292
|
+
if (mode === 'callback-error')
|
|
293
|
+
expect(output.settlement).toEqual({ outcome: 'done', output: { turns: 1 } })
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
test('root cancellation cannot become successful conversation recovery', async () => {
|
|
298
|
+
await exercise('root-cancel')
|
|
299
|
+
await exercise('observer-root-cancel')
|
|
300
|
+
})
|
|
301
|
+
test('a failed observed invocation settles without waiting for events that cannot arrive', async () => {
|
|
302
|
+
const { output } = await exercise('observer-unavailable')
|
|
303
|
+
expect(output.errors.length).toBeGreaterThan(0)
|
|
304
|
+
expect(output.turns).toHaveLength(0)
|
|
305
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
canonicalJson,
|
|
5
|
+
decodeJson1,
|
|
6
|
+
JSON_1_LIMITS,
|
|
7
|
+
type JsonValue,
|
|
8
|
+
validateJson1,
|
|
9
|
+
} from '../src/json.js'
|
|
10
|
+
|
|
11
|
+
const bytes = (text: string) => new TextEncoder().encode(text)
|
|
12
|
+
|
|
13
|
+
describe('local complete JSON/0 codec', () => {
|
|
14
|
+
test('decodes and canonicalizes portable scalars, magic names and Unicode', () => {
|
|
15
|
+
const value = decodeJson1(bytes('{"__proto__":1,"z":-0,"a":[true,false,null,"😀",1.25]}'))
|
|
16
|
+
expect(Object.getPrototypeOf(value)).toBeNull()
|
|
17
|
+
expect(new TextDecoder().decode(canonicalJson(value))).toBe(
|
|
18
|
+
'{"__proto__":1,"a":[true,false,null,"😀",1.25],"z":0}',
|
|
19
|
+
)
|
|
20
|
+
expect(decodeJson1(bytes('9007199254740991'))).toBe(Number.MAX_SAFE_INTEGER)
|
|
21
|
+
expect(Object.is(decodeJson1(bytes('-0')), -0)).toBe(true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('rejects duplicate keys, non-JSON syntax and out-of-domain numbers', () => {
|
|
25
|
+
for (const text of [
|
|
26
|
+
'',
|
|
27
|
+
'{"a":1,"\\u0061":2}',
|
|
28
|
+
'01',
|
|
29
|
+
'+1',
|
|
30
|
+
'.1',
|
|
31
|
+
'1.',
|
|
32
|
+
'1e',
|
|
33
|
+
'NaN',
|
|
34
|
+
'1e400',
|
|
35
|
+
'9007199254740993',
|
|
36
|
+
'-9007199254740992',
|
|
37
|
+
'true false',
|
|
38
|
+
'[1,]',
|
|
39
|
+
'{"a":1,}',
|
|
40
|
+
'"\\ud800"',
|
|
41
|
+
'"\\udc00"',
|
|
42
|
+
'\ufeff{}',
|
|
43
|
+
]) {
|
|
44
|
+
expect(() => decodeJson1(bytes(text))).toThrow()
|
|
45
|
+
}
|
|
46
|
+
expect(() => decodeJson1(Uint8Array.from([0x22, 0xc0, 0xaf, 0x22]))).toThrow('UTF-8')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('rejects encoded document, depth, node, container, key, string and token overflow', () => {
|
|
50
|
+
expect(() => decodeJson1(new Uint8Array(JSON_1_LIMITS.bytes + 1))).toThrow()
|
|
51
|
+
expect(() => decodeJson1(bytes('['.repeat(128) + '0' + ']'.repeat(128)))).toThrow('depth')
|
|
52
|
+
expect(() => validateJson1(Array(65_537).fill(0))).toThrow('array items')
|
|
53
|
+
expect(() => validateJson1(Array.from({ length: 4 }, () => Array(65_536).fill(0)))).toThrow(
|
|
54
|
+
'nodes',
|
|
55
|
+
)
|
|
56
|
+
expect(() => validateJson1({ ['a'.repeat(1025)]: 0 })).toThrow('string bytes')
|
|
57
|
+
expect(() => validateJson1('a'.repeat(8_388_609))).toThrow('string bytes')
|
|
58
|
+
expect(() => decodeJson1(bytes(`0.${'0'.repeat(128)}1`))).toThrow('number token')
|
|
59
|
+
expect(() => validateJson1(['\0'.repeat(3_000_000)])).toThrow('encoded bytes')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('checks ordinary host data without invoking accessors or serialization hooks', () => {
|
|
63
|
+
let called = false
|
|
64
|
+
const object = {
|
|
65
|
+
get value() {
|
|
66
|
+
called = true
|
|
67
|
+
return 'unsafe'
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
expect(() => validateJson1(object)).toThrow()
|
|
71
|
+
expect(called).toBe(false)
|
|
72
|
+
const cycle: JsonValue[] = []
|
|
73
|
+
cycle.push(cycle)
|
|
74
|
+
for (const value of [
|
|
75
|
+
cycle,
|
|
76
|
+
new Date(),
|
|
77
|
+
new Map(),
|
|
78
|
+
undefined,
|
|
79
|
+
Infinity,
|
|
80
|
+
NaN,
|
|
81
|
+
'\udfff',
|
|
82
|
+
new Array(1),
|
|
83
|
+
{ [Symbol('hidden')]: 1 },
|
|
84
|
+
]) {
|
|
85
|
+
expect(() => validateJson1(value)).toThrow()
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
})
|