// Pinned acceptance for P4.6 — install pages with accurate iOS modes // (subplan 04 Slice 5; PLAN.md "iOS Install Policy"). // // Publishes the APK+IPA example into a fresh store, starts the BUILT // server, and walks `dist app set` through the three iOS modes asserting // the page and OTA manifest at each step: // // * default (artifact_only): the IPA is a labeled download that // explicitly does NOT claim on-device install; manifest.plist is 404 // (install.not_enterprise). The APK row shows its sha256. // * testflight: setting the mode without a URL exits 1 // (app.testflight_url_required); with one, the page links TestFlight. // * enterprise: without a bundle id exits 1 (app.bundle_id_required); // with one, the page carries the itms-services deep link and // manifest.plist serves XML naming bundle id, version, and an // https:///download/ package URL. Flipping the mode back // 404s the plist again — all without restarting the server. // * UA detection: an iPhone UA marks the iOS section, an Android UA // the Android section. // * 404s: unknown slug/channel, channel-less path; the index links // every channel to its install page. #import "modules/std.sx"; #import "modules/std/json.sx"; process :: #import "modules/std/process.sx"; fs :: #import "modules/std/fs.sx"; hash :: #import "modules/std/hash.sx"; STORE :: ".sx-tmp/server_install"; PORT :: "18797"; BASE :: "http://127.0.0.1:18797"; 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; } contains :: (hay: string, needle: string) -> bool { if needle.len == 0 { return true; } if needle.len > hay.len { return false; } i := 0; while i + needle.len <= hay.len { j := 0; ok := true; while j < needle.len { if hay[i + j] != needle[j] { ok = false; break; } j += 1; } if ok { return true; } i += 1; } return false; } parse_body :: (body: string, what: string, scratch: Allocator) -> Object { v, e := parse(body, scratch); if e { process.assert(false, concat("must be valid JSON: ", what)); dummy : Object = .{}; return dummy; } return v.object; } fetch :: (path: string) -> string { r := process.run(concat(concat("curl -s -m 2 ", BASE), path)); process.assert(r != null, concat("curl spawn failed: ", path)); return r!.stdout; } fetch_code :: (path: string) -> string { r := process.run(concat(concat("curl -s -m 2 -o /dev/null -w '%{http_code}' ", BASE), path)); process.assert(r != null, concat("curl spawn failed: ", path)); return r!.stdout; } fetch_ua :: (path: string, ua: string) -> string { cmd := concat("curl -s -m 2 -A '", concat(ua, "' ")); r := process.run(concat(cmd, concat(BASE, path))); process.assert(r != null, concat("curl-ua spawn failed: ", path)); return r!.stdout; } // Run `dist app set` with extra `flags`; returns the run result. app_set :: (flags: string) -> ?process.ProcessResult { cmd := "build/dist app set --app acme-app --local-store .sx-tmp/server_install "; return process.run(concat(cmd, concat(flags, " --json 2>/dev/null"))); } assert_set_fails :: (r: ?process.ProcessResult, code: string, what: string, scratch: Allocator) { process.assert(r != null, concat("spawn failed: ", what)); process.assert(r!.exit_code == 1, concat("must exit 1: ", what)); o := parse_body(r!.stdout, what, scratch); process.assert(get_str(get_obj(o, "error"), "code") == code, concat(concat("error code must be ", code), concat(": ", what))); } // sha256 of a fixture file as a heap string. file_sha :: (path: string) -> string { b := fs.read_file(path); process.assert(b != null, concat("fixture must be readable: ", path)); d := hash.sha256_hex(b!); v := string.{ ptr = @d[0], len = 64 }; return substr(v, 0, 64); } main :: () -> i32 { gpa := GPA.init(); arena := Arena.init(xx gpa, 1 << 20); defer arena.deinit(); process.run("pkill -f 'dist server run --local-store .sx-tmp/server_install' 2>/dev/null"); process.run(concat("rm -rf ", STORE)); pub := process.run("build/dist ci publish --manifest examples/dist.json --local-store .sx-tmp/server_install --json 2>/dev/null >/dev/null"); process.assert(pub != null and pub!.exit_code == 0, "publish must exit 0"); apk_sha := file_sha("examples/fixtures/acme-1.2.3-android.apk"); ipa_sha := file_sha("examples/fixtures/acme-1.2.3-ios.ipa"); sp := process.run(concat(concat(concat("sh -c 'build/dist server run --local-store ", STORE), concat(concat(" --port ", PORT), " >/dev/null 2>&1 & echo $!'")), "")); process.assert(sp != null, "server spawn failed"); pid := sp!.stdout; ready := false; tries := 0; while tries < 50 { if fetch_code("/healthz") == "200" { ready = true; break; } process.run("sleep 0.2"); tries += 1; } process.assert(ready, "server must answer /healthz within 10s"); print(" server up\n"); PAGE :: "/install/acme-app/stable"; PLIST :: "/install/acme-app/stable/manifest.plist"; // ── default: artifact_only, honest labeling ─────────────────────── process.assert(fetch_code(PAGE) == "200", "install page is 200"); page := fetch(PAGE); process.assert(contains(page, "cannot be installed on an iPhone"), "artifact_only page disclaims on-device install"); process.assert(!contains(page, "itms-services") and !contains(page, "TestFlight"), "artifact_only page offers no install action"); process.assert(contains(page, apk_sha), "APK sha256 is visible on the page"); process.assert(contains(page, concat("/download/", ipa_sha)), "IPA is downloadable"); process.assert(fetch_code(PLIST) == "404", "plist is 404 outside enterprise mode"); pe := parse_body(fetch(PLIST), "plist error", xx arena); process.assert(get_str(get_obj(pe, "error"), "code") == "install.not_enterprise", "plist names install.not_enterprise"); print(" artifact_only: labeled honestly, sha visible, no plist\n"); // ── testflight ───────────────────────────────────────────────────── assert_set_fails(app_set("--ios-mode testflight"), "app.testflight_url_required", "testflight without url", xx arena); tf := app_set("--ios-mode testflight --testflight-url https://testflight.apple.com/join/abc123"); process.assert(tf != null and tf!.exit_code == 0, "testflight set must exit 0"); page2 := fetch(PAGE); process.assert(contains(page2, "Open in TestFlight"), "testflight page links TestFlight"); process.assert(contains(page2, "https://testflight.apple.com/join/abc123"), "page carries the TestFlight URL"); print(" testflight: precondition gated, page links Apple's flow\n"); // ── enterprise ───────────────────────────────────────────────────── assert_set_fails(app_set("--ios-mode enterprise"), "app.bundle_id_required", "enterprise without bundle id", xx arena); en := app_set("--ios-mode enterprise --ios-bundle-id co.acme.app"); process.assert(en != null and en!.exit_code == 0, "enterprise set must exit 0"); page3 := fetch(PAGE); process.assert(contains(page3, "itms-services://"), "enterprise page carries the OTA deep link"); process.assert(fetch_code(PLIST) == "200", "enterprise plist is 200"); plist := fetch(PLIST); process.assert(contains(plist, "bundle-identifierco.acme.app"), "plist names the bundle id"); process.assert(contains(plist, "bundle-version1.2.3"), "plist names the release version"); process.assert(contains(plist, concat("https://127.0.0.1:18797/download/", ipa_sha)), "plist package URL is https and content-addressed"); print(" enterprise: plist serves bundle id, version, https package URL\n"); // mode change is visible without a restart (per-request reload) back := app_set("--ios-mode testflight"); process.assert(back != null and back!.exit_code == 0, "flip back to testflight must exit 0"); process.assert(fetch_code(PLIST) == "404", "plist 404s again after leaving enterprise mode"); print(" mode flips apply without restart\n"); // ── UA detection ─────────────────────────────────────────────────── ipage := fetch_ua(PAGE, "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)"); process.assert(contains(ipage, "

iOS"), "iPhone UA marks the iOS section"); apage := fetch_ua(PAGE, "Mozilla/5.0 (Linux; Android 14; Pixel 8)"); process.assert(contains(apage, "

Android"), "Android UA marks the Android section"); print(" UA detection: device platform marked first\n"); // ── 404s + index links ───────────────────────────────────────────── process.assert(fetch_code("/install/nope/stable") == "404", "unknown slug is 404"); ue := parse_body(fetch("/install/nope/stable"), "unknown slug", xx arena); process.assert(get_str(get_obj(ue, "error"), "code") == "install.unknown_app", "names install.unknown_app"); ce := parse_body(fetch("/install/acme-app/nope"), "unknown channel", xx arena); process.assert(get_str(get_obj(ce, "error"), "code") == "install.unknown_channel", "names install.unknown_channel"); process.assert(fetch_code("/install/acme-app") == "404", "channel-less install path is 404"); idx := fetch("/"); process.assert(contains(idx, "href=\"/install/acme-app/stable\""), "index links the install page"); print(" 404s precise; index links install pages\n"); // ── teardown ─────────────────────────────────────────────────────── process.run(concat("kill ", pid)); process.run(concat("rm -rf ", STORE)); print("server_install: ALL CASES PASS\n"); return 0; }