diff --git a/cpp/update_flow_core/native_round.cpp b/cpp/update_flow_core/native_round.cpp index b8597f31..96594aca 100644 --- a/cpp/update_flow_core/native_round.cpp +++ b/cpp/update_flow_core/native_round.cpp @@ -67,14 +67,16 @@ Value StringOr(const Value& value, const char* fallback) { return value.IsString() ? value : Value::String(fallback); } -// A non-negative integral state counter; anything else reads as 0. +// A non-negative integral state counter; anything else reads as 0. Capped +// well inside a 32-bit size_t (armeabi-v7a, x86): converting a double beyond +// the target's range is undefined. size_t Counter(const Value& value) { const double number = value.AsNumber(); if (!value.IsNumber() || !std::isfinite(number) || number < 0) { return 0; } - return number >= 1e15 ? static_cast(1e15) - : static_cast(number); + return number >= 1e9 ? static_cast(1e9) + : static_cast(number); } // { packageVersion, currentVersion, uuid, rolledBackVersion } — what diff --git a/cpp/update_flow_core/native_round.h b/cpp/update_flow_core/native_round.h index ea5c4b80..32f3daa0 100644 --- a/cpp/update_flow_core/native_round.h +++ b/cpp/update_flow_core/native_round.h @@ -7,8 +7,8 @@ // The pure steps of the native cold-start round (NATIVE_CHECKUPDATE_DESIGN // §10), one implementation for the Android, iOS and Harmony orchestrators: // what to send, which endpoint to ask next, and everything a commit needs -// from the answer. HTTP, timeouts, locks, downloads and persistence stay -// native. JS has no counterpart (its client builds its own request through +// from the answer. round_engine.h drives these steps; HTTP, locks, +// downloads and persistence stay native. JS has no counterpart (its client builds its own request through // src/updateFlowCore.ts buildCheckRequestBody, which BuildRequestBody // mirrors); tests/native-round-vectors.json pins this layer. // diff --git a/cpp/update_flow_core/round_engine.cpp b/cpp/update_flow_core/round_engine.cpp new file mode 100644 index 00000000..1103e294 --- /dev/null +++ b/cpp/update_flow_core/round_engine.cpp @@ -0,0 +1,463 @@ +#include "round_engine.h" + +#include +#include + +#include "../patch_core/obscured_text.h" +#include "native_config.h" +#include "native_round.h" +#include "update_flow_core.h" + +namespace flowcore { + +using flowjson::Value; + +namespace { + +// Stored encoded (see obscured_text.h): the result status of a round that +// found nothing to install, and the version-info flag of a version a crash +// hold installed (read by JS). +const std::string& StatusNone() { + static const std::string value = pushy::text::Reveal("3418c1c1aa8a7c40"); + return value; +} +const std::string& InfoCrashHold() { + static const std::string value = + pushy::text::Reveal("3905f5c2a6b96d56212a19"); + return value; +} + +// Phases: what the round waits for. +const char kAwaitEnv[] = "env"; +const char kAwaitHttp[] = "http"; +const char kAwaitInstalled[] = "installed"; +const char kAwaitDownload[] = "download"; +const char kAwaitCommit[] = "committed"; +const char kDone[] = "done"; + +double Number(const Value& value) { + const double number = value.AsNumber(); + return value.IsNumber() && std::isfinite(number) ? number : 0; +} + +// A non-negative integral cursor; anything else reads as 0. Capped well +// inside a 32-bit size_t (armeabi-v7a, x86): converting a double beyond the +// target's range is undefined. +size_t Index(const Value& value) { + const double number = Number(value); + return number < 0 ? 0 : number >= 1e9 ? static_cast(1e9) + : static_cast(number); +} + +Value Command(const char* type) { + Value command = Value::Object(); + command.Set("type", Value::String(type)); + return command; +} + +bool IsIncremental(const std::string& strategy) { + return strategy == "diff" || strategy == "pdiff"; +} + +// One step in progress: the state being built and the commands emitted. +class Step { + public: + Step(Value state, const Value& event) : state_(std::move(state)), event_(event) { + now_ = Number(event.Get("now")); + const std::string type = event.Get("type").AsString(); + if (type == "start" || !state_.IsObject()) { + // A start begins a new round whatever came before; any other event + // without a round finds no phase that expects it. + state_ = Value::Object(); + } + // Once a crash is held or the launch waits on the round, it stays so. + for (const char* flag : {"crashHold", "forceActivate"}) { + if (event.Get(flag).Truthy()) { + state_.Set(flag, Value::Bool(true)); + } + } + } + + Value Run() { + const std::string type = event_.Get("type").AsString(); + if (type == "start") { + Start(); + } else if (Phase() == kDone) { + // A finished round ignores late events. + return Result(); + } else if (type != Phase()) { + Finish("failed", "internal_error"); + } else if (type == kAwaitEnv) { + OnEnv(); + } else if (type == kAwaitHttp) { + OnHttp(); + } else if (type == kAwaitInstalled) { + OnInstalled(); + } else if (type == kAwaitDownload) { + OnDownload(); + } else if (type == kAwaitCommit) { + OnCommitted(); + } else { + Finish("failed", "internal_error"); + } + return Result(); + } + + private: + std::string Phase() const { return state_.Get("phase").AsString(); } + void Await(const char* phase) { state_.Set("phase", Value::String(phase)); } + void Emit(Value command) { commands_.Push(std::move(command)); } + + Value Result() { + Value result = Value::Object(); + result.Set("state", std::move(state_)); + result.Set("commands", std::move(commands_)); + return result; + } + + void Finish(const std::string& status, const std::string& reason, + const Value& hash = Value::Undefined(), + const Value& activated = Value::Undefined()) { + Value finish = Command("finish"); + finish.Set("status", Value::String(status)); + finish.Set("reason", Value::String(reason)); + finish.Set("hash", hash); + finish.Set("activated", activated); + Emit(std::move(finish)); + Await(kDone); + } + + // What the remaining round budget allows for a bound of `limit` ms; + // `limit` itself without a deadline. + double Within(double limit) const { + const double deadline = Number(state_.Get("deadline")); + return deadline > 0 ? std::fmin(limit, deadline - now_) : limit; + } + + // A deadline `span` ms from now, capped by the round's. + double DeadlineIn(double span) const { + const double deadline = Number(state_.Get("deadline")); + return deadline > 0 ? std::fmin(now_ + span, deadline) : now_ + span; + } + + void Start() { + const Value& config = event_.Get("config"); + state_.Set("config", config.IsString() ? config : Value::String("")); + for (const char* key : + {"resetGeneration", "configGeneration", "sample", "deadline"}) { + state_.Set(key, Value::Number(Number(event_.Get(key)))); + } + const RunnableConfig gate = + ParseRunnableConfig(state_.Get("config").AsString()); + if (!gate.runnable()) { + // Not a round: nothing ran, so nothing to mark. + Finish(gate.status, gate.reason); + return; + } + Emit(Command("markIncomplete")); + Emit(Command("collectEnv")); + Await(kAwaitEnv); + } + + void OnEnv() { + const Value& env = event_.Get("env"); + Value round = PrepareRound(state_.Get("config").AsString(), env, + Number(state_.Get("sample"))); + if (!round.IsObject()) { + Finish("failed", "invalid_request"); + return; + } + state_.Set("env", env); + state_.Set("body", round.Get("body")); + state_.Set("query", round.Get("query")); + Query(Value::Undefined()); + } + + // Asks the next endpoint, or settles the query on `text`. + void Query(const Value& text) { + Value query = NextQueryStep(state_.Get("query"), text); + if (query.Get("found").Truthy()) { + state_.Remove("query"); + Respond(text.AsString()); + return; + } + const Value& url = query.Get("url"); + const double timeout = Within(kQueryIdleTimeoutMs); + if (!url.IsString() || timeout <= 0) { + // No endpoint answered (or the budget is gone): nothing to commit. + Finish("failed", "check_failed"); + return; + } + Value http = Command("http"); + http.Set("url", url); + const bool post = query.Get("post").Truthy(); + http.Set("post", Value::Bool(post)); + if (post) { + http.Set("body", state_.Get("body")); + } + http.Set("timeoutMs", Value::Number(timeout)); + http.Set("callTimeoutMs", Value::Number(Within(kQueryCallTimeoutMs))); + Emit(std::move(http)); + state_.Set("query", std::move(query)); + Await(kAwaitHttp); + } + + void OnHttp() { + const Value& text = event_.Get("text"); + state_.Set("at", Value::Number(std::floor(Number(event_.Get("at"))))); + Query(text.IsString() ? text : Value::Undefined()); + } + + void Respond(const std::string& text) { + Value decision = HandleRoundResponse( + text, state_.Get("config").AsString(), state_.Get("env"), + state_.Get("body").AsString(), Number(state_.Get("at"))); + state_.Remove("at"); + if (!decision.IsObject()) { + Finish("failed", "invalid_response"); + return; + } + state_.Set("cache", decision.Get("cache")); + if (decision.Get("action").AsString() != "download") { + CommitCache(StatusNone(), decision.Get("reason").AsString()); + return; + } + const Value hash = decision.Get("hash"); + if (!hash.IsString() || !IsSafePathComponent(hash.AsString())) { + Finish("failed", "invalid_response"); + return; + } + decision.Remove("action"); + decision.Remove("cache"); + state_.Set("decision", std::move(decision)); + Value check = Command("checkInstalled"); + check.Set("hash", hash); + Emit(std::move(check)); + Await(kAwaitInstalled); + } + + void OnInstalled() { + if (event_.Get("installed").Truthy()) { + CommitVersion(); + return; + } + Value cursor = Value::Object(); + cursor.Set("attempt", Value::Number(0)); + cursor.Set("url", Value::Number(0)); + cursor.Set("incremental", Value::Number(DeadlineIn(kDownloadPhaseMs))); + cursor.Set("full", Value::Number(0)); + state_.Set("cursor", std::move(cursor)); + NextAttempt(); + } + + // Strategies in the planned order (diff, pdiff, full), each over its + // mirrors, starting at the cursor. + void NextAttempt() { + Value cursor = state_.Get("cursor"); + const Value& attempts = state_.Get("decision").Get("attempts"); + size_t attempt = Index(cursor.Get("attempt")); + size_t url = Index(cursor.Get("url")); + for (; attempt < attempts.Size(); attempt++, url = 0) { + const Value& planned = attempts.At(attempt); + const std::string strategy = planned.Get("type").AsString(); + const bool full = !IsIncremental(strategy); + if (full && Number(cursor.Get("full")) == 0) { + cursor.Set("full", Value::Number(DeadlineIn(kDownloadPhaseMs))); + } + const double deadline = + Number(cursor.Get(full ? "full" : "incremental")); + const Value& urls = planned.Get("urls"); + if (url >= urls.Size()) { + continue; + } + // Never start an attempt whose phase budget is gone: it would outlive + // the round. The full phase is the last one. + if (deadline - now_ <= 0) { + if (full) { + break; + } + continue; + } + Value download = Command("download"); + download.Set("strategy", Value::String(full ? "full" : strategy)); + download.Set("url", urls.At(url)); + download.Set("hash", state_.Get("decision").Get("hash")); + if (strategy == "diff") { + download.Set("originHash", state_.Get("env").Get("currentVersion")); + } + download.Set("deadline", Value::Number(deadline)); + Emit(std::move(download)); + cursor.Set("attempt", Value::Number(static_cast(attempt))); + cursor.Set("url", Value::Number(static_cast(url))); + state_.Set("cursor", std::move(cursor)); + Await(kAwaitDownload); + return; + } + state_.Remove("cursor"); + // The native attempt is over, so JS may reuse the response and retry + // through its own strategy chain. + CommitCache("failed", "download_failed"); + } + + void OnDownload() { + if (event_.Get("ok").Truthy()) { + state_.Remove("cursor"); + CommitVersion(); + return; + } + Value cursor = state_.Get("cursor"); + const size_t attempt = Index(cursor.Get("attempt")); + const std::string strategy = state_.Get("decision") + .Get("attempts") + .At(attempt) + .Get("type") + .AsString(); + const bool nextStrategy = + event_.Get("timedOut").Truthy() || + // The bytes arrived but could not be applied: every mirror serves + // the same artifact, so only the next strategy can help. + !IsMirrorRetryableCode(event_.Get("code").AsString()); + if (event_.Get("timedOut").Truthy() && !IsIncremental(strategy)) { + // The full phase timed out: no budget is left for anything. + cursor.Set("attempt", Value::Number(static_cast( + state_.Get("decision").Get("attempts").Size()))); + } else if (nextStrategy) { + cursor.Set("attempt", Value::Number(static_cast(attempt + 1))); + cursor.Set("url", Value::Number(0)); + } else { + cursor.Set("url", Value::Number(Number(cursor.Get("url")) + 1)); + } + state_.Set("cursor", std::move(cursor)); + NextAttempt(); + } + + // Only the response cache: nothing to install or activate. + void CommitCache(const std::string& status, const std::string& reason) { + Value result = Value::Object(); + result.Set("status", Value::String(status)); + result.Set("reason", Value::String(reason)); + Commit("", "", false, std::move(result)); + } + + // The version info, the activation and the response cache in one commit. + void CommitVersion() { + const Value& decision = state_.Get("decision"); + const bool crashHold = state_.Get("crashHold").Truthy(); + Value hashInfo = decision.Get("hashInfo"); + if (crashHold) { + hashInfo.Set(InfoCrashHold(), Value::Bool(true)); + } + // Silent strategies or a server-marked forceBoot version activate for + // the next launch; otherwise activation stays with JS — unless JS is + // dead (a crash hold) or the launch itself waits on this round. + const bool activate = decision.Get("activate").Truthy() || crashHold || + state_.Get("forceActivate").Truthy(); + const std::string hash = decision.Get("hash").AsString(); + Value result = Value::Object(); + result.Set("status", Value::String("downloaded")); + result.Set("reason", Value::String("")); + result.Set("hash", Value::String(hash)); + result.Set("activated", Value::Bool(activate)); + Commit(hash, flowjson::Stringify(hashInfo), activate, std::move(result)); + } + + void Commit(const std::string& hash, const std::string& hashInfo, + bool activate, Value result) { + Value commit = Command("commit"); + commit.Set("hash", Value::String(hash)); + commit.Set("hashInfo", Value::String(hashInfo)); + commit.Set("activate", Value::Bool(activate)); + commit.Set("cache", state_.Get("cache")); + commit.Set("resetGeneration", state_.Get("resetGeneration")); + commit.Set("configGeneration", state_.Get("configGeneration")); + Emit(std::move(commit)); + state_.Set("result", std::move(result)); + Await(kAwaitCommit); + } + + void OnCommitted() { + const Value result = state_.Get("result"); + if (event_.Get("error").Truthy()) { + Finish("failed", "commit_failed"); + } else if (!event_.Get("ok").Truthy()) { + // The generation that moved: a config change, or (by default) a reset. + Finish("cancelled", event_.Get("reason").AsString() == "config_changed" + ? "config_changed" + : "reset"); + } else if (result.Get("hash").IsString()) { + const Value& activated = event_.Get("activated"); + Finish(result.Get("status").AsString(), result.Get("reason").AsString(), + result.Get("hash"), + activated.IsBool() ? activated : result.Get("activated")); + } else { + Finish(result.Get("status").AsString(), result.Get("reason").AsString()); + } + } + + Value state_; + const Value& event_; + double now_ = 0; + Value commands_ = Value::Array(); +}; + +} // namespace + +bool IsSafePathComponent(const std::string& name) { + return !name.empty() && name != "." && name != ".." && + name.find_first_of(std::string("/\\\0", 3)) == std::string::npos; +} + +Value StepRound(const Value& state, const Value& event) { + return Step(state, event).Run(); +} + +std::string StepRoundJson(const std::string& stateJson, + const std::string& eventJson) { + // The state carries server text (the response cache, the planned URLs) and + // the event a whole response body, so the byte cap is the platform's + // response cap's business; the node cap still bounds the heap. + flowjson::ParseLimits limits; + limits.max_input_bytes = 256 * 1024 * 1024; + limits.max_nodes = 4 * flowjson::kMaxNodes; + bool stateOk = true; + bool eventOk = false; + const Value state = + stateJson.empty() ? Value::Undefined() + : flowjson::Parse(stateJson, limits, &stateOk); + const Value event = flowjson::Parse(eventJson, limits, &eventOk); + if (!stateOk || !eventOk || !event.IsObject()) { + Value finish = Value::Object(); + finish.Set("type", Value::String("finish")); + finish.Set("status", Value::String("failed")); + finish.Set("reason", Value::String("internal_error")); + Value commands = Value::Array(); + commands.Push(std::move(finish)); + Value done = Value::Object(); + done.Set("phase", Value::String(kDone)); + Value result = Value::Object(); + result.Set("state", std::move(done)); + result.Set("commands", std::move(commands)); + return flowjson::Stringify(result); + } + return flowjson::Stringify(StepRound(state, event)); +} + +std::string HostCancellation(const std::string& hostConfig, + const std::string& roundConfig, + const std::string& storedConfig, + double roundConfigGeneration, + double configGeneration, + double roundResetGeneration, + double resetGeneration) { + if (roundConfigGeneration != configGeneration || + !ConfigsEquivalent(hostConfig, roundConfig) || + !ConfigsEquivalent(hostConfig, storedConfig)) { + return "config_changed"; + } + if (roundResetGeneration != resetGeneration) { + return "reset"; + } + return ""; +} + +} // namespace flowcore diff --git a/cpp/update_flow_core/round_engine.h b/cpp/update_flow_core/round_engine.h new file mode 100644 index 00000000..e412250c --- /dev/null +++ b/cpp/update_flow_core/round_engine.h @@ -0,0 +1,106 @@ +#pragma once + +#include + +#include "flow_json.h" + +// The driver of one native cold-start round (NATIVE_CHECKUPDATE_DESIGN §12): +// the loop the Android, iOS and Harmony orchestrators used to write by hand +// around the pure steps of native_round.h — the config gate, the breadcrumb, +// the endpoint query, the response decision, the already-installed check, the +// two download budgets, the commit and the result it maps to. +// +// Sans-IO: StepRound takes the previous state and what just happened, and +// returns the next state plus what to do. It never does IO, never reads a +// clock, never locks; the time arrives in each event. The platform adapter +// runs the commands (HTTP, downloads, persistence, the commit's lock and its +// generation re-check, the activation digest gate) and feeds the outcome back +// as the next event. The state is plain JSON, so the JNI and NAPI bindings +// pass it through as text and keep no handle. +// +// tests/round-engine-vectors.json pins every trace. +// Keep the core internal to whichever binary links it (see podspec Core). +#pragma GCC visibility push(hidden) + +namespace flowcore { + +// Each download phase's window: the incremental strategies (diff, pdiff) +// share one, the first full attempt opens its own, so incremental failures +// cannot starve the last-resort full download. A round deadline caps both. +constexpr double kDownloadPhaseMs = 600 * 1000; +// Per request of the endpoint query: the idle (connect/read) timeout and the +// whole-call bound. A round deadline caps both. +constexpr double kQueryIdleTimeoutMs = 10 * 1000; +constexpr double kQueryCallTimeoutMs = 15 * 1000; + +// One step of the round. `state` is the previous result's "state" (anything +// for a "start" event); the result is {"state": ..., "commands": [...]}. +// +// Events ({"type": ..., ...}; every event carries "now", the platform's +// monotonic clock in milliseconds, and may carry the sticky flags +// "crashHold" — a crash is being held, JS is dead — and "forceActivate" — +// the launch waits on this round, as the tvOS purge restore): +// start config (the persisted config text, "" when absent), +// resetGeneration, configGeneration (sampled before the config +// was read), sample (∈ [0, 1), picks the first endpoint), +// deadline (monotonic ms; 0 for none) +// env env: the launch as native_round.h describes it +// http text: the response body (absent when the request failed), +// at: wall-clock seconds the response arrived +// installed installed: a complete install of the hash is already on disk +// download ok; code: the failure's error code; timedOut: the attempt +// outlived its deadline +// committed ok: the commit landed; when false, reason names the +// generation that moved since the round started ("reset", the +// default, or "config_changed"); activated: what it really +// activated (absent = as asked); error: the commit itself failed +// (the activation gate rejected the version, a write threw) +// +// Commands: +// markIncomplete persist the interrupted-round breadcrumb (§11.4). The +// adapter clears it once the round is over, however it +// ends +// collectEnv reply with an env event +// http url, post, body (when post), timeoutMs, callTimeoutMs +// checkInstalled hash +// download strategy ("diff", "pdiff" or "full"), url, hash, +// originHash (diff only), deadline (monotonic ms) +// commit hash and hashInfo (the version info as JSON text; both "" +// when only the response cache is committed), activate, +// cache, resetGeneration, configGeneration — one atomic +// commit that first re-checks both generations +// finish status, reason, and for "downloaded" hash and activated. +// The round is over; later events are ignored. A +// downloaded but not activated version is the one a later +// crash hold may activate. +// +// An event the current phase does not expect (or of no known type) finishes +// the round as failed/internal_error. +flowjson::Value StepRound(const flowjson::Value& state, + const flowjson::Value& event); + +// StepRound over JSON text, for the bindings. A state or event that does not +// parse finishes the round as failed/internal_error. +std::string StepRoundJson(const std::string& stateJson, + const std::string& eventJson); + +// Whether a host that waited on a round may take its result, checked after +// the wait: "" when it may, otherwise the cancellation reason. The config +// the host saw, the one the round ran with and the one stored now must be +// equivalent (ConfigsEquivalent) and the config generation unchanged +// ("config_changed"); then the reset generation unchanged ("reset"). +std::string HostCancellation(const std::string& hostConfig, + const std::string& roundConfig, + const std::string& storedConfig, + double roundConfigGeneration, + double configGeneration, + double roundResetGeneration, + double resetGeneration); + +// A server-provided hash may name a directory: non-empty, not "." or "..", +// no '/', '\\' or NUL. +bool IsSafePathComponent(const std::string& name); + +} // namespace flowcore + +#pragma GCC visibility pop diff --git a/cpp/update_flow_core/tests/fuzz_flow_json.cpp b/cpp/update_flow_core/tests/fuzz_flow_json.cpp index f6229928..8ecf4407 100644 --- a/cpp/update_flow_core/tests/fuzz_flow_json.cpp +++ b/cpp/update_flow_core/tests/fuzz_flow_json.cpp @@ -11,6 +11,7 @@ #include "../flow_json.h" #include "../native_config.h" #include "../native_round.h" +#include "../round_engine.h" #include "../update_flow_core.h" extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { @@ -66,6 +67,47 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { flowjson::Stringify( flowcore::HandleRoundResponse("{\"upToDate\":true}", text, env, text, 1)); + // The round driver: the text as the response every endpoint returns, then + // as the binding's event and state. Every round must finish: one step per + // request, planned mirror (bounded by the response's node cap) and commit. + flowjson::Value engine; + flowjson::Value event = flowjson::Value::Object(); + event.Set("type", flowjson::Value::String("start")); + event.Set("config", flowjson::Value::String( + "{\"appKey\":\"k\",\"endpoints\":[\"https://a\"]}")); + for (int step = 0;; step++) { + if (step > 4 * static_cast(flowjson::kMaxNodes)) { + std::abort(); + } + const flowjson::Value result = flowcore::StepRound(engine, event); + engine = result.Get("state"); + if (engine.Get("phase").AsString() == "done") { + break; + } + const std::string type = result.Get("commands") + .At(result.Get("commands").Size() - 1) + .Get("type") + .AsString(); + event = flowjson::Value::Object(); + if (type == "collectEnv") { + event.Set("type", flowjson::Value::String("env")); + event.Set("env", env); + } else if (type == "http") { + event.Set("type", flowjson::Value::String("http")); + event.Set("text", flowjson::Value::String(text)); + } else if (type == "checkInstalled") { + event.Set("type", flowjson::Value::String("installed")); + } else if (type == "download") { + event.Set("type", flowjson::Value::String("download")); + event.Set("code", flowjson::Value::String("DOWNLOAD_FAILED")); + } else { + event.Set("type", flowjson::Value::String("committed")); + event.Set("ok", flowjson::Value::Bool(true)); + } + } + flowcore::StepRoundJson(text, "{\"type\":\"committed\",\"ok\":true}"); + flowcore::StepRoundJson("", text); + // The persisted-config layer reads host/JS-written text: equivalence must // be reflexive for every object, and a normalized config must be runnable // (or disabled) and stable under its own normalization. diff --git a/cpp/update_flow_core/tests/round-engine-vectors.json b/cpp/update_flow_core/tests/round-engine-vectors.json new file mode 100644 index 00000000..c06a6a84 --- /dev/null +++ b/cpp/update_flow_core/tests/round-engine-vectors.json @@ -0,0 +1,3152 @@ +{ + "description": "The native round driver (cpp/update_flow_core/round_engine.h), shared by the Android, iOS and Harmony orchestrators and replayed by scripts/test-update-flow-core.sh. `rounds`: each step feeds `event` to StepRound with the previous step's state and expects exactly `commands` (compared as JSON, key order ignored). A string \"@name\" anywhere in a round or a host case stands for fixtures[name]. `host`: HostCancellation. `safePath`: IsSafePathComponent. `before` records what a platform did where it disagreed before the engine replaced its hand-written loop. Hand-maintained.", + "fixtures": { + "config": "{\"appKey\":\"key\",\"endpoints\":[\"https://a.example\",\"https://b.example\"],\"queryUrls\":[],\"afterDownload\":\"none\",\"rnu\":\"10.0.0\",\"rn\":\"0.80.0\"}", + "config.silent": "{\"appKey\":\"key\",\"endpoints\":[\"https://a.example\"],\"queryUrls\":[],\"afterDownload\":\"setNeedUpdate\",\"rnu\":\"10.0.0\",\"rn\":\"0.80.0\"}", + "config.lists": "{\"appKey\":\"key\",\"endpoints\":[\"https://a.example\"],\"queryUrls\":[\"https://l.example/list.json\"],\"afterDownload\":\"none\",\"rnu\":\"10.0.0\",\"rn\":\"0.80.0\"}", + "env": { + "packageVersion": "1.0.0", + "currentVersion": "cur", + "uuid": "u-1", + "buildTime": "1700000000", + "os": "android 34", + "bundleHash": "bh" + }, + "env.fresh": { + "packageVersion": "1.0.0", + "uuid": "u-1", + "buildTime": "1700000000", + "os": "ios 18.0" + }, + "upToDate": "{\"upToDate\":true}", + "v2": "{\"update\":true,\"hash\":\"h2\",\"name\":\"v2\",\"description\":\"notes\",\"metaInfo\":\"m\",\"diff\":\"d.hdiff\",\"pdiff\":\"p.hdiff\",\"full\":\"f.ppk\",\"paths\":[\"https://cdn1.example\",\"https://cdn2.example\"]}", + "v2.forceBoot": "{\"update\":true,\"hash\":\"h2\",\"name\":\"v2\",\"full\":\"f.ppk\",\"paths\":[\"https://cdn1.example\"],\"config\":{\"forceBoot\":true}}", + "body": "{\"packageVersion\":\"1.0.0\",\"hash\":\"cur\",\"buildTime\":\"1700000000\",\"cInfo\":{\"rnu\":\"10.0.0\",\"rn\":\"0.80.0\",\"os\":\"android 34\",\"uuid\":\"u-1\"},\"diffV\":2,\"bundleHash\":\"bh\"}", + "body.fresh": "{\"packageVersion\":\"1.0.0\",\"buildTime\":\"1700000000\",\"cInfo\":{\"rnu\":\"10.0.0\",\"rn\":\"0.80.0\",\"os\":\"ios 18.0\",\"uuid\":\"u-1\"},\"diffV\":2}" + }, + "rounds": [ + { + "name": "no config: skipped, nothing marked", + "steps": [ + { + "event": { + "type": "start", + "config": "", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "skipped", + "reason": "not_configured" + } + ] + } + ] + }, + { + "name": "disabled config: skipped", + "steps": [ + { + "event": { + "type": "start", + "config": "{\"appKey\":\"key\",\"disabled\":true}", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "skipped", + "reason": "disabled" + } + ] + } + ] + }, + { + "name": "blank appKey: invalid config", + "steps": [ + { + "event": { + "type": "start", + "config": "{\"appKey\":\" \"}", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "failed", + "reason": "invalid_config" + } + ] + } + ] + }, + { + "name": "an env that is not an object: invalid request", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "x", + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "failed", + "reason": "invalid_request" + } + ] + } + ] + }, + { + "name": "up to date: commit the response cache only", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@upToDate", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "commit", + "hash": "", + "hashInfo": "", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"upToDate\\\":true}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "noUpdate", + "reason": "noUpdate" + } + ] + } + ] + }, + { + "name": "up to date, then a reset wins the commit", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@upToDate", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "commit", + "hash": "", + "hashInfo": "", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"upToDate\\\":true}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": false, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "cancelled", + "reason": "reset" + } + ] + } + ] + }, + { + "name": "a failed endpoint falls back to the next", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://b.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@upToDate", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "commit", + "hash": "", + "hashInfo": "", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"upToDate\\\":true}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "noUpdate", + "reason": "noUpdate" + } + ] + } + ] + }, + { + "name": "an invalid response does not count", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "{\"error\":\"x\"}", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "http", + "url": "https://b.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@upToDate", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "commit", + "hash": "", + "hashInfo": "", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"upToDate\\\":true}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "noUpdate", + "reason": "noUpdate" + } + ] + } + ] + }, + { + "name": "every endpoint fails: check_failed, nothing committed", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://b.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "failed", + "reason": "check_failed" + } + ] + } + ] + }, + { + "name": "endpoints fail, the remote list answers", + "steps": [ + { + "event": { + "type": "start", + "config": "@config.lists", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://l.example/list.json", + "post": false, + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "[\"https://r.example/\"]", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "http", + "url": "https://r.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@upToDate", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "commit", + "hash": "", + "hashInfo": "", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"upToDate\\\":true}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\"],\\\"queryUrls\\\":[\\\"https://l.example/list.json\\\"],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "noUpdate", + "reason": "noUpdate" + } + ] + } + ] + }, + { + "name": "download: diff on the first mirror, activation left to JS", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@v2", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": false, + "now": 0 + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn1.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "commit", + "hash": "h2", + "hashInfo": "{\"name\":\"v2\",\"description\":\"notes\",\"metaInfo\":\"m\"}", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"description\\\":\\\"notes\\\",\\\"metaInfo\\\":\\\"m\\\",\\\"diff\\\":\\\"d.hdiff\\\",\\\"pdiff\\\":\\\"p.hdiff\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\",\\\"https://cdn2.example\\\"]}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "downloaded", + "reason": "", + "hash": "h2", + "activated": false + } + ] + } + ] + }, + { + "name": "download with afterDownload setNeedUpdate activates", + "steps": [ + { + "event": { + "type": "start", + "config": "@config.silent", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@v2", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": false, + "now": 0 + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn1.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "commit", + "hash": "h2", + "hashInfo": "{\"name\":\"v2\",\"description\":\"notes\",\"metaInfo\":\"m\"}", + "activate": true, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"description\\\":\\\"notes\\\",\\\"metaInfo\\\":\\\"m\\\",\\\"diff\\\":\\\"d.hdiff\\\",\\\"pdiff\\\":\\\"p.hdiff\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\",\\\"https://cdn2.example\\\"]}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"setNeedUpdate\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "downloaded", + "reason": "", + "hash": "h2", + "activated": true + } + ] + } + ] + }, + { + "name": "forceBoot activates and marks the version info", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@v2.forceBoot", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": false, + "now": 0 + }, + "commands": [ + { + "type": "download", + "strategy": "full", + "url": "https://cdn1.example/f.ppk", + "hash": "h2", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "commit", + "hash": "h2", + "hashInfo": "{\"name\":\"v2\",\"forceBootRescue\":true}", + "activate": true, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\"],\\\"config\\\":{\\\"forceBoot\\\":true}}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "downloaded", + "reason": "", + "hash": "h2", + "activated": true + } + ] + } + ] + }, + { + "name": "already installed: commit without downloading", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@v2", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": true, + "now": 0 + }, + "commands": [ + { + "type": "commit", + "hash": "h2", + "hashInfo": "{\"name\":\"v2\",\"description\":\"notes\",\"metaInfo\":\"m\"}", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"description\\\":\\\"notes\\\",\\\"metaInfo\\\":\\\"m\\\",\\\"diff\\\":\\\"d.hdiff\\\",\\\"pdiff\\\":\\\"p.hdiff\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\",\\\"https://cdn2.example\\\"]}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "downloaded", + "reason": "", + "hash": "h2", + "activated": false + } + ] + } + ] + }, + { + "name": "mirror retry: DOWNLOAD_FAILED tries the next mirror, PATCH_FAILED the next strategy", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@v2", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": false, + "now": 0 + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn1.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": false, + "now": 0, + "code": "DOWNLOAD_FAILED" + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn2.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": false, + "now": 0, + "code": "PATCH_FAILED" + }, + "commands": [ + { + "type": "download", + "strategy": "pdiff", + "url": "https://cdn1.example/p.hdiff", + "hash": "h2", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "commit", + "hash": "h2", + "hashInfo": "{\"name\":\"v2\",\"description\":\"notes\",\"metaInfo\":\"m\"}", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"description\\\":\\\"notes\\\",\\\"metaInfo\\\":\\\"m\\\",\\\"diff\\\":\\\"d.hdiff\\\",\\\"pdiff\\\":\\\"p.hdiff\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\",\\\"https://cdn2.example\\\"]}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "downloaded", + "reason": "", + "hash": "h2", + "activated": false + } + ] + } + ] + }, + { + "name": "a failure without a code is mirror-retryable", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@v2", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": false, + "now": 0 + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn1.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": false, + "now": 0 + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn2.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "commit", + "hash": "h2", + "hashInfo": "{\"name\":\"v2\",\"description\":\"notes\",\"metaInfo\":\"m\"}", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"description\\\":\\\"notes\\\",\\\"metaInfo\\\":\\\"m\\\",\\\"diff\\\":\\\"d.hdiff\\\",\\\"pdiff\\\":\\\"p.hdiff\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\",\\\"https://cdn2.example\\\"]}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "downloaded", + "reason": "", + "hash": "h2", + "activated": false + } + ] + } + ] + }, + { + "name": "every attempt fails: download_failed, the cache is still committed", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@v2", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": false, + "now": 0 + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn1.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": false, + "now": 0, + "code": "DOWNLOAD_FAILED" + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn2.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": false, + "now": 0, + "code": "DOWNLOAD_FAILED" + }, + "commands": [ + { + "type": "download", + "strategy": "pdiff", + "url": "https://cdn1.example/p.hdiff", + "hash": "h2", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": false, + "now": 0, + "code": "PATCH_FAILED" + }, + "commands": [ + { + "type": "download", + "strategy": "full", + "url": "https://cdn1.example/f.ppk", + "hash": "h2", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": false, + "now": 0, + "code": "DOWNLOAD_FAILED" + }, + "commands": [ + { + "type": "download", + "strategy": "full", + "url": "https://cdn2.example/f.ppk", + "hash": "h2", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": false, + "now": 0, + "code": "DOWNLOAD_FAILED" + }, + "commands": [ + { + "type": "commit", + "hash": "", + "hashInfo": "", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"description\\\":\\\"notes\\\",\\\"metaInfo\\\":\\\"m\\\",\\\"diff\\\":\\\"d.hdiff\\\",\\\"pdiff\\\":\\\"p.hdiff\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\",\\\"https://cdn2.example\\\"]}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "failed", + "reason": "download_failed" + } + ] + } + ] + }, + { + "name": "the incremental budget runs out: pdiff is skipped, full opens its own window", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 1000, + "text": "@v2", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": false, + "now": 1000 + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn1.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 601000 + } + ] + }, + { + "event": { + "type": "download", + "ok": false, + "now": 601000, + "timedOut": true + }, + "commands": [ + { + "type": "download", + "strategy": "full", + "url": "https://cdn1.example/f.ppk", + "hash": "h2", + "deadline": 1201000 + } + ] + }, + { + "event": { + "type": "download", + "ok": true, + "now": 700000 + }, + "commands": [ + { + "type": "commit", + "hash": "h2", + "hashInfo": "{\"name\":\"v2\",\"description\":\"notes\",\"metaInfo\":\"m\"}", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"description\\\":\\\"notes\\\",\\\"metaInfo\\\":\\\"m\\\",\\\"diff\\\":\\\"d.hdiff\\\",\\\"pdiff\\\":\\\"p.hdiff\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\",\\\"https://cdn2.example\\\"]}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 700000 + }, + "commands": [ + { + "type": "finish", + "status": "downloaded", + "reason": "", + "hash": "h2", + "activated": false + } + ] + } + ] + }, + { + "name": "the incremental budget spent by failures: the full window starts when full is reached", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 1000, + "text": "@v2", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": false, + "now": 1000 + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn1.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 601000 + } + ] + }, + { + "event": { + "type": "download", + "ok": false, + "now": 400000, + "code": "DOWNLOAD_FAILED" + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn2.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 601000 + } + ] + }, + { + "event": { + "type": "download", + "ok": false, + "now": 601001, + "code": "DOWNLOAD_FAILED" + }, + "commands": [ + { + "type": "download", + "strategy": "full", + "url": "https://cdn1.example/f.ppk", + "hash": "h2", + "deadline": 1201001 + } + ] + }, + { + "event": { + "type": "download", + "ok": true, + "now": 900000 + }, + "commands": [ + { + "type": "commit", + "hash": "h2", + "hashInfo": "{\"name\":\"v2\",\"description\":\"notes\",\"metaInfo\":\"m\"}", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"description\\\":\\\"notes\\\",\\\"metaInfo\\\":\\\"m\\\",\\\"diff\\\":\\\"d.hdiff\\\",\\\"pdiff\\\":\\\"p.hdiff\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\",\\\"https://cdn2.example\\\"]}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 900000 + }, + "commands": [ + { + "type": "finish", + "status": "downloaded", + "reason": "", + "hash": "h2", + "activated": false + } + ] + } + ] + }, + { + "name": "the full download times out: download_failed", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@v2.forceBoot", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": false, + "now": 0 + }, + "commands": [ + { + "type": "download", + "strategy": "full", + "url": "https://cdn1.example/f.ppk", + "hash": "h2", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": false, + "now": 600000, + "timedOut": true + }, + "commands": [ + { + "type": "commit", + "hash": "", + "hashInfo": "", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\"],\\\"config\\\":{\\\"forceBoot\\\":true}}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 600000 + }, + "commands": [ + { + "type": "finish", + "status": "failed", + "reason": "download_failed" + } + ] + } + ] + }, + { + "name": "a reset during the download wins the commit", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@v2", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": false, + "now": 0 + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn1.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "commit", + "hash": "h2", + "hashInfo": "{\"name\":\"v2\",\"description\":\"notes\",\"metaInfo\":\"m\"}", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"description\\\":\\\"notes\\\",\\\"metaInfo\\\":\\\"m\\\",\\\"diff\\\":\\\"d.hdiff\\\",\\\"pdiff\\\":\\\"p.hdiff\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\",\\\"https://cdn2.example\\\"]}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": false, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "cancelled", + "reason": "reset" + } + ] + } + ] + }, + { + "name": "a config change during the download wins the commit", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@v2", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": false, + "now": 0 + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn1.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "commit", + "hash": "h2", + "hashInfo": "{\"name\":\"v2\",\"description\":\"notes\",\"metaInfo\":\"m\"}", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"description\\\":\\\"notes\\\",\\\"metaInfo\\\":\\\"m\\\",\\\"diff\\\":\\\"d.hdiff\\\",\\\"pdiff\\\":\\\"p.hdiff\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\",\\\"https://cdn2.example\\\"]}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": false, + "now": 0, + "reason": "config_changed" + }, + "commands": [ + { + "type": "finish", + "status": "cancelled", + "reason": "config_changed" + } + ] + } + ] + }, + { + "name": "the commit itself fails: commit_failed", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@v2", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": false, + "now": 0 + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn1.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "commit", + "hash": "h2", + "hashInfo": "{\"name\":\"v2\",\"description\":\"notes\",\"metaInfo\":\"m\"}", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"description\\\":\\\"notes\\\",\\\"metaInfo\\\":\\\"m\\\",\\\"diff\\\":\\\"d.hdiff\\\",\\\"pdiff\\\":\\\"p.hdiff\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\",\\\"https://cdn2.example\\\"]}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": false, + "now": 0, + "error": true + }, + "commands": [ + { + "type": "finish", + "status": "failed", + "reason": "commit_failed" + } + ] + } + ] + }, + { + "name": "no running version: no diff attempt, no originHash", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env.fresh", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body.fresh", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@v2", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": false, + "now": 0 + }, + "commands": [ + { + "type": "download", + "strategy": "pdiff", + "url": "https://cdn1.example/p.hdiff", + "hash": "h2", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "commit", + "hash": "h2", + "hashInfo": "{\"name\":\"v2\",\"description\":\"notes\",\"metaInfo\":\"m\"}", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"description\\\":\\\"notes\\\",\\\"metaInfo\\\":\\\"m\\\",\\\"diff\\\":\\\"d.hdiff\\\",\\\"pdiff\\\":\\\"p.hdiff\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\",\\\"https://cdn2.example\\\"]}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"ios 18.0\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "downloaded", + "reason": "", + "hash": "h2", + "activated": false + } + ] + } + ] + }, + { + "name": "an unsafe hash is an invalid response", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "{\"update\":true,\"hash\":\"../h\",\"full\":\"f.ppk\",\"paths\":[\"https://cdn1.example\"]}", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "finish", + "status": "failed", + "reason": "invalid_response" + } + ] + } + ] + }, + { + "name": "crash hold: the deadline caps requests and downloads, the version is force-activated and marked", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 8000, + "now": 1000, + "crashHold": true + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 1000 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 7000, + "callTimeoutMs": 7000 + } + ] + }, + { + "event": { + "type": "http", + "now": 4000 + }, + "commands": [ + { + "type": "http", + "url": "https://b.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 4000, + "callTimeoutMs": 4000 + } + ] + }, + { + "event": { + "type": "http", + "now": 6000, + "text": "@v2", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": false, + "now": 6000 + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn1.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 8000 + } + ] + }, + { + "event": { + "type": "download", + "ok": false, + "now": 7000, + "code": "DOWNLOAD_FAILED" + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn2.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 8000 + } + ] + }, + { + "event": { + "type": "download", + "ok": true, + "now": 7900 + }, + "commands": [ + { + "type": "commit", + "hash": "h2", + "hashInfo": "{\"name\":\"v2\",\"description\":\"notes\",\"metaInfo\":\"m\",\"crashRescue\":true}", + "activate": true, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"description\\\":\\\"notes\\\",\\\"metaInfo\\\":\\\"m\\\",\\\"diff\\\":\\\"d.hdiff\\\",\\\"pdiff\\\":\\\"p.hdiff\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\",\\\"https://cdn2.example\\\"]}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 7900 + }, + "commands": [ + { + "type": "finish", + "status": "downloaded", + "reason": "", + "hash": "h2", + "activated": true + } + ] + } + ] + }, + { + "name": "a crash hold that begins mid-round still force-activates", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@v2", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": false, + "now": 0 + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn1.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": true, + "now": 0, + "crashHold": true + }, + "commands": [ + { + "type": "commit", + "hash": "h2", + "hashInfo": "{\"name\":\"v2\",\"description\":\"notes\",\"metaInfo\":\"m\",\"crashRescue\":true}", + "activate": true, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"description\\\":\\\"notes\\\",\\\"metaInfo\\\":\\\"m\\\",\\\"diff\\\":\\\"d.hdiff\\\",\\\"pdiff\\\":\\\"p.hdiff\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\",\\\"https://cdn2.example\\\"]}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "downloaded", + "reason": "", + "hash": "h2", + "activated": true + } + ] + } + ] + }, + { + "name": "the round deadline passes during the query: check_failed", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 5000, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 5000, + "callTimeoutMs": 5000 + } + ] + }, + { + "event": { + "type": "http", + "now": 5000 + }, + "commands": [ + { + "type": "finish", + "status": "failed", + "reason": "check_failed" + } + ] + } + ] + }, + { + "name": "a deadline already past when the download would start: download_failed", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 5000, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 5000, + "callTimeoutMs": 5000 + } + ] + }, + { + "event": { + "type": "http", + "now": 5000, + "text": "@v2", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": false, + "now": 5000 + }, + "commands": [ + { + "type": "commit", + "hash": "", + "hashInfo": "", + "activate": false, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"description\\\":\\\"notes\\\",\\\"metaInfo\\\":\\\"m\\\",\\\"diff\\\":\\\"d.hdiff\\\",\\\"pdiff\\\":\\\"p.hdiff\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\",\\\"https://cdn2.example\\\"]}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 5000 + }, + "commands": [ + { + "type": "finish", + "status": "failed", + "reason": "download_failed" + } + ] + } + ] + }, + { + "name": "forceActivate (the launch waits): activation asked, the commit reports it dropped", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0, + "forceActivate": true + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@v2", + "at": 1700000000.9 + }, + "commands": [ + { + "type": "checkInstalled", + "hash": "h2" + } + ] + }, + { + "event": { + "type": "installed", + "installed": false, + "now": 0 + }, + "commands": [ + { + "type": "download", + "strategy": "diff", + "url": "https://cdn1.example/d.hdiff", + "hash": "h2", + "originHash": "cur", + "deadline": 600000 + } + ] + }, + { + "event": { + "type": "download", + "ok": true, + "now": 0 + }, + "commands": [ + { + "type": "commit", + "hash": "h2", + "hashInfo": "{\"name\":\"v2\",\"description\":\"notes\",\"metaInfo\":\"m\"}", + "activate": true, + "cache": "{\"ts\":1700000000,\"body\":\"{\\\"update\\\":true,\\\"hash\\\":\\\"h2\\\",\\\"name\\\":\\\"v2\\\",\\\"description\\\":\\\"notes\\\",\\\"metaInfo\\\":\\\"m\\\",\\\"diff\\\":\\\"d.hdiff\\\",\\\"pdiff\\\":\\\"p.hdiff\\\",\\\"full\\\":\\\"f.ppk\\\",\\\"paths\\\":[\\\"https://cdn1.example\\\",\\\"https://cdn2.example\\\"]}\",\"request\":\"{\\\"packageVersion\\\":\\\"1.0.0\\\",\\\"hash\\\":\\\"cur\\\",\\\"buildTime\\\":\\\"1700000000\\\",\\\"cInfo\\\":{\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\",\\\"os\\\":\\\"android 34\\\",\\\"uuid\\\":\\\"u-1\\\"},\\\"diffV\\\":2,\\\"bundleHash\\\":\\\"bh\\\"}\",\"config\":\"{\\\"appKey\\\":\\\"key\\\",\\\"endpoints\\\":[\\\"https://a.example\\\",\\\"https://b.example\\\"],\\\"queryUrls\\\":[],\\\"afterDownload\\\":\\\"none\\\",\\\"rnu\\\":\\\"10.0.0\\\",\\\"rn\\\":\\\"0.80.0\\\"}\"}", + "resetGeneration": 3, + "configGeneration": 5 + } + ] + }, + { + "event": { + "type": "committed", + "ok": true, + "now": 0, + "activated": false + }, + "commands": [ + { + "type": "finish", + "status": "downloaded", + "reason": "", + "hash": "h2", + "activated": false + } + ] + } + ] + }, + { + "name": "an event the phase does not expect: internal_error, later events ignored", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "http", + "url": "https://a.example/checkUpdate/key", + "post": true, + "body": "@body", + "timeoutMs": 10000, + "callTimeoutMs": 15000 + } + ] + }, + { + "event": { + "type": "installed", + "installed": true, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "failed", + "reason": "internal_error" + } + ] + }, + { + "event": { + "type": "http", + "now": 0, + "text": "@upToDate", + "at": 1700000000.9 + }, + "commands": [] + } + ] + }, + { + "name": "an event without a round: internal_error", + "steps": [ + { + "event": { + "type": "env", + "env": "@env", + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "failed", + "reason": "internal_error" + } + ] + } + ] + }, + { + "name": "an event without a type: internal_error", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "failed", + "reason": "internal_error" + } + ] + } + ] + }, + { + "name": "an empty event without a round: internal_error", + "steps": [ + { + "event": {}, + "commands": [ + { + "type": "finish", + "status": "failed", + "reason": "internal_error" + } + ] + } + ] + }, + { + "name": "a new start replaces a finished round", + "steps": [ + { + "event": { + "type": "start", + "config": "@config", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "markIncomplete" + }, + { + "type": "collectEnv" + } + ] + }, + { + "event": { + "type": "env", + "env": "x", + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "failed", + "reason": "invalid_request" + } + ] + }, + { + "event": { + "type": "start", + "config": "", + "resetGeneration": 3, + "configGeneration": 5, + "sample": 0, + "deadline": 0, + "now": 0 + }, + "commands": [ + { + "type": "finish", + "status": "skipped", + "reason": "not_configured" + } + ] + } + ] + } + ], + "host": [ + { + "name": "the round's own config, nothing changed", + "hostConfig": "@config", + "roundConfig": "@config", + "storedConfig": "@config", + "roundConfigGeneration": 5, + "configGeneration": 5, + "roundResetGeneration": 3, + "resetGeneration": 3, + "expected": "" + }, + { + "name": "the same config in another serialization is not a change", + "hostConfig": "{\"rn\":\"0.80.0\",\"appKey\":\"key\",\"endpoints\":[\"https://a.example/\",\"https://b.example\"],\"queryUrls\":[],\"afterDownload\":\"none\",\"rnu\":\"10.0.0\"}", + "roundConfig": "@config", + "storedConfig": "@config", + "roundConfigGeneration": 5, + "configGeneration": 5, + "roundResetGeneration": 3, + "resetGeneration": 3, + "expected": "", + "before": { + "harmony": "config_changed (string equality)" + } + }, + { + "name": "the config generation moved", + "hostConfig": "@config", + "roundConfig": "@config", + "storedConfig": "@config", + "roundConfigGeneration": 5, + "configGeneration": 6, + "roundResetGeneration": 3, + "resetGeneration": 3, + "expected": "config_changed" + }, + { + "name": "the round ran with another config", + "hostConfig": "@config", + "roundConfig": "@config.silent", + "storedConfig": "@config", + "roundConfigGeneration": 5, + "configGeneration": 5, + "roundResetGeneration": 3, + "resetGeneration": 3, + "expected": "config_changed" + }, + { + "name": "the stored config changed after the wait", + "hostConfig": "@config", + "roundConfig": "@config", + "storedConfig": "@config.silent", + "roundConfigGeneration": 5, + "configGeneration": 5, + "roundResetGeneration": 3, + "resetGeneration": 3, + "expected": "config_changed" + }, + { + "name": "no config is never equivalent", + "hostConfig": "", + "roundConfig": "", + "storedConfig": "", + "roundConfigGeneration": 5, + "configGeneration": 5, + "roundResetGeneration": 3, + "resetGeneration": 3, + "expected": "config_changed" + }, + { + "name": "a reset since the round started", + "hostConfig": "@config", + "roundConfig": "@config", + "storedConfig": "@config", + "roundConfigGeneration": 5, + "configGeneration": 5, + "roundResetGeneration": 3, + "resetGeneration": 4, + "expected": "reset" + }, + { + "name": "a config change outranks a reset", + "hostConfig": "@config", + "roundConfig": "@config", + "storedConfig": "@config", + "roundConfigGeneration": 5, + "configGeneration": 6, + "roundResetGeneration": 3, + "resetGeneration": 4, + "expected": "config_changed" + } + ], + "safePath": [ + { + "name": "h2", + "expected": true + }, + { + "name": "a.b-c_d", + "expected": true + }, + { + "name": "", + "expected": false + }, + { + "name": ".", + "expected": false + }, + { + "name": "..", + "expected": false + }, + { + "name": "../h", + "expected": false + }, + { + "name": "a/b", + "expected": false + }, + { + "name": "a\\b", + "expected": false + }, + { + "name": "a\u0000b", + "expected": false + }, + { + "name": "...", + "expected": true + } + ] +} diff --git a/cpp/update_flow_core/tests/update_flow_core_test.cpp b/cpp/update_flow_core/tests/update_flow_core_test.cpp index f1481cae..dea448ce 100644 --- a/cpp/update_flow_core/tests/update_flow_core_test.cpp +++ b/cpp/update_flow_core/tests/update_flow_core_test.cpp @@ -14,6 +14,7 @@ #include "../flow_json.h" #include "../native_config.h" #include "../native_round.h" +#include "../round_engine.h" #include "../update_flow_core.h" using flowjson::Parse; @@ -703,6 +704,118 @@ int RunNativeRound(const Value& doc) { return failures; } +// Replaces every "@name" string with fixtures[name]. +Value WithFixtures(const Value& value, const Value& fixtures) { + if (value.IsString() && !value.AsString().empty() && + value.AsString()[0] == '@') { + const Value& fixture = fixtures.Get(value.AsString().substr(1)); + if (fixture.IsUndefined()) { + std::fprintf(stderr, "unknown fixture %s\n", value.AsString().c_str()); + } + return fixture; + } + if (value.IsArray()) { + Value out = Value::Array(); + for (const Value& element : value.elements()) { + out.Push(WithFixtures(element, fixtures)); + } + return out; + } + if (value.IsObject()) { + Value out = Value::Object(); + for (const auto& member : value.members()) { + out.Set(member.first, WithFixtures(member.second, fixtures)); + } + return out; + } + return value; +} + +// The round driver (round_engine.h): every trace in +// tests/round-engine-vectors.json is fed event by event, each step's +// commands compared with the expected ones; then the host cancellation and +// the path component rule. The JSON binding must agree with the Value API. +int RunRoundEngine(const Value& doc) { + int failures = 0; + const Value& fixtures = doc.Get("fixtures"); + const Value& rounds = doc.Get("rounds"); + const Value& host = doc.Get("host"); + const Value& safePath = doc.Get("safePath"); + if (!rounds.IsArray() || rounds.Size() == 0 || !host.IsArray() || + host.Size() == 0 || !safePath.IsArray() || safePath.Size() == 0) { + std::fprintf(stderr, "round engine vectors: missing cases\n"); + return 1; + } + for (const Value& c : rounds.elements()) { + const std::string& name = c.Get("name").AsString(); + Value state = Value::Undefined(); + std::string stateJson; + size_t index = 0; + for (const Value& step : c.Get("steps").elements()) { + const Value event = WithFixtures(step.Get("event"), fixtures); + const Value expected = WithFixtures(step.Get("commands"), fixtures); + const Value result = flowcore::StepRound(state, event); + if (!Value::DeepEquals(result.Get("commands"), expected)) { + std::fprintf(stderr, + "round \"%s\" step %zu MISMATCH\n expected: %s\n" + " actual: %s\n", + name.c_str(), index, Stringify(expected).c_str(), + Stringify(result.Get("commands")).c_str()); + failures++; + } + bool ok = false; + const Value viaJson = + Parse(flowcore::StepRoundJson(stateJson, Stringify(event)), &ok); + if (!ok || Stringify(viaJson) != Stringify(result)) { + std::fprintf(stderr, "round \"%s\" step %zu: StepRoundJson differs\n", + name.c_str(), index); + failures++; + } + state = result.Get("state"); + stateJson = Stringify(state); + index++; + } + if (state.Get("phase").AsString() != "done") { + std::fprintf(stderr, "round \"%s\" did not finish\n", name.c_str()); + failures++; + } + } + for (const Value& raw : host.elements()) { + const Value c = WithFixtures(raw, fixtures); + const std::string actual = flowcore::HostCancellation( + c.Get("hostConfig").AsString(), c.Get("roundConfig").AsString(), + c.Get("storedConfig").AsString(), + c.Get("roundConfigGeneration").AsNumber(), + c.Get("configGeneration").AsNumber(), + c.Get("roundResetGeneration").AsNumber(), + c.Get("resetGeneration").AsNumber()); + if (actual != c.Get("expected").AsString()) { + std::fprintf(stderr, "host \"%s\" MISMATCH: expected \"%s\", got \"%s\"\n", + c.Get("name").AsString().c_str(), + c.Get("expected").AsString().c_str(), actual.c_str()); + failures++; + } + } + for (const Value& c : safePath.elements()) { + if (flowcore::IsSafePathComponent(c.Get("name").AsString()) != + c.Get("expected").AsBool()) { + std::fprintf(stderr, "safePath %s MISMATCH\n", + Stringify(c.Get("name")).c_str()); + failures++; + } + } + // A binding call that cannot parse its input still ends the round. + bool ok = false; + const Value broken = + Parse(flowcore::StepRoundJson("{", "{\"type\":\"env\"}"), &ok); + if (!ok || broken.Get("commands").At(0).Get("reason").AsString() != + "internal_error") { + std::fprintf(stderr, "StepRoundJson: an unparsable state must finish\n"); + failures++; + } + return failures; +} + bool LoadJson(const char* path, Value* doc) { std::ifstream file(path); if (!file) { @@ -722,12 +835,13 @@ bool LoadJson(const char* path, Value* doc) { } // namespace int main(int argc, char* argv[]) { - if (argc < 5) { + if (argc < 6) { std::fprintf(stderr, "usage: %s " " " " " - "\n", + " " + "\n", argv[0]); return 2; } @@ -735,9 +849,11 @@ int main(int argc, char* argv[]) { Value equivalenceDoc; Value nativeConfigDoc; Value nativeRoundDoc; + Value roundEngineDoc; if (!LoadJson(argv[1], &doc) || !LoadJson(argv[2], &equivalenceDoc) || !LoadJson(argv[3], &nativeConfigDoc) || - !LoadJson(argv[4], &nativeRoundDoc)) { + !LoadJson(argv[4], &nativeRoundDoc) || + !LoadJson(argv[5], &roundEngineDoc)) { return 2; } @@ -750,7 +866,8 @@ int main(int argc, char* argv[]) { RunInputCaps() + RunHandleCheckResponse() + RunConfigEquivalence(equivalenceDoc) + RunNativeConfig(nativeConfigDoc) + - RunNativeRound(nativeRoundDoc) + RunTrimAndDeepEquals(); + RunNativeRound(nativeRoundDoc) + + RunRoundEngine(roundEngineDoc) + RunTrimAndDeepEquals(); for (size_t i = 0; i < cases.Size(); i++) { const Value& testCase = cases.At(i); const std::string& fn = testCase.Get("fn").AsString(); @@ -782,12 +899,14 @@ int main(int argc, char* argv[]) { } std::printf( "all %zu flow vectors, %zu config equivalence vectors, %zu+%zu native " - "config vectors, %zu+%zu+%zu native round vectors + parser robustness " - "passed\n", + "config vectors, %zu+%zu+%zu native round vectors, %zu+%zu+%zu round " + "engine vectors + parser robustness passed\n", cases.Size(), equivalenceDoc.Get("cases").Size(), nativeConfigDoc.Get("runnable").Size(), nativeConfigDoc.Get("normalize").Size(), nativeRoundDoc.Get("prepare").Size(), nativeRoundDoc.Get("query").Size(), - nativeRoundDoc.Get("respond").Size()); + nativeRoundDoc.Get("respond").Size(), + roundEngineDoc.Get("rounds").Size(), roundEngineDoc.Get("host").Size(), + roundEngineDoc.Get("safePath").Size()); return 0; } diff --git a/docs/NATIVE_CHECKUPDATE_DESIGN.md b/docs/NATIVE_CHECKUPDATE_DESIGN.md index be0a1e98..f6d2010b 100644 --- a/docs/NATIVE_CHECKUPDATE_DESIGN.md +++ b/docs/NATIVE_CHECKUPDATE_DESIGN.md @@ -553,3 +553,92 @@ afterDownload)`:本地 silent 策略 或 响应标记 forceBoot 即激活。 改动不触发 .so 重编。发版 10.52.0 后按 2026-08-12 同一实验复测:markSuccess → 落盘标记 → 每次启动 ~230ms 即崩,验证 handler 窗口完成 check+download+ commit、下次启动进修复版,时序记录回 NATIVE_CHECK_FOLLOWUPS.md。 + +## 12. 共享轮次引擎(RoundEngine,2026-10) + +### 12.1 动机 + +§10 的纯决策早已下沉到 `cpp/update_flow_core`(`native_round.h` 的 PrepareRound / +NextQueryStep / HandleRoundResponse,`native_config.h` 的配置门与 C1 等价),由 +`tests/*-vectors.json` 锁定。但把这些步骤串起来的**驱动循环**仍是三份手写:代数采样 +→ 配置门 → 面包屑 → query 循环 → 响应决策 → 已安装判定 → 双预算下载循环 → 原子提交 +→ 结果映射 → 宿主等待后的取消判定。2026-10 的架构审查在这一层逐个人工对照才发现了错误码 +分类、连接超时、prepareBundle 超时、激活前摘要门等漂移;向量只锁住了被调用的纯函数, +锁不住"怎么调用"。 + +### 12.2 形态:sans-IO 的 step 函数 + +`cpp/update_flow_core/round_engine.h`: + +```text +StepRound(state, event) -> { state, commands } +``` + +- **纯函数**:不做 IO、不读时钟、不加锁、不起线程。时间由事件带入(`now`,平台单调时钟 + 毫秒),墙钟只在 HTTP 响应事件里带入(`at`,响应到达时刻,供 §10.3 缓存)。 +- **状态是 JSON 值**(与 NextQueryStep 同一风格):JNI / NAPI 绑定只传文本、不持有句柄, + 没有生命周期与跨线程问题;iOS 直接用 `flowjson::Value`。绑定入口 `StepRoundJson` + 解析失败时直接给出 `finish failed/internal_error`,平台不会卡在无命令的状态。 +- **鸿蒙单线程**:每步都是一次毫秒级的同步调用,绝不阻塞;HTTP、下载等重步骤仍由 ArkTS + 异步执行(下载内部照旧走 napi_async_work)。 + +事件(均带 `now`,可带粘滞标志 `crashHold` / `forceActivate`): + +| 事件 | 字段 | 说明 | +| --- | --- | --- | +| start | config, resetGeneration, configGeneration, sample, deadline | 代数在读配置之前采样;deadline=0 表示无预算 | +| env | env | 回应 collectEnv(bundleHash 等 IO 在面包屑落盘之后才算) | +| http | text?, at | text 缺省 = 请求失败 | +| installed | installed | 回应 checkInstalled | +| download | ok, code?, timedOut? | timedOut:尝试超出了自己的 deadline | +| committed | ok, reason?, activated?, error? | ok=false:reset 或配置变更赢了,reason 指明是哪个代数(默认 reset,或 config_changed);error:提交本身失败(摘要门拒绝、写入抛错) | + +命令: + +| 命令 | 字段 | 平台职责 | +| --- | --- | --- | +| markIncomplete | — | 落盘 §11.4 面包屑;轮次结束(无论如何结束)由适配器清除 | +| collectEnv | — | 采集本次启动事实,回 env 事件 | +| http | url, post, body?, timeoutMs, callTimeoutMs | 空闲超时与整次超时都已按预算钳制;https→http 重定向拒绝、响应体上限仍在平台 | +| checkInstalled | hash | 完整安装(`.pushy-complete`)是否已在磁盘 | +| download | strategy, url, hash, originHash?, deadline | 下载 + 解压/打补丁;超时由平台取消任务 | +| commit | hash, hashInfo, activate, cache, resetGeneration, configGeneration | 一把锁内复核两个代数,激活前走 CheckActivation 摘要门;hash 为空 = 只提交缓存 | +| finish | status, reason, hash?, activated? | 轮次结束;之后的事件被忽略 | + +阶段:`env → http(循环) → installed → download(循环) → committed → done`。当前阶段不期待的事件 +直接 `finish failed/internal_error`,适配器的编排错误会立刻暴露,而不是静默挂起。 + +### 12.3 引擎负责 / 平台保留 + +引擎负责:配置门(ParseRunnableConfig)、面包屑时机、请求体与 query 遍历、单请求超时 +(空闲 10s、整次 15s,均被预算钳制,预算耗尽不再发请求)、响应决策、hash 路径安全校验、 +已安装短路、双下载预算(diff/pdiff 共享 600s,首个 full 尝试另开 600s,均被预算钳制)、 +镜像重试(IsMirrorRetryableCode:PATCH_FAILED 换策略不换镜像)、超时语义(增量超时换策略, +full 超时整体失败)、激活判定(`activate || crashHold || forceActivate`)与 crash-hold +版本信息标记、提交结果到 status/reason/hash/activated 的映射,以及宿主等待后的取消判定 +`HostCancellation`(配置三方 C1 等价且配置代数不变,否则 config_changed;再看 reset 代数)。 + +平台保留:HTTP 客户端本身、下载与解压管线、持久化与提交的原子性(锁内复核代数、激活摘要门)、 +进程级单轮次(roundStarted CAS)、"JS 已查过"的跳过判定、5s 延迟与零延迟续传、crash-hold 的 +系统钩子与 hold 预算、iOS tvOS purge-restore 的启动阻塞与窗口关闭、Android 的线程与 JNI、 +鸿蒙的异步执行、RN reload。 + +### 12.4 相对原手写循环的有意对齐 + +由 `tests/round-engine-vectors.json` 锁定,接入各端时随之生效: + +- 宿主取消判定:鸿蒙原用字符串全等比较配置,改为 C1 等价(与 Android/iOS 一致)。 +- 提交带配置代数:iOS 早已在锁内复核;Android/鸿蒙接入时补上(原先只在宿主等待后检查)。 +- HTTP 超时:三端统一为空闲 10s + 整次 15s,并按预算钳制(iOS 原先的信号量兜底会比预算多等 5s)。 +- 下载超时:显式 `timedOut`,不再依赖鸿蒙把超时包装成 DOWNLOAD_FAILED 后由下一次循环看 deadline。 +- 只提交缓存的提交失败统一为 `failed/commit_failed`(Android 原为 internal_error)。 +- `finish.activated` 反映实际激活(iOS purge-restore 窗口关闭会丢弃激活),其余平台未上报时取请求值。 +- crash hold 在轮次中途开始也会强制激活(事件携带粘滞标志)。 + +### 12.5 分期 + +1. 引擎 + 轨迹向量 + C++ 测试进 CI(不改平台代码)。 +2. 补 CODE_AUDIT 4.4:鸿蒙 NAPI 回调与 iOS `.mm` 入口的 C++ `catch (...)`——引擎上移扩大了 + C++ 异常穿越 NAPI/ObjC 的面,必须先兜住。 +3. 逐端接入(Android → iOS → 鸿蒙),每端的编排器改为"驱动引擎 + 执行命令"的薄适配器, + 删除被取代的手写循环;Android/鸿蒙在 #669(prepareBundle 超时)合入之后基于最新 master 改。 diff --git a/scripts/test-update-flow-core.sh b/scripts/test-update-flow-core.sh index 917f5883..4e50ba05 100755 --- a/scripts/test-update-flow-core.sh +++ b/scripts/test-update-flow-core.sh @@ -28,7 +28,8 @@ c++ \ "$ROOT_DIR/cpp/update_flow_core/tests/flow_vectors.json" \ "$ROOT_DIR/cpp/update_flow_core/tests/config-equivalence-vectors.json" \ "$ROOT_DIR/cpp/update_flow_core/tests/native-config-vectors.json" \ - "$ROOT_DIR/cpp/update_flow_core/tests/native-round-vectors.json" + "$ROOT_DIR/cpp/update_flow_core/tests/native-round-vectors.json" \ + "$ROOT_DIR/cpp/update_flow_core/tests/round-engine-vectors.json" # Opt-in libFuzzer run of the JSON parser + decision pipeline (raw server # bytes): FUZZ=1 bun run test:flow-core.