canary-test-cli 5.15.0 → 6.0.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/agent/frameworks/registry.json +655 -0
- package/bin/canary.js +20 -15
- package/dist/doctor-manifest.d.ts +94 -0
- package/dist/doctor.d.ts +67 -0
- package/dist/engine/analysis/cli.js +270 -0
- package/dist/engine/analysis/engine.js +146 -0
- package/dist/engine/analysis/reports.js +0 -0
- package/dist/engine/analysis/rows.js +9 -0
- package/dist/engine/cli-commands.js +618 -0
- package/dist/engine/cli-common.js +60 -0
- package/dist/engine/cli.core.js +208 -0
- package/dist/engine/cli.js +31 -0
- package/dist/engine/company-knowledge-cli.js +201 -0
- package/dist/engine/core/ci-env.js +33 -0
- package/dist/engine/core/classifier.js +192 -0
- package/dist/engine/core/company-knowledge.js +765 -0
- package/dist/engine/core/config-validation.js +74 -0
- package/dist/engine/core/detection.js +48 -0
- package/dist/engine/core/domain-scanner.js +212 -0
- package/dist/engine/core/environment-detect.js +410 -0
- package/dist/engine/core/executor.js +181 -0
- package/dist/engine/core/feedback.js +93 -0
- package/dist/engine/core/fixture-scanner.js +173 -0
- package/dist/engine/core/framework-registry.js +123 -0
- package/dist/engine/core/mcp-validator.js +218 -0
- package/dist/engine/core/metadata-scanner.js +147 -0
- package/dist/engine/core/migrator.js +1112 -0
- package/dist/engine/core/overlays.js +176 -0
- package/dist/engine/core/pattern-healer.js +147 -0
- package/dist/engine/core/pattern-matcher.js +255 -0
- package/dist/engine/core/quality-scorer.js +213 -0
- package/dist/engine/core/recommender.js +152 -0
- package/dist/engine/core/reporter.js +211 -0
- package/dist/engine/core/scaffolder.js +236 -0
- package/dist/engine/core/skill-registry.js +522 -0
- package/dist/engine/core/static-linter.js +237 -0
- package/dist/engine/core/ticket-updater.js +639 -0
- package/dist/engine/core/workflow-discovery.js +693 -0
- package/dist/engine/guardian/agent-tier.js +338 -0
- package/dist/engine/guardian/analysis-emit.js +201 -0
- package/dist/engine/guardian/cli.js +787 -0
- package/dist/engine/guardian/coverage.js +1055 -0
- package/dist/engine/guardian/delta-emitter.js +46 -0
- package/dist/engine/guardian/diff-extractor.js +257 -0
- package/dist/engine/guardian/hard-gate.js +373 -0
- package/dist/engine/guardian/impact-mapper.js +121 -0
- package/dist/engine/guardian/pr-check.js +975 -0
- package/dist/engine/guardian/pr-comment.js +200 -0
- package/dist/engine/guardian/summary-emitter.js +94 -0
- package/dist/engine/guardian/tier.js +58 -0
- package/dist/engine/history/cli.js +303 -0
- package/dist/engine/history/detector.js +68 -0
- package/dist/engine/history/ndjson-store.js +177 -0
- package/dist/engine/history/record.js +14 -0
- package/dist/engine/history/schema.js +59 -0
- package/dist/engine/history/store.js +47 -0
- package/dist/engine/history/supabase-store.js +113 -0
- package/dist/engine/main-deps.js +105 -0
- package/dist/engine/mcp-server.js +647 -0
- package/dist/engine/package.json +4 -0
- package/dist/engine/skills-cli.js +181 -0
- package/dist/engine/ui/banner.js +50 -0
- package/dist/engine/util/coalesce.js +12 -0
- package/dist/engine/util/round.js +43 -0
- package/dist/engine/workflow-cli.js +242 -0
- package/dist/engine-checks.d.ts +49 -0
- package/dist/overlay-commands.d.ts +81 -0
- package/dist/overlay-conflicts.d.ts +33 -0
- package/dist/overlay-lint.d.ts +19 -0
- package/dist/overlays-registry.d.ts +74 -0
- package/dist/reporters/testtracker.d.ts +89 -0
- package/dist/reporters/testtracker.js +195 -0
- package/dist/router.d.ts +12 -0
- package/dist/router.js +4 -4
- package/dist/skill-requirements.d.ts +57 -0
- package/dist/source-spec.d.ts +20 -0
- package/package.json +30 -6
- package/bin/canary +0 -0
- package/scripts/install.js +0 -104
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { OverlayEntry, OverlayRegistry } from './overlays-registry.js';
|
|
2
|
+
/** One overlay contending for a skill name, with its effective precedence. */
|
|
3
|
+
export interface Contender {
|
|
4
|
+
overlay: string;
|
|
5
|
+
/** Declared precedence, or 0 when the entry left it null/absent. */
|
|
6
|
+
precedence: number;
|
|
7
|
+
}
|
|
8
|
+
/** A skill name shipped by two or more registered overlays. */
|
|
9
|
+
export interface SkillConflict {
|
|
10
|
+
skill: string;
|
|
11
|
+
/** All contending overlays, ordered by precedence desc then name asc. */
|
|
12
|
+
contenders: Contender[];
|
|
13
|
+
/** The overlay that wins by declared precedence, or null when it is a tie. */
|
|
14
|
+
winner: string | null;
|
|
15
|
+
/** True iff a single overlay holds the highest precedence. */
|
|
16
|
+
resolved: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface ConflictDeps {
|
|
19
|
+
/** List the skill (directory) names an overlay ships. Injectable for tests. */
|
|
20
|
+
readSkillNames?: (entry: OverlayEntry) => string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Skill (directory) names an overlay ships, read from
|
|
24
|
+
* `<overlay>/.canary/skills/<name>/SKILL.md`. A missing or unreadable skills
|
|
25
|
+
* dir yields no names (never throws) — mirrors the Python loader's tolerance.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readOverlaySkillNames(entry: OverlayEntry): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Detect skill-name collisions across the registered overlays. Returns one
|
|
30
|
+
* {@link SkillConflict} per skill name shipped by two or more overlays, sorted
|
|
31
|
+
* by skill name.
|
|
32
|
+
*/
|
|
33
|
+
export declare function detectSkillConflicts(reg: OverlayRegistry, deps?: ConflictDeps): SkillConflict[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Migration target shapes a `deploy_to` entry may name, plus the `all` sentinel. */
|
|
2
|
+
export declare const VALID_DEPLOY_TARGETS: ReadonlySet<string>;
|
|
3
|
+
export interface LintFinding {
|
|
4
|
+
/** Skill name, or `(overlay)` for an overlay-level finding. */
|
|
5
|
+
skill: string;
|
|
6
|
+
level: 'error' | 'warning';
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
export interface LintResult {
|
|
10
|
+
overlay: string;
|
|
11
|
+
skillsChecked: number;
|
|
12
|
+
findings: LintFinding[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Lint an overlay clone at `overlayPath`. Returns every finding; the caller
|
|
16
|
+
* decides how to render/exit. Never throws on a malformed overlay — a missing
|
|
17
|
+
* skills dir is itself an error finding.
|
|
18
|
+
*/
|
|
19
|
+
export declare function lintOverlay(overlayPath: string): LintResult;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader/writer for `~/.canary/overlays.json` — the tracked-overlay registry.
|
|
3
|
+
*
|
|
4
|
+
* Cross-runtime contract (spec Assumption): this file is written ONLY by the
|
|
5
|
+
* TS side. The Python skill loader never parses it; it directory-scans the
|
|
6
|
+
* `~/.canary/overlays/<name>/` clone dirs instead. So the schema here serves
|
|
7
|
+
* the `overlay` commands alone.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SCHEMA_VERSION = 1;
|
|
10
|
+
export interface OverlayEntry {
|
|
11
|
+
/** Registry key and clone dir name: `<owner>-<repo>`. */
|
|
12
|
+
name: string;
|
|
13
|
+
/** The source spec as the user gave it (`github:...`, URL, or path). */
|
|
14
|
+
source: string;
|
|
15
|
+
/** Pinned tag/branch, or null to track the remote default branch. */
|
|
16
|
+
ref: string | null;
|
|
17
|
+
/** Absolute clone path under `~/.canary/overlays/`. */
|
|
18
|
+
path: string;
|
|
19
|
+
/** ISO date the overlay was added. */
|
|
20
|
+
addedDate: string;
|
|
21
|
+
/**
|
|
22
|
+
* Consent for this overlay's `command-succeeds` doctor checks: true when the
|
|
23
|
+
* user allowed them at `overlay add`, false when declined, null when the
|
|
24
|
+
* overlay ships no such checks (nothing to gate).
|
|
25
|
+
*/
|
|
26
|
+
consent: boolean | null;
|
|
27
|
+
/**
|
|
28
|
+
* Fingerprint of the `command-succeeds` set that `consent` was granted for,
|
|
29
|
+
* or null when there are no such checks. Consent is re-requested when the
|
|
30
|
+
* live manifest's fingerprint no longer matches this.
|
|
31
|
+
*/
|
|
32
|
+
consentCommandsHash: string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Declared arbitration priority when two overlays ship the same skill name
|
|
35
|
+
* (#333). Higher wins; null/absent is treated as 0. A collision is only
|
|
36
|
+
* *resolved* when exactly one contending overlay holds the highest
|
|
37
|
+
* precedence — otherwise which definition wins is accidental and `doctor`
|
|
38
|
+
* flags it. Both runtimes read this: TS diagnostics and the Python skill
|
|
39
|
+
* loader must agree on the winner.
|
|
40
|
+
*/
|
|
41
|
+
precedence: number | null;
|
|
42
|
+
}
|
|
43
|
+
export interface OverlayRegistry {
|
|
44
|
+
schemaVersion: number;
|
|
45
|
+
overlays: OverlayEntry[];
|
|
46
|
+
}
|
|
47
|
+
/** Raised when the registry file exists but cannot be read or parsed. */
|
|
48
|
+
export declare class RegistryError extends Error {
|
|
49
|
+
constructor(message: string);
|
|
50
|
+
}
|
|
51
|
+
export declare function canaryHome(homeDir?: string): string;
|
|
52
|
+
export declare function registryPath(homeDir?: string): string;
|
|
53
|
+
export declare function overlaysDir(homeDir?: string): string;
|
|
54
|
+
export declare function clonePath(name: string, homeDir?: string): string;
|
|
55
|
+
export declare function emptyRegistry(): OverlayRegistry;
|
|
56
|
+
export declare function read(homeDir?: string): OverlayRegistry;
|
|
57
|
+
/**
|
|
58
|
+
* Whether `command-succeeds` doctor checks may run for an overlay: consent must
|
|
59
|
+
* have been granted (`true`) AND still cover the live manifest's command set
|
|
60
|
+
* (`liveHash` matching the recorded fingerprint). A changed manifest (hash
|
|
61
|
+
* mismatch) revokes consent until it is re-confirmed at `overlay add`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function consentGranted(entry: OverlayEntry, liveHash: string | null): boolean;
|
|
64
|
+
/** Write the registry atomically (temp file + rename), creating `~/.canary`. */
|
|
65
|
+
export declare function write(registry: OverlayRegistry, homeDir?: string): void;
|
|
66
|
+
export declare function get(registry: OverlayRegistry, name: string): OverlayEntry | null;
|
|
67
|
+
export declare function list(registry: OverlayRegistry): OverlayEntry[];
|
|
68
|
+
/** Add an entry. Throws if one with the same name is already registered. */
|
|
69
|
+
export declare function add(registry: OverlayRegistry, entry: OverlayEntry): OverlayRegistry;
|
|
70
|
+
/** Remove an entry by name. Returns the new registry and whether it existed. */
|
|
71
|
+
export declare function remove(registry: OverlayRegistry, name: string): {
|
|
72
|
+
registry: OverlayRegistry;
|
|
73
|
+
removed: boolean;
|
|
74
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { FullResult, Reporter, TestCase, TestResult } from "@playwright/test/reporter";
|
|
2
|
+
export interface TestTrackerReporterOptions {
|
|
3
|
+
suite?: string;
|
|
4
|
+
/** Prefix stripped from an absolute test file path. Default: `<cwd>/`. */
|
|
5
|
+
testFilePrefix?: string;
|
|
6
|
+
/** Explicit environment label; else derived from env at push time. */
|
|
7
|
+
environment?: string;
|
|
8
|
+
url?: string;
|
|
9
|
+
token?: string;
|
|
10
|
+
workflow?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ResolvedConfig {
|
|
13
|
+
suite: string;
|
|
14
|
+
testFilePrefix: string;
|
|
15
|
+
environment?: string;
|
|
16
|
+
url: string;
|
|
17
|
+
token: string;
|
|
18
|
+
workflow: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ResultEntry {
|
|
21
|
+
full_title: string;
|
|
22
|
+
test_file: string;
|
|
23
|
+
status: string;
|
|
24
|
+
duration_ms?: number;
|
|
25
|
+
error_message?: string;
|
|
26
|
+
error_stack?: string;
|
|
27
|
+
retries: number;
|
|
28
|
+
tags: string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The wire contract POSTed to `/api/ingest/runs`. Declared explicitly (rather
|
|
32
|
+
* than inferred from the object literal) so `declaration: true` ships a stable,
|
|
33
|
+
* reviewable public type and an accidental field change is a compile error.
|
|
34
|
+
*/
|
|
35
|
+
export interface IngestPayload {
|
|
36
|
+
canary_run_id: string;
|
|
37
|
+
suite: string;
|
|
38
|
+
branch: string;
|
|
39
|
+
commit_sha?: string;
|
|
40
|
+
workflow: string;
|
|
41
|
+
environment?: string;
|
|
42
|
+
status: "passed" | "failed" | "flaky" | "cancelled";
|
|
43
|
+
started_at: string;
|
|
44
|
+
finished_at: string;
|
|
45
|
+
totals: {
|
|
46
|
+
passed: number;
|
|
47
|
+
failed: number;
|
|
48
|
+
flaky: number;
|
|
49
|
+
skipped: number;
|
|
50
|
+
total: number;
|
|
51
|
+
};
|
|
52
|
+
results: ResultEntry[];
|
|
53
|
+
}
|
|
54
|
+
export declare function mapStatus(pw: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* Final per-test status, flaky-aware. Playwright's per-attempt
|
|
57
|
+
* `result.status` is NEVER "flaky" — flakiness is derived at the test level
|
|
58
|
+
* (a test that failed then passed on retry). `test.outcome()` is the canonical
|
|
59
|
+
* signal, so a recovered flake is reported as "flaky" (SDET-visible) rather
|
|
60
|
+
* than the last attempt's "passed". A recovered flake does NOT count as a
|
|
61
|
+
* failure at the run level (see buildPayload) — management sees a good run.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveTestStatus(outcome: string, lastAttemptStatus: string): string;
|
|
64
|
+
export declare function resolveConfig(opts?: TestTrackerReporterOptions, env?: NodeJS.ProcessEnv): ResolvedConfig;
|
|
65
|
+
export declare function shouldPush(cfg: Pick<ResolvedConfig, "url" | "token">, env?: NodeJS.ProcessEnv): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Overall run status. When Playwright reports the run as interrupted or timed
|
|
68
|
+
* out, the run did NOT complete — do not derive a green status from whatever
|
|
69
|
+
* per-test buckets happened to fill before the abort. `fullResultStatus` is
|
|
70
|
+
* Playwright's `FullResult.status` (`passed` | `failed` | `timedout` | `interrupted`).
|
|
71
|
+
*/
|
|
72
|
+
export declare function runStatus(totals: {
|
|
73
|
+
failed: number;
|
|
74
|
+
flaky: number;
|
|
75
|
+
}, fullResultStatus?: string): "passed" | "failed" | "flaky" | "cancelled";
|
|
76
|
+
export declare function buildPayload(results: ResultEntry[], cfg: ResolvedConfig, timing: {
|
|
77
|
+
startedAt: string;
|
|
78
|
+
finishedAt: string;
|
|
79
|
+
}, env?: NodeJS.ProcessEnv, fullResultStatus?: string): IngestPayload;
|
|
80
|
+
export default class TestTrackerReporter implements Reporter {
|
|
81
|
+
private options;
|
|
82
|
+
private results;
|
|
83
|
+
private startTime;
|
|
84
|
+
private cfg;
|
|
85
|
+
constructor(options?: TestTrackerReporterOptions);
|
|
86
|
+
onBegin(): void;
|
|
87
|
+
onTestEnd(test: TestCase, result: TestResult): void;
|
|
88
|
+
onEnd(result: FullResult): Promise<void>;
|
|
89
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.mapStatus = mapStatus;
|
|
7
|
+
exports.resolveTestStatus = resolveTestStatus;
|
|
8
|
+
exports.resolveConfig = resolveConfig;
|
|
9
|
+
exports.shouldPush = shouldPush;
|
|
10
|
+
exports.runStatus = runStatus;
|
|
11
|
+
exports.buildPayload = buildPayload;
|
|
12
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
13
|
+
// Optional .env load — MUST NOT crash the suite if dotenv is absent.
|
|
14
|
+
try {
|
|
15
|
+
require("dotenv").config();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
/* dotenv not installed — use ambient env */
|
|
19
|
+
}
|
|
20
|
+
function mapStatus(pw) {
|
|
21
|
+
switch (pw) {
|
|
22
|
+
case "passed":
|
|
23
|
+
return "passed";
|
|
24
|
+
case "failed":
|
|
25
|
+
case "timedOut":
|
|
26
|
+
return "failed";
|
|
27
|
+
case "flaky":
|
|
28
|
+
return "flaky";
|
|
29
|
+
case "skipped":
|
|
30
|
+
case "interrupted":
|
|
31
|
+
return "skipped";
|
|
32
|
+
default:
|
|
33
|
+
return "skipped";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Final per-test status, flaky-aware. Playwright's per-attempt
|
|
38
|
+
* `result.status` is NEVER "flaky" — flakiness is derived at the test level
|
|
39
|
+
* (a test that failed then passed on retry). `test.outcome()` is the canonical
|
|
40
|
+
* signal, so a recovered flake is reported as "flaky" (SDET-visible) rather
|
|
41
|
+
* than the last attempt's "passed". A recovered flake does NOT count as a
|
|
42
|
+
* failure at the run level (see buildPayload) — management sees a good run.
|
|
43
|
+
*/
|
|
44
|
+
function resolveTestStatus(outcome, lastAttemptStatus) {
|
|
45
|
+
if (outcome === "flaky")
|
|
46
|
+
return "flaky";
|
|
47
|
+
return mapStatus(lastAttemptStatus);
|
|
48
|
+
}
|
|
49
|
+
function resolveConfig(opts = {}, env = process.env) {
|
|
50
|
+
const suite = opts.suite ?? env.TESTTRACKER_SUITE;
|
|
51
|
+
if (!suite) {
|
|
52
|
+
throw new Error("TestTrackerReporter: `suite` is required (option or TESTTRACKER_SUITE).");
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
suite,
|
|
56
|
+
testFilePrefix: opts.testFilePrefix ?? env.TESTTRACKER_TEST_FILE_PREFIX ?? `${process.cwd()}/`,
|
|
57
|
+
environment: opts.environment ?? env.TESTTRACKER_ENVIRONMENT,
|
|
58
|
+
url: opts.url ?? env.TESTTRACKER_URL ?? "",
|
|
59
|
+
token: opts.token ?? env.TESTTRACKER_API_TOKEN ?? "",
|
|
60
|
+
workflow: opts.workflow ?? env.TESTTRACKER_WORKFLOW ?? "playwright",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function shouldPush(cfg, env = process.env) {
|
|
64
|
+
if (!cfg.url || !cfg.token)
|
|
65
|
+
return false;
|
|
66
|
+
const isCI = env.CI === "true" || env.GITHUB_ACTIONS === "true";
|
|
67
|
+
const force = env.TESTTRACKER_PUSH === "true";
|
|
68
|
+
return isCI || force;
|
|
69
|
+
}
|
|
70
|
+
function stableRunId(env) {
|
|
71
|
+
const runId = env.GITHUB_RUN_ID;
|
|
72
|
+
if (runId)
|
|
73
|
+
return env.GITHUB_RUN_ATTEMPT ? `${runId}-${env.GITHUB_RUN_ATTEMPT}` : runId;
|
|
74
|
+
const sha = env.GITHUB_SHA?.slice(0, 7);
|
|
75
|
+
return (sha ? `${sha}-` : "local-") + node_crypto_1.default.randomUUID();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Overall run status. When Playwright reports the run as interrupted or timed
|
|
79
|
+
* out, the run did NOT complete — do not derive a green status from whatever
|
|
80
|
+
* per-test buckets happened to fill before the abort. `fullResultStatus` is
|
|
81
|
+
* Playwright's `FullResult.status` (`passed` | `failed` | `timedout` | `interrupted`).
|
|
82
|
+
*/
|
|
83
|
+
function runStatus(totals, fullResultStatus) {
|
|
84
|
+
if (fullResultStatus === "interrupted")
|
|
85
|
+
return "cancelled";
|
|
86
|
+
if (fullResultStatus === "timedout" || fullResultStatus === "failed")
|
|
87
|
+
return "failed";
|
|
88
|
+
// "passed" or unknown → trust the per-test buckets.
|
|
89
|
+
if (totals.failed > 0)
|
|
90
|
+
return "failed";
|
|
91
|
+
if (totals.flaky > 0)
|
|
92
|
+
return "flaky";
|
|
93
|
+
return "passed";
|
|
94
|
+
}
|
|
95
|
+
function buildPayload(results, cfg, timing, env = process.env, fullResultStatus) {
|
|
96
|
+
const count = (s) => results.filter((r) => r.status === s).length;
|
|
97
|
+
const passed = count("passed");
|
|
98
|
+
const failed = count("failed");
|
|
99
|
+
const flaky = count("flaky");
|
|
100
|
+
const skipped = count("skipped");
|
|
101
|
+
return {
|
|
102
|
+
canary_run_id: stableRunId(env),
|
|
103
|
+
suite: cfg.suite,
|
|
104
|
+
branch: env.GITHUB_REF_NAME ?? "local",
|
|
105
|
+
commit_sha: env.GITHUB_SHA,
|
|
106
|
+
workflow: cfg.workflow,
|
|
107
|
+
environment: cfg.environment,
|
|
108
|
+
status: runStatus({ failed, flaky }, fullResultStatus),
|
|
109
|
+
started_at: timing.startedAt,
|
|
110
|
+
finished_at: timing.finishedAt,
|
|
111
|
+
totals: { passed, failed, flaky, skipped, total: results.length },
|
|
112
|
+
results,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
class TestTrackerReporter {
|
|
116
|
+
options;
|
|
117
|
+
results = new Map();
|
|
118
|
+
startTime = Date.now();
|
|
119
|
+
cfg = null;
|
|
120
|
+
constructor(options = {}) {
|
|
121
|
+
this.options = options;
|
|
122
|
+
}
|
|
123
|
+
onBegin() {
|
|
124
|
+
// Resolve config once; a config error here is logged, not thrown, so a
|
|
125
|
+
// misconfigured reporter never aborts the whole run.
|
|
126
|
+
try {
|
|
127
|
+
this.cfg = resolveConfig(this.options);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
console.log(`\nTestTracker: disabled — ${err instanceof Error ? err.message : String(err)}`);
|
|
131
|
+
this.cfg = null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
onTestEnd(test, result) {
|
|
135
|
+
if (!this.cfg)
|
|
136
|
+
return;
|
|
137
|
+
// A reporter hook must never fail the suite — guard the whole body.
|
|
138
|
+
try {
|
|
139
|
+
const fullTitle = test.titlePath().filter(Boolean).join(" > ");
|
|
140
|
+
// `test.tags` requires Playwright >= 1.42; guard for the peer floor.
|
|
141
|
+
const tags = (test.tags ?? []).map((t) => t.replace(/^@/, ""));
|
|
142
|
+
// Keyed by the session-unique `test.id` (NOT the title) so `--repeat-each`
|
|
143
|
+
// and duplicate-title executions don't collapse into one entry. Retries
|
|
144
|
+
// share one TestCase (same id), so last-write-wins for the final status
|
|
145
|
+
// and first-failing-attempt error preservation both still hold — a
|
|
146
|
+
// recovered flake keeps the error that shows the SDET why it flaked.
|
|
147
|
+
const prior = this.results.get(test.id);
|
|
148
|
+
this.results.set(test.id, {
|
|
149
|
+
full_title: fullTitle,
|
|
150
|
+
test_file: test.location.file.startsWith(this.cfg.testFilePrefix)
|
|
151
|
+
? test.location.file.slice(this.cfg.testFilePrefix.length)
|
|
152
|
+
: test.location.file,
|
|
153
|
+
status: resolveTestStatus(test.outcome(), result.status),
|
|
154
|
+
duration_ms: result.duration,
|
|
155
|
+
error_message: prior?.error_message ?? result.errors[0]?.message,
|
|
156
|
+
error_stack: prior?.error_stack ?? result.errors[0]?.stack,
|
|
157
|
+
retries: result.retry,
|
|
158
|
+
tags,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
console.log(`\nTestTracker: skipped a result — ${err instanceof Error ? err.message : String(err)}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async onEnd(result) {
|
|
166
|
+
if (!this.cfg || !shouldPush(this.cfg))
|
|
167
|
+
return;
|
|
168
|
+
try {
|
|
169
|
+
const payload = buildPayload([...this.results.values()], this.cfg, {
|
|
170
|
+
startedAt: new Date(this.startTime).toISOString(),
|
|
171
|
+
finishedAt: new Date().toISOString(),
|
|
172
|
+
}, process.env, result?.status);
|
|
173
|
+
const resp = await fetch(`${this.cfg.url}/api/ingest/runs`, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: {
|
|
176
|
+
"Content-Type": "application/json",
|
|
177
|
+
Authorization: `Bearer ${this.cfg.token}`,
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify(payload),
|
|
180
|
+
});
|
|
181
|
+
if (resp.ok) {
|
|
182
|
+
const data = (await resp.json().catch(() => ({})));
|
|
183
|
+
console.log(`\nTestTracker: run ${data.id ?? "?"} ingested${data.duplicate ? " (duplicate)" : ""}.`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const text = await resp.text().catch(() => "");
|
|
187
|
+
console.log(`\nTestTracker: push failed ${resp.status} — ${text.slice(0, 200)}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
console.log(`\nTestTracker: push error — ${err instanceof Error ? err.message : String(err)}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
exports.default = TestTrackerReporter;
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CommandDeps } from './overlay-commands.js';
|
|
2
|
+
/** Subcommands handled in TypeScript rather than forwarded to the binary. */
|
|
3
|
+
export declare const TS_COMMANDS: readonly string[];
|
|
4
|
+
/** True when `argv` (process.argv.slice(2)) targets a TS-handled command. */
|
|
5
|
+
export declare function isTsCommand(argv: readonly string[]): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Dispatch a TS-handled command. Returns the process exit code (or a Promise of
|
|
8
|
+
* one for async commands like `doctor`), or `null` when the command should fall
|
|
9
|
+
* through to the Python binary. `deps` is threaded to the command handlers for
|
|
10
|
+
* testing (real dependencies by default).
|
|
11
|
+
*/
|
|
12
|
+
export declare function route(argv: readonly string[], deps?: CommandDeps): number | Promise<number> | null;
|
package/dist/router.js
CHANGED
|
@@ -39,12 +39,12 @@ exports.route = route;
|
|
|
39
39
|
/**
|
|
40
40
|
* Router for TS-handled `canary` subcommands — the strangler seam (Decision 10).
|
|
41
41
|
*
|
|
42
|
-
* The npm shim (`bin/canary.js`) forwards every command to the bundled
|
|
43
|
-
*
|
|
44
|
-
* `
|
|
42
|
+
* The npm shim (`bin/canary.js`) forwards every command to the bundled
|
|
43
|
+
* TypeScript engine (`dist/engine/cli.js`) except the ones handled here
|
|
44
|
+
* natively: `overlay` and `doctor`.
|
|
45
45
|
*
|
|
46
46
|
* `route()` returns a process exit code, or `null` when the command is not
|
|
47
|
-
* TS-handled and the shim should fall through to the
|
|
47
|
+
* TS-handled and the shim should fall through to the bundled engine.
|
|
48
48
|
*/
|
|
49
49
|
const overlay = __importStar(require("./overlay-commands.js"));
|
|
50
50
|
const doctor_js_1 = require("./doctor.js");
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill runtime-requirement verification (#336).
|
|
3
|
+
*
|
|
4
|
+
* Skills declare the runtime tools they need in SKILL.md frontmatter:
|
|
5
|
+
*
|
|
6
|
+
* requires: [python3>=3.11, node>=20]
|
|
7
|
+
*
|
|
8
|
+
* `canary doctor` reads those declarations from the skills installed in a
|
|
9
|
+
* consuming repo (overlay / global / local) and reports named, remediable
|
|
10
|
+
* failures — so a skill fails at `doctor` time with a clear message instead of
|
|
11
|
+
* cryptically at first run.
|
|
12
|
+
*
|
|
13
|
+
* Scope / naming: `requires` is deliberately distinct from harness's *planned*
|
|
14
|
+
* `capabilities:` field (Intense-Visions/harness-engineering#558). The two are
|
|
15
|
+
* orthogonal: harness `capabilities` bounds the Claude Code tools/network/file
|
|
16
|
+
* access a skill may use (a permission ceiling); canary `requires` verifies the
|
|
17
|
+
* runtime environment (is the interpreter installed, at a new-enough version).
|
|
18
|
+
* Keeping the field names separate avoids a future collision.
|
|
19
|
+
*
|
|
20
|
+
* Token grammar (v1): `<command>` or `<command><op><version>`, op in
|
|
21
|
+
* `>=` | `>` | `==`, version a dotted numeric (`3`, `3.11`, `1.22.0`). Non-command
|
|
22
|
+
* capability tokens (e.g. browser bundles) are intentionally NOT invented here;
|
|
23
|
+
* an unparseable token verifies as `unverifiable`, never a hard failure.
|
|
24
|
+
*/
|
|
25
|
+
export type RequirementOp = '>=' | '>' | '==';
|
|
26
|
+
export interface Requirement {
|
|
27
|
+
/** The original token, verbatim. */
|
|
28
|
+
raw: string;
|
|
29
|
+
/** The command to look for on PATH, or null when the token is unparseable. */
|
|
30
|
+
command: string | null;
|
|
31
|
+
op: RequirementOp | null;
|
|
32
|
+
/** Dotted numeric version constraint, or null for a presence-only token. */
|
|
33
|
+
version: string | null;
|
|
34
|
+
}
|
|
35
|
+
export type RequirementStatus = 'ok' | 'missing' | 'too-old' | 'unverifiable';
|
|
36
|
+
export interface RequirementResult {
|
|
37
|
+
requirement: Requirement;
|
|
38
|
+
status: RequirementStatus;
|
|
39
|
+
detail: string;
|
|
40
|
+
}
|
|
41
|
+
/** What a command probe reports: is it on PATH, and its detected version. */
|
|
42
|
+
export interface ProbeResult {
|
|
43
|
+
present: boolean;
|
|
44
|
+
version: string | null;
|
|
45
|
+
}
|
|
46
|
+
export type CommandProbe = (command: string) => ProbeResult;
|
|
47
|
+
/** Parse one requirement token into its command + optional version constraint. */
|
|
48
|
+
export declare function parseRequirement(token: string): Requirement;
|
|
49
|
+
/** Verify one requirement against a command probe. Never throws. */
|
|
50
|
+
export declare function checkRequirement(req: Requirement, probe: CommandProbe): RequirementResult;
|
|
51
|
+
/**
|
|
52
|
+
* Extract the `requires:` flow list from SKILL.md frontmatter — the same
|
|
53
|
+
* tiny-YAML subset the Python loader parses (`requires: [a, b]`). Returns []
|
|
54
|
+
* when absent or when there is no frontmatter. Block-sequence form is not
|
|
55
|
+
* supported (kept in lockstep with the Python parser).
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseRequiresField(md: string): string[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source-spec grammar for `canary overlay add <source>` (mirrors the
|
|
3
|
+
* `github:owner/repo` / git-URL / local-path forms used elsewhere in the
|
|
4
|
+
* ecosystem). Parsing is net-new — there is no existing engine parser.
|
|
5
|
+
*/
|
|
6
|
+
export interface ParsedSource {
|
|
7
|
+
/** Argument handed to `git clone` (URL or local path). */
|
|
8
|
+
cloneUrl: string;
|
|
9
|
+
/** Registry key + clone directory name: `<owner>-<repo>` (or path basename). */
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
/** Raised when a source spec does not match any accepted form. */
|
|
13
|
+
export declare class SourceSpecError extends Error {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse a source spec into a clone URL and overlay name. Throws
|
|
18
|
+
* {@link SourceSpecError} on anything that does not match an accepted form.
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseSource(raw: string): ParsedSource;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canary-test-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Canary — AI-powered test automation agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -10,21 +10,45 @@
|
|
|
10
10
|
"bin": {
|
|
11
11
|
"canary": "./bin/canary.js"
|
|
12
12
|
},
|
|
13
|
+
"exports": {
|
|
14
|
+
"./reporter": {
|
|
15
|
+
"types": "./dist/reporters/testtracker.d.ts",
|
|
16
|
+
"require": "./dist/reporters/testtracker.js",
|
|
17
|
+
"default": "./dist/reporters/testtracker.js"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@playwright/test": ">=1.42.0"
|
|
23
|
+
},
|
|
24
|
+
"peerDependenciesMeta": {
|
|
25
|
+
"@playwright/test": {
|
|
26
|
+
"optional": true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
13
29
|
"scripts": {
|
|
14
|
-
"build": "tsc",
|
|
30
|
+
"build": "tsc && node scripts/build-engine.mjs",
|
|
15
31
|
"prepare": "npm run build",
|
|
16
|
-
"
|
|
17
|
-
"test": "npm run build && node --test \"scripts/__tests__/*.test.js\""
|
|
32
|
+
"test": "tsc && node --test \"scripts/__tests__/*.test.js\""
|
|
18
33
|
},
|
|
19
34
|
"engines": {
|
|
20
35
|
"node": ">=18"
|
|
21
36
|
},
|
|
22
37
|
"files": [
|
|
23
|
-
"bin/",
|
|
38
|
+
"bin/canary.js",
|
|
24
39
|
"dist/",
|
|
25
|
-
"
|
|
40
|
+
"agent/frameworks/registry.json"
|
|
26
41
|
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
44
|
+
"@supabase/supabase-js": "^2.45.0",
|
|
45
|
+
"commander": "^15.0.0",
|
|
46
|
+
"js-yaml": "^5.2.2",
|
|
47
|
+
"picocolors": "^1.1.1",
|
|
48
|
+
"zod": "^4.4.3"
|
|
49
|
+
},
|
|
27
50
|
"devDependencies": {
|
|
51
|
+
"@playwright/test": "^1.61.1",
|
|
28
52
|
"@types/node": "^22.0.0",
|
|
29
53
|
"typescript": "^5.6.0"
|
|
30
54
|
}
|
package/bin/canary
DELETED
|
Binary file
|