// Acceptance for P2.3 (round-trip) — the in-memory repository persisted to // `/db.json` via `std.json` and reloaded into a FRESH repository. // // Asserts: // 1. Every entity (app + bundle ids, release, two artifacts, channel, // audit event) survives save -> reload field-for-field. // 2. db.json is valid JSON, re-parseable by `std.json` (re-parse it). // 3. Re-saving the reloaded repo yields BYTE-IDENTICAL db.json — the // stable (insertion-order) key-order guarantee. // Uses a fresh `` under `.sx-tmp/` and cleans up. Exits 0 only if // every assertion holds (process.assert aborts otherwise). #import "modules/std.sx"; #import "modules/std/json.sx"; #import "modules/std/fs.sx"; process :: #import "modules/std/process.sx"; #import "../src/domain/platform.sx"; #import "../src/domain/app.sx"; #import "../src/domain/release.sx"; #import "../src/domain/artifact.sx"; #import "../src/domain/channel.sx"; #import "../src/domain/audit.sx"; #import "../src/repo/repo.sx"; #import "../src/repo/db.sx"; // ── Field-for-field equality over the domain entities ──────────────── bundle_eq :: (a: BundleId, b: BundleId) -> bool { return a.platform == b.platform and a.value == b.value; } app_eq :: (a: App, b: App) -> bool { if a.id != b.id { return false; } if a.slug != b.slug { return false; } if a.display_name != b.display_name { return false; } if a.owner != b.owner { return false; } if a.visibility != b.visibility { return false; } if a.created_at != b.created_at { return false; } if a.updated_at != b.updated_at { return false; } if a.bundle_ids.len != b.bundle_ids.len { return false; } i := 0; while i < a.bundle_ids.len { if !bundle_eq(a.bundle_ids.items[i], b.bundle_ids.items[i]) { return false; } i += 1; } return true; } release_eq :: (a: Release, b: Release) -> bool { return a.id == b.id and a.app_id == b.app_id and a.version == b.version and a.build == b.build and a.channel == b.channel and a.notes == b.notes and a.created_by == b.created_by and a.created_at == b.created_at and a.published_at == b.published_at; } artifact_eq :: (a: Artifact, b: Artifact) -> bool { return a.id == b.id and a.app_id == b.app_id and a.release_id == b.release_id and a.platform == b.platform and a.filename == b.filename and a.content_type == b.content_type and a.size_bytes == b.size_bytes and a.sha256 == b.sha256 and a.storage_key == b.storage_key and a.metadata == b.metadata and a.validation_status == b.validation_status; } channel_eq :: (a: Channel, b: Channel) -> bool { return a.app_id == b.app_id and a.name == b.name and a.current_release_id == b.current_release_id and a.policy == b.policy and a.rollout_percent == b.rollout_percent; } audit_eq :: (a: AuditEvent, b: AuditEvent) -> bool { return a.id == b.id and a.actor == b.actor and a.action == b.action and a.target_type == b.target_type and a.target_id == b.target_id and a.metadata == b.metadata and a.created_at == b.created_at; } // ── Fixtures ───────────────────────────────────────────────────────── the_app :: (alloc: Allocator) -> App { a : App = .{ id = "app_01", slug = "acme-app", display_name = "Acme App", owner = "user_01", visibility = .public, created_at = 1700000000, updated_at = 1700000050, }; a.bundle_ids.append(BundleId.{ platform = .ios, value = "co.acme.app" }, alloc); a.bundle_ids.append(BundleId.{ platform = .android_apk, value = "co.acme.app.android" }, alloc); return a; } the_release :: () -> Release { return Release.{ id = "rel_01", app_id = "app_01", version = "1.2.3-beta.1", build = 42, channel = "stable", notes = "first cut\nwith a newline", created_by = "user_01", created_at = 1700000100, published_at = 1700000200, }; } apk_artifact :: () -> Artifact { return Artifact.{ id = "art_apk", app_id = "app_01", release_id = "rel_01", platform = .android_apk, filename = "acme.apk", content_type = "application/vnd.android.package-archive", size_bytes = 10485760, sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", storage_key = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", metadata = "{\"min_os\":\"14\"}", validation_status = .valid, }; } ipa_artifact :: () -> Artifact { return Artifact.{ id = "art_ipa", app_id = "app_01", release_id = "rel_01", platform = .ios, filename = "acme.ipa", content_type = "application/octet-stream", size_bytes = 20971520, sha256 = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", storage_key = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", metadata = "", validation_status = .pending, }; } the_channel :: () -> Channel { return Channel.{ app_id = "app_01", name = "stable", current_release_id = "rel_01", policy = .percentage, rollout_percent = 25, }; } the_event :: () -> AuditEvent { return AuditEvent.{ id = "ev_01", actor = "user_01", action = "publish", target_type = "release", target_id = "rel_01", metadata = "{\"channel\":\"stable\"}", created_at = 1700000300, }; } main :: () -> i32 { root := ".sx-tmp/repo-roundtrip"; root2 := ".sx-tmp/repo-roundtrip-2"; process.run(concat("rm -rf ", root)); process.run(concat("rm -rf ", root2)); // ── Build the original model ───────────────────────────────────── repo := Repo.init(); app0 := the_app(repo.own_allocator); rel0 := the_release(); apk0 := apk_artifact(); ipa0 := ipa_artifact(); chan0 := the_channel(); ev0 := the_event(); repo.create_app(app0); repo.create_release(rel0); repo.create_artifact(apk0); repo.create_artifact(ipa0); repo.create_channel(chan0); repo.create_audit_event(ev0); // ── Persist ────────────────────────────────────────────────────── serr := false; save(repo, root) catch { serr = true; }; process.assert(!serr, "save must succeed"); process.assert(fs.exists(path_join(root, "db.json")), "db.json must exist after save"); // ── 2. db.json is valid JSON re-parseable by std.json ──────────── raw := fs.read_file(path_join(root, "db.json")); process.assert(raw != null, "db.json must be readable"); bytes := raw!; gpa := GPA.init(); arena := Arena.init(xx gpa, 65536); _, perr := parse(bytes, xx arena); process.assert(!perr, "db.json must be valid JSON re-parseable by std.json"); arena.deinit(); print(" db.json is valid JSON ({} bytes)\n", bytes.len); // ── 1. Reload into a FRESH repo and compare field-for-field ────── repo2, lerr := load(root); if lerr { process.assert(false, "load must succeed"); return 1; } process.assert(repo2.apps.len == 1, "one app reloaded"); process.assert(repo2.releases.len == 1, "one release reloaded"); process.assert(repo2.artifacts.len == 2, "two artifacts reloaded"); process.assert(repo2.channels.len == 1, "one channel reloaded"); process.assert(repo2.audit_events.len == 1, "one audit event reloaded"); process.assert(app_eq(repo2.apps.items[0], app0), "app survives round-trip"); rg := repo2.get_release("rel_01"); process.assert(rg != null, "release found by id after reload"); process.assert(release_eq(rg!, rel0), "release survives round-trip"); // find_artifact_by_digest — the P2.2 content-address lookup. ag := repo2.find_artifact_by_digest(apk0.sha256); process.assert(ag != null, "apk artifact found by digest after reload"); process.assert(artifact_eq(ag!, apk0), "apk artifact survives round-trip"); ig := repo2.find_artifact_by_digest(ipa0.sha256); process.assert(ig != null, "ipa artifact found by digest after reload"); process.assert(artifact_eq(ig!, ipa0), "ipa artifact survives round-trip"); cg := repo2.get_channel("app_01", "stable"); process.assert(cg != null, "channel found after reload"); process.assert(channel_eq(cg!, chan0), "channel survives round-trip"); process.assert(audit_eq(repo2.audit_events.items[0], ev0), "audit event survives round-trip"); // find_app_by_slug — the slug lookup. sg := repo2.find_app_by_slug("acme-app"); process.assert(sg != null, "app found by slug after reload"); sga := sg!; process.assert(sga.id == "app_01", "slug lookup returns the right app"); print(" reloaded model equals original (every field)\n"); // ── 3. Re-save the reloaded repo -> byte-identical db.json ─────── serr2 := false; save(repo2, root2) catch { serr2 = true; }; process.assert(!serr2, "re-save must succeed"); raw2 := fs.read_file(path_join(root2, "db.json")); process.assert(raw2 != null, "re-saved db.json must be readable"); process.assert(raw2! == bytes, "re-save is byte-identical (stable key order)"); print(" re-save is byte-identical (stable key order)\n"); // ── cleanup ────────────────────────────────────────────────────── process.run(concat("rm -rf ", root)); process.run(concat("rm -rf ", root2)); print("repo_roundtrip: ALL CASES PASS\n"); return 0; }