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,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// canary-fail-fast -- surface test failures fast and loud. Ported behavior-for-
|
|
3
|
+
// behavior from the Python original.
|
|
4
|
+
//
|
|
5
|
+
// Two halves:
|
|
6
|
+
// --config <playwright.config.*> audit fail-fast knobs (maxFailures/forbidOnly/
|
|
7
|
+
// retries); print recommendations (read-only).
|
|
8
|
+
// --results <playwright.json> print a loud, categorized failure digest to
|
|
9
|
+
// the CI log + ::error annotations; exit non-
|
|
10
|
+
// zero on any failure so the step fails.
|
|
11
|
+
//
|
|
12
|
+
// At least one of --config / --results is required. Self-contained -- no
|
|
13
|
+
// external skill dependency.
|
|
14
|
+
//
|
|
15
|
+
// Invoked via `canary skills run canary-fail-fast -- --results <json> [--config <path>]`.
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
createParser,
|
|
21
|
+
formatUsageError,
|
|
22
|
+
EXIT_USAGE,
|
|
23
|
+
} from '../../../lib/parse-args.mjs';
|
|
24
|
+
import { parseFailures } from './parse.mjs';
|
|
25
|
+
import { buildDigest } from './digest.mjs';
|
|
26
|
+
import { checkConfig } from './fastfail_check.mjs';
|
|
27
|
+
|
|
28
|
+
const PREFIX = 'canary-fail-fast:';
|
|
29
|
+
const DASH = '\u2014'; // em dash (see digest.mjs)
|
|
30
|
+
|
|
31
|
+
const USAGE =
|
|
32
|
+
'usage: canary-fail-fast [-h] [--results PATH] [--config PATH]\n' +
|
|
33
|
+
'\n' +
|
|
34
|
+
'Fail-fast config audit + loud run-end failure digest.';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Both value flags are optional here -- main enforces "at least one of
|
|
38
|
+
* --results / --config" itself, because argparse has no vocabulary for that.
|
|
39
|
+
* The four shared invariants live in the shared parser (#479).
|
|
40
|
+
*/
|
|
41
|
+
export const CLI_SPEC = {
|
|
42
|
+
prog: 'canary-fail-fast',
|
|
43
|
+
values: {
|
|
44
|
+
'--results': { key: 'results' },
|
|
45
|
+
'--config': { key: 'config' },
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const parseArgs = createParser(CLI_SPEC);
|
|
50
|
+
|
|
51
|
+
export function main(argv = []) {
|
|
52
|
+
const { opts: args, help, error } = parseArgs(argv);
|
|
53
|
+
|
|
54
|
+
if (help) {
|
|
55
|
+
console.log(USAGE);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
if (error) {
|
|
59
|
+
console.error(formatUsageError(CLI_SPEC.prog, error));
|
|
60
|
+
return EXIT_USAGE;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!args.results && !args.config) {
|
|
64
|
+
console.error(
|
|
65
|
+
`${PREFIX} nothing to do ${DASH} pass --results and/or --config.`,
|
|
66
|
+
);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---- config audit -----------------------------------------------------
|
|
71
|
+
if (args.config) {
|
|
72
|
+
let text;
|
|
73
|
+
try {
|
|
74
|
+
text = fs.readFileSync(args.config, 'utf8');
|
|
75
|
+
} catch (exc) {
|
|
76
|
+
console.error(`${PREFIX} cannot read config: ${exc.message}`);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
const recs = checkConfig(text);
|
|
80
|
+
if (recs.length) {
|
|
81
|
+
console.log('Fail-fast config recommendations:');
|
|
82
|
+
for (const r of recs) console.log(` - ${r}`);
|
|
83
|
+
} else {
|
|
84
|
+
console.log('Fail-fast config OK.');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---- failure digest ---------------------------------------------------
|
|
89
|
+
let exitCode = 0;
|
|
90
|
+
if (args.results) {
|
|
91
|
+
if (!fs.existsSync(args.results)) {
|
|
92
|
+
console.error(`${PREFIX} results file not found: ${args.results}`);
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
let failures;
|
|
96
|
+
try {
|
|
97
|
+
failures = parseFailures(args.results);
|
|
98
|
+
} catch (exc) {
|
|
99
|
+
console.error(`${PREFIX} ${exc.message}`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
const d = buildDigest(failures);
|
|
103
|
+
console.log(d.text);
|
|
104
|
+
for (const ann of d.annotations) console.log(ann);
|
|
105
|
+
exitCode = d.exitCode;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return exitCode;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Direct execution (the skill runner execs this file via its shebang).
|
|
112
|
+
//
|
|
113
|
+
// `process.exitCode`, not `process.exit()`: a large `--json` payload exceeds
|
|
114
|
+
// the pipe buffer, and `process.exit` tears the process down mid-write, leaving
|
|
115
|
+
// truncated JSON that still exits 0 (#791).
|
|
116
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
117
|
+
process.exitCode = main(process.argv.slice(2));
|
|
118
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// digest -- loud, categorized failure digest (pure). Ported behavior-for-
|
|
2
|
+
// behavior from the Python original.
|
|
3
|
+
//
|
|
4
|
+
// Turns a list of failures into a terse CI-log digest + `::error` workflow
|
|
5
|
+
// annotations + a non-zero exit code, so an engineer triages from the run log
|
|
6
|
+
// without opening the HTML report.
|
|
7
|
+
|
|
8
|
+
import { FAILURE_CATEGORIES, categorizeFailure } from './failures.mjs';
|
|
9
|
+
|
|
10
|
+
// Output glyphs kept as \u escapes so the source stays ASCII while the emitted
|
|
11
|
+
// text is byte-identical to the Python original.
|
|
12
|
+
const CHECK = '\u2705'; // white heavy check mark
|
|
13
|
+
const CROSS = '\u274c'; // cross mark
|
|
14
|
+
const DASH = '\u2014'; // em dash
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {{text: string, annotations: string[], exitCode: number}} Digest
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
function firstLine(error, limit = 160) {
|
|
21
|
+
if (!error) return '(no error message)';
|
|
22
|
+
for (const raw of error.split(/\r\n|\r|\n/)) {
|
|
23
|
+
const line = raw.trim();
|
|
24
|
+
if (line) return line.slice(0, limit);
|
|
25
|
+
}
|
|
26
|
+
return '(no error message)';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Build the digest text, `::error` annotations, and exit code from failures. */
|
|
30
|
+
export function buildDigest(failures) {
|
|
31
|
+
if (!failures.length) {
|
|
32
|
+
return { text: `${CHECK} 0 failing tests.`, annotations: [], exitCode: 0 };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const n = failures.length;
|
|
36
|
+
const byCat = new Map();
|
|
37
|
+
for (const f of failures) {
|
|
38
|
+
const cat = categorizeFailure(f.error);
|
|
39
|
+
if (!byCat.has(cat)) byCat.set(cat, []);
|
|
40
|
+
byCat.get(cat).push(f);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const lines = [
|
|
44
|
+
`${CROSS} ${n} failing test${n !== 1 ? 's' : ''} ${DASH} triage by category:`,
|
|
45
|
+
'',
|
|
46
|
+
];
|
|
47
|
+
for (const cat of FAILURE_CATEGORIES) {
|
|
48
|
+
const bucket = byCat.get(cat);
|
|
49
|
+
if (!bucket || !bucket.length) continue;
|
|
50
|
+
lines.push(` ${cat} (${bucket.length}):`);
|
|
51
|
+
for (const f of bucket) {
|
|
52
|
+
lines.push(` - ${f.title} ${DASH} ${firstLine(f.error)}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const text = lines.join('\n');
|
|
56
|
+
|
|
57
|
+
const annotations = [];
|
|
58
|
+
for (const f of failures) {
|
|
59
|
+
const cat = categorizeFailure(f.error);
|
|
60
|
+
let loc = '';
|
|
61
|
+
if (f.file) loc += `file=${f.file},`;
|
|
62
|
+
if (f.line !== null && f.line !== undefined) loc += `line=${f.line},`;
|
|
63
|
+
annotations.push(
|
|
64
|
+
`::error ${loc}title=Test failure::${f.title} ${DASH} ${cat}: ${firstLine(f.error)}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { text, annotations, exitCode: 1 };
|
|
69
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// failures -- heuristic failure categorization (self-contained, pure). Ported
|
|
2
|
+
// behavior-for-behavior from the Python original.
|
|
3
|
+
//
|
|
4
|
+
// Order matters -- the most distinctive signals are checked first so 4xx/5xx
|
|
5
|
+
// status-code patterns don't swallow schema errors that happen to mention a
|
|
6
|
+
// status code in a response preview.
|
|
7
|
+
|
|
8
|
+
export const FAILURE_CATEGORIES = [
|
|
9
|
+
'schema',
|
|
10
|
+
'auth',
|
|
11
|
+
'server',
|
|
12
|
+
'client',
|
|
13
|
+
'timeout',
|
|
14
|
+
'network',
|
|
15
|
+
'other',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// [category, pattern] pairs in match-priority order (distinct from the display
|
|
19
|
+
// order in FAILURE_CATEGORIES). Patterns are case-insensitive and stateless
|
|
20
|
+
// (no `g` flag), mirroring Python's re.search with re.IGNORECASE.
|
|
21
|
+
//
|
|
22
|
+
// Intentional deviation (exotic input only): the `\b` word boundaries use
|
|
23
|
+
// JavaScript's ASCII-only word-character definition, whereas Python's `re` is
|
|
24
|
+
// Unicode-aware by default. An accented/non-ASCII letter glued directly to a
|
|
25
|
+
// status code (a "u"-with-umlaut immediately before `401`, say) can therefore
|
|
26
|
+
// categorize differently than the Python original. This is negligible for real
|
|
27
|
+
// Playwright error messages, which are
|
|
28
|
+
// ASCII around status codes; documented so the behavior reads as deliberate.
|
|
29
|
+
const RULES = [
|
|
30
|
+
[
|
|
31
|
+
'schema',
|
|
32
|
+
/ZodError|invalid[_ ]type|unrecognized key|expected .+ received|at path "|\bzod\b/i,
|
|
33
|
+
],
|
|
34
|
+
[
|
|
35
|
+
'auth',
|
|
36
|
+
/\b401\b|unauthorized|\b403\b|forbidden|invalid(?: auth)? token|token expired/i,
|
|
37
|
+
],
|
|
38
|
+
['timeout', /timeout|timed out|etimedout|deadline exceeded/i],
|
|
39
|
+
[
|
|
40
|
+
'network',
|
|
41
|
+
/econnrefused|enotfound|econnreset|socket hang up|getaddrinfo|network request failed/i,
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
'server',
|
|
45
|
+
/\b5\d{2}\b|internal server error|bad gateway|service unavailable|gateway timeout/i,
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
'client',
|
|
49
|
+
/\b4(?:0[045-9]|1\d|2\d)\b|bad request|not found|unprocessable|conflict/i,
|
|
50
|
+
],
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/** Return the category of a failure error message ('other' when unknown). */
|
|
54
|
+
export function categorizeFailure(error) {
|
|
55
|
+
if (!error) return 'other';
|
|
56
|
+
for (const [category, pattern] of RULES) {
|
|
57
|
+
if (pattern.test(error)) return category;
|
|
58
|
+
}
|
|
59
|
+
return 'other';
|
|
60
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// fastfail_check -- fail-fast config audit for a Playwright config (pure, read-
|
|
2
|
+
// only). Ported behavior-for-behavior from the Python original.
|
|
3
|
+
//
|
|
4
|
+
// Scans a `playwright.config.*` for the fail-fast knobs a suite should set so a
|
|
5
|
+
// broken CI run aborts early instead of burning the whole matrix. Never edits
|
|
6
|
+
// the config -- it recommends, and CANONICAL is the block to paste in.
|
|
7
|
+
|
|
8
|
+
const DASH = '\u2014'; // em dash (see digest.mjs)
|
|
9
|
+
|
|
10
|
+
// The block to paste into playwright.config.ts. Exported for reference; the CLI
|
|
11
|
+
// does not emit it.
|
|
12
|
+
export const CANONICAL = `export default defineConfig({
|
|
13
|
+
// Fail fast in CI: abort once enough has clearly broken, never on local runs.
|
|
14
|
+
forbidOnly: !!process.env.CI, // a stray test.only fails the build
|
|
15
|
+
maxFailures: process.env.CI ? 10 : 0, // stop the run after 10 failures in CI
|
|
16
|
+
retries: process.env.CI ? 2 : 0, // absorb flakes in CI; surface them locally
|
|
17
|
+
// ...your existing config
|
|
18
|
+
});
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
// knob name -> why it matters, in the Python dict's insertion order.
|
|
22
|
+
const KNOBS = [
|
|
23
|
+
['forbidOnly', 'a stray `test.only` silently skips the rest of the suite'],
|
|
24
|
+
[
|
|
25
|
+
'maxFailures',
|
|
26
|
+
'a broken run keeps burning the matrix instead of aborting early',
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
'retries',
|
|
30
|
+
'flakes either fail the build or hide locally without a CI retry policy',
|
|
31
|
+
],
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Return one recommendation per fail-fast knob missing from the config text.
|
|
36
|
+
* Empty array means all knobs are present. Substring scan -- good enough to flag
|
|
37
|
+
* absence; it does not validate the knob's value.
|
|
38
|
+
*/
|
|
39
|
+
export function checkConfig(text) {
|
|
40
|
+
return KNOBS.filter(([knob]) => !text.includes(knob)).map(
|
|
41
|
+
([knob, why]) => `Add \`${knob}\` ${DASH} without it, ${why}.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// parse -- minimal Playwright JSON parser (failing tests only, self-contained).
|
|
2
|
+
// Ported behavior-for-behavior from the Python original.
|
|
3
|
+
//
|
|
4
|
+
// Walks the Playwright JSON reporter's nested suites/specs/tests and returns the
|
|
5
|
+
// real failures. A failed/unexpected test with a passing retry is flaky and
|
|
6
|
+
// excluded; without one it is a failure. Leading non-JSON banners are stripped
|
|
7
|
+
// before parsing (matches the reporter's defensive indexOf('{')).
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {{title: string, status: string, file: (string|null),
|
|
13
|
+
* line: (number|null), error: (string|null)}} Failure
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Build a Failure with the Python dataclass defaults (absent -> null). */
|
|
17
|
+
export function Failure(fields) {
|
|
18
|
+
return {
|
|
19
|
+
title: fields.title,
|
|
20
|
+
status: fields.status,
|
|
21
|
+
file: fields.file ?? null,
|
|
22
|
+
line: fields.line ?? null,
|
|
23
|
+
error: fields.error ?? null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isObject(x) {
|
|
28
|
+
return x !== null && typeof x === 'object' && !Array.isArray(x);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Mirror Python dict.get: `.get` on a non-dict (str/list/number/None) raises
|
|
32
|
+
// AttributeError, which parseFailures converts into a structured error. Here a
|
|
33
|
+
// non-object access throws TypeError, caught by the same conversion.
|
|
34
|
+
function dget(obj, key, dflt) {
|
|
35
|
+
if (!isObject(obj)) throw new TypeError('value is not an object');
|
|
36
|
+
return key in obj ? obj[key] : dflt;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Mirror Python's `container or []` for a for-loop target. Python truthiness
|
|
40
|
+
// differs from JS: an empty object `{}` and empty string `""` are FALSY in
|
|
41
|
+
// Python but TRUTHY in JS. A naive `x || []` would turn `{"suites": {}}` into a
|
|
42
|
+
// thrown "unexpected structure" (JS iterates the truthy `{}`) where Python
|
|
43
|
+
// quietly iterates nothing -- flipping a CI pass into a fail. So:
|
|
44
|
+
// - any Python-falsy value (null/undefined/false/0/""/[]/{}) -> [] (iterate nothing)
|
|
45
|
+
// - an Array -> the array
|
|
46
|
+
// - any other truthy value (non-empty object/string/number) -> returned as-is,
|
|
47
|
+
// so `for...of` reproduces Python's iterate-then-`.get`-fails path (a string
|
|
48
|
+
// iterates its chars; a non-empty object/number is non-iterable and throws),
|
|
49
|
+
// which surfaces as the same "unexpected structure" error.
|
|
50
|
+
function asItems(v) {
|
|
51
|
+
if (Array.isArray(v)) return v;
|
|
52
|
+
if (v && typeof v === 'object') return Object.keys(v).length ? v : [];
|
|
53
|
+
if (v && typeof v !== 'object') return v; // non-empty string / truthy number
|
|
54
|
+
return []; // null/undefined/false/0/""
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Parse a Playwright JSON results file, returning only the real failures. */
|
|
58
|
+
export function parseFailures(resultsPath) {
|
|
59
|
+
if (!fs.existsSync(resultsPath)) return [];
|
|
60
|
+
|
|
61
|
+
let text = fs.readFileSync(resultsPath, 'utf8');
|
|
62
|
+
const braceAt = text.indexOf('{');
|
|
63
|
+
if (braceAt > 0) text = text.slice(braceAt);
|
|
64
|
+
|
|
65
|
+
let data;
|
|
66
|
+
try {
|
|
67
|
+
data = JSON.parse(text);
|
|
68
|
+
} catch (exc) {
|
|
69
|
+
throw new Error(`results file is not valid JSON: ${exc.message}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!isObject(data)) {
|
|
73
|
+
throw new Error("results file's top-level value must be an object");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const failures = [];
|
|
77
|
+
try {
|
|
78
|
+
for (const suite of asItems(data.suites)) {
|
|
79
|
+
processSuite(suite, failures, '', '');
|
|
80
|
+
}
|
|
81
|
+
} catch (exc) {
|
|
82
|
+
if (exc instanceof TypeError) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`results file has an unexpected structure: ${exc.message}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
throw exc;
|
|
88
|
+
}
|
|
89
|
+
return failures;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function processSuite(suite, failures, parentPath, suiteFile) {
|
|
93
|
+
const suiteTitle = dget(suite, 'title', '');
|
|
94
|
+
const suitePath = parentPath ? `${parentPath} > ${suiteTitle}` : suiteTitle;
|
|
95
|
+
const currentFile = dget(suite, 'file', undefined) || suiteFile;
|
|
96
|
+
|
|
97
|
+
for (const child of asItems(dget(suite, 'suites', []))) {
|
|
98
|
+
processSuite(child, failures, suitePath, currentFile);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const spec of asItems(dget(suite, 'specs', []))) {
|
|
102
|
+
const specPath = `${suitePath} > ${dget(spec, 'title', '')}`;
|
|
103
|
+
const specLocation = dget(spec, 'location', undefined) || {};
|
|
104
|
+
for (const test of asItems(dget(spec, 'tests', []))) {
|
|
105
|
+
const testTitle =
|
|
106
|
+
dget(test, 'title', undefined) || dget(spec, 'title', '');
|
|
107
|
+
const testLocation = dget(test, 'location', undefined) || {};
|
|
108
|
+
const results = asItems(dget(test, 'results', undefined));
|
|
109
|
+
|
|
110
|
+
const status = dget(test, 'status', 'unknown');
|
|
111
|
+
if (status !== 'unexpected' && status !== 'failed') continue;
|
|
112
|
+
|
|
113
|
+
const hasPassingRetry = results.some((r) => {
|
|
114
|
+
const s = dget(r, 'status', undefined);
|
|
115
|
+
return s === 'passed' || s === 'expected';
|
|
116
|
+
});
|
|
117
|
+
if (hasPassingRetry) continue; // flaky -- excluded from the failure count
|
|
118
|
+
|
|
119
|
+
let error = null;
|
|
120
|
+
if (results.length) {
|
|
121
|
+
const last = results[results.length - 1];
|
|
122
|
+
const err = dget(last, 'error', undefined) || {};
|
|
123
|
+
error = dget(err, 'message', undefined);
|
|
124
|
+
if (error === undefined || error === null) {
|
|
125
|
+
const errs = dget(last, 'errors', undefined) || [];
|
|
126
|
+
if (errs.length) {
|
|
127
|
+
error = dget(errs[0], 'message', undefined);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
failures.push(
|
|
133
|
+
Failure({
|
|
134
|
+
title: `${specPath} > ${testTitle}`,
|
|
135
|
+
status,
|
|
136
|
+
file:
|
|
137
|
+
dget(testLocation, 'file', undefined) ||
|
|
138
|
+
dget(specLocation, 'file', undefined) ||
|
|
139
|
+
currentFile,
|
|
140
|
+
line:
|
|
141
|
+
dget(testLocation, 'line', undefined) ||
|
|
142
|
+
dget(specLocation, 'line', undefined) ||
|
|
143
|
+
null,
|
|
144
|
+
error,
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: canary-failure-impact
|
|
3
|
+
description: >
|
|
4
|
+
For a given test, function, or code path, traces downstream effects and
|
|
5
|
+
produces a severity label. Investigates config/auth failures using the
|
|
6
|
+
consuming repo's declared user_catalog_skill. Optionally focuses on critical
|
|
7
|
+
paths when critical-areas.json is present.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Canary: Failure Impact
|
|
11
|
+
|
|
12
|
+
Answers "what actually breaks if this code fails and no test catches it?"
|
|
13
|
+
Produces a severity label and a concrete description of downstream effects to
|
|
14
|
+
help prioritise where to invest test coverage.
|
|
15
|
+
|
|
16
|
+
## When to Use
|
|
17
|
+
|
|
18
|
+
- Before deciding which gap to close first: "which of these matters most?"
|
|
19
|
+
|
|
20
|
+
- When a test fails and you need to understand the blast radius
|
|
21
|
+
|
|
22
|
+
- As Phase 3 of `/canary-test-pipeline`
|
|
23
|
+
|
|
24
|
+
- When asked "what's the impact if this breaks?"
|
|
25
|
+
|
|
26
|
+
## This skill vs. `canary guardian analyze`
|
|
27
|
+
|
|
28
|
+
This skill discovers downstream dependents with harness's `compute_blast_radius`
|
|
29
|
+
primitive when the MCP is present (degrading to plain `grep -r` when it is not),
|
|
30
|
+
then applies a **domain-keyword heuristic** (Steps 3–4 below) to turn that
|
|
31
|
+
dependent set into a severity label. The severity labeling is the heuristic part
|
|
32
|
+
— keyword matching over dependent file and function names.
|
|
33
|
+
`canary guardian analyze` (`ts/src/guardian/`, wired to
|
|
34
|
+
`canary guardian analyze` in `ts/src/cli.ts`) is a **real OpenAPI-diff
|
|
35
|
+
blast-radius engine** — it diffs two OpenAPI specs (`--spec-before` /
|
|
36
|
+
`--spec-after`), extracts the actual added/removed/changed endpoints, and maps
|
|
37
|
+
each to coverage gaps against a `coverage-report.json`. For the class of change
|
|
38
|
+
it covers, guardian is strictly higher-fidelity than the heuristics here.
|
|
39
|
+
|
|
40
|
+
- **Use `canary guardian analyze`** when the change is an API/schema change and
|
|
41
|
+
you have (or can generate) before/after OpenAPI specs — it gives exact
|
|
42
|
+
endpoint-level impact and coverage-gap data instead of a keyword guess.
|
|
43
|
+
- **Use this skill** for everything guardian doesn't cover: non-API code paths
|
|
44
|
+
(services, UI components, internal functions), impact tracing where no OpenAPI
|
|
45
|
+
spec exists, or when you need the broader billing/auth/compliance
|
|
46
|
+
domain-severity labeling in Step 3 rather than a strict API diff.
|
|
47
|
+
|
|
48
|
+
They are complementary, not competing — do not duplicate guardian's spec-diff
|
|
49
|
+
logic here if an OpenAPI change is in scope; delegate to
|
|
50
|
+
`canary guardian analyze` instead.
|
|
51
|
+
|
|
52
|
+
## Input
|
|
53
|
+
|
|
54
|
+
Provide one of:
|
|
55
|
+
|
|
56
|
+
- A test file path: `tests/loyalty/points.spec.ts`
|
|
57
|
+
|
|
58
|
+
- A function name: `accruePoints`
|
|
59
|
+
|
|
60
|
+
- A code path: `src/loyalty/points.service.ts`
|
|
61
|
+
|
|
62
|
+
If `.canary/critical-areas.json` is present, focus tracing on paths with
|
|
63
|
+
`risk_score ≥ 0.7`.
|
|
64
|
+
|
|
65
|
+
## Tracing Logic
|
|
66
|
+
|
|
67
|
+
### Step 1 — Identify the code path
|
|
68
|
+
|
|
69
|
+
Resolve the input to a specific file and function. If ambiguous, ask before
|
|
70
|
+
proceeding.
|
|
71
|
+
|
|
72
|
+
### Step 2 — Walk downstream dependents
|
|
73
|
+
|
|
74
|
+
**With harness MCP available:** call `compute_blast_radius` for the target file
|
|
75
|
+
(`file`, `mode: "detailed"`). It simulates cascading failure with a
|
|
76
|
+
probability-weighted BFS and returns each affected node with a cumulative
|
|
77
|
+
failure probability — this is the purpose-built blast-radius primitive, so use
|
|
78
|
+
it instead of hand-walking `get_relationships` hop-by-hop. Feed the returned
|
|
79
|
+
node set into Step 3, and let the cumulative probability weight the severity
|
|
80
|
+
(high-probability nodes dominate). When you additionally need the affected set
|
|
81
|
+
grouped by kind (tests vs docs vs code), call `get_impact` for the same target.
|
|
82
|
+
|
|
83
|
+
**Fallback:** use `grep -r` to find files that import or call the target. Limit
|
|
84
|
+
to direct dependents (1 hop) when MCP is unavailable.
|
|
85
|
+
|
|
86
|
+
### Step 3 — Classify each dependent by domain
|
|
87
|
+
|
|
88
|
+
Apply these heuristics to the dependent paths and function names:
|
|
89
|
+
|
|
90
|
+
| Domain signal | Severity modifier |
|
|
91
|
+
| --------------------------------------- | ----------------------------------- |
|
|
92
|
+
| billing / payment / charge / invoice | +2 (financial impact) |
|
|
93
|
+
| auth / session / token / permission | +2 (security/access) |
|
|
94
|
+
| compliance / audit / PHI / PII / HIPAA | +2 (regulatory) |
|
|
95
|
+
| data / persist / write / store / commit | +1 (data integrity) |
|
|
96
|
+
| UI / render / display / format / label | −1 (user-facing only, no data risk) |
|
|
97
|
+
|
|
98
|
+
### Step 4 — Aggregate to severity label
|
|
99
|
+
|
|
100
|
+
Base score starts at 2 (Medium). Sum modifiers from step 3. Cap at 4 (Critical).
|
|
101
|
+
|
|
102
|
+
| Score | Label |
|
|
103
|
+
| ----- | -------- |
|
|
104
|
+
| 5+ | Critical |
|
|
105
|
+
| 3–4 | High |
|
|
106
|
+
| 2 | Medium |
|
|
107
|
+
| 0–1 | Low |
|
|
108
|
+
|
|
109
|
+
### Step 5 — User catalog investigation
|
|
110
|
+
|
|
111
|
+
When a test failure in the target path involves an auth, permission, or
|
|
112
|
+
configuration error:
|
|
113
|
+
|
|
114
|
+
1. Read `user_catalog_skill` from `.canary/company.json`
|
|
115
|
+
2. If present: invoke `canary skills run <user_catalog_skill>` with the required
|
|
116
|
+
attributes from the error context
|
|
117
|
+
3. If a matching user/config is found: surface it as a suggestion
|
|
118
|
+
4. If absent or no match: present constructively —
|
|
119
|
+
|
|
120
|
+
> "This failure may be a test user or test data configuration issue. Check
|
|
121
|
+
> your user catalog if you have one, or set up the required test data before
|
|
122
|
+
> re-running."
|
|
123
|
+
|
|
124
|
+
## Output Format
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
Failure impact — src/loyalty/points.service.ts::accruePoints
|
|
128
|
+
|
|
129
|
+
Severity: HIGH
|
|
130
|
+
|
|
131
|
+
If this breaks undetected:
|
|
132
|
+
· Members see incorrect balance in the partner portal (user-facing)
|
|
133
|
+
· Points journal diverges from the ledger (data integrity)
|
|
134
|
+
· Downstream: redemption.service.ts · tier-upgrade.service.ts ·
|
|
135
|
+
reporting.service.ts (3 dependents)
|
|
136
|
+
|
|
137
|
+
Priority: write failure-path tests before next release
|
|
138
|
+
Suggested: /canary-write-test "test failure paths for accruePoints"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Related skills
|
|
142
|
+
|
|
143
|
+
- `/canary-critical-areas` — produces `critical-areas.json` used for focus
|
|
144
|
+
|
|
145
|
+
- `/canary-ci-ready` — uses the same user-catalog investigation pattern
|
|
146
|
+
|
|
147
|
+
- `/canary-write-test` — generates tests for the identified high-impact gaps
|
|
148
|
+
|
|
149
|
+
- `/canary-test-pipeline` — Phase 3
|
|
150
|
+
|
|
151
|
+
- `canary guardian analyze` (CLI, `ts/src/guardian/`) — higher-fidelity
|
|
152
|
+
OpenAPI-diff blast-radius engine; use instead of this skill's heuristics when
|
|
153
|
+
the change is an API/schema change with before/after specs available
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: canary-failure-impact
|
|
2
|
+
version: '1.0.0'
|
|
3
|
+
description:
|
|
4
|
+
Trace the downstream blast radius of an undetected failure for a given test,
|
|
5
|
+
function, or code path and assign a severity label.
|
|
6
|
+
stability: static
|
|
7
|
+
triggers:
|
|
8
|
+
- manual
|
|
9
|
+
platforms:
|
|
10
|
+
- claude-code
|
|
11
|
+
type: rigid
|
|
12
|
+
tools: []
|
|
13
|
+
tier: 1
|
|
14
|
+
depends_on:
|
|
15
|
+
- canary-ci-ready
|