forked from koshikraj/ottopus
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplan.test.ts
More file actions
254 lines (231 loc) · 8.85 KB
/
Copy pathplan.test.ts
File metadata and controls
254 lines (231 loc) · 8.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
import { readFile } from 'node:fs/promises'
import { describe, expect, it } from 'vitest'
import { PLAN_STATUSES, callSchema, isTerminal, planDraftSchema, planStatusSchema, simulationSchema } from './plan.js'
describe('status vocabulary', () => {
/**
* The vocabulary lives in three places: this file, the database check
* constraint, and the canonical plan doc. Drift between them breaks MCP
* responses, activity filters and the review UI at once, so it is checked
* rather than trusted.
*/
it('matches the database check constraint exactly', async () => {
const sql = await readFile(new URL('../../drizzle/0000_base_schema.sql', import.meta.url), 'utf8')
const clause = /plan_events_status[^(]*\(([^)]*)\)/s.exec(sql)
expect(clause, 'plan_events_status constraint not found in the migration').toBeTruthy()
const inDb = [...clause![1]!.matchAll(/'([a-z_]+)'/g)].map((m) => m[1]!).sort()
expect(inDb).toEqual([...PLAN_STATUSES].sort())
})
it('has no "simulated" state — simulation is evidence, not a state', () => {
expect(PLAN_STATUSES).not.toContain('simulated')
})
it('has no "inked" state — that is mascot copy for cancelled', () => {
expect(PLAN_STATUSES).not.toContain('inked')
expect(PLAN_STATUSES).toContain('cancelled')
})
it('rejects a status outside the vocabulary', () => {
expect(() => planStatusSchema.parse('simulated')).toThrow()
expect(() => planStatusSchema.parse('reviewed')).toThrow()
})
})
describe('terminal states', () => {
it('treats every ending as terminal', () => {
for (const s of ['confirmed', 'failed', 'expired', 'blocked', 'superseded', 'cancelled'] as const) {
expect(isTerminal(s)).toBe(true)
}
})
it('treats states a plan can still leave as non-terminal', () => {
for (const s of ['draft', 'awaiting_review', 'awaiting_signature', 'submitted'] as const) {
expect(isTerminal(s)).toBe(false)
}
})
})
describe('calls', () => {
it('lowercases calldata, so comparison cannot depend on casing', () => {
const call = callSchema.parse({
to: 'eip155:8453:0xABCDEF0123456789abcdef0123456789ABCDEF01',
value: '0',
data: '0xA9059CBB',
chainId: 'eip155:8453',
})
expect(call.data).toBe('0xa9059cbb')
expect(call.to).toBe('eip155:8453:0xabcdef0123456789abcdef0123456789abcdef01')
})
it('rejects a value that is not an integer string', () => {
const bad = { to: 'eip155:1:0xd8da6bf26964af9d7eed9e03e53415d37aa96045', value: '1.5', data: '0x', chainId: 'eip155:1' }
expect(() => callSchema.parse(bad)).toThrow()
})
it('rejects a target and chainId that name different chains', () => {
// Otherwise the transaction goes to whatever lives at that address on the
// wrong network.
expect(() =>
callSchema.parse({
to: 'eip155:8453:0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
value: '0',
data: '0x',
chainId: 'eip155:1',
}),
).toThrow(/same chain/)
})
it('rejects an unknown key rather than silently dropping it', () => {
expect(() =>
callSchema.parse({
to: 'eip155:1:0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
value: '0',
data: '0x',
chainId: 'eip155:1',
gasLimit: '21000',
}),
).toThrow()
})
})
describe('plan draft is strict about security fields', () => {
const draft = {
id: '018f0b6c-4a3b-4b2e-9c1d-2f5a6b7c8d9e',
version: 1,
userId: '0191a2b3-c4d5-4e6f-8a9b-0c1d2e3f4a5b',
createdVia: 'agent' as const,
intent: {
kind: 'transfer' as const,
asset: 'eip155:8453/slip44:60',
amount: '1',
to: 'eip155:8453:0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
},
provenance: 'route_provider' as const,
resolution: {
account: { caip10: 'eip155:8453:0x0000000000000000000000000000000000000001' },
candidatesConsidered: [],
reason: 'only funded account',
},
outcome: {
type: 'calls' as const,
calls: [{ to: 'eip155:8453:0xd8da6bf26964af9d7eed9e03e53415d37aa96045', value: '1', data: '0x', chainId: 'eip155:8453' }],
},
quote: { provider: 'test', expiresAt: '2026-09-05T12:00:00Z' },
humanPlan: { summary: 's', steps: [], feesUsd: '0.01', warnings: [] },
status: 'awaiting_review' as const,
expiresAt: '2026-09-05T12:00:00Z',
}
it('accepts a coherent draft', () => {
expect(planDraftSchema.parse(draft).version).toBe(1)
})
it('refuses a plan carrying planHash rather than quietly stripping it', () => {
// Zod strips unknown keys by default, so a non-strict schema here would
// silently delete planHash, decodedActions and simulation.
expect(() => planDraftSchema.parse({ ...draft, planHash: '0xdeadbeef' })).toThrow()
})
it('rejects a call on a chain the resolved account is not on', () => {
expect(() =>
planDraftSchema.parse({
...draft,
outcome: {
type: 'calls' as const,
calls: [{ to: 'eip155:1:0xd8da6bf26964af9d7eed9e03e53415d37aa96045', value: '1', data: '0x', chainId: 'eip155:1' }],
},
}),
).toThrow(/same chain as the resolved account/)
})
})
describe('the plan is bound to its intent', () => {
const base = {
id: '018f0b6c-4a3b-4b2e-9c1d-2f5a6b7c8d9e',
version: 1,
userId: '0191a2b3-c4d5-4e6f-8a9b-0c1d2e3f4a5b',
createdVia: 'agent' as const,
provenance: 'route_provider' as const,
quote: { provider: 'test', expiresAt: '2026-09-05T12:00:00Z' },
humanPlan: { summary: 's', steps: [], feesUsd: '0.01', warnings: [] },
status: 'awaiting_review' as const,
expiresAt: '2026-09-05T12:00:00Z',
}
const baseIntent = {
kind: 'transfer' as const,
asset: 'eip155:8453/slip44:60',
amount: '1',
to: 'eip155:8453:0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
}
it('rejects a Base intent resolved against an Ethereum account', () => {
// Resolution and calls agree with each other, and still execute something
// the user never asked for.
expect(() =>
planDraftSchema.parse({
...base,
intent: baseIntent,
resolution: {
account: { caip10: 'eip155:1:0x0000000000000000000000000000000000000001' },
candidatesConsidered: [],
reason: 'r',
},
outcome: {
type: 'calls' as const,
calls: [{ to: 'eip155:1:0xd8da6bf26964af9d7eed9e03e53415d37aa96045', value: '1', data: '0x', chainId: 'eip155:1' }],
},
}),
).toThrow(/chain the intent executes on/)
})
it('accepts a plan whose intent, account and calls all agree', () => {
const ok = planDraftSchema.parse({
...base,
intent: baseIntent,
resolution: {
account: { caip10: 'eip155:8453:0x0000000000000000000000000000000000000001' },
candidatesConsidered: [],
reason: 'r',
},
outcome: {
type: 'calls' as const,
calls: [{ to: 'eip155:8453:0xd8da6bf26964af9d7eed9e03e53415d37aa96045', value: '1', data: '0x', chainId: 'eip155:8453' }],
},
})
expect(ok.status).toBe('awaiting_review')
})
it('binds a bridge to its source chain, where signing happens', () => {
const ok = planDraftSchema.parse({
...base,
intent: {
kind: 'bridge' as const,
from: 'eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
to: 'eip155:56/slip44:714',
amountIn: '1',
},
resolution: {
account: { caip10: 'eip155:8453:0x0000000000000000000000000000000000000001' },
candidatesConsidered: [],
reason: 'r',
},
outcome: {
type: 'calls' as const,
calls: [{ to: 'eip155:8453:0xd8da6bf26964af9d7eed9e03e53415d37aa96045', value: '0', data: '0x', chainId: 'eip155:8453' }],
},
})
expect(ok.intent.kind).toBe('bridge')
})
})
describe('a simulation on record', () => {
const sim = {
provider: 'eth_simulateV1',
chainId: 'eip155:8453',
blockNumber: '51119499',
success: true,
assetChanges: [],
gasUsed: '21000',
gasUsd: '0.01',
resultHash: 'a'.repeat(64),
ranAt: '2026-09-11T00:00:00.000Z',
}
/**
* `tracedAssets` shipped, wrote itself into four stored simulations, and was
* reverted out of this strict object — at which point `parsePlan` threw
* `unrecognized_keys` on those rows and the plans list stopped loading
* entirely. A key a strict schema has ever written it must accept forever.
*/
it('still parses when it carries a field nothing reads any more', () => {
expect(simulationSchema.parse({ ...sim, tracedAssets: true }).tracedAssets).toBe(true)
})
it('parses just as well without it, for runs written before it existed', () => {
expect(simulationSchema.parse(sim).tracedAssets).toBeUndefined()
})
/** Strict is still strict: an unknown key is a plan nobody wrote. */
it('refuses a key that was never ours', () => {
expect(() => simulationSchema.parse({ ...sim, whatIsThis: 1 })).toThrow(/[Uu]nrecognized/)
})
})