canary-test-cli 7.0.0 → 7.2.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/agents/skills/README.md +308 -0
- package/agents/skills/canary:generate.md +49 -0
- package/agents/skills/canary:init.md +37 -0
- package/agents/skills/canary:migrate.md +66 -0
- package/agents/skills/claude-code/canary-add-framework/SKILL.md +248 -0
- package/agents/skills/claude-code/canary-blackhawk/SKILL.md +170 -0
- package/agents/skills/claude-code/canary-blackhawk/scripts/cli.mjs +188 -0
- package/agents/skills/claude-code/canary-blackhawk/scripts/rules.mjs +120 -0
- package/agents/skills/claude-code/canary-blackhawk/scripts/scanner.mjs +244 -0
- package/agents/skills/claude-code/canary-blackhawk/scripts/string-literals.mjs +116 -0
- package/agents/skills/claude-code/canary-cassandra/SKILL.md +180 -0
- package/agents/skills/claude-code/canary-cassandra/scripts/cli.mjs +268 -0
- package/agents/skills/claude-code/canary-cassandra/scripts/engine.mjs +95 -0
- package/agents/skills/claude-code/canary-ci-ready/SKILL.md +161 -0
- package/agents/skills/claude-code/canary-ci-ready/skill.yaml +14 -0
- package/agents/skills/claude-code/canary-company-knowledge/SKILL.md +196 -0
- package/agents/skills/claude-code/canary-critical-areas/SKILL.md +142 -0
- package/agents/skills/claude-code/canary-critical-areas/skill.yaml +16 -0
- package/agents/skills/claude-code/canary-edge-case-discovery/SKILL.md +160 -0
- package/agents/skills/claude-code/canary-edge-case-discovery/skill.yaml +16 -0
- package/agents/skills/claude-code/canary-fail-fast/SKILL.md +75 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/cli.mjs +118 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/digest.mjs +69 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/failures.mjs +60 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/fastfail_check.mjs +43 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/parse.mjs +149 -0
- package/agents/skills/claude-code/canary-failure-impact/SKILL.md +153 -0
- package/agents/skills/claude-code/canary-failure-impact/skill.yaml +15 -0
- package/agents/skills/claude-code/canary-fleet-health/SKILL.md +196 -0
- package/agents/skills/claude-code/canary-generate-test/SKILL.md +185 -0
- package/agents/skills/claude-code/canary-instrument/SKILL.md +157 -0
- package/agents/skills/claude-code/canary-instrument/scripts/cli.mjs +178 -0
- package/agents/skills/claude-code/canary-instrument/scripts/otel_bootstrap/instrument.mjs +96 -0
- package/agents/skills/claude-code/canary-instrument/scripts/otel_bootstrap/playwright-fixture.ts +44 -0
- package/agents/skills/claude-code/canary-instrument/scripts/run_types.mjs +81 -0
- package/agents/skills/claude-code/canary-instrument/scripts/span_reader.mjs +187 -0
- package/agents/skills/claude-code/canary-katana/SKILL.md +243 -0
- package/agents/skills/claude-code/canary-katana/scripts/alarm.mjs +296 -0
- package/agents/skills/claude-code/canary-katana/scripts/cli.mjs +247 -0
- package/agents/skills/claude-code/canary-katana/scripts/diffscan.mjs +0 -0
- package/agents/skills/claude-code/canary-katana/scripts/ledger.mjs +183 -0
- package/agents/skills/claude-code/canary-pr-guardian/SKILL.md +129 -0
- package/agents/skills/claude-code/canary-pr-guardian/skill.yaml +17 -0
- package/agents/skills/claude-code/canary-promote-test/SKILL.md +228 -0
- package/agents/skills/claude-code/canary-savant/SKILL.md +233 -0
- package/agents/skills/claude-code/canary-savant/scripts/cli.mjs +274 -0
- package/agents/skills/claude-code/canary-savant/scripts/restoration.mjs +274 -0
- package/agents/skills/claude-code/canary-savant/scripts/rules.mjs +168 -0
- package/agents/skills/claude-code/canary-savant/scripts/runner.mjs +572 -0
- package/agents/skills/claude-code/canary-savant/scripts/scanner.mjs +374 -0
- package/agents/skills/claude-code/canary-savant/scripts/string-literals.mjs +116 -0
- package/agents/skills/claude-code/canary-setup-harness/SKILL.md +263 -0
- package/agents/skills/claude-code/canary-shadow/SKILL.md +131 -0
- package/agents/skills/claude-code/canary-shadow/scripts/cases.example.json +32 -0
- package/agents/skills/claude-code/canary-shadow/scripts/cli.mjs +195 -0
- package/agents/skills/claude-code/canary-ship/SKILL.md +177 -0
- package/agents/skills/claude-code/canary-ship/skill.yaml +16 -0
- package/agents/skills/claude-code/canary-strix/SKILL.md +130 -0
- package/agents/skills/claude-code/canary-strix/scripts/cli.mjs +255 -0
- package/agents/skills/claude-code/canary-strix/scripts/scanner.mjs +252 -0
- package/agents/skills/claude-code/canary-strix/scripts/terms.mjs +132 -0
- package/agents/skills/claude-code/canary-test-pipeline/SKILL.md +159 -0
- package/agents/skills/claude-code/canary-test-pipeline/skill.yaml +19 -0
- package/agents/skills/claude-code/canary-test-reporter/SKILL.md +138 -0
- package/agents/skills/claude-code/canary-test-reporter/scripts/cli.mjs +98 -0
- package/agents/skills/claude-code/canary-test-reporter/scripts/json_report.mjs +58 -0
- package/agents/skills/claude-code/canary-test-reporter/scripts/parse.mjs +216 -0
- package/agents/skills/claude-code/canary-test-reporter/scripts/render.mjs +114 -0
- package/agents/skills/lib/parse-args.mjs +214 -0
- package/dist/engine/analysis/cli.js +116 -54
- package/dist/engine/analysis/engine.js +34 -16
- package/dist/engine/analysis/reports.js +5 -4
- package/dist/engine/cli-commands.js +252 -106
- package/dist/engine/cli-common.js +15 -24
- package/dist/engine/cli.core.js +37 -11
- package/dist/engine/cli.js +2 -2
- package/dist/engine/company-knowledge-cli.js +2 -2
- package/dist/engine/core/adoption.js +408 -0
- package/dist/engine/core/framework-probes.js +7 -7
- package/dist/engine/core/fs-glob.js +2 -2
- package/dist/engine/core/gate-result.js +17 -0
- package/dist/engine/core/migrator.js +9 -17
- package/dist/engine/core/pattern-matcher.js +23 -5
- package/dist/engine/core/persona.js +421 -0
- package/dist/engine/core/promotion-verdict.js +261 -0
- package/dist/engine/core/quality-scorer.js +15 -2
- package/dist/engine/core/reporter.js +1 -9
- package/dist/engine/core/skill-dispatch.js +115 -0
- package/dist/engine/core/skill-examples.js +392 -0
- package/dist/engine/core/skill-registry.js +59 -4
- package/dist/engine/core/skill-surfaces.js +307 -0
- package/dist/engine/core/static-linter.js +310 -38
- package/dist/engine/core/test-files.js +77 -0
- package/dist/engine/core/ticket-updater.js +1 -7
- package/dist/engine/core/vacuity-scanner.js +726 -0
- package/dist/engine/core/workflow-discovery.js +2 -8
- package/dist/engine/core/workspace-detect.js +7 -6
- package/dist/engine/data/personas/registry.json +36 -0
- package/dist/engine/guardian/adjudication.js +5 -5
- package/dist/engine/guardian/analysis-emit.js +19 -28
- package/dist/engine/guardian/cli.js +150 -51
- package/dist/engine/guardian/coverage.js +1 -1
- package/dist/engine/guardian/diff-coverage/heuristic-tier.js +1 -1
- package/dist/engine/guardian/diff-coverage/orchestrator.js +2 -2
- package/dist/engine/guardian/pr-check.js +322 -33
- package/dist/engine/guardian/pr-comment.js +4 -3
- package/dist/engine/history/cli.js +210 -6
- package/dist/engine/history/ndjson-store.js +9 -5
- package/dist/engine/history/record.js +34 -5
- package/dist/engine/history/run-recorder.js +165 -0
- package/dist/engine/history/schema.js +25 -7
- package/dist/engine/history/store.js +9 -0
- package/dist/engine/mcp-server.js +35 -13
- package/dist/engine/skills-cli.js +249 -23
- package/dist/engine/util/ensure-ascii.js +37 -0
- package/dist/engine/workflow-cli.js +6 -6
- package/dist/gate-result.d.ts +11 -0
- package/dist/gate-result.js +18 -0
- package/dist/reporters/testtracker.d.ts +1 -1
- package/dist/reporters/testtracker.js +1 -1
- package/dist/uninstall.js +12 -5
- package/package.json +3 -2
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Shared skill-CLI argument parser (#479).
|
|
2
|
+
//
|
|
3
|
+
// Skill CLIs are deliberately self-contained -- they import no engine code, so
|
|
4
|
+
// the runner can exec them anywhere node runs. That constraint is why five of
|
|
5
|
+
// them hand-rolled the same `parseArgs` loop, and why the same bug class came
|
|
6
|
+
// back three consecutive rounds: the pattern was copy-paste, so each new skill
|
|
7
|
+
// inherited whichever version its author happened to copy.
|
|
8
|
+
//
|
|
9
|
+
// This module is the one implementation of the four invariants that loop has to
|
|
10
|
+
// honour. It stays dependency-free ESM under `agents/skills/lib/`, which keeps
|
|
11
|
+
// the self-contained property intact -- a skill importing a sibling file in the
|
|
12
|
+
// same shipped tree is still a skill that needs nothing installed.
|
|
13
|
+
//
|
|
14
|
+
// 1. null-prototype flag lookup -- an inherited key must never resolve
|
|
15
|
+
// 2. empty-value rejection -- `--flag=` and `--flag ''` alike
|
|
16
|
+
// 3. arity checking -- `argument <flag>: expected one argument`
|
|
17
|
+
// 4. `--flag=value` -- accepted everywhere, not per-skill
|
|
18
|
+
//
|
|
19
|
+
// `test/skill-cli-conformance.test.ts` discovers every SKILL.md declaring
|
|
20
|
+
// `cli:` and asserts the module exports the `CLI_SPEC` it passed here, so a
|
|
21
|
+
// sixth hand-rolled copy fails CI instead of quietly starting the cycle again.
|
|
22
|
+
|
|
23
|
+
/** Exit code argparse reserves for usage errors; the whole family follows it. */
|
|
24
|
+
export const EXIT_USAGE = 2;
|
|
25
|
+
|
|
26
|
+
/** Well-formed integer, sign allowed -- no floats, no exponents, no 0x. */
|
|
27
|
+
const INT_RE = /^[+-]?\d+$/;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* argparse prints `prog: error: message` for usage errors. The family's
|
|
31
|
+
* comments have claimed argparse parity throughout while shipping two formats
|
|
32
|
+
* under that banner; this is the one that makes the claim true. Runtime
|
|
33
|
+
* failures (exit 1) keep the plainer `prog: message` -- argparse never owned
|
|
34
|
+
* those, so there is nothing to be faithful to.
|
|
35
|
+
*/
|
|
36
|
+
export function formatUsageError(prog, message) {
|
|
37
|
+
return `${prog}: error: ${message}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Inherited keys must not resolve, so every lookup map is null-prototype. */
|
|
41
|
+
function nullProtoMap(entries) {
|
|
42
|
+
return Object.assign(Object.create(null), entries);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build a parser from a declarative spec.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} spec
|
|
49
|
+
* @param {string} spec.prog program name used in error output
|
|
50
|
+
* @param {Record<string,string>} [spec.booleans] '--json' -> 'json'
|
|
51
|
+
* @param {Record<string,{key:string,type?:'string'|'int'}>} [spec.values]
|
|
52
|
+
* @param {Record<string,unknown>} [spec.defaults] initial option values
|
|
53
|
+
* @param {string[]} [spec.required] value flags that must be supplied
|
|
54
|
+
* @param {{key:string,defaults?:string[]}} [spec.positionals]
|
|
55
|
+
* declaring positionals also enables the `--` end-of-options terminator
|
|
56
|
+
* and a lone `-`; a CLI that takes no paths gets neither, since there is
|
|
57
|
+
* nothing for them to protect.
|
|
58
|
+
* @returns {(argv: string[]) => {opts: Record<string,unknown>, positionals: string[], help: boolean, error: string|null}}
|
|
59
|
+
*/
|
|
60
|
+
export function createParser(spec) {
|
|
61
|
+
const {
|
|
62
|
+
prog,
|
|
63
|
+
booleans = {},
|
|
64
|
+
values = {},
|
|
65
|
+
defaults = {},
|
|
66
|
+
required = [],
|
|
67
|
+
positionals = null,
|
|
68
|
+
} = spec ?? {};
|
|
69
|
+
|
|
70
|
+
if (!prog) throw new Error('createParser: spec.prog is required');
|
|
71
|
+
|
|
72
|
+
const BOOLEANS = nullProtoMap(booleans);
|
|
73
|
+
const VALUES = nullProtoMap(values);
|
|
74
|
+
|
|
75
|
+
// Fail loudly at construction on a spec that cannot be satisfied -- a rename
|
|
76
|
+
// that leaves a stale default or a required flag behind is otherwise silent.
|
|
77
|
+
const declaredKeys = new Set([
|
|
78
|
+
...Object.values(booleans),
|
|
79
|
+
...Object.values(values).map((v) => v.key),
|
|
80
|
+
]);
|
|
81
|
+
for (const key of Object.keys(defaults)) {
|
|
82
|
+
if (!declaredKeys.has(key)) {
|
|
83
|
+
throw new Error(`createParser: defaults names unknown key '${key}'`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const flag of required) {
|
|
87
|
+
if (VALUES[flag] === undefined) {
|
|
88
|
+
throw new Error(`createParser: required names undeclared flag '${flag}'`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return function parse(argv = []) {
|
|
93
|
+
const opts = Object.create(null);
|
|
94
|
+
for (const key of Object.values(booleans)) opts[key] = false;
|
|
95
|
+
for (const { key } of Object.values(values)) {
|
|
96
|
+
opts[key] = key in defaults ? defaults[key] : null;
|
|
97
|
+
}
|
|
98
|
+
Object.assign(opts, defaults);
|
|
99
|
+
|
|
100
|
+
const found = [];
|
|
101
|
+
const result = { opts, positionals: found, help: false, error: null };
|
|
102
|
+
const fail = (message) => {
|
|
103
|
+
result.error = message;
|
|
104
|
+
return result;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
let endOfOptions = false;
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
110
|
+
const arg = argv[i];
|
|
111
|
+
|
|
112
|
+
if (endOfOptions) {
|
|
113
|
+
found.push(arg);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Help short-circuits everything, including the required-flag check
|
|
118
|
+
// below -- otherwise `--help` reports the arguments it is being asked to
|
|
119
|
+
// explain as missing.
|
|
120
|
+
if (arg === '-h' || arg === '--help') {
|
|
121
|
+
result.help = true;
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (arg === '--') {
|
|
126
|
+
if (!positionals) return fail('unrecognized arguments: --');
|
|
127
|
+
endOfOptions = true;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Split `--flag=value` once, up front, so both spellings share one path.
|
|
132
|
+
const eq = arg.startsWith('--') ? arg.indexOf('=') : -1;
|
|
133
|
+
const flag = eq === -1 ? arg : arg.slice(0, eq);
|
|
134
|
+
const inline = eq === -1 ? null : arg.slice(eq + 1);
|
|
135
|
+
|
|
136
|
+
if (BOOLEANS[flag] !== undefined && inline === null) {
|
|
137
|
+
opts[BOOLEANS[flag]] = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const def = VALUES[flag];
|
|
142
|
+
if (def !== undefined) {
|
|
143
|
+
let raw;
|
|
144
|
+
if (inline !== null) {
|
|
145
|
+
raw = inline;
|
|
146
|
+
} else {
|
|
147
|
+
const next = argv[i + 1];
|
|
148
|
+
// A leading '-' normally means "the next flag, not my value" -- but a
|
|
149
|
+
// well-formed integer is a legitimate value for an int flag, so
|
|
150
|
+
// `--seed -5` and `--seed=-5` stay the same command.
|
|
151
|
+
const looksLikeFlag =
|
|
152
|
+
next !== undefined &&
|
|
153
|
+
next.startsWith('-') &&
|
|
154
|
+
!(def.type === 'int' && INT_RE.test(next));
|
|
155
|
+
if (next === undefined || looksLikeFlag) {
|
|
156
|
+
return fail(`argument ${flag}: expected one argument`);
|
|
157
|
+
}
|
|
158
|
+
raw = next;
|
|
159
|
+
i += 1;
|
|
160
|
+
}
|
|
161
|
+
// Empty is the missing-value case wearing a disguise. `--repo=` is
|
|
162
|
+
// typed by nobody, but `--repo "$UNSET_VAR"` expands to `--repo ''` in
|
|
163
|
+
// any shell, and an accepted empty path silently retargets writes at
|
|
164
|
+
// the process CWD.
|
|
165
|
+
if (raw === '') return fail(`argument ${flag}: expected one argument`);
|
|
166
|
+
if (def.type === 'int') {
|
|
167
|
+
// Validate the VALUE, not just its presence: a flag whose purpose is
|
|
168
|
+
// determinism must not decay to a default when its value is junk.
|
|
169
|
+
if (!INT_RE.test(raw)) {
|
|
170
|
+
return fail(`argument ${flag}: invalid int value: '${raw}'`);
|
|
171
|
+
}
|
|
172
|
+
// Syntactically an integer is not enough: Number() silently rounds
|
|
173
|
+
// past 2^53-1, so `--seed 9007199254740993` would RUN with ...992 --
|
|
174
|
+
// the value used differing from the value asked for, which is the
|
|
175
|
+
// exact class of lie a determinism flag must not tell.
|
|
176
|
+
const parsed = Number(raw);
|
|
177
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
178
|
+
return fail(
|
|
179
|
+
`argument ${flag}: integer out of safe range: '${raw}'`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
opts[def.key] = parsed;
|
|
183
|
+
} else {
|
|
184
|
+
opts[def.key] = raw;
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// A lone `-` is a positional, as argparse treats it.
|
|
190
|
+
if (positionals && (arg === '-' || !arg.startsWith('-'))) {
|
|
191
|
+
found.push(arg);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return fail(`unrecognized arguments: ${arg}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const missing = required.filter((flag) => {
|
|
199
|
+
const value = opts[VALUES[flag].key];
|
|
200
|
+
return value === null || value === undefined;
|
|
201
|
+
});
|
|
202
|
+
if (missing.length) {
|
|
203
|
+
return fail(
|
|
204
|
+
`the following arguments are required: ${missing.join(', ')}`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (positionals && !found.length && positionals.defaults) {
|
|
209
|
+
found.push(...positionals.defaults);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return result;
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
* - `json.dumps(x, indent=2)` -> {@link jsonIndent2} (byte-exact + ensure_ascii).
|
|
14
14
|
* - The report builders are byte-exact ports (Markdown), so the human-readable
|
|
15
15
|
* paths match the oracle exactly.
|
|
16
|
-
* -
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* store.
|
|
16
|
+
* - `--db-url` / `CANARY_HISTORY_DB_URL` are HONOURED as of #711 (ADR 0013
|
|
17
|
+
* Decision 4): the engine is async, so analyze selects its backend through
|
|
18
|
+
* the shared `makeStore` factory like every other history consumer. Three
|
|
19
|
+
* reports (spikes, common-failures, regression-candidates) are computed by
|
|
20
|
+
* walking raw run records, which only the local backend exposes; against a
|
|
21
|
+
* remote backend each names itself as unverifiable instead of rendering an
|
|
22
|
+
* empty report. No Python analyze test exercised a remote store.
|
|
23
23
|
* - `area-health` accepts `--json` but ignores it -- faithful to the Python
|
|
24
24
|
* command, which never branches on `output_json`.
|
|
25
25
|
*/
|
|
@@ -29,29 +29,22 @@ import { Command, InvalidArgumentError, Option } from 'commander';
|
|
|
29
29
|
import { jsonIndent2, normalizeUsageExit } from '../cli-common.js';
|
|
30
30
|
import { gateOutcome } from '../core/gate-result.js';
|
|
31
31
|
import { AnalysisEngine } from './engine.js';
|
|
32
|
-
import { buildCommonFailuresReport,
|
|
33
|
-
import {
|
|
32
|
+
import { buildCommonFailuresReport, buildFlakyTestsReport, buildRegressionCandidatesReport, buildFailureSpikesReport, } from './reports.js';
|
|
33
|
+
import { makeStore } from '../history/store.js';
|
|
34
34
|
const DEFAULT_HISTORY_PATH = 'test-results/reports/history-v2.jsonl';
|
|
35
35
|
/** Process-backed defaults for production. */
|
|
36
36
|
export function defaultAnalyzeDeps() {
|
|
37
|
-
const err = (s) => {
|
|
38
|
-
process.stderr.write(`${s}\n`);
|
|
39
|
-
};
|
|
40
37
|
return {
|
|
41
38
|
out: (s) => process.stdout.write(`${s}\n`),
|
|
42
|
-
err,
|
|
39
|
+
err: (s) => process.stderr.write(`${s}\n`),
|
|
43
40
|
env: process.env,
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
err('note: --db-url is ignored by analyze; it reads local NDJSON only.');
|
|
52
|
-
}
|
|
53
|
-
return new NdjsonHistoryStore(DEFAULT_HISTORY_PATH);
|
|
54
|
-
},
|
|
41
|
+
// #711: analyze now goes through the shared factory, so --db-url and
|
|
42
|
+
// CANARY_HISTORY_DB_URL select the backend for real. Until the engine went
|
|
43
|
+
// async this could not be done -- the engine held the synchronous store
|
|
44
|
+
// contract -- and the flag was accepted with a printed apology instead.
|
|
45
|
+
// Where a remote backend cannot answer a given section, the command says so
|
|
46
|
+
// by name (see `cannotVerifyRawRecords`) rather than rendering an empty one.
|
|
47
|
+
makeStore: (dbUrl) => makeStore(dbUrl, DEFAULT_HISTORY_PATH),
|
|
55
48
|
};
|
|
56
49
|
}
|
|
57
50
|
/**
|
|
@@ -72,8 +65,14 @@ export function defaultAnalyzeDeps() {
|
|
|
72
65
|
*
|
|
73
66
|
* Returns true when the caller should stop (the store was empty).
|
|
74
67
|
*/
|
|
75
|
-
function abstainOnEmptyHistory(store, deps, json, what) {
|
|
76
|
-
|
|
68
|
+
async function abstainOnEmptyHistory(store, deps, json, what) {
|
|
69
|
+
// #711: `countRuns` is an OPTIONAL capability, and a backend that cannot
|
|
70
|
+
// report its denominator has an UNKNOWN one, not a zero one (ADR 0013
|
|
71
|
+
// Decision 3). Abstaining here would fire on every remote query and mute the
|
|
72
|
+
// doctrine -- the katana lesson again, from the other direction.
|
|
73
|
+
if (!store.countRuns)
|
|
74
|
+
return false;
|
|
75
|
+
if ((await store.countRuns()) > 0)
|
|
77
76
|
return false;
|
|
78
77
|
const outcome = gateOutcome({ checked: 0, findings: [] }, 'advisory');
|
|
79
78
|
const notice = `${outcome.summaryLine} No run history to analyze, so "${what}" is ` +
|
|
@@ -89,6 +88,45 @@ function abstainOnEmptyHistory(store, deps, json, what) {
|
|
|
89
88
|
}
|
|
90
89
|
return true;
|
|
91
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The capability guard for the sections built on raw run records (#711).
|
|
93
|
+
*
|
|
94
|
+
* `spikes`, `common-failures` and `regression-candidates` are computed by
|
|
95
|
+
* walking whole run records rather than by any aggregate query, so they exist
|
|
96
|
+
* only where the backend implements the optional `readAll()`. The local NDJSON
|
|
97
|
+
* store does; the remote Supabase store does not.
|
|
98
|
+
*
|
|
99
|
+
* Against a backend without it these commands used to emit an empty report,
|
|
100
|
+
* which is indistinguishable on screen from a measured all-clear — the exact
|
|
101
|
+
* false-green shape #508 exists to kill, and one that only became reachable
|
|
102
|
+
* once `--db-url` started being honoured. So the command names the section it
|
|
103
|
+
* could not compute, and names the ones that DO work against this backend so
|
|
104
|
+
* the answer is actionable rather than just a refusal.
|
|
105
|
+
*
|
|
106
|
+
* Advisory, matching `abstainOnEmptyHistory`: the exit stays 0, the human path
|
|
107
|
+
* gets the notice instead of the report, and `--json` keeps stdout a parseable
|
|
108
|
+
* empty array with the notice on stderr.
|
|
109
|
+
*
|
|
110
|
+
* Returns true when the caller should stop.
|
|
111
|
+
*/
|
|
112
|
+
function cannotVerifyRawRecords(store, deps, json, what) {
|
|
113
|
+
if (store.readAll)
|
|
114
|
+
return false;
|
|
115
|
+
const outcome = gateOutcome({ checked: 0, findings: [] }, 'advisory');
|
|
116
|
+
const notice = `${outcome.summaryLine} cannot verify: this history backend does not ` +
|
|
117
|
+
`expose raw run records, so "${what}" is UNKNOWN rather than clean. ` +
|
|
118
|
+
`\`analyze flaky\` works against this backend today; drop --db-url ` +
|
|
119
|
+
`(and CANARY_HISTORY_DB_URL) to analyze the local ${DEFAULT_HISTORY_PATH} ` +
|
|
120
|
+
`instead.`;
|
|
121
|
+
if (json) {
|
|
122
|
+
deps.out(jsonIndent2([]));
|
|
123
|
+
deps.err(notice);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
deps.out(notice);
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
92
130
|
// --- unit-bearing flags (#673) -----------------------------------------------
|
|
93
131
|
//
|
|
94
132
|
// #670 put the unit in the NAME one layer down -- `analysis/reports.ts` takes
|
|
@@ -190,26 +228,29 @@ function writeArtifacts(artifacts, output) {
|
|
|
190
228
|
writeFileSync(join(output, name), content, 'utf-8');
|
|
191
229
|
}
|
|
192
230
|
}
|
|
193
|
-
function flakyCmd(opts, deps) {
|
|
231
|
+
async function flakyCmd(opts, deps) {
|
|
194
232
|
const store = deps.makeStore(opts.dbUrl);
|
|
195
|
-
if (abstainOnEmptyHistory(store, deps, opts.json === true, 'flake rate')) {
|
|
233
|
+
if (await abstainOnEmptyHistory(store, deps, opts.json === true, 'flake rate')) {
|
|
196
234
|
return;
|
|
197
235
|
}
|
|
198
|
-
const rows = store.queryFlaky(opts.windowRuns, opts.suite ?? null, opts.minRatePct);
|
|
236
|
+
const rows = await store.queryFlaky(opts.windowRuns, opts.suite ?? null, opts.minRatePct);
|
|
199
237
|
if (opts.json) {
|
|
200
238
|
deps.out(jsonIndent2(rows));
|
|
201
239
|
}
|
|
202
240
|
else {
|
|
203
|
-
deps.out(
|
|
241
|
+
deps.out(buildFlakyTestsReport(rows, opts.windowRuns, opts.minRatePct));
|
|
204
242
|
}
|
|
205
243
|
}
|
|
206
|
-
function spikesCmd(opts, deps) {
|
|
244
|
+
async function spikesCmd(opts, deps) {
|
|
207
245
|
const store = deps.makeStore(opts.dbUrl);
|
|
208
|
-
if (abstainOnEmptyHistory(store, deps, opts.json === true, 'failure spikes')) {
|
|
246
|
+
if (await abstainOnEmptyHistory(store, deps, opts.json === true, 'failure spikes')) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (cannotVerifyRawRecords(store, deps, opts.json === true, 'failure spikes')) {
|
|
209
250
|
return;
|
|
210
251
|
}
|
|
211
252
|
const rows = [];
|
|
212
|
-
for (const r of store.readAll()) {
|
|
253
|
+
for (const r of await store.readAll()) {
|
|
213
254
|
if (opts.since && (r.timestamp ?? '') < opts.since)
|
|
214
255
|
continue;
|
|
215
256
|
rows.push({
|
|
@@ -225,7 +266,7 @@ function spikesCmd(opts, deps) {
|
|
|
225
266
|
deps.out(jsonIndent2(rows));
|
|
226
267
|
}
|
|
227
268
|
else {
|
|
228
|
-
deps.out(
|
|
269
|
+
deps.out(buildFailureSpikesReport(rows, opts.deltaPp));
|
|
229
270
|
}
|
|
230
271
|
}
|
|
231
272
|
function areaHealthCmd(opts, deps) {
|
|
@@ -243,13 +284,16 @@ function areaHealthCmd(opts, deps) {
|
|
|
243
284
|
`result here would be a fiction. Use \`analyze digest\` for the reports ` +
|
|
244
285
|
`that are wired, and track the area-health row set as unimplemented.`);
|
|
245
286
|
}
|
|
246
|
-
function commonFailuresCmd(opts, deps) {
|
|
287
|
+
async function commonFailuresCmd(opts, deps) {
|
|
247
288
|
const store = deps.makeStore(opts.dbUrl);
|
|
248
|
-
if (abstainOnEmptyHistory(store, deps, opts.json === true, 'common failures')) {
|
|
289
|
+
if (await abstainOnEmptyHistory(store, deps, opts.json === true, 'common failures')) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (cannotVerifyRawRecords(store, deps, opts.json === true, 'common failures')) {
|
|
249
293
|
return;
|
|
250
294
|
}
|
|
251
295
|
const rows = [];
|
|
252
|
-
for (const record of store.readAll()) {
|
|
296
|
+
for (const record of await store.readAll()) {
|
|
253
297
|
if (opts.since && (record.timestamp ?? '') < opts.since)
|
|
254
298
|
continue;
|
|
255
299
|
for (const t of record.tests ?? []) {
|
|
@@ -271,13 +315,16 @@ function commonFailuresCmd(opts, deps) {
|
|
|
271
315
|
deps.out(buildCommonFailuresReport(rows, opts.minSuites));
|
|
272
316
|
}
|
|
273
317
|
}
|
|
274
|
-
function regressionCandidatesCmd(opts, deps) {
|
|
318
|
+
async function regressionCandidatesCmd(opts, deps) {
|
|
275
319
|
const store = deps.makeStore(opts.dbUrl);
|
|
276
|
-
if (abstainOnEmptyHistory(store, deps, opts.json === true, 'regression candidates')) {
|
|
320
|
+
if (await abstainOnEmptyHistory(store, deps, opts.json === true, 'regression candidates')) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (cannotVerifyRawRecords(store, deps, opts.json === true, 'regression candidates')) {
|
|
277
324
|
return;
|
|
278
325
|
}
|
|
279
326
|
const engine = new AnalysisEngine(store);
|
|
280
|
-
const candidates = engine.detectRegressionCandidates(null, opts.minGreen, opts.recentFailures);
|
|
327
|
+
const candidates = await engine.detectRegressionCandidates(null, opts.minGreen, opts.recentFailures);
|
|
281
328
|
if (opts.json) {
|
|
282
329
|
deps.out(jsonIndent2(candidates));
|
|
283
330
|
}
|
|
@@ -285,13 +332,13 @@ function regressionCandidatesCmd(opts, deps) {
|
|
|
285
332
|
deps.out(buildRegressionCandidatesReport(candidates));
|
|
286
333
|
}
|
|
287
334
|
}
|
|
288
|
-
function digestCmd(opts, deps) {
|
|
335
|
+
async function digestCmd(opts, deps) {
|
|
289
336
|
const store = deps.makeStore(opts.dbUrl);
|
|
290
|
-
if (abstainOnEmptyHistory(store, deps, opts.json === true, 'fleet health')) {
|
|
337
|
+
if (await abstainOnEmptyHistory(store, deps, opts.json === true, 'fleet health')) {
|
|
291
338
|
return;
|
|
292
339
|
}
|
|
293
340
|
const engine = new AnalysisEngine(store);
|
|
294
|
-
const result = engine.run({
|
|
341
|
+
const result = await engine.run({
|
|
295
342
|
windowRuns: opts.windowRuns,
|
|
296
343
|
deltaPp: opts.deltaPp,
|
|
297
344
|
weeks: opts.weeks,
|
|
@@ -299,6 +346,19 @@ function digestCmd(opts, deps) {
|
|
|
299
346
|
suite: opts.suite ?? null,
|
|
300
347
|
});
|
|
301
348
|
writeArtifacts(result.artifacts, opts.output);
|
|
349
|
+
// Unlike the single-section commands, digest can partially succeed: the flaky
|
|
350
|
+
// leaderboard rides a contract method and is real even on a backend with no
|
|
351
|
+
// raw-record access. So it renders what it measured and names what it could
|
|
352
|
+
// not -- a digest silently missing three of its five sections would be the
|
|
353
|
+
// worst of both worlds. The notice rides stderr so --json and the Slack path
|
|
354
|
+
// stay byte-clean for their consumers.
|
|
355
|
+
if (result.degraded.length > 0) {
|
|
356
|
+
const outcome = gateOutcome({ checked: 0, findings: [] }, 'advisory');
|
|
357
|
+
deps.err(`${outcome.summaryLine} cannot verify: this history backend does not ` +
|
|
358
|
+
`expose raw run records, so ${result.degraded.join(', ')} ` +
|
|
359
|
+
`${result.degraded.length === 1 ? 'is' : 'are'} UNKNOWN rather than ` +
|
|
360
|
+
`clean in this digest. The flake leaderboard above is measured.`);
|
|
361
|
+
}
|
|
302
362
|
if (opts.json) {
|
|
303
363
|
deps.out(jsonIndent2({
|
|
304
364
|
flaky_count: result.flaky.length,
|
|
@@ -328,7 +388,9 @@ function printSlack(flakyCount, regCount, deps) {
|
|
|
328
388
|
// description: a blank one is how the silent unit survived this long, since the
|
|
329
389
|
// value's meaning lived only in the report it eventually printed.
|
|
330
390
|
const DB_URL_ENV = 'CANARY_HISTORY_DB_URL';
|
|
331
|
-
const DB_URL_DESC = 'History store URL
|
|
391
|
+
const DB_URL_DESC = 'History store URL. Defaults to the local NDJSON store; a remote backend ' +
|
|
392
|
+
'cannot answer the spikes, common-failures or regression-candidates ' +
|
|
393
|
+
'reports, which say so rather than reporting zero.';
|
|
332
394
|
const JSON_DESC = 'Emit the rows as JSON instead of a Markdown report.';
|
|
333
395
|
const WINDOW_RUNS_DESC = 'Rolling window measured in RUNS, not days.';
|
|
334
396
|
const MIN_RATE_PCT_DESC = 'Minimum flake rate to report, in PERCENT 0-100 (10 means 10%, not 0.1).';
|
|
@@ -355,8 +417,8 @@ export function createAnalyzeCommand(depsInit = {}) {
|
|
|
355
417
|
.addOption(new Option('--min-rate <percent>', 'Deprecated alias for --min-rate-pct.').argParser(parsePercentScale('--min-rate', 'percent')))
|
|
356
418
|
.addOption(new Option('--db-url <url>', DB_URL_DESC).env(DB_URL_ENV))
|
|
357
419
|
.option('--json', JSON_DESC)
|
|
358
|
-
.action((opts, cmd) => {
|
|
359
|
-
flakyCmd(resolveUnitFlags(opts, cmd, deps, [WINDOW_ALIAS, MIN_RATE_ALIAS]), deps);
|
|
420
|
+
.action(async (opts, cmd) => {
|
|
421
|
+
await flakyCmd(resolveUnitFlags(opts, cmd, deps, [WINDOW_ALIAS, MIN_RATE_ALIAS]), deps);
|
|
360
422
|
});
|
|
361
423
|
program
|
|
362
424
|
.command('spikes')
|
|
@@ -368,8 +430,8 @@ export function createAnalyzeCommand(depsInit = {}) {
|
|
|
368
430
|
.addOption(new Option('--delta <points>', 'Deprecated alias for --delta-pp.').argParser(parsePercentScale('--delta', 'percentage points')))
|
|
369
431
|
.addOption(new Option('--db-url <url>', DB_URL_DESC).env(DB_URL_ENV))
|
|
370
432
|
.option('--json', JSON_DESC)
|
|
371
|
-
.action((opts, cmd) => {
|
|
372
|
-
spikesCmd(resolveUnitFlags(opts, cmd, deps, [DELTA_ALIAS]), deps);
|
|
433
|
+
.action(async (opts, cmd) => {
|
|
434
|
+
await spikesCmd(resolveUnitFlags(opts, cmd, deps, [DELTA_ALIAS]), deps);
|
|
373
435
|
});
|
|
374
436
|
program
|
|
375
437
|
.command('area-health')
|
|
@@ -391,8 +453,8 @@ export function createAnalyzeCommand(depsInit = {}) {
|
|
|
391
453
|
.argParser((v) => Number.parseInt(v, 10)))
|
|
392
454
|
.addOption(new Option('--db-url <url>', DB_URL_DESC).env(DB_URL_ENV))
|
|
393
455
|
.option('--json', JSON_DESC)
|
|
394
|
-
.action((opts) => {
|
|
395
|
-
commonFailuresCmd(opts, deps);
|
|
456
|
+
.action(async (opts) => {
|
|
457
|
+
await commonFailuresCmd(opts, deps);
|
|
396
458
|
});
|
|
397
459
|
program
|
|
398
460
|
.command('regression-candidates')
|
|
@@ -405,8 +467,8 @@ export function createAnalyzeCommand(depsInit = {}) {
|
|
|
405
467
|
.argParser((v) => Number.parseInt(v, 10)))
|
|
406
468
|
.addOption(new Option('--db-url <url>', DB_URL_DESC).env(DB_URL_ENV))
|
|
407
469
|
.option('--json', JSON_DESC)
|
|
408
|
-
.action((opts) => {
|
|
409
|
-
regressionCandidatesCmd(opts, deps);
|
|
470
|
+
.action(async (opts) => {
|
|
471
|
+
await regressionCandidatesCmd(opts, deps);
|
|
410
472
|
});
|
|
411
473
|
program
|
|
412
474
|
.command('digest')
|
|
@@ -430,8 +492,8 @@ export function createAnalyzeCommand(depsInit = {}) {
|
|
|
430
492
|
.option('--json', 'Emit per-section counts as JSON instead of the digest.')
|
|
431
493
|
.option('--slack', 'Emit a short Slack-formatted summary.')
|
|
432
494
|
.addOption(new Option('--db-url <url>', DB_URL_DESC).env(DB_URL_ENV))
|
|
433
|
-
.action((opts, cmd) => {
|
|
434
|
-
digestCmd(resolveUnitFlags(opts, cmd, deps, [WINDOW_ALIAS, DELTA_ALIAS]), deps);
|
|
495
|
+
.action(async (opts, cmd) => {
|
|
496
|
+
await digestCmd(resolveUnitFlags(opts, cmd, deps, [WINDOW_ALIAS, DELTA_ALIAS]), deps);
|
|
435
497
|
});
|
|
436
498
|
for (const sub of program.commands) {
|
|
437
499
|
sub.exitOverride(normalizeUsageExit);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `run()` (the Python engine initialises `area_rows = []` and never appends), so
|
|
10
10
|
* the area-health artifact always renders the empty-data message.
|
|
11
11
|
*/
|
|
12
|
-
import { buildAreaHealthReport, buildCommonFailuresReport, buildDigest,
|
|
12
|
+
import { buildAreaHealthReport, buildCommonFailuresReport, buildDigest, buildFlakyTestsReport, buildRegressionCandidatesReport, buildFailureSpikesReport, } from './reports.js';
|
|
13
13
|
import { detectRegressions } from '../history/detector.js';
|
|
14
14
|
import { def } from '../util/coalesce.js';
|
|
15
15
|
function isReadable(store) {
|
|
@@ -26,12 +26,16 @@ function toCommonFailureRow(record, t) {
|
|
|
26
26
|
error_text: t.error_text,
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
/** Report-language names for the sections that need raw-record access. */
|
|
30
|
+
const SECTION_SPIKES = 'failure spikes';
|
|
31
|
+
const SECTION_COMMON_FAILURES = 'common failures';
|
|
32
|
+
const SECTION_REGRESSIONS = 'regression candidates';
|
|
29
33
|
export class AnalysisEngine {
|
|
30
34
|
store;
|
|
31
35
|
constructor(store) {
|
|
32
36
|
this.store = store;
|
|
33
37
|
}
|
|
34
|
-
run(opts = {}) {
|
|
38
|
+
async run(opts = {}) {
|
|
35
39
|
const windowRuns = opts.windowRuns ?? 30;
|
|
36
40
|
const deltaPp = opts.deltaPp ?? 20.0;
|
|
37
41
|
const weeks = opts.weeks ?? 4;
|
|
@@ -40,11 +44,20 @@ export class AnalysisEngine {
|
|
|
40
44
|
const minGreen = opts.minGreen ?? 5;
|
|
41
45
|
const recentFailures = opts.recentFailures ?? 3;
|
|
42
46
|
const suite = opts.suite ?? null;
|
|
43
|
-
const flaky = this.store.queryFlaky(windowRuns, suite, minFlakeRatePct);
|
|
44
|
-
|
|
47
|
+
const flaky = (await this.store.queryFlaky(windowRuns, suite, minFlakeRatePct));
|
|
48
|
+
// Sections that need raw-record access are UNKNOWN, not empty, on a backend
|
|
49
|
+
// that does not offer it (#711). `flaky` above and a suite-scoped `spikes`
|
|
50
|
+
// below ride real contract methods, so they stay measured either way.
|
|
51
|
+
const degraded = [];
|
|
52
|
+
const readable = isReadable(this.store);
|
|
53
|
+
const suitesToQuery = suite ? [suite] : await this.discoverSuites();
|
|
54
|
+
// With no explicit --suite, the suite list itself comes from raw records —
|
|
55
|
+
// so spikes is only degraded in that case, not whenever readAll is absent.
|
|
56
|
+
if (!readable && !suite)
|
|
57
|
+
degraded.push(SECTION_SPIKES);
|
|
45
58
|
const spikesRows = [];
|
|
46
59
|
for (const s of suitesToQuery) {
|
|
47
|
-
const summary = this.store.querySummary(s, windowRuns * 2);
|
|
60
|
+
const summary = await this.store.querySummary(s, windowRuns * 2);
|
|
48
61
|
for (const row of def(summary.runs, [])) {
|
|
49
62
|
// query_summary rows omit suite; the spikes builder groups by it, so
|
|
50
63
|
// tag each pooled row with the suite it came from (matches Python).
|
|
@@ -59,8 +72,12 @@ export class AnalysisEngine {
|
|
|
59
72
|
}
|
|
60
73
|
// Faithful to Python: area rows are never populated by run().
|
|
61
74
|
const areaRows = [];
|
|
62
|
-
const commonRows = this.queryCommonFailures(suite);
|
|
63
|
-
|
|
75
|
+
const commonRows = await this.queryCommonFailures(suite);
|
|
76
|
+
if (!readable)
|
|
77
|
+
degraded.push(SECTION_COMMON_FAILURES);
|
|
78
|
+
const regressionCandidates = await this.detectRegressionCandidates(suite, minGreen, recentFailures);
|
|
79
|
+
if (!readable)
|
|
80
|
+
degraded.push(SECTION_REGRESSIONS);
|
|
64
81
|
const digest = buildDigest({
|
|
65
82
|
flaky,
|
|
66
83
|
spikes: spikesRows,
|
|
@@ -73,8 +90,8 @@ export class AnalysisEngine {
|
|
|
73
90
|
minSuites,
|
|
74
91
|
});
|
|
75
92
|
const artifacts = {
|
|
76
|
-
'flaky.md':
|
|
77
|
-
'spikes.md':
|
|
93
|
+
'flaky.md': buildFlakyTestsReport(flaky, windowRuns, minFlakeRatePct),
|
|
94
|
+
'spikes.md': buildFailureSpikesReport(spikesRows, deltaPp),
|
|
78
95
|
'area-health.md': buildAreaHealthReport(areaRows, weeks),
|
|
79
96
|
'common-failures.md': buildCommonFailuresReport(commonRows, minSuites),
|
|
80
97
|
'regression-candidates.md': buildRegressionCandidatesReport(regressionCandidates),
|
|
@@ -88,23 +105,24 @@ export class AnalysisEngine {
|
|
|
88
105
|
regressionCandidates,
|
|
89
106
|
digestMd: digest,
|
|
90
107
|
artifacts,
|
|
108
|
+
degraded,
|
|
91
109
|
};
|
|
92
110
|
}
|
|
93
|
-
discoverSuites() {
|
|
111
|
+
async discoverSuites() {
|
|
94
112
|
if (!isReadable(this.store))
|
|
95
113
|
return [];
|
|
96
114
|
const suites = new Set();
|
|
97
|
-
for (const r of this.store.readAll()) {
|
|
115
|
+
for (const r of await this.store.readAll()) {
|
|
98
116
|
if (r.suite)
|
|
99
117
|
suites.add(r.suite);
|
|
100
118
|
}
|
|
101
119
|
return [...suites];
|
|
102
120
|
}
|
|
103
|
-
queryCommonFailures(suite) {
|
|
121
|
+
async queryCommonFailures(suite) {
|
|
104
122
|
if (!isReadable(this.store))
|
|
105
123
|
return [];
|
|
106
124
|
const rows = [];
|
|
107
|
-
for (const record of this.store.readAll()) {
|
|
125
|
+
for (const record of await this.store.readAll()) {
|
|
108
126
|
if (suite && record.suite !== suite)
|
|
109
127
|
continue;
|
|
110
128
|
for (const t of def(record.tests, [])) {
|
|
@@ -114,11 +132,11 @@ export class AnalysisEngine {
|
|
|
114
132
|
}
|
|
115
133
|
return rows;
|
|
116
134
|
}
|
|
117
|
-
detectRegressionCandidates(suite, minGreen, recentFailures) {
|
|
135
|
+
async detectRegressionCandidates(suite, minGreen, recentFailures) {
|
|
118
136
|
if (!isReadable(this.store))
|
|
119
137
|
return [];
|
|
120
138
|
const testNames = new Set();
|
|
121
|
-
for (const record of this.store.readAll()) {
|
|
139
|
+
for (const record of await this.store.readAll()) {
|
|
122
140
|
if (suite && record.suite !== suite)
|
|
123
141
|
continue;
|
|
124
142
|
for (const t of def(record.tests, []))
|
|
@@ -126,7 +144,7 @@ export class AnalysisEngine {
|
|
|
126
144
|
}
|
|
127
145
|
const candidates = [];
|
|
128
146
|
for (const name of testNames) {
|
|
129
|
-
const timeline = this.store.queryTimeline(name);
|
|
147
|
+
const timeline = await this.store.queryTimeline(name);
|
|
130
148
|
const result = detectRegressions(timeline, minGreen, recentFailures);
|
|
131
149
|
if (result.is_regression) {
|
|
132
150
|
candidates.push({
|