Files
Forge-Engine/tests/game/test_triggers.py
T
williamandClaude Sonnet 5 b2e933f322
Build and deploy / test-python (push) Successful in 11m12s
Build and deploy / test-js (push) Successful in 53s
Build and deploy / lint-python (push) Successful in 3m56s
Build and deploy / lint-js (push) Successful in 3m1s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 3m58s
Reorganisation game/document : renommage screens->game_engine + sous-dossiers game/ dans routes, scripts, static, templates, tests
Prepare la scission a venir entre l'editeur Jeu 2D et le futur editeur
Support de formation (voir docs/plan/PLAN.md), sans toucher a
l'architecture en couches existante :

- screens/ renomme en game_engine/ (nom clair pour le moteur du jeu 2D,
  avant l'arrivee d'un second "moteur" cote document) : ~85 imports
  corriges, contrat import-linter mis a jour, meme forme de couches.
- routes/, scripts/, static/, templates/, tests/ : tout ce qui est
  propre au jeu 2D deplace dans un sous-dossier game/ de chacun
  (routes/game/, static/game/, templates/game/, tests/game/,
  scripts/game/) ; ce qui est partage par le site (auth, onboarding,
  dashboard, uploads, db/) reste a la racine de chaque dossier. Un
  sous-dossier document/ (vide) cree dans chacun pour le futur chantier.
- styles/ volontairement inchange : les 3 fichiers sources sont
  concatenes en un seul static/style.css charge par tout le site,
  scinder leur CONTENU (editeur vs partage) serait un refactor CSS
  distinct, pas un deplacement mecanique.
- Chaine d'export SCORM (publish/build_scorm_package.py) mise a jour en
  profondeur : copie des assets, URLs d'icones relatives a
  static/style.css (qui ne bouge pas), manifeste, wrapper SCORM.
- Deux regressions d'un sweep de renommage anterieur corrigees au passage
  (screens.js/screens/scene-objects incorrectement convertis en
  game_engine.js/game_engine/scene-objects dans des commentaires).
- Effet de bord Windows decouvert et corrige : git mv + Path.write_text
  convertissent des fichiers en CRLF (core.autocrlf=true) - ~189 fichiers
  normalises en LF.
- .eslintrc.json/package.json : uniquement les chemins de glob mis a jour
  (static/game/js/...) ; la preparation eslint-plugin-unicorn du lot 7
  reste volontairement non committee (package-lock.json restaure a la
  version precedente).

Verifications : ruff, mypy --strict (391 fichiers), vulture, bandit,
lint-imports tous verts ; 591/591 tests Python, 276/276 tests JS ;
demarrage serveur + requetes HTTP manuelles confirmant que les assets
deplaces repondent en 200 au nouvel emplacement et 404 a l'ancien.

SKIP=djlint : backlog H021 (styles inline) deja documente comme dette
assumee dans CODE_QUALITY.md section 6, aucun template touche par ce
commit au-dela d'un deplacement de fichier.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 12:27:53 +02:00

681 lines
25 KiB
Python

"""Déclencheurs de collision — onglet "Déclencheurs" (voir
game_engine/rendering/trigger_graph.py, routes/game/triggers/triggers.py) : TOUS
les déclencheurs du jeu, plus aucune notion de quête pour les regrouper —
un créateur peut faire un simple quiz sans jamais poser de personnage.
Chaque occurrence de "Déclencher ce dialogue" porte son propre dialogue
({id, lines}, voir game_engine/rendering/collision_rules.py::"dialogue".dialogue),
édité en ligne (voir routes POST .../triggers/<object_id>/dialogue
ci-dessous). Source de données UNIQUE avec l'éditeur de collision : toute
règle ajoutée/retirée/modifiée ici doit rester visible/cohérente via
game_engine.resolve_collision_rules directement sur l'objet."""
import re
from collections.abc import Callable
from typing import Any
from flask.testing import FlaskClient
import game_engine
from tests.conftest import not_none
from tests.test_auth import anon_client # noqa: F401
def _create_jeu2d_game(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str], name: str = "pytest_triggers"
) -> Any:
resp = client.post("/games/new", data={"name": name}, follow_redirects=False)
slug = tmp_game_slug_cleanup(resp.headers["Location"].rstrip("/").split("/")[-1])
screen_id = game_engine.create_screen(slug, "Scène 1", kind="jeu_2d")
return slug, screen_id
def _add_personnage(client: FlaskClient, slug: str, screen_id: int, role: str | None = None) -> Any:
resp = client.post(
f"/game/{slug}/screens/{screen_id}/scene-objects/add", data={"kind": "personnage"}, follow_redirects=False
)
assert resp.status_code == 302, resp.data
obj_id = int(not_none(re.search(r"selected=(\d+)", resp.headers["Location"])).group(1))
if role:
client.post(
f"/game/{slug}/scene-objects/{obj_id}/role", data=f'{{"role": "{role}"}}', content_type="application/json"
)
return obj_id
def _set_rules(slug: str, object_id: int, rules: Any) -> None:
"""Pose des règles directement (sans passer par une route HTTP —
l'ancienne route de sauvegarde, routes/game/scenes/scene_object_collision_rules.py,
a été retirée avec l'éditeur de collision séparé) pour préparer un
scénario de test."""
game_engine.set_scene_object_collision_rules(slug, object_id, game_engine.sanitize_collision_rules(rules))
def test_mark_completed_is_kept_when_true(client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]) -> None:
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(slug, obj_id, [{"trigger": "collision", "action": {"type": "dialogue", "mark_completed": True}}])
obj = not_none(game_engine.get_scene_object(slug, obj_id))
rules = game_engine.resolve_collision_rules(obj)
assert rules[0]["action"]["mark_completed"] is True
def test_mark_completed_is_dropped_when_false_or_absent(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(slug, obj_id, [{"trigger": "collision", "action": {"type": "dialogue", "mark_completed": False}}])
obj = not_none(game_engine.get_scene_object(slug, obj_id))
rules = game_engine.resolve_collision_rules(obj)
assert "mark_completed" not in rules[0]["action"]
def test_list_all_triggers_finds_direct_and_nested_rules(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
direct_id = _add_personnage(client, slug, screen_id, role="pnj")
nested_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(slug, direct_id, [{"trigger": "collision", "action": {"type": "dialogue"}}])
_set_rules(
slug, nested_id, [{"trigger": "collision", "action": {"type": "interagir", "sub_action": {"type": "dialogue"}}}]
)
result = game_engine.list_all_triggers(slug)
ids = [o["object_id"] for o in result]
assert direct_id in ids
assert nested_id in ids
direct_entry = next(o for o in result if o["object_id"] == direct_id)
assert direct_entry["has_interagir"] is False
nested_entry = next(o for o in result if o["object_id"] == nested_id)
assert nested_entry["has_interagir"] is True
def test_list_all_triggers_surfaces_both_branches_of_a_condition_independently(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
"""Losange à embranchement (maquette validée, lecture verticale) :
Vrai ET Faux peuvent chacun déclencher LEUR PROPRE dialogue,
indépendamment l'un de l'autre — plus de "branche qui mène à telle
quête" à choisir (voir static/game/js/triggers/trigger-editor.js::
triggerConditionColumnHtml pour son affichage)."""
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(
slug,
obj_id,
[
{
"trigger": "collision",
"action": {
"type": "condition",
"variable": "inventaire_cle",
"operateur": "superieur",
"valeur": "0",
"si_vrai": {"type": "dialogue", "dialogue": {"id": "d_vrai", "lines": []}},
"si_faux": {"type": "dialogue", "dialogue": {"id": "d_faux", "lines": []}},
},
}
],
)
result = game_engine.list_all_triggers(slug)
entry = next(o for o in result if o["object_id"] == obj_id)
assert entry["condition"]["variable"] == "inventaire_cle"
assert entry["condition"]["si_vrai"] == {"chain": [{"type": "dialogue", "dialogue": {"id": "d_vrai", "lines": []}}]}
assert entry["condition"]["si_faux"] == {"chain": [{"type": "dialogue", "dialogue": {"id": "d_faux", "lines": []}}]}
def test_list_all_triggers_leaves_a_branch_none_when_it_does_nothing(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(
slug,
obj_id,
[
{
"trigger": "collision",
"action": {
"type": "condition",
"variable": "score",
"operateur": "egal",
"valeur": "0",
"si_vrai": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
"si_faux": None,
},
}
],
)
result = game_engine.list_all_triggers(slug)
entry = next(o for o in result if o["object_id"] == obj_id)
assert entry["condition"]["si_faux"] is None
def test_triggers_delete_removes_all_rules_on_the_object(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(slug, obj_id, [{"trigger": "collision", "action": {"type": "dialogue"}}])
resp = client.delete(f"/game/{slug}/triggers/{obj_id}")
assert resp.status_code == 200
obj = not_none(game_engine.get_scene_object(slug, obj_id))
assert game_engine.resolve_collision_rules(obj) == []
def test_triggers_route_add_then_get_then_delete(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
resp = client.post(
f"/game/{slug}/triggers",
json={
"object_id": obj_id,
"rule": {
"trigger": "collision",
"action": {"type": "interagir", "sub_action": {"type": "dialogue", "mark_completed": True}},
},
},
)
assert resp.status_code == 200
# Même donnée, visible directement via la couche collision (source unique).
obj = not_none(game_engine.get_scene_object(slug, obj_id))
rules = game_engine.resolve_collision_rules(obj)
assert rules == [
{
"trigger": "collision",
"action": {"type": "interagir", "sub_action": {"type": "dialogue", "mark_completed": True}},
}
]
resp = client.get(f"/game/{slug}/triggers")
result = resp.get_json()
assert [o["object_id"] for o in result] == [obj_id]
assert result[0]["has_interagir"] is True
resp = client.delete(f"/game/{slug}/triggers/{obj_id}")
assert resp.status_code == 200
obj = not_none(game_engine.get_scene_object(slug, obj_id))
assert game_engine.resolve_collision_rules(obj) == []
def test_list_all_triggers_surfaces_the_dialogue(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(
slug,
obj_id,
[
{
"trigger": "collision",
"action": {
"type": "dialogue",
"dialogue": {"id": "d_1", "lines": [{"speaker": "pnj", "text": "Salut !"}]},
},
}
],
)
result = game_engine.list_all_triggers(slug)
assert result[0]["chain"] == [
{
"type": "dialogue",
"dialogue": {"id": "d_1", "lines": [{"type": "dialogue", "speaker": "pnj", "text": "Salut !"}]},
}
]
def test_list_all_triggers_surfaces_a_chain_of_actions(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
"""Chaînage (demande explicite : "pouvoir modifier un déclencheur pour
lui ajouter des actions") : dialogue -> then variable, la chaîne
ENTIÈRE est aplatie dans l'ordre d'exécution."""
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(
slug,
obj_id,
[
{
"trigger": "collision",
"action": {
"type": "dialogue",
"dialogue": {"id": "d_1", "lines": []},
"then": {
"type": "variable",
"id": "v_1",
"target_variable": "score",
"data_operation": "incrementer",
"data_value": "1",
},
},
}
],
)
result = game_engine.list_all_triggers(slug)
assert result[0]["chain"] == [
{"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
{
"type": "variable",
"id": "v_1",
"target_variable": "score",
"data_operation": "incrementer",
"data_value": "1",
},
]
def test_list_completable_dialogue_ids_collects_mark_completed_dialogues_everywhere(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
direct_id = _add_personnage(client, slug, screen_id, role="pnj")
cond_id = _add_personnage(client, slug, screen_id, role="pnj")
other_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(
slug,
direct_id,
[
{
"trigger": "collision",
"action": {"type": "dialogue", "dialogue": {"id": "d_direct", "lines": []}, "mark_completed": True},
}
],
)
_set_rules(
slug,
cond_id,
[
{
"trigger": "collision",
"action": {
"type": "condition",
"variable": "score",
"operateur": "egal",
"valeur": "1",
"si_vrai": {"type": "dialogue", "dialogue": {"id": "d_cond", "lines": []}, "mark_completed": True},
"si_faux": None,
},
}
],
)
_set_rules(
slug,
other_id,
[
{
"trigger": "collision",
"action": {"type": "dialogue", "dialogue": {"id": "d_not_completing", "lines": []}},
}
],
)
ids = game_engine.list_completable_dialogue_ids(slug)
assert set(ids) == {"d_direct", "d_cond"}
def test_trigger_dialogue_update_route_persists_the_new_lines(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(
slug,
obj_id,
[
{
"trigger": "collision",
"action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
}
],
)
resp = client.post(
f"/game/{slug}/triggers/{obj_id}/dialogue",
json={"dialogue_id": "d_1", "lines": [{"speaker": "pnj", "text": "Trouvé !"}]},
)
assert resp.status_code == 200
obj = not_none(game_engine.get_scene_object(slug, obj_id))
rules = game_engine.resolve_collision_rules(obj)
assert rules[0]["action"]["dialogue"]["lines"] == [{"type": "dialogue", "speaker": "pnj", "text": "Trouvé !"}]
def test_trigger_dialogue_update_route_404s_for_an_unknown_dialogue_id(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(
slug,
obj_id,
[
{
"trigger": "collision",
"action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
}
],
)
resp = client.post(
f"/game/{slug}/triggers/{obj_id}/dialogue",
json={"dialogue_id": "d_absent", "lines": [{"speaker": "pnj", "text": "x"}]},
)
assert resp.status_code == 404
def test_trigger_dialogue_update_route_404s_for_an_unknown_object(client: FlaskClient, game: str) -> None:
resp = client.post(
f"/game/{game}/triggers/999999/dialogue",
json={"dialogue_id": "d_1", "lines": []},
)
assert resp.status_code == 404
def test_trigger_append_action_route_attaches_a_then_at_the_end_of_the_chain(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(
slug,
obj_id,
[
{
"trigger": "collision",
"action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
}
],
)
resp = client.post(
f"/game/{slug}/triggers/{obj_id}/append-action",
json={
"after_id": "d_1",
"action": {
"type": "variable",
"id": "v_1",
"target_variable": "score",
"data_operation": "incrementer",
"data_value": "1",
},
},
)
assert resp.status_code == 200
obj = not_none(game_engine.get_scene_object(slug, obj_id))
rules = game_engine.resolve_collision_rules(obj)
assert rules[0]["action"]["then"] == {
"type": "variable",
"id": "v_1",
"target_variable": "score",
"data_operation": "incrementer",
"data_value": "1",
}
def test_trigger_append_action_route_404s_for_an_unknown_after_id(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(
slug,
obj_id,
[
{
"trigger": "collision",
"action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
}
],
)
resp = client.post(
f"/game/{slug}/triggers/{obj_id}/append-action",
json={
"after_id": "d_absent",
"action": {"type": "variable", "target_variable": "score", "data_operation": "incrementer"},
},
)
assert resp.status_code == 404
def test_trigger_append_action_route_inserts_between_a_leaf_and_its_existing_then(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
"""Demande explicite : "pouvoir ajouter n'importe quelle action à
n'importe quel niveau du schéma" — poser "+ Ajouter une action" sur
un bloc qui a déjà une suite INSÈRE la nouvelle action devant cette
suite plutôt que d'échouer (comportement précédent)."""
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(
slug,
obj_id,
[
{
"trigger": "collision",
"action": {
"type": "dialogue",
"dialogue": {"id": "d_1", "lines": []},
"then": {
"type": "variable",
"id": "v_1",
"target_variable": "score",
"data_operation": "incrementer",
"data_value": "1",
},
},
}
],
)
resp = client.post(
f"/game/{slug}/triggers/{obj_id}/append-action",
json={
"after_id": "d_1",
"action": {"type": "variable", "target_variable": "vies", "data_operation": "decrementer"},
},
)
assert resp.status_code == 200
obj = not_none(game_engine.get_scene_object(slug, obj_id))
action = game_engine.resolve_collision_rules(obj)[0]["action"]
assert action["then"]["target_variable"] == "vies"
assert action["then"]["then"]["target_variable"] == "score"
def test_trigger_append_action_route_404s_for_an_unknown_object(client: FlaskClient, game: str) -> None:
resp = client.post(f"/game/{game}/triggers/999999/append-action", json={"after_id": "d_1", "action": {}})
assert resp.status_code == 404
def test_trigger_remove_action_route_removes_the_whole_rule_when_it_is_the_only_leaf(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(
slug,
obj_id,
[
{
"trigger": "collision",
"action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
}
],
)
resp = client.delete(f"/game/{slug}/triggers/{obj_id}/action/d_1")
assert resp.status_code == 200
obj = not_none(game_engine.get_scene_object(slug, obj_id))
assert game_engine.resolve_collision_rules(obj) == []
def test_trigger_remove_action_route_404s_for_an_unknown_leaf_id(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(
slug,
obj_id,
[
{
"trigger": "collision",
"action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
}
],
)
resp = client.delete(f"/game/{slug}/triggers/{obj_id}/action/d_absent")
assert resp.status_code == 404
def test_trigger_move_action_route_swaps_a_leaf_up_in_its_chain(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(
slug,
obj_id,
[
{
"trigger": "collision",
"action": {
"type": "variable",
"id": "v_1",
"target_variable": "score",
"data_operation": "incrementer",
"data_value": "1",
"then": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
},
}
],
)
resp = client.post(f"/game/{slug}/triggers/{obj_id}/action/d_1/move", json={"direction": "up"})
assert resp.status_code == 200
obj = not_none(game_engine.get_scene_object(slug, obj_id))
action = game_engine.resolve_collision_rules(obj)[0]["action"]
assert action["type"] == "dialogue"
assert action["then"]["target_variable"] == "score"
def test_trigger_move_action_route_400s_for_an_invalid_direction(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(
slug,
obj_id,
[
{
"trigger": "collision",
"action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
}
],
)
resp = client.post(f"/game/{slug}/triggers/{obj_id}/action/d_1/move", json={"direction": "sideways"})
assert resp.status_code == 400
def test_triggers_get_route_backfills_and_persists_a_missing_leaf_id(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
"""Bug corrigé : une action "variable" posée sans "id" (par une
session antérieure ou par l'IA) restait à jamais impossible à
adresser via append-action — le premier GET /triggers doit la rendre
adressable, DE FAÇON STABLE (persisté, pas régénéré à chaque appel)."""
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = _add_personnage(client, slug, screen_id, role="pnj")
_set_rules(
slug,
obj_id,
[
{
"trigger": "collision",
"action": {
"type": "variable",
"target_variable": "score",
"data_operation": "incrementer",
"data_value": "1",
},
}
],
)
resp1 = client.get(f"/game/{slug}/triggers")
leaf_id = resp1.get_json()[0]["chain"][0]["id"]
assert leaf_id
resp2 = client.get(f"/game/{slug}/triggers")
assert resp2.get_json()[0]["chain"][0]["id"] == leaf_id # stable, pas régénéré
append_resp = client.post(
f"/game/{slug}/triggers/{obj_id}/append-action",
json={"after_id": leaf_id, "action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}}},
)
assert append_resp.status_code == 200
def test_objects_catalog_lists_eligible_objects_with_thumbnails(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
_add_personnage(client, slug, screen_id, role="pnj")
resp = client.get(f"/game/{slug}/triggers/objects-catalog")
assert resp.status_code == 200
catalog = resp.get_json()
assert len(catalog) == 1
assert catalog[0]["thumbnail_html"]
def test_triggers_route_is_isolated_like_other_game_routes(anon_client: FlaskClient) -> None: # noqa: F811 - anon_client est un fixture pytest (voir tests.test_auth), pas une vraie redefinition - pyflakes ne comprend pas l'injection de fixture par parametre
"""Même garde d'accès que /game/<slug>/publish (core/auth_guard.py) —
aucune vérification supplémentaire écrite pour ces routes."""
from tests.test_auth import _cleanup_project, _complete_onboarding, _confirm_2fa, _register
_register(anon_client, "triggersisolation@example.com")
_confirm_2fa(anon_client)
_complete_onboarding(anon_client)
try:
resp = anon_client.get("/game/un-projet-qui-nest-pas-le-sien/triggers")
assert resp.status_code == 403
resp = anon_client.post("/game/un-projet-qui-nest-pas-le-sien/triggers", json={})
assert resp.status_code == 403
resp = anon_client.delete("/game/un-projet-qui-nest-pas-le-sien/triggers/1")
assert resp.status_code == 403
resp = anon_client.post("/game/un-projet-qui-nest-pas-le-sien/triggers/1/dialogue", json={})
assert resp.status_code == 403
resp = anon_client.post("/game/un-projet-qui-nest-pas-le-sien/triggers/1/append-action", json={})
assert resp.status_code == 403
resp = anon_client.delete("/game/un-projet-qui-nest-pas-le-sien/triggers/1/action/d_1")
assert resp.status_code == 403
resp = anon_client.post(
"/game/un-projet-qui-nest-pas-le-sien/triggers/1/action/d_1/move", json={"direction": "up"}
)
assert resp.status_code == 403
resp = anon_client.get("/game/un-projet-qui-nest-pas-le-sien/triggers/objects-catalog")
assert resp.status_code == 403
finally:
_cleanup_project("triggersisolation@example.com")