@mostajs/mjs-unit 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -100,6 +100,54 @@ Le fichier suit le schéma lu par Jenkins, GitLab (`artifacts:reports:junit`), G
100
100
  tombée, `<error>` pour une exception inattendue ou un délai dépassé, `<skipped message>` pour un cas ignoré,
101
101
  les étiquettes en `<property name="tag">`. `--tag=securite` ne joue que les cas étiquetés.
102
102
 
103
+ ## Compte rendu de validation — cycle CRA (0.9.0)
104
+
105
+ ```bash
106
+ npx mjs-sbom # la SBOM de la version publiée (@mostajs/mjs-sbom)
107
+ npx mjs-unit test-scripts/**/*.test.mjs \
108
+ --sbom=sbom.cyclonedx.json --plan=docs/DEVTEST-PLAN.<projet>.json \
109
+ --compte-rendu --ctrf --intoto
110
+ # 📄 Compte rendu : test-scripts/.out/compte-rendu.json — @acme/produit@1.2.0 — verdict : conforme
111
+ npx mjs-unit-menu # ou le menu : essais, SBOM, compte rendu, envoi à qatrax.dev
112
+ ```
113
+
114
+ Le compte rendu lie les résultats à la version (lue dans la SBOM), au plan et aux scripts (empreintes) ; son verdict
115
+ est `conforme`, `non conforme` ou `incomplet` (avec les défauts) ; il s'écrit toujours. qatrax.dev l'enregistre, le
116
+ relie à l'analyse des vulnérabilités de la même SBOM, et scelle le dossier.
117
+
118
+ ## Java, C#, Python, Go, C++… — le mode lecteur (0.10.0)
119
+
120
+ Vos essais restent joués par l'outil de votre langage ; mjs-unit **lit** leur rapport et produit le même compte
121
+ rendu scellé (lié à la SBOM et au plan), avec CTRF et in-toto. Node ≥ 18 suffit dans la CI ; aucun JavaScript à écrire.
122
+
123
+ **Formats lus tels quels, reconnus à leur contenu** : JUnit XML · NUnit 2 et 3 · TRX (VSTest) · TestNG natif ·
124
+ Robot Framework (`output.xml`) · CTest (`Test.xml`) · Cucumber JSON · Behave JSON · Allure 2 (`allure-results`) ·
125
+ CTRF · TAP · JSON mjs-unit. Votre rapport natif suffit : aucune conversion.
126
+
127
+ | Langage | Rapport | Commande |
128
+ |---|---|---|
129
+ | Java (Maven) | `target/surefire-reports/TEST-*.xml` | `npx @mostajs/mjs-unit --depuis=target/surefire-reports --sbom=target/bom.json --plan=plan.json --compte-rendu` |
130
+ | Java (Gradle) | `build/test-results/test/TEST-*.xml` | `npx @mostajs/mjs-unit --depuis=build/test-results/test …` |
131
+ | C# (.NET) | `dotnet test --logger "junit;LogFilePath=resultats.xml"` | `npx @mostajs/mjs-unit --depuis=resultats.xml …` |
132
+ | Python | `pytest --junitxml=resultats.xml` | `npx @mostajs/mjs-unit --depuis=resultats.xml …` |
133
+ | Go | `go test -v ./... 2>&1 \| go-junit-report > resultats.xml` | `npx @mostajs/mjs-unit --depuis=resultats.xml …` |
134
+ | C# (NUnit) | `TestResult.xml` (nunit3-console) | `npx @mostajs/mjs-unit --depuis=TestResult.xml …` |
135
+ | C# (TRX) | `dotnet test --logger trx` → `TestResults/*.trx` | `npx @mostajs/mjs-unit --depuis=TestResults …` |
136
+ | Java (TestNG) | `target/surefire-reports/testng-results.xml` | `npx @mostajs/mjs-unit --depuis=target/surefire-reports/testng-results.xml …` |
137
+ | Robot Framework | `output.xml` | `npx @mostajs/mjs-unit --depuis=output.xml …` |
138
+ | C/C++ (CTest) | `ctest -T Test` → `Testing/<date>/Test.xml` | `npx @mostajs/mjs-unit --depuis Testing/*/Test.xml …` |
139
+ | BDD (Cucumber) | `--format json:cucumber.json` (JS, JVM, Ruby) | `npx @mostajs/mjs-unit --depuis=cucumber.json …` |
140
+ | BDD (Behave) | `behave -f json -o behave.json` | `npx @mostajs/mjs-unit --depuis=behave.json …` |
141
+ | Allure (tout langage) | `allure-results/` | `npx @mostajs/mjs-unit --depuis=allure-results …` (conteneurs et pièces jointes écartés ; un essai rejoué : le dernier fait foi) |
142
+
143
+ Nommez vos essais d'après le plan : `test_T_MOD_01_…` est rattaché à `T-MOD-01`. En BDD, Robot ou Allure, une
144
+ **étiquette** égale à la référence suffit (`@T-MOD-01`). Avec le menu :
145
+ `MJS_DEPUIS=target/surefire-reports npx mjs-unit-menu --choix=3`, puis `--choix=4` (plan, SBOM, compte rendu vers
146
+ qatrax.dev). Les lecteurs sont exportés : `import { lireRapportEssais, refDuPlan } from "@mostajs/mjs-unit/lecteurs"`.
147
+
148
+ Ce que le sceau prouve : le rapport n'a pas changé **après sa lecture**, avec **cette** SBOM et **ce** plan. Il ne
149
+ prouve pas que l'outil du langage a joué les essais honnêtement.
150
+
103
151
  ## API
104
152
 
105
153
  | Fonction | Rôle |
package/bin/cli.mjs CHANGED
@@ -10,13 +10,29 @@
10
10
  * --timeout=ms délai par défaut de chaque cas (un cas le surcharge par { timeout })
11
11
  * --tag=a,b ne joue que les cas étiquetés a ou b ; les autres sont rapportés ignorés
12
12
  * --title="…" titre des rapports
13
+ * 0.9.0 — LE COMPTE RENDU DE VALIDATION (cycle CRA) :
14
+ * --sbom=fichier la version éprouvée, LUE dans la SBOM CycloneDX (nom, version, purl, serialNumber, empreinte)
15
+ * --plan=fichier le DEVTEST-PLAN (empreinte ; cas rattachés par leur ref ; cas du plan non exécutés relevés)
16
+ * --compte-rendu[=f] écrit le compte rendu mostajs-compte-rendu/1 (défaut : test-scripts/.out/compte-rendu.json),
17
+ * TOUJOURS — même invalide, avec ses défauts ; le code de sortie reste celui des essais
18
+ * --ctrf[=f] export CTRF (défaut : test-scripts/.out/ctrf.json)
19
+ * --intoto[=f] énoncé in-toto test-result v0.1, non signé (défaut : test-scripts/.out/test-result.intoto.json)
20
+ * 0.10.0 — LE MODE LECTEUR (Java, C#, Python, Go) : aucun essai exécuté, le rapport de l'outil du langage est LU
21
+ * --depuis les arguments sont des RAPPORTS : mjs-unit --depuis target/surefire-reports/*.xml
22
+ * JUnit XML, NUnit 2/3, TRX, TestNG, Robot Framework, CTest, Cucumber JSON, Behave JSON,
23
+ * Allure 2, CTRF, TAP, JSON mjs-unit — reconnus à leur contenu
24
+ * --depuis=chemin un dossier (tous ses .xml, .json, .tap, .trx ; dans allure-results, les *-container et
25
+ * *-attachment sont écartés) ou un fichier
26
+ * → les mêmes sorties : --compte-rendu, --ctrf, --intoto, --json, --junit, --report
13
27
  *
14
28
  * @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
15
29
  */
16
30
  import { register } from "node:module";
17
31
  import { pathToFileURL } from "node:url";
18
- import { writeFileSync } from "node:fs";
32
+ import { writeFileSync, readFileSync, mkdirSync, statSync, readdirSync } from "node:fs";
19
33
  import { hostname } from "node:os";
34
+ import { createHash } from "node:crypto";
35
+ import { dirname, join } from "node:path";
20
36
 
21
37
  register(new URL("../loader.mjs", import.meta.url));
22
38
  const { run, pending, registered, takeRuns, setDefaultTimeout, setTagFilter } = await import("../dist/index.js");
@@ -35,8 +51,12 @@ const junitOut = opt("junit");
35
51
  const title = opt("title");
36
52
  if (opt("timeout")) setDefaultTimeout(Number(opt("timeout")));
37
53
  if (opt("tag")) setTagFilter(opt("tag").split(","));
54
+ const crOut = opt("compte-rendu"), ctrfOut = opt("ctrf"), intotoOut = opt("intoto"), sbomIn = opt("sbom"), planIn = opt("plan");
55
+ const depuis = opt("depuis"); // 0.10.0 — mode lecteur : "" (les arguments sont des rapports) ou un chemin
56
+ const lecteur = depuis !== undefined;
38
57
  const files = argv.filter((a) => !a.startsWith("--"));
39
- if (!files.length) { console.error("usage: mjs-unit <fichier-de-test...> [--report[=f]] [--json[=f]]"); process.exit(2); }
58
+ const debut = new Date().toISOString();
59
+ if (!files.length && !depuis) { console.error("usage: mjs-unit <fichier-de-test...> [--report[=f]] [--json[=f]] — ou : mjs-unit --depuis <rapport...> (JUnit, NUnit, TRX, TestNG, Robot, CTest, Cucumber, Behave, Allure, CTRF, TAP)"); process.exit(2); }
40
60
 
41
61
  // ── Exécution, une suite par fichier ─────────────────────────────────────────
42
62
  // Le CLI DEMANDE au module ce qui a été enregistré et ce qui a été exécuté, fichier par fichier : il
@@ -44,7 +64,45 @@ if (!files.length) { console.error("usage: mjs-unit <fichier-de-test...> [--repo
44
64
  // hors du rapport, et un fichier sans aucun test disparaissait sans un mot.
45
65
  let totalFail = 0, totalDeclares = 0, totalExecutes = 0;
46
66
  const suites = [];
47
- for (const f of files) {
67
+ const sha = (b) => createHash("sha256").update(b).digest("hex");
68
+ // ── 0.10.0 — LE MODE LECTEUR : lire le rapport de l'outil du langage (Surefire, pytest, xUnit, go test…) ─────────
69
+ // Aucun essai n'est exécuté : les suites sont REMPLIES depuis les rapports, puis toute la chaîne suit (compte rendu,
70
+ // CTRF, in-toto, JSON, JUnit, HTML). Un rapport illisible est un défaut DIT ; un statut incompris est une erreur.
71
+ const lecture = lecteur ? { rapports: [], defauts: [] } : null;
72
+ let genereLe = null;
73
+ if (lecteur) {
74
+ const { lireRapportEssais } = await import("../dist/lecteurs.js");
75
+ const RAPPORT = /\.(xml|json|tap|trx)$/i;
76
+ const ANNEXE = /-(container|attachment)\.[^.]+$/i; // Allure : fixtures et pièces jointes, pas des résultats
77
+ const sources = depuis ? (statSync(depuis, { throwIfNoEntry: false })?.isDirectory() ? readdirSync(depuis).filter((n) => RAPPORT.test(n) && !ANNEXE.test(n)).sort().map((n) => join(depuis, n)) : [depuis]) : files;
78
+ if (!sources.length) { console.error(`✗ --depuis : aucun rapport (.xml, .json, .tap, .trx) dans ${depuis}`); process.exit(2); }
79
+ const STATUT = { pass: ["pass"], fail: ["fail", "failure"], skipped: ["skip"], blocked: ["fail", "error"] };
80
+ const ajouter = (rapport, f) => {
81
+ if (rapport.generatedAt && (!genereLe || rapport.generatedAt < genereLe)) genereLe = rapport.generatedAt;
82
+ for (const s of rapport.suites) {
83
+ const results = s.results.map((r) => { const [status, kind] = STATUT[r.status] ?? STATUT.blocked;
84
+ return { name: r.name, title: r.name, status, durationMs: r.durationMs ?? 0, ...(kind ? { kind } : {}), ...(r.error ? { error: r.error } : {}), ...(r.tags?.length ? { tags: r.tags } : {}),
85
+ ...(r.status === "blocked" ? { error: r.error ?? "statut incompris : ni réussi, ni échoué, ni ignoré" } : {}), ...(status === "skip" ? { skipReason: r.error ?? "ignoré par l'outil d'essai" } : {}) }; });
86
+ totalFail += results.filter((r) => r.status === "fail").length;
87
+ totalDeclares += results.length; totalExecutes += results.length;
88
+ suites.push({ file: s.file || f, results });
89
+ }
90
+ };
91
+ // Allure : un fichier par essai — lus ENSEMBLE, pour qu'un essai rejoué (même historyId) ne compte qu'une fois
92
+ const allure = []; let allureSource = null;
93
+ for (const f of sources) {
94
+ let brut;
95
+ try { brut = readFileSync(f); } catch (e) { lecture.defauts.push(`rapport illisible : ${f} — ${e?.code ?? e?.message ?? e}`); totalFail++; continue; }
96
+ const lu = lireRapportEssais(brut.toString("utf8"));
97
+ lecture.rapports.push({ fichier: f, format: lu.format, sha256: sha(brut) });
98
+ if (!lu.rapport) { lecture.defauts.push(`rapport illisible : ${f} — ${lu.erreur ?? lu.format}`); console.error(`✗ ${f} : ${lu.erreur ?? lu.format}`); totalFail++; continue; }
99
+ if (lu.format === "allure") { allure.push(...(Array.isArray(lu.brut) ? lu.brut : [lu.brut])); allureSource ??= depuis && statSync(depuis).isDirectory() ? depuis : f; continue; }
100
+ ajouter(lu.rapport, f);
101
+ }
102
+ if (allure.length) ajouter(lireRapportEssais(allure).rapport, allureSource);
103
+ console.log(`\n${totalFail ? "✗" : "✓"} ${totalExecutes} cas lu(s) dans ${lecture.rapports.length} rapport(s) — mode lecteur : aucun essai exécuté par mjs-unit`);
104
+ }
105
+ for (const f of lecteur ? [] : files) {
48
106
  const avant = registered();
49
107
  takeRuns(); // rien d'un fichier précédent ne doit être attribué à celui-ci
50
108
  await import(pathToFileURL(f).href); // le fichier enregistre ses cas (et peut appeler run() lui-même)
@@ -67,12 +125,12 @@ for (const f of files) {
67
125
  totalDeclares += declares; totalExecutes += cas.length;
68
126
  suites.push({ file: f, results });
69
127
  }
70
- console.log(`\n${totalExecutes === totalDeclares ? "✓" : "✗"} ${totalExecutes}/${totalDeclares} test(s) exécutés sur ${suites.length} fichier(s) — compte relevé auprès du module, pas supposé`);
128
+ if (!lecteur) console.log(`\n${totalExecutes === totalDeclares ? "✓" : "✗"} ${totalExecutes}/${totalDeclares} test(s) exécutés sur ${suites.length} fichier(s) — compte relevé auprès du module, pas supposé`);
71
129
 
72
130
  // ── Reporters optionnels ─────────────────────────────────────────────────────
73
131
  if (htmlOut !== undefined || jsonOut !== undefined || junitOut !== undefined) {
74
132
  const { renderHtmlReport, renderJsonReport, renderJunitReport } = await import("../dist/report.js");
75
- const report = { suites, generatedAt: new Date().toISOString(), title: title || undefined, hostname: hostname() };
133
+ const report = { suites, generatedAt: genereLe ?? new Date().toISOString(), title: title || undefined, hostname: hostname() };
76
134
  if (htmlOut !== undefined) {
77
135
  const out = htmlOut || "test-report.html";
78
136
  writeFileSync(out, renderHtmlReport(report));
@@ -90,4 +148,31 @@ if (htmlOut !== undefined || jsonOut !== undefined || junitOut !== undefined) {
90
148
  }
91
149
  }
92
150
 
151
+ // ── 0.9.0 — le compte rendu de validation, écrit TOUJOURS quand il est demandé (même invalide) ─────────────
152
+ if (crOut !== undefined || ctrfOut !== undefined || intotoOut !== undefined) {
153
+ const { construireCompteRendu, renderCtrf, renderInTotoTestResult } = await import("../dist/report.js");
154
+ const piece = (f) => { const b = readFileSync(f); return { fichier: f, sha256: sha(b), contenu: JSON.parse(b.toString("utf8")) }; };
155
+ const lire = (f) => { if (!f) return { p: null }; try { return { p: piece(f) }; } catch (e) { return { p: null, e: `${f} — ${e?.code ?? e?.message ?? e}` }; } };
156
+ const s = lire(sbomIn), pl = lire(planIn);
157
+ const moi = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
158
+ const env = process.env;
159
+ const ci = env.CI_PIPELINE_URL || env.CI_JOB_URL || (env.GITHUB_RUN_ID && env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` : undefined);
160
+ const cr = construireCompteRendu({ suites, generatedAt: genereLe ?? new Date().toISOString(), title: title || undefined, hostname: hostname() }, {
161
+ sbom: s.p, sbomErreur: s.e, plan: pl.p, planErreur: pl.e,
162
+ scripts: lecteur ? [] : files.map((f) => { try { return { fichier: f, sha256: sha(readFileSync(f)) }; } catch { return { fichier: f, sha256: "" }; } }),
163
+ lecture,
164
+ environnement: { node: process.version, plateforme: `${process.platform}-${process.arch}`, hote: hostname(), ...(ci ? { ci } : {}), debut, fin: new Date().toISOString() },
165
+ declares: totalDeclares, outil: { name: moi.name, version: moi.version, ...(lecteur ? { mode: "lecteur" } : {}) },
166
+ });
167
+ const ecrire = (f, texte) => { mkdirSync(dirname(f), { recursive: true }); writeFileSync(f, texte); return sha(texte); };
168
+ const exports_ = {};
169
+ if (ctrfOut !== undefined) { const f = ctrfOut || "test-scripts/.out/ctrf.json"; exports_.ctrf = ecrire(f, renderCtrf(cr)); console.log(`📄 CTRF : ${f}`); }
170
+ if (intotoOut !== undefined) { const f = intotoOut || "test-scripts/.out/test-result.intoto.json"; exports_.intoto = ecrire(f, renderInTotoTestResult(cr)); console.log(`📄 in-toto test-result : ${f}`); }
171
+ if (crOut !== undefined) {
172
+ const f = crOut || "test-scripts/.out/compte-rendu.json";
173
+ ecrire(f, `${JSON.stringify({ ...cr, integrite: { algorithme: "sha256", empreinte: sha(JSON.stringify(cr)), exports: exports_ } }, null, 2)}\n`);
174
+ console.log(`📄 Compte rendu : ${f} — ${cr.produit.nom}@${cr.produit.version} — verdict : ${cr.synthese.verdict}${cr.defauts.length ? ` (${cr.defauts.length} défaut(s))` : ""}`);
175
+ }
176
+ }
177
+
93
178
  if (totalFail > 0) process.exitCode = 1;
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # mjs-unit-menu — le cycle de validation en un menu (0.10.0) : essais → SBOM → compte rendu → qatrax.dev.
4
+ #
5
+ # 1) lancer les essais (rapports HTML, JSON, JUnit)
6
+ # 2) générer ou continuer la SBOM (npx mjs-sbom --diff)
7
+ # 3) compte rendu complet : SBOM + plan + essais + CTRF + in-toto
8
+ # (MJS_DEPUIS posé : les essais ne sont pas exécutés, le rapport de l'outil du langage est LU — Java, C#, Python, Go, C++ : JUnit, NUnit, TRX, TestNG, Robot, CTest, Cucumber, Behave, Allure…)
9
+ # 4) envoyer à qatrax.dev : POST /import (le plan, s'il existe), POST /sbom, puis POST /runs (le compte rendu)
10
+ # 5) lire les vulnérabilités de la version sur qatrax.dev (GET /vulnerabilites)
11
+ # 6) afficher le dernier compte rendu (verdict, défauts)
12
+ #
13
+ # Sans interaction : mjs-unit-menu --choix=N (CI, scripts).
14
+ # Variables : MJS_TESTS (fichiers d'essai, motif shell ; défaut test-scripts/**/*.test.mjs), MJS_DEPUIS (0.10 : rapports
15
+ # d'un autre langage — dossier, fichier ou motif shell : target/surefire-reports, resultats.xml), MJS_SBOM (défaut
16
+ # sbom.cyclonedx.json), MJS_PLAN (défaut : premier docs/DEVTEST-PLAN*.json), MJS_OUT (défaut test-scripts/.out),
17
+ # QATRAX_URL, QATRAX_CLE (clé de CI, lue dans l'environnement, jamais écrite), QATRAX_PROJET.
18
+ #
19
+ # @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
20
+ set -uo pipefail
21
+ shopt -s globstar nullglob
22
+
23
+ ICI="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
24
+ CLI="${MJS_UNIT_CLI:-$ICI/cli.mjs}"
25
+ OUT="${MJS_OUT:-test-scripts/.out}"
26
+ SBOM="${MJS_SBOM:-sbom.cyclonedx.json}"
27
+ PLAN="${MJS_PLAN:-$(ls docs/DEVTEST-PLAN*.json 2>/dev/null | head -1)}"
28
+ CR="$OUT/compte-rendu.json"
29
+
30
+ essais() { local motif="${MJS_TESTS:-test-scripts/**/*.test.mjs}"; local f; f=( $motif ); printf '%s\n' "${f[@]}"; }
31
+
32
+ lancer_essais() {
33
+ if [ -n "${MJS_DEPUIS:-}" ]; then echo "ℹ MJS_DEPUIS posé : lancez vos essais avec votre outil (mvn test, dotnet test, pytest, go test, robot, ctest, cucumber, behave…), puis choix 3 — mjs-unit LIT leur rapport"; return 0; fi
34
+ mkdir -p "$OUT"; mapfile -t F < <(essais)
35
+ [ ${#F[@]} -gt 0 ] || { echo "✗ aucun fichier d'essai (MJS_TESTS)"; return 2; }
36
+ node "$CLI" "${F[@]}" --report="$OUT/report.html" --json="$OUT/report.json" --junit="$OUT/report.xml"
37
+ }
38
+
39
+ generer_sbom() {
40
+ npx --no-install mjs-sbom --diff || { echo "✗ @mostajs/mjs-sbom n'est pas installé (npm i -D @mostajs/mjs-sbom)"; return 2; }
41
+ }
42
+
43
+ compte_rendu() {
44
+ mkdir -p "$OUT"
45
+ if [ -n "${MJS_DEPUIS:-}" ]; then
46
+ if [ -d "$MJS_DEPUIS" ] || [ -f "$MJS_DEPUIS" ]; then F=("--depuis=$MJS_DEPUIS"); else F=(--depuis $MJS_DEPUIS); fi
47
+ else
48
+ mapfile -t F < <(essais)
49
+ [ ${#F[@]} -gt 0 ] || { echo "✗ aucun fichier d'essai (MJS_TESTS)"; return 2; }
50
+ fi
51
+ local opts=(--compte-rendu="$CR" --ctrf="$OUT/ctrf.json" --intoto="$OUT/test-result.intoto.json" --json="$OUT/report.json" --junit="$OUT/report.xml")
52
+ [ -f "$SBOM" ] && opts+=(--sbom="$SBOM") || echo "⚠ pas de SBOM ($SBOM) : le compte rendu sera « incomplet » (choix 2)"
53
+ [ -n "$PLAN" ] && [ -f "$PLAN" ] && opts+=(--plan="$PLAN")
54
+ node "$CLI" "${F[@]}" "${opts[@]}"
55
+ }
56
+
57
+ # Les appels à qatrax.dev passent par Node (fetch) : pas de dépendance à curl, et la clé ne paraît dans aucune ligne de commande.
58
+ qatrax() { # $1 = action (envoyer | vulnerabilites)
59
+ : "${QATRAX_URL:?QATRAX_URL manquante}" "${QATRAX_CLE:?QATRAX_CLE manquante (clé de CI du projet)}" "${QATRAX_PROJET:?QATRAX_PROJET manquant}"
60
+ ACTION="$1" SBOM="$SBOM" CR="$CR" PLAN="$PLAN" node --input-type=module -e '
61
+ import { readFileSync, existsSync } from "node:fs";
62
+ const { QATRAX_URL: base, QATRAX_CLE: cle, QATRAX_PROJET: p, ACTION, SBOM, CR, PLAN } = process.env;
63
+ const e = encodeURIComponent; const auth = { authorization: `Bearer ${cle}` };
64
+ if (!existsSync(CR)) { console.error(`✗ pas de compte rendu (${CR}) : choix 3 d’abord`); process.exit(2); }
65
+ const cr = JSON.parse(readFileSync(CR, "utf8")); const v = cr.produit?.version ?? "inconnue";
66
+ const appel = async (methode, chemin, corps) => {
67
+ const r = await fetch(`${base}${chemin}`, { method: methode, headers: { ...auth, ...(corps ? { "content-type": "application/json" } : { accept: "application/json" }) }, body: corps });
68
+ const t = await r.text(); console.log(`${r.ok ? "✓" : "✗"} ${methode} ${chemin} → ${r.status}`); if (!r.ok) console.log(t.slice(0, 500)); return { ok: r.ok, t };
69
+ };
70
+ if (ACTION === "envoyer") {
71
+ let bon = true;
72
+ // 0.10.0 — le plan d’abord : sans lui, qatrax.dev ne sait pas quelle exigence chaque essai prouve.
73
+ if (PLAN && existsSync(PLAN)) bon = (await appel("POST", "/import", readFileSync(PLAN, "utf8"))).ok && bon;
74
+ if (existsSync(SBOM)) bon = (await appel("POST", `/sbom?p=${e(p)}&version=${e(v)}`, readFileSync(SBOM, "utf8"))).ok && bon;
75
+ else console.log(`⚠ pas de SBOM (${SBOM}) : seul le compte rendu part — qatrax.dev le consignera « SBOM non reçue »`);
76
+ bon = (await appel("POST", `/runs?p=${e(p)}&format=json`, readFileSync(CR, "utf8"))).ok && bon;
77
+ process.exit(bon ? 0 : 1);
78
+ } else {
79
+ const r = await appel("GET", `/vulnerabilites?p=${e(p)}&version=${e(v)}&format=json`);
80
+ console.log(r.t.slice(0, 4000)); process.exit(r.ok ? 0 : 1);
81
+ }'
82
+ }
83
+
84
+ dernier() {
85
+ [ -f "$CR" ] || { echo "✗ pas de compte rendu ($CR) : choix 3 d'abord"; return 2; }
86
+ CR="$CR" node --input-type=module -e '
87
+ import { readFileSync } from "node:fs";
88
+ const c = JSON.parse(readFileSync(process.env.CR, "utf8")); const s = c.synthese ?? {};
89
+ console.log(`${c.produit?.nom}@${c.produit?.version} — verdict : ${s.verdict}`);
90
+ console.log(` ${s.executes}/${s.declares} exécutés · ${s.succes} succès · ${s.echecs} échec(s) · ${s.erreurs} erreur(s) · ${s.ignores} ignoré(s)`);
91
+ if (c.sbom) console.log(` SBOM ${c.sbom.serialNumber} (révision ${c.sbom.revision}) sha256 ${c.sbom.sha256.slice(0, 16)}…`);
92
+ for (const d of c.defauts ?? []) console.log(` ⚠ ${d}`);'
93
+ }
94
+
95
+ faire() {
96
+ case "$1" in
97
+ 1) lancer_essais ;; 2) generer_sbom ;; 3) compte_rendu ;; 4) qatrax envoyer ;; 5) qatrax vulnerabilites ;; 6) dernier ;;
98
+ *) echo "✗ choix inconnu : $1"; return 2 ;;
99
+ esac
100
+ }
101
+
102
+ for a in "$@"; do case "$a" in --choix=*) faire "${a#--choix=}"; exit $? ;; -h|--help) sed -n '2,19p' "$0"; exit 0 ;; esac; done
103
+
104
+ while true; do
105
+ printf '\n── mjs-unit — cycle de validation ──\n 1) essais 2) SBOM 3) compte rendu complet 4) envoyer à qatrax.dev\n 5) vulnérabilités (qatrax.dev) 6) dernier compte rendu q) quitter\n> '
106
+ read -r c || exit 0
107
+ [ "$c" = "q" ] && exit 0
108
+ faire "$c"
109
+ done
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @mostajs/mjs-unit/lecteurs — LIRE UN RAPPORT D'ESSAIS, QUEL QU'EN SOIT L'OUTIL (0.10.0).
3
+ * @author Dr Hamid MADANI <drmdh@msn.com>
4
+ * @license AGPL-3.0-or-later
5
+ *
6
+ * Descendus de @mostajs/qa-engine/formats (DEVRULES R-G : la logique partagée vit à la couche la plus basse) pour le
7
+ * mode lecteur de mjs-unit (`--depuis`) : un projet Java, C#, Python ou Go garde son outil d'essai, et mjs-unit LIT
8
+ * son rapport pour en faire le compte rendu de validation. qa-engine les reprend d'ici.
9
+ *
10
+ * JUnit XML (Surefire, Gradle, pytest, xUnit/JunitXml.TestLogger, go-junit-report, mjs-unit --junit) → 'junit'
11
+ * NUnit 2 et 3 (TestResult.xml) → 'nunit'
12
+ * TRX (dotnet test --logger trx : VSTest, xUnit, MSTest, NUnit) → 'trx'
13
+ * TestNG natif (testng-results.xml) → 'testng'
14
+ * Robot Framework (output.xml, RF 6 et 7) → 'robot'
15
+ * CTest (ctest -T Test → Testing/<horodatage>/Test.xml) → 'ctest'
16
+ * Cucumber JSON (cucumber-js, Cucumber-JVM, SpecFlow/Reqnroll…) → 'cucumber'
17
+ * Behave JSON (behave -f json) → 'behave'
18
+ * Allure 2 (allure-results/*-result.json, un fichier ou un tableau) → 'allure'
19
+ * TAP (node --test, Perl…) → 'tap'
20
+ * CTRF (ctrf-io, mjs-unit --ctrf) → 'ctrf'
21
+ * mjs-unit --json / --compte-rendu / --intoto → 'mjs-unit' / 'compte-rendu' / 'intoto'
22
+ * rapport HTML → 'html' : preuve lisible par un humain, non lue
23
+ *
24
+ * Les formats NUnit à Allure ont été ajoutés le 10/10/2026 à la demande du fondateur : ce sont ceux que lisent Xray,
25
+ * Allure TestOps, TestRail, Zephyr, ReportPortal et Azure DevOps (docs/01c §5 bis). Le rapport du client reste le sien.
26
+ *
27
+ * Le format se reconnaît au CONTENU. Un statut incompris devient `blocked`, jamais `pass`. Pur : aucune E/S, aucune
28
+ * dépendance — `./lecteurs` s'importe partout (navigateur compris).
29
+ */
30
+ export type FormatRapport = 'mjs-unit' | 'compte-rendu' | 'junit' | 'nunit' | 'trx' | 'testng' | 'robot' | 'ctest' | 'cucumber' | 'behave' | 'allure' | 'ctrf' | 'intoto' | 'tap' | 'html' | 'illisible';
31
+ export type StatutLu = 'pass' | 'fail' | 'skipped' | 'blocked';
32
+ /** `tags` : les étiquettes de l'outil (Cucumber @T-MOD-01, Robot, Allure) — `rattacherAuPlan` y cherche une référence du plan. */
33
+ export interface ResultatLu {
34
+ name: string;
35
+ status: StatutLu;
36
+ durationMs?: number;
37
+ error?: string;
38
+ tags?: string[];
39
+ }
40
+ export interface RapportLu {
41
+ title: string;
42
+ generatedAt: string | null;
43
+ suites: {
44
+ file: string;
45
+ results: ResultatLu[];
46
+ }[];
47
+ }
48
+ export interface LectureRapport {
49
+ format: FormatRapport;
50
+ /** la forme que lit importTestRun ; null si le corps n'est pas un rapport d'essais lisible par une machine */
51
+ rapport: RapportLu | null;
52
+ /** l'objet reçu (JSON), tel quel — c'est sur lui que porte l'empreinte d'un compte rendu */
53
+ brut: unknown;
54
+ erreur?: string;
55
+ }
56
+ /** Les formats lus, NOMMÉS dans chaque motif de refus : le client sait quoi envoyer sans chercher. */
57
+ export declare const FORMATS_LUS: {
58
+ readonly xml: "JUnit XML, NUnit 2/3, TRX (VSTest), TestNG, Robot Framework (output.xml), CTest (Test.xml)";
59
+ readonly json: "CTRF, Cucumber JSON, Behave JSON, Allure 2, in-toto test-result, JSON et compte rendu mjs-unit";
60
+ readonly texte: "TAP";
61
+ };
62
+ /** Le format d'un corps de rapport, reconnu à son contenu. */
63
+ export declare function detecterFormat(corps: unknown): FormatRapport;
64
+ /** Lit un rapport d'essais, quel qu'en soit le format ; jamais d'exception : un corps illisible est DIT illisible. */
65
+ export declare function lireRapportEssais(corps: unknown): LectureRapport;
66
+ /** La référence du plan que désigne ce nom d'essai, ou '' : le nom exact d'abord, puis par jetons. */
67
+ export declare function refDuPlan(nom: string, refs: Iterable<string>): string;
68
+ /**
69
+ * Pose sur chaque essai sa référence du plan (`T-MOD-01 — test_T_MOD_01_…`) et le fichier de son `autoRef`
70
+ * (`<fichier>::<ref>`). Un fichier déjà conforme au plan est gardé ; une référence présente dans plusieurs fichiers
71
+ * ne fait rien deviner (le fichier d'origine reste) ; un essai hors plan est laissé tel quel. Pur.
72
+ */
73
+ export declare function rattacherAuPlan<R extends {
74
+ name: string;
75
+ }>(suites: {
76
+ file: string;
77
+ results: R[];
78
+ }[], autoRefs: Iterable<string>): {
79
+ file: string;
80
+ results: R[];
81
+ }[];
82
+ //# sourceMappingURL=lecteurs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lecteurs.d.ts","sourceRoot":"","sources":["../src/lecteurs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,cAAc,GAAG,OAAO,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,GAAG,WAAW,CAAC;AACzM,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;AAC/D,kIAAkI;AAClI,MAAM,WAAW,UAAU;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE;AACpH,MAAM,WAAW,SAAS;IAAG,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,UAAU,EAAE,CAAA;KAAE,EAAE,CAAA;CAAE;AAC3H,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,aAAa,CAAC;IACtB,8GAA8G;IAC9G,OAAO,EAAE,SAAS,GAAG,IAAI,CAAC;IAC1B,4FAA4F;IAC5F,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AASD,sGAAsG;AACtG,eAAO,MAAM,WAAW;;;;CAId,CAAC;AAwCX,8DAA8D;AAC9D,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,aAAa,CAM5D;AAwQD,sHAAsH;AACtH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CA4BhE;AASD,sGAAsG;AACtG,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,CAWrE;AAcD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,CAAC,EAAE,CAAA;CAAE,EAAE,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,CAAC,EAAE,CAAA;CAAE,EAAE,CAuBlK"}