@mmerterden/multi-agent-pipeline 20.27.1 → 20.28.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/CHANGELOG.md +38 -0
- package/README.md +6 -5
- package/README.tr.md +6 -5
- package/docs/adr/0008-installer-modularization-and-secret-leak-defense.md +0 -1
- package/docs/architecture.md +4 -4
- package/docs/ecosystem.md +5 -5
- package/docs/facts.json +14 -12
- package/docs/features.md +2 -0
- package/install/_dev-only-files.mjs +1 -0
- package/install/catalog-history.json +1 -1
- package/install/index.mjs +0 -2
- package/manifest.json +74 -51
- package/package.json +4 -1
- package/pipeline/agents/code-reviewer.md +1 -0
- package/pipeline/agents/test-gen-analyst.md +98 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/test-gen/SKILL.md +124 -0
- package/pipeline/multi-agent-refs/cross-cli-contract.md +11 -5
- package/pipeline/multi-agent-refs/features/doctor.md +13 -0
- package/pipeline/multi-agent-refs/features/test-classification.md +76 -0
- package/pipeline/multi-agent-refs/features/usage-reporting.md +17 -0
- package/pipeline/multi-agent-refs/help/en.md +1 -0
- package/pipeline/multi-agent-refs/help/tr.md +1 -0
- package/pipeline/multi-agent-refs/phases/operations.md +2 -2
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +6 -7
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +3 -3
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +6 -5
- package/pipeline/multi-agent-refs/store-ready/gate-3-policy.md +6 -2
- package/pipeline/multi-agent-refs/test-gen/stages.md +227 -0
- package/pipeline/preferences-template.json +3 -0
- package/pipeline/rules/app-store-guidelines.md +29 -41
- package/pipeline/schemas/prefs.schema.json +14 -0
- package/pipeline/schemas/test-gen-discovery.schema.json +73 -0
- package/pipeline/schemas/test-gen-gaps.schema.json +122 -0
- package/pipeline/schemas/test-gen-inventory.schema.json +59 -0
- package/pipeline/schemas/test-gen-results.schema.json +44 -0
- package/pipeline/scripts/_handle-patterns.mjs +166 -0
- package/pipeline/scripts/_http-post.mjs +141 -0
- package/pipeline/scripts/_stack-routing.mjs +4 -0
- package/pipeline/scripts/_test-gaming.mjs +227 -0
- package/pipeline/scripts/_unified-diff.mjs +104 -0
- package/pipeline/scripts/_worktree-snapshot.mjs +113 -0
- package/pipeline/scripts/doctor.mjs +51 -0
- package/pipeline/scripts/evidence-gate.mjs +74 -3
- package/pipeline/scripts/handle-diff-audit.mjs +188 -0
- package/pipeline/scripts/routine-registry.mjs +5 -1
- package/pipeline/scripts/run-ui-tests.sh +27 -3
- package/pipeline/scripts/test-classify.mjs +296 -0
- package/pipeline/scripts/test-gen-gate.mjs +409 -0
- package/pipeline/scripts/test-gen-state.mjs +313 -0
- package/pipeline/scripts/test-integrity-gate.mjs +76 -11
- package/pipeline/scripts/uninstall.mjs +1 -0
- package/pipeline/scripts/usage-register-beacon.mjs +239 -0
- package/pipeline/scripts/usage-register.mjs +128 -33
- package/pipeline/scripts/usage-report.mjs +43 -33
- package/pipeline/skills/.skill-manifest.json +25 -5
- package/pipeline/skills/.skills-index.json +57 -2
- package/pipeline/skills/shared/README.md +11 -6
- package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +5 -4
- package/pipeline/skills/shared/core/apple-archive-compliance/references/rules.yml +8 -8
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
- package/pipeline/skills/shared/core/multi-agent-test-gen/SKILL.md +120 -0
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +69 -8
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +12 -0
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +10 -1
- package/pipeline/skills/shared/external/fix-review-verdicts/SKILL.md +133 -0
- package/pipeline/skills/shared/external/handle-request/SKILL.md +97 -0
- package/pipeline/skills/shared/external/scenario-gap-analysis/SKILL.md +129 -0
- package/pipeline/skills/shared/external/scenario-gap-analysis/references/scenario-schema.md +60 -0
- package/pipeline/skills/shared/external/test-result-triage/SKILL.md +160 -0
- package/pipeline/skills/skills-index.md +7 -2
- package/install/_telemetry.mjs +0 -61
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file test-gen-gate.mjs - each /multi-agent:test-gen stage is held to its output.
|
|
4
|
+
*
|
|
5
|
+
* A stage that says it is done has produced something on disk, or it has not.
|
|
6
|
+
* This reads what is there:
|
|
7
|
+
*
|
|
8
|
+
* check an artefact matches its schema (test-gen-inventory,
|
|
9
|
+
* -discovery, -gaps, -results), read from schemas/ by a small
|
|
10
|
+
* validator covering the keywords those schemas use, so the
|
|
11
|
+
* runtime needs no dependency.
|
|
12
|
+
* baseline at the start of a stage that may edit the repository: the
|
|
13
|
+
* working tree, untracked files included, as a git tree
|
|
14
|
+
* (_worktree-snapshot.mjs). The stage is then judged by what
|
|
15
|
+
* changed after it, never by the person's uncommitted work.
|
|
16
|
+
* handles at the end of the handle stage: the delta adds at least one
|
|
17
|
+
* handle line in product code, or handles.md says
|
|
18
|
+
* `HANDLES: none-needed` and cites at least one `file:line`,
|
|
19
|
+
* inside the repository, that carries a handle by its own
|
|
20
|
+
* platform's pattern. Saying no handle was needed is a claim, and
|
|
21
|
+
* the claim is that the handles already exist.
|
|
22
|
+
* written at the end of the write stage: the delta touches test files
|
|
23
|
+
* only; every listed file is inside the repository, changed by
|
|
24
|
+
* the stage and not empty; the delta carries no test gaming
|
|
25
|
+
* (_test-gaming.mjs); the run command is one discovery found
|
|
26
|
+
* (--discovery); and the run executed more than zero tests
|
|
27
|
+
* (test-summary.mjs, from --junit, --xcresult-json or a --log).
|
|
28
|
+
* results the classification (test-classify.mjs) as results.json: three
|
|
29
|
+
* states, pass | fail | not_measured, every non-pass with a reason.
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* test-gen-gate.mjs check --stage inventory|discovery|gaps|results --file <path>
|
|
33
|
+
* test-gen-gate.mjs baseline --repo <path> --out <path>
|
|
34
|
+
* test-gen-gate.mjs handles --repo <path> --baseline <path> --handles <handles.md>
|
|
35
|
+
* test-gen-gate.mjs written --repo <path> --baseline <path> --written <written.json>
|
|
36
|
+
* [--discovery <discovery.json>]
|
|
37
|
+
* (--junit <file|dir> | --xcresult-json <file> | --log <file> [--stack <id>])
|
|
38
|
+
* test-gen-gate.mjs results --classification <path> --out <results.json> [--written <written.json>]
|
|
39
|
+
*
|
|
40
|
+
* Output: one JSON verdict on stdout.
|
|
41
|
+
* Exit: 0 pass, 1 fail, 2 usage, 3 input unreadable, 4 internal error (a crash,
|
|
42
|
+
* never a verdict)
|
|
43
|
+
*
|
|
44
|
+
* @module pipeline/scripts/test-gen-gate
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import {
|
|
48
|
+
existsSync,
|
|
49
|
+
mkdirSync,
|
|
50
|
+
readFileSync,
|
|
51
|
+
realpathSync,
|
|
52
|
+
statSync,
|
|
53
|
+
writeFileSync,
|
|
54
|
+
} from "node:fs";
|
|
55
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
56
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
57
|
+
|
|
58
|
+
import { countAddedHandles, isHandleLine, isTestPath } from "./_handle-patterns.mjs";
|
|
59
|
+
import { scanTestGaming } from "./_test-gaming.mjs";
|
|
60
|
+
import { parseUnifiedDiff } from "./_unified-diff.mjs";
|
|
61
|
+
import { GitError, diffSince, git, snapshotTree } from "./_worktree-snapshot.mjs";
|
|
62
|
+
|
|
63
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
64
|
+
const SCHEMAS = join(HERE, "..", "schemas");
|
|
65
|
+
const STAGES = ["inventory", "discovery", "gaps", "results"];
|
|
66
|
+
|
|
67
|
+
class Unreadable extends Error {}
|
|
68
|
+
|
|
69
|
+
function usage(msg) {
|
|
70
|
+
process.stderr.write(`test-gen-gate: ${msg}\n`);
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function verdict(ok, body) {
|
|
75
|
+
writeFileSync(1, `${JSON.stringify({ ok, ...body }, null, 2)}\n`);
|
|
76
|
+
process.exit(ok ? 0 : 1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseArgs(argv) {
|
|
80
|
+
const out = {};
|
|
81
|
+
for (let i = 0; i < argv.length; i++) {
|
|
82
|
+
const a = argv[i];
|
|
83
|
+
if (!a.startsWith("--")) usage(`unexpected argument ${a}`);
|
|
84
|
+
const v = argv[i + 1];
|
|
85
|
+
if (v === undefined || v.startsWith("-"))
|
|
86
|
+
usage(`${a} needs a value that does not start with "-"`);
|
|
87
|
+
out[a.slice(2)] = v;
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function need(args, key) {
|
|
94
|
+
if (!args[key]) usage(`--${key} is required`);
|
|
95
|
+
return args[key];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readText(path, what) {
|
|
99
|
+
try {
|
|
100
|
+
return readFileSync(path, "utf8");
|
|
101
|
+
} catch (err) {
|
|
102
|
+
throw new Unreadable(`cannot read ${what} ${path}: ${err.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readJson(path, what) {
|
|
107
|
+
const text = readText(path, what);
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(text);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
throw new Unreadable(`${what} ${path} is not JSON: ${err.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Schema check: the subset of JSON Schema the test-gen schemas use.
|
|
117
|
+
|
|
118
|
+
function typeOk(type, v) {
|
|
119
|
+
if (type === "object") return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
120
|
+
if (type === "array") return Array.isArray(v);
|
|
121
|
+
if (type === "integer") return Number.isInteger(v);
|
|
122
|
+
if (type === "string") return typeof v === "string";
|
|
123
|
+
if (type === "boolean") return typeof v === "boolean";
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {object} s schema node
|
|
129
|
+
* @param {*} v value
|
|
130
|
+
* @param {string} at JSON pointer for messages
|
|
131
|
+
* @param {object} root the whole schema, for $ref
|
|
132
|
+
* @returns {string[]} errors
|
|
133
|
+
*/
|
|
134
|
+
export function validate(s, v, at = "", root = s) {
|
|
135
|
+
if (s.$ref) return validate(root.$defs[s.$ref.replace("#/$defs/", "")], v, at, root);
|
|
136
|
+
const errors = [];
|
|
137
|
+
const here = at || "/";
|
|
138
|
+
if ("const" in s && v !== s.const) return [`${here} must be ${JSON.stringify(s.const)}`];
|
|
139
|
+
if (s.enum && !s.enum.includes(v)) return [`${here} must be one of ${s.enum.join(", ")}`];
|
|
140
|
+
if (s.type && !typeOk(s.type, v)) return [`${here} must be ${s.type}`];
|
|
141
|
+
if (typeof v === "string") {
|
|
142
|
+
if (s.minLength && v.length < s.minLength) errors.push(`${here} is empty`);
|
|
143
|
+
if (s.pattern && !new RegExp(s.pattern).test(v))
|
|
144
|
+
errors.push(`${here} does not match ${s.pattern}`);
|
|
145
|
+
}
|
|
146
|
+
if (typeof v === "number" && s.minimum !== undefined && v < s.minimum) {
|
|
147
|
+
errors.push(`${here} is below ${s.minimum}`);
|
|
148
|
+
}
|
|
149
|
+
if (Array.isArray(v)) {
|
|
150
|
+
if (s.minItems && v.length < s.minItems) errors.push(`${here} needs ${s.minItems}+ items`);
|
|
151
|
+
if (s.items) v.forEach((x, i) => errors.push(...validate(s.items, x, `${at}/${i}`, root)));
|
|
152
|
+
}
|
|
153
|
+
if (typeOk("object", v) && (s.properties || s.required)) {
|
|
154
|
+
for (const k of s.required || []) if (!(k in v)) errors.push(`${at}/${k} is required`);
|
|
155
|
+
for (const [k, x] of Object.entries(v)) {
|
|
156
|
+
if (s.properties?.[k]) errors.push(...validate(s.properties[k], x, `${at}/${k}`, root));
|
|
157
|
+
else if (s.additionalProperties === false) errors.push(`${at}/${k} is not allowed`);
|
|
158
|
+
}
|
|
159
|
+
if (s.if && validate(s.if, v, at, root).length === 0 && s.then) {
|
|
160
|
+
errors.push(...validate(s.then, v, at, root));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return errors;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function check(args) {
|
|
167
|
+
const stage = need(args, "stage");
|
|
168
|
+
if (!STAGES.includes(stage)) usage(`--stage must be one of ${STAGES.join(", ")}`);
|
|
169
|
+
const schema = readJson(join(SCHEMAS, `test-gen-${stage}.schema.json`), "schema");
|
|
170
|
+
const doc = readJson(need(args, "file"), `${stage} artefact`);
|
|
171
|
+
const errors = validate(schema, doc);
|
|
172
|
+
verdict(errors.length === 0, { stage, file: args.file, errors });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Handle stage
|
|
177
|
+
|
|
178
|
+
/** The repository-relative path of `file`, or null when it resolves outside the repository. */
|
|
179
|
+
function insideRepo(repo, file) {
|
|
180
|
+
const root = realpathSync(repo);
|
|
181
|
+
const abs = resolve(root, file);
|
|
182
|
+
const rel = relative(root, existsSync(abs) ? realpathSync(abs) : abs);
|
|
183
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;
|
|
184
|
+
return rel.split(sep).join("/");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function stageDelta(repo, baselinePath) {
|
|
188
|
+
const base = readJson(baselinePath, "baseline");
|
|
189
|
+
if (typeof base.tree !== "string") {
|
|
190
|
+
throw new Unreadable(
|
|
191
|
+
`baseline ${baselinePath} has no tree: take it with test-gen-gate.mjs baseline`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
return diffSince(repo, base.tree);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (err instanceof GitError) throw new Unreadable(err.message);
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function headOrNull(repo) {
|
|
203
|
+
try {
|
|
204
|
+
return git(repo, ["rev-parse", "--verify", "--quiet", "HEAD"]).trim();
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function baseline(args) {
|
|
211
|
+
const repo = need(args, "repo");
|
|
212
|
+
const out = need(args, "out");
|
|
213
|
+
let tree;
|
|
214
|
+
let head;
|
|
215
|
+
try {
|
|
216
|
+
tree = snapshotTree(repo);
|
|
217
|
+
head = headOrNull(repo);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
if (err instanceof GitError) throw new Unreadable(err.message);
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
const doc = { tree, head, startedAt: new Date().toISOString() };
|
|
223
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
224
|
+
writeFileSync(out, `${JSON.stringify(doc, null, 2)}\n`);
|
|
225
|
+
verdict(true, doc);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** `path:line` references in handles.md that resolve to a handle line. */
|
|
229
|
+
function resolvedHandleEvidence(repo, text) {
|
|
230
|
+
const refs = [...text.matchAll(/([\w./@+-]+\.[A-Za-z0-9]+):([1-9]\d*)\b/g)];
|
|
231
|
+
const resolved = [];
|
|
232
|
+
const rejected = [];
|
|
233
|
+
for (const [, file, lineStr] of refs) {
|
|
234
|
+
const line = Number(lineStr);
|
|
235
|
+
const rel = insideRepo(repo, file);
|
|
236
|
+
if (rel === null) {
|
|
237
|
+
rejected.push(`${file}:${line} is outside the repository`);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const abs = join(repo, rel);
|
|
241
|
+
let lines;
|
|
242
|
+
try {
|
|
243
|
+
lines = readFileSync(abs, "utf8").split("\n");
|
|
244
|
+
} catch {
|
|
245
|
+
rejected.push(`${file}:${line} does not exist`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (line > lines.length || !isHandleLine(file, lines[line - 1])) {
|
|
249
|
+
rejected.push(`${file}:${line} carries no handle`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
resolved.push(`${file}:${line}`);
|
|
253
|
+
}
|
|
254
|
+
return { resolved, rejected };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function handles(args) {
|
|
258
|
+
const repo = need(args, "repo");
|
|
259
|
+
const delta = stageDelta(repo, need(args, "baseline"));
|
|
260
|
+
const text = readText(need(args, "handles"), "handles.md");
|
|
261
|
+
const now = countAddedHandles(delta);
|
|
262
|
+
if (now.total > 0) {
|
|
263
|
+
verdict(true, { added: now.total, byFile: now.byFile });
|
|
264
|
+
}
|
|
265
|
+
if (/^\s*HANDLES:\s*none-needed\b/im.test(text)) {
|
|
266
|
+
const { resolved, rejected } = resolvedHandleEvidence(repo, text);
|
|
267
|
+
verdict(resolved.length > 0, {
|
|
268
|
+
added: 0,
|
|
269
|
+
noneNeeded: true,
|
|
270
|
+
evidence: resolved,
|
|
271
|
+
rejected,
|
|
272
|
+
reason: resolved.length
|
|
273
|
+
? "no handle needed: the cited handles exist"
|
|
274
|
+
: "HANDLES: none-needed needs file:line evidence of handles that exist",
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
verdict(false, {
|
|
278
|
+
added: 0,
|
|
279
|
+
reason:
|
|
280
|
+
"the stage added no handle line after its stage-start snapshot, and handles.md does not say HANDLES: none-needed",
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// Write stage
|
|
286
|
+
|
|
287
|
+
async function written(args) {
|
|
288
|
+
const repo = need(args, "repo");
|
|
289
|
+
const delta = stageDelta(repo, need(args, "baseline"));
|
|
290
|
+
const doc = readJson(need(args, "written"), "written.json");
|
|
291
|
+
const files = Array.isArray(doc.files) ? doc.files : [];
|
|
292
|
+
const problems = [];
|
|
293
|
+
const changed = new Map(parseUnifiedDiff(delta));
|
|
294
|
+
for (const [file, f] of changed) {
|
|
295
|
+
if (!isTestPath(file)) {
|
|
296
|
+
problems.push(`${file} is not a test file; after the handle stage only tests may change`);
|
|
297
|
+
} else if (f.isDeleted) problems.push(`${file} was deleted by the write stage`);
|
|
298
|
+
}
|
|
299
|
+
if (files.length === 0) problems.push("written.json lists no test file");
|
|
300
|
+
for (const f of files) {
|
|
301
|
+
const rel = typeof f === "string" ? insideRepo(repo, f) : null;
|
|
302
|
+
if (rel === null) {
|
|
303
|
+
problems.push(`${f} is outside the repository`);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const abs = join(repo, rel);
|
|
307
|
+
if (!existsSync(abs)) problems.push(`${f} does not exist`);
|
|
308
|
+
else if (statSync(abs).size === 0) problems.push(`${f} is empty`);
|
|
309
|
+
else if (!changed.has(rel)) problems.push(`${f} was not written or changed by this stage`);
|
|
310
|
+
}
|
|
311
|
+
const gaming = scanTestGaming(delta);
|
|
312
|
+
for (const g of gaming) problems.push(`${g.kind} at ${g.file}:${g.line}: ${g.evidence}`);
|
|
313
|
+
if (args.discovery) {
|
|
314
|
+
const disc = readJson(args.discovery, "discovery.json");
|
|
315
|
+
const known = (Array.isArray(disc.testRoots) ? disc.testRoots : []).map((r) => r.command);
|
|
316
|
+
const command = String(doc.command || "").trim();
|
|
317
|
+
if (!command) problems.push("written.json has no run command");
|
|
318
|
+
else if (!known.some((k) => typeof k === "string" && k && command.startsWith(k.trim()))) {
|
|
319
|
+
problems.push(`run command "${command}" is none of the runners discovery found`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const ts = await import("./test-summary.mjs");
|
|
323
|
+
let summary;
|
|
324
|
+
if (args.junit) summary = ts.summarizeJunit(args.junit);
|
|
325
|
+
else if (args["xcresult-json"]) {
|
|
326
|
+
summary = ts.fromXcresultJson(
|
|
327
|
+
readJson(args["xcresult-json"], "xcresult summary"),
|
|
328
|
+
args["xcresult-json"],
|
|
329
|
+
);
|
|
330
|
+
} else if (args.log) {
|
|
331
|
+
let parsers;
|
|
332
|
+
if (args.stack) {
|
|
333
|
+
const { getAdapter } = await import("./_stack-adapter.mjs");
|
|
334
|
+
parsers = getAdapter(args.stack, { strict: true }).testSummary?.parsers;
|
|
335
|
+
}
|
|
336
|
+
summary = ts.summarize(readText(args.log, "test log"), { parsers, source: args.log });
|
|
337
|
+
} else usage("written needs --junit, --xcresult-json or --log");
|
|
338
|
+
if (!summary || summary.executed === null) {
|
|
339
|
+
problems.push(`test counts unread: ${summary?.reason || "no result"}`);
|
|
340
|
+
} else if (summary.executed === 0) problems.push("the run executed 0 tests");
|
|
341
|
+
verdict(problems.length === 0, {
|
|
342
|
+
files: files.length,
|
|
343
|
+
executed: summary?.executed ?? null,
|
|
344
|
+
failed: summary?.failed ?? null,
|
|
345
|
+
problems,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
// Results
|
|
351
|
+
|
|
352
|
+
// A test this run wrote that is red on the base too is not inherited the way a
|
|
353
|
+
// preexisting failure is at Gate 3: the test is new, so its red says the
|
|
354
|
+
// behaviour it asserts is missing. That is a finding for triage, hence fail.
|
|
355
|
+
const STATE_OF = {
|
|
356
|
+
passed: "pass",
|
|
357
|
+
regression: "fail",
|
|
358
|
+
preexisting: "fail",
|
|
359
|
+
unstable: "fail",
|
|
360
|
+
not_measured: "not_measured",
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
export function toResults(classification, writtenDoc) {
|
|
364
|
+
const fileOf = new Map(
|
|
365
|
+
(Array.isArray(writtenDoc?.tests) ? writtenDoc.tests : []).map((t) => [t.id, t.file]),
|
|
366
|
+
);
|
|
367
|
+
const tests = (classification.tests || []).map((t) => {
|
|
368
|
+
const status = STATE_OF[t.status];
|
|
369
|
+
if (!status) throw new Unreadable(`unknown classification ${t.status} for ${t.id}`);
|
|
370
|
+
const row = { id: t.id, status, classification: t.status };
|
|
371
|
+
if (fileOf.get(t.id)) row.file = fileOf.get(t.id);
|
|
372
|
+
if (status !== "pass") row.reason = t.reason || `classified ${t.status}`;
|
|
373
|
+
return row;
|
|
374
|
+
});
|
|
375
|
+
const summary = { pass: 0, fail: 0, not_measured: 0 };
|
|
376
|
+
for (const t of tests) summary[t.status]++;
|
|
377
|
+
return {
|
|
378
|
+
schemaVersion: "1.0.0",
|
|
379
|
+
source: classification.source === "fallback" ? "fallback" : "toolkit",
|
|
380
|
+
green: classification.green === true && summary.fail === 0 && summary.not_measured === 0,
|
|
381
|
+
tests,
|
|
382
|
+
summary,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function results(args) {
|
|
387
|
+
const classification = readJson(need(args, "classification"), "classification");
|
|
388
|
+
const writtenDoc = args.written ? readJson(args.written, "written.json") : null;
|
|
389
|
+
const out = need(args, "out");
|
|
390
|
+
const doc = toResults(classification, writtenDoc);
|
|
391
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
392
|
+
writeFileSync(out, `${JSON.stringify(doc, null, 2)}\n`);
|
|
393
|
+
verdict(true, { out, summary: doc.summary, green: doc.green });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function main() {
|
|
397
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
398
|
+
const commands = { check, baseline, handles, written, results };
|
|
399
|
+
if (!commands[cmd]) usage(`first argument is one of ${Object.keys(commands).join(", ")}`);
|
|
400
|
+
try {
|
|
401
|
+
await commands[cmd](parseArgs(rest));
|
|
402
|
+
} catch (err) {
|
|
403
|
+
const code = err instanceof Unreadable ? 3 : 4;
|
|
404
|
+
writeFileSync(1, `${JSON.stringify({ ok: false, reason: err.message })}\n`);
|
|
405
|
+
process.exit(code);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) await main();
|