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
221 lines (202 loc) · 7.66 KB
/
Copy pathplan.test.ts
File metadata and controls
221 lines (202 loc) · 7.66 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
import { readFile } from 'node:fs/promises'
import { describe, expect, it } from 'vitest'
import { PLAN_STATUSES, callSchema, isTerminal, planDraftSchema, planStatusSchema } 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,
asset: 'eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
amount: '1',
toChain: 'eip155:56',
},
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')
})
})