// Pinned acceptance for P3.4b — failure paths are loud and machine-readable. // // Drives the BUILT `build/dist` binary (via `process.run`, like // publish_persist.sx) through every publish failure class the slice plan // names — a malformed manifest, a missing artifact file, an unknown // platform id, a declared-size mismatch, and a declared-sha256 mismatch — // and asserts the P3.4b contract for each: // // * exit code 1 (command failed; NOT the parser's EX_USAGE 64), // * stdout under `--json` is a SINGLE JSON object // `{"status":"error","error":{"code":,"message":...}}`, // * nothing is persisted: a fresh store gains no db.json. // // The no-partial-state crux is then asserted against a NON-EMPTY store: // publish version A successfully, fail version B on a digest mismatch into // the SAME store, and require db.json byte-state unchanged — one release, // channel still pointing at A (no partially-published release, no moved // channel pointer). #import "modules/std.sx"; #import "modules/std/json.sx"; process :: #import "modules/std/process.sx"; fs :: #import "modules/std/fs.sx"; STORE :: ".sx-tmp/publish_fail"; MDIR :: ".sx-tmp/publish_fail_m"; // Manifests share the committed 5-byte fixtures; paths resolve relative to // the manifest's own directory (MDIR), so ../../examples/fixtures reaches // them. GOOD_A :: "{\"app\":\"acme-app\",\"version\":\"1.2.3\",\"channel\":\"stable\",\"artifacts\":[{\"platform\":\"android_apk\",\"path\":\"../../examples/fixtures/acme-1.2.3-android.apk\"}]}"; BAD_DIGEST :: "{\"app\":\"acme-app\",\"version\":\"1.2.4\",\"channel\":\"stable\",\"artifacts\":[{\"platform\":\"android_apk\",\"path\":\"../../examples/fixtures/acme-1.2.3-android.apk\",\"sha256\":\"0000000000000000000000000000000000000000000000000000000000000000\"}]}"; BAD_SIZE :: "{\"app\":\"acme-app\",\"version\":\"1.2.5\",\"channel\":\"stable\",\"artifacts\":[{\"platform\":\"android_apk\",\"path\":\"../../examples/fixtures/acme-1.2.3-android.apk\",\"size\":9999}]}"; BAD_PLATFORM:: "{\"app\":\"acme-app\",\"version\":\"1.2.6\",\"channel\":\"stable\",\"artifacts\":[{\"platform\":\"playstation\",\"path\":\"../../examples/fixtures/acme-1.2.3-android.apk\"}]}"; NO_ARTIFACT :: "{\"app\":\"acme-app\",\"version\":\"1.2.7\",\"channel\":\"stable\",\"artifacts\":[{\"platform\":\"android_apk\",\"path\":\"no-such-file.apk\"}]}"; NOT_JSON :: "this is not a manifest"; get :: (o: Object, key: string) -> Value { i := 0; while i < o.len { if o.items[i].key == key { return o.items[i].val; } i += 1; } process.assert(false, concat("missing json key: ", key)); dummy : Value = .null_; return dummy; } get_str :: (o: Object, key: string) -> string { return get(o, key).str; } get_obj :: (o: Object, key: string) -> Object { return get(o, key).object; } get_arr :: (o: Object, key: string) -> Array { return get(o, key).array; } publish_cmd :: (mpath: string, store: string) -> string { c := concat("build/dist ci publish --manifest ", mpath); c = concat(c, concat(" --local-store ", store)); return concat(c, " --json 2>/dev/null"); } // Write `body` to `path` via the shell (single-quoted, so the JSON's double // quotes pass through literally). write_file :: (path: string, body: string) { cmd := concat(concat(concat("printf '%s' '", body), "' > "), path); process.run(cmd); } // Run one failing publish and assert the full failure contract: exit 1, // stdout is exactly one JSON error object, and its error.code equals // `want_code`. `store` distinguishes the per-case fresh stores. assert_fails :: (label: string, mpath: string, store: string, want_code: string, scratch: Allocator) { r := process.run(publish_cmd(mpath, store)); process.assert(r != null, concat("spawn failed: ", label)); res := r!; process.assert(res.exit_code == 1, concat("must exit 1 (command failed): ", label)); v, e := parse(res.stdout, scratch); if e { process.assert(false, concat("stdout must be one JSON object: ", label)); return; } o := v.object; process.assert(get_str(o, "status") == "error", concat("status must be \"error\": ", label)); eo := get_obj(o, "error"); process.assert(get_str(eo, "code") == want_code, concat("error.code mismatch: ", label)); process.assert(get_str(eo, "message").len > 0, concat("error.message must be non-empty: ", label)); out(concat(concat(" ", label), ": exit 1 + JSON error ok\n")); } main :: () -> i32 { gpa := GPA.init(); arena := Arena.init(xx gpa, 1 << 20); defer arena.deinit(); process.run(concat("rm -rf ", STORE)); process.run(concat("rm -rf ", MDIR)); process.run(concat("mkdir -p ", MDIR)); write_file(path_join(MDIR, "good_a.json"), GOOD_A); write_file(path_join(MDIR, "bad_digest.json"), BAD_DIGEST); write_file(path_join(MDIR, "bad_size.json"), BAD_SIZE); write_file(path_join(MDIR, "bad_platform.json"), BAD_PLATFORM); write_file(path_join(MDIR, "no_artifact.json"), NO_ARTIFACT); write_file(path_join(MDIR, "not_json.json"), NOT_JSON); // ── each failure class: exit 1 + the precise dotted code, into a // fresh per-case store that must gain NO db.json ──────────────── assert_fails("digest mismatch", path_join(MDIR, "bad_digest.json"), concat(STORE, "-digest"), "validation.digest_mismatch", xx arena); assert_fails("size mismatch", path_join(MDIR, "bad_size.json"), concat(STORE, "-size"), "validation.size_mismatch", xx arena); assert_fails("unknown platform", path_join(MDIR, "bad_platform.json"), concat(STORE, "-platform"), "manifest.unknown_platform", xx arena); assert_fails("missing artifact", path_join(MDIR, "no_artifact.json"), concat(STORE, "-missing"), "manifest.missing_artifact", xx arena); assert_fails("malformed manifest",path_join(MDIR, "not_json.json"), concat(STORE, "-badjson"), "manifest.bad_json", xx arena); process.assert(!fs.exists(concat(STORE, "-digest/db.json")), "failed publish into a fresh store must not create db.json"); // ── no-partial-state against a NON-EMPTY store ──────────────────── // Publish A successfully, then fail B on a digest mismatch into the // SAME store: db.json must be unchanged (one release, channel → A). ra := process.run(publish_cmd(path_join(MDIR, "good_a.json"), STORE)); process.assert(ra != null, "spawn publish A failed"); process.assert(ra!.exit_code == 0, "publish A must exit 0"); rb := process.run(publish_cmd(path_join(MDIR, "bad_digest.json"), STORE)); process.assert(rb != null, "spawn failing publish B failed"); process.assert(rb!.exit_code == 1, "publish B must exit 1 (digest mismatch)"); db_bytes := fs.read_file(path_join(STORE, "db.json")); process.assert(db_bytes != null, "db.json from publish A must still exist"); dv, de := parse(db_bytes!, xx arena); if de { process.assert(false, "db.json must be valid JSON"); return 1; } dbo := dv.object; process.assert(get_arr(dbo, "releases").len == 1, "after failed B: db still has ONE release (A)"); chans := get_arr(dbo, "channels"); process.assert(chans.len == 1, "after failed B: one channel"); process.assert(get_str(chans.items[0].object, "current_release_id") == "rel-acme-app-1.2.3", "after failed B: channel still points at A (no moved pointer)"); print(" non-empty store: failed publish left db.json unchanged\n"); process.run(concat("rm -rf ", concat(STORE, "-digest"))); process.run(concat("rm -rf ", concat(STORE, "-size"))); process.run(concat("rm -rf ", concat(STORE, "-platform"))); process.run(concat("rm -rf ", concat(STORE, "-missing"))); process.run(concat("rm -rf ", concat(STORE, "-badjson"))); process.run(concat("rm -rf ", STORE)); process.run(concat("rm -rf ", MDIR)); print("publish_fail: ALL CASES PASS\n"); return 0; }