Release notes
What changed in each SDODS version, generated from the changesets of the @sdods/* packages.
Every @sdods/* package is released together under one version number and published to npm.
Upgrade with sdods upgrade (or npm i -g @sdods/cli@latest); see the CLI reference.
0.20.0
- GitHub Copilot support:
--adapter copilotruns the SDODS agent roles through a logged-in Copilot CLI (copilot -p --output-format json, sdods MCP server attached, shell, file writes, web and the proposal review tools denied);sdods mcp install copilotregisters the server in~/.copilot/mcp-config.json;sdods agent install --for copilotwrites.github/agents/sdods-<role>.agent.md;sdods doctorshows acli:copilotrow; the installer takes--mcp copilot.
0.19.0
heal.ciMode: warn | fail | offdecides what self-healing does whenCIis set. The default,warn, heals and then lists every heal as a GitHub Actions annotation and in the job summary;failfails the step withHEAL_BLOCKED_IN_CIand names the locator that would have healed it;offdisables healing in CI. Local runs heal as before.sdods import cucumber <dir> -p <slug>maps a Cucumber-JS suite into an SDODS project as a proposal: features tagged with a layer and suite, library steps reused, the rest stubbed to port, and an import report.sdods import playwright <dir>analyses a Playwright suite. The source suite is read, never executed.--reporter junit|allure(andreports.junit/reports.allure) now work:allure-playwrightis a dependency of@sdods/core, so Allure results are written instead of the run failing to load the reporter.- Secrets stay out of agent prompts and tool results: agents' own file tools may not read dotenv files, logins, datasets or run artifacts; API response snapshots are redacted by key; URL passwords and more secret-looking keys are masked in
config showandproject_get_config;data_previewmasks more columns.
0.18.0
-
Security fixes from the 2026-10-26 audit (
docs/audits/SDODS_GAP_ANALYSIS.md):- Run ids. A run id is refused unless it names one directory: letters, digits,
.,_and-, not starting with.. This applies to MCPrun_tests,run_tests_asyncandrun_get, tosdods run --run-id, and to config resolution.run_tests {runId: "../.."}used to delete the workspace when the run finished.isRunIdis now exported from@sdods/contracts. - Ingest. Ingest reads attachment files only from inside the run directory, and an uploaded
sdods/metaattachment can no longer move where files are written. A trusted caller whose attachments live elsewhere passes the newattachmentRootsoption toingestRun. - MCP.
feature_writestages files only inside the project.- Project slugs and proposal ids can no longer walk out of their directories.
analyze_change_impactrefuses a range that starts with-.
- Server.
- Agent jobs and proposals are listed, shown, streamed, cancelled, accepted and rejected only for projects where the caller has the editor role.
GET /api/agents/jobshonours?project=.audit:readis platform-admin only, so existing non-admin tokens that listed it now get 403 on/api/audit.
- Run ids. A run id is refused unless it names one directory: letters, digits,
0.17.0
sdods servebound to loopback (the default) now answers only tolocalhost,127.0.0.1and::1Host headers, which blocks DNS-rebinding pages. Behind a reverse proxy on the same machine that forwards the public Host, setSDODS_ALLOWED_HOSTS(comma list,*disables the check). Public binds such asHOST=0.0.0.0keep accepting any host.sdods mcp --httpapplies the same rule to its own listener.
0.16.0
- The web UI uses the SDODS brand: IBM Plex Sans and Mono (bundled, so every machine renders the same type), the rail blue on ink and paper in place of the indigo and teal defaults, and readable brand-coloured links in dark mode (7.4:1, up from 3:1). Borders in dark mode follow the theme instead of drawing bright lines between rows.
0.15.0
- The web UI uses the SDODS brand: IBM Plex Sans and Mono (bundled, so every machine renders the same type), the rail blue on ink and paper in place of the indigo and teal defaults, and readable brand-coloured links in dark mode (7.4:1, up from 3:1). Borders in dark mode follow the theme instead of drawing bright lines between rows.
0.14.1
sdods doctorlists only credentials a user can act on. Theci-onlylevel and theNPM_TOKENrow are gone, and the workspace.env.exampleno longer lists the SDODS repository's own CI secrets (they are in CONTRIBUTING.md).
0.14.0
@sdods/core/stepsexportsrawResponseOf(apiContext)(and theRawSnapshottype): the last raw, redirect-preserving response sent in the scenario, orundefined. Project steps can assert on the snapshot the core raw-request steps captured instead of sending their own request.
0.13.2
sdods feedbackopens the sdods.com feedback form instead of a prefilled GitHub issue, which 404s now that the source repository is private.--title,--messageand, with--bug, the version, OS and doctor summary are printed for you to paste into the form;--jsonreturns them asdetails.--discusspoints at sdods.com/questions.
0.13.1
-
Playwright 1.64. The browser tools gain
browser_emulate_media(color scheme, reduced motion, forced colors, contrast, media type) and the new upstream arguments:browser_findmaxResultsandfilename,browser_tabsisolatedContext,browser_start_videofpsandcursor, andbrowser_video_show_actionsstyle. Tag expressions use@cucumber/tag-expressionsin place of the deprecatedcucumber-tag-expressions, so installs no longer print a deprecation warning. The CI image, the installers and the Docker images pin Bun 1.4.3. -
Browser tools that save a file (
browser_snapshot,browser_take_screenshot,browser_pdf_save,browser_console_messages,browser_network_requests,browser_network_request,browser_evaluate,browser_start_video, andbrowser_findfrom Playwright 1.64) now write only into the session's output directory. A relativefilenameused to resolve against the repository root, so a call could overwrite files in the working tree; it now resolves inside the output directory, and a path outside it fails withBROWSER_PATH_OUTSIDE.
0.13.0
Features
sdods runhas its own console output. Each scenario is one line,[n/total] ✔ Feature › Scenario [ui · chromium] (1.2s), in place of the runner's generated spec paths and hook lines. Failures show the feature file, the error (without generated-spec code or browser launch logs), the failure screenshot, the video andsdods trace <zip>.run --listlists scenarios by title. New--open always|on-failure|never(orSDODS_OPEN) opens the SDODS dashboard after a run; by default only on failure at an interactive terminal, never in CI or under a coding agent. The web UI puts the SDODS dashboard first and labels the HTML report and trace viewer as Playwright's.
Fixes
sdods runno longer ends with the runner's "To open last HTML report run: npx playwright show-report" hint. It prints the SDODS command to open the results instead, andsdods trace --run <id>after a failure. Every runner and generator invocation (run,watch,lint,steps list,record,auth capture,trace,show-report,report merge,browsers install) now resolves the workspace's own package instead of a barenpx, which could prompt to download a package or fetch an unrelated one.watch,recordandauth capturecheck for missing browsers up front and namesdods browsers install;doctor --fixandinitinstall browsers through the same path. GitHub failure issues namesdods trace <zip>.show-reportaccepts--last.sdods --helpcredits Playwright and playwright-bdd.
0.12.1
sdods runandsdods watchrun bddgen through Node from the workspace's installedplaywright-bddinstead ofnpx bddgen. Where npx finds nonode_modules/.binshim it recognises (bun installs on Windows), it downloaded the unrelated registry packagebddgen@1.0.5and every run failed with "bddgen failed to generate specs".
0.12.0
-
Runs no longer fail with Playwright's "Executable doesn't exist … run npx playwright install" when a test browser is missing or half-downloaded.
sdods runchecks, before generating specs, that every browser its targets launch (chromium for@apitargets too) finished downloading for the exact revision the workspace's Playwright uses. With--install-browsersorSDODS_AUTO_INSTALL_BROWSERS=1(set by the desktop app) it downloads what is missing into the cache the runner reads; otherwise it stops with thesdods browsers installcommand.sdods browsers listandsdods doctoruse the same check.Stopping a run from the UI or API now ends the whole process tree (the CLI, Playwright, its workers and browsers) on Windows, macOS and Linux, and requires the editor role, like starting one. New
POST /api/runs/:id/rerun({ scope: 'all' | 'failed', scenarios? }) replays a run's full selection, only its failed scenarios, or named ones; runs now store the selection they were started with (runs.params_json). NewGET/PUT /api/me/preferences/:keystore per-user UI preferences (user_preferencestable), used to remember the last run selection per workspace and project.
0.11.3
-
Fix
sdodsfailing to start on Windows withERR_UNSUPPORTED_ESM_URL_SCHEME: the bin shim now imports its entry point as afile://URL. This also unblocks the desktop app's first-run install on Windows.sdods initnow adds@axe-core/playwrightto the workspace, so the demo project's@a11yscenario passes on a fresh install instead of failing with "Accessibility audits need @axe-core/playwright".
0.11.2
-
Fixes found by using every web UI feature against a real server.
- Workspaces created in the web UI (or with
POST /api/workspaces) are written tosdods.workspace.yamlbefore the database, so projects can be created in and imported into them. Creating a workspace that is already declared returns 409. - Project Settings saves again:
PUT /api/projects/:slugaccepts{ patch }, the top-level keys that changed, applied to the yaml in place so comments and keys the form does not show are kept. The form now sends only what changed. - Dataset upload: the preview uses
POST .../datasetswithpreview=1(the page called a route that does not exist), and the name, environment and storage fields are sent before the file so the server reads them. A file dataset is registered underdata.sources, so it is listed and usable as@data:<name>. - Creating a user with the email field left empty no longer fails with "Invalid email address".
- The web app's mock dev server (
bun run web:dev) starts again: browser code imports@sdods/contracts/schemasinstead of the root export, which pulled innode:crypto. - Docs: account menu, Settings → Profile and Password & sessions, the account REST routes, and refreshed web UI screenshots.
- Workspaces created in the web UI (or with
0.11.1
-
Repository metadata moved to the new GitHub organisation (reachable as https://sdods.com/github):
repository.urlandbugs.urlpoint there,sdods feedbackopens issues there, and the documented server image moved with it. -
Fixes found by using every web UI feature against a real server.
- Workspaces created in the web UI (or with
POST /api/workspaces) are written tosdods.workspace.yamlbefore the database, so projects can be created in and imported into them. Creating a workspace that is already declared returns 409. - Project Settings saves again:
PUT /api/projects/:slugaccepts{ patch }, the top-level keys that changed, applied to the yaml in place so comments and keys the form does not show are kept. The form now sends only what changed. - Dataset upload: the preview uses
POST .../datasetswithpreview=1(the page called a route that does not exist), and the name, environment and storage fields are sent before the file so the server reads them. A file dataset is registered underdata.sources, so it is listed and usable as@data:<name>. - Creating a user with the email field left empty no longer fails with "Invalid email address".
- The web app's mock dev server (
bun run web:dev) starts again: browser code imports@sdods/contracts/schemasinstead of the root export, which pulled innode:crypto. - Docs: account menu, Settings → Profile and Password & sessions, the account REST routes, and refreshed web UI screenshots.
- Workspaces created in the web UI (or with
0.11.0
-
Account menu, profile and sessions in the web UI.
- The sidebar footer is now an account menu: profile, password and sessions, preferences, API tokens, MCP clients, admin pages, theme (system, light, dark), feedback and sign out.
- Settings gains Profile (display name, email, role, memberships), Password & sessions (change password with the current one, list and sign out other sessions) and Preferences (theme, default organization, workspace and project).
@sdods/db: migration0009_user_profileaddsusers.display_name;listSessionsForUser,deleteSessionById, anddeleteSessionsForUser(userId, { exceptToken }).@sdods/server:PATCH /api/me,POST /api/me/password,GET /api/me/sessions,DELETE /api/me/sessions/:id,POST /api/me/sessions/revoke-others(browser sessions only, never API tokens)./api/auth/mereturns email, display name, last login, creation date and the current session id.PATCH /api/users/:idacceptsdisplayName.- Web pages that called routes the server does not have now use the real ones: Recorder (job events, recorded spec via
GET /api/projects/:slug/recorded/:file, convert withkind: convert), Environments (create and edit throughPUT .../envs/:name, which now edits the file in place instead of rewriting it), Schedules (save, pause/resume, history), Integrations (PUT .../integrationsvalidates before writing and merges MCP servers by name) and the Agents live log. sdods users set-rolerejects an unknown role instead of reporting success without changing anything.
0.10.0
Features
-
Process gates judge sharded runs, CLI-ingested runs record their verdict, and
@a11ycan audit every page (#176, #177).sdods report merge --process <name> [-p <slug>]judges a process's gates on the merged shards: the pass rate and flaky count from the merged results, the@a11y/@perfevidence from the shards' run directories and the reports attached in the blobs. It prints the same gate table assdods run --process, writesgates.jsoninto the--rundirectory and exits 1 withGATE_FAILED. Without--process, a merge now also judges the process a shard'srun.jsonnames when that process declares gates.sdods report mergemerges several shard report directories, including blobs recorded under different checkouts (CI's Playwright container and its bare runner).playwright merge-reportsreads one directory and refuses mixed test roots, so the reports are staged into one directory with a merge config that pins the root. Before this, CI's merge step failed with "too many arguments" and its|| echohid it. A failed merge now shows Playwright's error rather than the tail of its stack.- Ingest records the process gate verdict under
totals_json.gatesand marks a gated-out run failed, for CLI, merged and server-started runs alike (gates.jsonin the run directory, or judged duringsdods report ingestand server ingest when the run's process has gates).RunRecord.gatescarries it, and the run page in the web UI shows the gate table. a11y.scope: final | every-page(defaultfinal), overridable per environment. Withevery-page, the@a11yhook audits each distinct URL a step leaves the scenario on, after the page settles at the point the@perfhook reads it, skips URLs an explicit audit step covered, and fails once at the end naming each violating URL.sdods/a11y-scenariocarries apageslist, and thea11ygate counts one audit per URL.
Fixes
-
Dependencies moved to their latest compatible releases: zod 4.6, yaml 2.9.1, fastify 5.12.4,
@axe-core/playwright4.13 (axe-core 4.13 rules),@anthropic-ai/sdk0.125,@anthropic-ai/claude-agent-sdk0.3.272, playwright-bdd 9.2.1 and tar 7.5.22. The installer now pins Bun 1.4.2.Tooling moves to TypeScript 6.0 and ESLint 10, and
fastify-pluginto 6. Errors re-thrown from a caught failure (JSON bodies that do not parse after templating, GitHub evidence updates, an OpenAI-compatible server that does not answer) now carry the original error ascause.sdods initscaffolds projects on TypeScript 6.0, playwright-bdd 9.2.1 and tsx 4.23.13, and the workflowsdods schedulegenerates usesactions/checkout@v7,actions/setup-node@v7andactions/upload-artifact@v7.
0.9.0
-
@a11yand@perfnow run, and process gates are evaluated.@a11y: at the end of a UI or hybrid scenario, the page it ended on is audited with axe-core (WCAG 2.x A and AA). The scenario fails on any violation at or above the newa11y.failOnsetting (seriousby default).a11y.includeanda11y.excludescope the audit, and an environment can override each key. The result is attached assdods/a11y-scenario. A URL that an explicit whole-page audit step already checked is not audited again.@perf: every document the scenario loads records its vitals once it has loaded and painted. At the end of the scenario, each recording is compared with the page budgets inperf.budgets, and the p95 of the scenario's live API calls (plus any latency samples) is compared withapiP95Ms. The scenario fails in four cases: a budget is breached, a budgeted vital is unmeasured, no budgets are configured (CONFIG_INVALID), or no budget applies to anything the scenario measured. The result is attached assdods/perf-scenario.sdods run --process <name>evaluatesgates.minPassRate,gates.maxFlaky,gates.a11yandgates.perfBudgetsafter the run and prints a gate table. When a gate fails, it exits 1 with the new error codeGATE_FAILED. The a11y and perf gates also fail when nothing was audited or judged. The verdict is written togates.json,summary.jsonand the--jsonoutput. The server's run manager records it on the run undertotals_json.gatesand marks a gated-out run failed. Gates are not evaluated on a single shard.- The axe audit and the vitals and budget helpers moved out of the step files into
a11y/audit.tsandperf/vitals.ts, so the hooks can use them without registering the steps a second time. The step files still re-export them.
-
User pools no longer starve workers silently (#153).
sdods runstops before generating specs when a selected@user:<role>has fewer pool accounts than the scenarios of that role the workers could run at once. It exits2withUSER_POOL_TOO_SMALLand names the role, the account count and the worker count. The count follows the selection (--layer,--browser,--tags,--module,--feature,--since, tag-gate skips, serial files, shards) and is skipped formode: shared,leaseScope: scenario, database or OpenAPI datasets,--list,--uiand--debug.--allow-pool-contentionruns anyway.sdods doctorprints accounts per role against the worker count (-wto set it).data.userPool.leaseScope: scenario | worker(defaultworker, unchanged). Withscenarioan account is released when each scenario ends, including accounts leased by theI use a leased user …steps, so one account serialises its scenarios instead of starving whole workers. In that modewaitMsdefaults toleaseTtlMs, and the time spent waiting is added to the scenario's timeout.- Waiters for a role are served in arrival order, so a worker that just released an account no longer wins it straight back.
- Removed:
data.userPool.leaseStore: db. It was accepted and never implemented, so sharded runs that relied on it did not share leases. It is now a configuration error ("leaseStore 'db' was never implemented; use 'file' (see #153)"); usefile, or give each runner its own accounts. data.userPool.waitMsis now optional in the schema (its effective default is unchanged forleaseScope: worker).- Fix: the shard offset in lease owners used
--workers ?? 1, so with Playwright's default worker count a worker on shard 2 could share an owner with one on shard 1.
-
Visual baselines you review before they change, per-baseline masks, and Linux baselines from CI.
screenshots.maxDiffPixelRatio(default0.01, previously hard-coded) andscreenshots.baselines.<name>with its ownmask(added toscreenshots.mask) andmaxDiffPixelRatio, in the project or an environment.- New step
the page should match the visual baseline {string} masking {string}with comma-separated selectors. - A failed visual check keeps its expected, actual and diff images and a
<name>.failure.jsonrecord under the scenario'svisual/<run target>/directory, so they survive the CI artifact upload. sdods baselines diff [--run <id>]lists failed visual checks;sdods baselines accept <name…> --run <id>(or--all) copies the actual screenshot tofeatures/__screenshots__/<run target>/<platform>/for the platform the run happened on.sdods run --update-snapshotsstill works and now warns that it overwrites without review.- Server:
GET /api/runs/:id/baselinesandPOST /api/runs/:id/baselines/accept(features:write, editor role); the web UI shows Accept as baseline with the diff on a failed visual step.
0.8.0
Features
-
GitHub issues can embed evidence that renders inline in private repositories (#82). With
integrations.github.evidence.host: branch(off by default), screenshots,video.webmand a GIF preview (when ffmpeg is on PATH) go to an orphansdods-evidencebranch through the git data API, in one fast-forward commit per run that is retried when another job moved the branch first, and are embedded asblob/<branch>/<path>?raw=true. The evidence can go to a separate repository with its own token (evidence.repo,evidence.tokenEnv). Per-file and per-run size caps apply (maxFileBytes5 MB,maxRunBytes25 MB), and files over them are listed in the issue.sdods integrations testchecks that the token can push to the evidence repository, andsdods integrations evidence prune [--older-than 14d]rewrites the branch without old runs. Traces are never uploaded. -
sdods load -p <project> -e <env> <profile>runs API load tests through k6. A profile inprojects/<slug>/load/<profile>.yamllists requests (method, path relative toapi.baseUrl, headers, body, expected status and body checks) with k6stagesorvus/durationandthresholds. SDODS generates a k6 script whose credentials come from the environment'sapi.authas__ENVlookups, never inlined, prints the target URL and peak virtual users, runsk6 run --summary-export(or thegrafana/k6image with--runner docker) and exits1when thresholds fail. Load is opt-in: an environment must setload.allowed: true,load.maxVuscaps the peak, and write methods needload.allowWrites: true.--dry-runwrites the script without k6. Results land in.sdods/runs/<id>/load/<profile>/. -
Role matrices (#118): declare an actor × surface grid once in
projects/<slug>/roles.matrix.yamland generate the Scenario Outline examples from it.@sdods/contracts:RolesMatrixFileSchemaandROLES_MATRIX_FILE. A matrix lists itsroles, adefaultoutcome, optionaloutcomesandtitle, androwswhose extra keys are Examples columns;expectis one outcome or a per-role map.@sdods/core:loadRolesMatrices,expandFeatureTextandplanMatrixExpansion. AScenario Outlinetagged@matrix:<name>gets oneExamples:block per role, tagged@user:<role>, between# sdods:matrix:begin/endmarkers; re-running is idempotent and hand-written Examples are kept.sdods lintaccepts@matrix:<name>and reports an unknown matrix (tags/matrix), an invalid matrix file or undeclared role, a misplaced template or unknown placeholder, an out-of-date feature (matrix/stale, warning), and a matrix role with no user-pool account in an environment (matrix/unseeded-role, warning).@sdods/cli:sdods matrix expand [-p <slug>] [--check] [--dry-run];--checkexits 3 when a feature is out of date, for CI.@sdods/mcp:matrix_expandstages the expanded features as a proposal instead of writing them. The conventions list@matrix:<name>among the value tags.
-
Requirement traceability (#119). A new value tag
@req:<id>links a scenario to a requirement; it may repeat, and on aFeatureorRuleit applies to every scenario under it. The id is opaque. An optionaltraceability:block insdods.project.yaml(requirements: a YAML or CSV list of ids and titles,link: a URL template with{id},require: boolean) makes lint reject ids missing from the list (tags/req) and, withrequire: true, scenarios without a@req:tag (tags/req-missing).sdods report traceability -p <slug> [-e <env>] [--run <id> | --last] [--format json|csv|md|html] [-o <file>]exports requirement → scenarios (feature, line, name, tags) → final result per runner project in the chosen run (status, browser, attempts, flaky, duration, timestamps), with the run's id, environment, commit and SDODS version, a coverage summary (passed / failed / not run / not covered) and an empty sign-off block that SDODS never fills in. It readsmessages.ndjsonfrom the run directory, falling back to per-scenariometa.json; no database or server is needed.@sdods/core/analyzeexportsbuildTraceabilityReportandrenderTraceability.The agent conventions (
@sdods/mcpCONVENTIONS) list@req:<id>among the optional value tags. -
Canvas steps for graph and node editors (
canvas.steps.ts): drag an element onto another or by an offset, drag a node by id, connect a handle of one node to a handle of another, select a node, set a field inside a node, and assert the node count, an edge from one node to another (or its absence), a node's visibility and a badge inside a node.- Drags are real pointer gestures (press, nudge, multi-step moves, a final move onto the target, release), so React Flow, d3-drag and native HTML5
draggablepalettes all react. The press lands on a plain part of the element, never on a field or handle inside it. - New project key
canvasin@sdods/contracts: CSS selector templates forroot,nodes,node,handle,edge,field,badgeandselected, defaulting to React Flow's DOM. Templates are validated for their required placeholders ({id},{handle},{source}/{target},{field},{badge}), and{testIdAttribute}expands to the project's attribute. - A project whose own steps already use these phrasings can keep them with
steps.core.exclude: [canvas].
- Drags are real pointer gestures (press, nudge, multi-step moves, a final move onto the target, release), so React Flow, d3-drag and native HTML5
-
Email assertions over Mailpit. A new
mailblock in the environment yaml (provider: mailpit,url, optional basicauthwith a${VAR}password) enables seven steps: clear the inbox for an address, wait for an email with a subject within N seconds, assert the latest email's text or sender, save a link or a one-time code from it, and open its link. "The latest email" is scoped to the scenario: only mail received after the scenario started or after the last clear counts, and the wait step pins the message it matched.@sdods/corealso exports theMailInboxinterface andMailpitInboxadapter.
Fixes
-
sdods report merge <dirs...> --run <id>now writes into that run. The parentreportcommand also defines--run, and Commander handed the option to it, somergesilently fell back to the latest run (or failed with "No run to merge into"). -
Per-scenario browser emulation:
@locale:<bcp47>,@timezone:<IANA>,@theme:<light|dark|no-preference>,@viewport:<W>x<H>and@device:<name>set the browser context before it opens, over the environment'suse:block, on a Scenario, a Feature, a Rule or oneExamples:block.sdods lintvalidates the values. New steps:I use the locale {string},I use the timezone {string},I use the {string} color scheme,I use the viewport {int} by {int},I use the device {string},the page should reflow without horizontal scrolling,the page should have no untranslated keysandthe page should have no untranslated keys matching {string}.Fix:
mobile-chromeandmobile-safarirun targets ran in a 1280x720 window instead of the device's viewport, because the generated project setviewport: undefined, which Playwright reads as "use the default".The agent rule lines (
CONVENTIONSin@sdods/mcp, thesdodsskill in@sdods/cli) list the new tags. -
Add
@sdods/mcp/gherkin:parseGherkin,basicTagCheckandsimilaritywithout the CLI, file system or tool registry, for code that checks a feature it was handed. The root export is unchanged.
0.7.3
-
ApiContext.headersis now case-insensitive, as HTTP header names are. It is aHeaderMap(exported from@sdods/core), aMapwhoseset,get,hasanddeletelower-case the name, sodelete('x-api-key')removes a header set asX-API-Keyandget('cookie')finds one set asCookie. Names are stored lower-cased, and when a scenario sets the same header under two casings the lastsetwins. What goes on the wire is unchanged: the client already lower-cased names when building a request. Project workarounds that installed their own case-insensitive map over theapiContextfixture can be removed. -
A JSONPath (
$…) that matches nothing now reads as absent (undefined), the same as a dotted path, instead of[].the response JSON path {string} should existfails on a missing key andshould not existpasses, whatever the path syntax.I save the response JSON path {string} as {string},should have {int} items/at least {int} itemsandthe UI should show the text from JSON path {string}fail withRUN_FAILEDand a hint when the path matches nothing; a key that is present with the valuenullor[]still saves.getPathreturnsundefinedfor no match, the value for one match and an array for two or more, and the newmatchedPaththrows on no match.Behaviour change: a wildcard over an empty array (
$.items[*]on{ "items": [] }) is now absent rather than[], sothe response JSON path "$.items[*]" should have 0 itemsfails. Point the path at the array itself ($.items).
0.7.2
- Add
@sdods/mcp/gherkin:parseGherkin,basicTagCheckandsimilaritywithout the CLI, file system or tool registry, for code that checks a feature it was handed. The root export is unchanged.
0.7.1
- Sponsorship is behind one switch,
SPONSOR_ENABLEDin@sdods/contracts/sponsor, and it is off. While off,sdods --help, the web UI sidebar, the desktop menu and the installers no longer link to the sponsor page, and sdods.com serves its sponsor pages as not found.
0.7.0
Features
-
SDODS for AI coding tools.
sdods skills listandsdods skills install [--agent claude,agents,cursor,copilot,gemini] [--global]copy the bundled Agent Skills (sdods,sdods-run,sdods-record,sdods-start-ui) to where Claude Code, Codex, Cursor, Copilot and Gemini CLI read them;npx -y @sdods/cli skills installneeds no global install.sdods initinstalls the same set into.claude/skillsand.agents/skills, and no longer copies SDODS's own release skills.sdods mcp install geminiwrites.gemini/settings.json.- Every stdio snippet now launches
npx -y @sdods/cli mcp.npx sdods mcppointed at a package that does not exist, so clients configured frommcp install --file,agent install, the web UI or/api/mcp/infofailed with CONNECTION_CLOSED outside a checkout. sdods mcp install claude|codex --http-urlpasses the bearer token (${SDODS_TOKEN}/--bearer-token-env-var SDODS_TOKEN); before, both registered a server that answered 401.sdods mcp install windsurfwrites Windsurf's real config,~/.codeium/windsurf/mcp_config.json, withserverUrlfor remote servers.
Fixes
- SDODS can now be sponsored. sdods.com/sponsor takes one-time or monthly sponsorships through Stripe, in any amount, and companies can ask for an invoice or a bank transfer.
sdods --help, the installer's closing notes, the web UI sidebar and the desktop app's SDODS menu each link to that page.
0.6.0
sdods users reset --yesremoves every user with their sessions, API tokens and memberships, so the nextsdods serveoffers the one-time/setuplink again; projects, runs and schedules are kept.sdods users set-password <username> --password <pw> [--activate]sets a password from the terminal, signs the user out everywhere and can re-enable a deactivated account, so a locked-out sole admin no longer has to wipe the database. Both are audited. The docs gain a "Reinstall or reset" section for installed, clone, Docker and desktop setups.
0.5.2
-
Stop publishing Playwright traces, and redact the ones a run keeps (#101).
A
trace.ziprecords every request'sCookie/Authorizationheaders,Set-Cookieresponses, the context'sstorageState(cookies, localStorage, IndexedDB — where Firebase keeps its refresh token) and typed values, so for projects with pool accounts it carries live sessions.- Integrations never upload, attach or link a trace. GitHub issues no longer link
trace.zipthroughSDODS_PUBLIC_URLor the CI artifacts page, anduploadToReleaseno longer uploads it; Jira no longer attaches it. Both name the trace's local path under a warning that it contains credentials, with thenpx playwright show-tracecommand. The video is still linked or uploaded (it shows the screen, not headers or storage). sdods runredacts every trace after the run, before ingest and integrations:runner-output/**/trace.zip, the HTML report's copies underhtml-report/data/, zips nested in blob shard reports, and the BASE64 trace bodies embedded inmessages.ndjson(which ingest turns back intotrace.zip). Credential headers (Cookie,Set-Cookie,Authorization,Proxy-Authorization,X-Api-Keyand similar), cookie values, localStorage/sessionStorage/IndexedDB values,httpCredentialspasswords, password inputs in DOM snapshots and values typed into password/secret/token fields become[redacted]. Other entries are copied through unchanged and the trace viewer opens the result. Request/response bodies and page text are not redacted.- New
evidence.redactTraces(defaulttrue; env files may override it).evidence.traceandsdods run --trace <mode>already control whether traces are recorded at all. - Docs warn that traces are credential-bearing (GitHub and Jira guide,
sdods trace,project.yamlreference).
- Integrations never upload, attach or link a trace. GitHub issues no longer link
0.5.1
-
Make every credential an API call carries explicit, and wire two config fields that did nothing.
I use an isolated API client(orapiContext.isolated = true) sends the rest of the scenario's calls through a separate request context with an empty cookie jar, created on first use and disposed at teardown. On@ui/@hybridthe sharedrequestcontext starts from the@user:<role>storageState, so "an invalid API key is refused" passed on the session cookie. The raw (without following redirects) and event-stream steps honour it. The step library reference documents which layers carry which jar.- String bodies are sent as bytes. Playwright JSON-encodes a string that does not parse when the content-type is exactly
application/json, so{ this is not jsonarrived as"{ this is not json". New stepI send a {method} request to {string} with the raw body:sends its doc string untouched (not parsed, not template-rendered). I use a leased user with role {string} for API callsno longer attaches the user's token behind the scenario's back forcustomauth strategies, whosetoken()may mint a different credential class than the session. New project settingauth.apiToken: implicit | explicit(default: explicit forcustom, implicit for every other strategy, unchanged) and new stepI authenticate the API with the leased user's token. Which credential was attached, and from where, is logged atinfo. Behaviour change forcustomstrategies that relied on the implicit bearer: add the step, or setauth.apiToken: implicit.- Recorded API evidence carries
request.auth(none,bearer,basic,header:<name>, never the value) andrequest.isolated. env.api.auth: { type: oauth-client-credentials }is implemented: aclient_credentialsgrant totokenUrl(withscope/audiencewhen set), sent as a bearer and cached per worker until 30 s beforeexpires_in. A refused grant fails the call asAUTH_FAILEDinstead of sending it anonymous, and an auth type the client does not implement throwsNOT_SUPPORTED. Theoauth-client-credentialsauth strategy'stoken()shares the implementation. Raw requests now resolve auth exactly like the client, which also makesI use no authenticationapply to them (null ?? envused to fall back to the environment credential).retries.byTagreaches the runner. Each entry becomes a sibling runner project with the same name that greps the tag and carries its retries; the base project excludes those tags, and a scenario with several such tags runs once with the highest count.--retriesstill overrides it, and a scenario's own@retries:Ntag overrides both.
-
Applying a cached session no longer navigates the page (#99).
I use a leased user with role "…"restored localStorage bypage.goto(origin). An app that sends a signed-in visitor away from/on the client then had a redirect chain still running when the step returned, and the scenario's firstpage.gotofailed withnet::ERR_ABORTED/ "interrupted by another navigation". Cookies are still added to the context; localStorage is now planted by a context init script that runs before the app's own scripts, only on the matching origin (or written straight into the page when it is already on that origin). The page is left where it was.- The localStorage is planted once per context: the init script is removed after the first document of that origin loads, so an app that signs out mid-scenario is not silently signed back in on the next navigation.
-
I should not see the textchecks what the user can see, and the text steps get exact variants.I should not see the text {string}passes when no VISIBLE element contains the text. It used to assert that no element in the DOM contained it, so a closed FAQ answer (hidden) or a collapsed panel failed the step although nothing was on screen. It now filters on visibility (getByText(...).filter({ visible: true })+toHaveCount(0)), which is strict-mode safe and retries until the text goes away.I should see the text {string}looks past hidden matches: a hidden copy earlier in the DOM no longer fails the step when a visible copy is on the page.- New
I should see the exact text {string}andI should not see the exact text {string}match an element's whole text, so"already"no longer collides with "the clients you already serve". - Migration: new
the page should not contain the text {string}keeps the old DOM-absence check (substring, hidden elements included). A scenario that relied onI should not see the textfailing for hidden text should switch to it. - All new steps render
{{variables}}throughrenderStrict(), so an unset variable fails the step instead of passing a negative check.
0.5.0
Features
-
Enabled integrations now act on
sdods run, and GitHub issues from failures carry video and trace, survive a fresh CI runner and have their labels checked.sdods runpublishes the finished run to every enabled integration (check runs, PR comment, issues percreateIssueOnFailure) through the same code path assdods integrations notify.integrations.github.createIssueOnFailureused to do nothing unless a separate notify step existed.--no-notifyopts out; cancelled runs, runs with no scenario results and sharded runs do not notify. A notify failure is printed as a warning and never changes the exit code, and--jsonoutput reports the outcome undernotify.- GitHub issue bodies link the failing attempt's
video.webmandtrace.zipfromrunner-output/, read fromrunner-results.json, with thenpx playwright show-tracecommand.uploadToReleaseuploads them with their real content type instead ofimage/png. - Before creating an issue, the GitHub provider searches the repository for an open issue carrying
sdods-fingerprint:<fingerprint>and comments on it, so a runner without.sdods/issue-links.jsonno longer opens a duplicate. Notifying the same run twice no longer comments twice. sdods integrations testfails when a configured GitHub label does not exist in the repository and names the missing labels;--create-labelscreates them.
-
Trace, video, parallelism and a setup tier are now configurable instead of hard-coded in the runner config.
evidence: { trace, video, screenshot }insdods.project.yaml, overridable per key inenvs/<env>.yaml, withsdods run --trace <mode>/--video <mode>andSDODS_TRACE/SDODS_VIDEOon top. Values are Playwright's modes and are validated. Defaults are unchanged (on-first-retry,retain-on-failure,off); withretries.local: 0the old default meant no trace locally, so--trace onis now the way to get one without forcing a retry.fullyParallel(defaulttrue) per project and per process.falsekeeps the scenarios of a feature file in order.setup: { tags: '@setup' }per project, or per process (setup: falseto turn it off): each run target gets a<target>--setupcompanion that runs the matching scenarios first, in the same browser, and the target depends on it, so the rest of the run does not start when a probe or login fails. Setup scenarios ignore--tagsand run once.parseRunnerProjectNamereads the new<project>--<layer>[--<browser>]--setupnames as their real layer and browser withphase: 'setup'.- A config value that is invalid only after
${VAR},SDODS_*or CLI overrides now fails with a configuration error naming the path instead of a raw validation error.
Fixes
-
Cucumber messages record which step passed or failed again.
On Playwright below 1.63, a workspace that installs SDODS from npm recorded every Gherkin step
SKIPPEDinmessages.ndjson(and the cucumber HTML report), with failures attached to a hook. playwright-bdd matches a step's result by its line in the generated spec, and those Playwright versions report the line in their transformed copy instead. Measured on 1.60.0, 1.61.1, 1.62.0 and 1.62.1 against 1.63.0.@playwright/test1.63.0 is now the floor:sdods initscaffolds^1.63.0,@sdods/coredeclares>=1.63as its peer range, and the server image usesmcr.microsoft.com/playwright:v1.63.0-noble.sdods runwarns andsdods doctorfails a "step results" check when the workspace resolves an older@playwright/test, and both name the upgrade command.
-
sdods auth captureloadssteps/auth.tsthe same waysdods rundoes.- A project's
steps/auth.tsthat imports a sibling helper the conventional way (import './helper.js'forhelper.ts) worked undersdods run, which goes through Playwright's TypeScript loader, and crashedsdods auth capturewithCannot find module .../helper.js: capture used a bareimport(), so the published CLI handed the file to Node's native type stripping, which does not map.jsto.ts. Capture now imports it through tsx, which maps.jsto.ts, transpiles full TypeScript (enums included) and honours the project's module type, like Playwright's loader. The hooks are registered for that import only and removed afterwards.tsxis now a dependency of@sdods/core. - A project without
"type": "module"has itsauthexport read from the CommonJS module too, instead of silently falling back to the yaml strategy.
- A project's
-
Built-in steps render every
{{variable}}the same way, and field steps no longer trip over a control whose label merely contains the field's.- Every built-in step renders its string arguments through one shared helper,
renderStrict().the page URL should contain,the page title should contain, the test-id, dropdown, checkbox, upload, visual-baseline and mock steps rendered nothing before, so"/workspace/{{fixtureWorkspaceId}}"was matched with its braces and could never pass. The API steps now also render JSON paths, header names and values, regex patterns and schema names. - A variable with no value now fails the step with
no such variable: <name>instead of being matched as the literal{{name}}. For a negative assertion that is the difference between a check and a no-op:I should not see the text "{{tenantBDatasetId}}"used to pass whatever the page showed, and a request to/datasets/{{tenantBDatasetId}}used to go out and 404. Doc-string bodies (JSON, HTML, raw text) still render leniently, since free text may carry{{…}}of its own. Arguments that name the variable being written (… as {string}) are identifiers and are not rendered. I fill the {string} field with {string},I fill the form:,I select … dropdown,I check … checkboxandI upload …prefer an exact label and a control whose role fits the action, then fall back to the partial label.getByLabel('Password')also matched a "Show password" toggle, and the fill failed on a strict-mode violation.Healer.resolve()treats a visible locator that matches several elements as a heal trigger for click, fill, select and check: it narrows to the one element whose role fits the action and records a HealEvent, or fails withHEAL_FAILEDnaming the match count. It used to return the ambiguous locator, so the action threw a strict-mode violation that healing never saw. Assertions and hovers are unchanged.
- Every built-in step renders its string arguments through one shared helper,
0.4.0
Features
-
A run that executes no scenarios now fails instead of passing.
sdods runexits2when the selection matches nothing, and says so. Zero scenarios with exit0looked exactly like a green run, so a mistyped--tagsor--featurekept CI passing while testing nothing. One shard of several may still come back empty, and--allow-emptyrestores the old behaviour for a run that is expected to select nothing.- A malformed tag expression (
--tags "@smoke and (") is a configuration error (exit2) naming the expression, raised before specs are generated; it used to crash bddgen with a stack trace and a hint about undefined steps. --featuremust name a feature file in the project. It accepts a path relative tofeatures/, to the project, to the repository, or an absolute one; a path that does not exist is refused instead of selecting nothing.sdods features list --tagsevaluates full tag expressions, the same waysdods rundoes. It compared the whole expression to each tag, so@ui and @smokelisted nothing.
Fixes
-
Cucumber messages record which step passed or failed again.
On Playwright below 1.63, a workspace that installs SDODS from npm recorded every Gherkin step
SKIPPEDinmessages.ndjson(and the cucumber HTML report), with failures attached to a hook. playwright-bdd matches a step's result by its line in the generated spec, and those Playwright versions report the line in their transformed copy instead. Measured on 1.60.0, 1.61.1, 1.62.0 and 1.62.1 against 1.63.0.@playwright/test1.63.0 is now the floor:sdods initscaffolds^1.63.0,@sdods/coredeclares>=1.63as its peer range, and the server image usesmcr.microsoft.com/playwright:v1.63.0-noble.sdods runwarns andsdods doctorfails a "step results" check when the workspace resolves an older@playwright/test, and both name the upgrade command.
0.3.2
-
The tag gate now really does decide before a pool account is leased.
0.3.0 claimed it did, on the strength of
$sdodsTagGatebeing declared first in test scope. Declaration order is not an ordering guarantee: Playwright instantiates fixtures in DEPENDENCY order, anduseris pulled in bystorageState, which the browser context needs. So the lease ran first.An
@env:local @user:noconsentscenario run against staging therefore failed withNo users with role "noconsent" in dataset "users"instead of skipping. Sixteen of seventy-two@env:localscenarios failed that way in one run, including two whose whole purpose is to fire a deliberate burst at a rate limiter — exactly the scenarios the tag exists to keep off a shared environment.usernow depends on$sdodsTagGate, which is the only ordering guarantee Playwright offers. Same run afterwards: 72 skipped, 0 failed.
0.3.1
-
steps.core.exclude— the migration path 0.3.0 should have shipped with.0.3.0 added ten step libraries at once. They share ONE step namespace with a project's own steps, and playwright-bdd fails generation outright when two definitions match the same text, so the upgrade broke every consumer that had already written those phrasings — before a single scenario ran, with
Multiple definitions matched scenario step. The first repo to upgrade hit 67 collisions, which is not a coincidence: the core libraries were modelled on the gaps that project documented.The only remedy was to rewrite all 67 in one commit, unverified, because the suite could not run until every one was gone.
steps: core: exclude: [a11y, browser, dom, net, perf]
excludes by file basename. With nothing excluded the pattern is byte-identical to what shipped before, so projects that never set it are unaffected. An unknown name throws
CONFIG_INVALIDlisting what is available — a typo that silently excluded nothing would leave the author believing the collision was handled while generation still failed, pointing at the step rather than at the typo.
0.3.0
Features
-
Four tags the runner validated and then ignored now actually do something.
@env:,@skip:<browser>and@flag:were checked at lint time and had no runtime path at all — the linter confirmed the tag was spelled correctly and the runner ignored it, so a project could carry hundreds of@env:tags and still point every one of them at production.@quarantinewas not a tag this framework knew about, so every recipe excluded it by hand in a tag expression that drifts and that nobody can audit centrally.scenarioSkipReason()is a pure function inconfig/tags.ts, applied by a new automatic fixture declared first in test scope — so a scenario is excluded before an account is leased, a browser context is built, or a session is minted. The reason is recorded as an annotation as well as a skip, because a report that says "skipped" without saying why is how parked scenarios go unnoticed.@flag:gates only when the environment declares a flag list. An absent list means "not known", and a gate that fails closed on missing metadata would silently skip an entire suite.sdods run --since <range>selects only the features a git range could have broken, using theanalyzeChangeImpactmapping that already existed and was reachable only through MCP. When nothing is impacted it says so and runs nothing, rather than running everything or exiting green on an empty run. -
Five new step libraries, and a publish path that cannot ship a stale build.
a11y.steps.tsruns a real axe-core audit through@axe-core/playwrightand adds the structural checks a rule engine cannot make — heading outline, alt text, per-rule isolation.gates.a11yand the@a11ytag stop being schema-only.perf.steps.tswires the previously inertPerfBudgetsSchema:pageLoadMs,lcpMs,fcpMs,ttfbMsfrom real web-vitals, andapiP95Msfrom sampled request latency.net.steps.tsadds download capture (filename, content type, CSV rows, JSON path) and SSE/streaming assertions.dom.steps.tsadds focus trapping, dialog and popover state, and keyboard navigation.browser.steps.tsadds cookies, storage, viewport and colour-scheme control, and console-error capture.Every step that quantifies over a set fails when the set is empty. "Every image has an alt" over a page with no images is silence, not a pass, and silence is what makes an accessibility suite worthless.
scripts/publish-npm.shnow builds clean instead of relying on incrementaltsc -b, and refuses to publish a package whose stageddist/is older than itssrc/.@sdods/core@0.2.2shippedapiContext.auth = undefinedwhile its source saidauth = null; because the publish loop skips versions already on the registry, that tarball can never be corrected at that version. -
Five step libraries for the surfaces Gherkin could not reach.
iframe.steps.ts— a frame is entered, acted in, and left. Threading an optional frame through every UI step would touch every signature for a surface most scenarios never see, so the scope is per-page state instead. Unblocks hosted checkout, which is the commonest untestable surface there is.tabs.steps.ts— the action that opens a tab and the wait for it are ONE step.waitForEvent('page')after the click is a race that loses a fast popup. Switching is explicit, because a library that silently re-pointspagemakes every later assertion ambiguous about which document it read.db.steps.ts— read-only assertions over the existing Kysely fixture, which had no step reading it. There is deliberately no INSERT: seeding through the database is how a suite comes to assert against states the product cannot produce. Table and column names are validated against a strict identifier pattern, because an identifier is interpolated rather than parameterised.clock.steps.ts— installing the clock is separate from moving it, and must come first. A 55-minute session TTL is not a thing a suite can wait through, so without this those scenarios are not slow, they are unwritable.webhook.steps.ts— an ephemeral loopback receiver whose URL is published as{{callback.url}}, torn down after every scenario including a failing one. Deliveries are asserted by polling, never by sleeping.
Fixes
-
Seven analyzer and auth-strategy defects, each of which stated something false with confidence.
detectPackageManagernow walks up to the workspace root, so scanning a member directory of a monorepo no longer reportsunknown/monorepo: false.Base URLs are ranked by how specifically the variable NAME claims to be the base URL, instead of first-wins by file order —
SIM_AGENT_API_URLno longer beatsNEXT_PUBLIC_API_URLfrom 39 lines above it.An express dependency no longer forces
api = ui: the.listen()port is only trusted when it names a different origin, so a full-stack app falls through toui + '/api'as intended.Gitignored
.env*files are skipped rather than read and republished into the report..env.exampleno longer outranks a real environment for base URLs.detectAuthbreaks ties by an explicit precedence and reports the ambiguity — with an epsilon, because these scores are sums of decimal weights and an exact===would call a 2e-16 difference a clear winner.sdods analyzegains--max-files/--max-depth, and the default file budget rises from 8,000 to 25,000: a single real app was 6,400 files, so the walk truncated before reaching.github/and then reported "no CI" as a fact.The
ssoandtokenauth strategies throw with a hint instead of silently returning no browser session. -
sdods auth captureno longer skips the token when the login state is fresh.The login state and the API token are two artefacts with two lifetimes, and the freshness check covered only the first — but it
continued past the whole user, so a role could hold a fresh browser session and no token file at all. The API layer reads that file (steps/data.steps.ts), so every@user:<role>scenario fell through to a livetoken()mint: one identity-provider sign-in per scenario. In mybotbox-qa#57 that produced 1,010QUOTA_EXCEEDEDrecords in a single run, which is 67% of that suite's API-layer failures.The freshness check now gates the login only. The token is minted when there is no token file yet, or when
--forceasks for a fresh one — so repeated captures no longer leak a new API key into the application under test on every call, which the previous unconditional mint did.
The versions below came before changesets; their notes were written by hand.
0.2.1
Fixes found by driving the web UI and sweeping every CLI command.
- Sign out works. It could do nothing at all: logging out without a live session returned 401, and the client discarded the rejection, so the button had no visible effect. Logout is now idempotent and always clears the cookie, while an authenticated logout still requires a CSRF token so no cross-site page can force one. Signing out also clears every cached query, so no data crosses a session boundary on a shared machine.
- An expired session returns you to the sign-in form instead of rendering "AUTH_REQUIRED: Authentication required" inside a shell that still looked signed in.
users deactivate,users set-roleandtokens revokeno longer crash with "root.opts is not a function".users deactivateis how access is revoked, so this was not cosmetic.tokens revoketells the truth: an unknown or already-revoked id now warns instead of reporting success.- The platform database is resolved from the workspace root. Running a command from a
subdirectory used to open a different, empty database —
sdods servestarted inside a run's artifacts folder rejected every login because the users lived in the workspace database. - The projects list shows its processes rather than
processes: , , ,.
0.2.0
First published release: npm i -g @sdods/cli installs SDODS as compiled packages, so there is no
longer a source checkout in ~/.sdods/app.
-
The installer defaults to the published packages;
--source gitclones a checkout for working on SDODS itself, and is never used as a silent fallback. -
sdods initscaffolds the demo project from assets shipped inside@sdods/cli. -
sdods serveships the dashboard inside@sdods/server.0.1.0 was never released for this project; the version number was already taken on npm by an earlier, unrelated placeholder, so the first release is 0.2.0.
0.1.0 (unreleased)
- Monorepo with
@sdods/contracts,@sdods/core,@sdods/cliand thedemo-shopproject. - Six-layer configuration precedence with provenance, secret-literal refusal and
${VAR:-default}interpolation. - Organization → workspace → project → module hierarchy and named processes.
sdods project,env,config,doctorcommands.- This documentation site.