Skip to main content

stateset_db/sqlite/
agent_cards.rs

1//! SQLite agent cards repository implementation
2
3use 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/// SQLite implementation of `AgentCardRepository`
19#[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, // active by default
167                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            // Search in JSON array
299            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(&params);
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(&params);
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        // Discovery returns only active agents with capabilities
465        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(&params);
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}