Étape 0d.1 : domaine « partie » en TypeScript, scénario exécuté, garde-fous
Fusion du travail du sous-agent (branche plateau-domaine-travail) ; plan supprimé à la clôture, son résultat est repris dans la merge request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,19 @@ if [ "${#scripts[@]}" -gt 0 ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Application web (ADR 0009), dans Docker : Biome (style et erreurs
|
||||
# courantes) et TypeScript (types). Le domaine est vérifié une seconde fois
|
||||
# seul, sans les types de Node ni du navigateur : il doit rester pur.
|
||||
if ! docker image inspect "${WEB_IMAGE:?lancer via make lint}" >/dev/null 2>&1; then
|
||||
signaler "Outils web absents : lancer d'abord make setup"
|
||||
else
|
||||
web=(docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/src:ro" -w /src/app "$WEB_IMAGE")
|
||||
"${web[@]}" biome check . || signaler "Biome a trouvé des problèmes (voir ci-dessus)"
|
||||
"${web[@]}" tsc -p tsconfig.json --noEmit || signaler "TypeScript a trouvé des erreurs (voir ci-dessus)"
|
||||
"${web[@]}" tsc -p src/domain/tsconfig.json \
|
||||
|| signaler "le domaine utilise Node ou le navigateur (voir ci-dessus)"
|
||||
fi
|
||||
|
||||
if [ "$erreurs" -gt 0 ]; then
|
||||
echo "Lint : $erreurs problème(s)."
|
||||
exit 1
|
||||
|
||||
@@ -138,6 +138,87 @@ attendre_erreur usage bestmove "$depart" "$libre_j0"
|
||||
attendre_erreur usage inconnue "$depart" "$libre_j0"
|
||||
attendre_erreur invalid_data --data /dossier/absent bestmove "$depart" "$libre_j0" 31
|
||||
|
||||
# 6. Ce dont a besoin le domaine d'une partie (étape 0d.1).
|
||||
|
||||
# Les coups légaux donnent leurs demi-coups, dans un ordre jouable.
|
||||
grep -qF '"move": "8/5 6/5", "position_id": "4HPwATCwZ/ABMA", "half_moves": [[8, 5], [6, 5]]' \
|
||||
<<<"$($cli legal "$depart" "$libre_j0" 31)" || echouer "legal : demi-coups de 8/5 6/5 absents"
|
||||
|
||||
# Match ID : codage, et décodage identique.
|
||||
[ "$(valeur match_id "$($cli matchid 1 -1)")" = "$libre_j0" ] || echouer "matchid 1 -1 : $libre_j0 attendu"
|
||||
videau4="$(valeur match_id "$($cli matchid 4 1)")"
|
||||
sortie="$($cli eval "$depart" "$videau4")"
|
||||
[ "$(valeur cube_value "$sortie") $(valeur cube_owner "$sortie")" = "4 1" ] \
|
||||
|| echouer "matchid 4 1 : videau mal codé ($sortie)"
|
||||
attendre_erreur invalid_match_id matchid 3 0
|
||||
attendre_erreur invalid_match_id matchid 2 5
|
||||
|
||||
# Plateau : position de départ, symétrique, sans résultat.
|
||||
sortie="$($cli position "$depart" "$libre_j0")"
|
||||
grep -qF '"on_roll": {"points": [0, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], "bar": 0, "off": 0, "pip_count": 167}' <<<"$sortie" \
|
||||
|| echouer "position de départ mal décodée : $sortie"
|
||||
[ "$(valeur swapped_position_id "$sortie")" = "$depart" ] || echouer "départ : la position vue de l'adversaire diffère"
|
||||
grep -qF '"result": null' <<<"$sortie" || echouer "départ : la partie ne devrait pas être finie"
|
||||
attendre_erreur invalid_position_id position 'ceci-nest-pas-un-id' "$libre_j0"
|
||||
|
||||
# Fin de partie : le joueur au trait a tout sorti, l'adversaire aucun pion
|
||||
# (gammon). Règle Jacoby : 1 point tant que le videau est au centre ; sinon
|
||||
# 2 fois le videau.
|
||||
fini=/38AAAAAAAAAAA
|
||||
grep -qF '"result": {"winner": "on_roll", "kind": "gammon", "points": 1}' \
|
||||
<<<"$($cli position "$fini" "$libre_j0")" || echouer "gammon avec Jacoby, videau au centre : 1 point attendu"
|
||||
grep -qF '"result": {"winner": "on_roll", "kind": "gammon", "points": 8}' \
|
||||
<<<"$($cli position "$fini" "$videau4")" || echouer "gammon, videau à 4 : 8 points attendus"
|
||||
vu_du_perdant="$(valeur swapped_position_id "$($cli position "$fini" "$libre_j0")")"
|
||||
grep -qF '"winner": "opponent"' <<<"$($cli position "$vu_du_perdant" "$libre_j0")" \
|
||||
|| echouer "fin de partie vue du perdant : l'adversaire devrait gagner"
|
||||
|
||||
# Demi-coups : « valid complete position_id » pour une saisie.
|
||||
demi_coups() {
|
||||
local sortie valid complete
|
||||
sortie="$($cli halfmoves "$@")"
|
||||
valid="$(grep -oE '"valid": (true|false)' <<<"$sortie" | cut -d' ' -f2)"
|
||||
complete="$(grep -oE '"complete": (true|false)' <<<"$sortie" | cut -d' ' -f2)"
|
||||
echo "$valid $complete $(valeur result_position_id "$sortie")"
|
||||
}
|
||||
attendre_demi_coups() {
|
||||
local attendu="$1"
|
||||
shift
|
||||
local obtenu
|
||||
obtenu="$(demi_coups "$@")"
|
||||
[ "$obtenu" = "$attendu" ] || echouer "halfmoves $* : « $obtenu » au lieu de « $attendu »"
|
||||
}
|
||||
attendre_demi_coups "true false 4HPwATDQZ/ABMA" "$depart" 31 "8/5"
|
||||
attendre_demi_coups "true true 4HPwATCwZ/ABMA" "$depart" 31 "8/5 6/5"
|
||||
attendre_demi_coups "true true 4HPwATCwZ/ABMA" "$depart" 31 "6/5 8/5"
|
||||
attendre_demi_coups "true false $depart" "$depart" 31
|
||||
# Case 12 tenue par l'adversaire ; 8/4 demande deux dés.
|
||||
attendre_demi_coups "false false " "$depart" 31 "13/12"
|
||||
attendre_demi_coups "false false " "$depart" 31 "8/4"
|
||||
# Le même pion avec les deux dés.
|
||||
attendre_demi_coups "true true 4HPwATDgc/ABIQ" "$depart" 31 "24/21 21/20"
|
||||
# Sortie : le pion le plus éloigné d'abord avec un dé trop fort.
|
||||
attendre_demi_coups "true true /38AAAAAAAAAAA" "$course" 66 "6/off 5/off"
|
||||
attendre_demi_coups "false false " "$course" 66 "5/off"
|
||||
attendre_demi_coups "true false /38AAAAgAAAAAA" "$course" 65 "5/off"
|
||||
# Course, jet 21 : il faut jouer les deux dés.
|
||||
attendre_demi_coups "false false " "$course" 21 "6/3"
|
||||
attendre_demi_coups "true true /38AAAAkAAAAAA" "$course" 21 "6/5 5/3"
|
||||
# Aucun coup possible : rien n'est valide.
|
||||
attendre_demi_coups "false false " 27YBBwAcz8EDQA 66
|
||||
attendre_erreur usage halfmoves "$depart" 31 "8-5"
|
||||
attendre_erreur invalid_dice halfmoves "$depart" 71 "8/5"
|
||||
|
||||
# Videau : droit de doubler et recommandations.
|
||||
sortie="$($cli cube "$depart" "$libre_j0")"
|
||||
grep -qF '"available": true, "double": false, "take": true' <<<"$sortie" \
|
||||
|| echouer "videau au départ : disponible, pas de double, prendre attendus"
|
||||
grep -qF '"available": true, "double": true, "take": false' <<<"$($cli cube "$course" "$libre_j0")" \
|
||||
|| echouer "videau en course gagnée : double et passe attendus"
|
||||
# Videau chez l'adversaire : le joueur au trait ne peut pas doubler.
|
||||
grep -qF '"available": false, "double": false' <<<"$($cli cube "$course" "$(valeur match_id "$($cli matchid 2 1)")")" \
|
||||
|| echouer "videau chez l'adversaire : double indisponible attendu"
|
||||
|
||||
if [ "$echecs" -gt 0 ]; then
|
||||
echo " $echecs vérification(s) du moteur en échec."
|
||||
exit 1
|
||||
|
||||
@@ -33,4 +33,44 @@ done
|
||||
duplicates="$(find . -name '*.md' ! -name README.md -exec sed -n 's/^english: //p' {} + | sort | uniq -d)"
|
||||
[ -z "$duplicates" ] || report "traductions anglaises en double : $duplicates"
|
||||
|
||||
# Lien glossaire <-> code (ADR 0008), dans les deux sens. Dans le code, un
|
||||
# commentaire « Glossaire : docs/glossaire/<fiche>.md » désigne le premier
|
||||
# type exporté qui le suit ; la fiche doit exister et nommer ce type dans son
|
||||
# champ code. Réciproquement, chaque champ code d'une fiche doit désigner un
|
||||
# type ainsi relié à elle.
|
||||
pairs="$(find ../../app/src -name '*.ts' -exec awk '
|
||||
function flush() { if (ref != "") print ref, "?"; ref = "" }
|
||||
FNR == 1 { flush() }
|
||||
match($0, /Glossaire : docs\/glossaire\/[a-z0-9-]+\.md/) {
|
||||
flush()
|
||||
ref = substr($0, RSTART + 27, RLENGTH - 27)
|
||||
next
|
||||
}
|
||||
ref != "" && match($0, /^export (interface|type|class|enum) [A-Za-z0-9_]+/) {
|
||||
n = split(substr($0, RSTART, RLENGTH), words, " ")
|
||||
print ref, words[n]
|
||||
ref = ""
|
||||
}
|
||||
END { flush() }
|
||||
' {} +)"
|
||||
|
||||
while read -r fiche type; do
|
||||
[ -n "$fiche" ] || continue
|
||||
if [ "$type" = "?" ]; then
|
||||
report "code : référence à $fiche sans type exporté juste après"
|
||||
elif [ ! -f "$fiche" ]; then
|
||||
report "code : le type $type renvoie à une fiche inexistante ($fiche)"
|
||||
elif [ "$(sed -n 's/^code: //p' "$fiche")" != "$type" ]; then
|
||||
report "$fiche : le type $type y renvoie, mais la fiche n'a pas « code: $type »"
|
||||
fi
|
||||
done <<<"$pairs"
|
||||
|
||||
for f in *.md; do
|
||||
[ "$f" = README.md ] && continue
|
||||
code="$(sed -n 's/^code: //p' "$f")"
|
||||
[ -n "$code" ] || continue
|
||||
grep -qx "$f $code" <<<"$pairs" \
|
||||
|| report "$f : « code: $code » ne désigne aucun type du code qui renvoie à cette fiche"
|
||||
done
|
||||
|
||||
[ "$errors" -eq 0 ]
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# Teste l'application web (app/, ADR 0009), dans l'image ustagammon/web :
|
||||
# - tests unitaires du domaine (faux moteur) et partie complète sur le vrai
|
||||
# moteur WebAssembly (node:test) ;
|
||||
# - scénarios de docs/scenarios/ exécutés par Cucumber sur le domaine branché
|
||||
# au vrai moteur (ADR 0008) ;
|
||||
# - règles d'architecture (dependency-cruiser, ADR 0007), et preuve qu'une
|
||||
# violation volontaire de chaque règle est bien détectée.
|
||||
#
|
||||
# Lance `make engine-wasm` et `make web` (sans effet si tout est à jour).
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
image="${WEB_IMAGE:?lancer via make test}"
|
||||
|
||||
# Lancé depuis `make test` : repartir d'un make indépendant du make parent.
|
||||
unset MAKEFLAGS MFLAGS MAKELEVEL
|
||||
make engine-wasm >/dev/null
|
||||
make web >/dev/null
|
||||
|
||||
errors=0
|
||||
report() {
|
||||
echo " $1"
|
||||
errors=$((errors + 1))
|
||||
}
|
||||
|
||||
# Lance une commande des outils web, dépôt monté sous /src ; le premier
|
||||
# argument est le dossier de travail, relatif au dépôt.
|
||||
web() {
|
||||
local dir="$1"
|
||||
shift
|
||||
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/src" -w "/src/$dir" "$image" "$@"
|
||||
}
|
||||
|
||||
web . node --test --test-reporter=dot 'build/web/tests/**/*.test.js' \
|
||||
|| report "tests unitaires ou partie complète en échec (voir ci-dessus)"
|
||||
|
||||
web . cucumber-js --format summary --import 'build/web/tests/steps/*.js' docs/scenarios/*.feature \
|
||||
|| report "scénarios en échec (voir ci-dessus)"
|
||||
|
||||
web app depcruise --config .dependency-cruiser.cjs src tests \
|
||||
|| report "règles d'architecture non respectées (voir ci-dessus)"
|
||||
|
||||
# Violations volontaires, dans un dossier jetable sous build/ : chaque règle
|
||||
# doit être signalée par dependency-cruiser.
|
||||
fixture=build/web-violations
|
||||
rm -rf "$fixture"
|
||||
mkdir -p "$fixture/src/domain" "$fixture/src/engine" "$fixture/src/ui"
|
||||
cp app/tsconfig.json "$fixture/"
|
||||
echo 'import { engine } from "../engine/adapter.js"; export const a = engine;' >"$fixture/src/domain/uses-engine.ts"
|
||||
echo 'import { readFileSync } from "node:fs"; export const b = readFileSync;' >"$fixture/src/domain/uses-node.ts"
|
||||
echo 'export const engine = 1;' >"$fixture/src/engine/adapter.ts"
|
||||
echo 'import { engine } from "../engine/adapter.js"; export const c = engine;' >"$fixture/src/ui/uses-engine.ts"
|
||||
echo 'import { e } from "./loop-b.js"; export const d = e;' >"$fixture/src/ui/loop-a.ts"
|
||||
echo 'import { d } from "./loop-a.js"; export const e = d;' >"$fixture/src/ui/loop-b.ts"
|
||||
output="$(web "$fixture" depcruise --config /src/app/.dependency-cruiser.cjs src 2>&1)" \
|
||||
&& report "dependency-cruiser accepte des violations volontaires"
|
||||
for rule in domain-is-pure ui-does-not-call-engine no-circular; do
|
||||
grep -q "$rule" <<<"$output" || report "violation volontaire de $rule non détectée"
|
||||
done
|
||||
grep -q 'uses-node' <<<"$output" || report "import de Node dans le domaine non détecté"
|
||||
rm -rf "$fixture"
|
||||
|
||||
[ "$errors" -eq 0 ]
|
||||
Reference in New Issue
Block a user