qualty 0.1.2 → 0.1.4
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/bin/qualty.js +106 -3
- package/package.json +1 -1
package/bin/qualty.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
|
|
6
|
-
/** When unset, CLI
|
|
7
|
-
const DEFAULT_QUALTY_API_URL = "https://qualty-api-
|
|
6
|
+
/** When unset, CLI uses Qualty development API. Override for production: QUALTY_API_URL=https://qualty-api-production.up.railway.app or local: http://localhost:8000 */
|
|
7
|
+
const DEFAULT_QUALTY_API_URL = "https://qualty-api-development.up.railway.app";
|
|
8
8
|
|
|
9
9
|
function resolveApiUrl(args) {
|
|
10
10
|
return String(args.api || process.env.QUALTY_API_URL || DEFAULT_QUALTY_API_URL).replace(/\/$/, "");
|
|
@@ -39,7 +39,10 @@ function usage() {
|
|
|
39
39
|
"Usage:",
|
|
40
40
|
" qualty connect --project <project-id> [--port 3000] [--api https://your-api] [--token <bearer-token>]",
|
|
41
41
|
" qualty run --project <project-id> [--suite-id <suite-id>] [--ids <id1,id2>] [--api https://your-api] [--token <bearer-token>]",
|
|
42
|
-
" [--poll-interval 5] [--timeout 30] [--fail-on-failure true]",
|
|
42
|
+
" [--poll-interval 5] [--timeout 30] [--fail-on-failure true] [--no-view-logs]",
|
|
43
|
+
"",
|
|
44
|
+
" After each run, logs include step results, explanation, and final evaluator output (dashboard \"View logs\" data).",
|
|
45
|
+
" Pass --no-view-logs for a short log only (e.g. very large evaluator text).",
|
|
43
46
|
" qualty resolve --project <project-id> [--suite-id <suite-id>] [--ids <id1,id2>] [--json] [--api https://your-api] [--token <bearer-token>]",
|
|
44
47
|
"",
|
|
45
48
|
"Env vars:",
|
|
@@ -171,6 +174,97 @@ function sleep(ms) {
|
|
|
171
174
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
172
175
|
}
|
|
173
176
|
|
|
177
|
+
function truncateForCiLog(text, maxLen) {
|
|
178
|
+
const s = String(text ?? "");
|
|
179
|
+
if (s.length <= maxLen) return s;
|
|
180
|
+
return `${s.slice(0, maxLen)}\n[qualty] … (truncated, ${s.length - maxLen} more chars)`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function logPrefixedLines(prefix, text) {
|
|
184
|
+
const body = String(text ?? "").trimEnd();
|
|
185
|
+
if (!body) return;
|
|
186
|
+
for (const line of body.split("\n")) {
|
|
187
|
+
// eslint-disable-next-line no-console
|
|
188
|
+
console.log(`${prefix}${line}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Print the same kind of detail as Qualty "View logs": steps, explanation, evaluator (agent) output.
|
|
194
|
+
* Data comes from GET status payload fields on each combination (steps_json, explanation, agent_output).
|
|
195
|
+
*/
|
|
196
|
+
function printQualtyViewLogsReport(executionId, status) {
|
|
197
|
+
// eslint-disable-next-line no-console
|
|
198
|
+
console.log(`\n[qualty] ━━━ View logs: ${executionId} (${status.episode_name || "run"}) ━━━`);
|
|
199
|
+
if (status.url) {
|
|
200
|
+
// eslint-disable-next-line no-console
|
|
201
|
+
console.log(`[qualty] URL: ${status.url}`);
|
|
202
|
+
}
|
|
203
|
+
if (status.error) {
|
|
204
|
+
// eslint-disable-next-line no-console
|
|
205
|
+
console.log(`[qualty] Run error: ${truncateForCiLog(status.error, 4000)}`);
|
|
206
|
+
}
|
|
207
|
+
if (status.expected_behavior) {
|
|
208
|
+
// eslint-disable-next-line no-console
|
|
209
|
+
console.log(`[qualty] Expected behavior:\n[qualty] ${truncateForCiLog(status.expected_behavior, 6000).split("\n").join("\n[qualty] ")}`);
|
|
210
|
+
}
|
|
211
|
+
const combos = Array.isArray(status.combinations) ? status.combinations : [];
|
|
212
|
+
if (combos.length === 0) {
|
|
213
|
+
// eslint-disable-next-line no-console
|
|
214
|
+
console.log(
|
|
215
|
+
"[qualty] (No per-device breakdown in API response yet — open this run in the Qualty dashboard for full logs.)"
|
|
216
|
+
);
|
|
217
|
+
// eslint-disable-next-line no-console
|
|
218
|
+
console.log(`[qualty] ━━━ End view logs: ${executionId} ━━━\n`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
for (let i = 0; i < combos.length; i += 1) {
|
|
222
|
+
const c = combos[i];
|
|
223
|
+
const device = c.device ?? "?";
|
|
224
|
+
const comboStatus = c.status ?? (c.success === true ? "passed" : c.success === false ? "failed" : "?");
|
|
225
|
+
// eslint-disable-next-line no-console
|
|
226
|
+
console.log(`\n[qualty] --- Combination ${i + 1}/${combos.length} (${device} · ${comboStatus}) ---`);
|
|
227
|
+
const steps = Array.isArray(c.steps_json) ? c.steps_json : [];
|
|
228
|
+
if (steps.length > 0) {
|
|
229
|
+
// eslint-disable-next-line no-console
|
|
230
|
+
console.log(`[qualty] Steps (${steps.length}):`);
|
|
231
|
+
for (let j = 0; j < steps.length; j += 1) {
|
|
232
|
+
const s = steps[j];
|
|
233
|
+
const label =
|
|
234
|
+
(s.name && String(s.name).trim()) ||
|
|
235
|
+
(s.description && String(s.description).trim().slice(0, 100)) ||
|
|
236
|
+
`Step ${j + 1}`;
|
|
237
|
+
const st = s.status != null ? s.status : "?";
|
|
238
|
+
// eslint-disable-next-line no-console
|
|
239
|
+
console.log(`[qualty] ${j + 1}. [${st}] ${label}`);
|
|
240
|
+
if (s.description && String(s.description).trim() && String(s.description) !== String(s.name)) {
|
|
241
|
+
logPrefixedLines("[qualty] ", truncateForCiLog(s.description, 4000));
|
|
242
|
+
}
|
|
243
|
+
if (s.action) {
|
|
244
|
+
// eslint-disable-next-line no-console
|
|
245
|
+
console.log(`[qualty] action: ${truncateForCiLog(s.action, 2000)}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} else if (c.total_steps) {
|
|
249
|
+
// eslint-disable-next-line no-console
|
|
250
|
+
console.log(`[qualty] (Step list not available yet; ${c.total_steps} step(s) reported.)`);
|
|
251
|
+
}
|
|
252
|
+
if (c.explanation) {
|
|
253
|
+
// eslint-disable-next-line no-console
|
|
254
|
+
console.log(`[qualty] Explanation:\n[qualty] ${truncateForCiLog(c.explanation, 12000).split("\n").join("\n[qualty] ")}`);
|
|
255
|
+
}
|
|
256
|
+
const evaluator = c.agent_output ?? c.gpt_output;
|
|
257
|
+
if (evaluator) {
|
|
258
|
+
// eslint-disable-next-line no-console
|
|
259
|
+
console.log(
|
|
260
|
+
`[qualty] Final evaluator output:\n[qualty] ${truncateForCiLog(evaluator, 16000).split("\n").join("\n[qualty] ")}`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// eslint-disable-next-line no-console
|
|
265
|
+
console.log(`\n[qualty] ━━━ End view logs: ${executionId} ━━━\n`);
|
|
266
|
+
}
|
|
267
|
+
|
|
174
268
|
async function runCi(args) {
|
|
175
269
|
const apiUrl = resolveApiUrl(args);
|
|
176
270
|
const token = args.token || process.env.QUALTY_API_TOKEN;
|
|
@@ -183,6 +277,7 @@ async function runCi(args) {
|
|
|
183
277
|
const pollIntervalSec = Number(args["poll-interval"] || 5);
|
|
184
278
|
const timeoutMin = Number(args.timeout || 30);
|
|
185
279
|
const failOnFailure = parseBoolean(args["fail-on-failure"], true);
|
|
280
|
+
const noViewLogs = parseBoolean(args["no-view-logs"], false);
|
|
186
281
|
|
|
187
282
|
if (!token) {
|
|
188
283
|
throw new Error("Missing auth token. Pass --token or set QUALTY_API_TOKEN.");
|
|
@@ -282,6 +377,14 @@ async function runCi(args) {
|
|
|
282
377
|
);
|
|
283
378
|
}
|
|
284
379
|
|
|
380
|
+
if (!noViewLogs) {
|
|
381
|
+
// eslint-disable-next-line no-console
|
|
382
|
+
console.log(`[qualty] Detailed run output (same fields as dashboard "View logs"):`);
|
|
383
|
+
for (const executionId of executionJobIds) {
|
|
384
|
+
printQualtyViewLogsReport(executionId, finalStatuses[executionId] || {});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
285
388
|
// eslint-disable-next-line no-console
|
|
286
389
|
console.log(`[qualty] Summary: ${passed} passed, ${failed} failed.`);
|
|
287
390
|
if (failOnFailure && failed > 0) {
|