name: test-providers
description: Configure the local test environment — enable / disable test-provider entries in UserDataProviders.json per TFM bucket, manage the docker containers those providers need (start / stop / setup-script run), and reset the file from UserDataProviders.json.template. Owns every edit to UserDataProviders.json and every docker start / docker stop call across the .claude/ toolset; /test and test-runner consume the resulting state read-only.
test-providers
User-triggered workflow for managing the local test environment that /test runs against. The single concern of this skill is making UserDataProviders.json and the docker containers behind it match what the user wants — it never runs tests and never builds anything.
Shared reference material:
- Test database catalog (provider IDs → setup script → container → image → preference):
.claude/docs/test-databases.md UserDataProviders.jsonshape: seeUserDataProviders.json.templateat the repo root and the Test Database Configuration section of.claude/docs/testing.md
When to run
Only when the user explicitly invokes /test-providers — typically before a /test run … to make sure the needed connection strings exist and the containers are up, or after a session to stop containers the user no longer needs. Do not invoke this skill from inside /test, from test-runner, or from any other skill — the boundary is intentional, and /test is allowed to fail when the env doesn't match (the user re-runs /test-providers to fix it).
Per-run provider selection lives in --provider, not this skill. To run a one-off against specific providers, use /test … on <provider> (or <exe> --provider <name>) — it replaces the active provider set for that run, so the provider does not need to be enabled in a Providers array. This skill's enduring jobs are (1) connection strings (filling TODO_ADD_CONNECTION_STRING, custom overrides), (2) docker containers (--provider X still needs X's container running), and (3) the default enabled set used when a run passes no --provider. The Providers-array editing below is mainly for (3).
Accepted arg shapes
/test-providers parses one of the shapes below from the args. On ambiguity, stop and ask with a single numbered prompt.
| Arg shape | Intent |
|---|---|
| empty | Show current per-TFM enabled-provider set + container statuses for any container referenced by an enabled provider, then ask what to do. |
<provider> [<provider>…] (e.g. SQLite.MS PostgreSQL.18) |
Set mode. Mark exactly the listed providers enabled in the affected TFM bucket(s) and disable every other provider in those buckets. Start the corresponding containers if needed. Default scope is NET100 only — see Bucket scope below to widen. |
add <provider> [...] |
Additive mode. Mark the listed providers enabled in the affected TFM bucket(s) without changing the rest. Start containers as needed. Default scope is NET100 only. |
remove <provider> [...] |
Subtractive mode. Mark the listed providers disabled in the affected TFM bucket(s). Does not stop containers (user does that explicitly via stop). Default scope is NET100 only. |
stop / stop <container> [...] |
Stop all (or named) containers we have records of from this session. No UserDataProviders.json edit. |
reset |
Restore UserDataProviders.json from UserDataProviders.json.template after explicit confirmation. Does not touch containers. |
Provider IDs are the strings under each TFM bucket's Providers array (e.g. SQLite.MS, PostgreSQL.18, SqlServer.2016.MS). Container names match the Container column of test-databases.md.
Bucket scope
Every Set / add / remove invocation accepts an optional in <bucket>[,<bucket>...] clause anywhere in the args. The clause selects which TFM bucket(s) of UserDataProviders.json get edited:
- No clause →
NET100only (the user's default workflow target). in <bucket>[,<bucket>...]→ the named buckets only (NETFX,NET80,NET90,NET100).in all→ every TFM bucket present in the file.
Examples:
/test-providers SQLite.MS PostgreSQL.18— editsNET100only./test-providers in NETFX,NET100 SQLite.MS Firebird.5— editsNETFXandNET100, leavesNET80andNET90alone./test-providers add Oracle.12 in all— addsOracle.12.Managed(after family-rule normalisation) to every bucket.
The clause may appear before or after the provider list; parse it once, then strip it before family-rule normalisation runs on the remaining args.
Provider name shortcuts and family rules
Bare-family / version-only inputs are normalised to fully-qualified provider IDs before any edit. The family-rule table, bare-family version resolution, override and exclusion tables, and sticky-entry rule live in test-databases.md → Provider name resolution. Step 1 (Resolve intent) calls those rules; the rest of this skill consumes the normalised output.
Permission-prompt discipline
Every Bash call is allowlist-matched as one opaque string, so:
- One Bash call per command. No
&&/||/;chaining; no shell control flow. (Also enforced by the rules in.claude/docs/agent-rules.md.) - Batch independent inspects in a single assistant turn (multiple parallel Bash tool calls), not one chained string.
- Setup scripts under
Data/Setup Scripts/<name>.cmdmust be run from that directory — issuecd "Data/Setup Scripts" && <script>.cmdis not allowed; use the script's documented invocation form via a single command (e.g. invoke throughpwsh -NoProfile -File ...if a sequence is required, or justData/Setup\ Scripts/<script>.cmdfrom the repo root if the script supports it). When the script truly requires acdfirst, wrap the two-step sequence in a small pwsh under.build/.claude/rather than chaining.
Steps
1. Resolve intent
Parse args in this order:
- Mode token. First positional token decides the mode:
add,remove,stop,reset, or (anything else) Set mode. - Bucket-scope clause. Look anywhere in the remaining args for
in <bucket>[,<bucket>...]orin all. Strip it from the args; record the resolved bucket list (default["NET100"]when absent). See Bucket scope in the Accepted arg shapes section. - Family-rule normalisation (Set / add / remove only). For each remaining provider token, apply the Family-variant shortcuts and Bare-family version rules per
test-databases.md→ Provider name resolution. Keep an internal recordrewrites: [{from, to, reason}]of every token that changed — this drives the Normalised inputs block in step 4. Tokens that are already fully qualified (or match an exclusion list and were typed explicitly) recordfrom === toand don't appear in the rewrites list. - Ambiguity check. If a bare token could plausibly be a provider ID or a container name (rare — most container names like
pgsql18don't collide with provider IDs), stop and ask. Do not guess.
After this step, the agent has: { mode, buckets[], providers[], rewrites[] }. Subsequent steps consume this normalised structure, never the raw user input.
2. Read current state
Always read state before computing any change. Batch the calls in one turn:
ReadUserDataProviders.jsonat the repo root. Parse the per-TFMProvidersarrays andMyConnectionStrings.BaselinesPath(string or absent).- Snapshot container state with a single
docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Image}}"call. The status column (Up …/Exited …/Created) and image column give everything needed to deciderunning/will-start/will-createfor every container referenced by the target enabled-provider set. Peragent-rules.md→ Docker containers: start/stop/create only, do not calldocker container inspectordocker image inspect— they're outside the agent's container scope. - Build an in-memory map:
{ container -> { exists, status, image } }. Containers not present in thedocker ps -asnapshot are treated as missing (will-create).
For empty args (status mode), report the snapshot and ask what to do; don't proceed past this step until the user picks an arg shape.
3. Compute target state
Affected TFM buckets
Use the buckets[] list resolved in step 1. Default is ["NET100"]; widened only if the user passed an in … clause. Buckets not in the list are completely untouched — no read, no edit, not even a no-op replacement of their Providers array.
Provider-set / add / remove modes
Per affected TFM bucket:
- Set mode — every entry whose ID is in the (normalised) target list becomes enabled (drop a leading
-if present); every other entry in the same bucket becomes disabled (add a leading-if absent), with one exception:TestNoopProvideris sticky and is skipped by the disable sweep — seetest-databases.md→ Sticky entries. - Add mode — listed entries become enabled; every other entry is left untouched.
- Remove mode — listed entries become disabled; every other entry is left untouched. The sticky rule for
TestNoopProviderdoes not apply to Remove mode — if the user explicitly typesremove TestNoopProvider, honour it (it's an explicit choice, not a sweep).
Preserve order, formatting, comments, and unrelated buckets exactly. Don't restructure, don't sort, don't add or remove array entries — only flip the enable/disable marker on existing lines. The file is JSONC (JSON with comments); single-line // comments and trailing commas are valid and must be preserved verbatim.
DefaultConfiguration is immutable
The per-bucket replacement edit (step 5b) covers only the "Providers": [ ... ] array. The surrounding bucket keys — BasedOn, DefaultConfiguration, and any others — stay byte-for-byte identical. Do not read these to make decisions, do not write them, do not re-emit them. If a bucket has no DefaultConfiguration today, do not add one. If it has "DefaultConfiguration": "SQLite.MS" today, do not change it to anything else.
Missing provider IDs
If a requested provider ID isn't present in the current bucket's Providers array, surface it explicitly in step 4 and ask the user whether to copy from the template (which has the full provider list) or skip the missing one. Do not auto-insert.
Container plan
For every container that the target enabled-provider set references (post-edit), compute one of:
running— container exists andinspectreturnedrunning. No action.will-start— container exists and status isexited/created. Action:docker start <name>.will-create— container does not exist. Action: runData/Setup Scripts/<script>.cmd(which creates and starts it; pulls the image if not cached).image-pull-needed— same aswill-createbut the image isn't cached locally either; setup script will pull. Surface the cost note when it's a heavy provider (DB2 / Informix / SAP HANA / SAP ASE — see Heavy providers (ask first) intest-databases.md).
Local non-docker SQL Server providers (SqlServer.2005 … SqlServer.2016 per test-databases.md → Local (non-docker) SQL Server) need no container action — note in the plan as local-instance.
4. Confirm with user
Show a single confirmation block in three sections:
Normalised inputs (only when step 1 produced rewrites — omit the section entirely otherwise):
Oracle → Oracle.23.Managed (bare-family + family rule)
MySql → MySqlConnector.8.0 (bare-family + family rule)
SqlServer → SqlServer.2019.MS (bare-family + family rule + override)
Oracle.12 → Oracle.12.Managed (family rule)
This is the user's chance to spot an unintended rewrite before any edit applies. If the user objects, restart from step 1 with their corrected input — do not patch the rewrite locally.
UserDataProviders.json — per affected TFM bucket, list:
NET100 enable: PostgreSQL.18
disable: Oracle.11.Native, Oracle.12.Native
sticky: TestNoopProvider (kept enabled)
no-change: 14 entries
Containers — one row each:
pgsql18 running (no action)
sql2022 will-start (docker start)
hana2 will-create (run Data/Setup Scripts/saphana2.cmd)
⚠ heavy: ~5–10 min startup, very high RAM (per test-databases.md)
Also include:
- The backup target path:
.build/.claude/UserDataProviders.json.bak.<ISO-timestamp>. - Whether
BaselinesPathis currently set; if unset, ask in this same prompt whether to set it (proposec:\\GitHub\\linq2db.blspertesting.md's Enabling baselines locally) — same numbered list, so the user can answer everything at once.
Wait for an explicit go-ahead before any mutation. On a refusal, stop cleanly — no partial application.
5. Apply
Run the confirmed plan. Order matters: JSON edits first, then container actions — if a setup script fails after a JSON edit, the user can re-run /test-providers to fix the container without re-typing the provider list, but a JSON edit after a successful container start would leave a half-applied state if the edit fails.
5a. Backup
On the first edit per session, copy the current file:
cp UserDataProviders.json .build/.claude/UserDataProviders.json.bak.<ISO-timestamp>
Single Bash call. Skip if the user explicitly chose skip-backup in the consent prompt; in either case keep an in-memory copy of the pre-edit contents for the duration of this /test-providers invocation in case the user aborts after the edit.
If the consent prompt was cancel, abort here — no edit, no container action.
5b. JSON edit (one Edit per affected bucket)
Per affected TFM bucket:
- The
old_stringis the exact current"Providers": [ … ]block from the opening[to the closing]. - The
new_stringis the same block with markers flipped per step 3. Do not reorder, do not regex-replace across the whole file, and do not split into oneEditper provider — that triggers N permission prompts. OneEditper bucket. - Leave whitespace, comments, and unrelated buckets exactly as they were.
If the user agreed in step 4 to set BaselinesPath: a separate Edit call inserts the "BaselinesPath": "c:\\GitHub\\linq2db.bls" field into the MyConnectionStrings block (see the example in testing.md → Enabling baselines locally).
5c. Container actions
For each will-start container: docker start <name> (one Bash call per container; can be parallelised across multiple tool calls in a single turn when the containers are independent — they almost always are).
For each will-create container: Data/Setup Scripts/<script>.cmd (one Bash call per script). These can take minutes; do not run more than two heavy-provider scripts in parallel because they compete heavily for I/O and RAM.
Record startedByUs[<container>] = true for every container we transitioned from exited / created / missing to running. Persist this map in memory for the lifetime of the agent session (read by step 6 of stop mode).
6. Stop mode
Triggered by /test-providers stop (with or without container names). Branches:
- Read state. Run a single
docker ps -a --format "table {{.Names}}\t{{.Status}}"call and filter for every container in thestartedByUsmap (and any containers the user named explicitly). Peragent-rules.md→ Docker containers: start/stop/create only, do not usedocker container inspect. - Confirm. Present a numbered list of running containers with their state and ask which to stop (
1,3,all,none). Default on empty reply isnone— never auto-stop. - Stop. One Bash call per chosen container:
docker stop <name>. Update the in-memory state map. - Report. What stopped, what stayed running.
Do not edit UserDataProviders.json in stop mode — the file is independent of container state, and disabling a provider doesn't require stopping its container.
7. Reset mode
Triggered by /test-providers reset.
- Confirm explicitly. Reset overwrites the user's local enable/disable choices and any custom
MyConnectionStringsentries. Make this clear in the prompt and require an explicit go-ahead. - Backup. Copy
UserDataProviders.jsonto.build/.claude/UserDataProviders.json.bak.<ISO-timestamp>(single Bash call). Same backup rules as step 5a; not skippable for reset (the destruction is wholesale). - Apply.
cp UserDataProviders.json.template UserDataProviders.json(single Bash call). - Report the backup path. Do not touch containers.
8. Report
End with a concise summary:
- Per affected TFM bucket: enabled-now, disabled-now, no-change counts.
- Per container: state transition (e.g.
pgsql18: exited → running). - Backup path.
- Any deferred items (heavy provider the user declined to start, BaselinesPath nudge skipped, etc.).
- One-liner: "Run
/test-providers stopwhen you're done with the containers."
Don'ts
- Do not run
dotnet testordotnet build. That's/test's job; this skill never invokes the test pipeline. - Do not silently edit
UserDataProviders.json. Step 4's confirmation is mandatory on every edit, includingadd/remove/reset. - Do not start heavy providers (DB2 / Informix / SAP HANA / SAP ASE) without surfacing the cost note from
test-databases.md. - Do not auto-stop containers. The user must name what to stop, or pick
all. Empty reply means "leave running". - Do not regex-replace across the whole
UserDataProviders.jsonor restructure it. Per-bucket batchedEditonly — see step 5b. - Do not edit
UserDataProviders.json.template. The template is the source of truth forreset; don't drift it from upstream. - Do not call
test-runneror invoke/test. The boundary is one-way:/testreads the state this skill left behind, never the other direction. - Do not edit TFM buckets the user didn't ask for. Default scope is
NET100only; widen exclusively via thein <bucket>clause.NETFX,NET80, andNET90should diff byte-for-byte clean after a default invocation. - Do not flip
TestNoopProviderto disabled in Set mode's sweep. It's sticky — seetest-databases.md→ Sticky entries. - Do not touch
DefaultConfiguration. Per-bucket edits replace only theProvidersarray; surrounding keys stay byte-for-byte identical. - Do not call
docker container inspectordocker image inspect. Container scope on this repo isdocker start/docker stop/docker create/docker psonly (peragent-rules.md→ Docker containers: start/stop/create only). Usedocker ps -a --filter name=<name>for state queries — see steps 2 and 6. - Do not auto-correct fully-qualified provider IDs. The family rules apply only to bare-family / version-only inputs;
Oracle.12.Native(and similar explicit variants) pass through unchanged even when the family rule would prefer Managed.