Four times my system looked correct and wasn’t
Most of the bugs that taught me something were not crashes. The system worked, the checks were green, and the property I cared about was still broken. That is a harder failure to catch because it does not announce itself. It looks like success.
For website and software teams, this is an architecture and delivery problem, not just a testing problem. A green check is only as good as the property it measures. If the check measures string assembly, UI visibility, happy-path behavior, or process liveness, it can pass while the actual security, correctness, or operational guarantee is broken.
Here are four times that pattern showed up, and what each one implies for architecture, ops, security, and delivery.

Figure: Editorial context image from the source page (preview only; rights remain with the publisher) · Credit / further reading: dev.to
1. The prompt-injection test that passed while the attack worked
In an LLM pipeline, three models answer a question, rank each other’s answers blind, and a chairman model writes the final synthesis. Output from one stage becomes input to the next, so untrusted model output has to be fenced off from the instructions around it.
The first version used static delimiters. The tests checked where the markers were placed and how the Python string was built. They passed.
The problem was that the tests checked the construction of the string, not the behavior of the system. An attacker does not care that the delimiter is in the right place in Python. They care whether they can close the fence, inject instructions, forge a ranking, or make a downstream model treat untrusted content as policy. A test that asserts "<untrusted>" in prompt can be green while the attack works.
Architecture implication: model output is data, never instruction. Do not build prompts by concatenating untrusted text into a privileged instruction channel. Use typed envelopes, explicit roles, allowlisted schemas, and constrained output formats where possible. Treat every model as an untrusted interpreter, and give tools and retrieval scopes the least privilege required for the task.
Security implication: adversarial tests must go through the full pipeline, not just assert string placement. Keep a corpus of injection payloads, canary tokens, and “forget previous instructions” variants. Assert that the attack fails to change control flow, not merely that a delimiter exists. Fail closed when the model returns malformed output.
Ops implication: log model inputs and outputs with redaction, and add detectors for suspicious instruction-like content. You need evidence after an incident, not just confidence before deployment.
Delivery implication: the acceptance criterion is “the exploit is blocked,” not “the builder function works.” That is a behavioral test, and it should be part of the release gate.
2. The authorization check that lived in the UI
A common version of this failure is a permission check that exists, but in the wrong place. The UI hides the “delete” button for users without the role. The end-to-end browser test passes because the button is absent. The API, however, still accepts the request if you call it directly.
The system looks correct because the tested path is correct. The untested path is the attack path. This is especially common when teams rely on front-end gating, internal network assumptions, or a gateway that only checks authentication and not authorization. The service then trusts a user ID, tenant ID, or role from the request body.
Architecture implication: enforce authorization at the service or data boundary, close to the resource. Deny by default. Do not let the UI be the policy engine. Prefer central policy with resource-level checks, and make tenant or user identity derive from the authenticated context rather than client-supplied fields.
Security implication: test negative cases directly against the API: wrong tenant, missing role, expired session, replayed token, forged headers. Include IDOR-style tests. If a proxy or gateway adds identity headers, make sure the service cannot be reached without that proxy and that spoofed headers are stripped.
Ops implication: audit authorization decisions with enough context to answer “who tried what, and why was it allowed?” Log deny decisions, not just errors.
Delivery implication: contract tests and API-level policy tests belong in CI. A browser test that only verifies the button is hidden is not an authorization test. It is a UI test pretending to be one.
3. The retry that looked idempotent
A request times out after the database commit but before the client receives the response. The client retries. The second request creates a duplicate charge, email, job, or record. In normal tests, the request succeeds once and the system looks correct. In production, ambiguous timeouts are normal.
This is a classic distributed-systems failure: the caller cannot distinguish “the server never processed it” from “the server processed it but the response was lost.” If the operation has side effects, a retry is not a repeat of a read. It is a second attempt to cause an effect.
Architecture implication: assume at-least-once delivery. Use idempotency keys for mutating requests, unique constraints for natural deduplication, and a transactional outbox for side effects. “Exactly once” is usually a property you build from at-least-once delivery plus deduplication, not a property you get for free.
Ops implication: inject failures at the worst point: after commit, before response, during callback, during queue acknowledgment. Monitor for duplicate side effects, not just HTTP 500s. A retry storm can look healthy at the request layer while producing duplicate business events.
Delivery implication: add a double-submit test to the release gate. If the same idempotency key is sent twice, the system must return the same result and perform the side effect once. That test is more valuable than another happy-path integration test.
4. The deploy that looked healthy because telemetry measured the wrong thing
A service returns 200 on its health check, the pod is running, and the dashboard is green. Meanwhile, a background worker is stalled, a cache is stale, or a dependency is misconfigured. The health check measures process liveness, not user-visible correctness.
This is the operational version of the same mistake: measuring the thing that is easy to measure instead of the property that matters. A system can be “up” and still be wrong.
Architecture implication: separate liveness from readiness from correctness. Readiness should reflect the dependencies needed to serve traffic. Correctness needs its own signals: queue age, freshness, reconciliation lag, error rate by user journey, and business-level invariants.
Ops implication: use synthetic transactions that exercise the real path, not just port checks. Alert on queue backlog and stale data, not only CPU and memory. If a worker is supposed to process events, alert when the oldest unprocessed event exceeds a threshold.
Delivery implication: progressive delivery needs correctness gates, not just error-rate gates. Canary a change and compare business invariants before widening rollout. Rollback should be rehearsed, and it should be triggered by the property that matters.

Figure: HEXOPS original illustration inspired by the topic (not a copy of the source article imagery) · Credit / further reading: dev.to
Builder notes: turn correctness into a gate
The common thread is that each system was tested against its implementation shape rather than its promised behavior. The fix is not “write more tests.” It is to name the property, then test the property through the path that can actually break it.
For website and software teams, that means:
- Test behavior, not structure. “The delimiter is present” is weaker than “the injected instruction cannot change control flow.”
- Test the deployed path. Full pipeline, real API, real proxy, real retry behavior. Unit tests are necessary but not sufficient for security and correctness properties.
- Enforce at trust boundaries. Authorization at the resource, untrusted input as data, and least privilege for tools and services.
- Assume partial failure. Retries, timeouts, duplicate delivery, and stale caches are normal production conditions.
- Measure user-visible correctness. Health checks and green dashboards are not enough; monitor invariants, freshness, and business outcomes.
- Make one failing test per incident. When a “looked correct” bug escapes, add the test that would have failed. Then keep it in the release gate.
A system that looks correct is not the same as a system that is correct. The difference is usually one well-chosen adversarial test, one policy enforced at the right layer, one idempotency key, or one metric that measures the property you actually promised.
Sources
- mk023, “Four times my system looked correct and wasn’t,” DEV Community: https://dev.to/mk023/four-times-my-system-looked-correct-and-wasnt-h07



