In a suite that can be pointed at a protected deployed target, where should the refusal to run destructive cases live?
answer
- Not every deployment may be written to
- A guard you must remember is forgotten
- One gate every case passes through
- Unknown deployment means protected, not permitted
basics
~20 sIn one shared gate the runner applies before every case, keyed off an attribute on the resolved target and a marker on the case. A guard each author must remember to write is a guard that will be forgotten.
solid answer
~50 sTwo declarations and one gate. The **resolved target** carries an attribute saying whether destructive work is permitted against that deployment, and it is written to fail closed, so a deployment whose entry says nothing is treated as protected. Each case that mutates or removes state it did not create declares itself **destructive**. The runner compares the two in the single path every case passes through before executing, and refuses the case there. What makes this work is that it is *one* place, enforced by the framework rather than by convention. A check written inside a case body is only as reliable as the author who remembered to write it, and the case that deletes the wrong records will be the one added in a hurry. Fail-closed matters just as much: a new deployment added with no attribute must not become the one that permits everything.
code
pseudocode · 12 linesTARGET_TABLE["prerelease"] = { ..., destructive_allowed: false }
TARGET_TABLE["local"] = { ..., destructive_allowed: true }
# a case declares its own blast radius
case "purge orders older than a year", destructive = true:
...
# ONE gate, in the runner, ahead of every case
before_each_case(case, target):
allowed = target.get("destructive_allowed", default = false) # fail closed
if case.destructive and not allowed:
refuse(case, reason = "destructive case against protected target " + target.name)go deeper
Know that a suite can be pointed at deployments where deleting or overwriting shared records is not acceptable, and that a case doing so must declare itself. Recall that the check belongs to the framework rather than to the case you happen to be writing.
Explain the two declarations — an attribute on the deployed target and a marker on the case — and the single pre-case gate that compares them. Be able to say why the default for a deployment nobody described must be protected rather than permitted.
Show the failure you are preventing: the hurried case, the mistyped target name, the guard nobody remembered. Talk about how a refused case is reported, how an override is recorded and expires, and how you would retrofit the gate onto a suite that has none.
Own the rule across the estate: which deployments automation may write to at all, who may change that, and what evidence a refused run leaves behind. Be ready to argue for a gate that occasionally blocks legitimate work over one that trusts everyone's memory.
## What counts as destructive Not every write is destructive. A case that creates its own records, exercises them and removes them afterwards changes nothing anybody else depends on. **Destructive** here means a case that mutates or removes state it did not create — purging records by age, resetting a shared account, truncating a table, rewriting a configuration row, cancelling other people's in-flight work. The test is simple: if the case runs and someone else's data is different afterwards, it is destructive. That definition matters because the guard has to be attached to something, and "writes anything" is too broad to be useful. A suite where every write is treated as destructive ends up with the gate disabled, which is the outcome you were trying to avoid. ## Three places the check could live | Placement | When it runs | How it gets forgotten | |---|---|---| | Inside the case body | only in cases whose author wrote it | the hurried case omits it, and that is the risky one | | In a base setup each case opts into | only for cases that opted in | a case written outside the base class simply misses it | | In the runner's single pre-case path | for every case, always | it cannot be, short of editing the gate itself | Only the third is a guard. The first two are conventions with a good reputation, and a convention's failure rate is the rate at which people are tired, new or in a hurry — which is exactly the population that writes the case that deletes the wrong records. The shape is: 1. Each deployment declares whether destructive work is permitted against it. 2. Each case declares whether it is destructive, in one line beside the case. 3. The runner, in the one path every case passes through before executing, refuses a destructive case against a deployment that does not permit it — and reports the refusal distinctly. ## Fail closed, and mean it The attribute must default to **protected**, not to permitted. The reasoning is asymmetric cost: a wrong default in the protected direction blocks a run and costs someone a one-line, reviewable edit; a wrong default in the permitted direction costs data that other people were relying on. And the entry most likely to be incomplete is the newest one — a deployment added at speed, by someone unfamiliar with the table, in the week when everything else is also changing. The same reasoning applies to the case marker. Where it is ambiguous whether a case is destructive, the ambiguous case is destructive. An unmarked case that touches shared state is a review finding, not a style note. ## What the gate is and is not - **It is not a substitute for narrow credentials.** The strongest complementary control is that a run pointed at a protected deployment should not be able to do the damage in the first place. That is a different layer, owned elsewhere, and it does not remove the need for a gate the suite itself enforces — the two fail in different ways and neither covers the other. - **It is not a control on who may start a run.** The gate answers "may this case do this here", not "may this person run this". Conflating them produces a gate that gets relaxed whenever the wrong person needs a run. - **It is not a naming convention.** Keeping destructive cases in a separate folder documents intent and enforces nothing; pointing the run at the wrong deployment still executes everything in that folder. - **It is not silent.** A refusal that looks like an ordinary inapplicable-case skip lets an entire destructive pack vanish from a run while the report still reads as complete. Give refusal its own outcome, count it, and fail the run when a pack that was expected to execute was refused wholesale. ## Practices 1. **Make the marker cheap.** If declaring a case destructive is one word beside the case, authors will do it. If it means inheriting from something or registering somewhere, they will not. 2. **Make overrides explicit inputs, not edits.** An urgent run against a protected deployment should require a recorded override supplied to the run, naming who asked and why, and it should expire. An override implemented by commenting out the gate is invisible a week later and the next person inherits a suite with no gate at all. 3. **Refuse before anything runs where you can.** Comparing declarations up front lets the run report "twelve cases refused against this deployment" in one place, rather than dribbling refusals through a long run. 4. **Retrofit outside in.** On a suite with no gate, add the attribute and the pre-case check first with everything permitted, watch what it would have refused for a week, then flip the default to protected. You get the inventory before you get the argument.
- Should a blocked case be reported as skipped, or as something else?As something else, and loudly. A refusal that looks like the routine inapplicable-case skip lets a whole destructive pack disappear from a run while the report still reads as complete. Treat refused-by-policy as its own outcome, count it, and fail the run when a pack that was expected to execute was refused wholesale.
- Where does the destructive marker on a case come from — the author, or something automatic?The author declares it and review enforces it. Nothing reliably infers intent from code, and an inference that errs in the permissive direction is precisely the failure the gate exists to prevent. Make the marker cheap to add, treat the ambiguous case as destructive, and read an unmarked case that touches shared state as a review finding.
- A team disables the gate for one urgent run against a protected deployment. What do you require?That the override is an explicit, recorded input to the run rather than an edit to the gate, that it names who asked and why, and that it expires. An override implemented by commenting out the check is invisible a week later, and the next person inherits a suite with no gate at all.
A door everyone is asked to lock behind them stays open; a door that locks itself does not depend on anybody's memory.
saying these in an interview costs you the question
- Putting the guard inside each destructive case body
- Treating a deployment with no setting as safe to write to
- Relying on folder names to keep cases off a deployment
- Turning the guard off for one run and leaving it off
- Assuming nobody will ever point the suite at the wrong deployment