Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
2d8f9d9
feat(outpost): add Outpost API client, schema cache and live tests
leggetter Aug 14, 2026
85ba39c
feat(outpost): add outpost command group and tenant commands
leggetter Aug 14, 2026
a38bfe8
feat(outpost): add destination commands
leggetter Aug 14, 2026
9fce42b
feat(outpost): add destination-type commands and per-type dynamic help
leggetter Aug 14, 2026
ea4ee42
feat(outpost): support dotted paths in --config and --credential
leggetter Aug 14, 2026
000cd7f
feat(outpost): add event, attempt, publish, topic, metrics, config an…
leggetter Aug 14, 2026
c235fd5
test(outpost): add acceptance suite and CI slice
leggetter Aug 14, 2026
1303023
docs(outpost): add REFERENCE.md and README sections
leggetter Aug 14, 2026
640b00f
test(outpost): cover event get, attempt get, tenant token and retry
leggetter Aug 14, 2026
b09cda9
Update package.json version to 2.6.0-beta.1
github-actions[bot] Aug 14, 2026
9445c8a
refactor(mcp): extract product-agnostic MCP server into pkg/mcpcore
leggetter Aug 14, 2026
536935d
feat(outpost): add an MCP server for AI agent access
leggetter Aug 14, 2026
2914526
refactor(mcp): keep login and projects on the hookdeck_ prefix in eve…
leggetter Aug 14, 2026
bf22092
Merge remote-tracking branch 'origin/main' into integrate/outpost-mcp
leggetter Aug 14, 2026
2b4ec87
Merge remote-tracking branch 'origin/main' into feat/outpost-api-client
leggetter Aug 14, 2026
e7cf41a
fix(mcp): resolve project names, and scope the publish credential
leggetter Aug 14, 2026
eb6ca42
docs: regenerate REFERENCE.md for the publish-api-key rename
leggetter Aug 14, 2026
1868d69
fix(outpost): stop publish silently going to the wrong project
leggetter Aug 14, 2026
3b48ac6
test(outpost): update MCP acceptance test for the platform tool prefix
leggetter Aug 17, 2026
f1e1923
Merge remote-tracking branch 'origin/integrate/outpost-mcp' into feat…
leggetter Aug 17, 2026
da683e3
Update package.json version to 2.6.0-beta.2
github-actions[bot] Aug 17, 2026
f16f1a0
refactor(mcp): lift the write-gating machinery into mcpcore
leggetter Aug 19, 2026
29ee04d
feat(gateway mcp): add write mode behind --allow-write, rename tools …
leggetter Aug 19, 2026
3a2b4a5
test(gateway mcp): cover the write-mode gate, and document it
leggetter Aug 19, 2026
0317f11
test(outpost): prove every MCP action with a successful call
leggetter Aug 19, 2026
ff7e3b2
feat(outpost): add --metadata to destination create and update
leggetter Aug 19, 2026
c065c77
fix(outpost): make API errors readable, and cover the portal and cust…
leggetter Aug 19, 2026
d2d0b91
ci(outpost): run the live smoke test nightly instead of never
leggetter Aug 19, 2026
db63434
fix(outpost): break the project-switch loop on the first-run path
leggetter Aug 19, 2026
0506c0c
test(outpost): keep the custom-domain test from colliding with a para…
leggetter Aug 19, 2026
e536308
fix(gateway mcp): treat transformations run as a read, not a write
leggetter Aug 19, 2026
c18a1d7
test(gateway mcp): call every tool action successfully at least once
leggetter Aug 19, 2026
38e12ee
test(outpost): stop the portal test assuming propagation is stable
leggetter Aug 19, 2026
a965637
Merge Gateway MCP write mode and the gateway_ tool rename into v3.0.0
leggetter Aug 19, 2026
0eb550b
Update package.json version to 3.0.0-pin-pre-tool-shape-review
github-actions[bot] Aug 19, 2026
cd58f8b
ci: pass the CLI key to acceptance runs so project tests stop skipping
leggetter Aug 19, 2026
e0f090d
feat(gateway mcp)!: split events and requests into plural search and …
leggetter Aug 19, 2026
1ef3e21
docs(mcp): document the events/requests plural-singular split
leggetter Aug 19, 2026
eda57f9
revert: restore the package.json version after an accidental release
leggetter Aug 20, 2026
2ca2cdb
feat(gateway): add the missing documented query filters to events and…
leggetter Aug 20, 2026
689d698
fix(mcp): stop numeric filters being dropped when sent as JSON numbers
leggetter Aug 20, 2026
f8c59a5
fix(api): reject identifiers that would retarget a request
leggetter Aug 20, 2026
26ffa5b
fix(cmd): stop --help printing the API key from the environment
leggetter Aug 20, 2026
9cdb193
fix(mcp): stop boolean and numeric filters being dropped when quoted
leggetter Aug 20, 2026
e78bdbf
fix(mcp): name the login tool and publish key that actually exist
leggetter Aug 20, 2026
731d38b
fix(outpost): allow partial destination updates
leggetter Aug 20, 2026
f34a8cd
fix(mcp): send topics when creating an Outpost destination
leggetter Aug 20, 2026
05003ee
fix(mcp): stop a tool offering pause claiming to be a pure read
leggetter Aug 20, 2026
f2de696
fix(mcp): do not sign the user out when a reauth is never completed
leggetter Aug 20, 2026
deabb51
test(listen): capture subprocess output so a failure says why
leggetter Aug 20, 2026
77fb794
test(listen): run a built binary instead of go run
leggetter Aug 20, 2026
f929ff5
refactor(mcp): remove the unused ToolSpec.DefaultAction field
leggetter Aug 20, 2026
68f773a
chore: set the package version to the 3.0.0 line
leggetter Aug 20, 2026
738e61d
docs(search): say that search_term matches a whole value, not a subst…
leggetter Aug 20, 2026
61ca177
fix(outpost): send the schema's declared defaults when creating a des…
leggetter Aug 20, 2026
6c16792
fix(events): report the status the API returned, not the one requested
leggetter Aug 20, 2026
94a4c08
fix(transformations): report a run that failed instead of an empty re…
leggetter Aug 20, 2026
0103488
fix(mcp): hide write-only prose and properties in read-only mode
leggetter Aug 20, 2026
dbd7948
docs(outpost): record why webhook secrets are readable and others are…
leggetter Aug 20, 2026
8176987
docs(filters): stop claiming count filters accept operator syntax
leggetter Aug 20, 2026
0fece55
fix(outpost): refuse to publish to a tenant the credential cannot see
leggetter Aug 20, 2026
dd15126
fix(mcp): reject arguments a tool does not have, and honour "id or name"
leggetter Aug 20, 2026
303996f
feat(mcp): make the plural/singular split recoverable when an agent g…
leggetter Aug 20, 2026
7afc68f
fix(help): stop pointing users at things that cannot help them
leggetter Aug 20, 2026
60c108a
test(events): stop asserting that a no-op reports success
leggetter Aug 20, 2026
f37253d
docs(mcp): say which test fails if a deliberate choice is "fixed"
leggetter Aug 21, 2026
dda6646
fix(mcp): close three read-only leaks found in review
leggetter Aug 21, 2026
14c0cb6
test(acceptance): stop the suite leaking sources and destinations
leggetter Aug 21, 2026
66168a0
fix: report the state the API returned, not the one that was asked for
leggetter Aug 21, 2026
a734bf4
docs: add a manual QA skill with a credential guard
leggetter Aug 21, 2026
ccfb191
fix: reject MCP argument values the tool cannot use
leggetter Aug 21, 2026
411b70c
docs: say that an upsert with no metadata clears it
leggetter Aug 21, 2026
9581891
test(acceptance): run the outpost slice locally and report each slice
leggetter Aug 21, 2026
78f7b7b
fix: stop update commands discarding the values they were given
leggetter Aug 21, 2026
7dae336
fix(mcp): stop write mode reopening the filter-that-does-not-filter hole
leggetter Aug 21, 2026
a81ce5b
fix: report what a request retry actually did, and what a dismiss cha…
leggetter Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions .agents/skills/hookdeck-cli-manual-qa/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
---
name: hookdeck-cli-manual-qa
description: >-
Runs exploratory manual QA of the Hookdeck CLI and its MCP servers against
real acceptance-test projects, safely. Provides a credential guard that keeps
destructive commands off any project not named up front and never touches the
operator's own login, an MCP stdio driver, and per-surface checklists. Use
when manually testing CLI commands or MCP tools, exercising write paths,
validating a release candidate, or reproducing a defect against the live API.
---

# Hookdeck CLI — manual QA

Exploratory testing against the live API, in acceptance-test projects, without
putting anything else at risk.

This exists because the automated suites cannot cover everything. They run in
fixed shapes; a QA pass goes looking for the things nobody thought to assert —
wrong success messages, defaults that never get sent, filters that silently do
nothing. Every defect this process has found so far was of one kind: **a wrong
answer that reads like a right one.** Look for those specifically.

## Safety model — read before running anything

Manual QA runs destructive commands against real projects. Two rules make that
survivable, and both are enforced by [scripts/hd-testenv.sh](scripts/hd-testenv.sh):

1. **Never use the default config.** Every command takes
`--hookdeck-config "$HD_CONFIG"`. The operator's own `hookdeck login` is
never read or overwritten. This is not hypothetical — a suite run without an
isolated config has previously written CI credentials over a working login.
2. **The project is confirmed from the credential, not from the argument.** You
name the project you expect up front; the guard authenticates, resolves the
project, and refuses to hand back a config if it is anything else or is
outside the acceptance-test organisation.

Credentials come only from `test/acceptance/.env` (gitignored, CI test keys), so
the reachable blast radius is the test projects those keys can see.

**Before any delete, call `hd_assert "$HD_PROJECT"` and abort on failure.** It is
one cheap line and it catches a config swapped or a project switched mid-run.

If the guard refuses, **stop and read the message**. It fails closed on purpose;
working around it defeats the point.

### Setup

```bash
source .agents/skills/hookdeck-cli-manual-qa/scripts/hd-testenv.sh
hd_testenv HOOKDECK_CLI_TESTING_API_KEY tm_xxxxxxxxxxxx # gateway project
hd_testenv HOOKDECK_CLI_OUTPOST_TESTING_API_KEY tm_yyyyyyyyyyyy # outpost project

go build -o hookdeck . # test the build, not `go run` each time
./hookdeck --hookdeck-config "$HD_CONFIG" whoami
```

**Rebuild before every pass, and after every fix.** A stale binary reports the
old behaviour convincingly: missing tools, missing flags, old names. If
something looks broken in a way that seems too fundamental to be true, check
that first — it has already produced one false finding.

Ask the maintainer which project IDs to use if you do not already know them;
they are deliberately not hardcoded here.

## Running a pass

Two independent surfaces. Run them as separate agents when doing a full sweep —
they do not share state beyond the projects, and one must not wait on the other.

- **CLI surface** — [references/cli-checklist.md](references/cli-checklist.md)
- **MCP surface** — [references/mcp-checklist.md](references/mcp-checklist.md)

For MCP, drive the server with [scripts/mcp-call.py](scripts/mcp-call.py) rather
than wiring up an editor:

```bash
python3 .agents/skills/hookdeck-cli-manual-qa/scripts/mcp-call.py \
--config "$HD_CONFIG" --server gateway --list
```

## What to look for

Ordinary "does it work" testing finds little at this point. These are the
patterns that have actually yielded defects:

- **Does the message match the state?** Run the command, then read the resource
back independently. `enable` printing "enabled" proves nothing; a subsequent
`get` showing it enabled does. Several commands announced the verb they were
asked to perform rather than the state the API returned.
- **Does an omitted flag mean what the help says?** Documented defaults are not
necessarily sent. A destination type whose schema says `tls` defaults to on
was being created with `tls` unset.
- **Can you undo it?** Setting a value is usually tested; clearing it is not.
`--filter '{}'` reported success and changed nothing because `omitempty`
dropped it before it reached the wire.
- **Does an error exit non-zero?** An endpoint that answers `200` with
`{"success": false}`, or a body carrying its own status field, will be
reported as success by anything that only checks the HTTP status.
- **Does a filter actually filter?** Pass one and confirm the result set
narrows. A silently ignored filter returns plausible data that is wrong.
- **Do the read and write paths agree?** A fix applied to a CLI command and not
its MCP sibling leaves the defect open on the surface with no human watching.
Check both.

When something looks wrong, **verify it against the live API before reporting
it** — construct the minimal call that distinguishes the two explanations. A
report that says "verified: log_level is severity, not a completion flag, here
are the five cases" is actionable; "this looks suspicious" costs someone else
the same investigation.

## Clean up

Acceptance-test projects accumulate resources fast, and a QA pass adds to it.

- Name everything you create with a run-specific prefix (`qa-<date>-<n>`) so it
can be found and removed later.
- Delete what you created before finishing, in reverse order of creation.
Deleting a tenant cascades to its destinations.
- If you leave anything behind, say so explicitly in the report with the names
and the reason — silent residue is indistinguishable from a leak.

## Reporting

Return findings ranked by severity, each with: what you ran, what you expected,
what happened, and the evidence. Separate **confirmed defects** from **things
worth a look** — mixing them makes the confirmed ones cheaper to ignore.

State plainly what you did **not** cover. A pass that reports only successes is
usually a pass that did not go looking.
80 changes: 80 additions & 0 deletions .agents/skills/hookdeck-cli-manual-qa/references/cli-checklist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# CLI surface — manual QA checklist

Read [../SKILL.md](../SKILL.md) first, including the safety model. Every command
below takes `--hookdeck-config "$HD_CONFIG"`.

Two projects, two credentials:

| Surface | Key in `test/acceptance/.env` |
|---|---|
| Gateway (`hookdeck gateway …`) | `HOOKDECK_CLI_TESTING_API_KEY` |
| Outpost (`hookdeck outpost …`) | `HOOKDECK_CLI_OUTPOST_TESTING_API_KEY` |

This is a checklist of **surfaces**, not a script. The value is in what you try
against each one, so vary it between runs rather than replaying the same calls.

## Gateway resources

`source` · `destination` · `connection` · `event` · `request` · `attempt` ·
`transformation` · `issue` · `metrics`

For each resource that supports them, exercise `list`, `get`, `create`,
`update`, `upsert`, `delete`, and any state verbs (`enable`/`disable`,
`pause`/`unpause`, `retry`/`cancel`/`mute`, `dismiss`).

Beyond the happy path:

- [ ] **State verbs report the resulting state.** After every `enable`,
`disable`, `pause`, `cancel`, `mute`, run a separate `get` and confirm the
resource actually changed. Repeat the verb on a resource already in that
state and check the second call does not claim to have changed anything.
- [ ] **`retry` on a body carrying its own status.** Confirm a declined retry
exits non-zero rather than printing a tick.
- [ ] **Filters narrow the result set.** For every list filter (`--status`,
`--source-id`, `--search-term`, date ranges), compare filtered and
unfiltered counts. A filter the API ignores returns plausible wrong data.
`--search-term` matches a whole field value, not a substring — confirm
that is still true and that the help says so.
- [ ] **Pagination.** `--next` / `--prev` across a boundary; confirm no repeats
and no skips.
- [ ] **`transformation run`.** A handler that returns a request; one that calls
`console.error` and still returns (must succeed, exit 0, and show the
console output); one that throws; one that returns nothing (must fail).
- [ ] **Output modes.** `--output json` parses as JSON on both success and
failure paths, and the exit code matches the outcome in both.

## Outpost resources

`tenant` · `destination` · `destination-type` · `event` · `attempt` · `topic` ·
`config` · `metrics` · `publish` · `status`

- [ ] **Destination types beyond `webhook`.** Create one of each supported type
(AWS, GCP, Azure, RabbitMQ, Kafka) and confirm the stored config matches
what the schema declares — particularly any field with a default, which
must be sent rather than left unset.
- [ ] **Credentials are masked on read, except where the platform generates
them.** Webhook secrets are produced server-side and must come back;
caller-supplied credentials must not.
- [ ] **Clearing works, not just setting.** `--filter '{}'`, `--unset` on
config, removing a topic. Read the resource back to confirm.
- [ ] **`config set`.** Round-trip a value and restore it; confirm `--dry-run`
changes nothing and that a real set is visible in `config get`.
- [ ] **`tenant portal`** returns a URL, and `--theme light|dark` is accepted.
Do not test `--open`; it launches a browser.
- [ ] **`publish`** an event and follow it through `event list` → `attempt list`.

## Cross-cutting

- [ ] **Unknown flags and unknown enum values** are rejected with a message that
names the offending value.
- [ ] **Missing required arguments** fail before any network call.
- [ ] **A resource id containing `/` or `..`** is rejected as an invalid
identifier rather than addressing a different resource.
- [ ] **`--help` matches behaviour** — documented defaults are actually applied,
documented flags exist, and `REFERENCE.md` is current
(`go run ./tools/generate-reference --check`).

## Cleanup

Delete every resource created, in reverse order. Deleting an Outpost tenant
cascades to its destinations. Report anything left behind by name.
64 changes: 64 additions & 0 deletions .agents/skills/hookdeck-cli-manual-qa/references/mcp-checklist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# MCP surface — manual QA checklist

Read [../SKILL.md](../SKILL.md) first, including the safety model.

Two servers: `hookdeck gateway mcp` and `hookdeck outpost mcp`. Drive them with
[../scripts/mcp-call.py](../scripts/mcp-call.py).

```bash
QA=.agents/skills/hookdeck-cli-manual-qa/scripts/mcp-call.py
python3 $QA --config "$HD_CONFIG" --server gateway --list
python3 $QA --config "$HD_CONFIG" --server gateway --allow-write --list
```

The MCP surface is where defects hide, because no human reads its output. A
wrong answer here is consumed directly by an agent that will act on it.

## Mode gating

- [ ] **Read-only is the default.** With no flag, `tools/list` advertises no
write actions and the descriptions do not mention them.
- [ ] **`--allow-write` adds them**, and `HOOKDECK_MCP_ALLOW_WRITE` does the
same. `--read-only` wins when both are given.
- [ ] **The guard is real, not cosmetic.** Call a write action without
`--allow-write` and confirm it is refused — a tool hidden from the listing
but still dispatchable is not gated.
- [ ] **The refusal explains how to enable it**, naming the help tool.
- [ ] **`*_help` reports the current mode** and matches the actual listing.
- [ ] **Actions that change state but stay available read-only** (connection
`pause`/`unpause`) still work without the flag. That is deliberate —
incident response — and its tests are the proof it was not an oversight.
- [ ] **Annotations are honest.** `readOnlyHint` false for anything that
changes state; `destructiveHint` true for deletes, cancel, mute, dismiss.

## Per tool

Call every action of every tool, in both modes. For each:

- [ ] **Unknown arguments are rejected**, and the message distinguishes "no such
argument" from "that argument needs write mode".
- [ ] **A failure is reported as one.** Check `isError`, not just that a
response came back. An envelope whose body says the operation was declined
while the result reads as success is the defect to hunt.
- [ ] **No hardcoded status fields.** Anything reporting `"status": "queued"`,
`"deleted"`, or similar must reflect what the API returned, not what the
handler intended.
- [ ] **Write actions are equivalent to their CLI siblings.** Create a resource
both ways with the same inputs and diff the stored result. Schema defaults,
required-field fallbacks and filter handling have diverged between the two
before — a fix applied to one surface and not the other.
- [ ] **Credential masking** matches the CLI: platform-generated secrets
returned, caller-supplied credentials masked.

## Payloads

- [ ] **Raw body tools** (`*_events raw_body`, `*_requests raw_body`) return the
body unmodified, including non-JSON and large payloads.
- [ ] **List filters narrow the result.** Same test as the CLI: compare against
unfiltered. An ignored filter is worse here — the agent cannot tell.
- [ ] **Pagination** is expressible and terminates.

## Cleanup

Anything created through MCP is as real as anything created through the CLI.
Delete it, and confirm the delete with an independent `get`.
106 changes: 106 additions & 0 deletions .agents/skills/hookdeck-cli-manual-qa/scripts/hd-testenv.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
# Set up an isolated CLI config for one acceptance-test project, and refuse to
# hand it back unless the credential really resolves to that project.
#
# source hd-testenv.sh <ENV_VAR_NAME> <expected tm_ id>
#
# On success exports HD_CONFIG. Use it on EVERY command:
# go run . --hookdeck-config "$HD_CONFIG" gateway source list
#
# Two things this guarantees:
#
# 1. The default config (~/.config/hookdeck/config.toml) is never read or
# written, so a QA run cannot clobber the operator's own login. This has
# happened: a suite run without an isolated config wrote CI credentials
# over a working one.
# 2. A destructive command cannot run against a project that was not named up
# front. The project is confirmed from the credential itself, not from the
# argument, so a mistyped or rotated key fails closed rather than pointing
# at something unexpected.
#
# Safety here comes from where the credential lives, not from a hardcoded list
# of project IDs. Keys must come from test/acceptance/.env, which is gitignored
# and holds CI test keys, so the reachable blast radius is the test projects
# those keys can see. A list of IDs in a public repo would rot and would leak
# identifiers for no gain.
# Deliberately no `set -u` / `set -e` at this level: this file is sourced, so
# those would apply to the caller's interactive shell and break it. The
# functions below use explicit defaults instead.

# Organisation every acceptance-test project belongs to. Override if your test
# projects live elsewhere.
ALLOWED_ORG="${HOOKDECK_QA_ALLOWED_ORG:-Automated Testing}"

# Resolve the repo from the working directory rather than from the script's own
# path: BASH_SOURCE does not exist under zsh, which is where this is most often
# sourced from, and a wrong answer here silently looks for .env in the wrong place.
_hd_repo() {
if [ -n "${HOOKDECK_QA_REPO:-}" ]; then
printf '%s\n' "$HOOKDECK_QA_REPO"
return 0
fi
git rev-parse --show-toplevel 2>/dev/null && return 0
printf '%s\n' "$PWD"
}

hd_testenv() {
local var="$1" expected="$2"
local repo scratch
repo="$(_hd_repo)"
scratch="${HOOKDECK_QA_SCRATCH:-${TMPDIR:-/tmp}/hookdeck-qa}"
mkdir -p "$scratch"

case "$expected" in
tm_*) ;;
*) echo "REFUSING: $expected does not look like a project id" >&2; return 1 ;;
esac

local envfile="$repo/test/acceptance/.env"
[ -f "$envfile" ] || { echo "REFUSING: $envfile not found — QA keys must come from there" >&2; return 1; }
set -a; . "$envfile"; set +a

local key
eval "key=\${$var:-}"
[ -n "$key" ] || { echo "REFUSING: $var is not set in test/acceptance/.env" >&2; return 1; }

# Authenticate into a scratch path and move it into place only on success.
# Wiping the target first meant a failed attempt destroyed a config that had
# been working, and the next run then failed for an unrelated reason.
local cfg="$scratch/hdcfg-$expected.toml"
# The extension has to stay .toml: the config format is inferred from it, and
# a temp name ending in the PID fails with Unsupported Config Type.
local tmp="$scratch/hdcfg-$expected.new-$$.toml"
rm -f "$tmp"
(cd "$repo" && go run . --hookdeck-config "$tmp" ci --api-key "$key" >/dev/null 2>&1) \
|| { rm -f "$tmp"; echo "REFUSING: could not authenticate with $var (existing config left alone)" >&2; return 1; }
mv -f "$tmp" "$cfg"

# Verify against the credential, not against what we hoped it was.
local who actual
who=$(cd "$repo" && go run . --hookdeck-config "$cfg" whoami 2>&1)
actual=$(grep -oE 'tm_[A-Za-z0-9]+' "$cfg" | head -1)

if [ "$actual" != "$expected" ]; then
echo "REFUSING: config resolved to $actual, expected $expected" >&2
rm -f "$cfg"; return 1
fi
if ! grep -qF "$ALLOWED_ORG" <<<"$who"; then
echo "REFUSING: not the $ALLOWED_ORG organization:" >&2
echo "$who" >&2
rm -f "$cfg"; return 1
fi

export HD_CONFIG="$cfg"
export HD_PROJECT="$expected"
echo "OK: $expected — $(grep -oE 'on project .*' <<<"$who")"
}

# Re-assert before anything destructive. Cheap, and it catches a config that was
# swapped or a project switched mid-run. Call it immediately before any delete,
# and let a non-zero return abort the step.
hd_assert() {
local expected="$1" actual
[ -n "${HD_CONFIG:-}" ] || { echo "ABORT: HD_CONFIG is not set — run hd_testenv first" >&2; return 1; }
actual=$(grep -oE 'tm_[A-Za-z0-9]+' "$HD_CONFIG" | head -1)
[ "$actual" = "$expected" ] || { echo "ABORT: $HD_CONFIG points at $actual, not $expected" >&2; return 1; }
}
Loading
Loading