GhostGet

Turn an unexpected input into a repeatable test

Generate inputs from a rule, reduce a failure, and keep both the replay recipe and the smallest example.

Drafted with AI and reviewed by Codex editorial review (AI).

One amber fold interrupts an ivory paper strip and repeats in a separate small piece.
Generated with SlopCamera.

An unexpected input becomes much easier to fix when a test can produce it again. Property testing starts with a rule, generates inputs that exercise it, and reduces a failing input to a smaller example. Keeping that example makes the discovery useful long after the first failure.

Write the rule before the examples

An example test might check that reading and writing a record preserves a person’s name. A property test states the broader rule: for every valid record the generator produces, reading and writing it preserves all its fields.

The generator should include the awkward cases that ordinary examples tend to miss: empty values, unusual characters, nested objects, and keys that resemble language features. Those inputs are valuable because they challenge assumptions the happy path never touches.

A small key can expose a large assumption

One useful example is this JSON object:

{"__proto__":0}

JSON.parse treats __proto__ as an ordinary key. Some ways of copying properties onto an ordinary JavaScript object treat the same name specially. A copy can therefore lose the key instead of preserving the JSON value.

GhostGet keeps a regression test for this case and a nested variant. The example gives a maintainer something concrete to understand, while a generated round-trip property continues looking for related cases.

Keep the replay recipe with the failure

A generated test uses a seed to choose its starting sequence. When a case fails, the testing library tries smaller inputs that still fail; the shrink path records that reduction. With the same test, generator, and library version, the seed and path reproduce the case.

Here is a complete fast-check example. It checks that reversing an array twice preserves its values, and records replay information if the rule fails:

import assert from "node:assert/strict";
import fc from "fast-check";

const property = fc.property(fc.array(fc.integer()), (values) => {
  assert.deepEqual(values.toReversed().toReversed(), values);
});

const result = fc.check(property);
if (result.failed && result.counterexamplePath !== null) {
  console.log({ seed: result.seed, path: result.counterexamplePath });
  fc.assert(property, {
    seed: result.seed,
    path: result.counterexamplePath,
    endOnFailure: true,
  });
}

The final call runs the failing case again. For a real failure, save those reported values and rerun the same property with that seed and path while you investigate.

Save the smallest failing input as a named example as well. A library upgrade can change what a seed generates; the explicit example preserves the behavior you intended to protect.

Test sequences as well as values

Some bugs depend on order. A send succeeds, the connection drops, the program restarts, and the request is submitted again. Each individual step may look reasonable while the sequence duplicates the message.

A stateful property test generates actions and compares the running program with a simpler model after each step. For storage, a useful rule is that an interrupted update leaves either the old value or the new one, never half of each. For a consequential request, recovery should preserve whether it may already have been sent.

Choose failure points around the steps that change state: before a write, after it reaches disk, and before the caller receives a reply. A restart should preserve enough information to decide what happened.

Use a second implementation for shared standards

When a format has a written standard, an independently written implementation provides another useful comparison. Run the same input through both and investigate disagreements. URL handling and canonical JSON, a consistent byte representation used when hashing structured data, both have standards against which implementations can be compared.

Agreement on generated inputs adds evidence; it does not establish agreement on every possible input. Combine it with examples from the standard and permanent regressions for disagreements you resolve. The fast-check documentation explains how to build the generators and stateful models.