@plune-ai/cli 0.6.0 → 0.8.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 +2 -1
- package/dist/cli.cjs +915 -162
- package/dist/index.cjs +46 -19
- package/dist/index.d.cts +39 -424
- package/dist/index.d.ts +39 -424
- package/dist/index.js +46 -19
- package/dist/reporter-core.cjs +467 -0
- package/dist/reporter-core.d.cts +339 -0
- package/dist/reporter-core.d.ts +339 -0
- package/dist/reporter-core.js +423 -0
- package/package.json +12 -3
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// src/cli/api-url.ts
|
|
2
|
+
var DEFAULT_API_URL = "https://beta-api.plune.ai";
|
|
3
|
+
function resolveApiUrl(explicit) {
|
|
4
|
+
const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL;
|
|
5
|
+
return raw.replace(/\/+$/, "");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/cli/credentials.ts
|
|
9
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { dirname, join } from "path";
|
|
12
|
+
function credentialsFile() {
|
|
13
|
+
const base = process.env["XDG_CONFIG_HOME"]?.trim() || join(homedir(), ".config");
|
|
14
|
+
return join(base, "plune", "credentials.json");
|
|
15
|
+
}
|
|
16
|
+
function loadToken() {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(readFileSync(credentialsFile(), "utf-8"));
|
|
19
|
+
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/reporter-core/client.ts
|
|
26
|
+
var RETRY_DELAYS_MS = [300, 900];
|
|
27
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
28
|
+
async function detailOf(res) {
|
|
29
|
+
try {
|
|
30
|
+
const body = await res.json();
|
|
31
|
+
return typeof body.error === "string" ? body.error : "";
|
|
32
|
+
} catch {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function retryAfterMs(res) {
|
|
37
|
+
const raw = res.headers.get("retry-after");
|
|
38
|
+
if (raw === null) return null;
|
|
39
|
+
const seconds = Number(raw.trim());
|
|
40
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : null;
|
|
41
|
+
}
|
|
42
|
+
function classify(status) {
|
|
43
|
+
if (status === 401 || status === 403) return "auth";
|
|
44
|
+
if (status === 409) return "conflict";
|
|
45
|
+
if (status === 400) return "invalid";
|
|
46
|
+
if (status === 429 || status >= 500) return "unavailable";
|
|
47
|
+
return "other";
|
|
48
|
+
}
|
|
49
|
+
function createClient(opts) {
|
|
50
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
51
|
+
const wait = opts.wait ?? sleep;
|
|
52
|
+
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
53
|
+
const scrub = (text) => opts.token === "" ? text : text.split(opts.token).join("***");
|
|
54
|
+
async function post(path2, body) {
|
|
55
|
+
let last = {
|
|
56
|
+
kind: "unavailable",
|
|
57
|
+
status: null,
|
|
58
|
+
detail: "no attempt was made"
|
|
59
|
+
};
|
|
60
|
+
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt += 1) {
|
|
61
|
+
let res;
|
|
62
|
+
try {
|
|
63
|
+
res = await doFetch(`${base}${path2}`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${opts.token}` },
|
|
66
|
+
body: JSON.stringify(body)
|
|
67
|
+
});
|
|
68
|
+
} catch (err) {
|
|
69
|
+
last = { kind: "unavailable", status: null, detail: scrub(String(err)) };
|
|
70
|
+
if (attempt < RETRY_DELAYS_MS.length) await wait(RETRY_DELAYS_MS[attempt]);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (res.ok) return { ok: true, status: res.status, body: await res.json() };
|
|
74
|
+
const detail = scrub(await detailOf(res));
|
|
75
|
+
const kind = classify(res.status);
|
|
76
|
+
last = { kind, status: res.status, detail };
|
|
77
|
+
if (kind !== "unavailable") break;
|
|
78
|
+
if (attempt < RETRY_DELAYS_MS.length) {
|
|
79
|
+
await wait(retryAfterMs(res) ?? RETRY_DELAYS_MS[attempt]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { ok: false, ...last };
|
|
83
|
+
}
|
|
84
|
+
return { post };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/reporter-core/env.ts
|
|
88
|
+
var UNSUPPORTED_VARS = ["PLUNE_GROUP"];
|
|
89
|
+
function value(env, name) {
|
|
90
|
+
const raw = env[name]?.trim();
|
|
91
|
+
return raw === void 0 || raw === "" ? void 0 : raw;
|
|
92
|
+
}
|
|
93
|
+
function flag(env, name) {
|
|
94
|
+
const raw = value(env, name)?.toLowerCase();
|
|
95
|
+
return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
|
|
96
|
+
}
|
|
97
|
+
function count(env, name) {
|
|
98
|
+
const raw = value(env, name);
|
|
99
|
+
if (raw === void 0) return void 0;
|
|
100
|
+
const n = Number(raw);
|
|
101
|
+
return Number.isInteger(n) && n > 0 ? n : void 0;
|
|
102
|
+
}
|
|
103
|
+
function list(env, name) {
|
|
104
|
+
const raw = value(env, name);
|
|
105
|
+
if (raw === void 0) return void 0;
|
|
106
|
+
const items = raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
107
|
+
return items.length === 0 ? void 0 : items;
|
|
108
|
+
}
|
|
109
|
+
function readEnv(env = process.env) {
|
|
110
|
+
const externalKey = value(env, "PLUNE_RUN");
|
|
111
|
+
const token = value(env, "PLUNE_TOKEN");
|
|
112
|
+
const apiUrl = value(env, "PLUNE_API_URL");
|
|
113
|
+
const fallbackPath = value(env, "PLUNE_FALLBACK");
|
|
114
|
+
const batchSize = count(env, "PLUNE_BATCH_SIZE");
|
|
115
|
+
const title = value(env, "PLUNE_RUN_TITLE");
|
|
116
|
+
const environment = value(env, "PLUNE_ENV");
|
|
117
|
+
const labels = list(env, "PLUNE_LABELS");
|
|
118
|
+
const offerDiscovered = flag(env, "PLUNE_CREATE");
|
|
119
|
+
return {
|
|
120
|
+
config: {
|
|
121
|
+
...externalKey !== void 0 ? { externalKey } : {},
|
|
122
|
+
...token !== void 0 ? { token } : {},
|
|
123
|
+
...apiUrl !== void 0 ? { apiUrl } : {},
|
|
124
|
+
...fallbackPath !== void 0 ? { fallbackPath } : {},
|
|
125
|
+
...batchSize !== void 0 ? { batchSize } : {},
|
|
126
|
+
...title !== void 0 ? { title } : {},
|
|
127
|
+
...environment !== void 0 ? { environment } : {},
|
|
128
|
+
...labels !== void 0 ? { labels } : {},
|
|
129
|
+
// Only when set: `false` here would outrank a config file that asked for it, and an unset
|
|
130
|
+
// variable is not an instruction.
|
|
131
|
+
...offerDiscovered ? { offerDiscovered } : {}
|
|
132
|
+
},
|
|
133
|
+
// Two names for two situations that end the same way. `SHARED_RUN` says other processes are
|
|
134
|
+
// reporting into this run; `PROCEED` says the job will close it on its own schedule. Either
|
|
135
|
+
// way this process must not be the one to call it finished.
|
|
136
|
+
keepOpen: flag(env, "PLUNE_SHARED_RUN") || flag(env, "PLUNE_PROCEED"),
|
|
137
|
+
ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/reporter-core/fallback.ts
|
|
142
|
+
import * as fs from "fs";
|
|
143
|
+
import * as path from "path";
|
|
144
|
+
var DEFAULT_FALLBACK_PATH = path.join(".plune", "pending-results.jsonl");
|
|
145
|
+
function appendBatch(file, batch) {
|
|
146
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
147
|
+
const line = {
|
|
148
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
149
|
+
runId: batch.runId,
|
|
150
|
+
externalKey: batch.externalKey,
|
|
151
|
+
results: batch.results
|
|
152
|
+
};
|
|
153
|
+
fs.appendFileSync(file, `${JSON.stringify(line)}
|
|
154
|
+
`, "utf8");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/reporter-core/session.ts
|
|
158
|
+
var RESOLVE_CHUNK = 500;
|
|
159
|
+
var BATCH_MAX = 500;
|
|
160
|
+
var DISCOVER_MAX = 500;
|
|
161
|
+
var BATCH_DEFAULT = 100;
|
|
162
|
+
var EXPECTED_MAX = 1e4;
|
|
163
|
+
function chunk(items, size) {
|
|
164
|
+
const out = [];
|
|
165
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
function warnIfDropped(sent, run, log) {
|
|
169
|
+
const names = Object.keys(sent);
|
|
170
|
+
if (names.length === 0) return;
|
|
171
|
+
if (names.some((name) => run[name] != null)) return;
|
|
172
|
+
log(
|
|
173
|
+
`plune: this deployment stored none of ${names.join(", ")} \u2014 it is older than the fields. The run is otherwise reported in full.`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
async function startRun(passed, expected) {
|
|
177
|
+
const env = readEnv();
|
|
178
|
+
const stated = Object.fromEntries(Object.entries(passed).filter(([, v]) => v !== void 0));
|
|
179
|
+
const cfg = { ...env.config, ...stated };
|
|
180
|
+
const log = cfg.log ?? ((line) => void process.stderr.write(`${line}
|
|
181
|
+
`));
|
|
182
|
+
const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
|
|
183
|
+
const externalKey = cfg.externalKey ?? null;
|
|
184
|
+
const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
|
|
185
|
+
const offerDiscovered = cfg.offerDiscovered ?? false;
|
|
186
|
+
const token = cfg.token ?? loadToken() ?? "";
|
|
187
|
+
const client = createClient({
|
|
188
|
+
apiUrl: resolveApiUrl(cfg.apiUrl),
|
|
189
|
+
token,
|
|
190
|
+
...cfg.fetchImpl !== void 0 ? { fetchImpl: cfg.fetchImpl } : {}
|
|
191
|
+
});
|
|
192
|
+
const stats = {
|
|
193
|
+
accepted: 0,
|
|
194
|
+
duplicate: 0,
|
|
195
|
+
conflict: 0,
|
|
196
|
+
rejected: 0,
|
|
197
|
+
unresolved: 0,
|
|
198
|
+
offered: 0,
|
|
199
|
+
deferred: 0
|
|
200
|
+
};
|
|
201
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
202
|
+
let buffer = [];
|
|
203
|
+
const discoveries = /* @__PURE__ */ new Map();
|
|
204
|
+
let runId = null;
|
|
205
|
+
let joined = false;
|
|
206
|
+
let offline = token === "";
|
|
207
|
+
let done = false;
|
|
208
|
+
if (offline) {
|
|
209
|
+
log('plune: no API token \u2014 run "plune login" first. Results will be written to the fallback file.');
|
|
210
|
+
}
|
|
211
|
+
if (env.ignored.length > 0) {
|
|
212
|
+
log(`plune: ignoring ${env.ignored.join(", ")} \u2014 a run has nowhere to store them yet.`);
|
|
213
|
+
}
|
|
214
|
+
function defer(results) {
|
|
215
|
+
appendBatch(fallbackPath, { runId, externalKey, results });
|
|
216
|
+
stats.deferred += results.length;
|
|
217
|
+
}
|
|
218
|
+
async function resolveKeys(values) {
|
|
219
|
+
const unknown = values.filter((k) => !resolved.has(k.value));
|
|
220
|
+
if (unknown.length === 0 || offline) return;
|
|
221
|
+
for (const part of chunk(unknown, RESOLVE_CHUNK)) {
|
|
222
|
+
const out = await client.post(
|
|
223
|
+
"/v1/test-cases/resolve",
|
|
224
|
+
{ keys: part }
|
|
225
|
+
);
|
|
226
|
+
if (!out.ok) {
|
|
227
|
+
log(`plune: could not look up test cases (${out.detail || out.kind}).`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
for (const row of out.body.results) resolved.set(row.key.value, row.testCaseId);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!offline) {
|
|
234
|
+
if (expected !== void 0) await resolveKeys(expected.flat());
|
|
235
|
+
const declared = expected?.slice(0, EXPECTED_MAX) ?? [];
|
|
236
|
+
const configuration = declared.length === 0 ? void 0 : {
|
|
237
|
+
expected: declared.flatMap((keys) => {
|
|
238
|
+
const hit = keys.map((k) => resolved.get(k.value)).find((v) => typeof v === "string");
|
|
239
|
+
if (typeof hit === "string") return [{ testCaseId: hit }];
|
|
240
|
+
return keys[0] === void 0 ? [] : [{ externalKey: keys[0] }];
|
|
241
|
+
})
|
|
242
|
+
};
|
|
243
|
+
if (expected !== void 0 && expected.length > EXPECTED_MAX) {
|
|
244
|
+
log(`plune: ${expected.length} tests is more than the run configuration holds \u2014 reporting the first ${EXPECTED_MAX}.`);
|
|
245
|
+
}
|
|
246
|
+
const described = {
|
|
247
|
+
...cfg.title !== void 0 ? { title: cfg.title } : {},
|
|
248
|
+
...cfg.environment !== void 0 ? { environment: cfg.environment } : {},
|
|
249
|
+
...cfg.labels !== void 0 ? { labels: cfg.labels } : {}
|
|
250
|
+
};
|
|
251
|
+
const start = await client.post("/v1/runs", {
|
|
252
|
+
schemaVersion: 2,
|
|
253
|
+
kind: cfg.kind ?? "automated",
|
|
254
|
+
...externalKey !== null ? { externalKey } : {},
|
|
255
|
+
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
256
|
+
...configuration !== void 0 ? { configuration } : {},
|
|
257
|
+
...described
|
|
258
|
+
});
|
|
259
|
+
if (start.ok) {
|
|
260
|
+
runId = start.body.run.id;
|
|
261
|
+
joined = start.status === 200;
|
|
262
|
+
warnIfDropped(described, start.body.run, log);
|
|
263
|
+
} else {
|
|
264
|
+
log(`plune: could not start the run (${start.detail || start.kind}). Results will be written to ${fallbackPath}.`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function matchFor(result) {
|
|
268
|
+
if (result.testCaseId !== void 0) return { kind: "found", testCaseId: result.testCaseId };
|
|
269
|
+
let unanswered = false;
|
|
270
|
+
for (const key of result.keys) {
|
|
271
|
+
const hit = resolved.get(key.value);
|
|
272
|
+
if (typeof hit === "string") return { kind: "found", testCaseId: hit };
|
|
273
|
+
if (hit === void 0) unanswered = true;
|
|
274
|
+
}
|
|
275
|
+
return unanswered ? { kind: "unanswered" } : { kind: "unmatched" };
|
|
276
|
+
}
|
|
277
|
+
function remember(result) {
|
|
278
|
+
if (!offerDiscovered) return;
|
|
279
|
+
if (result.title === void 0 || result.specRef === void 0) return;
|
|
280
|
+
if (result.keys.length === 0) return;
|
|
281
|
+
discoveries.set(result.keys[0].value, {
|
|
282
|
+
keys: result.keys,
|
|
283
|
+
title: result.title,
|
|
284
|
+
source: result.source,
|
|
285
|
+
specRef: result.specRef,
|
|
286
|
+
rawStatus: result.rawStatus
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
async function offer() {
|
|
290
|
+
if (discoveries.size === 0 || offline || runId === null) return;
|
|
291
|
+
for (const batch of chunk([...discoveries.values()], DISCOVER_MAX)) {
|
|
292
|
+
const out = await client.post("/v1/review-items/discovered", {
|
|
293
|
+
discovered: batch,
|
|
294
|
+
runId
|
|
295
|
+
});
|
|
296
|
+
if (!out.ok) {
|
|
297
|
+
log(`plune: could not offer ${batch.length} unknown tests for review (${out.detail || out.kind}).`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
stats.offered += out.body.results.filter((r) => r.outcome === "queued").length;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async function flush() {
|
|
304
|
+
if (buffer.length === 0) return;
|
|
305
|
+
const pending = buffer;
|
|
306
|
+
buffer = [];
|
|
307
|
+
if (offline || runId === null) {
|
|
308
|
+
defer(pending);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
await resolveKeys(pending.flatMap((p) => p.keys));
|
|
312
|
+
const submissions = [];
|
|
313
|
+
const unanswered = [];
|
|
314
|
+
for (const result of pending) {
|
|
315
|
+
const match = matchFor(result);
|
|
316
|
+
if (match.kind === "found") {
|
|
317
|
+
const { keys: _keys, ...rest } = result;
|
|
318
|
+
submissions.push({ ...rest, testCaseId: match.testCaseId });
|
|
319
|
+
} else if (match.kind === "unmatched") {
|
|
320
|
+
stats.unresolved += 1;
|
|
321
|
+
remember(result);
|
|
322
|
+
} else {
|
|
323
|
+
unanswered.push(result);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (unanswered.length > 0) defer(unanswered);
|
|
327
|
+
if (submissions.length === 0) return;
|
|
328
|
+
const out = await client.post(
|
|
329
|
+
`/v1/runs/${runId}/results`,
|
|
330
|
+
{ results: submissions }
|
|
331
|
+
);
|
|
332
|
+
if (out.ok) {
|
|
333
|
+
stats.accepted += out.body.counts.accepted ?? 0;
|
|
334
|
+
stats.duplicate += out.body.counts.duplicate ?? 0;
|
|
335
|
+
stats.conflict += out.body.counts.conflict ?? 0;
|
|
336
|
+
stats.rejected += out.body.counts.rejected ?? 0;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (out.kind === "auth") {
|
|
340
|
+
log('plune: the API token was refused \u2014 run "plune login" with a fresh one.');
|
|
341
|
+
offline = true;
|
|
342
|
+
} else if (out.kind === "conflict") {
|
|
343
|
+
log(`plune: this run is closed \u2014 ${out.detail}`);
|
|
344
|
+
} else {
|
|
345
|
+
log(`plune: could not send results (${out.detail || out.kind}).`);
|
|
346
|
+
}
|
|
347
|
+
defer(submissions);
|
|
348
|
+
}
|
|
349
|
+
function summarise() {
|
|
350
|
+
const parts = [`${stats.accepted} accepted`];
|
|
351
|
+
if (stats.duplicate > 0) parts.push(`${stats.duplicate} already reported`);
|
|
352
|
+
if (stats.conflict > 0) parts.push(`${stats.conflict} conflicting`);
|
|
353
|
+
if (stats.rejected > 0) parts.push(`${stats.rejected} rejected`);
|
|
354
|
+
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
355
|
+
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
356
|
+
if (stats.deferred > 0) parts.push(`${stats.deferred} written to ${fallbackPath}`);
|
|
357
|
+
log(`plune: ${parts.join(" \xB7 ")}`);
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
get runId() {
|
|
361
|
+
return runId;
|
|
362
|
+
},
|
|
363
|
+
get joined() {
|
|
364
|
+
return joined;
|
|
365
|
+
},
|
|
366
|
+
get stats() {
|
|
367
|
+
return stats;
|
|
368
|
+
},
|
|
369
|
+
async add(result) {
|
|
370
|
+
buffer.push(result);
|
|
371
|
+
if (buffer.length >= batchSize) await flush();
|
|
372
|
+
},
|
|
373
|
+
flush,
|
|
374
|
+
async finish(reason) {
|
|
375
|
+
if (done) return;
|
|
376
|
+
done = true;
|
|
377
|
+
await flush();
|
|
378
|
+
await offer();
|
|
379
|
+
if (runId !== null && !offline) {
|
|
380
|
+
const out = await client.post(`/v1/runs/${runId}/events`, {
|
|
381
|
+
event: "finish",
|
|
382
|
+
...reason !== void 0 ? { reason } : {}
|
|
383
|
+
});
|
|
384
|
+
if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
|
|
385
|
+
}
|
|
386
|
+
summarise();
|
|
387
|
+
},
|
|
388
|
+
async leaveOpen() {
|
|
389
|
+
if (done) return;
|
|
390
|
+
done = true;
|
|
391
|
+
await flush();
|
|
392
|
+
await offer();
|
|
393
|
+
if (runId !== null) {
|
|
394
|
+
log(`plune: run ${runId} is still open \u2014 close it with "plune run finish ${runId}".`);
|
|
395
|
+
}
|
|
396
|
+
summarise();
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/util/hash.ts
|
|
402
|
+
import { createHash } from "crypto";
|
|
403
|
+
function sha256(input) {
|
|
404
|
+
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/reporter-core/result-key.ts
|
|
408
|
+
var RESULT_KEY_MAX = 200;
|
|
409
|
+
var HASH_CHARS = 32;
|
|
410
|
+
function resultKey(testId, retry) {
|
|
411
|
+
const plain = `${testId}#${retry}`;
|
|
412
|
+
return plain.length <= RESULT_KEY_MAX ? plain : `${sha256(testId).slice(0, HASH_CHARS)}#${retry}`;
|
|
413
|
+
}
|
|
414
|
+
export {
|
|
415
|
+
DEFAULT_FALLBACK_PATH,
|
|
416
|
+
RESULT_KEY_MAX,
|
|
417
|
+
UNSUPPORTED_VARS,
|
|
418
|
+
appendBatch,
|
|
419
|
+
createClient,
|
|
420
|
+
readEnv,
|
|
421
|
+
resultKey,
|
|
422
|
+
startRun
|
|
423
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plune-ai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "AI-powered assertion testing for LLM apps — write assertions, run them against any provider, get a pass/fail report (local, CI, or diff).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
29
29
|
"import": "./dist/index.js",
|
|
30
30
|
"require": "./dist/index.cjs"
|
|
31
|
+
},
|
|
32
|
+
"./reporter-core": {
|
|
33
|
+
"types": "./dist/reporter-core.d.ts",
|
|
34
|
+
"import": "./dist/reporter-core.js",
|
|
35
|
+
"require": "./dist/reporter-core.cjs"
|
|
31
36
|
}
|
|
32
37
|
},
|
|
33
38
|
"files": [
|
|
@@ -44,10 +49,14 @@
|
|
|
44
49
|
"test": "vitest run --coverage",
|
|
45
50
|
"test:watch": "vitest",
|
|
46
51
|
"test:e2e": "tsup && vitest run -c vitest.e2e.config.ts",
|
|
47
|
-
"lint": "eslint \"src/**/*.ts\" --no-error-on-unmatched-pattern",
|
|
52
|
+
"lint": "eslint \"src/**/*.ts\" \"packages/*/src/**/*.ts\" --no-error-on-unmatched-pattern",
|
|
48
53
|
"format": "prettier --write .",
|
|
49
54
|
"format:check": "prettier --check .",
|
|
50
55
|
"typecheck": "tsc --noEmit",
|
|
56
|
+
"typecheck:playwright": "pnpm --filter @plune-ai/playwright typecheck",
|
|
57
|
+
"test:playwright": "pnpm --filter @plune-ai/playwright test",
|
|
58
|
+
"test:e2e:playwright": "pnpm --filter @plune-ai/playwright test:e2e",
|
|
59
|
+
"build:playwright": "pnpm --filter @plune-ai/playwright build",
|
|
51
60
|
"prepublishOnly": "tsup"
|
|
52
61
|
},
|
|
53
62
|
"dependencies": {
|
|
@@ -59,7 +68,7 @@
|
|
|
59
68
|
"dotenv": "^16.4.0",
|
|
60
69
|
"openai": "^6.42.0",
|
|
61
70
|
"yaml": "^2.9.0",
|
|
62
|
-
"zod": "^
|
|
71
|
+
"zod": "^4.5.4"
|
|
63
72
|
},
|
|
64
73
|
"devDependencies": {
|
|
65
74
|
"@huggingface/transformers": "4.2.0",
|