@plune-ai/cli 0.7.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/dist/cli.cjs +73 -13
- package/dist/reporter-core.cjs +68 -8
- package/dist/reporter-core.d.cts +39 -15
- package/dist/reporter-core.d.ts +39 -15
- package/dist/reporter-core.js +68 -8
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -2267,19 +2267,35 @@ function count(env, name) {
|
|
|
2267
2267
|
const n = Number(raw);
|
|
2268
2268
|
return Number.isInteger(n) && n > 0 ? n : void 0;
|
|
2269
2269
|
}
|
|
2270
|
+
function list(env, name) {
|
|
2271
|
+
const raw = value(env, name);
|
|
2272
|
+
if (raw === void 0) return void 0;
|
|
2273
|
+
const items = raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
2274
|
+
return items.length === 0 ? void 0 : items;
|
|
2275
|
+
}
|
|
2270
2276
|
function readEnv(env = process.env) {
|
|
2271
2277
|
const externalKey = value(env, "PLUNE_RUN");
|
|
2272
2278
|
const token = value(env, "PLUNE_TOKEN");
|
|
2273
2279
|
const apiUrl = value(env, "PLUNE_API_URL");
|
|
2274
2280
|
const fallbackPath = value(env, "PLUNE_FALLBACK");
|
|
2275
2281
|
const batchSize = count(env, "PLUNE_BATCH_SIZE");
|
|
2282
|
+
const title = value(env, "PLUNE_RUN_TITLE");
|
|
2283
|
+
const environment = value(env, "PLUNE_ENV");
|
|
2284
|
+
const labels = list(env, "PLUNE_LABELS");
|
|
2285
|
+
const offerDiscovered = flag(env, "PLUNE_CREATE");
|
|
2276
2286
|
return {
|
|
2277
2287
|
config: {
|
|
2278
2288
|
...externalKey !== void 0 ? { externalKey } : {},
|
|
2279
2289
|
...token !== void 0 ? { token } : {},
|
|
2280
2290
|
...apiUrl !== void 0 ? { apiUrl } : {},
|
|
2281
2291
|
...fallbackPath !== void 0 ? { fallbackPath } : {},
|
|
2282
|
-
...batchSize !== void 0 ? { batchSize } : {}
|
|
2292
|
+
...batchSize !== void 0 ? { batchSize } : {},
|
|
2293
|
+
...title !== void 0 ? { title } : {},
|
|
2294
|
+
...environment !== void 0 ? { environment } : {},
|
|
2295
|
+
...labels !== void 0 ? { labels } : {},
|
|
2296
|
+
// Only when set: `false` here would outrank a config file that asked for it, and an unset
|
|
2297
|
+
// variable is not an instruction.
|
|
2298
|
+
...offerDiscovered ? { offerDiscovered } : {}
|
|
2283
2299
|
},
|
|
2284
2300
|
// Two names for two situations that end the same way. `SHARED_RUN` says other processes are
|
|
2285
2301
|
// reporting into this run; `PROCEED` says the job will close it on its own schedule. Either
|
|
@@ -2293,12 +2309,7 @@ var init_env2 = __esm({
|
|
|
2293
2309
|
"src/reporter-core/env.ts"() {
|
|
2294
2310
|
"use strict";
|
|
2295
2311
|
init_cjs_shims();
|
|
2296
|
-
UNSUPPORTED_VARS = [
|
|
2297
|
-
"PLUNE_RUN_TITLE",
|
|
2298
|
-
"PLUNE_ENV",
|
|
2299
|
-
"PLUNE_LABELS",
|
|
2300
|
-
"PLUNE_GROUP"
|
|
2301
|
-
];
|
|
2312
|
+
UNSUPPORTED_VARS = ["PLUNE_GROUP"];
|
|
2302
2313
|
}
|
|
2303
2314
|
});
|
|
2304
2315
|
|
|
@@ -2331,6 +2342,14 @@ function chunk(items, size) {
|
|
|
2331
2342
|
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
2332
2343
|
return out;
|
|
2333
2344
|
}
|
|
2345
|
+
function warnIfDropped(sent, run, log) {
|
|
2346
|
+
const names = Object.keys(sent);
|
|
2347
|
+
if (names.length === 0) return;
|
|
2348
|
+
if (names.some((name) => run[name] != null)) return;
|
|
2349
|
+
log(
|
|
2350
|
+
`plune: this deployment stored none of ${names.join(", ")} \u2014 it is older than the fields. The run is otherwise reported in full.`
|
|
2351
|
+
);
|
|
2352
|
+
}
|
|
2334
2353
|
async function startRun(passed, expected) {
|
|
2335
2354
|
const env = readEnv();
|
|
2336
2355
|
const stated = Object.fromEntries(Object.entries(passed).filter(([, v]) => v !== void 0));
|
|
@@ -2340,6 +2359,7 @@ async function startRun(passed, expected) {
|
|
|
2340
2359
|
const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
|
|
2341
2360
|
const externalKey = cfg.externalKey ?? null;
|
|
2342
2361
|
const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
|
|
2362
|
+
const offerDiscovered = cfg.offerDiscovered ?? false;
|
|
2343
2363
|
const token = cfg.token ?? loadToken() ?? "";
|
|
2344
2364
|
const client = createClient({
|
|
2345
2365
|
apiUrl: resolveApiUrl(cfg.apiUrl),
|
|
@@ -2352,10 +2372,12 @@ async function startRun(passed, expected) {
|
|
|
2352
2372
|
conflict: 0,
|
|
2353
2373
|
rejected: 0,
|
|
2354
2374
|
unresolved: 0,
|
|
2375
|
+
offered: 0,
|
|
2355
2376
|
deferred: 0
|
|
2356
2377
|
};
|
|
2357
2378
|
const resolved = /* @__PURE__ */ new Map();
|
|
2358
2379
|
let buffer = [];
|
|
2380
|
+
const discoveries = /* @__PURE__ */ new Map();
|
|
2359
2381
|
let runId = null;
|
|
2360
2382
|
let joined = false;
|
|
2361
2383
|
let offline = token === "";
|
|
@@ -2398,16 +2420,23 @@ async function startRun(passed, expected) {
|
|
|
2398
2420
|
if (expected !== void 0 && expected.length > EXPECTED_MAX) {
|
|
2399
2421
|
log(`plune: ${expected.length} tests is more than the run configuration holds \u2014 reporting the first ${EXPECTED_MAX}.`);
|
|
2400
2422
|
}
|
|
2423
|
+
const described = {
|
|
2424
|
+
...cfg.title !== void 0 ? { title: cfg.title } : {},
|
|
2425
|
+
...cfg.environment !== void 0 ? { environment: cfg.environment } : {},
|
|
2426
|
+
...cfg.labels !== void 0 ? { labels: cfg.labels } : {}
|
|
2427
|
+
};
|
|
2401
2428
|
const start = await client.post("/v1/runs", {
|
|
2402
2429
|
schemaVersion: 2,
|
|
2403
2430
|
kind: cfg.kind ?? "automated",
|
|
2404
2431
|
...externalKey !== null ? { externalKey } : {},
|
|
2405
2432
|
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
2406
|
-
...configuration !== void 0 ? { configuration } : {}
|
|
2433
|
+
...configuration !== void 0 ? { configuration } : {},
|
|
2434
|
+
...described
|
|
2407
2435
|
});
|
|
2408
2436
|
if (start.ok) {
|
|
2409
2437
|
runId = start.body.run.id;
|
|
2410
2438
|
joined = start.status === 200;
|
|
2439
|
+
warnIfDropped(described, start.body.run, log);
|
|
2411
2440
|
} else {
|
|
2412
2441
|
log(`plune: could not start the run (${start.detail || start.kind}). Results will be written to ${fallbackPath}.`);
|
|
2413
2442
|
}
|
|
@@ -2422,6 +2451,32 @@ async function startRun(passed, expected) {
|
|
|
2422
2451
|
}
|
|
2423
2452
|
return unanswered ? { kind: "unanswered" } : { kind: "unmatched" };
|
|
2424
2453
|
}
|
|
2454
|
+
function remember(result) {
|
|
2455
|
+
if (!offerDiscovered) return;
|
|
2456
|
+
if (result.title === void 0 || result.specRef === void 0) return;
|
|
2457
|
+
if (result.keys.length === 0) return;
|
|
2458
|
+
discoveries.set(result.keys[0].value, {
|
|
2459
|
+
keys: result.keys,
|
|
2460
|
+
title: result.title,
|
|
2461
|
+
source: result.source,
|
|
2462
|
+
specRef: result.specRef,
|
|
2463
|
+
rawStatus: result.rawStatus
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
async function offer() {
|
|
2467
|
+
if (discoveries.size === 0 || offline || runId === null) return;
|
|
2468
|
+
for (const batch of chunk([...discoveries.values()], DISCOVER_MAX)) {
|
|
2469
|
+
const out = await client.post("/v1/review-items/discovered", {
|
|
2470
|
+
discovered: batch,
|
|
2471
|
+
runId
|
|
2472
|
+
});
|
|
2473
|
+
if (!out.ok) {
|
|
2474
|
+
log(`plune: could not offer ${batch.length} unknown tests for review (${out.detail || out.kind}).`);
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
stats.offered += out.body.results.filter((r) => r.outcome === "queued").length;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2425
2480
|
async function flush() {
|
|
2426
2481
|
if (buffer.length === 0) return;
|
|
2427
2482
|
const pending = buffer;
|
|
@@ -2440,6 +2495,7 @@ async function startRun(passed, expected) {
|
|
|
2440
2495
|
submissions.push({ ...rest, testCaseId: match.testCaseId });
|
|
2441
2496
|
} else if (match.kind === "unmatched") {
|
|
2442
2497
|
stats.unresolved += 1;
|
|
2498
|
+
remember(result);
|
|
2443
2499
|
} else {
|
|
2444
2500
|
unanswered.push(result);
|
|
2445
2501
|
}
|
|
@@ -2473,6 +2529,7 @@ async function startRun(passed, expected) {
|
|
|
2473
2529
|
if (stats.conflict > 0) parts.push(`${stats.conflict} conflicting`);
|
|
2474
2530
|
if (stats.rejected > 0) parts.push(`${stats.rejected} rejected`);
|
|
2475
2531
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
2532
|
+
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
2476
2533
|
if (stats.deferred > 0) parts.push(`${stats.deferred} written to ${fallbackPath}`);
|
|
2477
2534
|
log(`plune: ${parts.join(" \xB7 ")}`);
|
|
2478
2535
|
}
|
|
@@ -2495,6 +2552,7 @@ async function startRun(passed, expected) {
|
|
|
2495
2552
|
if (done) return;
|
|
2496
2553
|
done = true;
|
|
2497
2554
|
await flush();
|
|
2555
|
+
await offer();
|
|
2498
2556
|
if (runId !== null && !offline) {
|
|
2499
2557
|
const out = await client.post(`/v1/runs/${runId}/events`, {
|
|
2500
2558
|
event: "finish",
|
|
@@ -2508,6 +2566,7 @@ async function startRun(passed, expected) {
|
|
|
2508
2566
|
if (done) return;
|
|
2509
2567
|
done = true;
|
|
2510
2568
|
await flush();
|
|
2569
|
+
await offer();
|
|
2511
2570
|
if (runId !== null) {
|
|
2512
2571
|
log(`plune: run ${runId} is still open \u2014 close it with "plune run finish ${runId}".`);
|
|
2513
2572
|
}
|
|
@@ -2515,7 +2574,7 @@ async function startRun(passed, expected) {
|
|
|
2515
2574
|
}
|
|
2516
2575
|
};
|
|
2517
2576
|
}
|
|
2518
|
-
var RESOLVE_CHUNK, BATCH_MAX, BATCH_DEFAULT, EXPECTED_MAX;
|
|
2577
|
+
var RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, EXPECTED_MAX;
|
|
2519
2578
|
var init_session = __esm({
|
|
2520
2579
|
"src/reporter-core/session.ts"() {
|
|
2521
2580
|
"use strict";
|
|
@@ -2527,6 +2586,7 @@ var init_session = __esm({
|
|
|
2527
2586
|
init_fallback();
|
|
2528
2587
|
RESOLVE_CHUNK = 500;
|
|
2529
2588
|
BATCH_MAX = 500;
|
|
2589
|
+
DISCOVER_MAX = 500;
|
|
2530
2590
|
BATCH_DEFAULT = 100;
|
|
2531
2591
|
EXPECTED_MAX = 1e4;
|
|
2532
2592
|
}
|
|
@@ -3431,8 +3491,8 @@ async function defaultReadEvidence(dir) {
|
|
|
3431
3491
|
});
|
|
3432
3492
|
}
|
|
3433
3493
|
function casesFromDesign(report) {
|
|
3434
|
-
const
|
|
3435
|
-
return
|
|
3494
|
+
const list2 = Array.isArray(report["testCases"]) ? report["testCases"] : [];
|
|
3495
|
+
return list2.map((raw) => {
|
|
3436
3496
|
const c = asRecord(raw);
|
|
3437
3497
|
const steps = (Array.isArray(c["steps"]) ? c["steps"] : []).map((s) => str(s)).filter((s) => s !== "");
|
|
3438
3498
|
return {
|
|
@@ -3447,8 +3507,8 @@ function casesFromDesign(report) {
|
|
|
3447
3507
|
});
|
|
3448
3508
|
}
|
|
3449
3509
|
function casesFromApi(report, verdicts) {
|
|
3450
|
-
const
|
|
3451
|
-
return
|
|
3510
|
+
const list2 = Array.isArray(report["cases"]) ? report["cases"] : [];
|
|
3511
|
+
return list2.map((raw) => {
|
|
3452
3512
|
const c = asRecord(raw);
|
|
3453
3513
|
const name = str(c["name"], "unnamed operation");
|
|
3454
3514
|
const request = `${str(c["method"], "GET")} ${str(c["path"], "/")}`;
|
package/dist/reporter-core.cjs
CHANGED
|
@@ -128,12 +128,7 @@ function createClient(opts) {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
// src/reporter-core/env.ts
|
|
131
|
-
var UNSUPPORTED_VARS = [
|
|
132
|
-
"PLUNE_RUN_TITLE",
|
|
133
|
-
"PLUNE_ENV",
|
|
134
|
-
"PLUNE_LABELS",
|
|
135
|
-
"PLUNE_GROUP"
|
|
136
|
-
];
|
|
131
|
+
var UNSUPPORTED_VARS = ["PLUNE_GROUP"];
|
|
137
132
|
function value(env, name) {
|
|
138
133
|
const raw = env[name]?.trim();
|
|
139
134
|
return raw === void 0 || raw === "" ? void 0 : raw;
|
|
@@ -148,19 +143,35 @@ function count(env, name) {
|
|
|
148
143
|
const n = Number(raw);
|
|
149
144
|
return Number.isInteger(n) && n > 0 ? n : void 0;
|
|
150
145
|
}
|
|
146
|
+
function list(env, name) {
|
|
147
|
+
const raw = value(env, name);
|
|
148
|
+
if (raw === void 0) return void 0;
|
|
149
|
+
const items = raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
150
|
+
return items.length === 0 ? void 0 : items;
|
|
151
|
+
}
|
|
151
152
|
function readEnv(env = process.env) {
|
|
152
153
|
const externalKey = value(env, "PLUNE_RUN");
|
|
153
154
|
const token = value(env, "PLUNE_TOKEN");
|
|
154
155
|
const apiUrl = value(env, "PLUNE_API_URL");
|
|
155
156
|
const fallbackPath = value(env, "PLUNE_FALLBACK");
|
|
156
157
|
const batchSize = count(env, "PLUNE_BATCH_SIZE");
|
|
158
|
+
const title = value(env, "PLUNE_RUN_TITLE");
|
|
159
|
+
const environment = value(env, "PLUNE_ENV");
|
|
160
|
+
const labels = list(env, "PLUNE_LABELS");
|
|
161
|
+
const offerDiscovered = flag(env, "PLUNE_CREATE");
|
|
157
162
|
return {
|
|
158
163
|
config: {
|
|
159
164
|
...externalKey !== void 0 ? { externalKey } : {},
|
|
160
165
|
...token !== void 0 ? { token } : {},
|
|
161
166
|
...apiUrl !== void 0 ? { apiUrl } : {},
|
|
162
167
|
...fallbackPath !== void 0 ? { fallbackPath } : {},
|
|
163
|
-
...batchSize !== void 0 ? { batchSize } : {}
|
|
168
|
+
...batchSize !== void 0 ? { batchSize } : {},
|
|
169
|
+
...title !== void 0 ? { title } : {},
|
|
170
|
+
...environment !== void 0 ? { environment } : {},
|
|
171
|
+
...labels !== void 0 ? { labels } : {},
|
|
172
|
+
// Only when set: `false` here would outrank a config file that asked for it, and an unset
|
|
173
|
+
// variable is not an instruction.
|
|
174
|
+
...offerDiscovered ? { offerDiscovered } : {}
|
|
164
175
|
},
|
|
165
176
|
// Two names for two situations that end the same way. `SHARED_RUN` says other processes are
|
|
166
177
|
// reporting into this run; `PROCEED` says the job will close it on its own schedule. Either
|
|
@@ -189,6 +200,7 @@ function appendBatch(file, batch) {
|
|
|
189
200
|
// src/reporter-core/session.ts
|
|
190
201
|
var RESOLVE_CHUNK = 500;
|
|
191
202
|
var BATCH_MAX = 500;
|
|
203
|
+
var DISCOVER_MAX = 500;
|
|
192
204
|
var BATCH_DEFAULT = 100;
|
|
193
205
|
var EXPECTED_MAX = 1e4;
|
|
194
206
|
function chunk(items, size) {
|
|
@@ -196,6 +208,14 @@ function chunk(items, size) {
|
|
|
196
208
|
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
197
209
|
return out;
|
|
198
210
|
}
|
|
211
|
+
function warnIfDropped(sent, run, log) {
|
|
212
|
+
const names = Object.keys(sent);
|
|
213
|
+
if (names.length === 0) return;
|
|
214
|
+
if (names.some((name) => run[name] != null)) return;
|
|
215
|
+
log(
|
|
216
|
+
`plune: this deployment stored none of ${names.join(", ")} \u2014 it is older than the fields. The run is otherwise reported in full.`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
199
219
|
async function startRun(passed, expected) {
|
|
200
220
|
const env = readEnv();
|
|
201
221
|
const stated = Object.fromEntries(Object.entries(passed).filter(([, v]) => v !== void 0));
|
|
@@ -205,6 +225,7 @@ async function startRun(passed, expected) {
|
|
|
205
225
|
const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
|
|
206
226
|
const externalKey = cfg.externalKey ?? null;
|
|
207
227
|
const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
|
|
228
|
+
const offerDiscovered = cfg.offerDiscovered ?? false;
|
|
208
229
|
const token = cfg.token ?? loadToken() ?? "";
|
|
209
230
|
const client = createClient({
|
|
210
231
|
apiUrl: resolveApiUrl(cfg.apiUrl),
|
|
@@ -217,10 +238,12 @@ async function startRun(passed, expected) {
|
|
|
217
238
|
conflict: 0,
|
|
218
239
|
rejected: 0,
|
|
219
240
|
unresolved: 0,
|
|
241
|
+
offered: 0,
|
|
220
242
|
deferred: 0
|
|
221
243
|
};
|
|
222
244
|
const resolved = /* @__PURE__ */ new Map();
|
|
223
245
|
let buffer = [];
|
|
246
|
+
const discoveries = /* @__PURE__ */ new Map();
|
|
224
247
|
let runId = null;
|
|
225
248
|
let joined = false;
|
|
226
249
|
let offline = token === "";
|
|
@@ -263,16 +286,23 @@ async function startRun(passed, expected) {
|
|
|
263
286
|
if (expected !== void 0 && expected.length > EXPECTED_MAX) {
|
|
264
287
|
log(`plune: ${expected.length} tests is more than the run configuration holds \u2014 reporting the first ${EXPECTED_MAX}.`);
|
|
265
288
|
}
|
|
289
|
+
const described = {
|
|
290
|
+
...cfg.title !== void 0 ? { title: cfg.title } : {},
|
|
291
|
+
...cfg.environment !== void 0 ? { environment: cfg.environment } : {},
|
|
292
|
+
...cfg.labels !== void 0 ? { labels: cfg.labels } : {}
|
|
293
|
+
};
|
|
266
294
|
const start = await client.post("/v1/runs", {
|
|
267
295
|
schemaVersion: 2,
|
|
268
296
|
kind: cfg.kind ?? "automated",
|
|
269
297
|
...externalKey !== null ? { externalKey } : {},
|
|
270
298
|
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
271
|
-
...configuration !== void 0 ? { configuration } : {}
|
|
299
|
+
...configuration !== void 0 ? { configuration } : {},
|
|
300
|
+
...described
|
|
272
301
|
});
|
|
273
302
|
if (start.ok) {
|
|
274
303
|
runId = start.body.run.id;
|
|
275
304
|
joined = start.status === 200;
|
|
305
|
+
warnIfDropped(described, start.body.run, log);
|
|
276
306
|
} else {
|
|
277
307
|
log(`plune: could not start the run (${start.detail || start.kind}). Results will be written to ${fallbackPath}.`);
|
|
278
308
|
}
|
|
@@ -287,6 +317,32 @@ async function startRun(passed, expected) {
|
|
|
287
317
|
}
|
|
288
318
|
return unanswered ? { kind: "unanswered" } : { kind: "unmatched" };
|
|
289
319
|
}
|
|
320
|
+
function remember(result) {
|
|
321
|
+
if (!offerDiscovered) return;
|
|
322
|
+
if (result.title === void 0 || result.specRef === void 0) return;
|
|
323
|
+
if (result.keys.length === 0) return;
|
|
324
|
+
discoveries.set(result.keys[0].value, {
|
|
325
|
+
keys: result.keys,
|
|
326
|
+
title: result.title,
|
|
327
|
+
source: result.source,
|
|
328
|
+
specRef: result.specRef,
|
|
329
|
+
rawStatus: result.rawStatus
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
async function offer() {
|
|
333
|
+
if (discoveries.size === 0 || offline || runId === null) return;
|
|
334
|
+
for (const batch of chunk([...discoveries.values()], DISCOVER_MAX)) {
|
|
335
|
+
const out = await client.post("/v1/review-items/discovered", {
|
|
336
|
+
discovered: batch,
|
|
337
|
+
runId
|
|
338
|
+
});
|
|
339
|
+
if (!out.ok) {
|
|
340
|
+
log(`plune: could not offer ${batch.length} unknown tests for review (${out.detail || out.kind}).`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
stats.offered += out.body.results.filter((r) => r.outcome === "queued").length;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
290
346
|
async function flush() {
|
|
291
347
|
if (buffer.length === 0) return;
|
|
292
348
|
const pending = buffer;
|
|
@@ -305,6 +361,7 @@ async function startRun(passed, expected) {
|
|
|
305
361
|
submissions.push({ ...rest, testCaseId: match.testCaseId });
|
|
306
362
|
} else if (match.kind === "unmatched") {
|
|
307
363
|
stats.unresolved += 1;
|
|
364
|
+
remember(result);
|
|
308
365
|
} else {
|
|
309
366
|
unanswered.push(result);
|
|
310
367
|
}
|
|
@@ -338,6 +395,7 @@ async function startRun(passed, expected) {
|
|
|
338
395
|
if (stats.conflict > 0) parts.push(`${stats.conflict} conflicting`);
|
|
339
396
|
if (stats.rejected > 0) parts.push(`${stats.rejected} rejected`);
|
|
340
397
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
398
|
+
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
341
399
|
if (stats.deferred > 0) parts.push(`${stats.deferred} written to ${fallbackPath}`);
|
|
342
400
|
log(`plune: ${parts.join(" \xB7 ")}`);
|
|
343
401
|
}
|
|
@@ -360,6 +418,7 @@ async function startRun(passed, expected) {
|
|
|
360
418
|
if (done) return;
|
|
361
419
|
done = true;
|
|
362
420
|
await flush();
|
|
421
|
+
await offer();
|
|
363
422
|
if (runId !== null && !offline) {
|
|
364
423
|
const out = await client.post(`/v1/runs/${runId}/events`, {
|
|
365
424
|
event: "finish",
|
|
@@ -373,6 +432,7 @@ async function startRun(passed, expected) {
|
|
|
373
432
|
if (done) return;
|
|
374
433
|
done = true;
|
|
375
434
|
await flush();
|
|
435
|
+
await offer();
|
|
376
436
|
if (runId !== null) {
|
|
377
437
|
log(`plune: run ${runId} is still open \u2014 close it with "plune run finish ${runId}".`);
|
|
378
438
|
}
|
package/dist/reporter-core.d.cts
CHANGED
|
@@ -88,6 +88,19 @@ interface PendingResult {
|
|
|
88
88
|
rawStatus: string;
|
|
89
89
|
/** What the author expected — `test.fail()` and friends. Absent means a pass was expected. */
|
|
90
90
|
expectedStatus?: ResultStatus;
|
|
91
|
+
/**
|
|
92
|
+
* What to call this test, and where it lives — read ONLY when nothing resolved it (D14).
|
|
93
|
+
*
|
|
94
|
+
* A result that found its case needs neither: the case already has a title and a spec. These are
|
|
95
|
+
* for the other path, where the reporter offers the test to the review queue and the queue needs a
|
|
96
|
+
* name a person can judge and a spec a case can point at.
|
|
97
|
+
*
|
|
98
|
+
* Optional because an adapter that cannot say both simply does not participate. Deriving them from
|
|
99
|
+
* a key's internal spelling was the alternative and it is worse: it makes the core parse a string
|
|
100
|
+
* the adapter formatted, which is a contract nothing checks.
|
|
101
|
+
*/
|
|
102
|
+
title?: string;
|
|
103
|
+
specRef?: string;
|
|
91
104
|
execution?: Execution;
|
|
92
105
|
errorContext?: string;
|
|
93
106
|
params?: Record<string, unknown>;
|
|
@@ -121,8 +134,11 @@ interface SubmitCounts {
|
|
|
121
134
|
/** What the session accumulated, for the one summary line a reporter prints. */
|
|
122
135
|
interface RunStats extends SubmitCounts {
|
|
123
136
|
/** Results whose keys matched no case — never sent, because inventing an id would write into
|
|
124
|
-
* somebody else's history.
|
|
137
|
+
* somebody else's history. Rung 6 of C2's ladder is what to do about them: see `offered`. */
|
|
125
138
|
unresolved: number;
|
|
139
|
+
/** Unresolved tests offered to the review queue under `PLUNE_CREATE=1` (D14). Counted separately
|
|
140
|
+
* from `unresolved` because they are the same tests seen twice, not a second population. */
|
|
141
|
+
offered: number;
|
|
126
142
|
/** Results written to the fallback file instead of the platform. */
|
|
127
143
|
deferred: number;
|
|
128
144
|
}
|
|
@@ -136,10 +152,24 @@ interface ReporterConfig {
|
|
|
136
152
|
externalKey?: string;
|
|
137
153
|
kind?: RunKind;
|
|
138
154
|
meta?: RunMeta;
|
|
155
|
+
/** What to call this run in a list. The first shard to declare one wins. */
|
|
156
|
+
title?: string;
|
|
157
|
+
/** Where it ran — staging, prod, a preview. What keeps two CI matrices from reading as one. */
|
|
158
|
+
environment?: string;
|
|
159
|
+
/** How the run is marked — smoke, nightly, a team. */
|
|
160
|
+
labels?: string[];
|
|
139
161
|
/** Results per request. The platform's ceiling is 500. */
|
|
140
162
|
batchSize?: number;
|
|
141
163
|
/** Where unsent batches go. Defaults beside the run file the CLI already writes. */
|
|
142
164
|
fallbackPath?: string;
|
|
165
|
+
/**
|
|
166
|
+
* Offer tests that resolved to nothing to the platform's review queue (`PLUNE_CREATE=1`, D14).
|
|
167
|
+
*
|
|
168
|
+
* Off by default, and that is the product decision rather than caution: a reporter that filled a
|
|
169
|
+
* stranger's review queue on first run would teach the team to ignore the queue. Turning it on is
|
|
170
|
+
* how somebody says "yes, tell me what we are running that you do not know about".
|
|
171
|
+
*/
|
|
172
|
+
offerDiscovered?: boolean;
|
|
143
173
|
/** Transport seam — the same one `sync.ts` uses so tests need no server. */
|
|
144
174
|
fetchImpl?: typeof fetch;
|
|
145
175
|
/** Where a line for the operator goes. Defaults to stderr. */
|
|
@@ -167,15 +197,6 @@ interface RunSession {
|
|
|
167
197
|
/** Send what is left and deliberately leave the run open — the shard case (AC-03). */
|
|
168
198
|
leaveOpen(): Promise<void>;
|
|
169
199
|
}
|
|
170
|
-
/**
|
|
171
|
-
* Start a run, or join the one that already carries this key.
|
|
172
|
-
*
|
|
173
|
-
* `expected` — one list of candidate identifiers per test the runner knows about — is used twice
|
|
174
|
-
* on purpose. It becomes `configuration.expected`, from which the platform derives what never ran
|
|
175
|
-
* (AC-09); and it is the input to the single up-front resolve, so the per-test path afterwards is
|
|
176
|
-
* a map lookup rather than a request (AC-10). A runner that cannot enumerate its tests passes
|
|
177
|
-
* nothing and resolves lazily on the first flush — same function, later moment.
|
|
178
|
-
*/
|
|
179
200
|
declare function startRun(passed: ReporterConfig, expected?: readonly (readonly KeyRef[])[]): Promise<RunSession>;
|
|
180
201
|
|
|
181
202
|
/**
|
|
@@ -209,14 +230,17 @@ declare function resultKey(testId: string, retry: number): string;
|
|
|
209
230
|
* laptop the config was written on and the runner it executes on.
|
|
210
231
|
*/
|
|
211
232
|
/**
|
|
212
|
-
* Names the reporter promises but the platform cannot yet store
|
|
233
|
+
* Names the reporter promises but the platform cannot yet store.
|
|
213
234
|
*
|
|
214
235
|
* They are listed rather than ignored because of how they would fail otherwise: a run's `meta`
|
|
215
|
-
* strips keys it does not know, so sending
|
|
216
|
-
*
|
|
217
|
-
*
|
|
236
|
+
* strips keys it does not know, so sending one there would be accepted, dropped, and look exactly
|
|
237
|
+
* like success. A client believing it saved something the server discarded is the most expensive
|
|
238
|
+
* kind of quiet — cheaper to say "not yet" out loud.
|
|
239
|
+
*
|
|
240
|
+
* `PLUNE_RUN_TITLE`, `PLUNE_ENV` and `PLUNE_LABELS` left this list when D13 gave a run somewhere to
|
|
241
|
+
* put them. `PLUNE_GROUP` stays: a run group is D6's, and a group of one run means nothing.
|
|
218
242
|
*/
|
|
219
|
-
declare const UNSUPPORTED_VARS: readonly ["
|
|
243
|
+
declare const UNSUPPORTED_VARS: readonly ["PLUNE_GROUP"];
|
|
220
244
|
interface EnvSettings {
|
|
221
245
|
/** Config fields the environment supplied. Anything passed explicitly outranks these. */
|
|
222
246
|
config: Partial<ReporterConfig>;
|
package/dist/reporter-core.d.ts
CHANGED
|
@@ -88,6 +88,19 @@ interface PendingResult {
|
|
|
88
88
|
rawStatus: string;
|
|
89
89
|
/** What the author expected — `test.fail()` and friends. Absent means a pass was expected. */
|
|
90
90
|
expectedStatus?: ResultStatus;
|
|
91
|
+
/**
|
|
92
|
+
* What to call this test, and where it lives — read ONLY when nothing resolved it (D14).
|
|
93
|
+
*
|
|
94
|
+
* A result that found its case needs neither: the case already has a title and a spec. These are
|
|
95
|
+
* for the other path, where the reporter offers the test to the review queue and the queue needs a
|
|
96
|
+
* name a person can judge and a spec a case can point at.
|
|
97
|
+
*
|
|
98
|
+
* Optional because an adapter that cannot say both simply does not participate. Deriving them from
|
|
99
|
+
* a key's internal spelling was the alternative and it is worse: it makes the core parse a string
|
|
100
|
+
* the adapter formatted, which is a contract nothing checks.
|
|
101
|
+
*/
|
|
102
|
+
title?: string;
|
|
103
|
+
specRef?: string;
|
|
91
104
|
execution?: Execution;
|
|
92
105
|
errorContext?: string;
|
|
93
106
|
params?: Record<string, unknown>;
|
|
@@ -121,8 +134,11 @@ interface SubmitCounts {
|
|
|
121
134
|
/** What the session accumulated, for the one summary line a reporter prints. */
|
|
122
135
|
interface RunStats extends SubmitCounts {
|
|
123
136
|
/** Results whose keys matched no case — never sent, because inventing an id would write into
|
|
124
|
-
* somebody else's history.
|
|
137
|
+
* somebody else's history. Rung 6 of C2's ladder is what to do about them: see `offered`. */
|
|
125
138
|
unresolved: number;
|
|
139
|
+
/** Unresolved tests offered to the review queue under `PLUNE_CREATE=1` (D14). Counted separately
|
|
140
|
+
* from `unresolved` because they are the same tests seen twice, not a second population. */
|
|
141
|
+
offered: number;
|
|
126
142
|
/** Results written to the fallback file instead of the platform. */
|
|
127
143
|
deferred: number;
|
|
128
144
|
}
|
|
@@ -136,10 +152,24 @@ interface ReporterConfig {
|
|
|
136
152
|
externalKey?: string;
|
|
137
153
|
kind?: RunKind;
|
|
138
154
|
meta?: RunMeta;
|
|
155
|
+
/** What to call this run in a list. The first shard to declare one wins. */
|
|
156
|
+
title?: string;
|
|
157
|
+
/** Where it ran — staging, prod, a preview. What keeps two CI matrices from reading as one. */
|
|
158
|
+
environment?: string;
|
|
159
|
+
/** How the run is marked — smoke, nightly, a team. */
|
|
160
|
+
labels?: string[];
|
|
139
161
|
/** Results per request. The platform's ceiling is 500. */
|
|
140
162
|
batchSize?: number;
|
|
141
163
|
/** Where unsent batches go. Defaults beside the run file the CLI already writes. */
|
|
142
164
|
fallbackPath?: string;
|
|
165
|
+
/**
|
|
166
|
+
* Offer tests that resolved to nothing to the platform's review queue (`PLUNE_CREATE=1`, D14).
|
|
167
|
+
*
|
|
168
|
+
* Off by default, and that is the product decision rather than caution: a reporter that filled a
|
|
169
|
+
* stranger's review queue on first run would teach the team to ignore the queue. Turning it on is
|
|
170
|
+
* how somebody says "yes, tell me what we are running that you do not know about".
|
|
171
|
+
*/
|
|
172
|
+
offerDiscovered?: boolean;
|
|
143
173
|
/** Transport seam — the same one `sync.ts` uses so tests need no server. */
|
|
144
174
|
fetchImpl?: typeof fetch;
|
|
145
175
|
/** Where a line for the operator goes. Defaults to stderr. */
|
|
@@ -167,15 +197,6 @@ interface RunSession {
|
|
|
167
197
|
/** Send what is left and deliberately leave the run open — the shard case (AC-03). */
|
|
168
198
|
leaveOpen(): Promise<void>;
|
|
169
199
|
}
|
|
170
|
-
/**
|
|
171
|
-
* Start a run, or join the one that already carries this key.
|
|
172
|
-
*
|
|
173
|
-
* `expected` — one list of candidate identifiers per test the runner knows about — is used twice
|
|
174
|
-
* on purpose. It becomes `configuration.expected`, from which the platform derives what never ran
|
|
175
|
-
* (AC-09); and it is the input to the single up-front resolve, so the per-test path afterwards is
|
|
176
|
-
* a map lookup rather than a request (AC-10). A runner that cannot enumerate its tests passes
|
|
177
|
-
* nothing and resolves lazily on the first flush — same function, later moment.
|
|
178
|
-
*/
|
|
179
200
|
declare function startRun(passed: ReporterConfig, expected?: readonly (readonly KeyRef[])[]): Promise<RunSession>;
|
|
180
201
|
|
|
181
202
|
/**
|
|
@@ -209,14 +230,17 @@ declare function resultKey(testId: string, retry: number): string;
|
|
|
209
230
|
* laptop the config was written on and the runner it executes on.
|
|
210
231
|
*/
|
|
211
232
|
/**
|
|
212
|
-
* Names the reporter promises but the platform cannot yet store
|
|
233
|
+
* Names the reporter promises but the platform cannot yet store.
|
|
213
234
|
*
|
|
214
235
|
* They are listed rather than ignored because of how they would fail otherwise: a run's `meta`
|
|
215
|
-
* strips keys it does not know, so sending
|
|
216
|
-
*
|
|
217
|
-
*
|
|
236
|
+
* strips keys it does not know, so sending one there would be accepted, dropped, and look exactly
|
|
237
|
+
* like success. A client believing it saved something the server discarded is the most expensive
|
|
238
|
+
* kind of quiet — cheaper to say "not yet" out loud.
|
|
239
|
+
*
|
|
240
|
+
* `PLUNE_RUN_TITLE`, `PLUNE_ENV` and `PLUNE_LABELS` left this list when D13 gave a run somewhere to
|
|
241
|
+
* put them. `PLUNE_GROUP` stays: a run group is D6's, and a group of one run means nothing.
|
|
218
242
|
*/
|
|
219
|
-
declare const UNSUPPORTED_VARS: readonly ["
|
|
243
|
+
declare const UNSUPPORTED_VARS: readonly ["PLUNE_GROUP"];
|
|
220
244
|
interface EnvSettings {
|
|
221
245
|
/** Config fields the environment supplied. Anything passed explicitly outranks these. */
|
|
222
246
|
config: Partial<ReporterConfig>;
|
package/dist/reporter-core.js
CHANGED
|
@@ -85,12 +85,7 @@ function createClient(opts) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// src/reporter-core/env.ts
|
|
88
|
-
var UNSUPPORTED_VARS = [
|
|
89
|
-
"PLUNE_RUN_TITLE",
|
|
90
|
-
"PLUNE_ENV",
|
|
91
|
-
"PLUNE_LABELS",
|
|
92
|
-
"PLUNE_GROUP"
|
|
93
|
-
];
|
|
88
|
+
var UNSUPPORTED_VARS = ["PLUNE_GROUP"];
|
|
94
89
|
function value(env, name) {
|
|
95
90
|
const raw = env[name]?.trim();
|
|
96
91
|
return raw === void 0 || raw === "" ? void 0 : raw;
|
|
@@ -105,19 +100,35 @@ function count(env, name) {
|
|
|
105
100
|
const n = Number(raw);
|
|
106
101
|
return Number.isInteger(n) && n > 0 ? n : void 0;
|
|
107
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
|
+
}
|
|
108
109
|
function readEnv(env = process.env) {
|
|
109
110
|
const externalKey = value(env, "PLUNE_RUN");
|
|
110
111
|
const token = value(env, "PLUNE_TOKEN");
|
|
111
112
|
const apiUrl = value(env, "PLUNE_API_URL");
|
|
112
113
|
const fallbackPath = value(env, "PLUNE_FALLBACK");
|
|
113
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");
|
|
114
119
|
return {
|
|
115
120
|
config: {
|
|
116
121
|
...externalKey !== void 0 ? { externalKey } : {},
|
|
117
122
|
...token !== void 0 ? { token } : {},
|
|
118
123
|
...apiUrl !== void 0 ? { apiUrl } : {},
|
|
119
124
|
...fallbackPath !== void 0 ? { fallbackPath } : {},
|
|
120
|
-
...batchSize !== void 0 ? { batchSize } : {}
|
|
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 } : {}
|
|
121
132
|
},
|
|
122
133
|
// Two names for two situations that end the same way. `SHARED_RUN` says other processes are
|
|
123
134
|
// reporting into this run; `PROCEED` says the job will close it on its own schedule. Either
|
|
@@ -146,6 +157,7 @@ function appendBatch(file, batch) {
|
|
|
146
157
|
// src/reporter-core/session.ts
|
|
147
158
|
var RESOLVE_CHUNK = 500;
|
|
148
159
|
var BATCH_MAX = 500;
|
|
160
|
+
var DISCOVER_MAX = 500;
|
|
149
161
|
var BATCH_DEFAULT = 100;
|
|
150
162
|
var EXPECTED_MAX = 1e4;
|
|
151
163
|
function chunk(items, size) {
|
|
@@ -153,6 +165,14 @@ function chunk(items, size) {
|
|
|
153
165
|
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
154
166
|
return out;
|
|
155
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
|
+
}
|
|
156
176
|
async function startRun(passed, expected) {
|
|
157
177
|
const env = readEnv();
|
|
158
178
|
const stated = Object.fromEntries(Object.entries(passed).filter(([, v]) => v !== void 0));
|
|
@@ -162,6 +182,7 @@ async function startRun(passed, expected) {
|
|
|
162
182
|
const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
|
|
163
183
|
const externalKey = cfg.externalKey ?? null;
|
|
164
184
|
const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
|
|
185
|
+
const offerDiscovered = cfg.offerDiscovered ?? false;
|
|
165
186
|
const token = cfg.token ?? loadToken() ?? "";
|
|
166
187
|
const client = createClient({
|
|
167
188
|
apiUrl: resolveApiUrl(cfg.apiUrl),
|
|
@@ -174,10 +195,12 @@ async function startRun(passed, expected) {
|
|
|
174
195
|
conflict: 0,
|
|
175
196
|
rejected: 0,
|
|
176
197
|
unresolved: 0,
|
|
198
|
+
offered: 0,
|
|
177
199
|
deferred: 0
|
|
178
200
|
};
|
|
179
201
|
const resolved = /* @__PURE__ */ new Map();
|
|
180
202
|
let buffer = [];
|
|
203
|
+
const discoveries = /* @__PURE__ */ new Map();
|
|
181
204
|
let runId = null;
|
|
182
205
|
let joined = false;
|
|
183
206
|
let offline = token === "";
|
|
@@ -220,16 +243,23 @@ async function startRun(passed, expected) {
|
|
|
220
243
|
if (expected !== void 0 && expected.length > EXPECTED_MAX) {
|
|
221
244
|
log(`plune: ${expected.length} tests is more than the run configuration holds \u2014 reporting the first ${EXPECTED_MAX}.`);
|
|
222
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
|
+
};
|
|
223
251
|
const start = await client.post("/v1/runs", {
|
|
224
252
|
schemaVersion: 2,
|
|
225
253
|
kind: cfg.kind ?? "automated",
|
|
226
254
|
...externalKey !== null ? { externalKey } : {},
|
|
227
255
|
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
228
|
-
...configuration !== void 0 ? { configuration } : {}
|
|
256
|
+
...configuration !== void 0 ? { configuration } : {},
|
|
257
|
+
...described
|
|
229
258
|
});
|
|
230
259
|
if (start.ok) {
|
|
231
260
|
runId = start.body.run.id;
|
|
232
261
|
joined = start.status === 200;
|
|
262
|
+
warnIfDropped(described, start.body.run, log);
|
|
233
263
|
} else {
|
|
234
264
|
log(`plune: could not start the run (${start.detail || start.kind}). Results will be written to ${fallbackPath}.`);
|
|
235
265
|
}
|
|
@@ -244,6 +274,32 @@ async function startRun(passed, expected) {
|
|
|
244
274
|
}
|
|
245
275
|
return unanswered ? { kind: "unanswered" } : { kind: "unmatched" };
|
|
246
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
|
+
}
|
|
247
303
|
async function flush() {
|
|
248
304
|
if (buffer.length === 0) return;
|
|
249
305
|
const pending = buffer;
|
|
@@ -262,6 +318,7 @@ async function startRun(passed, expected) {
|
|
|
262
318
|
submissions.push({ ...rest, testCaseId: match.testCaseId });
|
|
263
319
|
} else if (match.kind === "unmatched") {
|
|
264
320
|
stats.unresolved += 1;
|
|
321
|
+
remember(result);
|
|
265
322
|
} else {
|
|
266
323
|
unanswered.push(result);
|
|
267
324
|
}
|
|
@@ -295,6 +352,7 @@ async function startRun(passed, expected) {
|
|
|
295
352
|
if (stats.conflict > 0) parts.push(`${stats.conflict} conflicting`);
|
|
296
353
|
if (stats.rejected > 0) parts.push(`${stats.rejected} rejected`);
|
|
297
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`);
|
|
298
356
|
if (stats.deferred > 0) parts.push(`${stats.deferred} written to ${fallbackPath}`);
|
|
299
357
|
log(`plune: ${parts.join(" \xB7 ")}`);
|
|
300
358
|
}
|
|
@@ -317,6 +375,7 @@ async function startRun(passed, expected) {
|
|
|
317
375
|
if (done) return;
|
|
318
376
|
done = true;
|
|
319
377
|
await flush();
|
|
378
|
+
await offer();
|
|
320
379
|
if (runId !== null && !offline) {
|
|
321
380
|
const out = await client.post(`/v1/runs/${runId}/events`, {
|
|
322
381
|
event: "finish",
|
|
@@ -330,6 +389,7 @@ async function startRun(passed, expected) {
|
|
|
330
389
|
if (done) return;
|
|
331
390
|
done = true;
|
|
332
391
|
await flush();
|
|
392
|
+
await offer();
|
|
333
393
|
if (runId !== null) {
|
|
334
394
|
log(`plune: run ${runId} is still open \u2014 close it with "plune run finish ${runId}".`);
|
|
335
395
|
}
|
package/package.json
CHANGED