@plune-ai/cli 0.23.0 → 0.24.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 CHANGED
@@ -119,7 +119,7 @@ the last resort, used only when `PLUNE_TOKEN` is set nowhere else. Nothing write
119
119
  | `plune init` | Scaffold `plune.yaml`, a sample dataset, and `.env.example`. Flags: `--yes` (non-interactive), `--force`. |
120
120
  | `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. |
121
121
  | `plune logout` | Remove the saved token. |
122
- | `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. |
122
+ | `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. |
123
123
  | `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), `--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 4](#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. |
124
124
  | `plune run start` | Open a platform run — or join the one already carrying `--key` — and print its id. Flags: `--key <externalKey>` (generated when absent), `--json` for one machine-readable line. |
125
125
  | `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
@@ -4086,12 +4086,18 @@ async function handleSync(deps = {}) {
4086
4086
  const run = readRun(cwd, deps.file);
4087
4087
  const doFetch = deps.fetchImpl ?? fetch;
4088
4088
  const endpoint = `${apiUrl}/v1/runs`;
4089
+ const payload = JSON.stringify(run);
4090
+ const idempotencyKey = (0, import_node_crypto4.createHash)("sha256").update(payload).digest("hex");
4089
4091
  let res;
4090
4092
  try {
4091
4093
  res = await doFetch(endpoint, {
4092
4094
  method: "POST",
4093
- headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
4094
- body: JSON.stringify(run)
4095
+ headers: {
4096
+ "content-type": "application/json",
4097
+ authorization: `Bearer ${token}`,
4098
+ "idempotency-key": idempotencyKey
4099
+ },
4100
+ body: payload
4095
4101
  });
4096
4102
  } catch {
4097
4103
  throw new SyncNetworkError(apiUrl);
@@ -4127,11 +4133,12 @@ function reportSyncFailure(err, write) {
4127
4133
  }
4128
4134
  return null;
4129
4135
  }
4130
- var fs8, path9, NotLoggedInError, TokenRejectedError, SyncFileError, SyncNetworkError, SyncHttpError;
4136
+ var import_node_crypto4, fs8, path9, NotLoggedInError, TokenRejectedError, SyncFileError, SyncNetworkError, SyncHttpError;
4131
4137
  var init_sync = __esm({
4132
4138
  "src/cli/commands/sync.ts"() {
4133
4139
  "use strict";
4134
4140
  init_cjs_shims();
4141
+ import_node_crypto4 = require("crypto");
4135
4142
  fs8 = __toESM(require("fs"), 1);
4136
4143
  path9 = __toESM(require("path"), 1);
4137
4144
  init_run_result_schema();
@@ -5363,7 +5370,10 @@ function describe(result, apiUrl) {
5363
5370
  );
5364
5371
  }
5365
5372
  if (result.offered > 0) {
5366
- lines.push(`${result.offered} unknown test(s) offered to the review queue.`);
5373
+ const source = result.format === "playwright-json" ? "playwright" : result.format;
5374
+ lines.push(
5375
+ `${result.offered} unknown test(s) offered to the review queue. They wait there for a decision and do not count on the run until approved. To skip review for this source, add \`${source}\` under Settings \u2192 Trusted sources.`
5376
+ );
5367
5377
  } else if (result.unresolved > 0 && result.created === 0 && !result.offering) {
5368
5378
  lines.push("Nothing was offered to the review queue \u2014 pass --create to propose the unmatched tests.");
5369
5379
  } else if (result.unresolved > 0 && result.created === 0 && result.unoffered === 0) {
package/dist/index.cjs CHANGED
@@ -1610,6 +1610,7 @@ async function handleRun(options, depsFactory) {
1610
1610
  }
1611
1611
 
1612
1612
  // src/cli/commands/sync.ts
1613
+ var import_node_crypto2 = require("crypto");
1613
1614
  var fs6 = __toESM(require("fs"), 1);
1614
1615
  var path5 = __toESM(require("path"), 1);
1615
1616
 
@@ -1794,12 +1795,18 @@ async function handleSync(deps = {}) {
1794
1795
  const run = readRun(cwd, deps.file);
1795
1796
  const doFetch = deps.fetchImpl ?? fetch;
1796
1797
  const endpoint = `${apiUrl}/v1/runs`;
1798
+ const payload = JSON.stringify(run);
1799
+ const idempotencyKey = (0, import_node_crypto2.createHash)("sha256").update(payload).digest("hex");
1797
1800
  let res;
1798
1801
  try {
1799
1802
  res = await doFetch(endpoint, {
1800
1803
  method: "POST",
1801
- headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
1802
- body: JSON.stringify(run)
1804
+ headers: {
1805
+ "content-type": "application/json",
1806
+ authorization: `Bearer ${token}`,
1807
+ "idempotency-key": idempotencyKey
1808
+ },
1809
+ body: payload
1803
1810
  });
1804
1811
  } catch {
1805
1812
  throw new SyncNetworkError(apiUrl);
package/dist/index.js CHANGED
@@ -1554,6 +1554,7 @@ async function handleRun(options, depsFactory) {
1554
1554
  }
1555
1555
 
1556
1556
  // src/cli/commands/sync.ts
1557
+ import { createHash as createHash2 } from "crypto";
1557
1558
  import * as fs6 from "fs";
1558
1559
  import * as path5 from "path";
1559
1560
 
@@ -1738,12 +1739,18 @@ async function handleSync(deps = {}) {
1738
1739
  const run = readRun(cwd, deps.file);
1739
1740
  const doFetch = deps.fetchImpl ?? fetch;
1740
1741
  const endpoint = `${apiUrl}/v1/runs`;
1742
+ const payload = JSON.stringify(run);
1743
+ const idempotencyKey = createHash2("sha256").update(payload).digest("hex");
1741
1744
  let res;
1742
1745
  try {
1743
1746
  res = await doFetch(endpoint, {
1744
1747
  method: "POST",
1745
- headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
1746
- body: JSON.stringify(run)
1748
+ headers: {
1749
+ "content-type": "application/json",
1750
+ authorization: `Bearer ${token}`,
1751
+ "idempotency-key": idempotencyKey
1752
+ },
1753
+ body: payload
1747
1754
  });
1748
1755
  } catch {
1749
1756
  throw new SyncNetworkError(apiUrl);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plune-ai/cli",
3
- "version": "0.23.0",
3
+ "version": "0.24.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",