Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions qa/release_readiness.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@


REQUIRED_RELEASE_PERSONAS = ["newbie", "veteran", "adversarial", "narrative", "optimizer"]
RELEASE_VERDICT_GATE = "full_five_persona_rri"
GATE_SPLIT_CONTRACT = {
"deterministic_built_app_smoke": {
"scope": "fast built-app wiring proof with deterministic provider",
"release_verdict": False,
},
"short_real_provider_playtest": {
"scope": "short built-app proof with a real provider and provider trace evidence",
"release_verdict": False,
},
RELEASE_VERDICT_GATE: {
"scope": "non-partial five-persona release readiness verdict",
"release_verdict": True,
},
}


def looks_like_path(value: str) -> bool:
Expand Down Expand Up @@ -323,6 +338,8 @@ def main() -> int:
result = {
"rri": rri,
"release_ready": release_ready,
"release_verdict_gate": RELEASE_VERDICT_GATE,
"gate_split_contract": GATE_SPLIT_CONTRACT,
"partial": bool(missing_personas or evidence_gaps),
"harness_contaminated": bool(missing_personas or harness_failures or evidence_gaps),
"expected_personas": expected_personas,
Expand Down
33 changes: 33 additions & 0 deletions qa/test_macos_app_static.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,39 @@ def test_scripted_provider_is_dev_gated_and_model_free(self):
self.assertNotIn("claude -p", script)
self.assertNotIn("codex -p", script)

def test_built_app_playtest_emits_split_failure_buckets(self):
harness = self.read("qa/ui_playtest_app.sh")

for bucket in (
"no_app",
"no_launcher",
"no_provider",
"no_art",
"no_actor",
"no_actions",
"move_rejected",
"no_narration",
"console_error",
"permission_prompt",
"score_failed",
):
self.assertIn(f'"{bucket}"', harness)

self.assertIn("PART_A_FAILURE_BUCKET", harness)
self.assertIn("PART_A_FAILURE_DETAIL", harness)
self.assertIn("PART_B_FAILURE_BUCKET", harness)
self.assertIn("PART_B_FAILURE_DETAIL", harness)
self.assertIn("classify_native_failure", harness)
self.assertIn("classify_part_b_readiness_failure", harness)
self.assertIn("classify_part_b_failure_from_artifacts", harness)
self.assertIn("classify_part_b_score_failure", harness)
self.assertIn("failure_bucket", harness)
self.assertIn("failure_detail", harness)
self.assertIn("original_result", harness)
self.assertIn("move_rejected", harness)
self.assertIn("console_error", harness)
self.assertIn("score_failed", harness)

def test_provider_viewer_stays_attached_during_native_restarts(self):
root_view = self.read("macos/WorldOSApp/Sources/WorldOSApp/Views/RootView.swift")
app_process = self.read("macos/WorldOSApp/Sources/WorldOSApp/Services/AppProcessService.swift")
Expand Down
4 changes: 4 additions & 0 deletions qa/test_release_readiness.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ def test_missing_expected_persona_score_marks_partial_and_fails_release(self):

self.assertEqual(rc, 1)
self.assertFalse(payload["release_ready"])
self.assertEqual(payload["release_verdict_gate"], "full_five_persona_rri")
self.assertFalse(payload["gate_split_contract"]["deterministic_built_app_smoke"]["release_verdict"])
self.assertFalse(payload["gate_split_contract"]["short_real_provider_playtest"]["release_verdict"])
self.assertTrue(payload["gate_split_contract"]["full_five_persona_rri"]["release_verdict"])
self.assertTrue(payload["partial"])
self.assertTrue(payload["harness_contaminated"])
self.assertEqual(payload["expected_personas"], ["newbie", "veteran"])
Expand Down
71 changes: 71 additions & 0 deletions qa/test_ui_playtest_app_buckets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import json
import subprocess
import tempfile
import unittest
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]


class UIPlaytestAppBucketFixtureTests(unittest.TestCase):
def run_classifier(self, body: str) -> str:
script = f"""
set -euo pipefail
funcs="$(mktemp)"
trap 'rm -f "$funcs"' EXIT
sed -e 's#^ROOT=.*#ROOT="$(pwd)"; cd "$ROOT" || exit 1#' -e '/^# DRIVE$/q' qa/ui_playtest_app.sh > "$funcs"
grep -qx 'ROOT="$(pwd)"; cd "$ROOT" || exit 1' "$funcs" || {{ echo "missing rewritten ROOT= sentinel" >&2; exit 1; }}
grep -qx '# DRIVE' "$funcs" || {{ echo "missing # DRIVE sentinel" >&2; exit 1; }}
grep -q '^classify_part_b_score_failure()' "$funcs" || {{ echo "missing classifier functions before # DRIVE" >&2; exit 1; }}
source "$funcs" >/dev/null
{body}
"""
Comment thread
coderabbitai[bot] marked this conversation as resolved.
proc = subprocess.run(
["bash", "-c", script],
cwd=ROOT,
text=True,
capture_output=True,
check=False,
)
self.assertEqual(proc.returncode, 0, proc.stderr)
return proc.stdout.strip()

def test_part_b_score_failure_gets_explicit_bucket(self):
with tempfile.TemporaryDirectory() as td:
score = Path(td) / "score.json"
score.write_text(
json.dumps(
{
"pass": False,
"completed_intro_flow": True,
"reached_play_screen": True,
"bug_reports_critical": 0,
"console_errors": 0,
"persona_satisfaction": 5,
}
),
encoding="utf-8",
)

out = self.run_classifier(f'classify_part_b_score_failure "{score}"')

self.assertEqual(out, "score_failed|score.json failed: satisfaction=5/10")

def test_part_b_artifact_classifier_prefers_console_error(self):
with tempfile.TemporaryDirectory() as td:
run = Path(td)
player = run / "player"
player.mkdir()
(player / "console.ndjson").write_text(
json.dumps({"type": "pageerror", "text": "Uncaught ReferenceError"}) + "\n",
encoding="utf-8",
)

out = self.run_classifier(f'classify_part_b_failure_from_artifacts "{run}" FAIL')

self.assertEqual(out, "console_error|browser console/page error recorded during app playtest")


if __name__ == "__main__":
unittest.main()
Loading
Loading