@plune-ai/cli 0.30.0 → 0.30.2

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 CHANGED
@@ -121,7 +121,7 @@ the last resort, used only when `PLUNE_TOKEN` is set nowhere else. Nothing write
121
121
  | `plune init` | Scaffold `plune.yaml`, a sample dataset, and `.env.example`. Flags: `--yes` (non-interactive), `--force`. |
122
122
  | `plune login` | Save a [Plune platform](https://plune.ai/platform) API token so `sync` and `ingest` can reach it. The token is **checked against the API before it is saved**, so a wrong one fails here rather than two commands later. Get one at `https://beta.plune.ai` → Account → API tokens. Flags: `--token <token>` (omit to paste it or pipe it via stdin), `--skip-verify` (save without checking, for offline setup). In CI, skip this step: every platform command reads `PLUNE_TOKEN` from the environment first, and the saved login only when it is not set. |
123
123
  | `plune logout` | Remove the saved token. |
124
- | `plune sync` | Upload the latest local run to the platform. Flags: `--file <path>` to send a specific run JSON, `--fail-on-failures` to read the run's summary back once after the upload and exit with [the gate](#a-release-gate---fail-on-failures)'s code. Each upload carries an `Idempotency-Key` (SHA-256 of the body), so sending the same run again returns the same run rather than creating a second one. Against a platform with `POST /v1/jobs` the upload is also reported as a job (kind `cli-sync`) that ends failed with the error text if the sync fails; an older platform without the route is not told, and a job report never fails the sync. |
124
+ | `plune sync` | Upload the latest local run to the platform. Flags: `--file <path>` to send a specific run JSON, `--fail-on-failures` to read the run's summary back once after the upload and exit with [the gate](#a-release-gate---fail-on-failures)'s code. Each upload carries an `Idempotency-Key` (SHA-256 of the body), so sending the same run again returns the same run rather than creating a second one. Against a platform with `POST /v1/jobs` the upload is also reported as a job (kind `sync`, counting the run's rows) that ends completed, or failed with the error text if the sync fails; an older platform without the route is not told, and a job report never fails the sync: when the sync goes through, one the platform did not take is said in one line on stderr. |
125
125
  | `plune run import <file>` | Turn a **JUnit XML** or **Playwright JSON** report, or an **Allure `allure-results` folder**, into a run in Plune. Needs no provider key — nothing is generated. Flags: `--format junit\|playwright-json\|allure` (detected from the file when omitted; a folder is Allure), `--name <text>` to call the run something other than the [folder-based default](#what-the-run-is-called), `--key <externalKey>` to land several reports in one run (with `PLUNE_SHARED_RUN=1` — see below), `--create` to offer unmatched tests to the review queue, `--fail-on-missing-attachments` to exit 1 when a file a JUnit report names with `[[ATTACHMENT\|path]]`, or an Allure result names as a `source`, is not there (the results are sent either way), `--upload-concurrency <n>` for files uploaded at once ([1 to 32, default 16](#many-screenshots-upload-concurrency-and-retries)), `--fail-on-failures` to exit with [the gate](#a-release-gate---fail-on-failures)'s code on the summary the platform froze when this import finished the run. |
126
126
  | `plune run start` | Open a platform run — or join the one already carrying `--key` — and print its id. Flags: `--key <externalKey>` (generated when absent), `--name <text>` to call the run something other than the [default](#what-the-run-is-called), `--json` for one machine-readable line. |
127
127
  | `plune run finish <id>` | Close a platform run. This is what the reporter tells you to do for a run it had to leave open. Flags: `--terminate` to record it as cut short, `--reason <text>`, `--fail-on-failures` to exit with [the gate](#a-release-gate---fail-on-failures)'s code on the summary the platform froze. |
package/dist/cli.cjs CHANGED
@@ -3230,7 +3230,7 @@ async function startRun(passed, declared2) {
3230
3230
  ...cfg.environment !== void 0 ? { environment: cfg.environment } : {},
3231
3231
  ...cfg.labels !== void 0 ? { labels: cfg.labels } : {}
3232
3232
  };
3233
- const start = await client.post("/v1/runs", {
3233
+ const request = {
3234
3234
  schemaVersion: 2,
3235
3235
  kind: cfg.kind ?? "automated",
3236
3236
  ...externalKey !== null ? { externalKey } : {},
@@ -3241,7 +3241,14 @@ async function startRun(passed, declared2) {
3241
3241
  // a person will miss, and a deployment older than the field must not be nagged on every run.
3242
3242
  title: defaultRunTitle(),
3243
3243
  ...described
3244
- });
3244
+ };
3245
+ let start = await client.post("/v1/runs", request);
3246
+ if (!start.ok && start.status === 413 && configuration !== void 0) {
3247
+ log(
3248
+ `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.`
3249
+ );
3250
+ start = await client.post("/v1/runs", { ...request, configuration: void 0 });
3251
+ }
3245
3252
  if (start.ok && typeof start.body?.run?.id === "string") {
3246
3253
  runId = start.body.run.id;
3247
3254
  joined = start.status === 200;
@@ -4369,10 +4376,11 @@ async function handleSync(deps = {}) {
4369
4376
  const idempotencyKey = (0, import_node_crypto4.createHash)("sha256").update(payload).digest("hex");
4370
4377
  const bearer = token;
4371
4378
  const job = new JobReporter(doFetch, apiUrl, bearer);
4372
- await job.open(path9.basename(deps.file !== void 0 ? path9.resolve(cwd, deps.file) : cwd));
4379
+ await job.open(path9.basename(deps.file !== void 0 ? path9.resolve(cwd, deps.file) : cwd), run.summary.total);
4373
4380
  try {
4374
4381
  const result = await upload();
4375
4382
  await job.finish({ ok: true });
4383
+ if (job.problem !== void 0) result.jobProblem = job.problem;
4376
4384
  return result;
4377
4385
  } catch (err) {
4378
4386
  await job.finish({ ok: false, reason: err instanceof Error ? err.message : "Sync failed." });
@@ -4401,7 +4409,7 @@ async function handleSync(deps = {}) {
4401
4409
  const body = await res.json().catch(() => ({}));
4402
4410
  const id = typeof body.id === "string" ? body.id : "";
4403
4411
  const result = { id, url: `${endpoint}/${id}` };
4404
- await job.report(deps.failOnFailures === true ? 80 : 95, id || void 0);
4412
+ await job.uploaded(id || void 0);
4405
4413
  if (deps.failOnFailures === true) {
4406
4414
  result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, bearer));
4407
4415
  }
@@ -4428,7 +4436,7 @@ function reportSyncFailure(err, write) {
4428
4436
  }
4429
4437
  return null;
4430
4438
  }
4431
- var import_node_crypto4, fs8, path9, NotLoggedInError, TokenRejectedError, SyncFileError, SyncNetworkError, SyncHttpError, JOB_KIND, JOB_TIMEOUT_MS, JobReporter;
4439
+ var import_node_crypto4, fs8, path9, NotLoggedInError, TokenRejectedError, SyncFileError, SyncNetworkError, SyncHttpError, JOB_KIND, JOB_TIMEOUT_MS, JOB_SUBJECT_NAME_MAX, JOB_REASON_MAX, JobReporter;
4432
4440
  var init_sync = __esm({
4433
4441
  "src/cli/commands/sync.ts"() {
4434
4442
  "use strict";
@@ -4476,8 +4484,10 @@ var init_sync = __esm({
4476
4484
  }
4477
4485
  status;
4478
4486
  };
4479
- JOB_KIND = "cli-sync";
4487
+ JOB_KIND = "sync";
4480
4488
  JOB_TIMEOUT_MS = 5e3;
4489
+ JOB_SUBJECT_NAME_MAX = 200;
4490
+ JOB_REASON_MAX = 2e3;
4481
4491
  JobReporter = class {
4482
4492
  constructor(doFetch, base, token) {
4483
4493
  this.doFetch = doFetch;
@@ -4488,32 +4498,43 @@ var init_sync = __esm({
4488
4498
  base;
4489
4499
  token;
4490
4500
  id;
4491
- async send(method, url, body) {
4501
+ /** The run's rows, the job's `total`; `null` while there are none to count. */
4502
+ size = null;
4503
+ /** The first report the platform did not take, for the CLI to say once. */
4504
+ problem;
4505
+ async send(method, route, body) {
4492
4506
  try {
4493
- const res = await this.doFetch(url, {
4507
+ const res = await this.doFetch(`${this.base}/v1/jobs${route}`, {
4494
4508
  method,
4495
4509
  headers: { "content-type": "application/json", authorization: `Bearer ${this.token}` },
4496
4510
  body: JSON.stringify(body),
4497
4511
  signal: AbortSignal.timeout(JOB_TIMEOUT_MS)
4498
4512
  });
4499
4513
  const json = await res.json().catch(() => void 0);
4500
- return res.ok ? json ?? {} : void 0;
4501
- } catch {
4502
- return void 0;
4514
+ if (res.ok) return json ?? {};
4515
+ if (route === "" && (res.status === 404 || res.status === 405)) return void 0;
4516
+ this.problem ??= `HTTP ${res.status}${typeof json?.error === "string" ? ` \u2014 ${json.error}` : ""}`;
4517
+ } catch (err) {
4518
+ this.problem ??= err instanceof Error ? err.message : String(err);
4503
4519
  }
4520
+ return void 0;
4504
4521
  }
4505
- async open(subjectName) {
4506
- const body = await this.send("POST", `${this.base}/v1/jobs`, {
4522
+ async open(subjectName, rows) {
4523
+ this.size = Number.isInteger(rows) && rows > 0 ? rows : null;
4524
+ const name = subjectName.trim().slice(0, JOB_SUBJECT_NAME_MAX);
4525
+ const body = await this.send("POST", "", {
4507
4526
  kind: JOB_KIND,
4508
- subjectName,
4509
- progress: 0
4527
+ ...name !== "" ? { subjectName: name } : {},
4528
+ ...this.size !== null ? { progress: { done: 0, total: this.size } } : {}
4510
4529
  });
4511
4530
  if (typeof body?.id === "string") this.id = body.id;
4512
4531
  }
4513
- async report(progress, cursor) {
4532
+ /** The upload is in: every row of the run is on the platform. */
4533
+ async uploaded(cursor) {
4514
4534
  if (this.id === void 0) return;
4515
- await this.send("PATCH", `${this.base}/v1/jobs/${encodeURIComponent(this.id)}`, {
4516
- progress,
4535
+ await this.send("PATCH", `/${encodeURIComponent(this.id)}/progress`, {
4536
+ done: this.size,
4537
+ total: this.size,
4517
4538
  ...cursor !== void 0 ? { cursor } : {}
4518
4539
  });
4519
4540
  }
@@ -4523,8 +4544,8 @@ var init_sync = __esm({
4523
4544
  this.id = void 0;
4524
4545
  await this.send(
4525
4546
  "PATCH",
4526
- `${this.base}/v1/jobs/${encodeURIComponent(id)}`,
4527
- outcome.ok ? { status: "succeeded", progress: 100 } : { status: "failed", reason: outcome.reason }
4547
+ `/${encodeURIComponent(id)}/state`,
4548
+ outcome.ok ? { state: "completed" } : { state: "failed", reason: outcome.reason.slice(0, JOB_REASON_MAX) }
4528
4549
  );
4529
4550
  }
4530
4551
  };
@@ -7658,13 +7679,17 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
7658
7679
  const { handleSync: handleSync2, reportSyncFailure: reportSyncFailure2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
7659
7680
  const cwd = globals.config !== void 0 ? (0, import_node_path6.dirname)((0, import_node_path6.resolve)(globals.config)) : process.cwd();
7660
7681
  try {
7661
- const { id, url, gate } = await handleSync2({
7682
+ const { id, url, gate, jobProblem } = await handleSync2({
7662
7683
  cwd,
7663
7684
  ...options.file !== void 0 ? { file: options.file } : {},
7664
7685
  ...options.failOnFailures ? { failOnFailures: true } : {}
7665
7686
  });
7666
7687
  process.stdout.write(`Synced run ${id} \u2192 ${url}
7667
7688
  `);
7689
+ if (jobProblem !== void 0) {
7690
+ process.stderr.write(`plune: could not report this sync to the job queue (${jobProblem}) \u2014 the run is uploaded.
7691
+ `);
7692
+ }
7668
7693
  if (gate !== void 0) {
7669
7694
  process.stdout.write(`${gate.line}
7670
7695
  `);
package/dist/index.cjs CHANGED
@@ -1892,8 +1892,10 @@ var SyncHttpError = class extends Error {
1892
1892
  }
1893
1893
  status;
1894
1894
  };
1895
- var JOB_KIND = "cli-sync";
1895
+ var JOB_KIND = "sync";
1896
1896
  var JOB_TIMEOUT_MS = 5e3;
1897
+ var JOB_SUBJECT_NAME_MAX = 200;
1898
+ var JOB_REASON_MAX = 2e3;
1897
1899
  var JobReporter = class {
1898
1900
  constructor(doFetch, base, token) {
1899
1901
  this.doFetch = doFetch;
@@ -1904,32 +1906,43 @@ var JobReporter = class {
1904
1906
  base;
1905
1907
  token;
1906
1908
  id;
1907
- async send(method, url, body) {
1909
+ /** The run's rows, the job's `total`; `null` while there are none to count. */
1910
+ size = null;
1911
+ /** The first report the platform did not take, for the CLI to say once. */
1912
+ problem;
1913
+ async send(method, route, body) {
1908
1914
  try {
1909
- const res = await this.doFetch(url, {
1915
+ const res = await this.doFetch(`${this.base}/v1/jobs${route}`, {
1910
1916
  method,
1911
1917
  headers: { "content-type": "application/json", authorization: `Bearer ${this.token}` },
1912
1918
  body: JSON.stringify(body),
1913
1919
  signal: AbortSignal.timeout(JOB_TIMEOUT_MS)
1914
1920
  });
1915
1921
  const json = await res.json().catch(() => void 0);
1916
- return res.ok ? json ?? {} : void 0;
1917
- } catch {
1918
- return void 0;
1922
+ if (res.ok) return json ?? {};
1923
+ if (route === "" && (res.status === 404 || res.status === 405)) return void 0;
1924
+ this.problem ??= `HTTP ${res.status}${typeof json?.error === "string" ? ` \u2014 ${json.error}` : ""}`;
1925
+ } catch (err) {
1926
+ this.problem ??= err instanceof Error ? err.message : String(err);
1919
1927
  }
1928
+ return void 0;
1920
1929
  }
1921
- async open(subjectName) {
1922
- const body = await this.send("POST", `${this.base}/v1/jobs`, {
1930
+ async open(subjectName, rows) {
1931
+ this.size = Number.isInteger(rows) && rows > 0 ? rows : null;
1932
+ const name = subjectName.trim().slice(0, JOB_SUBJECT_NAME_MAX);
1933
+ const body = await this.send("POST", "", {
1923
1934
  kind: JOB_KIND,
1924
- subjectName,
1925
- progress: 0
1935
+ ...name !== "" ? { subjectName: name } : {},
1936
+ ...this.size !== null ? { progress: { done: 0, total: this.size } } : {}
1926
1937
  });
1927
1938
  if (typeof body?.id === "string") this.id = body.id;
1928
1939
  }
1929
- async report(progress, cursor) {
1940
+ /** The upload is in: every row of the run is on the platform. */
1941
+ async uploaded(cursor) {
1930
1942
  if (this.id === void 0) return;
1931
- await this.send("PATCH", `${this.base}/v1/jobs/${encodeURIComponent(this.id)}`, {
1932
- progress,
1943
+ await this.send("PATCH", `/${encodeURIComponent(this.id)}/progress`, {
1944
+ done: this.size,
1945
+ total: this.size,
1933
1946
  ...cursor !== void 0 ? { cursor } : {}
1934
1947
  });
1935
1948
  }
@@ -1939,8 +1952,8 @@ var JobReporter = class {
1939
1952
  this.id = void 0;
1940
1953
  await this.send(
1941
1954
  "PATCH",
1942
- `${this.base}/v1/jobs/${encodeURIComponent(id)}`,
1943
- outcome.ok ? { status: "succeeded", progress: 100 } : { status: "failed", reason: outcome.reason }
1955
+ `/${encodeURIComponent(id)}/state`,
1956
+ outcome.ok ? { state: "completed" } : { state: "failed", reason: outcome.reason.slice(0, JOB_REASON_MAX) }
1944
1957
  );
1945
1958
  }
1946
1959
  };
@@ -1986,10 +1999,11 @@ async function handleSync(deps = {}) {
1986
1999
  const idempotencyKey = (0, import_node_crypto2.createHash)("sha256").update(payload).digest("hex");
1987
2000
  const bearer = token;
1988
2001
  const job = new JobReporter(doFetch, apiUrl, bearer);
1989
- await job.open(path5.basename(deps.file !== void 0 ? path5.resolve(cwd, deps.file) : cwd));
2002
+ await job.open(path5.basename(deps.file !== void 0 ? path5.resolve(cwd, deps.file) : cwd), run.summary.total);
1990
2003
  try {
1991
2004
  const result = await upload();
1992
2005
  await job.finish({ ok: true });
2006
+ if (job.problem !== void 0) result.jobProblem = job.problem;
1993
2007
  return result;
1994
2008
  } catch (err) {
1995
2009
  await job.finish({ ok: false, reason: err instanceof Error ? err.message : "Sync failed." });
@@ -2018,7 +2032,7 @@ async function handleSync(deps = {}) {
2018
2032
  const body = await res.json().catch(() => ({}));
2019
2033
  const id = typeof body.id === "string" ? body.id : "";
2020
2034
  const result = { id, url: `${endpoint}/${id}` };
2021
- await job.report(deps.failOnFailures === true ? 80 : 95, id || void 0);
2035
+ await job.uploaded(id || void 0);
2022
2036
  if (deps.failOnFailures === true) {
2023
2037
  result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, bearer));
2024
2038
  }
package/dist/index.d.cts CHANGED
@@ -364,6 +364,8 @@ interface SyncResult {
364
364
  code: number;
365
365
  line: string;
366
366
  };
367
+ /** Why the platform's job queue did not take this sync's report: the first refusal or error. The upload went in. */
368
+ jobProblem?: string;
367
369
  }
368
370
  /**
369
371
  * Upload the latest (or `--file`) local run to `POST {apiUrl}/v1/runs` with the stored Bearer token.
package/dist/index.d.ts CHANGED
@@ -364,6 +364,8 @@ interface SyncResult {
364
364
  code: number;
365
365
  line: string;
366
366
  };
367
+ /** Why the platform's job queue did not take this sync's report: the first refusal or error. The upload went in. */
368
+ jobProblem?: string;
367
369
  }
368
370
  /**
369
371
  * Upload the latest (or `--file`) local run to `POST {apiUrl}/v1/runs` with the stored Bearer token.
package/dist/index.js CHANGED
@@ -1832,8 +1832,10 @@ var SyncHttpError = class extends Error {
1832
1832
  }
1833
1833
  status;
1834
1834
  };
1835
- var JOB_KIND = "cli-sync";
1835
+ var JOB_KIND = "sync";
1836
1836
  var JOB_TIMEOUT_MS = 5e3;
1837
+ var JOB_SUBJECT_NAME_MAX = 200;
1838
+ var JOB_REASON_MAX = 2e3;
1837
1839
  var JobReporter = class {
1838
1840
  constructor(doFetch, base, token) {
1839
1841
  this.doFetch = doFetch;
@@ -1844,32 +1846,43 @@ var JobReporter = class {
1844
1846
  base;
1845
1847
  token;
1846
1848
  id;
1847
- async send(method, url, body) {
1849
+ /** The run's rows, the job's `total`; `null` while there are none to count. */
1850
+ size = null;
1851
+ /** The first report the platform did not take, for the CLI to say once. */
1852
+ problem;
1853
+ async send(method, route, body) {
1848
1854
  try {
1849
- const res = await this.doFetch(url, {
1855
+ const res = await this.doFetch(`${this.base}/v1/jobs${route}`, {
1850
1856
  method,
1851
1857
  headers: { "content-type": "application/json", authorization: `Bearer ${this.token}` },
1852
1858
  body: JSON.stringify(body),
1853
1859
  signal: AbortSignal.timeout(JOB_TIMEOUT_MS)
1854
1860
  });
1855
1861
  const json = await res.json().catch(() => void 0);
1856
- return res.ok ? json ?? {} : void 0;
1857
- } catch {
1858
- return void 0;
1862
+ if (res.ok) return json ?? {};
1863
+ if (route === "" && (res.status === 404 || res.status === 405)) return void 0;
1864
+ this.problem ??= `HTTP ${res.status}${typeof json?.error === "string" ? ` \u2014 ${json.error}` : ""}`;
1865
+ } catch (err) {
1866
+ this.problem ??= err instanceof Error ? err.message : String(err);
1859
1867
  }
1868
+ return void 0;
1860
1869
  }
1861
- async open(subjectName) {
1862
- const body = await this.send("POST", `${this.base}/v1/jobs`, {
1870
+ async open(subjectName, rows) {
1871
+ this.size = Number.isInteger(rows) && rows > 0 ? rows : null;
1872
+ const name = subjectName.trim().slice(0, JOB_SUBJECT_NAME_MAX);
1873
+ const body = await this.send("POST", "", {
1863
1874
  kind: JOB_KIND,
1864
- subjectName,
1865
- progress: 0
1875
+ ...name !== "" ? { subjectName: name } : {},
1876
+ ...this.size !== null ? { progress: { done: 0, total: this.size } } : {}
1866
1877
  });
1867
1878
  if (typeof body?.id === "string") this.id = body.id;
1868
1879
  }
1869
- async report(progress, cursor) {
1880
+ /** The upload is in: every row of the run is on the platform. */
1881
+ async uploaded(cursor) {
1870
1882
  if (this.id === void 0) return;
1871
- await this.send("PATCH", `${this.base}/v1/jobs/${encodeURIComponent(this.id)}`, {
1872
- progress,
1883
+ await this.send("PATCH", `/${encodeURIComponent(this.id)}/progress`, {
1884
+ done: this.size,
1885
+ total: this.size,
1873
1886
  ...cursor !== void 0 ? { cursor } : {}
1874
1887
  });
1875
1888
  }
@@ -1879,8 +1892,8 @@ var JobReporter = class {
1879
1892
  this.id = void 0;
1880
1893
  await this.send(
1881
1894
  "PATCH",
1882
- `${this.base}/v1/jobs/${encodeURIComponent(id)}`,
1883
- outcome.ok ? { status: "succeeded", progress: 100 } : { status: "failed", reason: outcome.reason }
1895
+ `/${encodeURIComponent(id)}/state`,
1896
+ outcome.ok ? { state: "completed" } : { state: "failed", reason: outcome.reason.slice(0, JOB_REASON_MAX) }
1884
1897
  );
1885
1898
  }
1886
1899
  };
@@ -1926,10 +1939,11 @@ async function handleSync(deps = {}) {
1926
1939
  const idempotencyKey = createHash2("sha256").update(payload).digest("hex");
1927
1940
  const bearer = token;
1928
1941
  const job = new JobReporter(doFetch, apiUrl, bearer);
1929
- await job.open(path5.basename(deps.file !== void 0 ? path5.resolve(cwd, deps.file) : cwd));
1942
+ await job.open(path5.basename(deps.file !== void 0 ? path5.resolve(cwd, deps.file) : cwd), run.summary.total);
1930
1943
  try {
1931
1944
  const result = await upload();
1932
1945
  await job.finish({ ok: true });
1946
+ if (job.problem !== void 0) result.jobProblem = job.problem;
1933
1947
  return result;
1934
1948
  } catch (err) {
1935
1949
  await job.finish({ ok: false, reason: err instanceof Error ? err.message : "Sync failed." });
@@ -1958,7 +1972,7 @@ async function handleSync(deps = {}) {
1958
1972
  const body = await res.json().catch(() => ({}));
1959
1973
  const id = typeof body.id === "string" ? body.id : "";
1960
1974
  const result = { id, url: `${endpoint}/${id}` };
1961
- await job.report(deps.failOnFailures === true ? 80 : 95, id || void 0);
1975
+ await job.uploaded(id || void 0);
1962
1976
  if (deps.failOnFailures === true) {
1963
1977
  result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, bearer));
1964
1978
  }
@@ -875,7 +875,7 @@ async function startRun(passed, declared) {
875
875
  ...cfg.environment !== void 0 ? { environment: cfg.environment } : {},
876
876
  ...cfg.labels !== void 0 ? { labels: cfg.labels } : {}
877
877
  };
878
- const start = await client.post("/v1/runs", {
878
+ const request = {
879
879
  schemaVersion: 2,
880
880
  kind: cfg.kind ?? "automated",
881
881
  ...externalKey !== null ? { externalKey } : {},
@@ -886,7 +886,14 @@ async function startRun(passed, declared) {
886
886
  // a person will miss, and a deployment older than the field must not be nagged on every run.
887
887
  title: defaultRunTitle(),
888
888
  ...described
889
- });
889
+ };
890
+ let start = await client.post("/v1/runs", request);
891
+ if (!start.ok && start.status === 413 && configuration !== void 0) {
892
+ log(
893
+ `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.`
894
+ );
895
+ start = await client.post("/v1/runs", { ...request, configuration: void 0 });
896
+ }
890
897
  if (start.ok && typeof start.body?.run?.id === "string") {
891
898
  runId = start.body.run.id;
892
899
  joined = start.status === 200;
@@ -825,7 +825,7 @@ async function startRun(passed, declared) {
825
825
  ...cfg.environment !== void 0 ? { environment: cfg.environment } : {},
826
826
  ...cfg.labels !== void 0 ? { labels: cfg.labels } : {}
827
827
  };
828
- const start = await client.post("/v1/runs", {
828
+ const request = {
829
829
  schemaVersion: 2,
830
830
  kind: cfg.kind ?? "automated",
831
831
  ...externalKey !== null ? { externalKey } : {},
@@ -836,7 +836,14 @@ async function startRun(passed, declared) {
836
836
  // a person will miss, and a deployment older than the field must not be nagged on every run.
837
837
  title: defaultRunTitle(),
838
838
  ...described
839
- });
839
+ };
840
+ let start = await client.post("/v1/runs", request);
841
+ if (!start.ok && start.status === 413 && configuration !== void 0) {
842
+ log(
843
+ `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
+ );
845
+ start = await client.post("/v1/runs", { ...request, configuration: void 0 });
846
+ }
840
847
  if (start.ok && typeof start.body?.run?.id === "string") {
841
848
  runId = start.body.run.id;
842
849
  joined = start.status === 200;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plune-ai/cli",
3
- "version": "0.30.0",
3
+ "version": "0.30.2",
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",