Skip to content

Commit 979e75f

Browse files
committed
test: add self-anchoring integration tests
1 parent bbdbef2 commit 979e75f

1 file changed

Lines changed: 261 additions & 0 deletions

File tree

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import { beforeAll, describe, expect, test, jest } from '@jest/globals'
2+
import {
3+
type ClientOptions,
4+
type FlightSqlClient,
5+
createFlightSqlClient,
6+
} from '@ceramic-sdk/flight-sql-client'
7+
import { CeramicClient } from '@ceramic-sdk/http-client'
8+
import type { StreamID } from '@ceramic-sdk/identifiers'
9+
import { ModelClient } from '@ceramic-sdk/model-client'
10+
import { ModelInstanceClient } from '@ceramic-sdk/model-instance-client'
11+
import type { ModelDefinition } from '@ceramic-sdk/model-protocol'
12+
import { tableFromIPC } from 'apache-arrow'
13+
import { base16 } from 'multiformats/bases/base16'
14+
import { randomDID } from '../../../utils/didHelper'
15+
import { waitForEventState } from '../../../utils/rustCeramicHelpers'
16+
import { urlsToEndpoint, utilities } from '../../../utils/common'
17+
18+
const delayMs = utilities.delayMs
19+
20+
const CeramicUrls = String(process.env.CERAMIC_URLS).split(',')
21+
const CeramicFlightUrls = String(process.env.CERAMIC_FLIGHT_URLS).split(',')
22+
const CeramicFlightEndpoints = urlsToEndpoint(CeramicFlightUrls)
23+
24+
const FLIGHT_OPTIONS: ClientOptions = {
25+
headers: [],
26+
username: undefined,
27+
password: undefined,
28+
token: undefined,
29+
tls: false,
30+
host: CeramicFlightEndpoints[0].host,
31+
port: CeramicFlightEndpoints[0].port,
32+
}
33+
34+
// Self-anchoring should complete within 2 minutes (anchor interval + processing time)
35+
const ANCHOR_TIMEOUT_MS = 2 * 60 * 1000
36+
const POLL_INTERVAL_MS = 5000
37+
38+
// Expected chain ID for local Ganache network configured in basic-rust.yaml
39+
const EXPECTED_CHAIN_ID = 'eip155:1337'
40+
41+
const testModel: ModelDefinition = {
42+
version: '2.0',
43+
name: 'SelfAnchorTestModel',
44+
description: 'Model for testing self-anchoring',
45+
accountRelation: { type: 'list' },
46+
interface: false,
47+
implements: [],
48+
schema: {
49+
type: 'object',
50+
properties: {
51+
value: { type: 'integer' },
52+
},
53+
additionalProperties: false,
54+
},
55+
}
56+
57+
/**
58+
* Wait for an event to be anchored (chain_id populated) for a given stream.
59+
* Polls the event_states table for events that have chain_id set.
60+
*/
61+
async function waitForAnchoredEvent(
62+
flightClient: FlightSqlClient,
63+
streamCidHex: string,
64+
timeoutMs: number = ANCHOR_TIMEOUT_MS,
65+
): Promise<{ anchored: boolean; chainId: string | null }> {
66+
const startTime = Date.now()
67+
// Remove 'f' prefix from base16 if present
68+
const cleanHex = streamCidHex.startsWith('f') ? streamCidHex.substring(1) : streamCidHex
69+
70+
while (Date.now() - startTime < timeoutMs) {
71+
try {
72+
// Query for events that have been anchored (chain_id is not null)
73+
const buffer = await flightClient.query(
74+
`SELECT chain_id FROM event_states
75+
WHERE stream_cid = X'${cleanHex}'
76+
AND chain_id IS NOT NULL
77+
LIMIT 1`,
78+
)
79+
80+
const table = tableFromIPC(buffer)
81+
if (table.numRows > 0) {
82+
const row = table.get(0)
83+
const chainId = row?.chain_id as string | null
84+
console.log(`Found anchored event for stream with chain_id: ${chainId}`)
85+
return { anchored: true, chainId }
86+
}
87+
} catch (error) {
88+
console.log(`Query error (retrying): ${error}`)
89+
}
90+
91+
await delayMs(POLL_INTERVAL_MS)
92+
}
93+
94+
return { anchored: false, chainId: null }
95+
}
96+
97+
/**
98+
* Count anchored events for a given stream.
99+
*/
100+
async function countAnchoredEvents(
101+
flightClient: FlightSqlClient,
102+
streamCidHex: string,
103+
): Promise<number> {
104+
const cleanHex = streamCidHex.startsWith('f') ? streamCidHex.substring(1) : streamCidHex
105+
106+
const buffer = await flightClient.query(
107+
`SELECT COUNT(*) as count FROM event_states
108+
WHERE stream_cid = X'${cleanHex}'
109+
AND chain_id IS NOT NULL`,
110+
)
111+
112+
const table = tableFromIPC(buffer)
113+
const row = table.get(0)
114+
return row ? Number(row.count) : 0
115+
}
116+
117+
describe('self-anchoring integration test', () => {
118+
jest.setTimeout(1000 * 60 * 10) // 10 minutes total test timeout
119+
120+
let flightClient: FlightSqlClient
121+
let client: CeramicClient
122+
let modelClient: ModelClient
123+
let modelInstanceClient: ModelInstanceClient
124+
let modelStream: StreamID
125+
126+
beforeAll(async () => {
127+
flightClient = await createFlightSqlClient(FLIGHT_OPTIONS)
128+
129+
client = new CeramicClient({
130+
url: CeramicUrls[0],
131+
})
132+
133+
modelClient = new ModelClient({
134+
ceramic: client,
135+
did: await randomDID(),
136+
})
137+
138+
modelInstanceClient = new ModelInstanceClient({
139+
ceramic: client,
140+
did: await randomDID(),
141+
})
142+
143+
// Create test model
144+
modelStream = await modelClient.createDefinition(testModel)
145+
await waitForEventState(flightClient, modelStream.cid)
146+
console.log(`Created test model: ${modelStream.toString()}`)
147+
}, 30000)
148+
149+
test('document creation triggers self-anchoring', async () => {
150+
// Create a document
151+
console.log('Creating document...')
152+
const documentStream = await modelInstanceClient.createInstance({
153+
model: modelStream,
154+
content: { value: 42 },
155+
shouldIndex: true,
156+
})
157+
158+
// Wait for the init event to be processed
159+
await waitForEventState(flightClient, documentStream.commit)
160+
console.log(`Document created: ${documentStream.baseID.toString()}`)
161+
162+
// Get the stream CID for querying
163+
const streamCidHex = documentStream.baseID.cid.toString(base16.encoder)
164+
console.log(`Waiting for anchored event for stream CID: ${streamCidHex}`)
165+
166+
// Wait for anchoring to complete
167+
const result = await waitForAnchoredEvent(flightClient, streamCidHex, ANCHOR_TIMEOUT_MS)
168+
169+
expect(result.anchored).toBe(true)
170+
expect(result.chainId).toBe(EXPECTED_CHAIN_ID)
171+
console.log(`Document successfully anchored via self-anchoring with chain_id: ${result.chainId}`)
172+
}, ANCHOR_TIMEOUT_MS + 30000)
173+
174+
test('document update triggers self-anchoring', async () => {
175+
// Create a document first
176+
console.log('Creating document...')
177+
const documentStream = await modelInstanceClient.createInstance({
178+
model: modelStream,
179+
content: { value: 1 },
180+
shouldIndex: true,
181+
})
182+
183+
await waitForEventState(flightClient, documentStream.commit)
184+
const streamCidHex = documentStream.baseID.cid.toString(base16.encoder)
185+
186+
// Wait for initial anchor
187+
console.log('Waiting for initial anchor...')
188+
const initialResult = await waitForAnchoredEvent(flightClient, streamCidHex, ANCHOR_TIMEOUT_MS)
189+
expect(initialResult.anchored).toBe(true)
190+
expect(initialResult.chainId).toBe(EXPECTED_CHAIN_ID)
191+
192+
const initialCount = await countAnchoredEvents(flightClient, streamCidHex)
193+
console.log(`Initial anchored events count: ${initialCount}`)
194+
195+
// Update the document
196+
console.log('Updating document...')
197+
const updatedState = await modelInstanceClient.updateDocument({
198+
streamID: documentStream.baseID.toString(),
199+
newContent: { value: 2 },
200+
shouldIndex: true,
201+
})
202+
203+
await waitForEventState(flightClient, updatedState.commitID.commit)
204+
console.log('Document updated, waiting for anchor...')
205+
206+
// Wait for the update to be anchored (should result in more anchored events)
207+
const startTime = Date.now()
208+
let newCount = initialCount
209+
while (Date.now() - startTime < ANCHOR_TIMEOUT_MS) {
210+
newCount = await countAnchoredEvents(flightClient, streamCidHex)
211+
if (newCount > initialCount) {
212+
break
213+
}
214+
await delayMs(POLL_INTERVAL_MS)
215+
}
216+
217+
expect(newCount).toBeGreaterThan(initialCount)
218+
console.log(`Update anchored. Anchored events count: ${newCount}`)
219+
}, (ANCHOR_TIMEOUT_MS * 2) + 60000)
220+
221+
test('multiple documents are anchored', async () => {
222+
// Create multiple documents
223+
console.log('Creating 3 documents...')
224+
const docs = await Promise.all([
225+
modelInstanceClient.createInstance({
226+
model: modelStream,
227+
content: { value: 10 },
228+
shouldIndex: true,
229+
}),
230+
modelInstanceClient.createInstance({
231+
model: modelStream,
232+
content: { value: 20 },
233+
shouldIndex: true,
234+
}),
235+
modelInstanceClient.createInstance({
236+
model: modelStream,
237+
content: { value: 30 },
238+
shouldIndex: true,
239+
}),
240+
])
241+
242+
// Wait for all init events to be processed
243+
await Promise.all(docs.map((doc) => waitForEventState(flightClient, doc.commit)))
244+
console.log('All documents created')
245+
246+
// Wait for all to be anchored
247+
const streamCidHexes = docs.map((doc) => doc.baseID.cid.toString(base16.encoder))
248+
249+
console.log('Waiting for all documents to be anchored...')
250+
const anchorResults = await Promise.all(
251+
streamCidHexes.map((cid) => waitForAnchoredEvent(flightClient, cid, ANCHOR_TIMEOUT_MS)),
252+
)
253+
254+
// Verify all were anchored with correct chain ID
255+
for (let i = 0; i < anchorResults.length; i++) {
256+
expect(anchorResults[i].anchored).toBe(true)
257+
expect(anchorResults[i].chainId).toBe(EXPECTED_CHAIN_ID)
258+
console.log(`Document ${i + 1} anchored: ${docs[i].baseID.toString()} with chain_id: ${anchorResults[i].chainId}`)
259+
}
260+
}, ANCHOR_TIMEOUT_MS + 60000)
261+
})

0 commit comments

Comments
 (0)