@runcofactor/cli 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -0
- package/dist/api-catalog-GFGCLVS4.js +401 -0
- package/dist/api-catalog-GFGCLVS4.js.map +1 -0
- package/dist/attachment-SJC7D7FY.js +276 -0
- package/dist/attachment-SJC7D7FY.js.map +1 -0
- package/dist/auth-EN6NQCJY.js +31 -0
- package/dist/auth-EN6NQCJY.js.map +1 -0
- package/dist/bin.js +7 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-3CZU7XCS.js +272 -0
- package/dist/chunk-3CZU7XCS.js.map +1 -0
- package/dist/chunk-4NRDONSC.js +1048 -0
- package/dist/chunk-4NRDONSC.js.map +1 -0
- package/dist/chunk-4PNOV7OV.js +51980 -0
- package/dist/chunk-4PNOV7OV.js.map +1 -0
- package/dist/chunk-52IO7ACJ.js +2941 -0
- package/dist/chunk-52IO7ACJ.js.map +1 -0
- package/dist/chunk-6LEZCKKV.js +663 -0
- package/dist/chunk-6LEZCKKV.js.map +1 -0
- package/dist/chunk-A4OFTUVV.js +170 -0
- package/dist/chunk-A4OFTUVV.js.map +1 -0
- package/dist/chunk-B2VISN2V.js +2459 -0
- package/dist/chunk-B2VISN2V.js.map +1 -0
- package/dist/chunk-CLAWT7BJ.js +271 -0
- package/dist/chunk-CLAWT7BJ.js.map +1 -0
- package/dist/chunk-GKY2KXO5.js +281 -0
- package/dist/chunk-GKY2KXO5.js.map +1 -0
- package/dist/chunk-I4QLI3JO.js +413 -0
- package/dist/chunk-I4QLI3JO.js.map +1 -0
- package/dist/chunk-IMPZAX6L.js +180 -0
- package/dist/chunk-IMPZAX6L.js.map +1 -0
- package/dist/chunk-J6SH7GQT.js +241 -0
- package/dist/chunk-J6SH7GQT.js.map +1 -0
- package/dist/chunk-JDBCZ3Z2.js +298 -0
- package/dist/chunk-JDBCZ3Z2.js.map +1 -0
- package/dist/chunk-JICSYFRJ.js +46 -0
- package/dist/chunk-JICSYFRJ.js.map +1 -0
- package/dist/chunk-JWHYXEA2.js +32 -0
- package/dist/chunk-JWHYXEA2.js.map +1 -0
- package/dist/chunk-N7HFALX3.js +88 -0
- package/dist/chunk-N7HFALX3.js.map +1 -0
- package/dist/chunk-Q4BO4Z5V.js +178 -0
- package/dist/chunk-Q4BO4Z5V.js.map +1 -0
- package/dist/chunk-QFDOWABX.js +195 -0
- package/dist/chunk-QFDOWABX.js.map +1 -0
- package/dist/chunk-SJJW77WN.js +605 -0
- package/dist/chunk-SJJW77WN.js.map +1 -0
- package/dist/chunk-VKVL7WBN.js +50 -0
- package/dist/chunk-VKVL7WBN.js.map +1 -0
- package/dist/chunk-WOHL4FTB.js +353 -0
- package/dist/chunk-WOHL4FTB.js.map +1 -0
- package/dist/chunk-XQXVG7RA.js +426 -0
- package/dist/chunk-XQXVG7RA.js.map +1 -0
- package/dist/chunk-ZD3KHESI.js +94 -0
- package/dist/chunk-ZD3KHESI.js.map +1 -0
- package/dist/credential-COLTBV6D.js +450 -0
- package/dist/credential-COLTBV6D.js.map +1 -0
- package/dist/credential-template-EEPYFG7R.js +291 -0
- package/dist/credential-template-EEPYFG7R.js.map +1 -0
- package/dist/debug-workflow-KOVH5GOP.js +259 -0
- package/dist/debug-workflow-KOVH5GOP.js.map +1 -0
- package/dist/dist-44PZR7BQ.js +423 -0
- package/dist/dist-44PZR7BQ.js.map +1 -0
- package/dist/docs-3SSORHB5.js +366 -0
- package/dist/docs-3SSORHB5.js.map +1 -0
- package/dist/doctor-OTSIWV6G.js +132 -0
- package/dist/doctor-OTSIWV6G.js.map +1 -0
- package/dist/feature-flag-T2LRZME3.js +283 -0
- package/dist/feature-flag-T2LRZME3.js.map +1 -0
- package/dist/file-type-S3UUBVCT.js +4205 -0
- package/dist/file-type-S3UUBVCT.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +982 -0
- package/dist/index.js.map +1 -0
- package/dist/init-TGWRECT2.js +208 -0
- package/dist/init-TGWRECT2.js.map +1 -0
- package/dist/install-DXSNWZKO.js +295 -0
- package/dist/install-DXSNWZKO.js.map +1 -0
- package/dist/issues-XIZOPX6T.js +564 -0
- package/dist/issues-XIZOPX6T.js.map +1 -0
- package/dist/knobs-NBINZIUB.js +295 -0
- package/dist/knobs-NBINZIUB.js.map +1 -0
- package/dist/list-3TDHK637.js +59 -0
- package/dist/list-3TDHK637.js.map +1 -0
- package/dist/local-HICVD6PU.js +100 -0
- package/dist/local-HICVD6PU.js.map +1 -0
- package/dist/login-UOIUXVUK.js +315 -0
- package/dist/login-UOIUXVUK.js.map +1 -0
- package/dist/mcp-54IJEOUA.js +1956 -0
- package/dist/mcp-54IJEOUA.js.map +1 -0
- package/dist/pdf-extract-ABWV3L5X.js +13 -0
- package/dist/pdf-extract-ABWV3L5X.js.map +1 -0
- package/dist/pdfjs-DQ2YA5S3.js +41561 -0
- package/dist/pdfjs-DQ2YA5S3.js.map +1 -0
- package/dist/property-UXLOLPTD.js +701 -0
- package/dist/property-UXLOLPTD.js.map +1 -0
- package/dist/property-skill-guide.md +189 -0
- package/dist/psql-BYUUNLGG.js +115 -0
- package/dist/psql-BYUUNLGG.js.map +1 -0
- package/dist/record-4TR2UIIS.js +233 -0
- package/dist/record-4TR2UIIS.js.map +1 -0
- package/dist/redis-35VBUYW7.js +116 -0
- package/dist/redis-35VBUYW7.js.map +1 -0
- package/dist/release-OOLTUBUT.js +354 -0
- package/dist/release-OOLTUBUT.js.map +1 -0
- package/dist/sandbox-XGSUU6G7.js +2046 -0
- package/dist/sandbox-XGSUU6G7.js.map +1 -0
- package/dist/scenarios-GWLJEGHA.js +280 -0
- package/dist/scenarios-GWLJEGHA.js.map +1 -0
- package/dist/session-MYFNZPRP.js +2414 -0
- package/dist/session-MYFNZPRP.js.map +1 -0
- package/dist/skill-KM34LDMO.js +1839 -0
- package/dist/skill-KM34LDMO.js.map +1 -0
- package/dist/skills/debug-run/SKILL.md +262 -0
- package/dist/skills/debug-run/history/1.0.0.md +244 -0
- package/dist/skills/debug-run/history/1.0.1.md +245 -0
- package/dist/skills/debug-run/history/1.0.2.md +254 -0
- package/dist/skills/debug-run/history/1.1.0.md +262 -0
- package/dist/skills/debug-run/version.json +3 -0
- package/dist/spec-RA3CFEKG.js +951 -0
- package/dist/spec-RA3CFEKG.js.map +1 -0
- package/dist/src-LB4HBTJQ.js +1516 -0
- package/dist/src-LB4HBTJQ.js.map +1 -0
- package/dist/src-N3TPKGTY.js +244 -0
- package/dist/src-N3TPKGTY.js.map +1 -0
- package/dist/src-UVDX5UP6.js +17 -0
- package/dist/src-UVDX5UP6.js.map +1 -0
- package/dist/start-PSEQSQA3.js +151 -0
- package/dist/start-PSEQSQA3.js.map +1 -0
- package/dist/tag-YY6YAABQ.js +22 -0
- package/dist/tag-YY6YAABQ.js.map +1 -0
- package/dist/task-IQC3YR5K.js +922 -0
- package/dist/task-IQC3YR5K.js.map +1 -0
- package/dist/test-suite-DZNJFXDW.js +394 -0
- package/dist/test-suite-DZNJFXDW.js.map +1 -0
- package/dist/trace-HXIW7U2D.js +14 -0
- package/dist/trace-HXIW7U2D.js.map +1 -0
- package/dist/triage-ORG45R5W.js +16 -0
- package/dist/triage-ORG45R5W.js.map +1 -0
- package/dist/upgrade-L2VYMP2V.js +61 -0
- package/dist/upgrade-L2VYMP2V.js.map +1 -0
- package/dist/workflow-XYFH5LTP.js +3708 -0
- package/dist/workflow-XYFH5LTP.js.map +1 -0
- package/dist/workflow-cache-NQJQFWMV.js +253 -0
- package/dist/workflow-cache-NQJQFWMV.js.map +1 -0
- package/dist/workflow-debug-BGD4UIXU.js +927 -0
- package/dist/workflow-debug-BGD4UIXU.js.map +1 -0
- package/dist/workflow-proposals-MTUDX46O.js +382 -0
- package/dist/workflow-proposals-MTUDX46O.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { createRequire as __cr } from "module"; const require = __cr(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
apiRequest
|
|
4
|
+
} from "./chunk-WOHL4FTB.js";
|
|
5
|
+
import {
|
|
6
|
+
getArgValue,
|
|
7
|
+
hasFlag,
|
|
8
|
+
resolveConfig
|
|
9
|
+
} from "./chunk-A4OFTUVV.js";
|
|
10
|
+
|
|
11
|
+
// src/triage.ts
|
|
12
|
+
import process from "process";
|
|
13
|
+
var TERMINAL = [
|
|
14
|
+
"completed",
|
|
15
|
+
"failed",
|
|
16
|
+
"canceled",
|
|
17
|
+
"skipped",
|
|
18
|
+
"timeout"
|
|
19
|
+
];
|
|
20
|
+
function printHelp() {
|
|
21
|
+
console.log(`canary triage \u2014 AI clustering of test suite failures (beta)
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
canary triage get <executionId|latest> [--cluster <clusterId>] [--json]
|
|
25
|
+
canary triage failures <executionId|latest> [--cluster <clusterId>] [--new-only] [--json]
|
|
26
|
+
canary triage wait <executionId|latest> [--timeout <seconds>] [--json]
|
|
27
|
+
|
|
28
|
+
executionId is the test-suite id from \`canary test-suite list\`.
|
|
29
|
+
Use \`latest\` to auto-resolve the most recent test suite.
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
--json Emit raw JSON instead of formatted output
|
|
33
|
+
--cluster <id> Restrict output to one cluster
|
|
34
|
+
--new-only (failures) Show only newly-failing tests
|
|
35
|
+
--timeout <secs> (wait) Max seconds to wait for terminal status (default 600)
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
canary triage wait latest Block until triage finishes on the most recent suite
|
|
39
|
+
canary triage get latest Show full clustered detail
|
|
40
|
+
canary triage get <id> --cluster <clusterId> Drill into one cluster
|
|
41
|
+
canary triage failures <id> --new-only List only newly-failing tests
|
|
42
|
+
`);
|
|
43
|
+
}
|
|
44
|
+
async function resolveLatestExecutionId(apiUrl, token) {
|
|
45
|
+
const result = await apiRequest(
|
|
46
|
+
apiUrl,
|
|
47
|
+
token,
|
|
48
|
+
"GET",
|
|
49
|
+
"/workflows/test-executions?page=1&pageSize=1&sortBy=createdAt&sortOrder=desc"
|
|
50
|
+
);
|
|
51
|
+
if (!result.ok) return null;
|
|
52
|
+
return result.data?.[0]?.id ?? null;
|
|
53
|
+
}
|
|
54
|
+
async function resolveExecutionId(rawId, apiUrl, token) {
|
|
55
|
+
if (rawId !== "latest") return rawId;
|
|
56
|
+
const id = await resolveLatestExecutionId(apiUrl, token);
|
|
57
|
+
if (!id) {
|
|
58
|
+
console.error("Error: no recent test-suite executions found to resolve `latest`.");
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
console.log(`Resolved latest test-suite execution: ${id}
|
|
62
|
+
`);
|
|
63
|
+
return id;
|
|
64
|
+
}
|
|
65
|
+
async function isTriageEnabledForExecution(apiUrl, token, executionId) {
|
|
66
|
+
try {
|
|
67
|
+
const result = await apiRequest(
|
|
68
|
+
apiUrl,
|
|
69
|
+
token,
|
|
70
|
+
"GET",
|
|
71
|
+
`/triage/by-execution/${executionId}`
|
|
72
|
+
);
|
|
73
|
+
return result.ok === true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function fetchTriage(apiUrl, token, executionId) {
|
|
79
|
+
const result = await apiRequest(
|
|
80
|
+
apiUrl,
|
|
81
|
+
token,
|
|
82
|
+
"GET",
|
|
83
|
+
`/triage/by-execution/${executionId}`
|
|
84
|
+
);
|
|
85
|
+
if (!result.ok) {
|
|
86
|
+
console.error(`Error: ${result.error ?? "unknown"}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
return result.triage ?? null;
|
|
90
|
+
}
|
|
91
|
+
function printCluster(c, isMostPressing = false) {
|
|
92
|
+
const star = isMostPressing ? " \u2605" : "";
|
|
93
|
+
const flagNew = c.newFailureCount > 0 ? ` (${c.newFailureCount} NEW)` : "";
|
|
94
|
+
console.log(
|
|
95
|
+
`#${c.rank}${star} ${c.title}
|
|
96
|
+
category=${c.category} severity=${c.severity} failures=${c.failureCount}${flagNew}`
|
|
97
|
+
);
|
|
98
|
+
if (c.summaryMd) console.log("\n" + c.summaryMd);
|
|
99
|
+
if (c.suggestedActionMd) console.log("\nSuggested next step:\n" + c.suggestedActionMd);
|
|
100
|
+
if (c.failures.length > 0) {
|
|
101
|
+
console.log("\nFailures:");
|
|
102
|
+
for (const f of c.failures) {
|
|
103
|
+
const tag = f.isNew ? " [NEW]" : "";
|
|
104
|
+
console.log(` - ${f.workflowName ?? f.workflowRunId.slice(0, 8)}${tag}`);
|
|
105
|
+
if (f.errorMessage) {
|
|
106
|
+
const trimmed = f.errorMessage.split("\n")[0]?.slice(0, 200);
|
|
107
|
+
if (trimmed) console.log(` ${trimmed}`);
|
|
108
|
+
}
|
|
109
|
+
if (f.rationale) console.log(` why: ${f.rationale}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
console.log(`
|
|
113
|
+
cluster id: ${c.id}`);
|
|
114
|
+
}
|
|
115
|
+
function printTriageSummary(t, executionId) {
|
|
116
|
+
console.log(`triage ${t.id} \u2014 status=${t.status}`);
|
|
117
|
+
console.log(
|
|
118
|
+
`failures=${t.failureCount} (new=${t.newFailureCount}) clusters=${t.clusters.length}` + (t.diagnosticsTimedOut ? " [partial diagnostics]" : "")
|
|
119
|
+
);
|
|
120
|
+
if (t.summaryMd) console.log("\n" + t.summaryMd);
|
|
121
|
+
if (t.errorMessage) console.log(`error: ${t.errorMessage}`);
|
|
122
|
+
if (t.clusters.length === 0) return;
|
|
123
|
+
const sorted = [...t.clusters].sort((a, b) => a.rank - b.rank);
|
|
124
|
+
for (const c of sorted) {
|
|
125
|
+
console.log("\n" + "\u2500".repeat(60));
|
|
126
|
+
printCluster(c, t.mostPressingClusterId === c.id);
|
|
127
|
+
}
|
|
128
|
+
console.log("\n" + "\u2500".repeat(60));
|
|
129
|
+
console.log("Next:");
|
|
130
|
+
console.log(
|
|
131
|
+
` canary triage get ${executionId} --cluster <clusterId> drill into a single cluster`
|
|
132
|
+
);
|
|
133
|
+
console.log(
|
|
134
|
+
` canary triage failures ${executionId} [--new-only] flat failure list`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
async function handleGet(argv, apiUrl, token) {
|
|
138
|
+
const rawId = argv[0];
|
|
139
|
+
if (!rawId || rawId.startsWith("--")) {
|
|
140
|
+
console.error("Error: missing <executionId>. Use `latest` to auto-resolve the most recent suite.");
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const json = hasFlag(argv, "--json");
|
|
144
|
+
const clusterId = getArgValue(argv, "--cluster");
|
|
145
|
+
const executionId = await resolveExecutionId(rawId, apiUrl, token);
|
|
146
|
+
const triage = await fetchTriage(apiUrl, token, executionId);
|
|
147
|
+
if (json) {
|
|
148
|
+
if (clusterId && triage) {
|
|
149
|
+
const cluster = triage.clusters.find((c) => c.id === clusterId) ?? null;
|
|
150
|
+
console.log(JSON.stringify(cluster, null, 2));
|
|
151
|
+
} else {
|
|
152
|
+
console.log(JSON.stringify(triage, null, 2));
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!triage) {
|
|
157
|
+
console.log("No triage run for this execution yet.");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (clusterId) {
|
|
161
|
+
const cluster = triage.clusters.find((c) => c.id === clusterId);
|
|
162
|
+
if (!cluster) {
|
|
163
|
+
console.error(`Error: cluster ${clusterId} not found in triage ${triage.id}`);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
printCluster(cluster, triage.mostPressingClusterId === cluster.id);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
printTriageSummary(triage, executionId);
|
|
170
|
+
}
|
|
171
|
+
async function handleFailures(argv, apiUrl, token) {
|
|
172
|
+
const rawId = argv[0];
|
|
173
|
+
if (!rawId || rawId.startsWith("--")) {
|
|
174
|
+
console.error("Error: missing <executionId>. Use `latest` to auto-resolve the most recent suite.");
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
const json = hasFlag(argv, "--json");
|
|
178
|
+
const newOnly = hasFlag(argv, "--new-only");
|
|
179
|
+
const clusterId = getArgValue(argv, "--cluster");
|
|
180
|
+
const executionId = await resolveExecutionId(rawId, apiUrl, token);
|
|
181
|
+
const triage = await fetchTriage(apiUrl, token, executionId);
|
|
182
|
+
if (!triage) {
|
|
183
|
+
if (json) console.log("[]");
|
|
184
|
+
else console.log("No triage run for this execution yet.");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const allFailures = [];
|
|
188
|
+
for (const c of triage.clusters) {
|
|
189
|
+
if (clusterId && c.id !== clusterId) continue;
|
|
190
|
+
for (const f of c.failures) {
|
|
191
|
+
if (newOnly && !f.isNew) continue;
|
|
192
|
+
allFailures.push({ ...f, clusterId: c.id, clusterTitle: c.title });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (json) {
|
|
196
|
+
console.log(JSON.stringify(allFailures, null, 2));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (allFailures.length === 0) {
|
|
200
|
+
console.log("No failures matched.");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
for (const f of allFailures) {
|
|
204
|
+
const tag = f.isNew ? " [NEW]" : "";
|
|
205
|
+
console.log(
|
|
206
|
+
` ${f.workflowName ?? f.workflowRunId.slice(0, 8)}${tag} \u2192 "${f.clusterTitle}"`
|
|
207
|
+
);
|
|
208
|
+
if (f.errorMessage) {
|
|
209
|
+
const trimmed = f.errorMessage.split("\n")[0]?.slice(0, 200);
|
|
210
|
+
if (trimmed) console.log(` ${trimmed}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function handleWait(argv, apiUrl, token) {
|
|
215
|
+
const rawId = argv[0];
|
|
216
|
+
if (!rawId || rawId.startsWith("--")) {
|
|
217
|
+
console.error("Error: missing <executionId>. Use `latest` to auto-resolve the most recent suite.");
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
const json = hasFlag(argv, "--json");
|
|
221
|
+
const timeoutSec = Number.parseInt(getArgValue(argv, "--timeout") ?? "600", 10);
|
|
222
|
+
const executionId = await resolveExecutionId(rawId, apiUrl, token);
|
|
223
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
224
|
+
let last = null;
|
|
225
|
+
while (Date.now() < deadline) {
|
|
226
|
+
last = await fetchTriage(apiUrl, token, executionId);
|
|
227
|
+
if (last && TERMINAL.includes(last.status)) {
|
|
228
|
+
if (json) console.log(JSON.stringify(last, null, 2));
|
|
229
|
+
else printTriageSummary(last, executionId);
|
|
230
|
+
if (last.status === "failed") process.exit(2);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
await new Promise((resolve) => setTimeout(resolve, 5e3));
|
|
234
|
+
}
|
|
235
|
+
if (json) {
|
|
236
|
+
console.log(JSON.stringify({ timedOut: true, last }, null, 2));
|
|
237
|
+
} else {
|
|
238
|
+
console.error(`Timed out after ${timeoutSec}s`);
|
|
239
|
+
if (last) printTriageSummary(last, executionId);
|
|
240
|
+
}
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
async function runTriage(argv) {
|
|
244
|
+
const [subCommand, ...rest] = argv;
|
|
245
|
+
if (!subCommand || subCommand === "help" || hasFlag(argv, "--help", "-h")) {
|
|
246
|
+
printHelp();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const { apiUrl, token } = await resolveConfig(argv);
|
|
250
|
+
switch (subCommand) {
|
|
251
|
+
case "get":
|
|
252
|
+
await handleGet(rest, apiUrl, token);
|
|
253
|
+
break;
|
|
254
|
+
case "failures":
|
|
255
|
+
await handleFailures(rest, apiUrl, token);
|
|
256
|
+
break;
|
|
257
|
+
case "wait":
|
|
258
|
+
await handleWait(rest, apiUrl, token);
|
|
259
|
+
break;
|
|
260
|
+
default:
|
|
261
|
+
console.error(`Unknown sub-command: ${subCommand}`);
|
|
262
|
+
printHelp();
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export {
|
|
268
|
+
isTriageEnabledForExecution,
|
|
269
|
+
runTriage
|
|
270
|
+
};
|
|
271
|
+
//# sourceMappingURL=chunk-CLAWT7BJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/triage.ts"],"sourcesContent":["/**\n * Canary CLI — `triage`\n *\n * Read-only access to AI triage data for a test suite execution.\n * canary triage get <executionId> [--cluster <id>] [--json]\n * canary triage failures <executionId> [--cluster <id>] [--new-only] [--json]\n * canary triage wait <executionId> [--timeout 600] [--json]\n */\n\nimport process from 'node:process';\nimport { resolveConfig, getArgValue, hasFlag } from './auth.js';\nimport { apiRequest } from './cli-helpers.js';\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low';\n\ntype TriageFailure = {\n workflowRunId: string;\n workflowName: string | null;\n errorMessage: string | null;\n isNew: boolean;\n rationale: string | null;\n testRunId: string | null;\n};\n\ntype TriageCluster = {\n id: string;\n rank: number;\n title: string;\n category: string;\n severity: Severity;\n failureCount: number;\n newFailureCount: number;\n summaryMd: string | null;\n suggestedActionMd: string | null;\n representativeFailureId: string | null;\n failures: TriageFailure[];\n};\n\ntype TriageRun = {\n id: string;\n status:\n | 'pending'\n | 'waiting'\n | 'running'\n | 'completed'\n | 'failed'\n | 'canceled'\n | 'skipped'\n | 'timeout';\n summaryMd: string | null;\n failureCount: number;\n newFailureCount: number;\n diagnosticsPending: number;\n diagnosticsTotal: number;\n diagnosticsTimedOut: boolean;\n mostPressingClusterId: string | null;\n errorMessage: string | null;\n startedAt: string | null;\n finishedAt: string | null;\n clusters: TriageCluster[];\n};\n\ntype TriageResponse = { ok: boolean; error?: string; triage?: TriageRun | null };\n\nconst TERMINAL: TriageRun['status'][] = [\n 'completed',\n 'failed',\n 'canceled',\n 'skipped',\n 'timeout',\n];\n\nfunction printHelp(): void {\n console.log(`canary triage — AI clustering of test suite failures (beta)\n\nUsage:\n canary triage get <executionId|latest> [--cluster <clusterId>] [--json]\n canary triage failures <executionId|latest> [--cluster <clusterId>] [--new-only] [--json]\n canary triage wait <executionId|latest> [--timeout <seconds>] [--json]\n\nexecutionId is the test-suite id from \\`canary test-suite list\\`.\nUse \\`latest\\` to auto-resolve the most recent test suite.\n\nOptions:\n --json Emit raw JSON instead of formatted output\n --cluster <id> Restrict output to one cluster\n --new-only (failures) Show only newly-failing tests\n --timeout <secs> (wait) Max seconds to wait for terminal status (default 600)\n\nExamples:\n canary triage wait latest Block until triage finishes on the most recent suite\n canary triage get latest Show full clustered detail\n canary triage get <id> --cluster <clusterId> Drill into one cluster\n canary triage failures <id> --new-only List only newly-failing tests\n`);\n}\n\ntype TestExecutionListItem = { id: string };\ntype TestExecutionListResponse = {\n ok: boolean;\n error?: string;\n data?: TestExecutionListItem[];\n};\n\nasync function resolveLatestExecutionId(\n apiUrl: string,\n token: string\n): Promise<string | null> {\n const result = await apiRequest<TestExecutionListResponse>(\n apiUrl,\n token,\n 'GET',\n '/workflows/test-executions?page=1&pageSize=1&sortBy=createdAt&sortOrder=desc'\n );\n if (!result.ok) return null;\n return result.data?.[0]?.id ?? null;\n}\n\nasync function resolveExecutionId(\n rawId: string,\n apiUrl: string,\n token: string\n): Promise<string> {\n if (rawId !== 'latest') return rawId;\n const id = await resolveLatestExecutionId(apiUrl, token);\n if (!id) {\n console.error('Error: no recent test-suite executions found to resolve `latest`.');\n process.exit(1);\n }\n console.log(`Resolved latest test-suite execution: ${id}\\n`);\n return id;\n}\n\n/**\n * Probe whether the org has the triage.beta feature flag enabled by hitting\n * the by-execution endpoint. Returns true when the API responds 200 (with or\n * without a triage row), false on TRIAGE_NOT_ENABLED or any other error.\n *\n * Used by `canary test-suite run` / `get` to gate the triage cross-link hint.\n */\nexport async function isTriageEnabledForExecution(\n apiUrl: string,\n token: string,\n executionId: string\n): Promise<boolean> {\n try {\n const result = await apiRequest<TriageResponse & { code?: string }>(\n apiUrl,\n token,\n 'GET',\n `/triage/by-execution/${executionId}`\n );\n return result.ok === true;\n } catch {\n return false;\n }\n}\n\nasync function fetchTriage(\n apiUrl: string,\n token: string,\n executionId: string\n): Promise<TriageRun | null> {\n const result = await apiRequest<TriageResponse>(\n apiUrl,\n token,\n 'GET',\n `/triage/by-execution/${executionId}`\n );\n if (!result.ok) {\n console.error(`Error: ${result.error ?? 'unknown'}`);\n process.exit(1);\n }\n return result.triage ?? null;\n}\n\nfunction printCluster(c: TriageCluster, isMostPressing = false): void {\n const star = isMostPressing ? ' ★' : '';\n const flagNew = c.newFailureCount > 0 ? ` (${c.newFailureCount} NEW)` : '';\n console.log(\n `#${c.rank}${star} ${c.title}\\ncategory=${c.category} severity=${c.severity} failures=${c.failureCount}${flagNew}`\n );\n if (c.summaryMd) console.log('\\n' + c.summaryMd);\n if (c.suggestedActionMd) console.log('\\nSuggested next step:\\n' + c.suggestedActionMd);\n if (c.failures.length > 0) {\n console.log('\\nFailures:');\n for (const f of c.failures) {\n const tag = f.isNew ? ' [NEW]' : '';\n console.log(` - ${f.workflowName ?? f.workflowRunId.slice(0, 8)}${tag}`);\n if (f.errorMessage) {\n const trimmed = f.errorMessage.split('\\n')[0]?.slice(0, 200);\n if (trimmed) console.log(` ${trimmed}`);\n }\n if (f.rationale) console.log(` why: ${f.rationale}`);\n }\n }\n console.log(`\\n cluster id: ${c.id}`);\n}\n\nfunction printTriageSummary(t: TriageRun, executionId: string): void {\n console.log(`triage ${t.id} — status=${t.status}`);\n console.log(\n `failures=${t.failureCount} (new=${t.newFailureCount}) clusters=${t.clusters.length}` +\n (t.diagnosticsTimedOut ? ' [partial diagnostics]' : '')\n );\n if (t.summaryMd) console.log('\\n' + t.summaryMd);\n if (t.errorMessage) console.log(`error: ${t.errorMessage}`);\n if (t.clusters.length === 0) return;\n\n const sorted = [...t.clusters].sort((a, b) => a.rank - b.rank);\n for (const c of sorted) {\n console.log('\\n' + '─'.repeat(60));\n printCluster(c, t.mostPressingClusterId === c.id);\n }\n\n console.log('\\n' + '─'.repeat(60));\n console.log('Next:');\n console.log(\n ` canary triage get ${executionId} --cluster <clusterId> drill into a single cluster`\n );\n console.log(\n ` canary triage failures ${executionId} [--new-only] flat failure list`\n );\n}\n\nasync function handleGet(argv: string[], apiUrl: string, token: string): Promise<void> {\n const rawId = argv[0];\n if (!rawId || rawId.startsWith('--')) {\n console.error('Error: missing <executionId>. Use `latest` to auto-resolve the most recent suite.');\n process.exit(1);\n }\n const json = hasFlag(argv, '--json');\n const clusterId = getArgValue(argv, '--cluster');\n const executionId = await resolveExecutionId(rawId, apiUrl, token);\n const triage = await fetchTriage(apiUrl, token, executionId);\n\n if (json) {\n if (clusterId && triage) {\n const cluster = triage.clusters.find((c) => c.id === clusterId) ?? null;\n console.log(JSON.stringify(cluster, null, 2));\n } else {\n console.log(JSON.stringify(triage, null, 2));\n }\n return;\n }\n\n if (!triage) {\n console.log('No triage run for this execution yet.');\n return;\n }\n if (clusterId) {\n const cluster = triage.clusters.find((c) => c.id === clusterId);\n if (!cluster) {\n console.error(`Error: cluster ${clusterId} not found in triage ${triage.id}`);\n process.exit(1);\n }\n printCluster(cluster, triage.mostPressingClusterId === cluster.id);\n return;\n }\n printTriageSummary(triage, executionId);\n}\n\nasync function handleFailures(argv: string[], apiUrl: string, token: string): Promise<void> {\n const rawId = argv[0];\n if (!rawId || rawId.startsWith('--')) {\n console.error('Error: missing <executionId>. Use `latest` to auto-resolve the most recent suite.');\n process.exit(1);\n }\n const json = hasFlag(argv, '--json');\n const newOnly = hasFlag(argv, '--new-only');\n const clusterId = getArgValue(argv, '--cluster');\n const executionId = await resolveExecutionId(rawId, apiUrl, token);\n const triage = await fetchTriage(apiUrl, token, executionId);\n\n if (!triage) {\n if (json) console.log('[]');\n else console.log('No triage run for this execution yet.');\n return;\n }\n\n const allFailures: Array<TriageFailure & { clusterId: string; clusterTitle: string }> = [];\n for (const c of triage.clusters) {\n if (clusterId && c.id !== clusterId) continue;\n for (const f of c.failures) {\n if (newOnly && !f.isNew) continue;\n allFailures.push({ ...f, clusterId: c.id, clusterTitle: c.title });\n }\n }\n\n if (json) {\n console.log(JSON.stringify(allFailures, null, 2));\n return;\n }\n if (allFailures.length === 0) {\n console.log('No failures matched.');\n return;\n }\n for (const f of allFailures) {\n const tag = f.isNew ? ' [NEW]' : '';\n console.log(\n ` ${f.workflowName ?? f.workflowRunId.slice(0, 8)}${tag} → \"${f.clusterTitle}\"`\n );\n if (f.errorMessage) {\n const trimmed = f.errorMessage.split('\\n')[0]?.slice(0, 200);\n if (trimmed) console.log(` ${trimmed}`);\n }\n }\n}\n\nasync function handleWait(argv: string[], apiUrl: string, token: string): Promise<void> {\n const rawId = argv[0];\n if (!rawId || rawId.startsWith('--')) {\n console.error('Error: missing <executionId>. Use `latest` to auto-resolve the most recent suite.');\n process.exit(1);\n }\n const json = hasFlag(argv, '--json');\n const timeoutSec = Number.parseInt(getArgValue(argv, '--timeout') ?? '600', 10);\n const executionId = await resolveExecutionId(rawId, apiUrl, token);\n const deadline = Date.now() + timeoutSec * 1000;\n\n let last: TriageRun | null = null;\n while (Date.now() < deadline) {\n last = await fetchTriage(apiUrl, token, executionId);\n if (last && TERMINAL.includes(last.status)) {\n if (json) console.log(JSON.stringify(last, null, 2));\n else printTriageSummary(last, executionId);\n if (last.status === 'failed') process.exit(2);\n return;\n }\n await new Promise((resolve) => setTimeout(resolve, 5000));\n }\n\n if (json) {\n console.log(JSON.stringify({ timedOut: true, last }, null, 2));\n } else {\n console.error(`Timed out after ${timeoutSec}s`);\n if (last) printTriageSummary(last, executionId);\n }\n process.exit(1);\n}\n\nexport async function runTriage(argv: string[]): Promise<void> {\n const [subCommand, ...rest] = argv;\n\n if (!subCommand || subCommand === 'help' || hasFlag(argv, '--help', '-h')) {\n printHelp();\n return;\n }\n\n const { apiUrl, token } = await resolveConfig(argv);\n\n switch (subCommand) {\n case 'get':\n await handleGet(rest, apiUrl, token);\n break;\n case 'failures':\n await handleFailures(rest, apiUrl, token);\n break;\n case 'wait':\n await handleWait(rest, apiUrl, token);\n break;\n default:\n console.error(`Unknown sub-command: ${subCommand}`);\n printHelp();\n process.exit(1);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { createRequire as __cr } from "module"; const require = __cr(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
isTriageEnabledForExecution
|
|
4
|
+
} from "./chunk-CLAWT7BJ.js";
|
|
5
|
+
import {
|
|
6
|
+
resolveConfig
|
|
7
|
+
} from "./chunk-A4OFTUVV.js";
|
|
8
|
+
|
|
9
|
+
// src/remote-test.ts
|
|
10
|
+
import process from "process";
|
|
11
|
+
import { createParser } from "eventsource-parser";
|
|
12
|
+
function getArgValue(argv, key) {
|
|
13
|
+
const index = argv.indexOf(key);
|
|
14
|
+
if (index === -1 || index >= argv.length - 1) return void 0;
|
|
15
|
+
return argv[index + 1];
|
|
16
|
+
}
|
|
17
|
+
function hasFlag(argv, ...flags) {
|
|
18
|
+
return flags.some((flag) => argv.includes(flag));
|
|
19
|
+
}
|
|
20
|
+
function formatFailedTests(failedTests, appUrl) {
|
|
21
|
+
if (failedTests.length === 0 || !appUrl) return null;
|
|
22
|
+
const lines = ["", "Failed tests:"];
|
|
23
|
+
for (const t of failedTests) {
|
|
24
|
+
lines.push(` \u2717 ${t.name}`);
|
|
25
|
+
lines.push(` ${appUrl}/runs/tests/${t.testRunId}`);
|
|
26
|
+
}
|
|
27
|
+
return lines.join("\n");
|
|
28
|
+
}
|
|
29
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
30
|
+
async function resolveTestConfig(apiUrl, token, propertyArg, environmentArg) {
|
|
31
|
+
if (!propertyArg && !environmentArg) return {};
|
|
32
|
+
const res = await fetch(`${apiUrl}/workflows/test-config`, {
|
|
33
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const text = await res.text();
|
|
37
|
+
throw new Error(`Failed to fetch test config: ${res.status} ${text}`);
|
|
38
|
+
}
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
if (!data.ok || !data.properties) {
|
|
41
|
+
throw new Error(`Failed to fetch test config: ${data.error ?? "Unknown error"}`);
|
|
42
|
+
}
|
|
43
|
+
const { properties } = data;
|
|
44
|
+
let property;
|
|
45
|
+
if (propertyArg) {
|
|
46
|
+
if (UUID_RE.test(propertyArg)) {
|
|
47
|
+
property = properties.find((p) => p.id === propertyArg);
|
|
48
|
+
} else {
|
|
49
|
+
property = properties.find(
|
|
50
|
+
(p) => p.name.toLowerCase() === propertyArg.toLowerCase()
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (!property) {
|
|
54
|
+
const available = properties.map((p) => ` - ${p.name} (${p.id})`).join("\n");
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Application "${propertyArg}" not found.
|
|
57
|
+
Available applications:
|
|
58
|
+
${available}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
} else if (environmentArg) {
|
|
62
|
+
if (properties.length === 0) {
|
|
63
|
+
throw new Error("No applications found in your organization.");
|
|
64
|
+
}
|
|
65
|
+
if (properties.length > 1) {
|
|
66
|
+
const available = properties.map((p) => ` - ${p.name} (${p.id})`).join("\n");
|
|
67
|
+
throw new Error(
|
|
68
|
+
`--environment requires --application when multiple applications exist.
|
|
69
|
+
Available applications:
|
|
70
|
+
${available}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
property = properties[0];
|
|
74
|
+
}
|
|
75
|
+
const result = {};
|
|
76
|
+
if (property) {
|
|
77
|
+
result.propertyId = property.id;
|
|
78
|
+
if (environmentArg) {
|
|
79
|
+
let env;
|
|
80
|
+
if (UUID_RE.test(environmentArg)) {
|
|
81
|
+
env = property.environments.find((e) => e.id === environmentArg);
|
|
82
|
+
} else {
|
|
83
|
+
env = property.environments.find(
|
|
84
|
+
(e) => e.name.toLowerCase() === environmentArg.toLowerCase()
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (!env) {
|
|
88
|
+
const available = property.environments.map((e) => ` - ${e.name}${e.isDefault ? " (default)" : ""} (${e.id})`).join("\n");
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Environment "${environmentArg}" not found for application "${property.name}".
|
|
91
|
+
Available environments:
|
|
92
|
+
${available}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
result.environmentId = env.id;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
async function runRemoteTest(argv) {
|
|
101
|
+
const { apiUrl, token } = await resolveConfig(argv);
|
|
102
|
+
const tag = getArgValue(argv, "--tag");
|
|
103
|
+
const namePattern = getArgValue(argv, "--name-pattern");
|
|
104
|
+
const propertyArg = getArgValue(argv, "--application") ?? getArgValue(argv, "--property");
|
|
105
|
+
const environmentArg = getArgValue(argv, "--environment");
|
|
106
|
+
const verbose = hasFlag(argv, "--verbose", "-v");
|
|
107
|
+
let resolvedConfig = {};
|
|
108
|
+
if (propertyArg || environmentArg) {
|
|
109
|
+
try {
|
|
110
|
+
resolvedConfig = await resolveTestConfig(apiUrl, token, propertyArg, environmentArg);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(`${err instanceof Error ? err.message : String(err)}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
console.log("Starting remote workflow tests...");
|
|
117
|
+
if (resolvedConfig.propertyId) console.log(` Application: ${propertyArg}`);
|
|
118
|
+
if (resolvedConfig.environmentId) console.log(` Environment: ${environmentArg}`);
|
|
119
|
+
if (tag) console.log(` Filtering by tag: ${tag}`);
|
|
120
|
+
if (namePattern) console.log(` Filtering by name pattern: ${namePattern}`);
|
|
121
|
+
console.log("");
|
|
122
|
+
const queryParams = new URLSearchParams();
|
|
123
|
+
if (tag) queryParams.set("tag", tag);
|
|
124
|
+
if (namePattern) queryParams.set("namePattern", namePattern);
|
|
125
|
+
const triggerUrl = `${apiUrl}/workflows/test-runs${queryParams.toString() ? `?${queryParams}` : ""}`;
|
|
126
|
+
const body = {};
|
|
127
|
+
if (resolvedConfig.propertyId) body.propertyId = resolvedConfig.propertyId;
|
|
128
|
+
if (resolvedConfig.environmentId) body.environmentId = resolvedConfig.environmentId;
|
|
129
|
+
let triggerRes;
|
|
130
|
+
try {
|
|
131
|
+
triggerRes = await fetch(triggerUrl, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: {
|
|
134
|
+
Authorization: `Bearer ${token}`,
|
|
135
|
+
"Content-Type": "application/json"
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify(body)
|
|
138
|
+
});
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error(`Failed to connect to API: ${err}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
if (!triggerRes.ok) {
|
|
144
|
+
const errorText = await triggerRes.text();
|
|
145
|
+
console.error(`Failed to start tests: ${triggerRes.status}`);
|
|
146
|
+
console.error(errorText);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
const triggerData = await triggerRes.json();
|
|
150
|
+
if (!triggerData.ok || !triggerData.suiteId) {
|
|
151
|
+
console.error(`Failed to start tests: ${triggerData.error ?? "Unknown error"}`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
const { suiteId, jobId, appUrl } = triggerData;
|
|
155
|
+
if (verbose) {
|
|
156
|
+
console.log(`Suite ID: ${suiteId}`);
|
|
157
|
+
console.log(`Job ID: ${jobId}`);
|
|
158
|
+
console.log("");
|
|
159
|
+
}
|
|
160
|
+
const streamUrl = `${apiUrl}/workflows/test-runs/stream?suiteId=${suiteId}`;
|
|
161
|
+
let streamRes;
|
|
162
|
+
try {
|
|
163
|
+
streamRes = await fetch(streamUrl, {
|
|
164
|
+
headers: {
|
|
165
|
+
Authorization: `Bearer ${token}`,
|
|
166
|
+
Accept: "text/event-stream"
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error(`Failed to connect to event stream: ${err}`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
if (!streamRes.ok || !streamRes.body) {
|
|
174
|
+
console.error(`Failed to connect to event stream: ${streamRes.status}`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
let exitCode = 0;
|
|
178
|
+
let hasCompleted = false;
|
|
179
|
+
const workflowNames = /* @__PURE__ */ new Map();
|
|
180
|
+
const failedTests = [];
|
|
181
|
+
let totalWorkflows = 0;
|
|
182
|
+
let completedWorkflows = 0;
|
|
183
|
+
let failedWorkflows = 0;
|
|
184
|
+
let successfulWorkflows = 0;
|
|
185
|
+
const parser = createParser({
|
|
186
|
+
onEvent: (event) => {
|
|
187
|
+
if (!event.data) return;
|
|
188
|
+
try {
|
|
189
|
+
const data = JSON.parse(event.data);
|
|
190
|
+
if (verbose) {
|
|
191
|
+
console.log(`[${event.event}] ${JSON.stringify(data)}`);
|
|
192
|
+
}
|
|
193
|
+
if (event.event === "workflow-test") {
|
|
194
|
+
const testEvent = data;
|
|
195
|
+
const { status, workflowId, message, errorMessage } = testEvent;
|
|
196
|
+
const name = workflowNames.get(workflowId) || message?.replace(/^Flow "(.+)" .*$/, "$1") || workflowId;
|
|
197
|
+
if (message?.startsWith('Flow "')) {
|
|
198
|
+
const match = message.match(/^Flow "(.+?)" /);
|
|
199
|
+
if (match) workflowNames.set(workflowId, match[1]);
|
|
200
|
+
}
|
|
201
|
+
if (!verbose) {
|
|
202
|
+
if (status === "success") {
|
|
203
|
+
console.log(` \u2713 ${name}`);
|
|
204
|
+
} else if (status === "failed") {
|
|
205
|
+
console.log(` \u2717 ${name}`);
|
|
206
|
+
if (errorMessage) {
|
|
207
|
+
console.log(` Error: ${errorMessage.slice(0, 200)}`);
|
|
208
|
+
}
|
|
209
|
+
failedTests.push({ name, testRunId: testEvent.testRunId });
|
|
210
|
+
exitCode = 1;
|
|
211
|
+
} else if (status === "running") {
|
|
212
|
+
} else if (status === "waiting") {
|
|
213
|
+
console.log(` \u23F3 ${name} (waiting for scheduled time)`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (event.event === "workflow-test-suite") {
|
|
218
|
+
const suiteEvent = data;
|
|
219
|
+
totalWorkflows = suiteEvent.totalWorkflows;
|
|
220
|
+
completedWorkflows = suiteEvent.completedWorkflows;
|
|
221
|
+
failedWorkflows = suiteEvent.failedWorkflows;
|
|
222
|
+
successfulWorkflows = suiteEvent.successfulWorkflows;
|
|
223
|
+
if (suiteEvent.status === "completed") {
|
|
224
|
+
hasCompleted = true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (event.event === "error") {
|
|
228
|
+
const errorData = data;
|
|
229
|
+
console.error(`Stream error: ${errorData.error ?? "Unknown error"}`);
|
|
230
|
+
exitCode = 1;
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
const reader = streamRes.body.getReader();
|
|
237
|
+
const decoder = new TextDecoder();
|
|
238
|
+
try {
|
|
239
|
+
while (!hasCompleted) {
|
|
240
|
+
const { done, value } = await reader.read();
|
|
241
|
+
if (done) break;
|
|
242
|
+
parser.feed(decoder.decode(value, { stream: true }));
|
|
243
|
+
}
|
|
244
|
+
} finally {
|
|
245
|
+
reader.releaseLock();
|
|
246
|
+
}
|
|
247
|
+
console.log("");
|
|
248
|
+
console.log("\u2500".repeat(50));
|
|
249
|
+
if (totalWorkflows === 0) {
|
|
250
|
+
console.log("No workflows found matching the filter criteria.");
|
|
251
|
+
process.exit(0);
|
|
252
|
+
}
|
|
253
|
+
const passRate = totalWorkflows > 0 ? Math.round(successfulWorkflows / totalWorkflows * 100) : 0;
|
|
254
|
+
if (failedWorkflows > 0) {
|
|
255
|
+
console.log(`FAILED: ${failedWorkflows} of ${totalWorkflows} workflows failed (${passRate}% pass rate)`);
|
|
256
|
+
exitCode = 1;
|
|
257
|
+
} else {
|
|
258
|
+
console.log(`PASSED: ${successfulWorkflows} of ${totalWorkflows} workflows passed`);
|
|
259
|
+
}
|
|
260
|
+
const waitingWorkflows = totalWorkflows - completedWorkflows;
|
|
261
|
+
if (waitingWorkflows > 0) {
|
|
262
|
+
console.log(`Note: ${waitingWorkflows} workflow(s) are still waiting (scheduled for later)`);
|
|
263
|
+
}
|
|
264
|
+
const failedSection = formatFailedTests(failedTests, appUrl);
|
|
265
|
+
if (failedSection) {
|
|
266
|
+
console.log(failedSection);
|
|
267
|
+
}
|
|
268
|
+
if (failedWorkflows > 0) {
|
|
269
|
+
const triageEnabled = await isTriageEnabledForExecution(apiUrl, token, suiteId);
|
|
270
|
+
if (triageEnabled) {
|
|
271
|
+
console.log("");
|
|
272
|
+
console.log(`Triage: canary triage wait ${suiteId}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
process.exit(exitCode);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export {
|
|
279
|
+
runRemoteTest
|
|
280
|
+
};
|
|
281
|
+
//# sourceMappingURL=chunk-GKY2KXO5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/remote-test.ts"],"sourcesContent":["/**\n * Remote Test Command\n *\n * Triggers workflow tests on the Canary platform and streams results via SSE.\n * Designed for CI/CD pipelines - exits with code 0 on success, 1 on failure.\n *\n * Usage:\n * canary test-suite run [options]\n *\n * Options:\n * --token <key> API key (or set CANARY_API_TOKEN env var)\n * --api-url <url> API URL (default: https://api.trycanary.ai)\n * --tag <tag> Filter workflows by tag\n * --name-pattern <pat> Filter workflows by name pattern\n * --verbose, -v Show all events (not just results)\n */\n\nimport process from \"node:process\";\nimport { createParser, type EventSourceMessage } from \"eventsource-parser\";\nimport { resolveConfig } from \"./auth\";\nimport { isTriageEnabledForExecution } from \"./triage.js\";\n\ntype WorkflowTestStatus = \"queued\" | \"running\" | \"waiting\" | \"success\" | \"failed\";\n\ntype WorkflowTestEvent = {\n type: \"workflow-test\";\n suiteId: string;\n workflowId: string;\n testRunId: string;\n workflowRunId: string | null;\n status: WorkflowTestStatus;\n startedAt: string | null;\n finishedAt: string | null;\n durationMs: number | null;\n errorMessage?: string | null;\n linkedIssueId?: string | null;\n message?: string;\n resumeAt?: string | null;\n segmentIndex?: number | null;\n totalSegments?: number | null;\n};\n\ntype WorkflowTestSuiteEvent = {\n type: \"workflow-test-suite\";\n suiteId: string;\n status: \"running\" | \"completed\";\n startedAt: string;\n finishedAt: string | null;\n durationMs: number | null;\n totalWorkflows: number;\n completedWorkflows: number;\n failedWorkflows: number;\n successfulWorkflows: number;\n errorMessage?: string | null;\n message?: string;\n};\n\ntype TriggerResponse = {\n ok: boolean;\n jobId?: string;\n suiteId?: string;\n startedAt?: string;\n appUrl?: string;\n error?: string;\n};\n\nfunction getArgValue(argv: string[], key: string): string | undefined {\n const index = argv.indexOf(key);\n if (index === -1 || index >= argv.length - 1) return undefined;\n return argv[index + 1];\n}\n\nfunction hasFlag(argv: string[], ...flags: string[]): boolean {\n return flags.some((flag) => argv.includes(flag));\n}\n\nfunction buildQueryParams(tag?: string, namePattern?: string): string {\n const params = new URLSearchParams();\n if (tag) params.set(\"tag\", tag);\n if (namePattern) params.set(\"namePattern\", namePattern);\n return params.toString();\n}\n\nfunction extractWorkflowName(message: string | undefined, workflowId: string): string {\n if (!message) return workflowId;\n const match = message.match(/^Flow \"(.+?)\" /);\n return match ? match[1] : workflowId;\n}\n\ntype FailedTest = { name: string; testRunId: string };\n\nfunction formatFailedTests(\n failedTests: FailedTest[],\n appUrl: string | undefined,\n): string | null {\n if (failedTests.length === 0 || !appUrl) return null;\n\n const lines = [\"\", \"Failed tests:\"];\n for (const t of failedTests) {\n lines.push(` \\u2717 ${t.name}`);\n lines.push(` ${appUrl}/runs/tests/${t.testRunId}`);\n }\n return lines.join(\"\\n\");\n}\n\nfunction formatSummary(stats: {\n totalWorkflows: number;\n successfulWorkflows: number;\n failedWorkflows: number;\n completedWorkflows: number;\n}): { message: string; exitCode: number } {\n const { totalWorkflows, successfulWorkflows, failedWorkflows, completedWorkflows } = stats;\n\n if (totalWorkflows === 0) {\n return { message: \"No workflows found matching the filter criteria.\", exitCode: 0 };\n }\n\n const passRate = Math.round((successfulWorkflows / totalWorkflows) * 100);\n const waitingWorkflows = totalWorkflows - completedWorkflows;\n\n let message: string;\n let exitCode: number;\n\n if (failedWorkflows > 0) {\n message = `FAILED: ${failedWorkflows} of ${totalWorkflows} workflows failed (${passRate}% pass rate)`;\n exitCode = 1;\n } else {\n message = `PASSED: ${successfulWorkflows} of ${totalWorkflows} workflows passed`;\n exitCode = 0;\n }\n\n if (waitingWorkflows > 0) {\n message += `\\nNote: ${waitingWorkflows} workflow(s) are still waiting (scheduled for later)`;\n }\n\n return { message, exitCode };\n}\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ntype TestConfigProperty = {\n id: string;\n name: string;\n baseUrl: string;\n environments: Array<{ id: string; name: string; url: string; isDefault: boolean }>;\n};\n\ntype TestConfigResponse = {\n ok: boolean;\n properties: TestConfigProperty[];\n error?: string;\n};\n\ntype ResolvedConfig = {\n propertyId?: string;\n environmentId?: string;\n};\n\nasync function resolveTestConfig(\n apiUrl: string,\n token: string,\n propertyArg?: string,\n environmentArg?: string,\n): Promise<ResolvedConfig> {\n if (!propertyArg && !environmentArg) return {};\n\n const res = await fetch(`${apiUrl}/workflows/test-config`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Failed to fetch test config: ${res.status} ${text}`);\n }\n\n const data = (await res.json()) as TestConfigResponse;\n if (!data.ok || !data.properties) {\n throw new Error(`Failed to fetch test config: ${data.error ?? \"Unknown error\"}`);\n }\n\n const { properties } = data;\n let property: TestConfigProperty | undefined;\n\n if (propertyArg) {\n if (UUID_RE.test(propertyArg)) {\n property = properties.find((p) => p.id === propertyArg);\n } else {\n property = properties.find(\n (p) => p.name.toLowerCase() === propertyArg.toLowerCase()\n );\n }\n if (!property) {\n const available = properties.map((p) => ` - ${p.name} (${p.id})`).join(\"\\n\");\n throw new Error(\n `Application \"${propertyArg}\" not found.\\nAvailable applications:\\n${available}`\n );\n }\n } else if (environmentArg) {\n // --environment without --application: auto-resolve if single application\n if (properties.length === 0) {\n throw new Error(\"No applications found in your organization.\");\n }\n if (properties.length > 1) {\n const available = properties.map((p) => ` - ${p.name} (${p.id})`).join(\"\\n\");\n throw new Error(\n `--environment requires --application when multiple applications exist.\\nAvailable applications:\\n${available}`\n );\n }\n property = properties[0];\n }\n\n const result: ResolvedConfig = {};\n if (property) {\n result.propertyId = property.id;\n\n if (environmentArg) {\n let env: TestConfigProperty[\"environments\"][number] | undefined;\n if (UUID_RE.test(environmentArg)) {\n env = property.environments.find((e) => e.id === environmentArg);\n } else {\n env = property.environments.find(\n (e) => e.name.toLowerCase() === environmentArg.toLowerCase()\n );\n }\n if (!env) {\n const available = property.environments\n .map((e) => ` - ${e.name}${e.isDefault ? \" (default)\" : \"\"} (${e.id})`)\n .join(\"\\n\");\n throw new Error(\n `Environment \"${environmentArg}\" not found for application \"${property.name}\".\\nAvailable environments:\\n${available}`\n );\n }\n result.environmentId = env.id;\n }\n }\n\n return result;\n}\n\n// Export internals for testing\nexport const __internals = {\n getArgValue,\n hasFlag,\n buildQueryParams,\n extractWorkflowName,\n formatSummary,\n formatFailedTests,\n resolveTestConfig,\n UUID_RE,\n};\n\nexport async function runRemoteTest(argv: string[]): Promise<void> {\n const { apiUrl, token } = await resolveConfig(argv);\n\n const tag = getArgValue(argv, \"--tag\");\n const namePattern = getArgValue(argv, \"--name-pattern\");\n // \"--application\" is the user-facing flag; \"--property\" stays as a backwards-compat alias.\n const propertyArg = getArgValue(argv, \"--application\") ?? getArgValue(argv, \"--property\");\n const environmentArg = getArgValue(argv, \"--environment\");\n const verbose = hasFlag(argv, \"--verbose\", \"-v\");\n\n // 0. Resolve property/environment if specified\n let resolvedConfig: ResolvedConfig = {};\n if (propertyArg || environmentArg) {\n try {\n resolvedConfig = await resolveTestConfig(apiUrl, token, propertyArg, environmentArg);\n } catch (err) {\n console.error(`${err instanceof Error ? err.message : String(err)}`);\n process.exit(1);\n }\n }\n\n console.log(\"Starting remote workflow tests...\");\n if (resolvedConfig.propertyId) console.log(` Application: ${propertyArg}`);\n if (resolvedConfig.environmentId) console.log(` Environment: ${environmentArg}`);\n if (tag) console.log(` Filtering by tag: ${tag}`);\n if (namePattern) console.log(` Filtering by name pattern: ${namePattern}`);\n console.log(\"\");\n\n // 1. Trigger test run\n const queryParams = new URLSearchParams();\n if (tag) queryParams.set(\"tag\", tag);\n if (namePattern) queryParams.set(\"namePattern\", namePattern);\n\n const triggerUrl = `${apiUrl}/workflows/test-runs${queryParams.toString() ? `?${queryParams}` : \"\"}`;\n\n const body: Record<string, string> = {};\n if (resolvedConfig.propertyId) body.propertyId = resolvedConfig.propertyId;\n if (resolvedConfig.environmentId) body.environmentId = resolvedConfig.environmentId;\n\n let triggerRes: Response;\n try {\n triggerRes = await fetch(triggerUrl, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n } catch (err) {\n console.error(`Failed to connect to API: ${err}`);\n process.exit(1);\n }\n\n if (!triggerRes.ok) {\n const errorText = await triggerRes.text();\n console.error(`Failed to start tests: ${triggerRes.status}`);\n console.error(errorText);\n process.exit(1);\n }\n\n const triggerData = (await triggerRes.json()) as TriggerResponse;\n if (!triggerData.ok || !triggerData.suiteId) {\n console.error(`Failed to start tests: ${triggerData.error ?? \"Unknown error\"}`);\n process.exit(1);\n }\n\n const { suiteId, jobId, appUrl } = triggerData;\n if (verbose) {\n console.log(`Suite ID: ${suiteId}`);\n console.log(`Job ID: ${jobId}`);\n console.log(\"\");\n }\n\n // 2. Subscribe to SSE stream\n const streamUrl = `${apiUrl}/workflows/test-runs/stream?suiteId=${suiteId}`;\n\n let streamRes: Response;\n try {\n streamRes = await fetch(streamUrl, {\n headers: {\n Authorization: `Bearer ${token}`,\n Accept: \"text/event-stream\",\n },\n });\n } catch (err) {\n console.error(`Failed to connect to event stream: ${err}`);\n process.exit(1);\n }\n\n if (!streamRes.ok || !streamRes.body) {\n console.error(`Failed to connect to event stream: ${streamRes.status}`);\n process.exit(1);\n }\n\n // Track state\n let exitCode = 0;\n let hasCompleted = false;\n const workflowNames = new Map<string, string>();\n const failedTests: FailedTest[] = [];\n let totalWorkflows = 0;\n let completedWorkflows = 0;\n let failedWorkflows = 0;\n let successfulWorkflows = 0;\n\n // 3. Parse SSE events\n const parser = createParser({\n onEvent: (event: EventSourceMessage) => {\n if (!event.data) return;\n\n try {\n const data = JSON.parse(event.data) as\n | WorkflowTestEvent\n | WorkflowTestSuiteEvent\n | { error?: string };\n\n if (verbose) {\n console.log(`[${event.event}] ${JSON.stringify(data)}`);\n }\n\n if (event.event === \"workflow-test\") {\n const testEvent = data as WorkflowTestEvent;\n const { status, workflowId, message, errorMessage } = testEvent;\n const name = workflowNames.get(workflowId) || message?.replace(/^Flow \"(.+)\" .*$/, \"$1\") || workflowId;\n\n if (message?.startsWith('Flow \"')) {\n const match = message.match(/^Flow \"(.+?)\" /);\n if (match) workflowNames.set(workflowId, match[1]);\n }\n\n if (!verbose) {\n if (status === \"success\") {\n console.log(` \\u2713 ${name}`);\n } else if (status === \"failed\") {\n console.log(` \\u2717 ${name}`);\n if (errorMessage) {\n console.log(` Error: ${errorMessage.slice(0, 200)}`);\n }\n failedTests.push({ name, testRunId: testEvent.testRunId });\n exitCode = 1;\n } else if (status === \"running\") {\n // Don't log running status in non-verbose mode\n } else if (status === \"waiting\") {\n console.log(` \\u23F3 ${name} (waiting for scheduled time)`);\n }\n }\n }\n\n if (event.event === \"workflow-test-suite\") {\n const suiteEvent = data as WorkflowTestSuiteEvent;\n totalWorkflows = suiteEvent.totalWorkflows;\n completedWorkflows = suiteEvent.completedWorkflows;\n failedWorkflows = suiteEvent.failedWorkflows;\n successfulWorkflows = suiteEvent.successfulWorkflows;\n\n if (suiteEvent.status === \"completed\") {\n hasCompleted = true;\n }\n }\n\n if (event.event === \"error\") {\n const errorData = data as { error?: string };\n console.error(`Stream error: ${errorData.error ?? \"Unknown error\"}`);\n exitCode = 1;\n }\n } catch {\n // Ignore parse errors for keepalive, etc.\n }\n },\n });\n\n const reader = streamRes.body.getReader();\n const decoder = new TextDecoder();\n\n try {\n while (!hasCompleted) {\n const { done, value } = await reader.read();\n if (done) break;\n parser.feed(decoder.decode(value, { stream: true }));\n }\n } finally {\n reader.releaseLock();\n }\n\n // 4. Print summary\n console.log(\"\");\n console.log(\"─\".repeat(50));\n\n if (totalWorkflows === 0) {\n console.log(\"No workflows found matching the filter criteria.\");\n process.exit(0);\n }\n\n const passRate = totalWorkflows > 0 ? Math.round((successfulWorkflows / totalWorkflows) * 100) : 0;\n\n if (failedWorkflows > 0) {\n console.log(`FAILED: ${failedWorkflows} of ${totalWorkflows} workflows failed (${passRate}% pass rate)`);\n exitCode = 1;\n } else {\n console.log(`PASSED: ${successfulWorkflows} of ${totalWorkflows} workflows passed`);\n }\n\n const waitingWorkflows = totalWorkflows - completedWorkflows;\n if (waitingWorkflows > 0) {\n console.log(`Note: ${waitingWorkflows} workflow(s) are still waiting (scheduled for later)`);\n }\n\n const failedSection = formatFailedTests(failedTests, appUrl);\n if (failedSection) {\n console.log(failedSection);\n }\n\n if (failedWorkflows > 0) {\n const triageEnabled = await isTriageEnabledForExecution(apiUrl, token, suiteId);\n if (triageEnabled) {\n console.log(\"\");\n console.log(`Triage: canary triage wait ${suiteId}`);\n }\n }\n\n process.exit(exitCode);\n}\n"],"mappings":"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","names":[]}
|