From b782f73f4900565a5e6d45dde692fe9aa14703e1 Mon Sep 17 00:00:00 2001 From: Asger Geel Weirsoee Date: Sun, 1 Mar 2026 06:39:51 +0000 Subject: [PATCH] Add staging gameplay smoke artifact output --- infra/staging/README.md | 2 ++ infra/staging/smoke_suite.sh | 8 +++-- lobby/management/commands/smoke_staging.py | 34 ++++++++++++++++++++++ lobby/tests.py | 31 ++++++++++++++++++++ 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/infra/staging/README.md b/infra/staging/README.md index 3c02b82..d3b7c91 100644 --- a/infra/staging/README.md +++ b/infra/staging/README.md @@ -22,6 +22,8 @@ Forventet: - service er active - healthz returnerer JSON med ok=true +Smoke-suite skriver nu et gameplay-artifact som JSON under `/opt/wpp-staging/app/artifacts/smoke/` (kan overrides via `ARTIFACT_DIR`/`ARTIFACT_FILE`). + Efter deploy validerer scriptet, at `DB_ENGINE` ikke er `django.db.backends.sqlite3` før migrations køres. Deploy-scriptet bruger en release-candidate mappe og promoverer først til `/opt/wpp-staging/app` efter succesfuld `migrate`. Det reducerer schema/code drift ved afbrudte deploys (issue #130) og understøtter release-readiness gate (issue #90). diff --git a/infra/staging/smoke_suite.sh b/infra/staging/smoke_suite.sh index ca47bf5..34bd546 100755 --- a/infra/staging/smoke_suite.sh +++ b/infra/staging/smoke_suite.sh @@ -69,7 +69,11 @@ run_manage() { echo "[smoke] migration consistency check" run_manage "migrate --check --noinput" || { SMOKE_FAIL_MESSAGE="schema drift: unapplied migrations in staging" fail "schema drift: unapplied migrations in staging"; } -echo "[smoke] gameplay flow via management command" -run_manage "smoke_staging" || { SMOKE_FAIL_MESSAGE="manage.py smoke_staging failed" fail "manage.py smoke_staging failed"; } +ARTIFACT_DIR="${ARTIFACT_DIR:-${APP_DIR}/artifacts/smoke}" +ARTIFACT_FILE="${ARTIFACT_FILE:-${ARTIFACT_DIR}/smoke-$(date -u +%Y%m%dT%H%M%SZ).json}" +echo "[smoke] gameplay flow via management command" +run_manage "smoke_staging --artifact ${ARTIFACT_FILE}" || { SMOKE_FAIL_MESSAGE="manage.py smoke_staging failed" fail "manage.py smoke_staging failed"; } + +echo "[smoke] artifact: ${ARTIFACT_FILE}" echo "[smoke] OK" diff --git a/lobby/management/commands/smoke_staging.py b/lobby/management/commands/smoke_staging.py index d2aff7f..f5b1701 100644 --- a/lobby/management/commands/smoke_staging.py +++ b/lobby/management/commands/smoke_staging.py @@ -1,4 +1,6 @@ import json +from datetime import datetime, timezone +from pathlib import Path from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError @@ -10,6 +12,12 @@ from fupogfakta.models import Category, GameSession, Player, Question, RoundQues class Command(BaseCommand): help = "Run minimal staging smoke flow for lobby gameplay" + def add_arguments(self, parser): + parser.add_argument( + "--artifact", + help="Optional path to write smoke result artifact as JSON", + ) + def handle(self, *args, **options): GameSession.objects.all().delete() Player.objects.all().delete() @@ -127,4 +135,30 @@ class Command(BaseCommand): if finish_res.status_code != 200: raise CommandError(f"finish_game failed: {finish_res.status_code}") + artifact_path = options.get("artifact") + if artifact_path: + artifact = { + "ok": True, + "command": "smoke_staging", + "generated_at": datetime.now(timezone.utc).isoformat(), + "session_code": code, + "players": [player["nickname"] for player in players], + "round_question_id": round_question_id, + "steps": [ + "create_session", + "join_players", + "start_round", + "show_question", + "submit_lies", + "mix_answers", + "submit_guesses", + "calculate_scores", + "reveal_scoreboard", + "finish_game", + ], + } + output_path = Path(artifact_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(artifact, indent=2) + "\n", encoding="utf-8") + self.stdout.write(self.style.SUCCESS(f"Smoke flow OK for session {code}")) diff --git a/lobby/tests.py b/lobby/tests.py index 6248811..051298a 100644 --- a/lobby/tests.py +++ b/lobby/tests.py @@ -1,4 +1,7 @@ +import json +import tempfile from datetime import timedelta +from pathlib import Path from django.contrib.auth import get_user_model from django.core.management import call_command @@ -972,3 +975,31 @@ class SmokeStagingCommandTests(TestCase): session = GameSession.objects.latest("created_at") self.assertEqual(session.status, GameSession.Status.FINISHED) self.assertEqual(Player.objects.filter(session=session).count(), 3) + + def test_smoke_staging_writes_artifact_when_requested(self): + with tempfile.TemporaryDirectory() as tmp_dir: + artifact_path = Path(tmp_dir) / "smoke.json" + call_command("smoke_staging", artifact=str(artifact_path)) + + self.assertTrue(artifact_path.exists()) + payload = json.loads(artifact_path.read_text(encoding="utf-8")) + self.assertTrue(payload["ok"]) + self.assertEqual(payload["command"], "smoke_staging") + self.assertEqual(payload["players"], ["P1", "P2", "P3"]) + self.assertIn("generated_at", payload) + self.assertIn("session_code", payload) + self.assertEqual( + payload["steps"], + [ + "create_session", + "join_players", + "start_round", + "show_question", + "submit_lies", + "mix_answers", + "submit_guesses", + "calculate_scores", + "reveal_scoreboard", + "finish_game", + ], + ) -- 2.39.5