mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
test(mobile): expand native cache concurrency
This commit is contained in:
@@ -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,
|
||||
|
||||
+82
@@ -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",
|
||||
|
||||
+124
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user