@plune-ai/cli 0.7.0 → 0.9.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 +29 -0
- package/dist/cli.cjs +622 -36
- 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/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