test(mobile): expand native cache concurrency

This commit is contained in:
OrcaWin
2026-08-09 18:35:01 -04:00
committed by Jinwoo-H
parent b2068661f7
commit 64f28fcb2f
5 changed files with 232 additions and 17 deletions
@@ -1894,11 +1894,12 @@ and diff hygiene pass. A fresh production RNW build remains
49 assets, 9,281,663 raw bytes, and 2,684,764 gzip bytes.
The native generated-mutation and concurrency slice passes 1,152 generated
rejections and 72 concurrent cache flows per platform. The complete iOS
fault executable and all 37 Android module tests pass. Android lint reports zero
errors after replacing API-26-only base64 calls and guarding optional WebView
features; Swift format lint, mobile lint/typecheck/format, max-lines, and diff
hygiene also pass.
rejections and 120 concurrent cache flows per platform. The expanded matrix
adds competing same-host distinct generations, live-session activation and
cleanup, and interleaved commit/abort mutations. The complete iOS fault
executable and Android module tests pass. Android lint reports zero errors after
replacing API-26-only base64 calls and guarding optional WebView features; Swift
format lint, mobile lint/typecheck/format, max-lines, and diff hygiene also pass.
The bridge mutation and lifecycle slice rejects ambiguous raw JSON before
schema traversal, runs 2,016 generated operation payload/size cases across all
@@ -2815,4 +2816,5 @@ package verification pass. The rebuilt package is
| 2026-07-28 | Complete | Every hosted mutation reauthorizes its exact workspace, repository, task target, native-chat session, or Agent History session immediately before its privileged write. Three deterministic race suites revoke authority during awaited preflight across file, Markdown, Source Control, provider review, task, native-chat, workspace creation, browser, and Agent History operations. Privileged state is keyed by the paired Desktop public key. |
| 2026-07-28 | Complete | A 25-case schema-valid semantic response corpus rejects cross-workspace/tab/repository/target/host/scope/configuration/cursor results and per-request collection overruns. Session events are workspace-bound. All 18 mobile-web files / 90 tests, 10 focused mobile files / 48 tests, the full 576-file mobile suite / 3,440 tests with 2 expected skips, all typechecks/lints/55 reliability gates, max-lines, localization, and package verification pass. Build `bcd9c95e3a1e416d82a240ff10ef311375d03cbc3210d44876c7bca896e093b7` contains 50 assets, 9,299,540 raw bytes, and 2,691,263 gzip bytes. |
| 2026-07-28 | Finding | The full root run passes 3,829 files / 40,205 tests with 70 expected skips but the unrelated `ProjectViewWrapper` dynamic-import boundary again reaches its 30-second timeout under full-suite load. Its isolated rerun passes 2/2 in 4.69 seconds. |
| 2026-07-28 | Next | Run exact release-app corpus injection, concurrent cache mutation, privacy audit, broader live two-host/topology races, and independent security review before the remaining physical-device, packaged-release, rollback, and App Store gates. |
| 2026-07-28 | Complete | Mirrored Swift and Kotlin package-store suites now pass 120 concurrent cache flows per platform. The added same-host matrix covers 16 competing distinct-generation commits, 16 live-session activation/cleanup flows with post-activation reads, and 16 interleaved commit/abort mutations; final activation retains at most active plus previous, and host removal leaves no cache subtree. |
| 2026-07-28 | Next | Run exact release-app corpus injection, privacy audit, broader live two-host/topology races, and independent security review before the remaining physical-device, packaged-release, rollback, and App Store gates. |
@@ -164,12 +164,13 @@ prototype:
malformed metadata, incomplete/corrupt generations, wrong-host opens, and
corruption discovered after a session has opened with bounded errors.
- Mirrored native generated tests reject 1,152 malformed manifests, chunks,
offsets, paths, and SHA-256 tokens per platform. Both stores pass 24
concurrent distinct-host package lifecycles, 24 duplicate-generation commits,
and 24 same-host open/read/activate/close flows. Android package code remains
compatible with minSdk 24 by using the API-8 platform base64 codec, and
optional WebView message/document-start features fail closed behind explicit
availability checks.
offsets, paths, and SHA-256 tokens per platform. Both stores pass 120
concurrent cache flows: distinct-host lifecycles, duplicate commits,
competing same-host generations, live-session activation and cleanup, and
interleaved commit/abort mutations. Android package code remains compatible
with minSdk 24 by using the API-8 platform base64 codec, and optional WebView
message/document-start features fail closed behind explicit availability
checks.
- The native shell negotiates only the named operations in the production
grant registry across workspace, session, terminal, file, source-control,
review, task, account, browser, speech, native-chat, navigation, and narrow
@@ -322,9 +322,13 @@ verification pass. The full root run passes 3,829 files / 40,205 tests with 70
expected skips except for one unrelated 30-second dynamic-import timeout under
load; its isolated 2-test rerun passes in 4.69 seconds.
The remaining security work below is exact release-app corpus testing,
concurrent cache mutation, broader live cross-scope races, privacy audit, and
independent review.
The mirrored native package-store suites now pass 120 concurrent cache flows per
platform. The expanded same-host matrix covers competing distinct generations,
live-session activation and cleanup with post-activation reads, interleaved
commit/abort mutations, bounded final retention, and host removal.
The remaining security work below is exact release-app corpus testing, broader
live cross-scope races, privacy audit, and independent review.
## 1. Production Cutover and Cleanup
@@ -383,8 +387,10 @@ independent review.
and an oversized request before authority access. The exported result and
subscription event schemas reject a generated valid-envelope payload
corpus and retire invalid events. Pending subscription
cancellation/client replacement/disposal races pass. A fresh exact-app
rerun, concurrent cache mutation, and the other listed boundaries remain.
cancellation/client replacement/disposal races pass. Mirrored Swift and
Kotlin stores pass 120 concurrent cache flows, including same-host
generation, activation/cleanup, commit/abort, and removal mutations. A
fresh exact-app rerun and the other listed boundaries remain.
The page-side semantic corpus rejects 25 schema-valid cross-operation
identity, action, cursor, and request-specific-limit mutations.
- [ ] Attempt cross-host, cross-build, cross-workspace, cross-session, replay,
@@ -63,6 +63,80 @@ class MobileWebPackageStoreConcurrencyTest {
assertFalse(staging.listFiles()?.isNotEmpty() == true)
}
@Test
fun preservesCompetingLiveGenerationsDuringConcurrentActivation() {
val root = temporary.newFolder()
val store = concurrencyStore(root)
val failures = ConcurrentLinkedQueue<Throwable>()
val fixtures = (0 until 16).map {
concurrencyFixture("<title>generation-$it</title>")
}
runConcurrent(fixtures.size, failures) { index ->
concurrencyStagePackage(store, "generation-host", fixtures[index])
}
assertTrue(failures.joinToString("\n") { it.stackTraceToString() }, failures.isEmpty())
val sessions = fixtures.map { store.openSession("generation-host", it.buildId, 1) }
runConcurrent(sessions.size, failures) { index ->
val sessionId = sessions[index].getValue("sessionId")
assertArrayEquals(fixtures[index].bytes, store.readAsset(sessionId, "index.html").bytes)
assertEquals(fixtures[index].buildId, store.markSessionHealthy(sessionId))
assertArrayEquals(fixtures[index].bytes, store.readAsset(sessionId, "index.html").bytes)
}
assertTrue(failures.joinToString("\n") { it.stackTraceToString() }, failures.isEmpty())
val active = store.openSession("generation-host", null, 1)
val activeBuildId = active.getValue("buildId")
val activeFixture = fixtures.first { it.buildId == activeBuildId }
assertArrayEquals(
activeFixture.bytes,
store.readAsset(active.getValue("sessionId"), "index.html").bytes
)
sessions.forEach { store.closeSession(it.getValue("sessionId")) }
assertEquals(activeBuildId, store.markSessionHealthy(active.getValue("sessionId")))
val generations = File(
root,
"${concurrencySha256("generation-host".toByteArray())}/generations"
)
assertTrue(generations.listFiles().orEmpty().size <= 2)
store.closeSession(active.getValue("sessionId"))
}
@Test
fun serializesConcurrentCommitAndAbortMutations() {
val root = temporary.newFolder()
val store = concurrencyStore(root)
val failures = ConcurrentLinkedQueue<Throwable>()
val fixtures = (0 until 16).map { concurrencyFixture("<title>stage-$it</title>") }
val stages = fixtures.map { store.beginStage("stage-host", it.manifest, it.canonical) }
runConcurrent(stages.size, failures) { index ->
if (index % 2 == 0) {
concurrencyFinishStage(store, stages[index], fixtures[index])
} else {
store.abortStage(stages[index])
}
}
assertTrue(failures.joinToString("\n") { it.stackTraceToString() }, failures.isEmpty())
fixtures.forEachIndexed { index, fixture ->
if (index % 2 == 0) {
val session = store.openSession("stage-host", fixture.buildId, 1)
assertArrayEquals(
fixture.bytes,
store.readAsset(session.getValue("sessionId"), "index.html").bytes
)
store.closeSession(session.getValue("sessionId"))
} else {
assertTrue(runCatching { store.openSession("stage-host", fixture.buildId, 1) }.isFailure)
}
}
store.removeHost("stage-host")
val hostRoot = File(root, concurrencySha256("stage-host".toByteArray()))
assertFalse(hostRoot.exists())
}
private fun runConcurrent(
count: Int,
failures: ConcurrentLinkedQueue<Throwable>,
@@ -97,6 +171,14 @@ class MobileWebPackageStoreConcurrencyTest {
fixture: ConcurrencyFixture
) {
val stageId = store.beginStage(host, fixture.manifest, fixture.canonical)
concurrencyFinishStage(store, stageId, fixture)
}
private fun concurrencyFinishStage(
store: MobileWebPackageStore,
stageId: String,
fixture: ConcurrencyFixture
) {
store.writeAssetChunk(
stageId,
"index.html",
@@ -5,6 +5,8 @@ enum MobileWebPackageStoreConcurrencyTests {
static func run(root: URL) throws {
try exerciseIndependentHosts(root: root.appendingPathComponent("hosts"))
try exerciseDuplicateGeneration(root: root.appendingPathComponent("duplicate"))
try exerciseCompetingGenerations(root: root.appendingPathComponent("generations"))
try exerciseCommitAndAbort(root: root.appendingPathComponent("commit-abort"))
}
private static func exerciseIndependentHosts(root: URL) throws {
@@ -81,6 +83,120 @@ enum MobileWebPackageStoreConcurrencyTests {
let remaining = try? FileManager.default.contentsOfDirectory(atPath: staging.path)
precondition(remaining?.isEmpty != false)
}
private static func exerciseCompetingGenerations(root: URL) throws {
let store = MobileWebPackageStore(cacheRoot: root)
let fixtures = try (0..<16).map {
try concurrencyFixture(content: "<title>generation-\($0)</title>")
}
let failures = ConcurrentFailureCollector()
DispatchQueue.concurrentPerform(iterations: fixtures.count) { index in
do {
try concurrencyStagePackage(store: store, host: "generation-host", fixture: fixtures[index])
} catch {
failures.append(error)
}
}
precondition(failures.isEmpty)
let sessions = try fixtures.map {
try store.openSession(
hostIdentity: "generation-host",
buildId: $0.buildId,
bridgeVersion: 1
)
}
DispatchQueue.concurrentPerform(iterations: sessions.count) { index in
do {
let sessionId = sessions[index]["sessionId"]!
let before = try store.readAsset(sessionId: sessionId, path: "index.html")
precondition(before.data == fixtures[index].bytes)
let healthyBuildId = try store.markSessionHealthy(sessionId: sessionId)
precondition(healthyBuildId == fixtures[index].buildId)
let after = try store.readAsset(sessionId: sessionId, path: "index.html")
precondition(after.data == fixtures[index].bytes)
} catch {
failures.append(error)
}
}
precondition(failures.isEmpty)
let active = try store.openSession(
hostIdentity: "generation-host",
buildId: nil,
bridgeVersion: 1
)
let activeBuildId = active["buildId"]!
let activeFixture = fixtures.first { $0.buildId == activeBuildId }!
let activeAsset = try store.readAsset(sessionId: active["sessionId"]!, path: "index.html")
precondition(activeAsset.data == activeFixture.bytes)
for session in sessions {
store.closeSession(sessionId: session["sessionId"]!)
}
let retainedBuildId = try store.markSessionHealthy(sessionId: active["sessionId"]!)
precondition(retainedBuildId == activeBuildId)
let generationRoot =
root
.appendingPathComponent(concurrencySha256(Data("generation-host".utf8)))
.appendingPathComponent("generations")
let retained = try FileManager.default.contentsOfDirectory(atPath: generationRoot.path)
precondition(retained.count <= 2)
store.closeSession(sessionId: active["sessionId"]!)
}
private static func exerciseCommitAndAbort(root: URL) throws {
let store = MobileWebPackageStore(cacheRoot: root)
let fixtures = try (0..<16).map {
try concurrencyFixture(content: "<title>stage-\($0)</title>")
}
let stages = try fixtures.map {
try store.beginStage(
hostIdentity: "stage-host",
manifestJson: $0.manifest,
canonicalManifestJson: $0.canonical
)
}
let failures = ConcurrentFailureCollector()
DispatchQueue.concurrentPerform(iterations: stages.count) { index in
if index.isMultiple(of: 2) {
do {
try concurrencyFinishStage(store: store, stageId: stages[index], fixture: fixtures[index])
} catch {
failures.append(error)
}
} else {
store.abortStage(stageId: stages[index])
}
}
precondition(failures.isEmpty)
for index in fixtures.indices {
if index.isMultiple(of: 2) {
let session = try store.openSession(
hostIdentity: "stage-host",
buildId: fixtures[index].buildId,
bridgeVersion: 1
)
let asset = try store.readAsset(sessionId: session["sessionId"]!, path: "index.html")
precondition(asset.data == fixtures[index].bytes)
store.closeSession(sessionId: session["sessionId"]!)
} else {
do {
_ = try store.openSession(
hostIdentity: "stage-host",
buildId: fixtures[index].buildId,
bridgeVersion: 1
)
preconditionFailure("aborted stage opened a generation")
} catch {}
}
}
try store.removeHost(hostIdentity: "stage-host")
let hostRoot = root.appendingPathComponent(concurrencySha256(Data("stage-host".utf8)))
precondition(!FileManager.default.fileExists(atPath: hostRoot.path))
}
}
private final class ConcurrentFailureCollector: @unchecked Sendable {
@@ -117,6 +233,14 @@ private func concurrencyStagePackage(
manifestJson: fixture.manifest,
canonicalManifestJson: fixture.canonical
)
try concurrencyFinishStage(store: store, stageId: stageId, fixture: fixture)
}
private func concurrencyFinishStage(
store: MobileWebPackageStore,
stageId: String,
fixture: ConcurrencyFixture
) throws {
try store.writeAssetChunk(
stageId: stageId,
path: "index.html",