From 99020ee4875ae00031fcc56c9ea54f4564a3a614 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Fri, 15 May 2026 11:54:44 -0400 Subject: [PATCH 01/11] feat(.claude): bug-repro plugin for OpenTDF (DSPX-3302) Adds five Claude Code skills under tests/.claude/skills/ that together turn a Jira bug ticket into a running reproduction, plus a downstream- installable plugin manifest under .claude/plugin/. Why --- The end-to-end goal of DSPX-3302 is to make bug reproduction approachable for QA, downstream-product engineers, and CI. PRs 1-4 build the plumbing (shared schema, platform installer, multi-instance otdf-local, xtest conftest hooks). This PR is the user-facing surface: a Claude can pull context from Jira, draft an xtest/scenarios/.yaml (and, when needed, an xtest/bug__test.py), bring the environment up at the right version pins, run the scenario's pytest selection, and tear down. Skills ------ scenario-from-bug-report Pulls the Jira issue and its comments via `acli jira workitem view --fields '*all' --json` and `acli jira workitem comment list`, extracts version pins / KAS topology / container type / feature flags, then writes xtest/scenarios/.yaml validated against otdf_sdk_mgr.schema.Scenario. Drafts a new xtest/bug__test.py only when no existing pytest covers the case; never silently lands assertions. scenario-up Runs `otdf-sdk-mgr install scenario`, then `otdf-local instance init --from-scenario`, then `otdf-local --instance up`, and polls status until healthy. Surfaces logs rather than retrying blindly when something stays unhealthy. scenario-run Invokes `otdf-local scenario run ` and classifies the result: "bug reproduced" / "not reproduced" / "unrelated failure". Cites the evidence line and points at per-service logs. scenario-tear-down Stops the instance and optionally removes the directory after explicit user confirmation. instance-status Lists known instances, their port bases, health, and flags port collisions. Jira-safety ----------- Permissions in both .claude/settings.json and the plugin manifest allow only read+comment via acli jira: workitem view, workitem search, workitem comment list, workitem comment create, plus a handful of read-only project/board/sprint queries. edit, delete, transition, assign, archive, link create, watcher add are all denied. The plugin.json carries a permission_notes block explaining the policy. Plugin manifest --------------- .claude/plugin/plugin.json declares the skill names, runtime requirements (uv, go, git, docker, acli), and the canonical permission allowlist, so downstream first/third-party integrators can install this plugin into their own Claude Code setups. Refs: https://virtru.atlassian.net/browse/DSPX-3302 Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/plugin/plugin.json | 38 ++++++ .claude/settings.json | 32 +++++ .claude/skills/instance-status/SKILL.md | 36 ++++++ .../skills/scenario-from-bug-report/SKILL.md | 111 ++++++++++++++++++ .claude/skills/scenario-run/SKILL.md | 43 +++++++ .claude/skills/scenario-tear-down/SKILL.md | 42 +++++++ .claude/skills/scenario-up/SKILL.md | 51 ++++++++ 7 files changed, 353 insertions(+) create mode 100644 .claude/plugin/plugin.json create mode 100644 .claude/settings.json create mode 100644 .claude/skills/instance-status/SKILL.md create mode 100644 .claude/skills/scenario-from-bug-report/SKILL.md create mode 100644 .claude/skills/scenario-run/SKILL.md create mode 100644 .claude/skills/scenario-tear-down/SKILL.md create mode 100644 .claude/skills/scenario-up/SKILL.md diff --git a/.claude/plugin/plugin.json b/.claude/plugin/plugin.json new file mode 100644 index 00000000..fce6cee4 --- /dev/null +++ b/.claude/plugin/plugin.json @@ -0,0 +1,38 @@ +{ + "name": "opentdf-test-harness", + "version": "0.1.0", + "description": "Skills for reproducing OpenTDF bugs locally via otdf-local and otdf-sdk-mgr. Pulls bug context from Jira (acli), provisions pinned platform/KAS/SDK versions, runs the xtest pytest suite, and tears down. Useful for QA, platform/SDK developers, and downstream first/third-party integrators.", + "skills_dir": "../skills", + "skills": [ + "scenario-from-bug-report", + "scenario-up", + "scenario-run", + "scenario-tear-down", + "instance-status" + ], + "requirements": [ + "uv (python package manager) on PATH", + "go toolchain (platform binaries are built from source)", + "git (for worktrees of opentdf/platform)", + "docker (for keycloak/postgres dependencies)", + "acli (Atlassian CLI; needed for the scenario-from-bug-report skill)" + ], + "permissions": { + "allow": [ + "Bash(uv run otdf-local *)", + "Bash(uv run otdf-sdk-mgr *)", + "Bash(uv run pytest *)", + "Bash(acli jira workitem view *)", + "Bash(acli jira workitem search *)", + "Bash(acli jira workitem comment list *)", + "Bash(acli jira workitem comment create *)", + "Bash(acli jira workitem attachment list *)", + "Bash(acli jira workitem link list *)", + "Bash(acli jira project view *)", + "Write(xtest/scenarios/**)", + "Write(xtest/bug_*_test.py)", + "Write(tests/instances/**)" + ] + }, + "permission_notes": "acli jira write-paths intentionally excluded: edit/delete/transition/assign/archive/clone/create/create-bulk/link create/watcher add/comment update/comment delete. Add them explicitly via .claude/settings.local.json if your team needs them; the default plugin is read+comment only." +} diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..a1dba2d8 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,32 @@ +{ + "permissions": { + "allow": [ + "Bash(uv run otdf-local *)", + "Bash(uv run otdf-sdk-mgr *)", + "Bash(uv run pytest *)", + "Bash(uv sync *)", + "Bash(git status *)", + "Bash(git diff *)", + "Bash(git log *)", + "Bash(git show *)", + "Bash(gh api *)", + "Bash(gh issue view *)", + "Bash(gh pr view *)", + "Bash(gh run *)", + "Bash(acli jira workitem view *)", + "Bash(acli jira workitem search *)", + "Bash(acli jira workitem comment list *)", + "Bash(acli jira workitem comment create *)", + "Bash(acli jira workitem attachment list *)", + "Bash(acli jira workitem link list *)", + "Bash(acli jira workitem watcher list *)", + "Bash(acli jira project view *)", + "Bash(acli jira board view *)", + "Bash(acli jira sprint view *)", + "Write(xtest/scenarios/**)", + "Write(xtest/bug_*_test.py)", + "Write(tests/instances/**)", + "Write(.claude/tmp/**)" + ] + } +} diff --git a/.claude/skills/instance-status/SKILL.md b/.claude/skills/instance-status/SKILL.md new file mode 100644 index 00000000..64bd545a --- /dev/null +++ b/.claude/skills/instance-status/SKILL.md @@ -0,0 +1,36 @@ +--- +name: instance-status +description: Report which test instances exist on disk, which are running, and the health of each service. Use when the user asks "what's running" or before bringing up another scenario to avoid port collisions. +allowed-tools: Bash, Read +--- + +# instance-status + +You give the user a snapshot of all test instances in this checkout: what's defined, what's running, and whether each service is healthy. + +## Process + +1. **List instances on disk**: + + ```bash + uv run otdf-local instance ls --json + ``` + + Each entry includes `name`, `platform` version, `ports_base`, and the `kas:` keys. Flag any two instances that share a `ports_base` — they cannot run concurrently. + +2. **For each instance**, check service status: + + ```bash + uv run otdf-local --instance status --json + ``` + + Each service reports `running`, `healthy`, and the bound port. Don't run all instances in parallel — iterate; a status query is cheap. + +3. **Summarize**: + - A short table per instance: service → port → state. + - Flag any unhealthy service with the path to its log (e.g. `tests/instances//logs/kas-alpha.log`). + - Mention port conflicts if two instances would collide on `ports.base`. + +## When ports collide + +`otdf-local instance init` warns about this at creation time but does not enforce it. If you see two instances with the same `ports_base`, recommend the user reassign one via `uv run otdf-local instance init --from-scenario --ports-base ` (or hand-edit the `instance.yaml`). diff --git a/.claude/skills/scenario-from-bug-report/SKILL.md b/.claude/skills/scenario-from-bug-report/SKILL.md new file mode 100644 index 00000000..3c28144f --- /dev/null +++ b/.claude/skills/scenario-from-bug-report/SKILL.md @@ -0,0 +1,111 @@ +--- +name: scenario-from-bug-report +description: Pull a Jira bug into context (via `acli jira workitem view`) and turn it into an xtest/scenarios/.yaml manifest, optionally drafting xtest/bug__test.py when no existing pytest covers it. Use when the user mentions a Jira issue key like DSPX-1234 (or another [PROJECT]-[NUMBER] format) and asks for a reproducer. +allowed-tools: Bash, Read, Write, Grep, Glob +--- + +# scenario-from-bug-report + +Bugs are tracked in Jira. The user will reference an issue by its key in the form `[PROJECT]-[NUMBER]` — examples: `DSPX-3302`, `DSPX-1234`. `DSPX` is the current project's prefix but the prefix can change (e.g. `OPS-`, `SDK-`); accept any short uppercase prefix. + +You produce two artifacts the rest of the toolchain consumes: + +1. `xtest/scenarios/.yaml` — validated against `otdf_sdk_mgr.schema.Scenario`. +2. (Optional) `xtest/bug__test.py` — only if no existing xtest pytest already exercises the bug. + +The Jira key also becomes the working **branch name** (`-repro` if a fresh branch is needed) and the scenario file's `metadata.id`. + +## Step 1 — Pull the Jira issue into context + +Always start by fetching the full issue content. Don't proceed on the user's free-text summary alone — the issue body has the version pins and reproduction details you need. + +```bash +acli jira workitem view --fields '*all' --json +acli jira workitem comment list +``` + +The first command's JSON output includes `summary`, `description`, `status`, and labels. The second lists comments. Extract: + +- The **summary** (becomes scenario `metadata.title`). +- The **description** (read carefully — version numbers, KAS topology, container types, and feature flags typically live here). +- Recent **comments** — reproductions and "what changed" notes often appear in comments rather than the original description. + +If the issue references attached logs, screenshots, or linked PRs, list them via `acli jira workitem attachment list ` and `acli jira workitem link list ` and mention them in your reply. + +**Permitted Jira writes**: only `acli jira workitem comment create ...` (to post a reproduction-status update if the user asks). Everything else — `edit`, `transition`, `assign`, `archive`, `delete`, `link create`, `watcher add` — is explicitly disallowed by the plugin's permissions; if the user wants those actions, instruct them to run the command themselves. + +## Step 2 — Identify the scenario inputs + +From the issue text, extract: + +- **Encrypt-side SDKs** — which SDKs *create* the TDF? (`go`, `java`, `js`). Pin versions. +- **Decrypt-side SDKs** — which SDKs *consume* the TDF? Pin versions. +- **Platform version** — git tag like `v0.9.0` (resolves to the `service/v0.9.0` tag in `opentdf/platform`). +- **KAS topology** — which KAS instances must be running (`alpha`, `beta`, `gamma`, `delta`, `km1`, `km2`) and whether any need a different pinned version than the platform. +- **Container type** — `ztdf`, `ztdf-ecwrap`, `nano`, or `nano-with-policy`. +- **Feature flags** — e.g. `ec_tdf_enabled`. +- **Expected vs actual behavior** — copy concise prose from the issue. + +If anything is ambiguous in the Jira issue, ask the user — don't guess at versions. + +## Step 3 — Pick the id and (optionally) the branch + +- `metadata.id = ` — e.g. `DSPX-3302` → `dspx-3302`. +- Scenario file path: `xtest/scenarios/.yaml`. +- If you need a new git branch, propose `-repro` (e.g. `DSPX-3302-repro`) and let the user confirm before switching. + +## Step 4 — Search for an existing pytest + +```bash +grep -rn "" xtest/test_*.py +``` + +Likely candidates: `test_tdfs.py` (roundtrip), `test_abac.py` (ABAC), `test_legacy.py` (golden), `test_pqc.py`. If a test already asserts the relevant behavior, reuse it — only the scenario changes, not the code. + +## Step 5 — Write `xtest/scenarios/.yaml` + +Exact field shape (the schema rejects unknown fields): + +```yaml +apiVersion: opentdf.io/v1alpha1 +kind: Scenario +metadata: + id: + title: "" + created: +instance: + metadata: { name: } + platform: { dist: } + ports: { base: } + kas: + : { dist: , mode: standard } # or mode: key_management +sdks: + encrypt: + : { version: } + decrypt: + : { version: } +suite: + select: "" + containers: + # markers: "not slow" + # extra_args: ["--no-audit-logs"] +expected: "" +actual: "" +``` + +Validate before reporting success: + +```bash +uv run python -m otdf_sdk_mgr.schema validate xtest/scenarios/.yaml +``` + +## Step 6 — If no existing test fits + +Draft `xtest/bug__test.py` using the `encrypt_sdk` / `decrypt_sdk` fixtures (pattern: `xtest/test_tdfs.py`). Surface the new file in your reply for the user to review — never silently land assertions. + +## Notes + +- `sdks.encrypt` and `sdks.decrypt` map to xtest's `--sdks-encrypt` / `--sdks-decrypt`. After PR #446 those pytest options take `sdk@version` specifiers like `go@v0.24.0`, `go@main`, or `go@*`. **Do NOT write those tokens in the YAML** — write a normal `{ version: lts }` (or any version string `otdf-sdk-mgr resolve` accepts: `v0.24.0`, `main`, an SDK-specific SHA, etc.). The `scenario-up` skill runs `otdf-sdk-mgr install scenario`, which records the resolved dist directory names in `xtest/scenarios/.installed.json`; the bridge layers (`otdf-local scenario run` and pytest's `--scenario` default in `xtest/conftest.py`) read that file to emit the right `sdk@` tokens. If you forget the install step, those commands fail with `.installed.json not found — run otdf-sdk-mgr install scenario first`. +- List the same SDK in both `encrypt` and `decrypt` maps to reproduce xtest's legacy "all pairs" mode. Listing it on only one side keeps the scenario focused (a→b without b→a). +- `instance.platform.dist` and each `kas..dist` need `otdf-sdk-mgr install scenario ` (or `install release platform:`) to have built the binary first. `scenario-up` handles that downstream. +- One-line summary when done: report the scenario path, the new test file (if any), and the Jira link `https://virtru.atlassian.net/browse/` so the user can cross-reference. diff --git a/.claude/skills/scenario-run/SKILL.md b/.claude/skills/scenario-run/SKILL.md new file mode 100644 index 00000000..633846cf --- /dev/null +++ b/.claude/skills/scenario-run/SKILL.md @@ -0,0 +1,43 @@ +--- +name: scenario-run +description: Execute the pytest suite declared by a scenarios.yaml against the running instance, then classify the result as "bug reproduced", "not reproduced", or "unrelated failure". Use after `scenario-up` has confirmed the instance is healthy. +allowed-tools: Bash, Read +--- + +# scenario-run + +You run the pytest selection declared by the scenario's `suite` block against the running instance and interpret the result in terms of the bug being investigated. + +## Inputs + +- Path to the scenario YAML (`xtest/scenarios/.yaml`). +- (Optional) the user's expected outcome, if the scenario's `expected:` field is sparse. + +## Process + +1. **Invoke the runner**: + + ```bash + uv run otdf-local scenario run xtest/scenarios/.yaml + ``` + + This translates the scenario's `suite.select`, `suite.containers`, `suite.markers`, and `sdks.{encrypt,decrypt}` into the equivalent `pytest --sdks-encrypt ... --sdks-decrypt ... --containers ...` invocation under `xtest/` with `OTDF_LOCAL_INSTANCE_NAME` set. SDK tokens are emitted in xtest's `sdk@version` form (see PR #446) — the resolved version names come from the sibling `.installed.json` that `otdf-sdk-mgr install scenario` writes. + + If `scenario run` exits with `Error: .installed.json not found`, the user skipped the install step. Tell them to run `uv run otdf-sdk-mgr install scenario ` (or re-run `scenario-up`) before retrying. + +2. **Capture exit code and tail of output**. The pytest output is the source of truth; don't re-interpret. + +3. **Classify**: + - **Bug reproduced** — the test failed with an assertion or stderr that matches the scenario's `actual:` field. Cite the matching line. + - **Bug NOT reproduced** — the test passed. This is meaningful: either the bug is fixed at this version combination, or the scenario doesn't capture it precisely yet. Suggest the user widen the assertion or pick a different version pin. + - **Unrelated failure** — pytest errored out (collection error, environment issue, import error, timeout). Don't claim repro success or failure; report the error and recommend a next diagnostic step. + +4. **Record artifacts**. The pytest run leaves logs under `tests/instances//logs/`. List the relevant log files in your reply so the user can attach them to the Jira ticket. + +## Output format + +One-line headline (`bug reproduced` / `not reproduced` / `unrelated failure`), then a short bulleted summary: +- `select:` the pytest selector +- `exit_code:` the return value +- `evidence:` 1-2 lines from the output that justify the classification +- `logs:` paths to the relevant per-service logs diff --git a/.claude/skills/scenario-tear-down/SKILL.md b/.claude/skills/scenario-tear-down/SKILL.md new file mode 100644 index 00000000..39398c76 --- /dev/null +++ b/.claude/skills/scenario-tear-down/SKILL.md @@ -0,0 +1,42 @@ +--- +name: scenario-tear-down +description: Stop the services for a scenario's instance and optionally delete the instance directory. Use when the user is done with a reproduction or wants to free ports/disk for a different scenario. +allowed-tools: Bash, Read +--- + +# scenario-tear-down + +You stop a running scenario cleanly and optionally remove its on-disk state. + +## Inputs + +- The instance name (typically the lowercased Jira key, e.g. `dspx-3302`). If the user passes the scenario YAML path instead, read its `instance.metadata.name`. +- Whether the user wants the instance directory preserved (default: yes — keep it for re-runs). + +## Process + +1. **Stop services**: + + ```bash + uv run otdf-local --instance down + ``` + + The `down` command halts the platform process, all KAS instances under management, and the docker dependencies (keycloak, postgres) — unless another instance is still using them, in which case docker is left running. + +2. **Optionally clean state**. Only if the user explicitly asked to remove: + + ```bash + uv run otdf-local instance rm -y + ``` + + This deletes `tests/instances//` including its `logs/`, `keys/`, and per-KAS configs. The platform binary at `xtest/platform/dist//service` is shared and is NOT removed (`otdf-sdk-mgr clean --dist-only` is the right command if the user wants to free that too). + +3. **Confirm port range is free** (useful if the user is about to bring up another scenario on the same base): + + ```bash + uv run otdf-local instance ls --json + ``` + +## Caution + +Never remove an instance without explicit user confirmation. The directory may contain golden keys or generated configs that took time to assemble. If unsure, leave it. diff --git a/.claude/skills/scenario-up/SKILL.md b/.claude/skills/scenario-up/SKILL.md new file mode 100644 index 00000000..955fce8b --- /dev/null +++ b/.claude/skills/scenario-up/SKILL.md @@ -0,0 +1,51 @@ +--- +name: scenario-up +description: Provision artifacts, scaffold the instance directory, and start the test environment for a given xtest/scenarios/.yaml. Use after `scenario-from-bug-report` (or when the user already has a scenario YAML) and wants the environment running. +allowed-tools: Bash, Read +--- + +# scenario-up + +You bring the environment described by a `scenarios.yaml` up and confirm it's healthy. The three steps are non-negotiable; do them in order. + +## Inputs + +- Path to a validated `xtest/scenarios/.yaml`. If the user doesn't provide one, ask. + +## Process + +1. **Install artifacts** — platform binary, per-KAS binaries, helper scripts, and the encrypt+decrypt SDKs declared in the scenario: + + ```bash + uv run otdf-sdk-mgr install scenario xtest/scenarios/.yaml + ``` + + This writes `xtest/scenarios/.installed.json` next to the scenario with the resolved dist paths. The first `go build` per platform version takes ~30-60s; subsequent runs reuse the cached binary. + +2. **Scaffold the instance directory** (creates `tests/instances//`): + + ```bash + uv run otdf-local instance init --from-scenario xtest/scenarios/.yaml + ``` + + If the instance already exists, this is a no-op for the existing files; double-check with `uv run otdf-local instance ls` first to avoid surprising the user. + +3. **Bring it up**: + + ```bash + uv run otdf-local --instance up + ``` + + Then poll status until everything is healthy (don't proceed before this succeeds): + + ```bash + uv run otdf-local --instance status --json + ``` + + If any service stays unhealthy after ~60 seconds, surface the relevant log via `uv run otdf-local --instance logs -n 50` and report the failure mode rather than retrying blindly. + +## Output + +Once healthy, report: +- The instance name and which ports it occupies (look at `instance.yaml`'s `ports.base`). +- The next command the user is likely to run (`scenario-run`). From 79f61b088625d0528e54971630b8635937609cb4 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Fri, 15 May 2026 15:24:16 -0400 Subject: [PATCH 02/11] =?UTF-8?q?refactor(.claude):=20generalize=20scenari?= =?UTF-8?q?o-from-bug-report=20=E2=86=92=20scenario-from-ticket=20(DSPX-33?= =?UTF-8?q?02)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Headless dogfooding (run-1 on DSPX-2719) showed the bug-only framing was too narrow — the common workflow is writing tests for new features first (TDD), not reproducing version-pinned bugs. - Rename and rewrite the skill to branch on Jira Issue Type. Bug follows the old expected/actual flow; Story/Task uses ref pins (`main`, feature branch, PR SHA via `gh pr view --json headRefOid`) for forward-looking regression gates; Spike bails out rather than fabricating. Mandates `acli workitem comment list` and steers away from cli.sh greps (both were run-1 gaps). - New `scenario-matrix` sibling skill: write N scenario files from a base × N refs (PRs/branches/releases). Schema/installer support was already there via `PlatformPin.source.ref` and `install_platform_source(ref)` — no other changes needed. - `scenario-run` output classification generalized from "bug reproduced / not reproduced" to "expected / unexpected outcome", with explicit branches for bug-repro vs TDD interpretations. - `scenario-up` description and `plugin.json` (description, skills array, requirements) updated to match. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/plugin/plugin.json | 8 +- .../skills/scenario-from-bug-report/SKILL.md | 111 ------------ .claude/skills/scenario-from-ticket/SKILL.md | 159 ++++++++++++++++++ .claude/skills/scenario-matrix/SKILL.md | 91 ++++++++++ .claude/skills/scenario-run/SKILL.md | 19 ++- .claude/skills/scenario-up/SKILL.md | 2 +- 6 files changed, 268 insertions(+), 122 deletions(-) delete mode 100644 .claude/skills/scenario-from-bug-report/SKILL.md create mode 100644 .claude/skills/scenario-from-ticket/SKILL.md create mode 100644 .claude/skills/scenario-matrix/SKILL.md diff --git a/.claude/plugin/plugin.json b/.claude/plugin/plugin.json index fce6cee4..bb4c1311 100644 --- a/.claude/plugin/plugin.json +++ b/.claude/plugin/plugin.json @@ -1,10 +1,11 @@ { "name": "opentdf-test-harness", "version": "0.1.0", - "description": "Skills for reproducing OpenTDF bugs locally via otdf-local and otdf-sdk-mgr. Pulls bug context from Jira (acli), provisions pinned platform/KAS/SDK versions, runs the xtest pytest suite, and tears down. Useful for QA, platform/SDK developers, and downstream first/third-party integrators.", + "description": "Jira-ticket-driven scenarios for the OpenTDF test harness. Pulls ticket context from Jira (acli) — any ticket type, including bugs, feature stories, and PR-driven work — provisions pinned platform/KAS/SDK versions or refs (released versions, main, feature branches, PR SHAs), runs the xtest pytest suite, and tears down. Useful for QA, platform/SDK developers writing tests for new features first, and downstream first/third-party integrators.", "skills_dir": "../skills", "skills": [ - "scenario-from-bug-report", + "scenario-from-ticket", + "scenario-matrix", "scenario-up", "scenario-run", "scenario-tear-down", @@ -15,7 +16,8 @@ "go toolchain (platform binaries are built from source)", "git (for worktrees of opentdf/platform)", "docker (for keycloak/postgres dependencies)", - "acli (Atlassian CLI; needed for the scenario-from-bug-report skill)" + "acli (Atlassian CLI; needed for the scenario-from-ticket skill)", + "gh (GitHub CLI; needed for scenario-matrix to resolve PR refs)" ], "permissions": { "allow": [ diff --git a/.claude/skills/scenario-from-bug-report/SKILL.md b/.claude/skills/scenario-from-bug-report/SKILL.md deleted file mode 100644 index 3c28144f..00000000 --- a/.claude/skills/scenario-from-bug-report/SKILL.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -name: scenario-from-bug-report -description: Pull a Jira bug into context (via `acli jira workitem view`) and turn it into an xtest/scenarios/.yaml manifest, optionally drafting xtest/bug__test.py when no existing pytest covers it. Use when the user mentions a Jira issue key like DSPX-1234 (or another [PROJECT]-[NUMBER] format) and asks for a reproducer. -allowed-tools: Bash, Read, Write, Grep, Glob ---- - -# scenario-from-bug-report - -Bugs are tracked in Jira. The user will reference an issue by its key in the form `[PROJECT]-[NUMBER]` — examples: `DSPX-3302`, `DSPX-1234`. `DSPX` is the current project's prefix but the prefix can change (e.g. `OPS-`, `SDK-`); accept any short uppercase prefix. - -You produce two artifacts the rest of the toolchain consumes: - -1. `xtest/scenarios/.yaml` — validated against `otdf_sdk_mgr.schema.Scenario`. -2. (Optional) `xtest/bug__test.py` — only if no existing xtest pytest already exercises the bug. - -The Jira key also becomes the working **branch name** (`-repro` if a fresh branch is needed) and the scenario file's `metadata.id`. - -## Step 1 — Pull the Jira issue into context - -Always start by fetching the full issue content. Don't proceed on the user's free-text summary alone — the issue body has the version pins and reproduction details you need. - -```bash -acli jira workitem view --fields '*all' --json -acli jira workitem comment list -``` - -The first command's JSON output includes `summary`, `description`, `status`, and labels. The second lists comments. Extract: - -- The **summary** (becomes scenario `metadata.title`). -- The **description** (read carefully — version numbers, KAS topology, container types, and feature flags typically live here). -- Recent **comments** — reproductions and "what changed" notes often appear in comments rather than the original description. - -If the issue references attached logs, screenshots, or linked PRs, list them via `acli jira workitem attachment list ` and `acli jira workitem link list ` and mention them in your reply. - -**Permitted Jira writes**: only `acli jira workitem comment create ...` (to post a reproduction-status update if the user asks). Everything else — `edit`, `transition`, `assign`, `archive`, `delete`, `link create`, `watcher add` — is explicitly disallowed by the plugin's permissions; if the user wants those actions, instruct them to run the command themselves. - -## Step 2 — Identify the scenario inputs - -From the issue text, extract: - -- **Encrypt-side SDKs** — which SDKs *create* the TDF? (`go`, `java`, `js`). Pin versions. -- **Decrypt-side SDKs** — which SDKs *consume* the TDF? Pin versions. -- **Platform version** — git tag like `v0.9.0` (resolves to the `service/v0.9.0` tag in `opentdf/platform`). -- **KAS topology** — which KAS instances must be running (`alpha`, `beta`, `gamma`, `delta`, `km1`, `km2`) and whether any need a different pinned version than the platform. -- **Container type** — `ztdf`, `ztdf-ecwrap`, `nano`, or `nano-with-policy`. -- **Feature flags** — e.g. `ec_tdf_enabled`. -- **Expected vs actual behavior** — copy concise prose from the issue. - -If anything is ambiguous in the Jira issue, ask the user — don't guess at versions. - -## Step 3 — Pick the id and (optionally) the branch - -- `metadata.id = ` — e.g. `DSPX-3302` → `dspx-3302`. -- Scenario file path: `xtest/scenarios/.yaml`. -- If you need a new git branch, propose `-repro` (e.g. `DSPX-3302-repro`) and let the user confirm before switching. - -## Step 4 — Search for an existing pytest - -```bash -grep -rn "" xtest/test_*.py -``` - -Likely candidates: `test_tdfs.py` (roundtrip), `test_abac.py` (ABAC), `test_legacy.py` (golden), `test_pqc.py`. If a test already asserts the relevant behavior, reuse it — only the scenario changes, not the code. - -## Step 5 — Write `xtest/scenarios/.yaml` - -Exact field shape (the schema rejects unknown fields): - -```yaml -apiVersion: opentdf.io/v1alpha1 -kind: Scenario -metadata: - id: - title: "" - created: -instance: - metadata: { name: } - platform: { dist: } - ports: { base: } - kas: - : { dist: , mode: standard } # or mode: key_management -sdks: - encrypt: - : { version: } - decrypt: - : { version: } -suite: - select: "" - containers: - # markers: "not slow" - # extra_args: ["--no-audit-logs"] -expected: "" -actual: "" -``` - -Validate before reporting success: - -```bash -uv run python -m otdf_sdk_mgr.schema validate xtest/scenarios/.yaml -``` - -## Step 6 — If no existing test fits - -Draft `xtest/bug__test.py` using the `encrypt_sdk` / `decrypt_sdk` fixtures (pattern: `xtest/test_tdfs.py`). Surface the new file in your reply for the user to review — never silently land assertions. - -## Notes - -- `sdks.encrypt` and `sdks.decrypt` map to xtest's `--sdks-encrypt` / `--sdks-decrypt`. After PR #446 those pytest options take `sdk@version` specifiers like `go@v0.24.0`, `go@main`, or `go@*`. **Do NOT write those tokens in the YAML** — write a normal `{ version: lts }` (or any version string `otdf-sdk-mgr resolve` accepts: `v0.24.0`, `main`, an SDK-specific SHA, etc.). The `scenario-up` skill runs `otdf-sdk-mgr install scenario`, which records the resolved dist directory names in `xtest/scenarios/.installed.json`; the bridge layers (`otdf-local scenario run` and pytest's `--scenario` default in `xtest/conftest.py`) read that file to emit the right `sdk@` tokens. If you forget the install step, those commands fail with `.installed.json not found — run otdf-sdk-mgr install scenario first`. -- List the same SDK in both `encrypt` and `decrypt` maps to reproduce xtest's legacy "all pairs" mode. Listing it on only one side keeps the scenario focused (a→b without b→a). -- `instance.platform.dist` and each `kas..dist` need `otdf-sdk-mgr install scenario ` (or `install release platform:`) to have built the binary first. `scenario-up` handles that downstream. -- One-line summary when done: report the scenario path, the new test file (if any), and the Jira link `https://virtru.atlassian.net/browse/` so the user can cross-reference. diff --git a/.claude/skills/scenario-from-ticket/SKILL.md b/.claude/skills/scenario-from-ticket/SKILL.md new file mode 100644 index 00000000..1f573b59 --- /dev/null +++ b/.claude/skills/scenario-from-ticket/SKILL.md @@ -0,0 +1,159 @@ +--- +name: scenario-from-ticket +description: Pull a Jira ticket of any type (Bug, Story, Task, Spike) into context via `acli jira workitem view` + `acli jira workitem comment list`, then turn it into an xtest/scenarios/.yaml manifest. Pins platform/KAS/SDKs to a released version (`dist:`), a branch or SHA (`source.ref:`), or the head of a PR — whichever matches the ticket. Optionally drafts xtest/bug__test.py when no existing pytest covers the behavior. Use when the user mentions a Jira key like DSPX-1234 (or any [PROJECT]-[NUMBER]) and wants a runnable scenario — reproducing a bug, writing a TDD test for a new feature, or validating behavior at a specific ref. +allowed-tools: Bash, Read, Write, Grep, Glob +--- + +# scenario-from-ticket + +You produce a `xtest/scenarios/.yaml` manifest from a Jira ticket. The same skill handles bugs, features (TDD), and exploratory work — the *Issue Type* field on the ticket selects which way the rest of this skill behaves. + +Two artifacts: + +1. `xtest/scenarios/.yaml` — validated against `otdf_sdk_mgr.schema.Scenario`. +2. (Optional) `xtest/bug__test.py` — only if no existing xtest pytest already exercises the behavior. The `bug_` prefix is a slug, not a type marker: feature-driven tests use it too. + +The Jira key also becomes the working **branch name** (`-repro` for Bugs, `-tdd` for Stories/Tasks) and the scenario file's `metadata.id`. + +## Step 1 — Pull the Jira ticket into context + +**Always run BOTH commands**. Don't skip the comment list — comments often carry the most recent reproduction status, "what changed" notes, or "fixed by PR #N" pointers that aren't in the original description: + +```bash +acli jira workitem view --fields '*all' --json +acli jira workitem comment list +``` + +From the JSON output of the first command, extract: + +- **Issue Type** (Bug, Story, Task, Spike) — load-bearing; selects which Step 2 branch to follow. +- **Summary** — becomes scenario `metadata.title`. +- **Description** — version numbers, KAS topology, container types, feature flags, acceptance criteria typically live here. +- **Status** — Backlog / In Progress / Done affects whether the scenario is forward-looking (TDD on Backlog) or retroactive (regression gate on Done). + +From the comments, pull any "tested at version X" / "reproduces on platform Y" / "fixed by PR #N" annotations into your mental model. + +If the ticket references attached logs, screenshots, or linked PRs, list them via `acli jira workitem attachment list ` and `acli jira workitem link list ` and call them out in your reply. + +**Permitted Jira writes**: only `acli jira workitem comment create ...` (to post a reproduction-status update if the user asks). Everything else — `edit`, `transition`, `assign`, `archive`, `delete`, `link create`, `watcher add` — is explicitly disallowed by the plugin's permissions; if the user wants those actions, instruct them to run the command themselves. + +## Step 2 — Branch on Issue Type + +### Bug + +The ticket describes a behavior that should work but doesn't. + +- `expected:` — what should happen (copy from the description's "expected behavior" section or rephrase the summary). +- `actual:` — what actually happens, including the exact error message if the ticket quotes one. +- Pin platform / KAS / SDKs to the **versions where the bug reproduces**. Usually `dist:` against a released version. Mixed-version topologies (e.g. platform `v0.9.0` + km1 `v0.9.0-rc.2`) are common and the schema supports them. + +If the description doesn't name versions, ask the user. (A headless agent has no user — in that case default to `dist: lts` everywhere and call out the assumption in `actual:`.) + +### Story / Task (feature work, TDD-style) + +The ticket describes a behavior the user wants to *add*. The scenario you produce is a forward-looking regression gate, not a bug reproducer. + +- `expected:` — the new behavior the feature should provide, paraphrased from acceptance criteria. +- `actual:` — the current state, e.g. "feature not implemented; tests skip via `.supports('')` until the supports entry lands." The scenario's `actual:` is what `scenario-run`'s "expected outcome" classifier compares against: a real failure means progress was made; a uniform skip means the prereq SDK plumbing is still pending. +- Pin platform / KAS / SDKs to the **ref where the feature will land**: + - HEAD of mainline: `platform: { source: { ref: main } }`, `sdks..version: main`. + - Feature branch: `platform: { source: { ref: feature/ecdsa-binding } }`. + - Draft PR under review: resolve to its head SHA with `gh pr view --json headRefOid` and pin `platform: { source: { ref: <40-char-SHA> } }`. SHAs are reproducible; branch names move every push. +- Only pin the component(s) the feature actually touches. Leave the rest on `lts` / `stable`. + +### Spike / unclear + +The ticket asks an open question or lacks enough concrete behavior to encode. Don't fabricate a scenario. Emit: + +``` + is a Spike (or has no specific behavior / version pins yet). Add either: + (a) the version or ref where you want behavior exercised, or + (b) a concrete pass/fail criterion (what should the test assert?) +…and re-invoke this skill. +``` + +…and stop. + +## Step 3 — Pick the id and (optionally) the branch + +- `metadata.id = ` — e.g. `DSPX-3302` → `dspx-3302`. +- Scenario file path: `xtest/scenarios/.yaml`. +- If you need a new git branch, propose `-repro` for Bugs and `-tdd` for Stories/Tasks; let the user confirm before switching. + +## Step 4 — Search for an existing pytest + +```bash +grep -rn "" xtest/test_*.py xtest/tdfs.py +``` + +Likely candidates: `test_tdfs.py` (roundtrip), `test_abac.py` (ABAC), `test_legacy.py` (golden), `test_pqc.py`. If a test already asserts the relevant behavior, reuse it via `suite.select` — no draft test needed. + +**Don't grep `xtest/sdk//cli.sh`.** Those wrappers are reusable infrastructure (versioned alongside each SDK dist) and their contents have nothing to do with scenario YAML fields. The scenario YAML doesn't need to know HOW a feature is plumbed — only WHICH pytest suite exercises it. Reading the wrappers is a waste of turns. If a feature's `supports("")` gate isn't in `tdfs.py` yet, that's a signal that supporting infrastructure has to land separately from the scenario — note it in `actual:` and move on. + +## Step 5 — Write `xtest/scenarios/.yaml` + +The schema (`otdf_sdk_mgr.schema.Scenario`) rejects unknown fields. Each pin (`PlatformPin`, `KasPin`) requires **exactly one** of `dist:`, `source:`, or `image:`. `image:` is reserved for forward-compat and rejected today — pick `dist:` or `source:`. + +Released-version pin (typical Bug scenario): + +```yaml +apiVersion: opentdf.io/v1alpha1 +kind: Scenario +metadata: + id: + title: "" + created: +instance: + metadata: { name: } + platform: { dist: v0.9.0 } + ports: { base: } + kas: + alpha: { dist: v0.9.0, mode: standard } +sdks: + encrypt: + go: { version: lts } + decrypt: + java: { version: "0.7.8" } +suite: + select: "xtest/test_tdfs.py::test_tdf_roundtrip" + containers: ztdf +expected: "..." +actual: "..." +``` + +Ref pin (TDD / HEAD / branch / PR): + +```yaml +instance: + platform: + source: { ref: main } # branch, tag, or 40-char SHA + kas: + alpha: + source: { ref: feature/ecdsa-binding } + mode: standard +sdks: + encrypt: + go: { version: main } # SdkPin.version accepts the same range of strings +``` + +Mix-and-match is fine — `platform` on `main`, `kas.alpha` on a released `dist:`, SDKs on different refs. + +Validate before reporting success: + +```bash +uv run python -m otdf_sdk_mgr.schema validate xtest/scenarios/.yaml +``` + +## Step 6 — If no existing test fits + +Draft `xtest/bug__test.py` using the `encrypt_sdk` / `decrypt_sdk` fixtures (pattern: `xtest/test_tdfs.py`). The `bug_` prefix is a historical slug applied to every scenario-tied test — feature/TDD ones use it too; don't let the name confuse you. Surface the new file in your reply for the user to review — never silently land assertions. + +For TDD tests where the underlying feature isn't yet implemented, gate participation behind `.supports("")` and call `pytest.skip(...)` when the gate fails. The scenario then runs as "all skipped" until the SDK supports entry lands, at which point the test becomes a real assertion. + +## Notes + +- `sdks.encrypt` and `sdks.decrypt` map to xtest's `--sdks-encrypt` / `--sdks-decrypt`. After PR #446 those pytest options take `sdk@version` specifiers like `go@v0.24.0`, `go@main`, or `go@*`. **Do NOT write those tokens in the YAML** — write a normal `{ version: lts }` (or any version string `otdf-sdk-mgr resolve` accepts: `v0.24.0`, `main`, an SDK-specific SHA, etc.). The `scenario-up` skill runs `otdf-sdk-mgr install scenario`, which records the resolved dist directory names in `xtest/scenarios/.installed.json`; the bridge layers (`otdf-local scenario run` and pytest's `--scenario` default in `xtest/conftest.py`) read that file to emit the right `sdk@` tokens. If you forget the install step, those commands fail with `.installed.json not found — run otdf-sdk-mgr install scenario first`. +- List the same SDK in both `encrypt` and `decrypt` maps to reproduce xtest's legacy "all pairs" mode. Listing it on only one side keeps the scenario focused (a→b without b→a). +- `instance.platform.dist` / `source.ref` and each `kas..dist` / `source.ref` need `otdf-sdk-mgr install scenario ` to have built the binary first. `scenario-up` handles that downstream. +- For matrix runs (same suite × N refs), don't author N scenarios by hand — invoke the `scenario-matrix` skill against this scenario as the base. +- One-line summary when done: report the scenario path, the new test file (if any), and the Jira link `https://virtru.atlassian.net/browse/` so the user can cross-reference. diff --git a/.claude/skills/scenario-matrix/SKILL.md b/.claude/skills/scenario-matrix/SKILL.md new file mode 100644 index 00000000..d2863749 --- /dev/null +++ b/.claude/skills/scenario-matrix/SKILL.md @@ -0,0 +1,91 @@ +--- +name: scenario-matrix +description: Given a base scenario (or a Jira ticket) plus a list of refs (PRs, branches, released versions), write one scenario file per ref so the same pytest suite runs across all of them. Use to bisect a regression across releases, validate a fix across multiple PRs, or check feature compatibility between versions. Generates files only — does not install or run them. +allowed-tools: Bash, Read, Write, Grep, Glob +--- + +# scenario-matrix + +You produce N scenario files from one base scenario, where N = the number of refs the user wants exercised. Each output scenario differs only in `instance.platform` (and optionally any KAS pins the user says should track the same ref). SDK pins are preserved unless explicitly told to vary. + +## Inputs + +- A **base**, either: + - Path to an existing `xtest/scenarios/.yaml`, OR + - A Jira ticket key — in which case invoke `scenario-from-ticket` first to produce the base, then proceed. +- A **ref list** — any combination of: + - Released versions: `v0.9.0`, `v0.8.5` + - Branch names: `main`, `feature/ecdsa-binding` + - PR numbers: `1234`, `1235` (resolved to head SHAs for reproducibility) +- (Optional) which KAS instances should track the same ref as `platform`. Default: every KAS instance in the base also tracks the ref. + +## Process + +### Step 1 — Resolve the base scenario + +- If given a path: `Read` it. +- If given a ticket key: invoke `scenario-from-ticket` against the ticket first, then `Read` the produced file. + +The base scenario provides everything except `instance.platform` (and tracked KAS pins): metadata.title becomes the title prefix, `suite` is shared across all cells, `sdks` is preserved. + +### Step 2 — Resolve each ref to a concrete value + +- Released version → use verbatim under `dist:`. Example: `v0.9.0` → `platform: { dist: v0.9.0 }`. +- Branch name → use under `source.ref:`. Example: `main` → `platform: { source: { ref: main } }`. +- PR number `N` → fetch: + + ```bash + gh pr view --json number,headRefName,headRefOid + ``` + + …and pin under `source.ref:` to the **`headRefOid`** (40-char SHA), **not** `headRefName`. Reason: branch names move on every push, SHAs don't. Record `headRefName` in the scenario title for human readability. + +### Step 3 — Emit one scenario file per ref + +Naming: `xtest/scenarios/-.yaml`. Tokens: + +- Released version: strip `v` and dots — `v0.9.0` → `v090`. +- Branch: replace `/` with `-` — `feature/ecdsa-binding` → `feature-ecdsa-binding`. +- PR: `pr` — `1234` → `pr1234`. The SHA still lives inside the file. + +Each cell scenario gets: + +- A unique `metadata.id` (`-`) matching the file basename. +- A unique `instance.metadata.name` (same as `metadata.id`). +- A unique `instance.ports.base` — start from the base's value and add `+1000` per additional cell. `scenario-up` rejects overlapping port bases between concurrent instances. +- `metadata.title` gets a ` []` suffix for at-a-glance identification. +- `instance.platform` rewritten to the resolved ref. For KAS pins that should track the same ref (default: all of them), rewrite their pin too. KAS pins the user explicitly excluded keep the base's value. +- `suite`, `sdks`, `expected`, `actual` — unchanged from the base. + +### Step 4 — Validate every file + +```bash +for f in xtest/scenarios/-*.yaml; do + uv run python -m otdf_sdk_mgr.schema validate "$f" +done +``` + +Bail (delete the just-written files) if any cell fails validation — partial matrices are confusing. + +### Step 5 — Report + +- The list of files written. +- The exact `scenario-up` / `scenario-run` chain the user can run per cell (or in a loop): + + ```bash + for f in xtest/scenarios/-*.yaml; do + name="$(basename "$f" .yaml)" + uv run otdf-sdk-mgr install scenario "$f" + uv run otdf-local instance init "$name" --from-scenario "$f" + uv run otdf-local --instance "$name" up + uv run otdf-local scenario run "$f" + uv run otdf-local --instance "$name" down + done + ``` + +## Notes + +- This skill **writes scenario files only**. It does not install artifacts, scaffold instances, or run pytest. Hand the resulting files to `scenario-up` and `scenario-run` per cell. +- For two PRs that differ in *SDK* (not platform), vary `sdks...version` instead of `platform`. Same pattern, different field — `SdkPin.version` accepts the same range of refs (`v0.24.0`, `main`, SHA). +- For a full platform × SDK matrix, generate N×M scenarios. Be prepared for long install times — each new platform ref triggers a `go build` (~30-60s first time per version); subsequent runs reuse the cached binary. +- Don't update `expected:` / `actual:` per cell unless the user specifies that one of the refs is the "known good" or "known broken" baseline. diff --git a/.claude/skills/scenario-run/SKILL.md b/.claude/skills/scenario-run/SKILL.md index 633846cf..c9a73eef 100644 --- a/.claude/skills/scenario-run/SKILL.md +++ b/.claude/skills/scenario-run/SKILL.md @@ -1,12 +1,12 @@ --- name: scenario-run -description: Execute the pytest suite declared by a scenarios.yaml against the running instance, then classify the result as "bug reproduced", "not reproduced", or "unrelated failure". Use after `scenario-up` has confirmed the instance is healthy. +description: Execute the pytest suite declared by a scenarios.yaml against the running instance, then classify the result as "expected outcome", "unexpected outcome", or "unrelated failure" against the scenario's `expected:` / `actual:` fields. Works for bug-repro scenarios, TDD/feature scenarios, and matrix runs. Use after `scenario-up` has confirmed the instance is healthy. allowed-tools: Bash, Read --- # scenario-run -You run the pytest selection declared by the scenario's `suite` block against the running instance and interpret the result in terms of the bug being investigated. +You run the pytest selection declared by the scenario's `suite` block against the running instance and interpret the result in terms of the ticket the scenario was authored for. The same three-bucket classification works for bug-repros (where "expected" means *failure that matches `actual:`*) and for TDD scenarios (where "expected" means *skip-until-feature-lands*). ## Inputs @@ -27,16 +27,21 @@ You run the pytest selection declared by the scenario's `suite` block against th 2. **Capture exit code and tail of output**. The pytest output is the source of truth; don't re-interpret. -3. **Classify**: - - **Bug reproduced** — the test failed with an assertion or stderr that matches the scenario's `actual:` field. Cite the matching line. - - **Bug NOT reproduced** — the test passed. This is meaningful: either the bug is fixed at this version combination, or the scenario doesn't capture it precisely yet. Suggest the user widen the assertion or pick a different version pin. - - **Unrelated failure** — pytest errored out (collection error, environment issue, import error, timeout). Don't claim repro success or failure; report the error and recommend a next diagnostic step. +3. **Classify** against the scenario's `expected:` and `actual:` fields: + - **Expected outcome** — the test result matches what `expected:` (or, for a bug, `actual:`) predicts. + - Bug scenario: pytest FAILED with an assertion/stderr matching `actual:`. Bug reproduced. Cite the matching line. + - TDD/feature scenario on a ref where the feature isn't landed yet: tests SKIPPED via `supports("")`. Feature gate is still pending as predicted. + - TDD/feature scenario on a ref where the feature is landed: tests PASSED. Feature works; the scenario is now a regression gate. + - **Unexpected outcome** — the test result is *not* what the scenario predicted. + - Bug scenario: pytest PASSED. Either the bug is fixed at this pin, or the scenario doesn't capture it tightly enough. Suggest widening the assertion, pinning a different ref, or marking the bug closed. + - TDD/feature scenario: tests FAILED for a reason that doesn't match `actual:`. A real bug surfaced, OR the prereq implementation work landed and the test now needs a real assertion (not a skip). Surface the actual failure to the user. + - **Unrelated failure** — pytest errored out (collection error, environment issue, import error, timeout). Don't claim outcome match either way; report the error and recommend a next diagnostic step. 4. **Record artifacts**. The pytest run leaves logs under `tests/instances//logs/`. List the relevant log files in your reply so the user can attach them to the Jira ticket. ## Output format -One-line headline (`bug reproduced` / `not reproduced` / `unrelated failure`), then a short bulleted summary: +One-line headline (`expected outcome` / `unexpected outcome` / `unrelated failure`), then a short bulleted summary: - `select:` the pytest selector - `exit_code:` the return value - `evidence:` 1-2 lines from the output that justify the classification diff --git a/.claude/skills/scenario-up/SKILL.md b/.claude/skills/scenario-up/SKILL.md index 955fce8b..8254dd61 100644 --- a/.claude/skills/scenario-up/SKILL.md +++ b/.claude/skills/scenario-up/SKILL.md @@ -1,6 +1,6 @@ --- name: scenario-up -description: Provision artifacts, scaffold the instance directory, and start the test environment for a given xtest/scenarios/.yaml. Use after `scenario-from-bug-report` (or when the user already has a scenario YAML) and wants the environment running. +description: Provision artifacts, scaffold the instance directory, and start the test environment for a given xtest/scenarios/.yaml. Use after `scenario-from-ticket` (or `scenario-matrix`, or when the user already has a scenario YAML) and wants the environment running. allowed-tools: Bash, Read --- From bd5d3fb370c39adfbc0f9623933fa1d871812a02 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Fri, 15 May 2026 21:24:46 -0400 Subject: [PATCH 03/11] feat(.claude): feature-design skill for cross-repo features (DSPX-3302) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For features (or bugs) that touch more than one OpenTDF repo — platform plus the Go / Java / JS SDKs — feature-design captures the work as a single spec at xtest/features/.yaml plus the tests-side artifacts that land first (feature_type entry in tdfs.py, scenario, draft test). The model matches the team's existing pattern: tests-side artifacts merge first, dormant under a `supports("")` gate, and each per-repo PR activates the gate by adding `supports ` to its cli.sh. PRs land async, in any order; no cross-PR lockstep needed. - `feature-design` SKILL: propose-then-iterate authoring from a Jira ticket (or free-form description). Drafts a complete spec on the first pass, asks one composite redirect question, then writes the spec + patches tdfs.py + invokes scenario-from-ticket internally to produce the dormant scenario and draft test. Bails on Spike or unclear tickets rather than fabricating. - `xtest/features/{README,CLAUDE}.md`: progressive-disclosure docs — human-facing README and agent-facing CLAUDE.md. - `xtest/README.md` gains a brief "Test artifact directories" section pointing at scenarios/ and features/. - `settings.json` + `plugin.json`: Write(xtest/features/**) allowlist, feature-design added to plugin skills array. The complementary feature-orchestrate skill (fanning out per-repo subagents to draft impl PRs in each touched repo) is a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/plugin/plugin.json | 2 + .claude/settings.json | 1 + .claude/skills/feature-design/SKILL.md | 118 +++++++++++++++++++++++++ xtest/README.md | 7 ++ xtest/features/CLAUDE.md | 13 +++ xtest/features/README.md | 14 +++ 6 files changed, 155 insertions(+) create mode 100644 .claude/skills/feature-design/SKILL.md create mode 100644 xtest/features/CLAUDE.md create mode 100644 xtest/features/README.md diff --git a/.claude/plugin/plugin.json b/.claude/plugin/plugin.json index bb4c1311..952feaea 100644 --- a/.claude/plugin/plugin.json +++ b/.claude/plugin/plugin.json @@ -4,6 +4,7 @@ "description": "Jira-ticket-driven scenarios for the OpenTDF test harness. Pulls ticket context from Jira (acli) — any ticket type, including bugs, feature stories, and PR-driven work — provisions pinned platform/KAS/SDK versions or refs (released versions, main, feature branches, PR SHAs), runs the xtest pytest suite, and tears down. Useful for QA, platform/SDK developers writing tests for new features first, and downstream first/third-party integrators.", "skills_dir": "../skills", "skills": [ + "feature-design", "scenario-from-ticket", "scenario-matrix", "scenario-up", @@ -32,6 +33,7 @@ "Bash(acli jira workitem link list *)", "Bash(acli jira project view *)", "Write(xtest/scenarios/**)", + "Write(xtest/features/**)", "Write(xtest/bug_*_test.py)", "Write(tests/instances/**)" ] diff --git a/.claude/settings.json b/.claude/settings.json index a1dba2d8..9fd70f3c 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -24,6 +24,7 @@ "Bash(acli jira board view *)", "Bash(acli jira sprint view *)", "Write(xtest/scenarios/**)", + "Write(xtest/features/**)", "Write(xtest/bug_*_test.py)", "Write(tests/instances/**)", "Write(.claude/tmp/**)" diff --git a/.claude/skills/feature-design/SKILL.md b/.claude/skills/feature-design/SKILL.md new file mode 100644 index 00000000..ebb2eed9 --- /dev/null +++ b/.claude/skills/feature-design/SKILL.md @@ -0,0 +1,118 @@ +--- +name: feature-design +description: Turn a multi-repo feature (or cross-repo bug fix) into a concrete spec at xtest/features/.yaml plus the tests-side artifacts that have to land first (scenario, draft pytest, feature_type entry in tdfs.py). Pulls Jira context, drafts a complete spec from the ticket, then iterates with the user. Use when a feature touches more than one repo (e.g. platform + Go SDK + Java SDK + JS SDK) and you want to set up the cross-repo work in one go without manually authoring each piece. +allowed-tools: Bash, Read, Write, Edit, Grep, Glob, Skill +--- + +# feature-design + +You turn a fuzzy "let's build X across the OpenTDF repos" into a concrete bundle of artifacts that pin down the tests-side work first and stage the cross-repo work for handoff to `feature-orchestrate`. + +Two ideas to internalize before reading the steps: + +1. **Tests-side artifacts land first, dormant.** The scenario + draft test + `feature_type` entry merge to `tests/main` as a regular PR. They stay "all skipped" until each SDK opens its own PR adding a `supports ` case to its `cli.sh` source — that PR's CI activates the test for that SDK. This means no cross-PR lockstep coordination; per-repo PRs land async, in any order. +2. **Propose, don't ask.** Draft a complete spec from the Jira ticket on the first pass and let the user redirect what's wrong in a single revision. Only ask one composite question. If you're missing information you can't fill in (no Jira ticket, ambiguous scope, unclear feature name), bail — don't fabricate. + +## Inputs + +- Jira key (Story/Task usually; Bug works the same way), OR a free-text description of the feature. +- (Optional) explicit list of repos to scope to, if the user wants something tighter than the default. + +## Steps + +### Step 1 — Pull the Jira context + +If a Jira key was given, run both — comments often carry scope refinements that aren't in the description: + +```bash +acli jira workitem view --fields '*all' --json +acli jira workitem comment list +``` + +Extract Issue Type, summary, description, status, and any comments about scope or implementation notes. If no Jira key, the user's description IS the spec input. + +### Step 2 — Propose a complete draft + +Draft the full spec body and the per-repo todo lists inline in your reply. Don't ask the user one field at a time — produce a complete first draft they can react to: + +- **Feature flag name** — snake_case identifier derived from the Jira summary. Becomes the `supports("")` gate string AND the `feature_type` entry in `xtest/tdfs.py`. Validate it's a valid Python identifier and doesn't collide with an existing `feature_type` member. +- **Touched repos** — default set is `tests, platform, sdk-go, sdk-java, sdk-web`. Trim or expand based on what the ticket says. Pure platform features skip the SDK repos; pure SDK-only features skip platform; `tests` is always present (the dormant scenario + tdfs.py entry has to live there). +- **Per-repo todo lists** — 2-4 bullets per repo, derived from the description plus each repo's known role: + - `tests` — register the feature in `feature_type`, author the scenario, draft the test gated on `supports("")`. + - `platform` — service-side implementation (KAS path, policy plumbing, etc.) and any env-var handling in the dev harness (e.g. honoring `XT_WITH_`). + - `sdk-go` / `sdk-java` / `sdk-web` — encrypt/decrypt path implementation, plus a `supports ` case in that SDK's `cli.sh` source. **Don't pin the version bound in the spec** — the implementing engineer sets the `awk` predicate at PR time, since the bound depends on which release will ship the impl. +- **Branch name** — `-`, the same string across every touched repo so `feature-orchestrate` (and the user) can find each repo's PR by branch alone. + +Present the draft, then ask exactly one composite question: "Anything to redirect — feature name, touched repos, todo items, branch?" Apply edits in a single revision rather than turn-by-turn. The user can always drop into plain chat if they want to think out loud — just answer them and re-invoke this skill once the design firms up. + +If no Jira key was given AND the user's description doesn't pin down a clear scope (feature flag name, touched repos, intended behavior), bail rather than fabricate: + +``` +I need either (a) a Jira Story/Task/Bug key, or (b) a description that names +the feature flag, the repos it touches, and the intended behavior. Add either +and re-invoke this skill. +``` + +### Step 3 — Write the spec + +Write `xtest/features/.yaml`. Shape (still informal — no Pydantic model yet): + +```yaml +apiVersion: opentdf.io/v1alpha1 +kind: Feature +metadata: + name: # supports() string + feature_type entry, snake_case + jira: # omit if no ticket + title: "" + created: +repos: + tests: + branch: - + todo: + - Register "" in xtest/tdfs.py feature_type + - Author scenario + draft test (via scenario-from-ticket) + platform: + branch: - + todo: [ ... ] + sdk-go: + branch: - + todo: + - Implement in the encrypt/decrypt path + - Add `supports ` case to cli.sh with version-bound awk predicate + sdk-java: { branch: ..., todo: [ ... ] } + sdk-web: { branch: ..., todo: [ ... ] } +scenarios: + - xtest/scenarios/.yaml +``` + +PR status (open/merged/CI passing) deliberately is NOT in the spec — it's auto-discovered from `gh pr list --search "head:"` per repo whenever something asks "where are we?" The spec is a declaration of intent. + +### Step 4 — Drive the tests-side artifacts + +In this order, so each step's output feeds the next: + +1. **Add the feature flag to `xtest/tdfs.py`**. Find the `feature_type` Literal alias near the top of the file. Insert the new entry alphabetically. Don't touch any `cli.sh` files — `supports ` cases land per-SDK in their own PRs. + +2. **Invoke `scenario-from-ticket`** via the Skill tool (`skill: scenario-from-ticket`, `args: `). It runs its Story/Task branch and produces the scenario + draft test gated on `supports("")` — pinning the feature-introducing components to `main` via `source.ref:`. If no Jira key was given, draft the scenario directly using the same shape (`xtest/scenarios/.yaml`). + +3. **Validate the scenario**: + + ```bash + uv run python -m otdf_sdk_mgr.schema validate xtest/scenarios/.yaml + ``` + +### Step 5 — Report + +One block summarizing: + +- The spec path (`xtest/features/.yaml`). +- The scenario + draft test paths. +- The line(s) added to `xtest/tdfs.py`. +- A one-liner suggesting the next step: `feature-orchestrate xtest/features/.yaml`. + +## Notes + +- This skill produces **tests-side artifacts only**. It does NOT create branches in other repos, does NOT open PRs, does NOT install platform/SDK builds. That's `feature-orchestrate`'s job. +- Bugs that span repos use the same shape — pass the Bug ticket key and `scenario-from-ticket`'s Bug branch fills `expected:` / `actual:` from the reproduction prose. The cross-repo gating still works: tests land dormant, each per-repo PR activates them by adding the supports case as part of the fix. +- For an existing spec being revised, read it first and propose a diff rather than a full rewrite. The tests-side artifacts (scenario, tdfs.py entry) usually shouldn't be regenerated — just edit them surgically. +- If the user starts the conversation by describing the feature in plain chat rather than invoking this skill, answer normally — re-invoke the skill once the scope firms up. Don't gatekeep. diff --git a/xtest/README.md b/xtest/README.md index 6bdfcc40..5de98942 100644 --- a/xtest/README.md +++ b/xtest/README.md @@ -122,3 +122,10 @@ pytest rm -rf tmp pytest test_tdfs.py ``` + +## Test artifact directories + +- **`scenarios/`** — Per-ticket scenario YAMLs that pin a platform / KAS / SDK topology to a specific pytest selection. Consumed by `otdf-local scenario run`. +- **`features/`** — Multi-repo feature specs: features that touch more than one OpenTDF repo (platform + SDKs) authored as a single declaration of intent. See `features/README.md`. + +Both are produced by the Claude Code skills under `tests/.claude/skills/` (`scenario-from-ticket`, `feature-design`, etc.) and can also be hand-authored. diff --git a/xtest/features/CLAUDE.md b/xtest/features/CLAUDE.md new file mode 100644 index 00000000..9f5e9a7e --- /dev/null +++ b/xtest/features/CLAUDE.md @@ -0,0 +1,13 @@ +# Agent guidance for xtest/features + +This directory is owned by two skills: + +- **`feature-design`** drafts new spec files here from a Jira ticket (or free-form description) using propose-then-iterate authoring. It also writes the tests-side artifacts that have to land first: the `feature_type` entry in `xtest/tdfs.py`, the scenario under `xtest/scenarios/`, and (if needed) a draft pytest. +- **`feature-orchestrate`** reads spec files and fans out per-repo subagents that implement the feature in each touched repo and open draft PRs. + +When you see a `xtest/features/.yaml` referenced: + +- It is canonical for the feature's flag name, scope, and per-repo todos. +- It is NOT canonical for status — query `gh pr list --search "head:"` per repo. + +Don't hand-author spec files in this directory unless you've also done what `feature-design` would do (add the entry to `feature_type` in `xtest/tdfs.py`, generate the scenario + draft test). Those side effects keep the spec consistent with the tests it depends on. diff --git a/xtest/features/README.md b/xtest/features/README.md new file mode 100644 index 00000000..2a1f5551 --- /dev/null +++ b/xtest/features/README.md @@ -0,0 +1,14 @@ +# xtest/features + +Specs for features that touch more than one OpenTDF repo (e.g. platform + Go SDK + Java SDK + JS SDK). + +Each `.yaml` captures: + +- The feature flag name — the `supports("")` gate string in `xtest/tdfs.py`. +- The Jira ticket driving the work, if any. +- Per-repo todo lists and the shared branch name to use across them. +- The scenario(s) under `xtest/scenarios/` that exercise the feature once each repo's PR lands. + +Specs are declarative — they describe intent, not status. PR state (open / merged / CI passing) is auto-discovered from `gh pr list --search "head:"` per repo, not stored here. + +See `CLAUDE.md` in this directory for how Claude Code skills produce and consume these files. From 2004ddba4870c43f225fa527873cb2c1846b6089 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Fri, 15 May 2026 21:45:00 -0400 Subject: [PATCH 04/11] fix(.claude): allow Skill tool + correct acli comment list syntax (DSPX-3302) Headless dogfooding (runs 1 and 2 of scenario-from-ticket on DSPX-2719) surfaced two real gaps: - The `Skill` tool was denied on both runs because the allowlist didn't cover it, so the body of SKILL.md wasn't injected on invocation; the agent had to manually `Read` the skill file ~25 turns in, wasting time and biasing exploration toward grepping unrelated files first. Add `Skill(*)` to settings.json and per-skill `Skill()` entries to plugin.json (the latter enumerates exactly what downstream installs get, since they shouldn't inherit a wildcard). - `acli jira workitem comment list` requires `--key ` (the subcommand differs from `view`, which takes the key positionally). Both scenario-from-ticket and feature-design had the wrong form; corrected, with a one-line note about the asymmetry so the next agent doesn't paraphrase. Verified via run-3 on DSPX-2719: 41 turns / 5m16s / $1.07 (vs run-1's 48 turns / 6m44s / $1.27). Skill tool returned success on first call, both acli commands ran cleanly, the Story/Task branch produced `source.ref: main` pins correctly (no more incorrectly defaulting to `dist: lts`), and the agent's `actual:` field correctly enumerated all three test-infrastructure prerequisites including a `with_ecdsa_binding` parameter that run-1's scenario missed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/plugin/plugin.json | 7 +++++++ .claude/settings.json | 1 + .claude/skills/feature-design/SKILL.md | 4 ++-- .claude/skills/scenario-from-ticket/SKILL.md | 4 ++-- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.claude/plugin/plugin.json b/.claude/plugin/plugin.json index 952feaea..cbf554ef 100644 --- a/.claude/plugin/plugin.json +++ b/.claude/plugin/plugin.json @@ -32,6 +32,13 @@ "Bash(acli jira workitem attachment list *)", "Bash(acli jira workitem link list *)", "Bash(acli jira project view *)", + "Skill(feature-design)", + "Skill(scenario-from-ticket)", + "Skill(scenario-matrix)", + "Skill(scenario-up)", + "Skill(scenario-run)", + "Skill(scenario-tear-down)", + "Skill(instance-status)", "Write(xtest/scenarios/**)", "Write(xtest/features/**)", "Write(xtest/bug_*_test.py)", diff --git a/.claude/settings.json b/.claude/settings.json index 9fd70f3c..0f4e65da 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -23,6 +23,7 @@ "Bash(acli jira project view *)", "Bash(acli jira board view *)", "Bash(acli jira sprint view *)", + "Skill(*)", "Write(xtest/scenarios/**)", "Write(xtest/features/**)", "Write(xtest/bug_*_test.py)", diff --git a/.claude/skills/feature-design/SKILL.md b/.claude/skills/feature-design/SKILL.md index ebb2eed9..170e650a 100644 --- a/.claude/skills/feature-design/SKILL.md +++ b/.claude/skills/feature-design/SKILL.md @@ -22,11 +22,11 @@ Two ideas to internalize before reading the steps: ### Step 1 — Pull the Jira context -If a Jira key was given, run both — comments often carry scope refinements that aren't in the description: +If a Jira key was given, run both — `view` takes the key positionally, `comment list` requires `--key`; comments often carry scope refinements that aren't in the description: ```bash acli jira workitem view --fields '*all' --json -acli jira workitem comment list +acli jira workitem comment list --key ``` Extract Issue Type, summary, description, status, and any comments about scope or implementation notes. If no Jira key, the user's description IS the spec input. diff --git a/.claude/skills/scenario-from-ticket/SKILL.md b/.claude/skills/scenario-from-ticket/SKILL.md index 1f573b59..7f154ded 100644 --- a/.claude/skills/scenario-from-ticket/SKILL.md +++ b/.claude/skills/scenario-from-ticket/SKILL.md @@ -17,11 +17,11 @@ The Jira key also becomes the working **branch name** (`-repro` for Bu ## Step 1 — Pull the Jira ticket into context -**Always run BOTH commands**. Don't skip the comment list — comments often carry the most recent reproduction status, "what changed" notes, or "fixed by PR #N" pointers that aren't in the original description: +**Always run BOTH commands** — exactly as shown; the two subcommands take the key differently (`view` is positional, `comment list` requires `--key`). Don't skip the comment list — comments often carry the most recent reproduction status, "what changed" notes, or "fixed by PR #N" pointers that aren't in the original description: ```bash acli jira workitem view --fields '*all' --json -acli jira workitem comment list +acli jira workitem comment list --key ``` From the JSON output of the first command, extract: From 1e53b1ea0e3be7a5d8e64554b28c36f6c010d7ed Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Fri, 15 May 2026 21:53:41 -0400 Subject: [PATCH 05/11] feat(otdf-sdk-mgr): schema dump CLI + xtest/schema canonical JSON Schemas (DSPX-3302) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Headless runs of scenario-from-ticket kept trying `python3 -c "from otdf_sdk_mgr.schema import Scenario; ..."` to introspect Pydantic model shape while authoring scenarios. That form isn't in the plugin's Bash allowlist (deliberately — it's arbitrary code execution), so the agent fell back to Reading schema.py source. Static, committed JSON Schemas give the same information declaratively without needing a python verb in the allowlist at all. - `otdf-sdk-mgr schema dump [--out-dir]`: writes `xtest/schema/{scenario,instance}.schema.json` from `Model.model_json_schema()`, sorted-keys + trailing newline so output is byte-stable. Add new models to `SCHEMAS` in cli_schema.py and they get picked up automatically. - `xtest/schema/` is committed with the generated files plus brief README/CLAUDE.md (progressive-disclosure, mirroring xtest/features/). - `test_schema_sync.py` parametrizes over `SCHEMAS` and fails if any committed file drifts from the live model — the safety net for "someone edited a Pydantic model without regenerating." - `scenario-from-ticket` SKILL.md Step 5 now points at `xtest/schema/scenario.schema.json` as the canonical field list. - `xtest/README.md` lists the new directory alongside `scenarios/` and `features/`. No allowlist changes needed — `Bash(uv run otdf-sdk-mgr *)` already covers the dump subcommand, and `Read` is unrestricted. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/scenario-from-ticket/SKILL.md | 2 +- otdf-sdk-mgr/src/otdf_sdk_mgr/cli.py | 2 + otdf-sdk-mgr/src/otdf_sdk_mgr/cli_schema.py | 57 +++ otdf-sdk-mgr/tests/test_schema_sync.py | 38 ++ xtest/README.md | 3 +- xtest/schema/CLAUDE.md | 8 + xtest/schema/README.md | 16 + xtest/schema/instance.schema.json | 261 +++++++++++ xtest/schema/scenario.schema.json | 443 +++++++++++++++++++ 9 files changed, 828 insertions(+), 2 deletions(-) create mode 100644 otdf-sdk-mgr/src/otdf_sdk_mgr/cli_schema.py create mode 100644 otdf-sdk-mgr/tests/test_schema_sync.py create mode 100644 xtest/schema/CLAUDE.md create mode 100644 xtest/schema/README.md create mode 100644 xtest/schema/instance.schema.json create mode 100644 xtest/schema/scenario.schema.json diff --git a/.claude/skills/scenario-from-ticket/SKILL.md b/.claude/skills/scenario-from-ticket/SKILL.md index 7f154ded..12e96067 100644 --- a/.claude/skills/scenario-from-ticket/SKILL.md +++ b/.claude/skills/scenario-from-ticket/SKILL.md @@ -92,7 +92,7 @@ Likely candidates: `test_tdfs.py` (roundtrip), `test_abac.py` (ABAC), `test_lega ## Step 5 — Write `xtest/scenarios/.yaml` -The schema (`otdf_sdk_mgr.schema.Scenario`) rejects unknown fields. Each pin (`PlatformPin`, `KasPin`) requires **exactly one** of `dist:`, `source:`, or `image:`. `image:` is reserved for forward-compat and rejected today — pick `dist:` or `source:`. +The canonical field list (titles, types, defaults, `anyOf` branches) lives in `xtest/schema/scenario.schema.json` — `Read` it whenever you need to know what's allowed. Each pin (`PlatformPin`, `KasPin`) requires **exactly one** of `dist:`, `source:`, or `image:`. `image:` is reserved for forward-compat and rejected today — pick `dist:` or `source:`. Released-version pin (typical Bug scenario): diff --git a/otdf-sdk-mgr/src/otdf_sdk_mgr/cli.py b/otdf-sdk-mgr/src/otdf_sdk_mgr/cli.py index 24148bdd..78b137c9 100644 --- a/otdf-sdk-mgr/src/otdf_sdk_mgr/cli.py +++ b/otdf-sdk-mgr/src/otdf_sdk_mgr/cli.py @@ -10,6 +10,7 @@ import typer from otdf_sdk_mgr.cli_install import install_app +from otdf_sdk_mgr.cli_schema import schema_app from otdf_sdk_mgr.cli_versions import versions_app from otdf_sdk_mgr.config import ALL_SDKS, get_sdk_dirs @@ -20,6 +21,7 @@ ) app.add_typer(install_app, name="install") +app.add_typer(schema_app, name="schema") app.add_typer(versions_app, name="versions") diff --git a/otdf-sdk-mgr/src/otdf_sdk_mgr/cli_schema.py b/otdf-sdk-mgr/src/otdf_sdk_mgr/cli_schema.py new file mode 100644 index 00000000..b3fb17b7 --- /dev/null +++ b/otdf-sdk-mgr/src/otdf_sdk_mgr/cli_schema.py @@ -0,0 +1,57 @@ +"""`otdf-sdk-mgr schema` subcommands. + +Emit canonical JSON Schemas for the Pydantic models in `otdf_sdk_mgr.schema` +so agents (and humans) can introspect the on-disk YAML formats without +running `python -c` against the package. The generated files live under +`xtest/schema/` and are kept in sync via `tests/test_schema_sync.py`. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Annotated + +import typer +from otdf_sdk_mgr.schema import Instance, Scenario + +schema_app = typer.Typer(help="Emit JSON Schemas for the scenario/instance models.") + +# (model_class, output_filename). Add new models here and `schema dump` +# will pick them up automatically. +SCHEMAS: tuple[tuple[type, str], ...] = ( + (Scenario, "scenario.schema.json"), + (Instance, "instance.schema.json"), +) + + +def render(model: type) -> str: + """Render `model.model_json_schema()` as a deterministic JSON string. + + Sorted keys and a trailing newline so byte-equality comparisons in the + sync test are stable. + """ + return json.dumps(model.model_json_schema(), indent=2, sort_keys=True) + "\n" + + +@schema_app.command("dump") +def dump( + out_dir: Annotated[ + Path, + typer.Option( + "--out-dir", + help="Directory to write *.schema.json files into.", + ), + ] = Path("xtest/schema"), +) -> None: + """Write JSON Schemas for every canonical scenario/instance model. + + Overwrites existing files. Re-run whenever a Pydantic model changes; + the committed schemas in xtest/schema/ are otherwise the source of + truth that the scenario-authoring skills read. + """ + out_dir.mkdir(parents=True, exist_ok=True) + for model, filename in SCHEMAS: + path = out_dir / filename + path.write_text(render(model), encoding="utf-8") + typer.echo(f" wrote {path}") diff --git a/otdf-sdk-mgr/tests/test_schema_sync.py b/otdf-sdk-mgr/tests/test_schema_sync.py new file mode 100644 index 00000000..addeaf8a --- /dev/null +++ b/otdf-sdk-mgr/tests/test_schema_sync.py @@ -0,0 +1,38 @@ +"""Guard that the committed JSON Schemas under xtest/schema/ stay in sync +with the live Pydantic models. + +The skills authoring scenarios read those JSON files directly to know what +fields are allowed; if a Pydantic model gains, loses, or renames a field +without a corresponding `uv run otdf-sdk-mgr schema dump`, the skills will +silently rely on a stale schema. This test makes that drift loud. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from otdf_sdk_mgr.cli_schema import SCHEMAS, render + + +def _xtest_schema_dir() -> Path: + """Locate xtest/schema/ relative to this test file. + + The repo layout puts otdf-sdk-mgr/tests/ next to xtest/, so two parents + up from this file is the tests/ root. + """ + return Path(__file__).resolve().parents[2] / "xtest" / "schema" + + +@pytest.mark.parametrize(("model", "filename"), SCHEMAS, ids=lambda v: getattr(v, "__name__", v)) +def test_committed_schema_matches_model(model: type, filename: str) -> None: + path = _xtest_schema_dir() / filename + assert path.is_file(), ( + f"Missing {path}. Run `uv run otdf-sdk-mgr schema dump` to regenerate." + ) + expected = render(model) + actual = path.read_text(encoding="utf-8") + assert actual == expected, ( + f"{path} is out of sync with {model.__name__}. " + f"Run `uv run otdf-sdk-mgr schema dump` to regenerate." + ) diff --git a/xtest/README.md b/xtest/README.md index 5de98942..0c7400fa 100644 --- a/xtest/README.md +++ b/xtest/README.md @@ -127,5 +127,6 @@ pytest test_tdfs.py - **`scenarios/`** — Per-ticket scenario YAMLs that pin a platform / KAS / SDK topology to a specific pytest selection. Consumed by `otdf-local scenario run`. - **`features/`** — Multi-repo feature specs: features that touch more than one OpenTDF repo (platform + SDKs) authored as a single declaration of intent. See `features/README.md`. +- **`schema/`** — Generated JSON Schemas for the canonical scenario / instance models. Regenerate via `uv run otdf-sdk-mgr schema dump` after editing the Pydantic models in `otdf-sdk-mgr/src/otdf_sdk_mgr/schema.py`. See `schema/README.md`. -Both are produced by the Claude Code skills under `tests/.claude/skills/` (`scenario-from-ticket`, `feature-design`, etc.) and can also be hand-authored. +The first two are produced by the Claude Code skills under `tests/.claude/skills/` (`scenario-from-ticket`, `feature-design`, etc.) and can also be hand-authored. diff --git a/xtest/schema/CLAUDE.md b/xtest/schema/CLAUDE.md new file mode 100644 index 00000000..7b215459 --- /dev/null +++ b/xtest/schema/CLAUDE.md @@ -0,0 +1,8 @@ +# Agent guidance for xtest/schema + +These JSON Schemas are the canonical reference for the on-disk YAML formats. When you need to know what fields a scenario or instance accepts: + +- **Read these files**. Don't run `python -c "from otdf_sdk_mgr.schema import ..."` to introspect — those forms aren't in the plugin's allowlist, and the JSON Schemas have the same information in declarative form (titles, types, `anyOf` for ref-vs-version pins, `additionalProperties: false`, default values, etc.). +- The files are byte-stable and sorted; safe to grep, diff, or quote. + +If a Pydantic model changes and these files drift, the user (or CI) will regenerate them via `uv run otdf-sdk-mgr schema dump`. Don't try to regenerate them yourself unless you're explicitly fixing the drift in a schema-editing PR. diff --git a/xtest/schema/README.md b/xtest/schema/README.md new file mode 100644 index 00000000..c292457a --- /dev/null +++ b/xtest/schema/README.md @@ -0,0 +1,16 @@ +# xtest/schema + +JSON Schemas for the canonical scenario / instance YAML formats. One file per Pydantic model in `otdf-sdk-mgr/src/otdf_sdk_mgr/schema.py`: + +- `scenario.schema.json` — the shape that `xtest/scenarios/.yaml` validates against. +- `instance.schema.json` — the shape of `tests/instances//instance.yaml`. + +These files are generated artifacts. To refresh them after editing a Pydantic model: + +```bash +uv run --project otdf-sdk-mgr otdf-sdk-mgr schema dump +``` + +A pytest in `otdf-sdk-mgr/tests/test_schema_sync.py` fails CI if the committed files drift from what the live models would produce. + +See `CLAUDE.md` for how Claude Code skills consume these files. diff --git a/xtest/schema/instance.schema.json b/xtest/schema/instance.schema.json new file mode 100644 index 00000000..cdf172db --- /dev/null +++ b/xtest/schema/instance.schema.json @@ -0,0 +1,261 @@ +{ + "$defs": { + "Fixtures": { + "additionalProperties": false, + "properties": { + "attributes": { + "anyOf": [ + { + "format": "path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Attributes" + }, + "policy": { + "anyOf": [ + { + "format": "path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Policy" + } + }, + "title": "Fixtures", + "type": "object" + }, + "KasPin": { + "additionalProperties": false, + "description": "Per-KAS-instance version + mode pin.", + "properties": { + "dist": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dist" + }, + "features": { + "additionalProperties": { + "type": "boolean" + }, + "title": "Features", + "type": "object" + }, + "mode": { + "default": "standard", + "enum": [ + "standard", + "key_management" + ], + "title": "Mode", + "type": "string" + }, + "source": { + "anyOf": [ + { + "$ref": "#/$defs/SourceRef" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "KasPin", + "type": "object" + }, + "Metadata": { + "additionalProperties": false, + "properties": { + "created": { + "anyOf": [ + { + "format": "date", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Id" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Title" + } + }, + "title": "Metadata", + "type": "object" + }, + "PlatformPin": { + "additionalProperties": false, + "description": "Version pin for the platform service.\n\n`dist` references a built binary at `xtest/platform/dist//service`\nproduced by `otdf-sdk-mgr install platform:`.\n`source.ref` is a git ref to build from on demand.", + "properties": { + "dist": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dist" + }, + "source": { + "anyOf": [ + { + "$ref": "#/$defs/SourceRef" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "PlatformPin", + "type": "object" + }, + "PortsConfig": { + "additionalProperties": false, + "properties": { + "base": { + "default": 8080, + "maximum": 60000, + "minimum": 1024, + "title": "Base", + "type": "integer" + } + }, + "title": "PortsConfig", + "type": "object" + }, + "SourceRef": { + "additionalProperties": false, + "properties": { + "path": { + "anyOf": [ + { + "format": "path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional local checkout path", + "title": "Path" + }, + "ref": { + "description": "Git tag, branch, or SHA", + "title": "Ref", + "type": "string" + } + }, + "required": [ + "ref" + ], + "title": "SourceRef", + "type": "object" + } + }, + "additionalProperties": false, + "description": "Standalone instance definition (one platform + N KAS).\n\nPersisted to `tests/instances//instance.yaml`. Also embedded inside\nScenario to keep the \"describe a bug-repro environment\" entry point a\nsingle file.", + "properties": { + "apiVersion": { + "const": "opentdf.io/v1alpha1", + "default": "opentdf.io/v1alpha1", + "title": "Apiversion", + "type": "string" + }, + "features": { + "additionalProperties": { + "type": "boolean" + }, + "title": "Features", + "type": "object" + }, + "fixtures": { + "$ref": "#/$defs/Fixtures" + }, + "kas": { + "additionalProperties": { + "$ref": "#/$defs/KasPin" + }, + "title": "Kas", + "type": "object" + }, + "kind": { + "const": "Instance", + "default": "Instance", + "title": "Kind", + "type": "string" + }, + "metadata": { + "$ref": "#/$defs/Metadata" + }, + "platform": { + "$ref": "#/$defs/PlatformPin" + }, + "ports": { + "$ref": "#/$defs/PortsConfig" + } + }, + "required": [ + "platform" + ], + "title": "Instance", + "type": "object" +} diff --git a/xtest/schema/scenario.schema.json b/xtest/schema/scenario.schema.json new file mode 100644 index 00000000..426e11c5 --- /dev/null +++ b/xtest/schema/scenario.schema.json @@ -0,0 +1,443 @@ +{ + "$defs": { + "Fixtures": { + "additionalProperties": false, + "properties": { + "attributes": { + "anyOf": [ + { + "format": "path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Attributes" + }, + "policy": { + "anyOf": [ + { + "format": "path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Policy" + } + }, + "title": "Fixtures", + "type": "object" + }, + "Instance": { + "additionalProperties": false, + "description": "Standalone instance definition (one platform + N KAS).\n\nPersisted to `tests/instances//instance.yaml`. Also embedded inside\nScenario to keep the \"describe a bug-repro environment\" entry point a\nsingle file.", + "properties": { + "apiVersion": { + "const": "opentdf.io/v1alpha1", + "default": "opentdf.io/v1alpha1", + "title": "Apiversion", + "type": "string" + }, + "features": { + "additionalProperties": { + "type": "boolean" + }, + "title": "Features", + "type": "object" + }, + "fixtures": { + "$ref": "#/$defs/Fixtures" + }, + "kas": { + "additionalProperties": { + "$ref": "#/$defs/KasPin" + }, + "title": "Kas", + "type": "object" + }, + "kind": { + "const": "Instance", + "default": "Instance", + "title": "Kind", + "type": "string" + }, + "metadata": { + "$ref": "#/$defs/Metadata" + }, + "platform": { + "$ref": "#/$defs/PlatformPin" + }, + "ports": { + "$ref": "#/$defs/PortsConfig" + } + }, + "required": [ + "platform" + ], + "title": "Instance", + "type": "object" + }, + "KasPin": { + "additionalProperties": false, + "description": "Per-KAS-instance version + mode pin.", + "properties": { + "dist": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dist" + }, + "features": { + "additionalProperties": { + "type": "boolean" + }, + "title": "Features", + "type": "object" + }, + "mode": { + "default": "standard", + "enum": [ + "standard", + "key_management" + ], + "title": "Mode", + "type": "string" + }, + "source": { + "anyOf": [ + { + "$ref": "#/$defs/SourceRef" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "KasPin", + "type": "object" + }, + "Metadata": { + "additionalProperties": false, + "properties": { + "created": { + "anyOf": [ + { + "format": "date", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Id" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Title" + } + }, + "title": "Metadata", + "type": "object" + }, + "PlatformPin": { + "additionalProperties": false, + "description": "Version pin for the platform service.\n\n`dist` references a built binary at `xtest/platform/dist//service`\nproduced by `otdf-sdk-mgr install platform:`.\n`source.ref` is a git ref to build from on demand.", + "properties": { + "dist": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dist" + }, + "source": { + "anyOf": [ + { + "$ref": "#/$defs/SourceRef" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "PlatformPin", + "type": "object" + }, + "PortsConfig": { + "additionalProperties": false, + "properties": { + "base": { + "default": 8080, + "maximum": 60000, + "minimum": 1024, + "title": "Base", + "type": "integer" + } + }, + "title": "PortsConfig", + "type": "object" + }, + "ScenarioSdk": { + "additionalProperties": false, + "description": "One ordered SDK selection within a scenario role.", + "properties": { + "sdk": { + "enum": [ + "go", + "java", + "js" + ], + "title": "Sdk", + "type": "string" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "For Go: \"platform\" to use the monorepo module path", + "title": "Source" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "sdk", + "version" + ], + "title": "ScenarioSdk", + "type": "object" + }, + "ScenarioSdks": { + "additionalProperties": false, + "description": "Encrypt/decrypt split mirrors xtest's --sdks-encrypt/--sdks-decrypt.\n\nSelections are ordered to preserve the eventual argv order, and are\nde-duplicated within each role by (sdk, version, source).", + "properties": { + "decrypt": { + "items": { + "$ref": "#/$defs/ScenarioSdk" + }, + "title": "Decrypt", + "type": "array" + }, + "encrypt": { + "items": { + "$ref": "#/$defs/ScenarioSdk" + }, + "title": "Encrypt", + "type": "array" + } + }, + "title": "ScenarioSdks", + "type": "object" + }, + "SourceRef": { + "additionalProperties": false, + "properties": { + "path": { + "anyOf": [ + { + "format": "path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional local checkout path", + "title": "Path" + }, + "ref": { + "description": "Git tag, branch, or SHA", + "title": "Ref", + "type": "string" + } + }, + "required": [ + "ref" + ], + "title": "SourceRef", + "type": "object" + }, + "Suite": { + "additionalProperties": false, + "description": "Pytest selection + flags.", + "properties": { + "containers": { + "description": "Forwarded to --containers as a whitespace-separated list", + "items": { + "enum": [ + "ztdf", + "ztdf-ecwrap" + ], + "type": "string" + }, + "title": "Containers", + "type": "array" + }, + "extra_args": { + "items": { + "type": "string" + }, + "title": "Extra Args", + "type": "array" + }, + "kexpr": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Forwarded to pytest -k", + "title": "Kexpr" + }, + "markers": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Forwarded to -m", + "title": "Markers" + }, + "targets": { + "description": "Positional pytest targets, e.g. test files or path::node ids", + "items": { + "type": "string" + }, + "title": "Targets", + "type": "array" + } + }, + "title": "Suite", + "type": "object" + } + }, + "additionalProperties": false, + "description": "Top-level scenarios.yaml model.\n\nComposes an Instance with SDK pins and a pytest Suite selection.", + "properties": { + "actual": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Actual" + }, + "apiVersion": { + "const": "opentdf.io/v1alpha1", + "default": "opentdf.io/v1alpha1", + "title": "Apiversion", + "type": "string" + }, + "expected": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Expected" + }, + "instance": { + "$ref": "#/$defs/Instance", + "description": "Inline instance definition" + }, + "kind": { + "const": "Scenario", + "default": "Scenario", + "title": "Kind", + "type": "string" + }, + "metadata": { + "$ref": "#/$defs/Metadata" + }, + "sdks": { + "$ref": "#/$defs/ScenarioSdks" + }, + "suite": { + "$ref": "#/$defs/Suite" + } + }, + "required": [ + "instance", + "suite" + ], + "title": "Scenario", + "type": "object" +} From 0c5d90fa888e0f1e13a865081ad5d57f967338da Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Fri, 22 May 2026 10:15:16 -0400 Subject: [PATCH 06/11] fixup play nicer with claude code permissions model --- .claude/plugin/plugin.json | 2 +- .claude/settings.json | 2 +- .claude/skills/scenario-from-ticket/SKILL.md | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude/plugin/plugin.json b/.claude/plugin/plugin.json index cbf554ef..906d0d24 100644 --- a/.claude/plugin/plugin.json +++ b/.claude/plugin/plugin.json @@ -41,7 +41,7 @@ "Skill(instance-status)", "Write(xtest/scenarios/**)", "Write(xtest/features/**)", - "Write(xtest/bug_*_test.py)", + "Write(xtest/bugs/*_test.py)", "Write(tests/instances/**)" ] }, diff --git a/.claude/settings.json b/.claude/settings.json index 0f4e65da..a14484c3 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -26,7 +26,7 @@ "Skill(*)", "Write(xtest/scenarios/**)", "Write(xtest/features/**)", - "Write(xtest/bug_*_test.py)", + "Write(xtest/bugs/*_test.py)", "Write(tests/instances/**)", "Write(.claude/tmp/**)" ] diff --git a/.claude/skills/scenario-from-ticket/SKILL.md b/.claude/skills/scenario-from-ticket/SKILL.md index 12e96067..21db97f8 100644 --- a/.claude/skills/scenario-from-ticket/SKILL.md +++ b/.claude/skills/scenario-from-ticket/SKILL.md @@ -1,6 +1,6 @@ --- name: scenario-from-ticket -description: Pull a Jira ticket of any type (Bug, Story, Task, Spike) into context via `acli jira workitem view` + `acli jira workitem comment list`, then turn it into an xtest/scenarios/.yaml manifest. Pins platform/KAS/SDKs to a released version (`dist:`), a branch or SHA (`source.ref:`), or the head of a PR — whichever matches the ticket. Optionally drafts xtest/bug__test.py when no existing pytest covers the behavior. Use when the user mentions a Jira key like DSPX-1234 (or any [PROJECT]-[NUMBER]) and wants a runnable scenario — reproducing a bug, writing a TDD test for a new feature, or validating behavior at a specific ref. +description: Pull a Jira ticket of any type (Bug, Story, Task, Spike) into context via `acli jira workitem view` + `acli jira workitem comment list`, then turn it into an xtest/scenarios/.yaml manifest. Pins platform/KAS/SDKs to a released version (`dist:`), a branch or SHA (`source.ref:`), or the head of a PR — whichever matches the ticket. Optionally drafts xtest/bugs/_test.py when no existing pytest covers the behavior. Use when the user mentions a Jira key like DSPX-1234 (or any [PROJECT]-[NUMBER]) and wants a runnable scenario — reproducing a bug, writing a TDD test for a new feature, or validating behavior at a specific ref. allowed-tools: Bash, Read, Write, Grep, Glob --- @@ -11,7 +11,7 @@ You produce a `xtest/scenarios/.yaml` manifest from a Jira Two artifacts: 1. `xtest/scenarios/.yaml` — validated against `otdf_sdk_mgr.schema.Scenario`. -2. (Optional) `xtest/bug__test.py` — only if no existing xtest pytest already exercises the behavior. The `bug_` prefix is a slug, not a type marker: feature-driven tests use it too. +2. (Optional) `xtest/bugs/_test.py` — only if no existing xtest pytest already exercises the behavior. The Jira key also becomes the working **branch name** (`-repro` for Bugs, `-tdd` for Stories/Tasks) and the scenario file's `metadata.id`. @@ -146,7 +146,7 @@ uv run python -m otdf_sdk_mgr.schema validate xtest/scenarios/.yaml ## Step 6 — If no existing test fits -Draft `xtest/bug__test.py` using the `encrypt_sdk` / `decrypt_sdk` fixtures (pattern: `xtest/test_tdfs.py`). The `bug_` prefix is a historical slug applied to every scenario-tied test — feature/TDD ones use it too; don't let the name confuse you. Surface the new file in your reply for the user to review — never silently land assertions. +Draft `xtest/bugs/_test.py` using the `encrypt_sdk` / `decrypt_sdk` fixtures (pattern: `xtest/test_tdfs.py`). Surface the new file in your reply for the user to review — never silently land assertions. For TDD tests where the underlying feature isn't yet implemented, gate participation behind `.supports("")` and call `pytest.skip(...)` when the gate fails. The scenario then runs as "all skipped" until the SDK supports entry lands, at which point the test becomes a real assertion. From a9110ab8c05bb24af96f9b9530a718d78722e185 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Fri, 22 May 2026 10:31:58 -0400 Subject: [PATCH 07/11] fixup better skill descriptions --- .claude/skills/feature-design/SKILL.md | 2 +- .claude/skills/instance-status/SKILL.md | 2 +- .claude/skills/scenario-matrix/SKILL.md | 2 +- .claude/skills/scenario-run/SKILL.md | 2 +- .claude/skills/scenario-tear-down/SKILL.md | 2 +- .claude/skills/scenario-up/SKILL.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude/skills/feature-design/SKILL.md b/.claude/skills/feature-design/SKILL.md index 170e650a..bf3854aa 100644 --- a/.claude/skills/feature-design/SKILL.md +++ b/.claude/skills/feature-design/SKILL.md @@ -1,6 +1,6 @@ --- name: feature-design -description: Turn a multi-repo feature (or cross-repo bug fix) into a concrete spec at xtest/features/.yaml plus the tests-side artifacts that have to land first (scenario, draft pytest, feature_type entry in tdfs.py). Pulls Jira context, drafts a complete spec from the ticket, then iterates with the user. Use when a feature touches more than one repo (e.g. platform + Go SDK + Java SDK + JS SDK) and you want to set up the cross-repo work in one go without manually authoring each piece. +description: Use when a feature or bug fix spans multiple repos (platform + Go/Java/JS SDKs) and you want the cross-repo spec and test artifacts set up in one pass. allowed-tools: Bash, Read, Write, Edit, Grep, Glob, Skill --- diff --git a/.claude/skills/instance-status/SKILL.md b/.claude/skills/instance-status/SKILL.md index 64bd545a..cef888d2 100644 --- a/.claude/skills/instance-status/SKILL.md +++ b/.claude/skills/instance-status/SKILL.md @@ -1,6 +1,6 @@ --- name: instance-status -description: Report which test instances exist on disk, which are running, and the health of each service. Use when the user asks "what's running" or before bringing up another scenario to avoid port collisions. +description: Use when the user asks what's running, or before starting a scenario to check for port collisions. allowed-tools: Bash, Read --- diff --git a/.claude/skills/scenario-matrix/SKILL.md b/.claude/skills/scenario-matrix/SKILL.md index d2863749..ee01aba5 100644 --- a/.claude/skills/scenario-matrix/SKILL.md +++ b/.claude/skills/scenario-matrix/SKILL.md @@ -1,6 +1,6 @@ --- name: scenario-matrix -description: Given a base scenario (or a Jira ticket) plus a list of refs (PRs, branches, released versions), write one scenario file per ref so the same pytest suite runs across all of them. Use to bisect a regression across releases, validate a fix across multiple PRs, or check feature compatibility between versions. Generates files only — does not install or run them. +description: Use when running the same test suite across multiple refs, branches, PRs, or releases — bisecting regressions or validating a fix across versions. Generates scenario files only; does not run them. allowed-tools: Bash, Read, Write, Grep, Glob --- diff --git a/.claude/skills/scenario-run/SKILL.md b/.claude/skills/scenario-run/SKILL.md index c9a73eef..c127ecee 100644 --- a/.claude/skills/scenario-run/SKILL.md +++ b/.claude/skills/scenario-run/SKILL.md @@ -1,6 +1,6 @@ --- name: scenario-run -description: Execute the pytest suite declared by a scenarios.yaml against the running instance, then classify the result as "expected outcome", "unexpected outcome", or "unrelated failure" against the scenario's `expected:` / `actual:` fields. Works for bug-repro scenarios, TDD/feature scenarios, and matrix runs. Use after `scenario-up` has confirmed the instance is healthy. +description: Use after `scenario-up` to run the scenario's test suite and classify results against its expected/actual fields. allowed-tools: Bash, Read --- diff --git a/.claude/skills/scenario-tear-down/SKILL.md b/.claude/skills/scenario-tear-down/SKILL.md index 39398c76..0838e958 100644 --- a/.claude/skills/scenario-tear-down/SKILL.md +++ b/.claude/skills/scenario-tear-down/SKILL.md @@ -1,6 +1,6 @@ --- name: scenario-tear-down -description: Stop the services for a scenario's instance and optionally delete the instance directory. Use when the user is done with a reproduction or wants to free ports/disk for a different scenario. +description: Use when the user is done with a scenario or wants to stop, clean up, or free ports/disk. allowed-tools: Bash, Read --- diff --git a/.claude/skills/scenario-up/SKILL.md b/.claude/skills/scenario-up/SKILL.md index 8254dd61..dcf1ea35 100644 --- a/.claude/skills/scenario-up/SKILL.md +++ b/.claude/skills/scenario-up/SKILL.md @@ -1,6 +1,6 @@ --- name: scenario-up -description: Provision artifacts, scaffold the instance directory, and start the test environment for a given xtest/scenarios/.yaml. Use after `scenario-from-ticket` (or `scenario-matrix`, or when the user already has a scenario YAML) and wants the environment running. +description: Use when the user has a scenario YAML and wants the environment started (before running tests). allowed-tools: Bash, Read --- From afc72ba7ab95f3f4c2b150a07a3cec84172057b4 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Fri, 22 May 2026 10:48:59 -0400 Subject: [PATCH 08/11] fixup(scenario-from-ticket): shorten description to trigger condition only Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .claude/skills/scenario-from-ticket/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/scenario-from-ticket/SKILL.md b/.claude/skills/scenario-from-ticket/SKILL.md index 21db97f8..5c8693e8 100644 --- a/.claude/skills/scenario-from-ticket/SKILL.md +++ b/.claude/skills/scenario-from-ticket/SKILL.md @@ -1,6 +1,6 @@ --- name: scenario-from-ticket -description: Pull a Jira ticket of any type (Bug, Story, Task, Spike) into context via `acli jira workitem view` + `acli jira workitem comment list`, then turn it into an xtest/scenarios/.yaml manifest. Pins platform/KAS/SDKs to a released version (`dist:`), a branch or SHA (`source.ref:`), or the head of a PR — whichever matches the ticket. Optionally drafts xtest/bugs/_test.py when no existing pytest covers the behavior. Use when the user mentions a Jira key like DSPX-1234 (or any [PROJECT]-[NUMBER]) and wants a runnable scenario — reproducing a bug, writing a TDD test for a new feature, or validating behavior at a specific ref. +description: Use when the user mentions a Jira key ([PROJECT]-[NUMBER]) and wants a scenario — bug repro, TDD test, or behavior validation at a specific ref. allowed-tools: Bash, Read, Write, Grep, Glob --- From aa46d94aace241283c12fb08c63d25e29b6c9378 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Fri, 29 May 2026 17:15:03 -0400 Subject: [PATCH 09/11] feat(.claude/skills): full overhaul + new scenario-doctor (DSPX-3302) Rewrite all seven scenario/instance skills to follow plugin-dev's skill-development conventions (third-person trigger descriptions, imperative body voice, progressive disclosure into references/ and scripts/), and close the friction items surfaced during the pure-mlkem session: - instance-status: cross-worktree port + docker-compose probe so sibling-worktree services don't go undetected. - scenario-up: bootstrap-pr-worktree.sh pre-flight for missing kas-*.pem / keys / opentdf.yaml on fresh PR worktrees, plus a partial-install guard against silent empty SDK arrays in installed.json. Documents OTDFCTL_HEADS and PLATFORM_VERSION workarounds. - scenario-run: source-build pytest fallback when installed.json is empty, and a new "assertion-stricter-than-implementation" classifier bucket for aspirational expectations. - scenario-from-ticket: auto-pin tickets with linked GitHub PRs to source.ref:; YAML templates extracted to references/yaml-templates.md. - scenario-tear-down: shared-docker probe across worktrees. - scenario-matrix: dedup workaround note pending DSPX-3417. - feature-design: lightly retouched, cross-link to scenario-doctor. - New scenario-doctor skill: diff running-vs-intended state via scripts/diff-running-vs-intended.sh; verbose recipes in references/probe-recipes.md. Inline links to DSPX-3415..3419 mark each documented workaround as temporary so it can be removed when the corresponding CLI fix lands. Co-Authored-By: Claude Sonnet 4.5 --- .claude/skills/feature-design/SKILL.md | 22 +-- .claude/skills/instance-status/SKILL.md | 78 +++++++-- .../scripts/cross-worktree-probe.sh | 57 +++++++ .claude/skills/scenario-doctor/SKILL.md | 95 +++++++++++ .../references/probe-recipes.md | 93 +++++++++++ .../scripts/diff-running-vs-intended.sh | 150 ++++++++++++++++++ .claude/skills/scenario-from-ticket/SKILL.md | 106 +++++-------- .../references/yaml-templates.md | 96 +++++++++++ .claude/skills/scenario-matrix/SKILL.md | 25 ++- .claude/skills/scenario-run/SKILL.md | 85 +++++++--- .claude/skills/scenario-tear-down/SKILL.md | 72 ++++++--- .claude/skills/scenario-up/SKILL.md | 100 +++++++++--- .../scripts/bootstrap-pr-worktree.sh | 131 +++++++++++++++ 13 files changed, 944 insertions(+), 166 deletions(-) create mode 100755 .claude/skills/instance-status/scripts/cross-worktree-probe.sh create mode 100644 .claude/skills/scenario-doctor/SKILL.md create mode 100644 .claude/skills/scenario-doctor/references/probe-recipes.md create mode 100755 .claude/skills/scenario-doctor/scripts/diff-running-vs-intended.sh create mode 100644 .claude/skills/scenario-from-ticket/references/yaml-templates.md create mode 100755 .claude/skills/scenario-up/scripts/bootstrap-pr-worktree.sh diff --git a/.claude/skills/feature-design/SKILL.md b/.claude/skills/feature-design/SKILL.md index bf3854aa..4e1482e1 100644 --- a/.claude/skills/feature-design/SKILL.md +++ b/.claude/skills/feature-design/SKILL.md @@ -1,17 +1,17 @@ --- name: feature-design -description: Use when a feature or bug fix spans multiple repos (platform + Go/Java/JS SDKs) and you want the cross-repo spec and test artifacts set up in one pass. +description: This skill should be used when the user asks to "design a cross-repo feature", "set up a feature spec", "draft a feature across platform and SDKs", "design a fix that spans repos", or wants the tests-side artifacts + per-repo todo lists set up in one pass for work that crosses platform + Go/Java/JS SDKs. Hands off to `feature-orchestrate` for the per-repo PR work. allowed-tools: Bash, Read, Write, Edit, Grep, Glob, Skill --- # feature-design -You turn a fuzzy "let's build X across the OpenTDF repos" into a concrete bundle of artifacts that pin down the tests-side work first and stage the cross-repo work for handoff to `feature-orchestrate`. +Turn a fuzzy "let's build X across the OpenTDF repos" into a concrete bundle of artifacts that pin down the tests-side work first and stage the cross-repo work for handoff to `feature-orchestrate`. Two ideas to internalize before reading the steps: -1. **Tests-side artifacts land first, dormant.** The scenario + draft test + `feature_type` entry merge to `tests/main` as a regular PR. They stay "all skipped" until each SDK opens its own PR adding a `supports ` case to its `cli.sh` source — that PR's CI activates the test for that SDK. This means no cross-PR lockstep coordination; per-repo PRs land async, in any order. -2. **Propose, don't ask.** Draft a complete spec from the Jira ticket on the first pass and let the user redirect what's wrong in a single revision. Only ask one composite question. If you're missing information you can't fill in (no Jira ticket, ambiguous scope, unclear feature name), bail — don't fabricate. +1. **Tests-side artifacts land first, dormant.** The scenario + draft test + `feature_type` entry merge to `tests/main` as a regular PR. They stay "all skipped" until each SDK opens its own PR adding a `supports ` case to its `cli.sh` source — that PR's CI activates the test for that SDK. No cross-PR lockstep coordination; per-repo PRs land async, in any order. +2. **Propose, don't ask.** Draft a complete spec from the Jira ticket on the first pass and let the user redirect what's wrong in a single revision. Only ask one composite question. If information is missing that can't be filled in (no Jira ticket, ambiguous scope, unclear feature name), bail — don't fabricate. ## Inputs @@ -22,7 +22,7 @@ Two ideas to internalize before reading the steps: ### Step 1 — Pull the Jira context -If a Jira key was given, run both — `view` takes the key positionally, `comment list` requires `--key`; comments often carry scope refinements that aren't in the description: +If a Jira key was given, run both — `view` takes the key positionally, `comment list` requires `--key`; comments often carry scope refinements: ```bash acli jira workitem view --fields '*all' --json @@ -33,17 +33,17 @@ Extract Issue Type, summary, description, status, and any comments about scope o ### Step 2 — Propose a complete draft -Draft the full spec body and the per-repo todo lists inline in your reply. Don't ask the user one field at a time — produce a complete first draft they can react to: +Draft the full spec body and the per-repo todo lists inline in the reply. Don't ask the user one field at a time — produce a complete first draft they can react to: - **Feature flag name** — snake_case identifier derived from the Jira summary. Becomes the `supports("")` gate string AND the `feature_type` entry in `xtest/tdfs.py`. Validate it's a valid Python identifier and doesn't collide with an existing `feature_type` member. - **Touched repos** — default set is `tests, platform, sdk-go, sdk-java, sdk-web`. Trim or expand based on what the ticket says. Pure platform features skip the SDK repos; pure SDK-only features skip platform; `tests` is always present (the dormant scenario + tdfs.py entry has to live there). -- **Per-repo todo lists** — 2-4 bullets per repo, derived from the description plus each repo's known role: +- **Per-repo todo lists** — 2–4 bullets per repo: - `tests` — register the feature in `feature_type`, author the scenario, draft the test gated on `supports("")`. - `platform` — service-side implementation (KAS path, policy plumbing, etc.) and any env-var handling in the dev harness (e.g. honoring `XT_WITH_`). - `sdk-go` / `sdk-java` / `sdk-web` — encrypt/decrypt path implementation, plus a `supports ` case in that SDK's `cli.sh` source. **Don't pin the version bound in the spec** — the implementing engineer sets the `awk` predicate at PR time, since the bound depends on which release will ship the impl. - **Branch name** — `-`, the same string across every touched repo so `feature-orchestrate` (and the user) can find each repo's PR by branch alone. -Present the draft, then ask exactly one composite question: "Anything to redirect — feature name, touched repos, todo items, branch?" Apply edits in a single revision rather than turn-by-turn. The user can always drop into plain chat if they want to think out loud — just answer them and re-invoke this skill once the design firms up. +Present the draft, then ask exactly one composite question: "Anything to redirect — feature name, touched repos, todo items, branch?" Apply edits in a single revision rather than turn-by-turn. The user can always drop into plain chat if they want to think out loud — answer normally and re-invoke this skill once the design firms up. If no Jira key was given AND the user's description doesn't pin down a clear scope (feature flag name, touched repos, intended behavior), bail rather than fabricate: @@ -93,7 +93,7 @@ In this order, so each step's output feeds the next: 1. **Add the feature flag to `xtest/tdfs.py`**. Find the `feature_type` Literal alias near the top of the file. Insert the new entry alphabetically. Don't touch any `cli.sh` files — `supports ` cases land per-SDK in their own PRs. -2. **Invoke `scenario-from-ticket`** via the Skill tool (`skill: scenario-from-ticket`, `args: `). It runs its Story/Task branch and produces the scenario + draft test gated on `supports("")` — pinning the feature-introducing components to `main` via `source.ref:`. If no Jira key was given, draft the scenario directly using the same shape (`xtest/scenarios/.yaml`). +2. **Invoke `scenario-from-ticket`** via the Skill tool (`skill: scenario-from-ticket`, `args: `). It runs its Story/Task branch and produces the scenario + draft test gated on `supports("")`. If no Jira key was given, draft the scenario directly using the same shape (`xtest/scenarios/.yaml`). 3. **Validate the scenario**: @@ -108,11 +108,11 @@ One block summarizing: - The spec path (`xtest/features/.yaml`). - The scenario + draft test paths. - The line(s) added to `xtest/tdfs.py`. -- A one-liner suggesting the next step: `feature-orchestrate xtest/features/.yaml`. +- A one-liner suggesting next steps: `feature-orchestrate xtest/features/.yaml` (for per-repo PR work), or `scenario-up xtest/scenarios/.yaml` + `scenario-doctor ` (to bring the dormant scenario up against `main` and confirm "all skipped" baseline before SDK work starts). ## Notes - This skill produces **tests-side artifacts only**. It does NOT create branches in other repos, does NOT open PRs, does NOT install platform/SDK builds. That's `feature-orchestrate`'s job. - Bugs that span repos use the same shape — pass the Bug ticket key and `scenario-from-ticket`'s Bug branch fills `expected:` / `actual:` from the reproduction prose. The cross-repo gating still works: tests land dormant, each per-repo PR activates them by adding the supports case as part of the fix. -- For an existing spec being revised, read it first and propose a diff rather than a full rewrite. The tests-side artifacts (scenario, tdfs.py entry) usually shouldn't be regenerated — just edit them surgically. +- For an existing spec being revised, read it first and propose a diff rather than a full rewrite. The tests-side artifacts (scenario, tdfs.py entry) usually shouldn't be regenerated — edit them surgically. - If the user starts the conversation by describing the feature in plain chat rather than invoking this skill, answer normally — re-invoke the skill once the scope firms up. Don't gatekeep. diff --git a/.claude/skills/instance-status/SKILL.md b/.claude/skills/instance-status/SKILL.md index cef888d2..9467c2b4 100644 --- a/.claude/skills/instance-status/SKILL.md +++ b/.claude/skills/instance-status/SKILL.md @@ -1,36 +1,80 @@ --- name: instance-status -description: Use when the user asks what's running, or before starting a scenario to check for port collisions. +description: This skill should be used when the user asks "what's running", "check ports", "show instance status", "list test instances", "are any services up", or before invoking `scenario-up` to detect port collisions (including from sibling git worktrees). For deeper "does the running env match what the scenario yaml says" verification, defer to `scenario-doctor` instead. allowed-tools: Bash, Read --- # instance-status -You give the user a snapshot of all test instances in this checkout: what's defined, what's running, and whether each service is healthy. +Report a snapshot of test environment state: which instances are defined in this worktree, what is actually listening on the conventional ports (regardless of which worktree owns it), and whether each service is healthy. Surface port collisions before they bite `scenario-up`. ## Process -1. **List instances on disk**: +### Step 0 — Cross-worktree probe (always first) - ```bash - uv run otdf-local instance ls --json - ``` +`otdf-local instance ls` is scoped to the current worktree's `tests/instances/`. Sibling worktrees' running services are invisible to that listing but very much listening on the host's ports. Probe the host directly: - Each entry includes `name`, `platform` version, `ports_base`, and the `kas:` keys. Flag any two instances that share a `ports_base` — they cannot run concurrently. +```bash +bash ${CLAUDE_PLUGIN_ROOT:-.}/skills/instance-status/scripts/cross-worktree-probe.sh +``` -2. **For each instance**, check service status: +Output is tab-separated, one row per listener: - ```bash - uv run otdf-local --instance status --json - ``` +``` +port proto pid cwd kind +8080 tcp 28656 /Users/.../reproducing-things/... platform +8585 tcp 28684 /Users/.../reproducing-things/... kas +compose docker - main compose-project +``` - Each service reports `running`, `healthy`, and the bound port. Don't run all instances in parallel — iterate; a status query is cheap. +Carry forward two facts into the rest of the report: +1. Which of the conventional ports (`8080`, `8181..8686`, `5432`, `8888`) are occupied. +2. The owning `cwd` for each — when it differs from the current worktree, label the line as **foreign** in the final summary so the user knows to tear that down before re-using the port. -3. **Summarize**: - - A short table per instance: service → port → state. - - Flag any unhealthy service with the path to its log (e.g. `tests/instances//logs/kas-alpha.log`). - - Mention port conflicts if two instances would collide on `ports.base`. +### Step 1 — List instances on disk + +```bash +uv run otdf-local instance ls --json +``` + +Each entry includes `name`, `platform` version, `ports_base`, and the `kas:` keys. Two checks: +- Flag any two local instances that share a `ports_base` — they cannot run concurrently. +- Note: this listing is **worktree-scoped**. The cross-worktree probe from Step 0 is the source of truth for "what's actually using port X." + +### Step 2 — Per-instance status + +For each local instance from Step 1: + +```bash +uv run otdf-local --instance status --json +``` + +Each service reports `running`, `healthy`, and the bound port. Run sequentially (a status query is cheap; parallel adds nothing). Cross-reference each "running" entry with Step 0's table — if the port shows `kind=platform` but the owning `cwd` is a sibling worktree, the local instance's status reading is misleading (it's reporting on someone else's binary). + +### Step 3 — Summarize + +Compose the reply in this order: +1. **Cross-worktree listeners** — the Step 0 table, with each foreign row labeled. Skip if no ports are occupied. +2. **Local instances** — one short block per instance: service → port → state (running/healthy). Mark each row's port as `local` or `foreign` based on Step 0's owner. +3. **Port-base collisions** — any pair of local instances with the same `ports_base`, recommending a re-init: `uv run otdf-local instance init --from-scenario --ports-base `. +4. **Unhealthy rows** — each with the path to its log (e.g. `tests/instances//logs/kas-alpha.log`). + +Skip empty sections rather than print "(none)". ## When ports collide -`otdf-local instance init` warns about this at creation time but does not enforce it. If you see two instances with the same `ports_base`, recommend the user reassign one via `uv run otdf-local instance init --from-scenario --ports-base ` (or hand-edit the `instance.yaml`). +If Step 0 shows a foreign listener on a port the user is about to use, two paths: +- Tear down the foreign instance first. Find the owning worktree from the `cwd` column; cd there and run `OTDF_LOCAL_INSTANCE_NAME= uv run otdf-local down`. +- Or pick a different ports base for the new instance: `uv run otdf-local instance init --from-scenario --ports-base 9080` (or any free base). + +If `otdf-local instance init` warns about a local collision at creation time, it doesn't enforce it; re-running with `--ports-base ` is the fix. + +## What this skill does NOT do + +For the deeper question "is the binary serving port X actually the one my scenario YAML pinned?", use `scenario-doctor` — that skill diffs the running service's `.version` sidecar against the instance's expected pin. `instance-status` reports *what's listening*, not *whether it's the right thing*. + +## Additional Resources + +### Script + +- **`scripts/cross-worktree-probe.sh`** — surveys conventional ports + docker compose projects across all worktrees on this host. Always run first in Step 0. Tab-separated stdout (header on line 1). diff --git a/.claude/skills/instance-status/scripts/cross-worktree-probe.sh b/.claude/skills/instance-status/scripts/cross-worktree-probe.sh new file mode 100755 index 00000000..baed227f --- /dev/null +++ b/.claude/skills/instance-status/scripts/cross-worktree-probe.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# cross-worktree-probe.sh — surface listeners on opentdf test ports across ALL worktrees. +# +# `otdf-local instance ls` is scoped to one worktree's tests/instances/; sibling +# worktrees' running services are invisible to it. This script probes the host +# directly so the agent can detect cross-worktree port collisions before +# `scenario-up` (or explain why a port appears "free" from one CLI but isn't). +# +# Output: tab-separated, one record per line, header on first line. +# Columns: port proto pid cwd kind +# kind ∈ { platform | kas | docker-keycloak | docker-postgres | unknown } + +set -u + +PORTS=(8080 8181 8282 8383 8484 8585 8686 5432 8888) + +printf 'port\tproto\tpid\tcwd\tkind\n' + +for port in "${PORTS[@]}"; do + # -F to use parseable format; -n -P to skip name resolution (faster) + while IFS= read -r line; do + [[ -z "$line" ]] && continue + pid="$(awk '{print $2}' <<<"$line")" + [[ -z "$pid" || "$pid" == "PID" ]] && continue + + cwd="$(lsof -p "$pid" -d cwd -Fn 2>/dev/null | awk '/^n/ { sub(/^n/,""); print; exit }')" + cwd="${cwd:-?}" + + cmd="$(ps -o command= -p "$pid" 2>/dev/null | head -c 200)" + case "$port" in + 8080) kind=platform ;; + 8181|8282|8383|8484|8585|8686) kind=kas ;; + 8888) kind=docker-keycloak ;; + 5432) kind=docker-postgres ;; + *) kind=unknown ;; + esac + # Refine kind if process command says otherwise (e.g. a misbound port). + case "$cmd" in + *"/service "*|*"/service start"*) kind=platform ;; + *opentdf-kas*|*"kas start"*) kind=kas ;; + esac + + printf '%s\ttcp\t%s\t%s\t%s\n' "$port" "$pid" "$cwd" "$kind" + done < <(lsof -nP -iTCP:"$port" -sTCP:LISTEN 2>/dev/null | tail -n +2) +done + +# Docker compose projects sharing the host docker daemon — names like +# `-keycloak-1`, `-opentdfdb-1`. The project is whatever +# directory `docker compose` was invoked from (typically a worktree's +# xtest/platform/src// directory). +docker ps --format '{{.Names}}' 2>/dev/null | while IFS= read -r name; do + [[ -z "$name" ]] && continue + case "$name" in + *-keycloak-*) printf 'compose\tdocker\t-\t%s\tcompose-project\n' "${name%-keycloak-*}" ;; + *-opentdfdb-*) printf 'compose\tdocker\t-\t%s\tcompose-project\n' "${name%-opentdfdb-*}" ;; + esac +done | sort -u diff --git a/.claude/skills/scenario-doctor/SKILL.md b/.claude/skills/scenario-doctor/SKILL.md new file mode 100644 index 00000000..f55bd58f --- /dev/null +++ b/.claude/skills/scenario-doctor/SKILL.md @@ -0,0 +1,95 @@ +--- +name: scenario-doctor +description: This skill should be used when the user asks to "verify my instance", "doctor my scenario", "is my environment healthy", "does the running platform match the scenario", or to diagnose a flaky test run by confirming the expected binaries / keys / health are actually live. Cross-checks running state against `tests/instances//instance.yaml`. +allowed-tools: Bash, Read +--- + +# scenario-doctor + +Cross-check what an instance's `instance.yaml` *intends* against what is *actually* running, and produce a verdict the user can act on. Most "the test failed for a weird reason" sessions trace back to a drift here — the wrong binary serving the port, stale keys in the worktree, an extra service from a sibling worktree squatting on a port, or a process owned by a different worktree's `otdf-local`. + +## Inputs + +- Instance name (typically the lowercased Jira key, e.g. `dspx-3302`). If a scenario YAML path is provided instead, read its `instance.metadata.name` and proceed. + +## Process + +### Step 1 — Run the diff script + +```bash +bash ${CLAUDE_PLUGIN_ROOT:-.}/skills/scenario-doctor/scripts/diff-running-vs-intended.sh +``` + +Output is tab-separated, one row per service: + +``` +service port expected_sha actual_sha health status +platform 8080 08ab3a0aef27 08ab3a0aef27 200 MATCH +km1 8585 08ab3a0aef27 - down NOT-RUNNING +alpha 8181 v090... a1b2c3d4... 200 WRONG-BINARY +``` + +`status` enumerates: +- `MATCH` — expected ref matches the running binary's `.version` sha, health is 200. +- `WRONG-BINARY` — service is up but serving from a different ref than the instance pins. Often means a sibling worktree's environment is shadowing this one's expected binary. +- `NOT-RUNNING` — port is empty; `otdf-local --instance up` (or `restart `) is needed. +- `EXTRA` — port is occupied by a service the instance didn't declare. Usually a leftover from another instance/worktree. +- `NO-PIN` — instance manifest didn't pin this service (skip). + +### Step 2 — Verify seed files + +For each unique worktree referenced in the diff output (parse the `expected_sha` rows back to `.version` sidecars), invoke the bootstrap script in dry-run inspection mode — re-using `scenario-up`'s probe so the file checks stay consistent: + +```bash +bash ${CLAUDE_PLUGIN_ROOT:-.}/skills/scenario-up/scripts/bootstrap-pr-worktree.sh +``` + +Treat any `state=empty-dir` or `state=missing action=manual-required` row as a real problem worth surfacing — those are the silent-failure shapes (Docker bind-mount stubs, ungenerated dev keys). + +### Step 3 — Assign a verdict + +Roll up Steps 1–2 into one of three colors. Lead the reply with the verdict; users scan for this. + +- **GREEN** — every declared service is `MATCH` + 200, no `EXTRA` rows, every seed file `ok`. Nothing for the user to do. +- **YELLOW** — at least one `WRONG-BINARY`, `EXTRA`, or `empty-dir`/`missing` row, but the instance is *running*. Tests may pass or fail unpredictably until the drift is resolved. +- **RED** — at least one declared service is `NOT-RUNNING`. Tests cannot succeed; recommend `otdf-local --instance up` (fresh start) or per-service `restart`. + +### Step 4 — Per-row remedy + +For each non-`MATCH` row, emit a one-line remedy alongside the diff table: + +| Status | Remedy | +|---|---| +| `NOT-RUNNING` | `otdf-local --instance up` (full) or `restart ` (single service) | +| `WRONG-BINARY` | Identify owning PID's worktree via `lsof -p -d cwd`. If sibling worktree: tear that down first (`OTDF_LOCAL_INSTANCE_NAME= otdf-local down`). If same worktree, stale binary: `otdf-sdk-mgr install tip --ref platform` then restart. | +| `EXTRA` | Confirm the PID and its cwd. Stop owning instance or kill the stale PID. | +| `empty-dir` / `missing` | Re-run `bootstrap-pr-worktree.sh` (Phase B of `scenario-up`) or hand-run `bash .github/scripts/init-temp-keys.sh` in the worktree. | + +### Step 5 — Output + +Compose the reply in this order: verdict line, diff table (Step 1 output, lightly formatted), seed-file table (Step 2 output, only rows that aren't `ok`), per-row remedy bullets. Skip empty sections rather than print "(none)" — agents pattern-match on what's present. + +## When this skill triggers + +After any of: +- A surprising pytest result (skip when expected to pass, or pass when expected to skip-then-fail). +- The user asking "what's running" with the implication that they suspect drift, not a simple `instance ls` query (that's `instance-status`'s job). +- Returning to a long-lived branch where the running environment might be stale. + +For the simpler "what's defined / what's listening here" question without the diff-against-intent angle, defer to `instance-status`. + +## Limits + +- The script depends on the `.version` sidecar that `otdf-sdk-mgr install platform` writes. Binaries placed under `xtest/platform/dist/` by other means won't be diffable; they show as `expected_sha=?`. +- `yq` is preferred for parsing `instance.yaml`; the script falls back to grep when `yq` isn't installed. Coverage of the fallback is narrower — install `yq` for accurate KAS-list extraction in unusual manifests. +- Cross-worktree owner detection uses `lsof -p -d cwd`. Containers running services (rare today) wouldn't surface that way; the verdict would still flag the port collision via `EXTRA`, just without an owning-worktree label. + +## Additional Resources + +### Script + +- **`scripts/diff-running-vs-intended.sh`** — automates Step 1's expected-vs-actual diff. Takes one positional argument: the instance name. Tab-separated stdout. + +### Reference files + +- **`references/probe-recipes.md`** — verbose shell snippets for ad-hoc inspection: resolving a PID to its worktree, comparing `.version` sidecars by hand, detecting Docker-created empty-dir stubs, listing compose-project owners. Read this when the script's output is ambiguous or the user wants the underlying mechanics. diff --git a/.claude/skills/scenario-doctor/references/probe-recipes.md b/.claude/skills/scenario-doctor/references/probe-recipes.md new file mode 100644 index 00000000..bcb5440b --- /dev/null +++ b/.claude/skills/scenario-doctor/references/probe-recipes.md @@ -0,0 +1,93 @@ +# Probe recipes + +Shell snippets the `scenario-doctor` skill uses (or recommends users run by hand) to inspect running services and compare against `instance.yaml` expectations. `scripts/diff-running-vs-intended.sh` automates the common path; reach for these recipes when the script's output needs deeper investigation or the agent has to answer an ad-hoc "what's actually running on port X?" question. + +## Identify what's listening on the conventional ports + +```bash +lsof -nP -iTCP:8080,8181,8282,8383,8484,8585,8686,5432,8888 -sTCP:LISTEN +``` + +Reads as `COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME`. The PID is what to chase next. + +## Resolve a PID to the binary path and its source worktree + +```bash +ps -o command= -p +# → /Users/.../tests/xtest/platform/dist//service start --config-file … +``` + +The binary lives at `…/dist//service`. Its sibling `.version` file records the source worktree and git SHA that built it: + +```bash +cat "$(dirname "$(ps -o command= -p | awk '{print $1}')")/.version" +# ref=refs/pull/3537/head +# sha=08ab3a0aef… +# worktree=/Users/.../DSPX-3302-02-platform-installer/tests/xtest/platform/src/refs--pull--3537--head +``` + +Whatever the `worktree=` line says is the directory the service binary loads keys / templates relative to — useful when investigating "platform started but says key X not found." + +## Resolve a PID to its cwd (often a different worktree than the agent) + +```bash +lsof -p -d cwd -Fn | awk '/^n/ { sub(/^n/,""); print; exit }' +``` + +A PID's cwd reveals which worktree initiated the service. Use this to spot cases where the agent thinks it's in worktree A but a sibling worktree B owns the running binary. + +## Compare expected ref to actual ref for an instance + +```bash +inst="tests/instances//instance.yaml" +yq -r '.platform.source.ref // .platform.dist' "$inst" # expected +ps -o command= -p "$(lsof -nP -iTCP:8080 -sTCP:LISTEN | awk 'NR>1 {print $2; exit}')" \ + | awk '{print $1}' | xargs -I{} cat "$(dirname {})/.version" # actual +``` + +Diff the two. Mismatch → either the instance is being served by a stale binary or by a binary from a different worktree. + +## Health pings + +```bash +curl -fsS http://localhost:8080/healthz # platform +curl -fsS http://localhost:8585/healthz # km1 +``` + +Returns `{"status":"SERVING"}` (HTTP 200) when healthy. Anything else is a real failure — check the corresponding log under `tests/instances//logs/`. + +## Confirm seed files exist (not Docker-created empty dirs) + +```bash +worktree="…/xtest/platform/src/" +for f in kas-private.pem kas-cert.pem kas-ec-private.pem kas-ec-cert.pem \ + keys/ca.jks keys/localhost.crt keys/localhost.key opentdf.yaml; do + if [[ -f "$worktree/$f" ]]; then + printf 'ok\t%s\n' "$f" + elif [[ -d "$worktree/$f" ]]; then + printf 'empty-dir\t%s\n' "$f" # Docker bind-mount left a stub directory + else + printf 'missing\t%s\n' "$f" + fi +done +``` + +`empty-dir` is the silent-failure shape: Docker auto-created the path as a directory because the source file didn't exist when compose first ran. Removing the stub and re-bootstrapping (via `scripts/bootstrap-pr-worktree.sh` or `init-temp-keys.sh`) is the fix. + +## Detect cross-worktree docker compose sharing + +```bash +docker ps --format '{{.Names}}' | grep -E -- '-keycloak-|-opentdfdb-' \ + | sed -E 's/-(keycloak|opentdfdb)-[0-9]+$//' | sort -u +``` + +Lists every compose-project name currently sharing the docker daemon. Each project is typically named after the directory `docker compose` was invoked from (i.e. a worktree's `xtest/platform/src//`). When multiple projects appear, `otdf-local --instance X down` will *not* stop docker — another instance is still using it. + +## Kill a stale platform/KAS process (use with care) + +```bash +pkill -9 -f "/dist//service start" # platform +pkill -9 -f "/dist//service kas start" # KAS +``` + +Prefer `otdf-local --instance down` when possible; `pkill` is the escape hatch when the instance owning the process doesn't match the worktree the agent is in (so `otdf-local` won't manage it cleanly). diff --git a/.claude/skills/scenario-doctor/scripts/diff-running-vs-intended.sh b/.claude/skills/scenario-doctor/scripts/diff-running-vs-intended.sh new file mode 100755 index 00000000..b7ff9f11 --- /dev/null +++ b/.claude/skills/scenario-doctor/scripts/diff-running-vs-intended.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# diff-running-vs-intended.sh — verify that running services match what an +# instance.yaml claims they should be. +# +# Usage: diff-running-vs-intended.sh +# +# Walks the per-instance manifest at tests/instances//instance.yaml, +# resolves each pin (dist:/source.ref:) to its expected dist directory and +# git SHA via the .version sidecar, then compares against what's actually +# listening on the conventional ports. +# +# Output: tab-separated, header on first line. +# Columns: service port expected_sha actual_sha health status +# status ∈ { MATCH | WRONG-BINARY | NOT-RUNNING | EXTRA | NO-PIN } +# health ∈ { 200 | | down | - } + +set -u + +if [[ $# -lt 1 ]]; then + echo "usage: $(basename "$0") " >&2 + exit 2 +fi + +name="$1" + +# Resolve repo root by walking up from CWD until we find tests/instances/. +dir="$PWD" +while [[ "$dir" != "/" && ! -d "$dir/tests/instances" && ! -d "$dir/instances" ]]; do + dir="$(dirname "$dir")" +done +[[ -d "$dir/tests/instances" ]] && INST_ROOT="$dir/tests/instances" +[[ -d "$dir/instances" ]] && INST_ROOT="$dir/instances" +: "${INST_ROOT:?could not locate tests/instances/ above $PWD}" + +inst="$INST_ROOT/$name/instance.yaml" +[[ -f "$inst" ]] || { echo "no instance.yaml at $inst" >&2; exit 2; } + +PLATFORM_DIST="${INST_ROOT%/instances}/xtest/platform/dist" + +# Port map (matches otdf-local's Ports defaults). +declare -A PORT_OF=( + [platform]=8080 + [alpha]=8181 + [beta]=8282 + [gamma]=8383 + [delta]=8484 + [km1]=8585 + [km2]=8686 +) + +# Helper: resolve a pin (ref or dist) to expected_sha by reading .version. +expected_sha_for() { + local pin="$1" # could be a ref like 'main' or 'pr:3537', or a dist slug + local slug + for cand in "$PLATFORM_DIST"/*/; do + [[ -f "$cand/.version" ]] || continue + if grep -Fq "ref=$pin" "$cand/.version" \ + || grep -Fq "ref=refs/pull/${pin#pr:}/head" "$cand/.version" \ + || [[ "$(basename "${cand%/}")" == "$pin" ]]; then + awk -F= '/^sha=/ {print substr($2,1,12); exit}' "$cand/.version" + return + fi + done + echo "?" +} + +# Helper: actual_sha by inspecting the running binary at $port. +actual_sha_for_port() { + local port="$1" + local pid binary version + pid="$(lsof -nP -iTCP:"$port" -sTCP:LISTEN 2>/dev/null | awk 'NR>1 {print $2; exit}')" + [[ -z "$pid" ]] && { echo ""; return; } + binary="$(ps -o command= -p "$pid" 2>/dev/null | awk '{print $1}')" + [[ -f "$binary" ]] || { echo "?"; return; } + version="$(dirname "$binary")/.version" + [[ -f "$version" ]] && awk -F= '/^sha=/ {print substr($2,1,12); exit}' "$version" || echo "?" +} + +# Helper: http health code. +health_of() { + local port="$1" + curl -fsS -o /dev/null -w '%{http_code}' "http://localhost:$port/healthz" 2>/dev/null || echo down +} + +# Extract pins from instance.yaml. yq optional; fall back to grep. +get_pin() { + local field="$1" # e.g. .platform OR .kas.km1 + if command -v yq >/dev/null 2>&1; then + yq -r "($field.source.ref? // $field.dist? // \"\")" "$inst" + else + # Crude fallback: pull the first ref|dist under the section name. + awk -v sec="${field#.}" ' + $0 ~ "^"sec":" {f=1; next} + f && /^[^[:space:]]/ {f=0} + f && /(ref|dist):/ {gsub(/[",{}]/,""); for(i=1;i<=NF;i++) if($i ~ /^(ref|dist):/) {print $(i+1); exit}} + ' "$inst" + fi +} + +printf 'service\tport\texpected_sha\tactual_sha\thealth\tstatus\n' + +# Platform first. +pin="$(get_pin .platform)" +exp="$(expected_sha_for "$pin")" +act="$(actual_sha_for_port 8080)" +hc="$(health_of 8080)" +if [[ -z "$pin" ]]; then status=NO-PIN +elif [[ -z "$act" ]]; then status=NOT-RUNNING +elif [[ "$act" == "$exp" ]]; then status=MATCH +else status=WRONG-BINARY; fi +printf 'platform\t8080\t%s\t%s\t%s\t%s\n' "${exp:-?}" "${act:--}" "$hc" "$status" + +# KAS instances declared in the manifest. Build the list either via yq or +# the grep fallback. +kas_names=() +if command -v yq >/dev/null 2>&1; then + while IFS= read -r n; do kas_names+=("$n"); done < <(yq -r '.kas | keys[]' "$inst") +else + while IFS= read -r n; do kas_names+=("$n"); done < <( + awk '/^kas:/{f=1;next} f && /^[a-z0-9_-]+:/{gsub(":",""); print $1} f && /^[^[:space:]]/{f=0}' "$inst" + ) +fi + +for kas in "${kas_names[@]}"; do + port="${PORT_OF[$kas]:-?}" + pin="$(get_pin ".kas.$kas")" + exp="$(expected_sha_for "$pin")" + act="$(actual_sha_for_port "$port")" + hc="$([[ "$port" != "?" ]] && health_of "$port" || echo -)" + if [[ -z "$pin" ]]; then status=NO-PIN + elif [[ -z "$act" ]]; then status=NOT-RUNNING + elif [[ "$act" == "$exp" ]]; then status=MATCH + else status=WRONG-BINARY; fi + printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$kas" "$port" "${exp:-?}" "${act:--}" "$hc" "$status" +done + +# Detect EXTRA services: any port in PORT_OF that's listening but wasn't +# declared in instance.yaml. +declared_ports=(8080) +for k in "${kas_names[@]}"; do declared_ports+=("${PORT_OF[$k]:-0}"); done +for svc in "${!PORT_OF[@]}"; do + port="${PORT_OF[$svc]}" + in_declared=0 + for d in "${declared_ports[@]}"; do [[ "$d" == "$port" ]] && in_declared=1 && break; done + [[ "$in_declared" == 1 ]] && continue + act="$(actual_sha_for_port "$port")" + [[ -z "$act" ]] && continue + hc="$(health_of "$port")" + printf '%s\t%s\t-\t%s\t%s\tEXTRA\n' "$svc" "$port" "$act" "$hc" +done diff --git a/.claude/skills/scenario-from-ticket/SKILL.md b/.claude/skills/scenario-from-ticket/SKILL.md index 5c8693e8..5743e5f6 100644 --- a/.claude/skills/scenario-from-ticket/SKILL.md +++ b/.claude/skills/scenario-from-ticket/SKILL.md @@ -1,12 +1,12 @@ --- name: scenario-from-ticket -description: Use when the user mentions a Jira key ([PROJECT]-[NUMBER]) and wants a scenario — bug repro, TDD test, or behavior validation at a specific ref. +description: This skill should be used when the user mentions a Jira key (e.g. "DSPX-3302") and asks to "create a scenario", "write a repro from the ticket", "make a TDD scenario", "draft a test for this bug", or otherwise turn a ticket into a `xtest/scenarios/.yaml` manifest plus (optionally) a draft pytest. Handles Bugs, Stories/Tasks (TDD), and Spikes via Issue Type. allowed-tools: Bash, Read, Write, Grep, Glob --- # scenario-from-ticket -You produce a `xtest/scenarios/.yaml` manifest from a Jira ticket. The same skill handles bugs, features (TDD), and exploratory work — the *Issue Type* field on the ticket selects which way the rest of this skill behaves. +Produce a `xtest/scenarios/.yaml` manifest from a Jira ticket. The same skill handles bugs, features (TDD), and exploratory work — the *Issue Type* field on the ticket selects which way the rest of the skill behaves. Two artifacts: @@ -17,7 +17,7 @@ The Jira key also becomes the working **branch name** (`-repro` for Bu ## Step 1 — Pull the Jira ticket into context -**Always run BOTH commands** — exactly as shown; the two subcommands take the key differently (`view` is positional, `comment list` requires `--key`). Don't skip the comment list — comments often carry the most recent reproduction status, "what changed" notes, or "fixed by PR #N" pointers that aren't in the original description: +Run **both** commands — they take the key differently (`view` is positional, `comment list` requires `--key`). Don't skip the comment list; comments often carry the most recent reproduction status, "what changed" notes, or "fixed by PR #N" pointers that aren't in the original description: ```bash acli jira workitem view --fields '*all' --json @@ -31,11 +31,24 @@ From the JSON output of the first command, extract: - **Description** — version numbers, KAS topology, container types, feature flags, acceptance criteria typically live here. - **Status** — Backlog / In Progress / Done affects whether the scenario is forward-looking (TDD on Backlog) or retroactive (regression gate on Done). -From the comments, pull any "tested at version X" / "reproduces on platform Y" / "fixed by PR #N" annotations into your mental model. +From the comments, pull any "tested at version X" / "reproduces on platform Y" / "fixed by PR #N" annotations into context. -If the ticket references attached logs, screenshots, or linked PRs, list them via `acli jira workitem attachment list ` and `acli jira workitem link list ` and call them out in your reply. +If the ticket references attached logs, screenshots, or linked PRs, list them: -**Permitted Jira writes**: only `acli jira workitem comment create ...` (to post a reproduction-status update if the user asks). Everything else — `edit`, `transition`, `assign`, `archive`, `delete`, `link create`, `watcher add` — is explicitly disallowed by the plugin's permissions; if the user wants those actions, instruct them to run the command themselves. +```bash +acli jira workitem attachment list +acli jira workitem link list +``` + +**Linked-PR auto-pin.** When `link list` returns a PR URL (e.g. `https://github.com/opentdf/platform/pull/3537`), resolve it immediately and prefer it over the headless default `dist: lts`: + +```bash +gh pr view --repo --json number,headRefName,headRefOid +``` + +Use the 40-char `headRefOid` as `source.ref:` for the platform/KAS pin. Branch names move on every push; SHAs don't. Record the branch name in `metadata.title` for human readability. See `references/yaml-templates.md` → "PR pin via Jira link" for the full template. + +**Permitted Jira writes**: only `acli jira workitem comment create ...` (to post a reproduction-status update if the user asks). Everything else — `edit`, `transition`, `assign`, `archive`, `delete`, `link create`, `watcher add` — is explicitly disallowed by the plugin's permissions; if the user wants those actions, instruct them to run the command. ## Step 2 — Branch on Issue Type @@ -47,23 +60,19 @@ The ticket describes a behavior that should work but doesn't. - `actual:` — what actually happens, including the exact error message if the ticket quotes one. - Pin platform / KAS / SDKs to the **versions where the bug reproduces**. Usually `dist:` against a released version. Mixed-version topologies (e.g. platform `v0.9.0` + km1 `v0.9.0-rc.2`) are common and the schema supports them. -If the description doesn't name versions, ask the user. (A headless agent has no user — in that case default to `dist: lts` everywhere and call out the assumption in `actual:`.) +If the description doesn't name versions: prefer a linked PR (from Step 1) if any; otherwise ask the user. A headless agent with no PR and no version pin defaults to `dist: lts` and calls out the assumption in `actual:`. ### Story / Task (feature work, TDD-style) -The ticket describes a behavior the user wants to *add*. The scenario you produce is a forward-looking regression gate, not a bug reproducer. +The ticket describes a behavior the user wants to *add*. The scenario is a forward-looking regression gate, not a bug reproducer. -- `expected:` — the new behavior the feature should provide, paraphrased from acceptance criteria. -- `actual:` — the current state, e.g. "feature not implemented; tests skip via `.supports('')` until the supports entry lands." The scenario's `actual:` is what `scenario-run`'s "expected outcome" classifier compares against: a real failure means progress was made; a uniform skip means the prereq SDK plumbing is still pending. -- Pin platform / KAS / SDKs to the **ref where the feature will land**: - - HEAD of mainline: `platform: { source: { ref: main } }`, `sdks..version: main`. - - Feature branch: `platform: { source: { ref: feature/ecdsa-binding } }`. - - Draft PR under review: resolve to its head SHA with `gh pr view --json headRefOid` and pin `platform: { source: { ref: <40-char-SHA> } }`. SHAs are reproducible; branch names move every push. -- Only pin the component(s) the feature actually touches. Leave the rest on `lts` / `stable`. +- `expected:` — the new behavior, paraphrased from acceptance criteria. +- `actual:` — current state, e.g. "feature not implemented; tests skip via `.supports('')` until the supports entry lands." `scenario-run`'s "expected outcome" classifier compares against this — a real failure means progress; a uniform skip means the prereq SDK plumbing is still pending. +- Pin platform / KAS / SDKs to the **ref where the feature will land**: linked PR (from Step 1) if any, else HEAD of mainline (`source: { ref: main }`), else a feature branch the user names. Only pin components the feature actually touches; leave the rest on `lts` / `stable`. ### Spike / unclear -The ticket asks an open question or lacks enough concrete behavior to encode. Don't fabricate a scenario. Emit: +The ticket asks an open question or lacks enough concrete behavior to encode. Don't fabricate. Emit: ``` is a Spike (or has no specific behavior / version pins yet). Add either: @@ -78,7 +87,7 @@ The ticket asks an open question or lacks enough concrete behavior to encode. Do - `metadata.id = ` — e.g. `DSPX-3302` → `dspx-3302`. - Scenario file path: `xtest/scenarios/.yaml`. -- If you need a new git branch, propose `-repro` for Bugs and `-tdd` for Stories/Tasks; let the user confirm before switching. +- If a new git branch is needed, propose `-repro` for Bugs and `-tdd` for Stories/Tasks; let the user confirm before switching. ## Step 4 — Search for an existing pytest @@ -88,55 +97,11 @@ grep -rn "" xtest/test_*.py xtest/tdfs.py Likely candidates: `test_tdfs.py` (roundtrip), `test_abac.py` (ABAC), `test_legacy.py` (golden), `test_pqc.py`. If a test already asserts the relevant behavior, reuse it via `suite.select` — no draft test needed. -**Don't grep `xtest/sdk//cli.sh`.** Those wrappers are reusable infrastructure (versioned alongside each SDK dist) and their contents have nothing to do with scenario YAML fields. The scenario YAML doesn't need to know HOW a feature is plumbed — only WHICH pytest suite exercises it. Reading the wrappers is a waste of turns. If a feature's `supports("")` gate isn't in `tdfs.py` yet, that's a signal that supporting infrastructure has to land separately from the scenario — note it in `actual:` and move on. +**Don't grep `xtest/sdk//cli.sh`.** Those wrappers are reusable infrastructure (versioned alongside each SDK dist) and their contents have nothing to do with scenario YAML fields. The scenario doesn't need to know HOW a feature is plumbed — only WHICH pytest suite exercises it. If a feature's `supports("")` gate isn't in `tdfs.py` yet, that's a signal that supporting infrastructure has to land separately from the scenario — note it in `actual:` and move on. ## Step 5 — Write `xtest/scenarios/.yaml` -The canonical field list (titles, types, defaults, `anyOf` branches) lives in `xtest/schema/scenario.schema.json` — `Read` it whenever you need to know what's allowed. Each pin (`PlatformPin`, `KasPin`) requires **exactly one** of `dist:`, `source:`, or `image:`. `image:` is reserved for forward-compat and rejected today — pick `dist:` or `source:`. - -Released-version pin (typical Bug scenario): - -```yaml -apiVersion: opentdf.io/v1alpha1 -kind: Scenario -metadata: - id: - title: "" - created: -instance: - metadata: { name: } - platform: { dist: v0.9.0 } - ports: { base: } - kas: - alpha: { dist: v0.9.0, mode: standard } -sdks: - encrypt: - go: { version: lts } - decrypt: - java: { version: "0.7.8" } -suite: - select: "xtest/test_tdfs.py::test_tdf_roundtrip" - containers: ztdf -expected: "..." -actual: "..." -``` - -Ref pin (TDD / HEAD / branch / PR): - -```yaml -instance: - platform: - source: { ref: main } # branch, tag, or 40-char SHA - kas: - alpha: - source: { ref: feature/ecdsa-binding } - mode: standard -sdks: - encrypt: - go: { version: main } # SdkPin.version accepts the same range of strings -``` - -Mix-and-match is fine — `platform` on `main`, `kas.alpha` on a released `dist:`, SDKs on different refs. +Templates (released-version, ref-pin, mixed-mode, PR-pin-via-Jira-link) live in **`references/yaml-templates.md`**. Pick the matching shape, copy, and fill in. Validate before reporting success: @@ -146,14 +111,19 @@ uv run python -m otdf_sdk_mgr.schema validate xtest/scenarios/.yaml ## Step 6 — If no existing test fits -Draft `xtest/bugs/_test.py` using the `encrypt_sdk` / `decrypt_sdk` fixtures (pattern: `xtest/test_tdfs.py`). Surface the new file in your reply for the user to review — never silently land assertions. +Draft `xtest/bugs/_test.py` using the `encrypt_sdk` / `decrypt_sdk` fixtures (pattern: `xtest/test_tdfs.py`). Surface the new file in the reply for the user to review — never silently land assertions. -For TDD tests where the underlying feature isn't yet implemented, gate participation behind `.supports("")` and call `pytest.skip(...)` when the gate fails. The scenario then runs as "all skipped" until the SDK supports entry lands, at which point the test becomes a real assertion. +For TDD tests where the underlying feature isn't yet implemented, gate participation behind `.supports("")` and call `pytest.skip(...)` when the gate fails. The scenario then runs as "all skipped" until the SDK supports entry lands. ## Notes -- `sdks.encrypt` and `sdks.decrypt` map to xtest's `--sdks-encrypt` / `--sdks-decrypt`. After PR #446 those pytest options take `sdk@version` specifiers like `go@v0.24.0`, `go@main`, or `go@*`. **Do NOT write those tokens in the YAML** — write a normal `{ version: lts }` (or any version string `otdf-sdk-mgr resolve` accepts: `v0.24.0`, `main`, an SDK-specific SHA, etc.). The `scenario-up` skill runs `otdf-sdk-mgr install scenario`, which records the resolved dist directory names in `xtest/scenarios/.installed.json`; the bridge layers (`otdf-local scenario run` and pytest's `--scenario` default in `xtest/conftest.py`) read that file to emit the right `sdk@` tokens. If you forget the install step, those commands fail with `.installed.json not found — run otdf-sdk-mgr install scenario first`. +- `sdks.encrypt` and `sdks.decrypt` map to xtest's `--sdks-encrypt` / `--sdks-decrypt`. Pytest options take `sdk@version` specifiers (e.g. `go@v0.24.0`). **Do NOT write those tokens in the YAML** — write a normal `{ version: lts }` (or any version string `otdf-sdk-mgr resolve` accepts). `scenario-up` runs `otdf-sdk-mgr install scenario`, which records the resolved dist names in `xtest/scenarios/.installed.json`; the bridge layers read that file to emit the right `sdk@` tokens. - List the same SDK in both `encrypt` and `decrypt` maps to reproduce xtest's legacy "all pairs" mode. Listing it on only one side keeps the scenario focused (a→b without b→a). -- `instance.platform.dist` / `source.ref` and each `kas..dist` / `source.ref` need `otdf-sdk-mgr install scenario ` to have built the binary first. `scenario-up` handles that downstream. - For matrix runs (same suite × N refs), don't author N scenarios by hand — invoke the `scenario-matrix` skill against this scenario as the base. -- One-line summary when done: report the scenario path, the new test file (if any), and the Jira link `https://virtru.atlassian.net/browse/` so the user can cross-reference. +- Hand the resulting scenario to `scenario-up` next. + +## Additional Resources + +### Reference files + +- **`references/yaml-templates.md`** — every scenario YAML shape: released-version (Bug), ref-pin (TDD/HEAD), mixed-mode (new platform + shipped KAS), PR-pin-via-Jira-link (recommended when `acli link list` returned an opentdf PR), plus the validation command. Read this when writing or reviewing a scenario manifest. diff --git a/.claude/skills/scenario-from-ticket/references/yaml-templates.md b/.claude/skills/scenario-from-ticket/references/yaml-templates.md new file mode 100644 index 00000000..24b2640d --- /dev/null +++ b/.claude/skills/scenario-from-ticket/references/yaml-templates.md @@ -0,0 +1,96 @@ +# Scenario YAML templates + +The canonical field list (titles, types, defaults, `anyOf` branches) lives in `xtest/schema/scenario.schema.json`. Read it whenever a question about an allowed field arises. Each pin (`PlatformPin`, `KasPin`) requires **exactly one** of `dist:`, `source:`, or `image:`. `image:` is reserved for forward-compat and is rejected today — pick `dist:` or `source:`. + +## Released-version pin (typical Bug scenario) + +Use when reproducing a bug on a published release. + +```yaml +apiVersion: opentdf.io/v1alpha1 +kind: Scenario +metadata: + id: + title: "" + created: +instance: + metadata: { name: } + platform: { dist: v0.9.0 } + ports: { base: } + kas: + alpha: { dist: v0.9.0, mode: standard } +sdks: + encrypt: + go: { version: lts } + decrypt: + java: { version: "0.7.8" } +suite: + select: "xtest/test_tdfs.py::test_tdf_roundtrip" + containers: ztdf +expected: "..." +actual: "..." +``` + +## Ref pin (TDD / HEAD / branch / PR) + +Use when the behavior under test lives on an unreleased branch, an in-flight PR, or HEAD. For PRs, prefer the 40-char `headRefOid` from `gh pr view --json headRefOid` over the branch name — SHAs are immutable, branches move. + +```yaml +instance: + platform: + source: { ref: main } # branch, tag, 40-char SHA, or pr:N + kas: + alpha: + source: { ref: feature/ecdsa-binding } + mode: standard +sdks: + encrypt: + go: { version: main } # SdkPin.version accepts the same range of strings +``` + +## Mixed-mode (platform on a ref, KAS on a release) + +Use when validating that an unreleased platform interoperates with shipped KAS deployments (or vice versa). + +```yaml +instance: + platform: + source: { ref: pr:3537 } # in-flight PR + kas: + alpha: { dist: v0.9.0, mode: standard } # shipped KAS + km1: + source: { ref: pr:3537 } # KAS that needs PR changes + mode: key_management +sdks: + encrypt: { go: { version: main } } + decrypt: { go: { version: lts } } # old client decrypting new platform output +``` + +## PR pin via Jira link (recommended for Story/Task tickets) + +When `acli jira workitem link list ` returned a linked PR (URL like `github.com/opentdf/platform/pull/`), resolve and pin to the head SHA: + +```bash +gh pr view --repo opentdf/platform --json number,headRefName,headRefOid +# → { "number": 3537, "headRefName": "DSPX-3383-post-quantum-kem", "headRefOid": "08ab3a0a…" } +``` + +Then in the scenario: + +```yaml +metadata: + title: " [opentdf/platform#3537 @ DSPX-3383-post-quantum-kem]" +instance: + platform: + source: { ref: 08ab3a0a... } # immutable 40-char SHA +``` + +Record the branch name in `metadata.title` for human readability; the SHA is what `otdf-sdk-mgr install` uses. + +## Validation + +Always validate before reporting success: + +```bash +uv run python -m otdf_sdk_mgr.schema validate xtest/scenarios/.yaml +``` diff --git a/.claude/skills/scenario-matrix/SKILL.md b/.claude/skills/scenario-matrix/SKILL.md index ee01aba5..9e4bbd90 100644 --- a/.claude/skills/scenario-matrix/SKILL.md +++ b/.claude/skills/scenario-matrix/SKILL.md @@ -1,18 +1,18 @@ --- name: scenario-matrix -description: Use when running the same test suite across multiple refs, branches, PRs, or releases — bisecting regressions or validating a fix across versions. Generates scenario files only; does not run them. +description: This skill should be used when the user asks to "run the same suite across multiple versions", "bisect a regression across releases", "validate a fix across PRs", "generate a scenario matrix", or wants the same test suite exercised at N different platform / SDK refs. Generates scenario files only; does not run them — hand the output to `scenario-up` / `scenario-run` per cell. allowed-tools: Bash, Read, Write, Grep, Glob --- # scenario-matrix -You produce N scenario files from one base scenario, where N = the number of refs the user wants exercised. Each output scenario differs only in `instance.platform` (and optionally any KAS pins the user says should track the same ref). SDK pins are preserved unless explicitly told to vary. +Produce N scenario files from one base scenario, where N is the number of refs the user wants exercised. Each output scenario differs only in `instance.platform` (and optionally any KAS pins the user says should track the same ref). SDK pins are preserved unless explicitly told to vary. ## Inputs - A **base**, either: - Path to an existing `xtest/scenarios/.yaml`, OR - - A Jira ticket key — in which case invoke `scenario-from-ticket` first to produce the base, then proceed. + - A Jira ticket key — invoke `scenario-from-ticket` first to produce the base, then proceed. - A **ref list** — any combination of: - Released versions: `v0.9.0`, `v0.8.5` - Branch names: `main`, `feature/ecdsa-binding` @@ -26,7 +26,7 @@ You produce N scenario files from one base scenario, where N = the number of ref - If given a path: `Read` it. - If given a ticket key: invoke `scenario-from-ticket` against the ticket first, then `Read` the produced file. -The base scenario provides everything except `instance.platform` (and tracked KAS pins): metadata.title becomes the title prefix, `suite` is shared across all cells, `sdks` is preserved. +The base scenario provides everything except `instance.platform` (and tracked KAS pins): `metadata.title` becomes the title prefix, `suite` is shared across all cells, `sdks` is preserved. ### Step 2 — Resolve each ref to a concrete value @@ -54,7 +54,7 @@ Each cell scenario gets: - A unique `instance.metadata.name` (same as `metadata.id`). - A unique `instance.ports.base` — start from the base's value and add `+1000` per additional cell. `scenario-up` rejects overlapping port bases between concurrent instances. - `metadata.title` gets a ` []` suffix for at-a-glance identification. -- `instance.platform` rewritten to the resolved ref. For KAS pins that should track the same ref (default: all of them), rewrite their pin too. KAS pins the user explicitly excluded keep the base's value. +- `instance.platform` rewritten to the resolved ref. For KAS pins that should track the same ref (default: all of them), rewrite their pin too. Pins the user explicitly excluded keep the base's value. - `suite`, `sdks`, `expected`, `actual` — unchanged from the base. ### Step 4 — Validate every file @@ -87,5 +87,18 @@ Bail (delete the just-written files) if any cell fails validation — partial ma - This skill **writes scenario files only**. It does not install artifacts, scaffold instances, or run pytest. Hand the resulting files to `scenario-up` and `scenario-run` per cell. - For two PRs that differ in *SDK* (not platform), vary `sdks...version` instead of `platform`. Same pattern, different field — `SdkPin.version` accepts the same range of refs (`v0.24.0`, `main`, SHA). -- For a full platform × SDK matrix, generate N×M scenarios. Be prepared for long install times — each new platform ref triggers a `go build` (~30-60s first time per version); subsequent runs reuse the cached binary. +- For a full platform × SDK matrix, generate N×M scenarios. Be prepared for long install times — each new platform ref triggers a `go build` (~30–60s first time per version); subsequent runs reuse the cached binary. - Don't update `expected:` / `actual:` per cell unless the user specifies that one of the refs is the "known good" or "known broken" baseline. + +### Pre-install shared refs (workaround for [DSPX-3417](https://virtru.atlassian.net/browse/DSPX-3417)) + +`otdf-sdk-mgr install scenario` currently rebuilds the platform once per pin even when N pins share a ref — so an N-cell matrix on the same platform ref triggers N rebuilds, each ~30–60s. Workaround: + +```bash +# Build once. +uv run otdf-sdk-mgr install tip --ref platform +# Then run the per-cell loop in Step 5; each `install scenario` will reuse +# the cached binary instead of rebuilding. +``` + +When DSPX-3417's dedup ships, the workaround becomes unnecessary. diff --git a/.claude/skills/scenario-run/SKILL.md b/.claude/skills/scenario-run/SKILL.md index c127ecee..f141e961 100644 --- a/.claude/skills/scenario-run/SKILL.md +++ b/.claude/skills/scenario-run/SKILL.md @@ -1,12 +1,12 @@ --- name: scenario-run -description: Use after `scenario-up` to run the scenario's test suite and classify results against its expected/actual fields. +description: This skill should be used when the user asks to "run the scenario", "run the scenario tests", "execute the scenario suite", "test the scenario", or after `scenario-up` to invoke the pytest selection declared by `xtest/scenarios/.yaml` and classify the result against the scenario's `expected:` / `actual:` fields. allowed-tools: Bash, Read --- # scenario-run -You run the pytest selection declared by the scenario's `suite` block against the running instance and interpret the result in terms of the ticket the scenario was authored for. The same three-bucket classification works for bug-repros (where "expected" means *failure that matches `actual:`*) and for TDD scenarios (where "expected" means *skip-until-feature-lands*). +Invoke the pytest selection declared by the scenario's `suite` block against the running instance, then classify the result in terms of the ticket the scenario was authored for. The same four-bucket classification works for bug-repros (where "expected" means *failure that matches `actual:`*), TDD scenarios (where "expected" means *skip-until-feature-lands*), and assertion drift between draft tests and what the implementation actually emits. ## Inputs @@ -15,34 +15,73 @@ You run the pytest selection declared by the scenario's `suite` block against th ## Process -1. **Invoke the runner**: +### Step 1 — Invoke the runner - ```bash - uv run otdf-local scenario run xtest/scenarios/.yaml - ``` +```bash +uv run otdf-local scenario run xtest/scenarios/.yaml +``` - This translates the scenario's `suite.select`, `suite.containers`, `suite.markers`, and `sdks.{encrypt,decrypt}` into the equivalent `pytest --sdks-encrypt ... --sdks-decrypt ... --containers ...` invocation under `xtest/` with `OTDF_LOCAL_INSTANCE_NAME` set. SDK tokens are emitted in xtest's `sdk@version` form (see PR #446) — the resolved version names come from the sibling `.installed.json` that `otdf-sdk-mgr install scenario` writes. +This translates the scenario's `suite.select`, `suite.containers`, `suite.markers`, and `sdks.{encrypt,decrypt}` into the equivalent `pytest --sdks-encrypt … --sdks-decrypt … --containers …` invocation under `xtest/` with `OTDF_LOCAL_INSTANCE_NAME` set. SDK tokens are emitted in xtest's `sdk@version` form; the resolved version names come from the sibling `.installed.json`. - If `scenario run` exits with `Error: .installed.json not found`, the user skipped the install step. Tell them to run `uv run otdf-sdk-mgr install scenario ` (or re-run `scenario-up`) before retrying. +Failure modes: +- `Error: .installed.json not found` — the user skipped Step 1 of `scenario-up`. Run `uv run otdf-sdk-mgr install scenario ` first. +- `installed.json` is present but `sdks.encrypt` / `sdks.decrypt` are empty arrays despite the scenario declaring SDK pins — this is the **source-built SDK** case; fall back to a direct pytest invocation (see Step 1b). -2. **Capture exit code and tail of output**. The pytest output is the source of truth; don't re-interpret. +### Step 1b — Source-build fallback -3. **Classify** against the scenario's `expected:` and `actual:` fields: - - **Expected outcome** — the test result matches what `expected:` (or, for a bug, `actual:`) predicts. - - Bug scenario: pytest FAILED with an assertion/stderr matching `actual:`. Bug reproduced. Cite the matching line. - - TDD/feature scenario on a ref where the feature isn't landed yet: tests SKIPPED via `supports("")`. Feature gate is still pending as predicted. - - TDD/feature scenario on a ref where the feature is landed: tests PASSED. Feature works; the scenario is now a regression gate. - - **Unexpected outcome** — the test result is *not* what the scenario predicted. - - Bug scenario: pytest PASSED. Either the bug is fixed at this pin, or the scenario doesn't capture it tightly enough. Suggest widening the assertion, pinning a different ref, or marking the bug closed. - - TDD/feature scenario: tests FAILED for a reason that doesn't match `actual:`. A real bug surfaced, OR the prereq implementation work landed and the test now needs a real assertion (not a skip). Surface the actual failure to the user. - - **Unrelated failure** — pytest errored out (collection error, environment issue, import error, timeout). Don't claim outcome match either way; report the error and recommend a next diagnostic step. +When the scenario pins source-built SDKs (`source.ref` rather than `version`), `otdf-local scenario run` today produces an empty `--sdks-*` argv. Invoke pytest directly instead: -4. **Record artifacts**. The pytest run leaves logs under `tests/instances//logs/`. List the relevant log files in your reply so the user can attach them to the Jira ticket. +```bash +cd xtest +set -a +eval "$(cd ../otdf-local && OTDF_LOCAL_INSTANCE_NAME= uv run otdf-local env)" +source test.env +set +a + +# Map each source-pinned SDK to its dist slug under xtest/sdk//dist/. +# For platform PR #N, the slug is typically `refs--pull----head`. +PLATFORM_VERSION= OTDFCTL_HEADS='[""]' \ + uv run pytest \ + --sdks-encrypt @ \ + --sdks-decrypt @ \ + --containers +``` + +`PLATFORM_VERSION` and `OTDFCTL_HEADS` defaults are noted in `scenario-up`; pull them from there or from the scenario's source-build env knobs section. This fallback is temporary — tracked at [DSPX-3417](https://virtru.atlassian.net/browse/DSPX-3417) (scenario YAML accepting source builds) and [DSPX-3418](https://virtru.atlassian.net/browse/DSPX-3418) (`OTDFCTL_HEADS` → CLI flag). + +### Step 2 — Capture exit code and tail of output + +The pytest output is the source of truth; do not re-interpret it. Save the last ~60 lines for the evidence quote in the classification. + +### Step 3 — Classify against `expected:` and `actual:` + +Pick exactly one bucket. Lead the reply with the bucket name; users skim for it. + +- **Expected outcome** — the test result matches what `expected:` (or, for a bug, `actual:`) predicts. + - Bug scenario: pytest FAILED with an assertion or stderr matching `actual:`. Bug reproduced; cite the matching line. + - TDD/feature scenario on a ref where the feature isn't landed: tests SKIPPED via `supports("")`. Gate still pending as predicted. + - TDD/feature scenario on a ref where the feature is landed: tests PASSED. The scenario is now a regression gate. + +- **Unexpected outcome** — the test result is *not* what the scenario predicted. + - Bug scenario: pytest PASSED. Either the bug is fixed at this pin, or the scenario doesn't capture it tightly enough. Suggest widening the assertion, pinning a different ref, or closing the bug. + - TDD/feature scenario: tests FAILED for a reason that doesn't match `actual:`. A real bug surfaced, OR the prereq implementation landed and the test now needs a real assertion rather than a skip. + +- **Assertion-stricter-than-implementation** — pytest FAILED on a specific assertion whose expected value is *aspirational* (drawn from a PR description, spec, or RFC) rather than current behaviour. Diagnostic: one assertion compares a single real field to a single concrete value, both legitimate, and they simply don't match. The implementation works correctly under a *different* contract than the test encodes. Action: relax the assertion to the observed value (record both old and new in a comment so the intent is preserved), file a follow-up if the strict value is load-bearing. This is what catches "PR description said KAO type is `mlkem-wrapped` but the binary emits `wrapped`." + +- **Unrelated failure** — pytest errored out (collection error, environment issue, import error, timeout). Don't claim outcome match either way; report the error and recommend a next diagnostic step. If services look wrong, defer to `scenario-doctor` for a state diff. + +### Step 4 — Record artifacts + +Pytest leaves logs under `tests/instances//logs/`. List the relevant per-service log paths in the reply so the user can attach them to the Jira ticket. ## Output format -One-line headline (`expected outcome` / `unexpected outcome` / `unrelated failure`), then a short bulleted summary: -- `select:` the pytest selector -- `exit_code:` the return value -- `evidence:` 1-2 lines from the output that justify the classification +One-line headline naming the bucket, then a short bulleted summary: +- `select:` the pytest selector that ran +- `exit_code:` the pytest return value +- `evidence:` 1–2 lines from the output that justify the classification - `logs:` paths to the relevant per-service logs + +## When to defer + +If the failure looks environmental (services missing, ports drift, stale binary) rather than test-substantive, hand off to `scenario-doctor` for a state-vs-intent diff before iterating on the test or scenario. diff --git a/.claude/skills/scenario-tear-down/SKILL.md b/.claude/skills/scenario-tear-down/SKILL.md index 0838e958..a37b5fbc 100644 --- a/.claude/skills/scenario-tear-down/SKILL.md +++ b/.claude/skills/scenario-tear-down/SKILL.md @@ -1,42 +1,78 @@ --- name: scenario-tear-down -description: Use when the user is done with a scenario or wants to stop, clean up, or free ports/disk. +description: This skill should be used when the user asks to "tear down the scenario", "stop the instance", "shut down the test environment", "clean up the scenario", "free the ports", or is done with a scenario and wants services stopped and (optionally) on-disk state removed. allowed-tools: Bash, Read --- # scenario-tear-down -You stop a running scenario cleanly and optionally remove its on-disk state. +Stop a running scenario cleanly and optionally remove its on-disk state. Confirm shared resources (docker stacks across worktrees, symlinked platform dirs) are handled appropriately. ## Inputs -- The instance name (typically the lowercased Jira key, e.g. `dspx-3302`). If the user passes the scenario YAML path instead, read its `instance.metadata.name`. -- Whether the user wants the instance directory preserved (default: yes — keep it for re-runs). +- The instance name (typically the lowercased Jira key, e.g. `dspx-3302`). If the user passed the scenario YAML path instead, read its `instance.metadata.name`. +- Whether to preserve the instance directory (default: yes — keep it for re-runs). ## Process -1. **Stop services**: +### Step 1 — Pre-flight shared resources - ```bash - uv run otdf-local --instance down - ``` +Before stopping anything, list the docker compose projects currently sharing the host daemon: - The `down` command halts the platform process, all KAS instances under management, and the docker dependencies (keycloak, postgres) — unless another instance is still using them, in which case docker is left running. +```bash +docker ps --format '{{.Names}}' \ + | grep -E -- '-keycloak-|-opentdfdb-' \ + | sed -E 's/-(keycloak|opentdfdb)-[0-9]+$//' \ + | sort -u +``` -2. **Optionally clean state**. Only if the user explicitly asked to remove: +Each line is a compose-project name — typically the directory name where `docker compose` was invoked (a worktree's `xtest/platform/src//`). If more than one project appears, surface this in the reply: `down` will *keep* docker keycloak/postgres running because another instance still uses them. The user's expectation that "ports 5432 and 8888 are now free" would be wrong. - ```bash - uv run otdf-local instance rm -y - ``` +### Step 2 — Stop services - This deletes `tests/instances//` including its `logs/`, `keys/`, and per-KAS configs. The platform binary at `xtest/platform/dist//service` is shared and is NOT removed (`otdf-sdk-mgr clean --dist-only` is the right command if the user wants to free that too). +```bash +uv run otdf-local --instance down +``` -3. **Confirm port range is free** (useful if the user is about to bring up another scenario on the same base): +Halts the platform process, all KAS instances under management, and the docker dependencies — unless another instance is still using them, in which case docker is left running (per Step 1's pre-flight). Other instances' platforms and KAS processes are untouched. - ```bash - uv run otdf-local instance ls --json - ``` +### Step 3 — Optionally clean state + +Only if the user explicitly asked to remove: + +```bash +uv run otdf-local instance rm -y +``` + +Deletes `tests/instances//` including its `logs/`, `keys/`, and per-KAS configs. The platform binary at `xtest/platform/dist//service` is shared and is NOT removed. To free those too: + +```bash +uv run otdf-sdk-mgr clean --dist-only +``` + +### Step 4 — Confirm + +```bash +uv run otdf-local instance ls --json +``` + +Verify the instance is gone (if `rm`'d) or that its services no longer appear running. If sibling worktrees still own ports, that's recorded in Step 1's output — flag it in the summary. + +## Post-down notes to surface + +- **Symlinked platform dir**: if this worktree's `xtest/platform` is a symlink (or `xtest/platform.local-backup/` exists), mention it. That was a one-time workaround for `uv tool install`'d CLIs anchoring to a sibling worktree (see DSPX-3415). The backup directory accumulates stale `src/` and can be reclaimed (`rm -rf xtest/platform.local-backup`) once the user is sure the symlink is permanent. +- **Foreign docker-compose project**: if Step 1 surfaced another project, name it so the user knows which worktree to manage if they want a truly clean host. ## Caution Never remove an instance without explicit user confirmation. The directory may contain golden keys or generated configs that took time to assemble. If unsure, leave it. + +## Output + +One-line summary, then optional sections in this order: +- Stop result (services stopped: …). +- Cleaned (if `rm` was run): instance dir removed at … +- Docker status: stopped / still running (with project names if shared). +- Post-down notes (symlinks, backup dirs, foreign projects). + +Skip empty sections. diff --git a/.claude/skills/scenario-up/SKILL.md b/.claude/skills/scenario-up/SKILL.md index dcf1ea35..45050fe7 100644 --- a/.claude/skills/scenario-up/SKILL.md +++ b/.claude/skills/scenario-up/SKILL.md @@ -1,51 +1,105 @@ --- name: scenario-up -description: Use when the user has a scenario YAML and wants the environment started (before running tests). +description: This skill should be used when the user asks to "bring up a scenario", "start a scenario environment", "spin up the test instance", "install and run the scenario", or has authored a `xtest/scenarios/.yaml` and wants the platform + KAS + dependencies started before invoking pytest. Use `scenario-run` after this succeeds. allowed-tools: Bash, Read --- # scenario-up -You bring the environment described by a `scenarios.yaml` up and confirm it's healthy. The three steps are non-negotiable; do them in order. +Bring the environment described by a `xtest/scenarios/.yaml` up and confirm it is healthy. The four steps are non-negotiable; do them in order. ## Inputs -- Path to a validated `xtest/scenarios/.yaml`. If the user doesn't provide one, ask. +- Path to a validated `xtest/scenarios/.yaml`. If the user did not provide one, ask. ## Process -1. **Install artifacts** — platform binary, per-KAS binaries, helper scripts, and the encrypt+decrypt SDKs declared in the scenario: +### Step 1 — Install artifacts - ```bash - uv run otdf-sdk-mgr install scenario xtest/scenarios/.yaml - ``` +```bash +uv run otdf-sdk-mgr install scenario xtest/scenarios/.yaml +``` - This writes `xtest/scenarios/.installed.json` next to the scenario with the resolved dist paths. The first `go build` per platform version takes ~30-60s; subsequent runs reuse the cached binary. +Installs the platform binary, per-KAS binaries, helper scripts, and the encrypt + decrypt SDKs declared in the scenario. The result is recorded at `xtest/scenarios/.installed.json` next to the scenario. -2. **Scaffold the instance directory** (creates `tests/instances//`): +**Guard against partial installs.** Read the resulting `.installed.json` immediately: - ```bash - uv run otdf-local instance init --from-scenario xtest/scenarios/.yaml - ``` +```bash +cat xtest/scenarios/.installed.json | jq '{status, sdk_count: (.sdks.encrypt + .sdks.decrypt | length)}' +``` - If the instance already exists, this is a no-op for the existing files; double-check with `uv run otdf-local instance ls` first to avoid surprising the user. +If `status == "partial"` OR `sdks.encrypt` / `sdks.decrypt` are empty arrays *but* the scenario declared SDK entries, treat it as a hard failure and stop. Today `install scenario` silently ignores source-built SDK pins (only released versions resolve via `install_release`). The remedy: -3. **Bring it up**: +```bash +# Install source-built SDKs separately, then continue. +uv run otdf-sdk-mgr install tip --ref +``` - ```bash - uv run otdf-local --instance up - ``` +This limitation is tracked at [DSPX-3417](https://virtru.atlassian.net/browse/DSPX-3417). When that ships, the guard becomes redundant — keep it until then. - Then poll status until everything is healthy (don't proceed before this succeeds): +First `go build` per platform version takes ~30–60s; subsequent runs reuse the cached binary. - ```bash - uv run otdf-local --instance status --json - ``` +### Step 2 — Scaffold the instance directory - If any service stays unhealthy after ~60 seconds, surface the relevant log via `uv run otdf-local --instance logs -n 50` and report the failure mode rather than retrying blindly. +```bash +uv run otdf-local instance init --from-scenario xtest/scenarios/.yaml +``` + +Creates `tests/instances//`. If the instance already exists, the command is a no-op for existing files. Double-check with `uv run otdf-local instance ls` first to avoid surprising the user with overwrites. + +### Step 2.5 — Bootstrap PR worktrees (when source-pinned) + +A freshly built PR worktree from `install tip --ref pr:N` ships *templates* but not generated dev keys, and lacks the `opentdf.yaml` filename `otdf-local` expects. Running `up` against it produces cryptic Docker "Is a directory" and platform "no such file" errors. Pre-flight the seed files: + +```bash +bash ${CLAUDE_PLUGIN_ROOT:-.}/skills/scenario-up/scripts/bootstrap-pr-worktree.sh xtest/scenarios/.yaml +``` + +Script behaviour: for each `source.ref` pin in the scenario, resolve the dist's worktree via its `.version` sidecar; check that `kas-*.pem`, `keys/{ca.jks,localhost.crt,localhost.key}`, and `opentdf.yaml` exist as *files* (not Docker-created empty dirs). On miss it generates / copies from `xtest/platform/src/main/` / suggests `bash .github/scripts/init-temp-keys.sh`. Output is tab-separated; review the rows where `action != kept` before proceeding. + +Skip this step for scenarios pinned entirely on `dist:` (released versions) — those use pre-baked artifacts and don't need seeding. + +[DSPX-3416](https://virtru.atlassian.net/browse/DSPX-3416) tracks moving this bootstrap into `otdf-local up` itself. Until it lands, run the script. + +### Step 3 — Bring it up + +```bash +uv run otdf-local --instance up +``` + +Then poll status until everything is healthy (do not proceed before this succeeds): + +```bash +uv run otdf-local --instance status --json +``` + +If any service stays unhealthy after ~60 seconds, surface the relevant log via `uv run otdf-local --instance logs -n 50` and report the failure mode rather than retrying blindly. + +## Source-build env knobs + +When the scenario pins source-built artifacts (`source.ref` on platform / KAS / SDKs), two env-var overrides are temporarily required for `scenario-run`. Note them now so the user has them ready: + +```bash +# Tell xtest which otdfctl binary to use (the slug under xtest/sdk/go/dist/). +export OTDFCTL_HEADS='["refs--pull----head"]' + +# Make tdfs.get_platform_features() enable in-flight feature flags whose semver +# gate is in the future; PR builds self-report old versions. +export PLATFORM_VERSION=0.17.0 +``` + +These workarounds are tracked at [DSPX-3418](https://virtru.atlassian.net/browse/DSPX-3418) (`OTDFCTL_HEADS` → CLI flag) and [DSPX-3419](https://virtru.atlassian.net/browse/DSPX-3419) (auto-derive `PLATFORM_VERSION`). When either lands, remove the corresponding line. ## Output Once healthy, report: - The instance name and which ports it occupies (look at `instance.yaml`'s `ports.base`). -- The next command the user is likely to run (`scenario-run`). +- The path to `.installed.json` (so `scenario-run` can find it). +- Any unusual rows from the bootstrap probe (e.g. "seeded `keys/ca.jks` from main worktree"). +- The next command the user is likely to run: `scenario-run xtest/scenarios/.yaml`. + +## Additional Resources + +### Script + +- **`scripts/bootstrap-pr-worktree.sh`** — pre-flights a PR worktree's seed files before `otdf-local up`. Takes one positional argument: the scenario YAML path. Tab-separated stdout. Idempotent — safe to re-run. diff --git a/.claude/skills/scenario-up/scripts/bootstrap-pr-worktree.sh b/.claude/skills/scenario-up/scripts/bootstrap-pr-worktree.sh new file mode 100755 index 00000000..72a97bdd --- /dev/null +++ b/.claude/skills/scenario-up/scripts/bootstrap-pr-worktree.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# bootstrap-pr-worktree.sh — ensure platform source worktrees referenced by a +# scenario have the seed files otdf-local + docker-compose expect. +# +# Usage: bootstrap-pr-worktree.sh +# +# A fresh `otdf-sdk-mgr install tip --ref platform` produces the +# /service binary and a populated git worktree at xtest/platform/src//, +# but it does NOT generate the dev keys (kas-*.pem, keys/ca.jks, …) or copy +# opentdf-dev.yaml → opentdf.yaml. `otdf-local up` then fails in cryptic +# ways (Keycloak "Is a directory", platform "no such file"). This script +# pre-flights each referenced worktree and either bootstraps or fails loudly +# with the exact remedy. +# +# Output: tab-separated, header on first line. +# Columns: worktree file state action +# state ∈ { ok | missing | empty-dir } +# action ∈ { kept | generated | copied | manual-required } + +set -u + +if [[ $# -lt 1 ]]; then + echo "usage: $(basename "$0") " >&2 + exit 2 +fi + +scenario="$1" +[[ -f "$scenario" ]] || { echo "scenario not found: $scenario" >&2; exit 2; } + +# Resolve repo root (the dir containing xtest/) by walking up from $scenario. +dir="$(cd "$(dirname "$scenario")" && pwd)" +while [[ "$dir" != "/" && ! -d "$dir/xtest" ]]; do dir="$(dirname "$dir")"; done +[[ -d "$dir/xtest" ]] || { echo "could not locate xtest/ above $scenario" >&2; exit 2; } +PLATFORM_DIST="$dir/xtest/platform/dist" + +# Files each worktree needs. Order matters for the action log — opentdf.yaml +# last so its "copied from opentdf-dev.yaml" message lands after the keys. +REQUIRED_FILES=( + kas-private.pem + kas-cert.pem + kas-ec-private.pem + kas-ec-cert.pem + keys/ca.jks + keys/localhost.crt + keys/localhost.key + opentdf.yaml +) + +# Extract referenced refs from the scenario. We tolerate yq presence/absence: +# prefer `yq -r` when available, fall back to a grep that handles the two +# shapes we emit (`ref: pr:3537` inline and `{ ref: pr:3537 }` flow-style). +refs=() +if command -v yq >/dev/null 2>&1; then + while IFS= read -r r; do [[ -n "$r" && "$r" != "null" ]] && refs+=("$r"); done < <( + yq -r ' + [ .instance.platform.source.ref?, + (.instance.kas[]?.source.ref?) + ] | .[] | select(. != null) + ' "$scenario" 2>/dev/null | sort -u + ) +else + while IFS= read -r r; do refs+=("$r"); done < <( + grep -E '\{?\s*ref:' "$scenario" | sed -E 's/.*ref:[[:space:]]*"?([^",}[:space:]]+)"?.*/\1/' | sort -u + ) +fi + +if [[ ${#refs[@]} -eq 0 ]]; then + echo "no source.ref pins found in $scenario (dist-only scenario, nothing to bootstrap)" >&2 + exit 0 +fi + +printf 'worktree\tfile\tstate\taction\n' + +for ref in "${refs[@]}"; do + # Slug used by otdf-sdk-mgr: replace `/` and `:` with `--`. Mutable refs + # like `main`, `pr:3537` get slugs `main`, `refs--pull--3537--head` (the + # `pr:N` shorthand expands inside the installer). Read the .version sidecar + # to get the canonical worktree path rather than guess. + dist_dir="" + for slug_candidate in "$PLATFORM_DIST"/*/; do + [[ -f "$slug_candidate/.version" ]] || continue + if grep -Fq "ref=$ref" "$slug_candidate/.version" || grep -Fq "ref=refs/pull/${ref#pr:}/head" "$slug_candidate/.version"; then + dist_dir="${slug_candidate%/}"; break + fi + done + if [[ -z "$dist_dir" ]]; then + printf '%s\t-\tmissing\tmanual-required\n' "$ref" + echo "no dist dir found for ref=$ref; run 'otdf-sdk-mgr install tip --ref $ref platform' first" >&2 + continue + fi + worktree="$(awk -F= '/^worktree=/ {print $2}' "$dist_dir/.version")" + [[ -d "$worktree" ]] || { printf '%s\t.version\tmissing\tmanual-required\n' "$ref"; continue; } + + for f in "${REQUIRED_FILES[@]}"; do + path="$worktree/$f" + if [[ -f "$path" ]]; then + printf '%s\t%s\tok\tkept\n' "$worktree" "$f" + continue + fi + if [[ -d "$path" ]]; then + # Docker bind-mount created an empty dir on a prior failed up. Remove it + # so the bootstrap fill below can replace it with a real file. + rmdir "$path" 2>/dev/null || true + printf '%s\t%s\tempty-dir\tremoved\n' "$worktree" "$f" + fi + + # Fill rules: + # opentdf.yaml: cp opentdf-dev.yaml → opentdf.yaml (legacy template name). + # everything else: try copying from xtest/platform/src/main/, else fail. + if [[ "$f" == "opentdf.yaml" && -f "$worktree/opentdf-dev.yaml" ]]; then + cp "$worktree/opentdf-dev.yaml" "$path" + printf '%s\t%s\tmissing\tcopied(opentdf-dev.yaml)\n' "$worktree" "$f" + continue + fi + + main_dir="$dir/xtest/platform/src/main" + if [[ -f "$main_dir/$f" ]]; then + mkdir -p "$(dirname "$path")" + cp "$main_dir/$f" "$path" + printf '%s\t%s\tmissing\tcopied(main)\n' "$worktree" "$f" + continue + fi + + printf '%s\t%s\tmissing\tmanual-required\n' "$worktree" "$f" + cat >&2 < Date: Tue, 2 Jun 2026 09:26:54 -0400 Subject: [PATCH 10/11] fix(.claude/skills): align scenario-* with self-provisioning init + correct schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scenario-up: drop Step 2.5 bootstrap script (instance init now self-provisions keys + opentdf.yaml per 74492a47); add an `otdf-local env` sanity check before handing off to scenario-run. - scenario-run: replace stale `suite.select` references with `suite.targets` (list); update Step 1b source-build fallback template to unpack targets positionally. - scenario-doctor: Step 2 now invokes the read-only verifier against the instance dir instead of the platform worktree. - scenario-from-ticket: one stale `suite.select` mention fixed. - Move bootstrap-pr-worktree.sh → scenario-doctor/scripts/check-instance-seed.sh and rewrite as a read-only verifier (no cp/rmdir side effects). Co-Authored-By: Claude Sonnet 4.5 --- .claude/skills/scenario-doctor/SKILL.md | 15 +- .../scripts/check-instance-seed.sh | 77 ++++++++++ .claude/skills/scenario-from-ticket/SKILL.md | 2 +- .claude/skills/scenario-run/SKILL.md | 8 +- .claude/skills/scenario-up/SKILL.md | 31 ++--- .../scripts/bootstrap-pr-worktree.sh | 131 ------------------ 6 files changed, 99 insertions(+), 165 deletions(-) create mode 100644 .claude/skills/scenario-doctor/scripts/check-instance-seed.sh delete mode 100755 .claude/skills/scenario-up/scripts/bootstrap-pr-worktree.sh diff --git a/.claude/skills/scenario-doctor/SKILL.md b/.claude/skills/scenario-doctor/SKILL.md index f55bd58f..fb918c68 100644 --- a/.claude/skills/scenario-doctor/SKILL.md +++ b/.claude/skills/scenario-doctor/SKILL.md @@ -36,22 +36,22 @@ alpha 8181 v090... a1b2c3d4... 200 WRONG-BINARY - `EXTRA` — port is occupied by a service the instance didn't declare. Usually a leftover from another instance/worktree. - `NO-PIN` — instance manifest didn't pin this service (skip). -### Step 2 — Verify seed files +### Step 2 — Verify instance-dir seed files -For each unique worktree referenced in the diff output (parse the `expected_sha` rows back to `.version` sidecars), invoke the bootstrap script in dry-run inspection mode — re-using `scenario-up`'s probe so the file checks stay consistent: +`otdf-local instance init` is responsible for seeding `keys/{ca.jks,localhost.crt,localhost.key}`, `keys/kas-*.pem`, and `instances//opentdf.yaml` (with a generated `services.kas.root_key`). Confirm they're all present: ```bash -bash ${CLAUDE_PLUGIN_ROOT:-.}/skills/scenario-up/scripts/bootstrap-pr-worktree.sh +bash ${CLAUDE_PLUGIN_ROOT:-.}/skills/scenario-doctor/scripts/check-instance-seed.sh ``` -Treat any `state=empty-dir` or `state=missing action=manual-required` row as a real problem worth surfacing — those are the silent-failure shapes (Docker bind-mount stubs, ungenerated dev keys). +Tab-separated output, one row per artifact, `state ∈ {ok, missing, empty}`. Treat any non-`ok` row as a real problem — re-run `uv run otdf-local instance init --from-scenario ` to refresh (existing files are preserved, so this won't churn the root_key). ### Step 3 — Assign a verdict Roll up Steps 1–2 into one of three colors. Lead the reply with the verdict; users scan for this. - **GREEN** — every declared service is `MATCH` + 200, no `EXTRA` rows, every seed file `ok`. Nothing for the user to do. -- **YELLOW** — at least one `WRONG-BINARY`, `EXTRA`, or `empty-dir`/`missing` row, but the instance is *running*. Tests may pass or fail unpredictably until the drift is resolved. +- **YELLOW** — at least one `WRONG-BINARY`, `EXTRA`, or `missing`/`empty` seed-file row, but the instance is *running*. Tests may pass or fail unpredictably until the drift is resolved. - **RED** — at least one declared service is `NOT-RUNNING`. Tests cannot succeed; recommend `otdf-local --instance up` (fresh start) or per-service `restart`. ### Step 4 — Per-row remedy @@ -63,7 +63,7 @@ For each non-`MATCH` row, emit a one-line remedy alongside the diff table: | `NOT-RUNNING` | `otdf-local --instance up` (full) or `restart ` (single service) | | `WRONG-BINARY` | Identify owning PID's worktree via `lsof -p -d cwd`. If sibling worktree: tear that down first (`OTDF_LOCAL_INSTANCE_NAME= otdf-local down`). If same worktree, stale binary: `otdf-sdk-mgr install tip --ref platform` then restart. | | `EXTRA` | Confirm the PID and its cwd. Stop owning instance or kill the stale PID. | -| `empty-dir` / `missing` | Re-run `bootstrap-pr-worktree.sh` (Phase B of `scenario-up`) or hand-run `bash .github/scripts/init-temp-keys.sh` in the worktree. | +| `missing` / `empty` (seed file) | Re-run `otdf-local instance init --from-scenario `. Existing files are preserved; only the missing seed gets regenerated. | ### Step 5 — Output @@ -86,9 +86,10 @@ For the simpler "what's defined / what's listening here" question without the di ## Additional Resources -### Script +### Scripts - **`scripts/diff-running-vs-intended.sh`** — automates Step 1's expected-vs-actual diff. Takes one positional argument: the instance name. Tab-separated stdout. +- **`scripts/check-instance-seed.sh`** — read-only verifier for Step 2. Takes one positional argument: the instance name. Confirms `keys/{ca.jks,localhost.crt,localhost.key}`, `keys/kas-*.pem`, and `opentdf.yaml` (with a non-empty `services.kas.root_key`) are present in `tests/instances//`. Tab-separated stdout. ### Reference files diff --git a/.claude/skills/scenario-doctor/scripts/check-instance-seed.sh b/.claude/skills/scenario-doctor/scripts/check-instance-seed.sh new file mode 100644 index 00000000..98b30f68 --- /dev/null +++ b/.claude/skills/scenario-doctor/scripts/check-instance-seed.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# check-instance-seed.sh — read-only verifier that `otdf-local instance init` +# left the instance dir with the seed bundle `up` and pytest expect. +# +# Usage: check-instance-seed.sh +# +# `instance init` self-provisions: keys/{ca.jks,localhost.crt,localhost.key}, +# keys/kas-{private,cert,ec-private,ec-cert}.pem, and instances//opentdf.yaml +# with a generated services.kas.root_key. This script confirms each is present +# and reports tab-separated rows; it does not modify anything. +# +# Output: tab-separated, header on first line. +# Columns: artifact state detail +# state ∈ { ok | missing | empty } + +set -u + +if [[ $# -lt 1 ]]; then + echo "usage: $(basename "$0") " >&2 + exit 2 +fi + +name="$1" + +# Resolve repo root by walking up from $PWD until we find the tests/ marker. +dir="$PWD" +while [[ "$dir" != "/" && ! -d "$dir/instances" ]]; do dir="$(dirname "$dir")"; done +if [[ ! -d "$dir/instances" ]]; then + echo "could not locate tests/instances/ above $PWD" >&2 + exit 2 +fi +instance_dir="$dir/instances/$name" +if [[ ! -d "$instance_dir" ]]; then + echo "instance not found: $instance_dir" >&2 + exit 2 +fi + +REQUIRED_FILES=( + keys/ca.jks + keys/localhost.crt + keys/localhost.key + keys/kas-private.pem + keys/kas-cert.pem + keys/kas-ec-private.pem + keys/kas-ec-cert.pem + opentdf.yaml +) + +printf 'artifact\tstate\tdetail\n' + +for f in "${REQUIRED_FILES[@]}"; do + path="$instance_dir/$f" + if [[ ! -e "$path" ]]; then + printf '%s\tmissing\t-\n' "$f" + elif [[ -d "$path" ]]; then + printf '%s\tempty\tdirectory leftover (docker bind-mount stub)\n' "$f" + elif [[ ! -s "$path" ]]; then + printf '%s\tempty\tzero bytes\n' "$f" + else + printf '%s\tok\t-\n' "$f" + fi +done + +# Confirm the per-instance root_key got written into opentdf.yaml. +config="$instance_dir/opentdf.yaml" +if [[ -f "$config" ]]; then + if command -v yq >/dev/null 2>&1; then + rk="$(yq -r '.services.kas.root_key // ""' "$config" 2>/dev/null)" + else + rk="$(grep -E '^[[:space:]]*root_key:' "$config" 2>/dev/null | head -1 | sed -E 's/.*root_key:[[:space:]]*"?([^"[:space:]]+)"?.*/\1/')" + fi + if [[ -z "$rk" || "$rk" == "null" ]]; then + printf '%s\tmissing\troot_key empty in opentdf.yaml\n' "services.kas.root_key" + else + printf '%s\tok\t-\n' "services.kas.root_key" + fi +fi diff --git a/.claude/skills/scenario-from-ticket/SKILL.md b/.claude/skills/scenario-from-ticket/SKILL.md index 5743e5f6..8daff3da 100644 --- a/.claude/skills/scenario-from-ticket/SKILL.md +++ b/.claude/skills/scenario-from-ticket/SKILL.md @@ -95,7 +95,7 @@ The ticket asks an open question or lacks enough concrete behavior to encode. Do grep -rn "" xtest/test_*.py xtest/tdfs.py ``` -Likely candidates: `test_tdfs.py` (roundtrip), `test_abac.py` (ABAC), `test_legacy.py` (golden), `test_pqc.py`. If a test already asserts the relevant behavior, reuse it via `suite.select` — no draft test needed. +Likely candidates: `test_tdfs.py` (roundtrip), `test_abac.py` (ABAC), `test_legacy.py` (golden), `test_pqc.py`. If a test already asserts the relevant behavior, reuse it via `suite.targets` (list of pytest selectors) — no draft test needed. **Don't grep `xtest/sdk//cli.sh`.** Those wrappers are reusable infrastructure (versioned alongside each SDK dist) and their contents have nothing to do with scenario YAML fields. The scenario doesn't need to know HOW a feature is plumbed — only WHICH pytest suite exercises it. If a feature's `supports("")` gate isn't in `tdfs.py` yet, that's a signal that supporting infrastructure has to land separately from the scenario — note it in `actual:` and move on. diff --git a/.claude/skills/scenario-run/SKILL.md b/.claude/skills/scenario-run/SKILL.md index f141e961..7bd3a360 100644 --- a/.claude/skills/scenario-run/SKILL.md +++ b/.claude/skills/scenario-run/SKILL.md @@ -21,7 +21,7 @@ Invoke the pytest selection declared by the scenario's `suite` block against the uv run otdf-local scenario run xtest/scenarios/.yaml ``` -This translates the scenario's `suite.select`, `suite.containers`, `suite.markers`, and `sdks.{encrypt,decrypt}` into the equivalent `pytest --sdks-encrypt … --sdks-decrypt … --containers …` invocation under `xtest/` with `OTDF_LOCAL_INSTANCE_NAME` set. SDK tokens are emitted in xtest's `sdk@version` form; the resolved version names come from the sibling `.installed.json`. +This translates the scenario's `suite.targets` (list — each entry becomes a positional pytest arg), `suite.containers` (list — joined into a single whitespace-separated `--containers` value), `suite.kexpr`, `suite.markers`, and `sdks.{encrypt,decrypt}` into the equivalent `pytest --sdks-encrypt … --sdks-decrypt … --containers …` invocation under `xtest/` with `OTDF_LOCAL_INSTANCE_NAME` set. SDK tokens are emitted in xtest's `sdk@version` form; the resolved version names come from the sibling `.installed.json`. Failure modes: - `Error: .installed.json not found` — the user skipped Step 1 of `scenario-up`. Run `uv run otdf-sdk-mgr install scenario ` first. @@ -41,10 +41,10 @@ set +a # Map each source-pinned SDK to its dist slug under xtest/sdk//dist/. # For platform PR #N, the slug is typically `refs--pull----head`. PLATFORM_VERSION= OTDFCTL_HEADS='[""]' \ - uv run pytest \ + uv run pytest \ --sdks-encrypt @ \ --sdks-decrypt @ \ - --containers + --containers "" ``` `PLATFORM_VERSION` and `OTDFCTL_HEADS` defaults are noted in `scenario-up`; pull them from there or from the scenario's source-build env knobs section. This fallback is temporary — tracked at [DSPX-3417](https://virtru.atlassian.net/browse/DSPX-3417) (scenario YAML accepting source builds) and [DSPX-3418](https://virtru.atlassian.net/browse/DSPX-3418) (`OTDFCTL_HEADS` → CLI flag). @@ -77,7 +77,7 @@ Pytest leaves logs under `tests/instances//logs/`. List the relevant per-ser ## Output format One-line headline naming the bucket, then a short bulleted summary: -- `select:` the pytest selector that ran +- `targets:` the pytest selectors that ran (one per `suite.targets` entry) - `exit_code:` the pytest return value - `evidence:` 1–2 lines from the output that justify the classification - `logs:` paths to the relevant per-service logs diff --git a/.claude/skills/scenario-up/SKILL.md b/.claude/skills/scenario-up/SKILL.md index 45050fe7..ebbe9f26 100644 --- a/.claude/skills/scenario-up/SKILL.md +++ b/.claude/skills/scenario-up/SKILL.md @@ -45,21 +45,7 @@ First `go build` per platform version takes ~30–60s; subsequent runs reuse the uv run otdf-local instance init --from-scenario xtest/scenarios/.yaml ``` -Creates `tests/instances//`. If the instance already exists, the command is a no-op for existing files. Double-check with `uv run otdf-local instance ls` first to avoid surprising the user with overwrites. - -### Step 2.5 — Bootstrap PR worktrees (when source-pinned) - -A freshly built PR worktree from `install tip --ref pr:N` ships *templates* but not generated dev keys, and lacks the `opentdf.yaml` filename `otdf-local` expects. Running `up` against it produces cryptic Docker "Is a directory" and platform "no such file" errors. Pre-flight the seed files: - -```bash -bash ${CLAUDE_PLUGIN_ROOT:-.}/skills/scenario-up/scripts/bootstrap-pr-worktree.sh xtest/scenarios/.yaml -``` - -Script behaviour: for each `source.ref` pin in the scenario, resolve the dist's worktree via its `.version` sidecar; check that `kas-*.pem`, `keys/{ca.jks,localhost.crt,localhost.key}`, and `opentdf.yaml` exist as *files* (not Docker-created empty dirs). On miss it generates / copies from `xtest/platform/src/main/` / suggests `bash .github/scripts/init-temp-keys.sh`. Output is tab-separated; review the rows where `action != kept` before proceeding. - -Skip this step for scenarios pinned entirely on `dist:` (released versions) — those use pre-baked artifacts and don't need seeding. - -[DSPX-3416](https://virtru.atlassian.net/browse/DSPX-3416) tracks moving this bootstrap into `otdf-local up` itself. Until it lands, run the script. +Creates `tests/instances//` and **self-provisions the bootstrap bundle**: generates the Keycloak TLS pair + `keys/ca.jks` truststore, creates `kas-*.pem` keys, and copies the platform's `opentdf-dev.yaml` (or `opentdf-example.yaml`) into `instances//opentdf.yaml` with a freshly generated `services.kas.root_key`. Idempotent — existing files are preserved, so the per-instance root key survives re-runs. Double-check with `uv run otdf-local instance ls` first to avoid surprising the user with overwrites. ### Step 3 — Bring it up @@ -75,6 +61,14 @@ uv run otdf-local --instance status --json If any service stays unhealthy after ~60 seconds, surface the relevant log via `uv run otdf-local --instance logs -n 50` and report the failure mode rather than retrying blindly. +Once healthy, sanity-check the env exports `scenario-run` will rely on: + +```bash +uv run otdf-local --instance env --format json | jq '{PLATFORM_DIR,PLATFORMURL,SCHEMA_FILE,OT_ROOT_KEY}' +``` + +All four must be non-null. If `OT_ROOT_KEY` is null, the instance's `opentdf.yaml` is missing or didn't get a `services.kas.root_key` written (re-run `instance init` to refresh). + ## Source-build env knobs When the scenario pins source-built artifacts (`source.ref` on platform / KAS / SDKs), two env-var overrides are temporarily required for `scenario-run`. Note them now so the user has them ready: @@ -95,11 +89,4 @@ These workarounds are tracked at [DSPX-3418](https://virtru.atlassian.net/browse Once healthy, report: - The instance name and which ports it occupies (look at `instance.yaml`'s `ports.base`). - The path to `.installed.json` (so `scenario-run` can find it). -- Any unusual rows from the bootstrap probe (e.g. "seeded `keys/ca.jks` from main worktree"). - The next command the user is likely to run: `scenario-run xtest/scenarios/.yaml`. - -## Additional Resources - -### Script - -- **`scripts/bootstrap-pr-worktree.sh`** — pre-flights a PR worktree's seed files before `otdf-local up`. Takes one positional argument: the scenario YAML path. Tab-separated stdout. Idempotent — safe to re-run. diff --git a/.claude/skills/scenario-up/scripts/bootstrap-pr-worktree.sh b/.claude/skills/scenario-up/scripts/bootstrap-pr-worktree.sh deleted file mode 100755 index 72a97bdd..00000000 --- a/.claude/skills/scenario-up/scripts/bootstrap-pr-worktree.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env bash -# bootstrap-pr-worktree.sh — ensure platform source worktrees referenced by a -# scenario have the seed files otdf-local + docker-compose expect. -# -# Usage: bootstrap-pr-worktree.sh -# -# A fresh `otdf-sdk-mgr install tip --ref platform` produces the -# /service binary and a populated git worktree at xtest/platform/src//, -# but it does NOT generate the dev keys (kas-*.pem, keys/ca.jks, …) or copy -# opentdf-dev.yaml → opentdf.yaml. `otdf-local up` then fails in cryptic -# ways (Keycloak "Is a directory", platform "no such file"). This script -# pre-flights each referenced worktree and either bootstraps or fails loudly -# with the exact remedy. -# -# Output: tab-separated, header on first line. -# Columns: worktree file state action -# state ∈ { ok | missing | empty-dir } -# action ∈ { kept | generated | copied | manual-required } - -set -u - -if [[ $# -lt 1 ]]; then - echo "usage: $(basename "$0") " >&2 - exit 2 -fi - -scenario="$1" -[[ -f "$scenario" ]] || { echo "scenario not found: $scenario" >&2; exit 2; } - -# Resolve repo root (the dir containing xtest/) by walking up from $scenario. -dir="$(cd "$(dirname "$scenario")" && pwd)" -while [[ "$dir" != "/" && ! -d "$dir/xtest" ]]; do dir="$(dirname "$dir")"; done -[[ -d "$dir/xtest" ]] || { echo "could not locate xtest/ above $scenario" >&2; exit 2; } -PLATFORM_DIST="$dir/xtest/platform/dist" - -# Files each worktree needs. Order matters for the action log — opentdf.yaml -# last so its "copied from opentdf-dev.yaml" message lands after the keys. -REQUIRED_FILES=( - kas-private.pem - kas-cert.pem - kas-ec-private.pem - kas-ec-cert.pem - keys/ca.jks - keys/localhost.crt - keys/localhost.key - opentdf.yaml -) - -# Extract referenced refs from the scenario. We tolerate yq presence/absence: -# prefer `yq -r` when available, fall back to a grep that handles the two -# shapes we emit (`ref: pr:3537` inline and `{ ref: pr:3537 }` flow-style). -refs=() -if command -v yq >/dev/null 2>&1; then - while IFS= read -r r; do [[ -n "$r" && "$r" != "null" ]] && refs+=("$r"); done < <( - yq -r ' - [ .instance.platform.source.ref?, - (.instance.kas[]?.source.ref?) - ] | .[] | select(. != null) - ' "$scenario" 2>/dev/null | sort -u - ) -else - while IFS= read -r r; do refs+=("$r"); done < <( - grep -E '\{?\s*ref:' "$scenario" | sed -E 's/.*ref:[[:space:]]*"?([^",}[:space:]]+)"?.*/\1/' | sort -u - ) -fi - -if [[ ${#refs[@]} -eq 0 ]]; then - echo "no source.ref pins found in $scenario (dist-only scenario, nothing to bootstrap)" >&2 - exit 0 -fi - -printf 'worktree\tfile\tstate\taction\n' - -for ref in "${refs[@]}"; do - # Slug used by otdf-sdk-mgr: replace `/` and `:` with `--`. Mutable refs - # like `main`, `pr:3537` get slugs `main`, `refs--pull--3537--head` (the - # `pr:N` shorthand expands inside the installer). Read the .version sidecar - # to get the canonical worktree path rather than guess. - dist_dir="" - for slug_candidate in "$PLATFORM_DIST"/*/; do - [[ -f "$slug_candidate/.version" ]] || continue - if grep -Fq "ref=$ref" "$slug_candidate/.version" || grep -Fq "ref=refs/pull/${ref#pr:}/head" "$slug_candidate/.version"; then - dist_dir="${slug_candidate%/}"; break - fi - done - if [[ -z "$dist_dir" ]]; then - printf '%s\t-\tmissing\tmanual-required\n' "$ref" - echo "no dist dir found for ref=$ref; run 'otdf-sdk-mgr install tip --ref $ref platform' first" >&2 - continue - fi - worktree="$(awk -F= '/^worktree=/ {print $2}' "$dist_dir/.version")" - [[ -d "$worktree" ]] || { printf '%s\t.version\tmissing\tmanual-required\n' "$ref"; continue; } - - for f in "${REQUIRED_FILES[@]}"; do - path="$worktree/$f" - if [[ -f "$path" ]]; then - printf '%s\t%s\tok\tkept\n' "$worktree" "$f" - continue - fi - if [[ -d "$path" ]]; then - # Docker bind-mount created an empty dir on a prior failed up. Remove it - # so the bootstrap fill below can replace it with a real file. - rmdir "$path" 2>/dev/null || true - printf '%s\t%s\tempty-dir\tremoved\n' "$worktree" "$f" - fi - - # Fill rules: - # opentdf.yaml: cp opentdf-dev.yaml → opentdf.yaml (legacy template name). - # everything else: try copying from xtest/platform/src/main/, else fail. - if [[ "$f" == "opentdf.yaml" && -f "$worktree/opentdf-dev.yaml" ]]; then - cp "$worktree/opentdf-dev.yaml" "$path" - printf '%s\t%s\tmissing\tcopied(opentdf-dev.yaml)\n' "$worktree" "$f" - continue - fi - - main_dir="$dir/xtest/platform/src/main" - if [[ -f "$main_dir/$f" ]]; then - mkdir -p "$(dirname "$path")" - cp "$main_dir/$f" "$path" - printf '%s\t%s\tmissing\tcopied(main)\n' "$worktree" "$f" - continue - fi - - printf '%s\t%s\tmissing\tmanual-required\n' "$worktree" "$f" - cat >&2 < Date: Tue, 2 Jun 2026 13:44:43 -0400 Subject: [PATCH 11/11] fix(.claude/skills): add --instance parameter to scenario run examples Ensures consistency with other otdf-local commands (up, down, status, logs) that already show the --instance parameter. While scenario run has a default that reads from the YAML, explicitly including --instance makes examples self-documenting and prevents confusion. Co-Authored-By: Claude Sonnet 4.5 --- .claude/skills/scenario-matrix/SKILL.md | 2 +- .claude/skills/scenario-run/SKILL.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/skills/scenario-matrix/SKILL.md b/.claude/skills/scenario-matrix/SKILL.md index 9e4bbd90..b5e23953 100644 --- a/.claude/skills/scenario-matrix/SKILL.md +++ b/.claude/skills/scenario-matrix/SKILL.md @@ -78,7 +78,7 @@ Bail (delete the just-written files) if any cell fails validation — partial ma uv run otdf-sdk-mgr install scenario "$f" uv run otdf-local instance init "$name" --from-scenario "$f" uv run otdf-local --instance "$name" up - uv run otdf-local scenario run "$f" + uv run otdf-local scenario run --instance "$name" "$f" uv run otdf-local --instance "$name" down done ``` diff --git a/.claude/skills/scenario-run/SKILL.md b/.claude/skills/scenario-run/SKILL.md index 7bd3a360..e533145b 100644 --- a/.claude/skills/scenario-run/SKILL.md +++ b/.claude/skills/scenario-run/SKILL.md @@ -18,7 +18,7 @@ Invoke the pytest selection declared by the scenario's `suite` block against the ### Step 1 — Invoke the runner ```bash -uv run otdf-local scenario run xtest/scenarios/.yaml +uv run otdf-local scenario run --instance xtest/scenarios/.yaml ``` This translates the scenario's `suite.targets` (list — each entry becomes a positional pytest arg), `suite.containers` (list — joined into a single whitespace-separated `--containers` value), `suite.kexpr`, `suite.markers`, and `sdks.{encrypt,decrypt}` into the equivalent `pytest --sdks-encrypt … --sdks-decrypt … --containers …` invocation under `xtest/` with `OTDF_LOCAL_INSTANCE_NAME` set. SDK tokens are emitted in xtest's `sdk@version` form; the resolved version names come from the sibling `.installed.json`.