Verify native notification artwork with real JPEG regression

This commit is contained in:
archipelago
2026-10-07 19:00:20 -04:00
parent b0b95810e0
commit 60bd728a04
2 changed files with 58 additions and 2 deletions
@@ -58,6 +58,38 @@ class CompanionAudioTest {
} finally { CompanionAudioBridge.release(web); lifecycle.destroy(); web.destroy() } } finally { CompanionAudioBridge.release(web); lifecycle.destroy(); web.destroy() }
} }
@Test
@Config(shadows = [RecordingAudioMediaSession::class])
fun jpegArtworkReachesNotificationAndMediaDescription() {
// Real 16x16 JPEG generated by Chromium canvas, independent of Android bitmap shadows.
val artwork = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAn/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AJVAA//Z"
assertNotNull(decodeArtwork(artwork))
val app = RuntimeEnvironment.getApplication()
val web = android.webkit.WebView(app); web.loadUrl("https://node.test/cloud")
val payload = state().put("session", "32345678-1234-1234-1234-123456789abc").put("artwork", artwork)
CompanionAudioBridge.receive(web, payload.toString(), "https://node.test", true, setOf("https://node.test")) {}
val lifecycle = Robolectric.buildService(CompanionAudioService::class.java).create()
try {
lifecycle.get().onStartCommand(null, 0, 1)
val notification = Shadows.shadowOf(lifecycle.get()).lastForegroundNotification
assertNotNull(notification.getLargeIcon())
// Robolectric's MediaController does not read MediaSession metadata;
// capture the actual service's setMetadata call instead.
val metadata = RecordingAudioMediaSession.metadata!!
assertNotNull(metadata.getBitmap(android.media.MediaMetadata.METADATA_KEY_ALBUM_ART))
assertNotNull(metadata.description.iconBitmap)
// Position-only refresh must retain the same thumbnail.
CompanionAudioBridge.receive(web, state().put("session", payload.getString("session")).put("sequence", 2).toString(),
"https://node.test", true, setOf("https://node.test")) {}
assertNotNull(Shadows.shadowOf(lifecycle.get()).lastForegroundNotification.getLargeIcon())
// A following song without art must not keep the previous cover.
CompanionAudioBridge.receive(web, state().put("session", payload.getString("session")).put("sequence", 3).put("artwork", "").toString(),
"https://node.test", true, setOf("https://node.test")) {}
assertNull(Shadows.shadowOf(lifecycle.get()).lastForegroundNotification.getLargeIcon())
assertNull(RecordingAudioMediaSession.metadata!!.description.iconBitmap)
} finally { CompanionAudioBridge.release(web); lifecycle.destroy(); web.destroy() }
}
@Test fun diagnosticReportExcludesPrivateMessagesAndRecordsRejectionStage() { @Test fun diagnosticReportExcludesPrivateMessagesAndRecordsRejectionStage() {
val app = RuntimeEnvironment.getApplication() val app = RuntimeEnvironment.getApplication()
val web = android.webkit.WebView(app) val web = android.webkit.WebView(app)
@@ -116,3 +148,10 @@ class CompanionAudioTest {
} finally { lifecycle.destroy() } } finally { lifecycle.destroy() }
} }
} }
@org.robolectric.annotation.Implements(android.media.session.MediaSession::class)
class RecordingAudioMediaSession : org.robolectric.shadows.ShadowMediaSession() {
companion object { var metadata: android.media.MediaMetadata? = null }
@org.robolectric.annotation.Implementation
fun setMetadata(value: android.media.MediaMetadata) { metadata = value }
}
+19 -2
View File
@@ -124,10 +124,27 @@ Evidence:
never produced a thumbnail; corrected composable produced a real JPEG. Browser never produced a thumbnail; corrected composable produced a real JPEG. Browser
checks also verified no cross-origin cookie, no followed redirect, and playback checks also verified no cross-origin cookie, no followed redirect, and playback
remained active after the optional image error. remained active after the optional image error.
- No native runtime or APK version change. A positive native JPEG/notification/ - No native runtime or APK version change. The focused positive native JPEG
metadata regression is prepared separately and awaits its coordinated test slot. regression passed on SDK 28 and 35 (2 tests, zero failures/errors/skips). It
verifies the actual service places a real JPEG in the notification large icon
and media-description bitmap, retains it on position-only updates, and clears
it for an explicitly artless following song. Initial test compilation used
unavailable Java desktop ImageIO APIs; replacing that test-only fixture with a
fixed Chromium-generated JPEG resolved the harness issue. Production Kotlin
remained unchanged/up-to-date. All owned test JVMs exited after success.
- This identifies and fixes a real artwork boundary defect, not the operator's - This identifies and fixes a real artwork boundary defect, not the operator's
exact cover URL. Physical notification-artwork acceptance remains open until exact cover URL. Physical notification-artwork acceptance remains open until
the qualified UI reaches the node and the operator verifies it. the qualified UI reaches the node and the operator verifies it.
Evidence folder: `~/.local/state/archipelago/release-qualification/companion-artwork-20261007/`. Evidence folder: `~/.local/state/archipelago/release-qualification/companion-artwork-20261007/`.
## Actual app artwork access limitation
A separate read-only Chromium probe retained the managed node-demo-v4v iframe,
but the actual app required Nostr login before initializing its media catalog.
With signing and payments blocked, the probe timed out before obtaining a cover
URL. No image access/CORS policy was bypassed, no credentials were forwarded,
and no real app artwork was decoded in that probe. The successful browser A/B
fixture and native JPEG test establish the corrected pipeline, not that the
operator's particular cover is anonymously accessible. Physical artwork acceptance
remains pending after the qualified UI deployment; APK 0.5.37 stays unchanged.