1use super::{
4 build_in_clause, map_db_error, params_refs, parse_datetime_opt_row, parse_datetime_row,
5 parse_enum_row, parse_json_opt_row, parse_json_row, parse_uuid_row, uuid_params,
6};
7use chrono::Utc;
8use r2d2::Pool;
9use r2d2_sqlite::SqliteConnectionManager;
10use rusqlite::OptionalExtension;
11use stateset_core::{
12 A2ASkill, AgentCard, AgentCardFilter, AgentCardRepository, BatchResult, CommerceError,
13 CreateAgentCard, Result, TrustLevel, UpdateAgentCard, X402Asset, X402Network,
14 validate_batch_size,
15};
16use uuid::Uuid;
17
18#[derive(Debug)]
20pub struct SqliteAgentCardRepository {
21 pool: Pool<SqliteConnectionManager>,
22}
23
24impl SqliteAgentCardRepository {
25 #[must_use]
26 pub const fn new(pool: Pool<SqliteConnectionManager>) -> Self {
27 Self { pool }
28 }
29
30 fn trust_levels_at_or_above(min: TrustLevel) -> Vec<TrustLevel> {
31 match min {
32 TrustLevel::Sandbox => vec![
33 TrustLevel::Sandbox,
34 TrustLevel::Standard,
35 TrustLevel::Verified,
36 TrustLevel::Enterprise,
37 ],
38 TrustLevel::Standard => {
39 vec![TrustLevel::Standard, TrustLevel::Verified, TrustLevel::Enterprise]
40 }
41 TrustLevel::Verified => vec![TrustLevel::Verified, TrustLevel::Enterprise],
42 TrustLevel::Enterprise => vec![TrustLevel::Enterprise],
43 _ => vec![min],
44 }
45 }
46
47 fn conn(&self) -> Result<r2d2::PooledConnection<SqliteConnectionManager>> {
48 self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))
49 }
50
51 fn row_to_agent_card(row: &rusqlite::Row<'_>) -> rusqlite::Result<AgentCard> {
52 let supported_networks_json: String = row.get("supported_networks")?;
53 let supported_assets_json: String = row.get("supported_assets")?;
54 let a2a_skills_json: Option<String> = row.get("a2a_skills")?;
55
56 let supported_networks: Vec<X402Network> =
57 parse_json_row(&supported_networks_json, "agent_card", "supported_networks")?;
58 let supported_assets: Vec<X402Asset> =
59 parse_json_row(&supported_assets_json, "agent_card", "supported_assets")?;
60 let a2a_skills: Vec<A2ASkill> =
61 parse_json_opt_row(a2a_skills_json, "agent_card", "a2a_skills")?.unwrap_or_default();
62
63 Ok(AgentCard {
64 id: parse_uuid_row(&row.get::<_, String>("id")?, "agent_card", "id")?,
65 name: row.get("name")?,
66 description: row.get("description")?,
67 wallet_address: row.get("wallet_address")?,
68 public_key: row.get("public_key")?,
69 supported_networks,
70 supported_assets,
71 a2a_skills,
72 trust_level: parse_enum_row(
73 &row.get::<_, String>("trust_level")?,
74 "agent_card",
75 "trust_level",
76 )?,
77 verified_at: parse_datetime_opt_row(
78 row.get::<_, Option<String>>("verified_at")?,
79 "agent_card",
80 "verified_at",
81 )?,
82 verification_method: row.get("verification_method")?,
83 endpoint_url: row.get("endpoint_url")?,
84 endpoint_protocol: row.get("endpoint_protocol")?,
85 merchant_id: row.get("merchant_id")?,
86 merchant_name: row.get("merchant_name")?,
87 business_category: row.get("business_category")?,
88 max_transaction_amount: row
89 .get::<_, Option<i64>>("max_transaction_amount")?
90 .map(|n| n as u64),
91 daily_volume_limit: row.get::<_, Option<i64>>("daily_volume_limit")?.map(|n| n as u64),
92 requires_kyc: row.get::<_, i32>("requires_kyc")? == 1,
93 active: row.get::<_, i32>("active")? == 1,
94 suspended_at: parse_datetime_opt_row(
95 row.get::<_, Option<String>>("suspended_at")?,
96 "agent_card",
97 "suspended_at",
98 )?,
99 suspension_reason: row.get("suspension_reason")?,
100 metadata: row.get("metadata")?,
101 created_at: parse_datetime_row(
102 &row.get::<_, String>("created_at")?,
103 "agent_card",
104 "created_at",
105 )?,
106 updated_at: parse_datetime_row(
107 &row.get::<_, String>("updated_at")?,
108 "agent_card",
109 "updated_at",
110 )?,
111 })
112 }
113}
114
115impl AgentCardRepository for SqliteAgentCardRepository {
116 fn create(&self, input: CreateAgentCard) -> Result<AgentCard> {
117 let conn = self.conn()?;
118 let now = Utc::now();
119 let id = Uuid::new_v4();
120
121 let supported_networks =
122 input.supported_networks.unwrap_or_else(|| vec![X402Network::SetChain]);
123 let supported_assets = input.supported_assets.unwrap_or_else(|| vec![X402Asset::Usdc]);
124 let a2a_skills = input.a2a_skills.unwrap_or_default();
125 let trust_level = input.trust_level.unwrap_or_default();
126
127 let networks_json = serde_json::to_string(&supported_networks)
128 .map_err(|e| CommerceError::Internal(e.to_string()))?;
129 let assets_json = serde_json::to_string(&supported_assets)
130 .map_err(|e| CommerceError::Internal(e.to_string()))?;
131 let skills_json = if a2a_skills.is_empty() {
132 None
133 } else {
134 Some(
135 serde_json::to_string(&a2a_skills)
136 .map_err(|e| CommerceError::Internal(e.to_string()))?,
137 )
138 };
139
140 conn.execute(
141 "INSERT INTO agent_cards (
142 id, name, description, wallet_address, public_key,
143 supported_networks, supported_assets, a2a_skills, trust_level,
144 endpoint_url, endpoint_protocol, merchant_id, merchant_name,
145 business_category, max_transaction_amount, daily_volume_limit,
146 requires_kyc, active, metadata, created_at, updated_at
147 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
148 rusqlite::params![
149 id.to_string(),
150 input.name,
151 input.description,
152 input.wallet_address,
153 input.public_key,
154 networks_json,
155 assets_json,
156 skills_json,
157 trust_level.to_string(),
158 input.endpoint_url,
159 input.endpoint_protocol,
160 input.merchant_id,
161 input.merchant_name,
162 input.business_category,
163 input.max_transaction_amount.map(|n| n as i64),
164 input.daily_volume_limit.map(|n| n as i64),
165 i32::from(input.requires_kyc.unwrap_or(false)),
166 1, input.metadata,
168 now.to_rfc3339(),
169 now.to_rfc3339(),
170 ],
171 )
172 .map_err(map_db_error)?;
173
174 self.get(id)?.ok_or(CommerceError::NotFound)
175 }
176
177 fn get(&self, id: Uuid) -> Result<Option<AgentCard>> {
178 let conn = self.conn()?;
179 let mut stmt =
180 conn.prepare("SELECT * FROM agent_cards WHERE id = ?").map_err(map_db_error)?;
181
182 stmt.query_row([id.to_string()], Self::row_to_agent_card).optional().map_err(map_db_error)
183 }
184
185 fn get_by_wallet(&self, wallet_address: &str) -> Result<Option<AgentCard>> {
186 let conn = self.conn()?;
187 let mut stmt = conn
188 .prepare("SELECT * FROM agent_cards WHERE wallet_address = ?")
189 .map_err(map_db_error)?;
190
191 stmt.query_row([wallet_address], Self::row_to_agent_card).optional().map_err(map_db_error)
192 }
193
194 fn update(&self, id: Uuid, input: UpdateAgentCard) -> Result<AgentCard> {
195 let conn = self.conn()?;
196
197 let existing = self.get(id)?.ok_or(CommerceError::NotFound)?;
198
199 let name = input.name.unwrap_or(existing.name);
200 let description = input.description.or(existing.description);
201 let supported_networks = input.supported_networks.unwrap_or(existing.supported_networks);
202 let supported_assets = input.supported_assets.unwrap_or(existing.supported_assets);
203 let a2a_skills = input.a2a_skills.unwrap_or(existing.a2a_skills);
204 let trust_level = input.trust_level.unwrap_or(existing.trust_level);
205 let endpoint_url = input.endpoint_url.or(existing.endpoint_url);
206 let endpoint_protocol = input.endpoint_protocol.or(existing.endpoint_protocol);
207 let merchant_id = input.merchant_id.or(existing.merchant_id);
208 let merchant_name = input.merchant_name.or(existing.merchant_name);
209 let business_category = input.business_category.or(existing.business_category);
210 let max_transaction_amount =
211 input.max_transaction_amount.or(existing.max_transaction_amount);
212 let daily_volume_limit = input.daily_volume_limit.or(existing.daily_volume_limit);
213 let requires_kyc = input.requires_kyc.unwrap_or(existing.requires_kyc);
214 let active = input.active.unwrap_or(existing.active);
215 let metadata = input.metadata.or(existing.metadata);
216
217 let networks_json = serde_json::to_string(&supported_networks)
218 .map_err(|e| CommerceError::Internal(e.to_string()))?;
219 let assets_json = serde_json::to_string(&supported_assets)
220 .map_err(|e| CommerceError::Internal(e.to_string()))?;
221 let skills_json = if a2a_skills.is_empty() {
222 None
223 } else {
224 Some(
225 serde_json::to_string(&a2a_skills)
226 .map_err(|e| CommerceError::Internal(e.to_string()))?,
227 )
228 };
229
230 conn.execute(
231 "UPDATE agent_cards SET
232 name = ?, description = ?, supported_networks = ?, supported_assets = ?,
233 a2a_skills = ?, trust_level = ?, endpoint_url = ?, endpoint_protocol = ?,
234 merchant_id = ?, merchant_name = ?, business_category = ?,
235 max_transaction_amount = ?, daily_volume_limit = ?, requires_kyc = ?,
236 active = ?, metadata = ?, updated_at = ?
237 WHERE id = ?",
238 rusqlite::params![
239 name,
240 description,
241 networks_json,
242 assets_json,
243 skills_json,
244 trust_level.to_string(),
245 endpoint_url,
246 endpoint_protocol,
247 merchant_id,
248 merchant_name,
249 business_category,
250 max_transaction_amount.map(|n| n as i64),
251 daily_volume_limit.map(|n| n as i64),
252 i32::from(requires_kyc),
253 i32::from(active),
254 metadata,
255 Utc::now().to_rfc3339(),
256 id.to_string(),
257 ],
258 )
259 .map_err(map_db_error)?;
260
261 self.get(id)?.ok_or(CommerceError::NotFound)
262 }
263
264 fn delete(&self, id: Uuid) -> Result<()> {
265 let conn = self.conn()?;
266 let rows = conn
267 .execute("DELETE FROM agent_cards WHERE id = ?", [id.to_string()])
268 .map_err(map_db_error)?;
269
270 if rows == 0 {
271 return Err(CommerceError::NotFound);
272 }
273 Ok(())
274 }
275
276 fn list(&self, filter: AgentCardFilter) -> Result<Vec<AgentCard>> {
277 let conn = self.conn()?;
278 let mut conditions = vec!["1=1".to_string()];
279 let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![];
280
281 if let Some(ref wallet) = filter.wallet_address {
282 conditions.push("wallet_address = ?".to_string());
283 params.push(Box::new(wallet.clone()));
284 }
285 if let Some(ref trust) = filter.trust_level {
286 conditions.push("trust_level = ?".to_string());
287 params.push(Box::new(trust.to_string()));
288 }
289 if let Some(min_trust) = filter.min_trust_level {
290 let levels = Self::trust_levels_at_or_above(min_trust);
291 let placeholders = build_in_clause(levels.len());
292 conditions.push(format!("trust_level IN ({placeholders})"));
293 for level in levels {
294 params.push(Box::new(level.to_string()));
295 }
296 }
297 if let Some(ref network) = filter.network {
298 conditions.push("supported_networks LIKE ?".to_string());
300 params.push(Box::new(format!("%\"{network}%")));
301 }
302 if let Some(ref asset) = filter.asset {
303 let asset_value = serde_json::to_value(asset)
304 .ok()
305 .and_then(|v| v.as_str().map(std::string::ToString::to_string))
306 .unwrap_or_else(|| asset.to_string().to_lowercase());
307 conditions.push("supported_assets LIKE ?".to_string());
308 params.push(Box::new(format!("%\"{asset_value}%")));
309 }
310 if let Some(ref skill) = filter.skill {
311 let skill_value = serde_json::to_value(skill)
312 .ok()
313 .and_then(|v| v.as_str().map(std::string::ToString::to_string))
314 .unwrap_or_else(|| skill.to_string());
315 conditions.push("a2a_skills LIKE ?".to_string());
316 params.push(Box::new(format!("%\"{skill_value}%")));
317 }
318 if let Some(active) = filter.active {
319 conditions.push("active = ?".to_string());
320 params.push(Box::new(i32::from(active)));
321 }
322 if let Some(ref merchant) = filter.merchant_id {
323 conditions.push("merchant_id = ?".to_string());
324 params.push(Box::new(merchant.clone()));
325 }
326
327 let limit = filter.limit.unwrap_or(100).min(1000);
328 let offset = filter.offset.unwrap_or(0);
329
330 let sql = format!(
331 "SELECT * FROM agent_cards WHERE {} ORDER BY created_at DESC LIMIT ? OFFSET ?",
332 conditions.join(" AND ")
333 );
334 params.push(Box::new(i64::from(limit)));
335 params.push(Box::new(i64::from(offset)));
336
337 let param_refs = params_refs(¶ms);
338 let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
339
340 let rows = stmt
341 .query_map(rusqlite::params_from_iter(param_refs), Self::row_to_agent_card)
342 .map_err(map_db_error)?;
343 let mut results = Vec::new();
344 for row in rows {
345 results.push(row.map_err(map_db_error)?);
346 }
347 Ok(results)
348 }
349
350 fn count(&self, filter: AgentCardFilter) -> Result<u64> {
351 let conn = self.conn()?;
352 let mut conditions = vec!["1=1".to_string()];
353 let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![];
354
355 if let Some(ref wallet) = filter.wallet_address {
356 conditions.push("wallet_address = ?".to_string());
357 params.push(Box::new(wallet.clone()));
358 }
359 if let Some(ref trust) = filter.trust_level {
360 conditions.push("trust_level = ?".to_string());
361 params.push(Box::new(trust.to_string()));
362 }
363 if let Some(min_trust) = filter.min_trust_level {
364 let levels = Self::trust_levels_at_or_above(min_trust);
365 let placeholders = build_in_clause(levels.len());
366 conditions.push(format!("trust_level IN ({placeholders})"));
367 for level in levels {
368 params.push(Box::new(level.to_string()));
369 }
370 }
371 if let Some(ref network) = filter.network {
372 conditions.push("supported_networks LIKE ?".to_string());
373 params.push(Box::new(format!("%\"{network}%")));
374 }
375 if let Some(ref asset) = filter.asset {
376 let asset_value = serde_json::to_value(asset)
377 .ok()
378 .and_then(|v| v.as_str().map(std::string::ToString::to_string))
379 .unwrap_or_else(|| asset.to_string().to_lowercase());
380 conditions.push("supported_assets LIKE ?".to_string());
381 params.push(Box::new(format!("%\"{asset_value}%")));
382 }
383 if let Some(ref skill) = filter.skill {
384 let skill_value = serde_json::to_value(skill)
385 .ok()
386 .and_then(|v| v.as_str().map(std::string::ToString::to_string))
387 .unwrap_or_else(|| skill.to_string());
388 conditions.push("a2a_skills LIKE ?".to_string());
389 params.push(Box::new(format!("%\"{skill_value}%")));
390 }
391 if let Some(active) = filter.active {
392 conditions.push("active = ?".to_string());
393 params.push(Box::new(i32::from(active)));
394 }
395 if let Some(ref merchant) = filter.merchant_id {
396 conditions.push("merchant_id = ?".to_string());
397 params.push(Box::new(merchant.clone()));
398 }
399
400 let sql = format!("SELECT COUNT(*) FROM agent_cards WHERE {}", conditions.join(" AND "));
401
402 let param_refs = params_refs(¶ms);
403 let count: i64 = conn
404 .query_row(&sql, rusqlite::params_from_iter(param_refs), |row| row.get(0))
405 .map_err(map_db_error)?;
406
407 Ok(count as u64)
408 }
409
410 fn verify(&self, id: Uuid, trust_level: TrustLevel, method: &str) -> Result<AgentCard> {
411 let conn = self.conn()?;
412
413 conn.execute(
414 "UPDATE agent_cards SET
415 trust_level = ?, verified_at = ?, verification_method = ?, updated_at = ?
416 WHERE id = ?",
417 rusqlite::params![
418 trust_level.to_string(),
419 Utc::now().to_rfc3339(),
420 method,
421 Utc::now().to_rfc3339(),
422 id.to_string(),
423 ],
424 )
425 .map_err(map_db_error)?;
426
427 self.get(id)?.ok_or(CommerceError::NotFound)
428 }
429
430 fn suspend(&self, id: Uuid, reason: &str) -> Result<AgentCard> {
431 let conn = self.conn()?;
432
433 conn.execute(
434 "UPDATE agent_cards SET
435 active = 0, suspended_at = ?, suspension_reason = ?, updated_at = ?
436 WHERE id = ?",
437 rusqlite::params![
438 Utc::now().to_rfc3339(),
439 reason,
440 Utc::now().to_rfc3339(),
441 id.to_string(),
442 ],
443 )
444 .map_err(map_db_error)?;
445
446 self.get(id)?.ok_or(CommerceError::NotFound)
447 }
448
449 fn reactivate(&self, id: Uuid) -> Result<AgentCard> {
450 let conn = self.conn()?;
451
452 conn.execute(
453 "UPDATE agent_cards SET
454 active = 1, suspended_at = NULL, suspension_reason = NULL, updated_at = ?
455 WHERE id = ?",
456 rusqlite::params![Utc::now().to_rfc3339(), id.to_string()],
457 )
458 .map_err(map_db_error)?;
459
460 self.get(id)?.ok_or(CommerceError::NotFound)
461 }
462
463 fn discover(&self, filter: AgentCardFilter) -> Result<Vec<AgentCard>> {
464 let mut adjusted_filter = filter;
466 adjusted_filter.active = Some(true);
467 self.list(adjusted_filter)
468 }
469
470 fn create_batch(&self, inputs: Vec<CreateAgentCard>) -> Result<BatchResult<AgentCard>> {
471 validate_batch_size(&inputs)?;
472 let mut result = BatchResult::with_capacity(inputs.len());
473
474 for (idx, input) in inputs.into_iter().enumerate() {
475 match self.create(input) {
476 Ok(card) => result.record_success(card),
477 Err(e) => result.record_failure(idx, None, &e),
478 }
479 }
480
481 Ok(result)
482 }
483
484 fn create_batch_atomic(&self, inputs: Vec<CreateAgentCard>) -> Result<Vec<AgentCard>> {
485 validate_batch_size(&inputs)?;
486 if inputs.is_empty() {
487 return Ok(vec![]);
488 }
489
490 let mut conn = self.conn()?;
491 let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
492 let mut ids = Vec::with_capacity(inputs.len());
493
494 for input in inputs {
495 let now = Utc::now();
496 let id = Uuid::new_v4();
497
498 let supported_networks =
499 input.supported_networks.unwrap_or_else(|| vec![X402Network::SetChain]);
500 let supported_assets = input.supported_assets.unwrap_or_else(|| vec![X402Asset::Usdc]);
501 let a2a_skills = input.a2a_skills.unwrap_or_default();
502 let trust_level = input.trust_level.unwrap_or_default();
503
504 let networks_json = serde_json::to_string(&supported_networks)
505 .map_err(|e| CommerceError::Internal(e.to_string()))?;
506 let assets_json = serde_json::to_string(&supported_assets)
507 .map_err(|e| CommerceError::Internal(e.to_string()))?;
508 let skills_json = if a2a_skills.is_empty() {
509 None
510 } else {
511 Some(
512 serde_json::to_string(&a2a_skills)
513 .map_err(|e| CommerceError::Internal(e.to_string()))?,
514 )
515 };
516
517 tx.execute(
518 "INSERT INTO agent_cards (
519 id, name, description, wallet_address, public_key,
520 supported_networks, supported_assets, a2a_skills, trust_level,
521 endpoint_url, endpoint_protocol, merchant_id, merchant_name,
522 business_category, max_transaction_amount, daily_volume_limit,
523 requires_kyc, active, metadata, created_at, updated_at
524 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
525 rusqlite::params![
526 id.to_string(),
527 input.name,
528 input.description,
529 input.wallet_address,
530 input.public_key,
531 networks_json,
532 assets_json,
533 skills_json,
534 trust_level.to_string(),
535 input.endpoint_url,
536 input.endpoint_protocol,
537 input.merchant_id,
538 input.merchant_name,
539 input.business_category,
540 input.max_transaction_amount.map(|n| n as i64),
541 input.daily_volume_limit.map(|n| n as i64),
542 i32::from(input.requires_kyc.unwrap_or(false)),
543 1,
544 input.metadata,
545 now.to_rfc3339(),
546 now.to_rfc3339(),
547 ],
548 )
549 .map_err(map_db_error)?;
550
551 ids.push(id);
552 }
553
554 tx.commit().map_err(map_db_error)?;
555
556 ids.into_iter().map(|id| self.get(id)?.ok_or(CommerceError::NotFound)).collect()
557 }
558
559 fn get_batch(&self, ids: Vec<Uuid>) -> Result<Vec<AgentCard>> {
560 if ids.is_empty() {
561 return Ok(vec![]);
562 }
563 validate_batch_size(&ids)?;
564
565 let conn = self.conn()?;
566 let placeholders = build_in_clause(ids.len());
567 let sql = format!("SELECT * FROM agent_cards WHERE id IN ({placeholders})");
568
569 let params = uuid_params(&ids);
570 let param_refs = params_refs(¶ms);
571 let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
572
573 let rows = stmt
574 .query_map(rusqlite::params_from_iter(param_refs), Self::row_to_agent_card)
575 .map_err(map_db_error)?;
576 let mut results = Vec::new();
577 for row in rows {
578 results.push(row.map_err(map_db_error)?);
579 }
580 Ok(results)
581 }
582}
583
584#[cfg(test)]
585mod tests {
586 use super::*;
587
588 fn map_agent_card_from_inline_row(
589 conn: &rusqlite::Connection,
590 supported_networks: &str,
591 supported_assets: &str,
592 a2a_skills: Option<&str>,
593 ) -> rusqlite::Result<AgentCard> {
594 conn.query_row(
595 "SELECT
596 ?1 AS id,
597 ?2 AS name,
598 ?3 AS description,
599 ?4 AS wallet_address,
600 ?5 AS public_key,
601 ?6 AS supported_networks,
602 ?7 AS supported_assets,
603 ?8 AS a2a_skills,
604 ?9 AS trust_level,
605 ?10 AS verified_at,
606 ?11 AS verification_method,
607 ?12 AS endpoint_url,
608 ?13 AS endpoint_protocol,
609 ?14 AS merchant_id,
610 ?15 AS merchant_name,
611 ?16 AS business_category,
612 ?17 AS max_transaction_amount,
613 ?18 AS daily_volume_limit,
614 ?19 AS requires_kyc,
615 ?20 AS active,
616 ?21 AS suspended_at,
617 ?22 AS suspension_reason,
618 ?23 AS metadata,
619 ?24 AS created_at,
620 ?25 AS updated_at",
621 rusqlite::params![
622 "550e8400-e29b-41d4-a716-446655440000",
623 "Agent",
624 Option::<String>::None,
625 "wallet",
626 "public-key",
627 supported_networks,
628 supported_assets,
629 a2a_skills,
630 "standard",
631 Option::<String>::None,
632 Option::<String>::None,
633 Option::<String>::None,
634 Option::<String>::None,
635 Option::<String>::None,
636 Option::<String>::None,
637 Option::<String>::None,
638 Option::<i64>::None,
639 Option::<i64>::None,
640 0i32,
641 1i32,
642 Option::<String>::None,
643 Option::<String>::None,
644 Option::<String>::None,
645 "2026-01-01T00:00:00Z",
646 "2026-01-01T00:00:00Z",
647 ],
648 SqliteAgentCardRepository::row_to_agent_card,
649 )
650 }
651
652 #[test]
653 fn row_to_agent_card_parses_json_arrays() {
654 let conn = rusqlite::Connection::open_in_memory().expect("in-memory sqlite should open");
655 let card = map_agent_card_from_inline_row(
656 &conn,
657 r#"["set_chain"]"#,
658 r#"["usdc"]"#,
659 Some(r#"["sell"]"#),
660 )
661 .expect("valid JSON arrays should parse");
662
663 assert_eq!(card.supported_networks, vec![X402Network::SetChain]);
664 assert_eq!(card.supported_assets, vec![X402Asset::Usdc]);
665 assert_eq!(card.a2a_skills, vec![A2ASkill::Sell]);
666 }
667
668 #[test]
669 fn row_to_agent_card_rejects_invalid_json() {
670 let conn = rusqlite::Connection::open_in_memory().expect("in-memory sqlite should open");
671 let err = map_agent_card_from_inline_row(&conn, r#"["set_chain"]"#, "not-json", None)
672 .expect_err("invalid JSON should fail row mapping");
673
674 assert!(err.to_string().contains("agent_card.supported_assets"));
675 }
676}