@z3rno/sdk 0.8.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@z3rno/sdk-linux-x64-gnu",
3
+ "version": "0.23.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/the-ai-project-co/z3rno"
7
+ },
8
+ "os": [
9
+ "linux"
10
+ ],
11
+ "cpu": [
12
+ "x64"
13
+ ],
14
+ "main": "z3rno-sdk-native.linux-x64-gnu.node",
15
+ "files": [
16
+ "z3rno-sdk-native.linux-x64-gnu.node"
17
+ ],
18
+ "license": "Apache-2.0",
19
+ "engines": {
20
+ "node": ">= 18"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public",
24
+ "provenance": true
25
+ },
26
+ "libc": [
27
+ "glibc"
28
+ ]
29
+ }
@@ -0,0 +1,3 @@
1
+ # `@z3rno/sdk-win32-x64-msvc`
2
+
3
+ This is the **x86_64-pc-windows-msvc** binary for `@z3rno/sdk`
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@z3rno/sdk-win32-x64-msvc",
3
+ "version": "0.23.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/the-ai-project-co/z3rno"
7
+ },
8
+ "os": [
9
+ "win32"
10
+ ],
11
+ "cpu": [
12
+ "x64"
13
+ ],
14
+ "main": "z3rno-sdk-native.win32-x64-msvc.node",
15
+ "files": [
16
+ "z3rno-sdk-native.win32-x64-msvc.node"
17
+ ],
18
+ "license": "Apache-2.0",
19
+ "engines": {
20
+ "node": ">= 18"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public",
24
+ "provenance": true
25
+ }
26
+ }
package/package.json CHANGED
@@ -1,60 +1,44 @@
1
1
  {
2
2
  "name": "@z3rno/sdk",
3
- "version": "0.8.1",
4
- "description": "TypeScript SDK for Z3rno — AI agent memory database",
5
- "type": "module",
6
- "main": "./dist/index.cjs",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js",
13
- "require": "./dist/index.cjs"
14
- }
3
+ "version": "0.23.0",
4
+ "license": "Apache-2.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/the-ai-project-co/z3rno"
15
8
  },
16
- "files": [
17
- "dist"
18
- ],
19
- "scripts": {
20
- "build": "tsup",
21
- "test": "vitest run",
22
- "test:watch": "vitest",
23
- "lint": "tsc --noEmit",
24
- "format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'",
25
- "format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts'",
26
- "typecheck": "tsc --noEmit",
27
- "docs": "typedoc"
9
+ "main": "index.js",
10
+ "types": "index.d.ts",
11
+ "napi": {
12
+ "name": "z3rno-sdk-native",
13
+ "triples": {
14
+ "defaults": true,
15
+ "additional": [
16
+ "aarch64-unknown-linux-gnu",
17
+ "aarch64-apple-darwin"
18
+ ]
19
+ }
28
20
  },
29
- "author": "The AI Project Co. <engineering@z3rno.dev>",
30
- "homepage": "https://z3rno.dev",
31
21
  "engines": {
32
- "node": ">=18.0.0"
22
+ "node": ">= 18"
33
23
  },
34
- "dependencies": {
35
- "zod": "^3.23.0"
24
+ "publishConfig": {
25
+ "access": "public",
26
+ "provenance": true
27
+ },
28
+ "scripts": {
29
+ "build": "napi build --platform --release",
30
+ "prepublishOnly": "napi prepublish -t npm",
31
+ "test": "node test.js",
32
+ "version": "napi version"
36
33
  },
37
34
  "devDependencies": {
38
- "@types/node": "^22.0.0",
39
- "prettier": "^3.8.0",
40
- "tsup": "^8.5.0",
41
- "typedoc": "^0.28.19",
42
- "typescript": "^5.7.0",
43
- "vitest": "^3.2.0"
35
+ "@napi-rs/cli": "^2"
44
36
  },
45
- "keywords": [
46
- "ai-agents",
47
- "memory",
48
- "vector-database",
49
- "rag",
50
- "agent-memory",
51
- "pgvector",
52
- "deno",
53
- "bun"
54
- ],
55
- "license": "Apache-2.0",
56
- "repository": {
57
- "type": "git",
58
- "url": "https://github.com/the-ai-project-co/z3rno-sdk-typescript"
37
+ "optionalDependencies": {
38
+ "@z3rno/sdk-win32-x64-msvc": "0.23.0",
39
+ "@z3rno/sdk-darwin-x64": "0.23.0",
40
+ "@z3rno/sdk-linux-x64-gnu": "0.23.0",
41
+ "@z3rno/sdk-linux-arm64-gnu": "0.23.0",
42
+ "@z3rno/sdk-darwin-arm64": "0.23.0"
59
43
  }
60
- }
44
+ }
package/src/lib.rs ADDED
@@ -0,0 +1,508 @@
1
+ #![deny(clippy::all)]
2
+
3
+ //! The real napi-rs bindings over `z3rno-engine` (0006.1/0006.2/0006.4):
4
+ //! `Client.connect()` -> `store`/`recall`/`forget`, `client.advanced.audit`.
5
+ //! Async-only — every I/O method returns a native `Promise`, run on napi-rs's
6
+ //! managed Tokio runtime (`tokio_rt` feature). `tier()` is the one exception:
7
+ //! no I/O, so it's a plain sync getter.
8
+ //!
9
+ //! Kept deliberately thin: this module maps napi's JS-facing types onto
10
+ //! `z3rno_engine::MemoryEngine`'s calls. All the actual logic (tier/uuid
11
+ //! parsing, error mapping, DTO conversion) is plain Rust functions with no
12
+ //! napi types in their signatures, so it's unit-tested below with ordinary
13
+ //! `#[test]`s — no JS runtime required to run `cargo test`.
14
+
15
+ use std::sync::Arc;
16
+
17
+ use napi::bindgen_prelude::*;
18
+ use napi_derive::napi;
19
+ use serde_json::Value as JsonValue;
20
+ use uuid::Uuid;
21
+
22
+ use z3rno_engine::backend::BackendError;
23
+ use z3rno_engine::{
24
+ AuditEvent, AuditOperation, BackendTier, ForgetProof, Memory, MemoryEngine, Tier,
25
+ };
26
+
27
+ const DEFAULT_EMBEDDED_PATH: &str = "z3rno.db";
28
+
29
+ // ---------------------------------------------------------------------
30
+ // Plain Rust helpers — no napi types, unit-tested at the bottom of this
31
+ // file with no live Node/JS runtime needed.
32
+ // ---------------------------------------------------------------------
33
+
34
+ fn parse_tier(s: &str) -> std::result::Result<Tier, String> {
35
+ match s {
36
+ "working" => Ok(Tier::Working),
37
+ "episodic" => Ok(Tier::Episodic),
38
+ "semantic" => Ok(Tier::Semantic),
39
+ "procedural" => Ok(Tier::Procedural),
40
+ other => Err(format!(
41
+ "invalid tier {other:?}: expected one of \"working\", \"episodic\", \"semantic\", \"procedural\""
42
+ )),
43
+ }
44
+ }
45
+
46
+ fn tier_to_js_str(tier: Tier) -> &'static str {
47
+ match tier {
48
+ Tier::Working => "working",
49
+ Tier::Episodic => "episodic",
50
+ Tier::Semantic => "semantic",
51
+ Tier::Procedural => "procedural",
52
+ }
53
+ }
54
+
55
+ fn backend_tier_to_js_str(tier: BackendTier) -> &'static str {
56
+ match tier {
57
+ BackendTier::Embedded => "embedded",
58
+ BackendTier::Postgres => "postgres",
59
+ }
60
+ }
61
+
62
+ fn audit_operation_to_js_str(op: AuditOperation) -> &'static str {
63
+ match op {
64
+ AuditOperation::Store => "store",
65
+ AuditOperation::Forget => "forget",
66
+ }
67
+ }
68
+
69
+ fn parse_uuid(field: &str, s: &str) -> std::result::Result<Uuid, String> {
70
+ Uuid::parse_str(s).map_err(|e| format!("invalid {field} {s:?}: {e}"))
71
+ }
72
+
73
+ /// napi-rs only implements `FromNapiValue` for `f64` (JS's one number
74
+ /// type), not `f32` — so embedding/query vectors cross the FFI boundary as
75
+ /// `Vec<f64>` and get narrowed here before reaching the engine, which
76
+ /// stores vectors as `Vec<f32>`.
77
+ fn to_f32_vec(v: Vec<f64>) -> Vec<f32> {
78
+ v.into_iter().map(|x| x as f32).collect()
79
+ }
80
+
81
+ /// Maps a `BackendError` to a `napi::Error`, preserving the original message.
82
+ /// Both variants become `Status::GenericFailure` — neither maps cleanly onto
83
+ /// one of napi's more specific statuses (there's no "not found" status, and
84
+ /// `GenericFailure` is what napi-rs itself recommends for domain errors);
85
+ /// the distinction survives in the message text instead.
86
+ fn map_backend_error(err: BackendError) -> Error {
87
+ match err {
88
+ BackendError::NotFound(msg) => {
89
+ Error::new(Status::GenericFailure, format!("not found: {msg}"))
90
+ }
91
+ BackendError::Storage(e) => {
92
+ Error::new(Status::GenericFailure, format!("storage error: {e}"))
93
+ }
94
+ }
95
+ }
96
+
97
+ fn invalid_arg(message: impl Into<String>) -> Error {
98
+ Error::new(Status::InvalidArg, message.into())
99
+ }
100
+
101
+ fn memory_to_dto(m: Memory) -> MemoryDto {
102
+ MemoryDto {
103
+ id: m.id.to_string(),
104
+ tenant_id: m.tenant_id,
105
+ tier: tier_to_js_str(m.tier).to_string(),
106
+ content: m.content,
107
+ metadata: m.metadata,
108
+ created_at: m.created_at.to_rfc3339(),
109
+ }
110
+ }
111
+
112
+ fn forget_proof_to_dto(p: ForgetProof) -> ForgetProofDto {
113
+ ForgetProofDto {
114
+ audit_event_id: p.audit_event_id.to_string(),
115
+ hash: p.hash,
116
+ }
117
+ }
118
+
119
+ fn audit_event_to_dto(e: AuditEvent) -> AuditEventDto {
120
+ AuditEventDto {
121
+ id: e.id.to_string(),
122
+ tenant_id: e.tenant_id,
123
+ operation: audit_operation_to_js_str(e.operation).to_string(),
124
+ memory_id: e.memory_id.to_string(),
125
+ at: e.at.to_rfc3339(),
126
+ prev_hash: e.prev_hash,
127
+ hash: e.hash,
128
+ }
129
+ }
130
+
131
+ fn parse_links(links: Vec<LinkInput>) -> std::result::Result<Vec<(Uuid, String)>, String> {
132
+ links
133
+ .into_iter()
134
+ .map(|l| parse_uuid("targetId", &l.target_id).map(|id| (id, l.relationship)))
135
+ .collect()
136
+ }
137
+
138
+ // ---------------------------------------------------------------------
139
+ // JS-facing DTOs.
140
+ // ---------------------------------------------------------------------
141
+
142
+ /// `client.store(...)` input. `embedding` and `links` are optional (a
143
+ /// memory with no embedding simply isn't recallable by similarity search;
144
+ /// no links means no graph edges).
145
+ #[napi(object)]
146
+ pub struct StoreOptions {
147
+ pub tenant_id: String,
148
+ /// One of "working" | "episodic" | "semantic" | "procedural".
149
+ pub tier: String,
150
+ pub content: String,
151
+ pub embedding: Option<Vec<f64>>,
152
+ pub metadata: JsonValue,
153
+ pub links: Option<Vec<LinkInput>>,
154
+ }
155
+
156
+ /// One graph edge from the memory being stored to an existing memory.
157
+ #[napi(object)]
158
+ pub struct LinkInput {
159
+ pub target_id: String,
160
+ pub relationship: String,
161
+ }
162
+
163
+ /// `client.recall(...)` input.
164
+ #[napi(object)]
165
+ pub struct RecallOptions {
166
+ pub tenant_id: String,
167
+ pub query: Vec<f64>,
168
+ pub k: u32,
169
+ }
170
+
171
+ /// `client.forget(...)` input.
172
+ #[napi(object)]
173
+ pub struct ForgetOptions {
174
+ pub tenant_id: String,
175
+ pub id: String,
176
+ }
177
+
178
+ /// `Client.connect(...)` input. `backend` defaults to `"embedded"`; `path`
179
+ /// defaults to `"z3rno.db"` in the current working directory when the
180
+ /// embedded backend is used and `path` is omitted.
181
+ #[napi(object)]
182
+ pub struct ConnectOptions {
183
+ /// One of "embedded" | "postgres". Defaults to "embedded".
184
+ pub backend: Option<String>,
185
+ pub path: Option<String>,
186
+ pub connection_string: Option<String>,
187
+ }
188
+
189
+ #[napi(object)]
190
+ pub struct MemoryDto {
191
+ pub id: String,
192
+ pub tenant_id: String,
193
+ pub tier: String,
194
+ pub content: String,
195
+ pub metadata: JsonValue,
196
+ /// RFC 3339 timestamp.
197
+ pub created_at: String,
198
+ }
199
+
200
+ #[napi(object)]
201
+ pub struct ForgetProofDto {
202
+ pub audit_event_id: String,
203
+ pub hash: String,
204
+ }
205
+
206
+ #[napi(object)]
207
+ pub struct AuditEventDto {
208
+ pub id: String,
209
+ pub tenant_id: String,
210
+ /// "store" | "forget".
211
+ pub operation: String,
212
+ pub memory_id: String,
213
+ /// RFC 3339 timestamp.
214
+ pub at: String,
215
+ pub prev_hash: Option<String>,
216
+ pub hash: String,
217
+ }
218
+
219
+ // ---------------------------------------------------------------------
220
+ // Client.
221
+ // ---------------------------------------------------------------------
222
+
223
+ /// A connected z3rno client. Construct with `Client.connect(...)`, never
224
+ /// `new Client()` — connecting the embedded backend is sync but connecting
225
+ /// Postgres needs an `await`, so both go through one consistent async
226
+ /// factory rather than a sync constructor plus a separate async path.
227
+ #[napi]
228
+ pub struct Client {
229
+ engine: Arc<MemoryEngine>,
230
+ }
231
+
232
+ #[napi]
233
+ impl Client {
234
+ #[napi(factory)]
235
+ pub async fn connect(options: Option<ConnectOptions>) -> Result<Client> {
236
+ let options = options.unwrap_or(ConnectOptions {
237
+ backend: None,
238
+ path: None,
239
+ connection_string: None,
240
+ });
241
+ let backend = options.backend.as_deref().unwrap_or("embedded");
242
+ let engine = match backend {
243
+ "embedded" => {
244
+ let path = options
245
+ .path
246
+ .unwrap_or_else(|| DEFAULT_EMBEDDED_PATH.to_string());
247
+ MemoryEngine::embedded(path).map_err(map_backend_error)?
248
+ }
249
+ "postgres" => {
250
+ let connection_string = options.connection_string.ok_or_else(|| {
251
+ invalid_arg("connectionString is required when backend is \"postgres\"")
252
+ })?;
253
+ MemoryEngine::postgres(&connection_string)
254
+ .await
255
+ .map_err(map_backend_error)?
256
+ }
257
+ other => {
258
+ return Err(invalid_arg(format!(
259
+ "invalid backend {other:?}: expected \"embedded\" or \"postgres\""
260
+ )))
261
+ }
262
+ };
263
+ Ok(Client {
264
+ engine: Arc::new(engine),
265
+ })
266
+ }
267
+
268
+ /// Which tenant-isolation guarantee this client's backend provides:
269
+ /// "embedded" or "postgres". No I/O, so this is sync (not a Promise).
270
+ #[napi]
271
+ pub fn tier(&self) -> String {
272
+ backend_tier_to_js_str(self.engine.tier()).to_string()
273
+ }
274
+
275
+ #[napi]
276
+ pub async fn store(&self, options: StoreOptions) -> Result<MemoryDto> {
277
+ let tier = parse_tier(&options.tier).map_err(invalid_arg)?;
278
+ let links = parse_links(options.links.unwrap_or_default()).map_err(invalid_arg)?;
279
+ let memory = self
280
+ .engine
281
+ .store(
282
+ &options.tenant_id,
283
+ tier,
284
+ options.content,
285
+ options.embedding.map(to_f32_vec),
286
+ options.metadata,
287
+ links,
288
+ )
289
+ .await
290
+ .map_err(map_backend_error)?;
291
+ Ok(memory_to_dto(memory))
292
+ }
293
+
294
+ #[napi]
295
+ pub async fn recall(&self, options: RecallOptions) -> Result<Vec<MemoryDto>> {
296
+ let memories = self
297
+ .engine
298
+ .recall(
299
+ &options.tenant_id,
300
+ to_f32_vec(options.query),
301
+ options.k as usize,
302
+ )
303
+ .await
304
+ .map_err(map_backend_error)?;
305
+ Ok(memories.into_iter().map(memory_to_dto).collect())
306
+ }
307
+
308
+ #[napi]
309
+ pub async fn forget(&self, options: ForgetOptions) -> Result<Option<ForgetProofDto>> {
310
+ let id = parse_uuid("id", &options.id).map_err(invalid_arg)?;
311
+ let proof = self
312
+ .engine
313
+ .forget(&options.tenant_id, id)
314
+ .await
315
+ .map_err(map_backend_error)?;
316
+ Ok(proof.map(forget_proof_to_dto))
317
+ }
318
+
319
+ /// Advanced operations namespace — currently just `.audit(tenantId)`,
320
+ /// the one advanced operation the engine has. Mirrors the engine's own
321
+ /// `MemoryEngine::advanced()` two-tier verb surface.
322
+ #[napi(getter)]
323
+ pub fn advanced(&self) -> Advanced {
324
+ Advanced {
325
+ engine: self.engine.clone(),
326
+ }
327
+ }
328
+ }
329
+
330
+ #[napi]
331
+ pub struct Advanced {
332
+ engine: Arc<MemoryEngine>,
333
+ }
334
+
335
+ #[napi]
336
+ impl Advanced {
337
+ /// The tenant's full audit chain, oldest first.
338
+ #[napi]
339
+ pub async fn audit(&self, tenant_id: String) -> Result<Vec<AuditEventDto>> {
340
+ let events = self
341
+ .engine
342
+ .advanced()
343
+ .audit(&tenant_id)
344
+ .await
345
+ .map_err(map_backend_error)?;
346
+ Ok(events.into_iter().map(audit_event_to_dto).collect())
347
+ }
348
+ }
349
+
350
+ #[cfg(test)]
351
+ mod tests {
352
+ use super::*;
353
+
354
+ #[test]
355
+ fn parse_tier_accepts_all_four_snake_case_variants() {
356
+ assert_eq!(parse_tier("working"), Ok(Tier::Working));
357
+ assert_eq!(parse_tier("episodic"), Ok(Tier::Episodic));
358
+ assert_eq!(parse_tier("semantic"), Ok(Tier::Semantic));
359
+ assert_eq!(parse_tier("procedural"), Ok(Tier::Procedural));
360
+ }
361
+
362
+ #[test]
363
+ fn parse_tier_rejects_unknown_value_with_descriptive_message() {
364
+ let err = parse_tier("bogus").unwrap_err();
365
+ assert!(
366
+ err.contains("bogus"),
367
+ "message should name the bad value: {err}"
368
+ );
369
+ assert!(
370
+ err.contains("working"),
371
+ "message should list valid values: {err}"
372
+ );
373
+ }
374
+
375
+ #[test]
376
+ fn tier_round_trips_through_js_str() {
377
+ for tier in [
378
+ Tier::Working,
379
+ Tier::Episodic,
380
+ Tier::Semantic,
381
+ Tier::Procedural,
382
+ ] {
383
+ let s = tier_to_js_str(tier);
384
+ assert_eq!(parse_tier(s), Ok(tier));
385
+ }
386
+ }
387
+
388
+ #[test]
389
+ fn backend_tier_to_js_str_matches_expected_strings() {
390
+ assert_eq!(backend_tier_to_js_str(BackendTier::Embedded), "embedded");
391
+ assert_eq!(backend_tier_to_js_str(BackendTier::Postgres), "postgres");
392
+ }
393
+
394
+ #[test]
395
+ fn audit_operation_to_js_str_matches_expected_strings() {
396
+ assert_eq!(audit_operation_to_js_str(AuditOperation::Store), "store");
397
+ assert_eq!(audit_operation_to_js_str(AuditOperation::Forget), "forget");
398
+ }
399
+
400
+ #[test]
401
+ fn to_f32_vec_narrows_each_element() {
402
+ assert_eq!(
403
+ to_f32_vec(vec![0.1, 0.2, 1.0]),
404
+ vec![0.1f32, 0.2f32, 1.0f32]
405
+ );
406
+ }
407
+
408
+ #[test]
409
+ fn parse_uuid_accepts_valid_uuid() {
410
+ let id = Uuid::new_v4();
411
+ assert_eq!(parse_uuid("id", &id.to_string()), Ok(id));
412
+ }
413
+
414
+ #[test]
415
+ fn parse_uuid_rejects_garbage_with_field_name_in_message() {
416
+ let err = parse_uuid("targetId", "not-a-uuid").unwrap_err();
417
+ assert!(
418
+ err.contains("targetId"),
419
+ "message should name the field: {err}"
420
+ );
421
+ }
422
+
423
+ #[test]
424
+ fn parse_links_parses_target_ids_and_preserves_relationship() {
425
+ let id = Uuid::new_v4();
426
+ let links = vec![LinkInput {
427
+ target_id: id.to_string(),
428
+ relationship: "caused_by".to_string(),
429
+ }];
430
+ let parsed = parse_links(links).unwrap();
431
+ assert_eq!(parsed, vec![(id, "caused_by".to_string())]);
432
+ }
433
+
434
+ #[test]
435
+ fn parse_links_surfaces_which_target_id_is_invalid() {
436
+ let links = vec![LinkInput {
437
+ target_id: "nope".to_string(),
438
+ relationship: "r".to_string(),
439
+ }];
440
+ let err = parse_links(links).unwrap_err();
441
+ assert!(err.contains("nope"));
442
+ }
443
+
444
+ #[test]
445
+ fn map_backend_error_preserves_not_found_message() {
446
+ let err = map_backend_error(BackendError::NotFound("memory abc123".to_string()));
447
+ assert_eq!(err.status, Status::GenericFailure);
448
+ assert!(err.reason.contains("memory abc123"));
449
+ }
450
+
451
+ #[test]
452
+ fn map_backend_error_preserves_storage_error_message() {
453
+ let err = map_backend_error(BackendError::Storage(anyhow::anyhow!("disk full")));
454
+ assert_eq!(err.status, Status::GenericFailure);
455
+ assert!(err.reason.contains("disk full"));
456
+ }
457
+
458
+ #[test]
459
+ fn invalid_arg_uses_invalid_arg_status() {
460
+ let err = invalid_arg("bad input");
461
+ assert_eq!(err.status, Status::InvalidArg);
462
+ assert_eq!(err.reason, "bad input");
463
+ }
464
+
465
+ #[test]
466
+ fn memory_to_dto_maps_every_field() {
467
+ let id = Uuid::new_v4();
468
+ let memory = Memory {
469
+ id,
470
+ tenant_id: "t1".to_string(),
471
+ tier: Tier::Semantic,
472
+ content: "hello".to_string(),
473
+ metadata: serde_json::json!({"k": "v"}),
474
+ created_at: chrono::Utc::now(),
475
+ };
476
+ let dto = memory_to_dto(memory.clone());
477
+ assert_eq!(dto.id, id.to_string());
478
+ assert_eq!(dto.tenant_id, "t1");
479
+ assert_eq!(dto.tier, "semantic");
480
+ assert_eq!(dto.content, "hello");
481
+ assert_eq!(dto.metadata, serde_json::json!({"k": "v"}));
482
+ assert_eq!(dto.created_at, memory.created_at.to_rfc3339());
483
+ }
484
+
485
+ #[test]
486
+ fn forget_proof_to_dto_maps_every_field() {
487
+ let event_id = Uuid::new_v4();
488
+ let dto = forget_proof_to_dto(ForgetProof {
489
+ audit_event_id: event_id,
490
+ hash: "deadbeef".to_string(),
491
+ });
492
+ assert_eq!(dto.audit_event_id, event_id.to_string());
493
+ assert_eq!(dto.hash, "deadbeef");
494
+ }
495
+
496
+ #[test]
497
+ fn audit_event_to_dto_maps_every_field_including_none_prev_hash() {
498
+ let event = AuditEvent::next("t1", AuditOperation::Store, Uuid::new_v4(), None);
499
+ let dto = audit_event_to_dto(event.clone());
500
+ assert_eq!(dto.id, event.id.to_string());
501
+ assert_eq!(dto.tenant_id, "t1");
502
+ assert_eq!(dto.operation, "store");
503
+ assert_eq!(dto.memory_id, event.memory_id.to_string());
504
+ assert_eq!(dto.at, event.at.to_rfc3339());
505
+ assert_eq!(dto.prev_hash, None);
506
+ assert_eq!(dto.hash, event.hash);
507
+ }
508
+ }