commit 90bd7c82fa0cda65917f96ac593ededa7b732a05
parent b3bd53e982ab254dc5baecdd38c56a78dbcb088e
Author: triesap <tyson@radroots.org>
Date: Fri, 28 Aug 2026 18:58:19 +0000
test: bind photo personas to authored wire shape
- accept only the governed two-line localhost Photo update content
- require matching BUD11 digest and NIP-92 metadata in fixture evidence
- fail fast when Add navigation or Today projection evidence is absent
- cover exact acceptance and near-match rejection in fixture tests
Diffstat:
3 files changed, 104 insertions(+), 11 deletions(-)
diff --git a/RadrootsUITests/RadrootsRemoteQualificationUITests.swift b/RadrootsUITests/RadrootsRemoteQualificationUITests.swift
@@ -107,10 +107,10 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
try publishFood(app, marker: markers[3])
let expected = [marker] + markers
- assertTodayContains(app, markers: expected)
+ try assertTodayContains(app, markers: expected)
app.terminate()
app = launchToRoot(configuration)
- assertTodayContains(app, markers: expected)
+ try assertTodayContains(app, markers: expected)
guard openAdd(app) != nil, openDrafts(app) else {
return XCTFail("The durable outbox was unavailable after the final relaunch")
@@ -189,13 +189,13 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
default:
throw QualificationError.invalidPersonaFixture
}
- assertTodayContains(app, markers: [attempt.marker])
+ try assertTodayContains(app, markers: [attempt.marker])
}
let markers = persona.attempts.map(\.marker)
app.terminate()
app = launchPersona(configuration)
- assertTodayContains(app, markers: markers)
+ try assertTodayContains(app, markers: markers)
XCTAssertEqual(try readPublicKey(app), publicKey)
app.terminate()
}
@@ -947,19 +947,31 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
@MainActor
private func beginDraft(_ app: XCUIApplication, type: String) throws {
- guard let picker = openAdd(app) else {
+ guard openAdd(app) != nil else {
XCTFail("The Add bottom tab did not present the real Add store")
throw QualificationError.missingProductSurface
}
let newDraft = app.buttons["radroots.add.new"]
- XCTAssertTrue(newDraft.waitForExistence(timeout: 10))
+ guard newDraft.waitForExistence(timeout: 10), waitUntilHittable(newDraft, timeout: 10) else {
+ XCTFail("The New draft action was unavailable")
+ throw QualificationError.missingProductSurface
+ }
newDraft.tap()
+ scrollAddFormToTop(app)
+ let picker = app.descendants(matching: .any)["radroots.add.type"]
+ guard picker.waitForExistence(timeout: 10), waitUntilHittable(picker, timeout: 10) else {
+ XCTFail("The Add type picker was unavailable")
+ throw QualificationError.missingProductSurface
+ }
if picker.value as? String == type {
return
}
picker.tap()
let option = app.buttons[type]
- XCTAssertTrue(option.waitForExistence(timeout: 10))
+ guard option.waitForExistence(timeout: 10), waitUntilHittable(option, timeout: 10) else {
+ XCTFail("The Add type picker did not present \(type)")
+ throw QualificationError.missingProductSurface
+ }
option.tap()
guard waitForValue(picker, value: type, timeout: 10) else {
XCTFail("The Add type picker did not select \(type)")
@@ -1019,7 +1031,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
}
@MainActor
- private func assertTodayContains(_ app: XCUIApplication, markers: [String]) {
+ private func assertTodayContains(_ app: XCUIApplication, markers: [String]) throws {
app.tabBars.buttons["Today"].tap()
let refresh = app.buttons["radroots.today.refresh"]
if refresh.waitForExistence(timeout: 5) {
@@ -1028,7 +1040,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
let feed = app.descendants(matching: .any)["radroots.today.feed"].firstMatch
guard feed.waitForExistence(timeout: 30) else {
XCTFail("The local relay refresh did not materialize the Today feed")
- return
+ throw QualificationError.missingProductSurface
}
for marker in markers.reversed() {
let card = app.descendants(matching: .any).matching(
@@ -1037,7 +1049,10 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
for _ in 0..<6 where !card.exists {
feed.swipeUp()
}
- XCTAssertTrue(card.waitForExistence(timeout: 30), "Missing Today card \(marker)")
+ guard card.waitForExistence(timeout: 30) else {
+ XCTFail("Missing Today card \(marker)")
+ throw QualificationError.missingProductSurface
+ }
}
}
diff --git a/scripts/local-social-fixture.py b/scripts/local-social-fixture.py
@@ -564,10 +564,45 @@ def classify_attempt(
values = [event.get("content")]
for tag in event.get("tags", []):
values.extend(tag)
- matches = [attempt for attempt in attempts.values() if attempt["marker"] in values]
+ matches = [
+ attempt
+ for attempt in attempts.values()
+ if (
+ photo_attempt_matches(event, attempt)
+ if attempt.get("flow") == "PhotoUpdate"
+ else attempt["marker"] in values
+ )
+ ]
return matches[0] if len(matches) == 1 else None
+def photo_attempt_matches(event: dict[str, Any], attempt: dict[str, Any]) -> bool:
+ if attempt.get("flow") != "PhotoUpdate":
+ return False
+ content = event.get("content")
+ if not isinstance(content, str):
+ return False
+ lines = content.split("\n")
+ if len(lines) != 2 or lines[0] != attempt.get("marker"):
+ return False
+ url = lines[1]
+ match = re.fullmatch(
+ r"http://127\.0\.0\.1:([1-9][0-9]{0,4})/([0-9a-f]{64})\.png", url
+ )
+ if match is None or int(match.group(1)) > 65535:
+ return False
+ digest = match.group(2)
+ imeta = [
+ tag
+ for tag in event.get("tags", [])
+ if isinstance(tag, list) and tag[:1] == ["imeta"]
+ ]
+ return len(imeta) == 1 and all(
+ field in imeta[0]
+ for field in (f"url {url}", f"x {digest}", "m image/png")
+ )
+
+
def identity_digest(public_key: str | None) -> str:
if public_key is None:
return "0" * 64
diff --git a/scripts/test_local_social_fixture.py b/scripts/test_local_social_fixture.py
@@ -209,6 +209,49 @@ class LocalSocialFixtureTests(unittest.TestCase):
)
self.assertIsNone(fixture.read_control(path))
+ def test_attempt_classification_accepts_only_exact_photo_wire_shape(self) -> None:
+ marker = "rr-p01-a02-photo"
+ digest = "a" * 64
+ url = f"http://127.0.0.1:21101/{digest}.png"
+ attempt = {"flow": "PhotoUpdate", "marker": marker}
+ event = {
+ "content": f"{marker}\n{url}",
+ "tags": [
+ [
+ "imeta",
+ f"url {url}",
+ f"x {digest}",
+ "m image/png",
+ "dim 1x1",
+ "size 1",
+ "alt Local qualification image",
+ ]
+ ],
+ }
+
+ self.assertIs(
+ fixture.classify_attempt(event, {"P01-A02": attempt}), attempt
+ )
+ for mutation in (
+ lambda value: value.update({"content": marker}),
+ lambda value: value.update({"content": f"prefix {marker}\n{url}"}),
+ lambda value: value.update(
+ {"content": f"{marker}\nhttps://example.com/{digest}.png"}
+ ),
+ lambda value: value["tags"][0].remove(f"x {digest}"),
+ ):
+ changed = copy.deepcopy(event)
+ mutation(changed)
+ self.assertIsNone(
+ fixture.classify_attempt(changed, {"P01-A02": attempt})
+ )
+
+ text_attempt = {"flow": "Update", "marker": "rr-p01-a01-update"}
+ embedded = {"content": "prefix rr-p01-a01-update", "tags": []}
+ self.assertIsNone(
+ fixture.classify_attempt(embedded, {"P01-A01": text_attempt})
+ )
+
if __name__ == "__main__":
unittest.main()