@sdods/cli 0.8.0 → 0.10.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.
@@ -279,7 +279,7 @@ export function register(program) {
279
279
  if (opts.target === 'github') {
280
280
  const file = join(ctx.rootDir, '.github', 'workflows', `scheduled-${e.project}-${e.name}.yml`);
281
281
  mkdirSync(join(ctx.rootDir, '.github', 'workflows'), { recursive: true });
282
- writeFileSync(file, `name: scheduled ${e.project} ${e.name}\non:\n schedule:\n - cron: '${e.cron}'\n workflow_dispatch: {}\njobs:\n run:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: oven-sh/setup-bun@v2\n - uses: actions/setup-node@v4\n with: { node-version: 22 }\n - run: bun install\n - run: bun run sdods browsers install --with-deps\n - run: bun run sdods run ${e.args.join(' ')} --trigger schedule --run-id sched-${e.name}-\${{ github.run_id }}\n env:\n SDODS_SERVER_URL: \${{ secrets.SDODS_SERVER_URL }}\n SDODS_TOKEN: \${{ secrets.SDODS_TOKEN }}\n - uses: actions/upload-artifact@v4\n if: always()\n with: { name: sdods-${e.project}-${e.name}, path: .sdods/runs }\n`);
282
+ writeFileSync(file, `name: scheduled ${e.project} ${e.name}\non:\n schedule:\n - cron: '${e.cron}'\n workflow_dispatch: {}\njobs:\n run:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v7\n - uses: oven-sh/setup-bun@v2\n - uses: actions/setup-node@v7\n with: { node-version: 22 }\n - run: bun install\n - run: bun run sdods browsers install --with-deps\n - run: bun run sdods run ${e.args.join(' ')} --trigger schedule --run-id sched-${e.name}-\${{ github.run_id }}\n env:\n SDODS_SERVER_URL: \${{ secrets.SDODS_SERVER_URL }}\n SDODS_TOKEN: \${{ secrets.SDODS_TOKEN }}\n - uses: actions/upload-artifact@v7\n if: always()\n with: { name: sdods-${e.project}-${e.name}, path: .sdods/runs }\n`);
283
283
  written.push(file);
284
284
  }
285
285
  else if (opts.target === 'launchd') {
@@ -56,8 +56,8 @@ export function register(program) {
56
56
  .option('--list', 'list trace files instead of opening them')
57
57
  .action(async (zip, opts, cmd) => {
58
58
  const ctx = createContext(cmd);
59
- let zips = [];
60
- let source = '';
59
+ let zips;
60
+ let source;
61
61
  if (zip) {
62
62
  const p = resolvePath(ctx.opts.cwd ?? process.cwd(), zip);
63
63
  if (!existsSync(p))
@@ -0,0 +1,11 @@
1
+ import type { GateResult } from '@sdods/contracts';
2
+ import { SdodsError } from '@sdods/core';
3
+ /**
4
+ * How a process gate verdict reads on the terminal, and the error a breach fails with. Shared by
5
+ * `sdods run --process` (an unsharded run) and `sdods report merge --process` (the shards of one),
6
+ * so a gate looks the same wherever it was judged.
7
+ */
8
+ export declare function printGates(gates: GateResult): void;
9
+ /** The `GATE_FAILED` error for a verdict that did not pass. Exit 1: a gate not met is a failure. */
10
+ export declare function gateFailedError(gates: GateResult, verdictFile: string): SdodsError;
11
+ //# sourceMappingURL=gates.d.ts.map
package/dist/gates.js ADDED
@@ -0,0 +1,29 @@
1
+ import pc from 'picocolors';
2
+ import { SdodsError } from '@sdods/core';
3
+ import { out, table } from './ui.js';
4
+ /**
5
+ * How a process gate verdict reads on the terminal, and the error a breach fails with. Shared by
6
+ * `sdods run --process` (an unsharded run) and `sdods report merge --process` (the shards of one),
7
+ * so a gate looks the same wherever it was judged.
8
+ */
9
+ export function printGates(gates) {
10
+ out('');
11
+ out(pc.bold(`gates · process ${gates.process}`));
12
+ table(gates.rows.map((r) => ({
13
+ gate: r.gate,
14
+ threshold: r.threshold,
15
+ actual: r.actual,
16
+ result: r.passed ? 'pass' : 'FAIL',
17
+ detail: r.detail ?? '',
18
+ })));
19
+ }
20
+ /** The `GATE_FAILED` error for a verdict that did not pass. Exit 1: a gate not met is a failure. */
21
+ export function gateFailedError(gates, verdictFile) {
22
+ const failed = gates.rows.filter((r) => !r.passed);
23
+ return new SdodsError('GATE_FAILED', `Process "${gates.process}" did not meet ${failed.length} gate(s): ${failed.map((r) => `${r.gate} (${r.actual}, needs ${r.threshold})`).join('; ')}.`, {
24
+ hint: `The verdict is in ${verdictFile}. Gates are set under processes[].gates in sdods.project.yaml.`,
25
+ docsPath: '/docs/guides/processes-and-testing-types',
26
+ exitCode: 1,
27
+ });
28
+ }
29
+ //# sourceMappingURL=gates.js.map
package/dist/program.js CHANGED
@@ -24,6 +24,7 @@ import { register as registerCoverage } from './commands/coverage.js';
24
24
  import { register as registerBrowsers } from './commands/browsers.js';
25
25
  import { register as registerWatch } from './commands/watch.js';
26
26
  import { register as registerTrace } from './commands/trace.js';
27
+ import { register as registerBaselines } from './commands/baselines.js';
27
28
  import { register as registerAgent } from './commands/agent.js';
28
29
  import { register as registerProposals } from './commands/proposals.js';
29
30
  import { register as registerMcp } from './commands/mcp.js';
@@ -80,6 +81,7 @@ export function buildProgram() {
80
81
  registerBrowsers(program);
81
82
  registerWatch(program);
82
83
  registerTrace(program);
84
+ registerBaselines(program);
83
85
  registerAgent(program);
84
86
  registerProposals(program);
85
87
  registerMcp(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdods/cli",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "SDODS command line: run, record, analyze, agents, MCP server, scheduler and the web server.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "SDODS <admin@sdods.com>",
@@ -24,22 +24,22 @@
24
24
  "node": ">=22"
25
25
  },
26
26
  "dependencies": {
27
- "@sdods/contracts": "0.8.0",
28
- "@sdods/core": "0.8.0",
27
+ "@sdods/contracts": "0.10.0",
28
+ "@sdods/core": "0.10.0",
29
29
  "commander": "^15.0.0",
30
30
  "execa": "^10.0.1",
31
31
  "picocolors": "^1.1.1",
32
32
  "semver": "^7.8.5",
33
- "yaml": "^2.9.0",
34
- "zod": "^4.5.4",
33
+ "yaml": "^2.9.1",
34
+ "zod": "^4.6.5",
35
35
  "@playwright/test": "^1.63.0",
36
36
  "playwright-core": "^1.63.0",
37
- "@sdods/integrations": "0.8.0",
38
- "@sdods/mcp": "0.8.0",
39
- "@sdods/agents": "0.8.0",
40
- "@sdods/db": "0.8.0",
37
+ "@sdods/integrations": "0.10.0",
38
+ "@sdods/mcp": "0.10.0",
39
+ "@sdods/agents": "0.10.0",
40
+ "@sdods/db": "0.10.0",
41
41
  "csv-parse": "^7.0.2",
42
- "@sdods/server": "0.8.0"
42
+ "@sdods/server": "0.10.0"
43
43
  },
44
44
  "homepage": "https://sdods.com",
45
45
  "bugs": {
@@ -161,11 +161,12 @@ Defaults: **`http://127.0.0.1:4444`** for the CLI, **`http://localhost:8080`** f
161
161
  image binds `0.0.0.0:8080`). `--open` launches the browser (`open` / `start` / `xdg-open` by
162
162
  platform). `--host 0.0.0.0` exposes it on the LAN — only do that deliberately.
163
163
 
164
- On Apple silicon the published image is `linux/amd64` only, and Docker refuses the pull rather
165
- than emulating. Add the platform flag:
164
+ The image is published for `linux/amd64` and `linux/arm64`, so the same command runs natively on
165
+ Apple silicon and arm64 servers. Tags up to `0.9.0` carry amd64 only, and Docker refuses that pull
166
+ on arm64 rather than emulating; for those, add the platform flag:
166
167
 
167
168
  ```bash
168
- docker run --rm --platform linux/amd64 -p 8080:8080 -v sdods-data:/data ghcr.io/siri1410/sdods-server
169
+ docker run --rm --platform linux/amd64 -p 8080:8080 -v sdods-data:/data ghcr.io/siri1410/sdods-server:0.9.0
169
170
  ```
170
171
 
171
172
  If the run must come from a source checkout, `bun run web:build` is not optional — see
@@ -278,7 +279,7 @@ the API running **without** the UI bundle — that is the source-checkout case,
278
279
  | Fresh install, but `/login` says "ask an admin" | the login page does not redirect to `/setup` | open the `/setup?token=…` URL from the `serve` output, or `sdods users create --admin` |
279
280
  | Global CLI errors on a repo project (`CONFIG_INVALID`, unknown browser) | version skew between the global CLI and the checkout | use `bun run sdods` inside a checkout; upgrade the global one otherwise |
280
281
  | `sdods: command not found` after installing | the bin dir is not on PATH — `~/.local/bin`, or `%LOCALAPPDATA%\SDODS\bin` on Windows | add it, or re-run the installer with `--modify-path` / `-ModifyPath` |
281
- | Docker: `no matching manifest for linux/arm64/v8` | amd64-only image on Apple silicon | add `--platform linux/amd64` |
282
+ | Docker: `no matching manifest for linux/arm64/v8` | an amd64-only tag (`0.9.0` or earlier) on Apple silicon or arm64 | pull a newer tag, or add `--platform linux/amd64` |
282
283
  | UI loads, runs never start | browser engines missing | `sdods browsers install -b chromium` (the positional `sdods browsers install chromium` also works on current versions, and failed with `too many arguments` before). Bare `sdods browsers install` installs all engines; on Linux `--with-deps` needs root |
283
284
  | Node too old | SDODS needs Node 22+ | `nvm use 22` / `fnm use 22`. On macOS/Linux the installer also takes `--install-node`; **`install.ps1` has no such flag** — it exits and tells you to run `winget install OpenJS.NodeJS.LTS`, `scoop install nodejs-lts` or `nvm install 22` |
284
285
  | Anything else | — | `sdods doctor` first; it reports what it found, not what it expected |
@@ -1,6 +1,6 @@
1
1
  @ui @auth @pool
2
2
  Feature: User pool
3
- Accounts are leased per worker from the pool dataset; login state is cached as storageState.
3
+ Accounts are leased per scenario from the pool dataset; login state is cached as storageState.
4
4
 
5
5
  @sanity @user:standard
6
6
  Scenario: A leased standard user starts logged in through cached storage state
@@ -0,0 +1,14 @@
1
+ @ui @cart @perf
2
+ Feature: Cart performance
3
+ The @perf tag records the vitals of every document the scenario loads and judges them against
4
+ perf.budgets (pageLoadMs and lcpMs from the project, pageLoadMs overridden in envs/staging.yaml).
5
+
6
+ # WebKit does not implement largest-contentful-paint, and a vital a budget applies to that never
7
+ # fired fails the scenario rather than passing it, so this runs on Chromium and Firefox only.
8
+ @regression @skip:webkit
9
+ Scenario: The login and cart pages load within their budgets
10
+ Given I am on the login page
11
+ When I login with "standard_user" and "{{standardPassword}}"
12
+ And I add "{{defaultProduct}}" to the cart
13
+ And I navigate to the "cart" page
14
+ Then the cart should list "{{defaultProduct}}"
@@ -0,0 +1,12 @@
1
+ @ui @inventory @a11y
2
+ Feature: Inventory accessibility
3
+ The @a11y tag audits the page each scenario ends on with axe (WCAG 2.x A and AA) and fails on
4
+ anything at or above a11y.failOn (serious by default). SauceDemo's login, inventory and cart
5
+ pages have no WCAG A/AA violation axe can detect, so nothing is scoped out with include/exclude.
6
+
7
+ @regression
8
+ Scenario: The inventory page has no serious accessibility violations
9
+ Given I am on the login page
10
+ When I login with "standard_user" and "{{standardPassword}}"
11
+ Then I should be on the inventory page
12
+ And there should be 6 products listed
@@ -1,6 +1,7 @@
1
1
  @ui @inventory @visual
2
2
  Feature: Inventory visual baseline
3
- Pixel comparison against a per-browser baseline (run once with --update-snapshots).
3
+ Pixel comparison against a per-browser, per-platform baseline. Review a failure with
4
+ `sdods baselines diff` and accept it with `sdods baselines accept inventory --run <id>`.
4
5
 
5
6
  @regression
6
7
  Scenario: Inventory page matches its baseline
@@ -101,6 +101,9 @@ data:
101
101
  roleColumn: role
102
102
  leaseStore: file
103
103
  leaseTtlMs: 600000
104
+ # One account per role after poolSize, and `release:check:live` runs three browsers in one
105
+ # invocation: release each account when its scenario ends so the browsers queue for it (#153).
106
+ leaseScope: scenario
104
107
 
105
108
  auth:
106
109
  strategy: form