@plune-ai/cli 0.30.2 → 0.31.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.
@@ -324,6 +324,12 @@ interface ReporterConfig {
324
324
  environment?: string;
325
325
  /** How the run is marked — smoke, nightly, a team. */
326
326
  labels?: string[];
327
+ /**
328
+ * The combination this job runs, group name → value label (`PLUNE_CONFIGURATION`, plune-ai/plune#1251):
329
+ * sent as `configuration` on every results batch. Not the run's own `configuration` (what it expects).
330
+ * Stated, it is all there is: the variable is not read, and `{}` sends none (what a replay passes for a line that recorded none).
331
+ */
332
+ configuration?: Record<string, string>;
327
333
  /** Results per request. The platform's ceiling is 500. */
328
334
  batchSize?: number;
329
335
  /** Files in flight at once (`PLUNE_UPLOAD_CONCURRENCY`, 1 to 32, default 16). */
@@ -445,6 +451,8 @@ interface EnvSettings {
445
451
  shard?: ShardOf;
446
452
  /** `PLUNE_SHARD` was set to something that is not `k/N`. A configuration error: the caller refuses to start. */
447
453
  shardError?: string;
454
+ /** `PLUNE_CONFIGURATION` was set to something that is not `Group=Value;…`. Refused the same way. */
455
+ configurationError?: string;
448
456
  /** `PLUNE_IMPORT_TIMEOUT` in seconds, when set and usable. */
449
457
  importTimeout?: number;
450
458
  /** Variables that were set and have nowhere to land. The caller says so once. */
@@ -498,12 +506,17 @@ declare const DEFAULT_FALLBACK_PATH: string;
498
506
  * A run of 24 tests at a batch of 10 is three lines, and replay opens one run for the lines that share a
499
507
  * marker rather than one for each. A line written before the field existed has none, and is one run's
500
508
  * worth by itself — nothing in it says otherwise.
509
+ *
510
+ * `configuration` is the combination the job named (`PLUNE_CONFIGURATION`, plune-ai/plune#1251), recorded for
511
+ * the same reason as `meta`: replay sends the batch into the combination it ran in, whatever the machine that
512
+ * replays it has set. A line without it was written by a job that named none, or before the field existed.
501
513
  */
502
514
  interface DeferredBatch {
503
515
  runId: string | null;
504
516
  externalKey: string | null;
505
517
  session?: string;
506
518
  meta?: RunMeta;
519
+ configuration?: Record<string, string>;
507
520
  results: DeferredResult[];
508
521
  }
509
522
  /**
@@ -265,6 +265,25 @@ function shardOf(env) {
265
265
  error: `PLUNE_SHARD=${JSON.stringify(raw.slice(0, 40))} is not "k/N" with 1 <= k <= N <= ${SHARDS_MAX} (for example 2/4) - nothing was sent.`
266
266
  };
267
267
  }
268
+ function configurationOf(env) {
269
+ const raw = value(env, "PLUNE_CONFIGURATION");
270
+ if (raw === void 0) return {};
271
+ const pairs = [];
272
+ for (const pair of raw.split(";").map((s) => s.trim()).filter((s) => s !== "")) {
273
+ const at = pair.indexOf("=");
274
+ const name = at < 0 ? "" : pair.slice(0, at).trim();
275
+ const label = at < 0 ? "" : pair.slice(at + 1).trim();
276
+ const twice = pairs.find(([seen]) => seen.toLowerCase() === name.toLowerCase());
277
+ const why = at < 0 ? 'has no "="' : name === "" ? "has no group name" : label === "" ? "has no value" : twice !== void 0 ? `names the group ${JSON.stringify(twice[0])} a second time` : void 0;
278
+ if (why !== void 0) {
279
+ return {
280
+ error: `PLUNE_CONFIGURATION: ${JSON.stringify(pair.slice(0, 40))} ${why} - write it as "Group=Value;Group=Value" (for example "Browser=Chrome;Operating system=Windows") - nothing was sent.`
281
+ };
282
+ }
283
+ pairs.push([name, label]);
284
+ }
285
+ return pairs.length === 0 ? {} : { configuration: Object.fromEntries(pairs) };
286
+ }
268
287
  var UPLOAD_CONCURRENCY = { min: 1, max: 32, default: 16 };
269
288
  var UPLOAD_RETRIES = { min: 0, max: 20, default: 6 };
270
289
  var IMPORT_TIMEOUT = { min: 0, max: 86400, default: 600 };
@@ -312,6 +331,7 @@ function readEnv(env = process.env) {
312
331
  const offerDiscovered = flag(env, "PLUNE_CREATE");
313
332
  const full = flag(env, "PLUNE_FULL_RUN");
314
333
  const { shard, error: shardError } = shardOf(env);
334
+ const { configuration, error: configurationError } = configurationOf(env);
315
335
  return {
316
336
  config: {
317
337
  ...externalKey !== void 0 ? { externalKey } : {},
@@ -324,6 +344,7 @@ function readEnv(env = process.env) {
324
344
  ...title !== void 0 ? { title } : {},
325
345
  ...environment !== void 0 ? { environment } : {},
326
346
  ...labels !== void 0 ? { labels } : {},
347
+ ...configuration !== void 0 ? { configuration } : {},
327
348
  // Only when set: `false` here would outrank a config file that asked for it, and an unset
328
349
  // variable is not an instruction.
329
350
  ...offerDiscovered ? { offerDiscovered } : {},
@@ -337,6 +358,7 @@ function readEnv(env = process.env) {
337
358
  keepOpen: flag(env, "PLUNE_SHARED_RUN") || flag(env, "PLUNE_PROCEED") || shard !== void 0 || shardError !== void 0,
338
359
  ...shard !== void 0 ? { shard } : {},
339
360
  ...shardError !== void 0 ? { shardError } : {},
361
+ ...configurationError !== void 0 ? { configurationError } : {},
340
362
  ...importTimeout !== void 0 ? { importTimeout } : {},
341
363
  ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0),
342
364
  warnings
@@ -547,6 +569,7 @@ function appendBatch(file, batch) {
547
569
  externalKey: batch.externalKey,
548
570
  ...batch.session !== void 0 ? { session: batch.session } : {},
549
571
  ...batch.meta !== void 0 ? { meta: batch.meta } : {},
572
+ ...batch.configuration !== void 0 ? { configuration: batch.configuration } : {},
550
573
  results: batch.results
551
574
  };
552
575
  fs.appendFileSync(file, `${JSON.stringify(line)}
@@ -697,8 +720,10 @@ function warnIfDropped(sent, run, log) {
697
720
  async function startRun(passed, declared) {
698
721
  const env = readEnv();
699
722
  if (env.shardError !== void 0) throw new Error(env.shardError);
723
+ if (env.configurationError !== void 0 && passed.configuration === void 0) throw new Error(env.configurationError);
700
724
  const stated = Object.fromEntries(Object.entries(passed).filter(([, v]) => v !== void 0));
701
725
  const cfg = { ...env.config, ...stated };
726
+ const configuration = cfg.configuration !== void 0 && Object.keys(cfg.configuration).length > 0 ? cfg.configuration : void 0;
702
727
  const log = cfg.log ?? ((line) => void process.stderr.write(`${line}
703
728
  `));
704
729
  const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
@@ -782,6 +807,7 @@ async function startRun(passed, declared) {
782
807
  externalKey,
783
808
  session: sessionMarker,
784
809
  ...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
810
+ ...configuration !== void 0 ? { configuration } : {},
785
811
  results: results.map(onDisk)
786
812
  });
787
813
  stats.deferred += results.length;
@@ -806,7 +832,7 @@ async function startRun(passed, declared) {
806
832
  if (!offline) {
807
833
  if (expected !== void 0) await resolveKeys(expected.flat());
808
834
  const stated2 = expected?.slice(0, EXPECTED_MAX) ?? [];
809
- const configuration = stated2.length === 0 ? void 0 : {
835
+ const configuration2 = stated2.length === 0 ? void 0 : {
810
836
  expected: stated2.flatMap((keys) => {
811
837
  const hit = keys.map((k) => resolved.get(k.value)).find((v) => typeof v === "string");
812
838
  if (typeof hit === "string") return [{ testCaseId: hit }];
@@ -830,7 +856,7 @@ async function startRun(passed, declared) {
830
856
  kind: cfg.kind ?? "automated",
831
857
  ...externalKey !== null ? { externalKey } : {},
832
858
  ...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
833
- ...configuration !== void 0 ? { configuration } : {},
859
+ ...configuration2 !== void 0 ? { configuration: configuration2 } : {},
834
860
  // Named even when nobody named it — the list has to read without opening a row. Only what
835
861
  // the caller DESCRIBED goes to `warnIfDropped` below: a title this side invented is not one
836
862
  // a person will miss, and a deployment older than the field must not be nagged on every run.
@@ -838,7 +864,7 @@ async function startRun(passed, declared) {
838
864
  ...described
839
865
  };
840
866
  let start = await client.post("/v1/runs", request);
841
- if (!start.ok && start.status === 413 && configuration !== void 0) {
867
+ if (!start.ok && start.status === 413 && configuration2 !== void 0) {
842
868
  log(
843
869
  `plune: the platform refused the run's list of expected tests as too large (${start.detail || start.kind}) \u2014 starting without it: the results still land, but this run counts no tests that did not run, and claims no full run.`
844
870
  );
@@ -1384,7 +1410,8 @@ async function startRun(passed, declared) {
1384
1410
  }
1385
1411
  const out = await client.post(
1386
1412
  `/v1/runs/${runId}/results`,
1387
- { results: part }
1413
+ // The job's combination on every batch (plune-ai/plune#1251); a refusal of it is a 422 said below as it came.
1414
+ { results: part, ...configuration !== void 0 ? { configuration } : {} }
1388
1415
  );
1389
1416
  const counts = out.ok ? out.body?.counts : void 0;
1390
1417
  const told = COUNTED.filter((k) => typeof counts?.[k] === "number");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plune-ai/cli",
3
- "version": "0.30.2",
3
+ "version": "0.31.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",