The generated function still needed a test

GitHub Copilot's 2021 technical preview could produce a convincing whole function from nearby context; Z29C supplied the counterexample that made its hidden fallback visible.

GitHub Copilot's technical preview arrived with an immediately understandable trick: write a function name, signature, or comment, and the editor could suggest whole lines or an entire function.

I tried the idea on a Z29C fixture parser. Given TypeScript types and the name parseAttemptEvent, the completion produced a compact transformation that read the JSON fields, converted timestamps, and returned the expected event shape.

The function compiled. It passed the ordinary fixture. It was also wrong.

When observedAt was missing, the generated code created a date at the Unix epoch. That is a familiar defensive fallback. In Z29C, a missing observation time meant malformed evidence and had to remain distinguishable from a real event at time zero.

The generated function looked finished. The missing test restored the question it had answered on my behalf.

The technical preview changed the size of a suggestion

Traditional autocomplete had already saved enormous mechanical effort. It completed names, properties, imports, and syntax from relatively visible sources.

Copilot's preview could propose control flow and behavior. Its launch described drawing context from the code being edited and suggesting whole lines or functions, with particular early strength across Python, JavaScript, TypeScript, Ruby, and Go.

That difference was qualitative. A property completion usually asked “which name?” A function completion could answer:

  • What counts as valid input?
  • Which error becomes recoverable?
  • What default should missing data receive?
  • Which API version probably exists?
  • Which state should unknown map to?

The editor inserted the answer with the same low-friction gesture used for a variable name.

The tool did not become an authority because the suggestion was larger. The review surface had expanded beyond syntax.

The surrounding type was not the whole contract

The parser signature looked informative:

function parseAttemptEvent(input: unknown): AttemptEvent

The AttemptEvent type required a valid observedAt: Date. A generator trying to satisfy the type needed to produce one even when input was missing. The epoch fallback made the output type-check.

The real contract was a result type:

type ParseResult<T> =
  | { ok: true; value: T }
  | { ok: false; issues: ParseIssue[] }

Changing the signature exposed that invalid input was expected and meaningful. The completion now had less room to invent success, but it could still choose which fields were required or how errors were labeled.

I added a behavior table before requesting more code:

valid timestamp   → parsed instant
missing timestamp → required-field issue at observedAt
invalid timestamp → invalid-time issue preserving source text
unknown field     → preserved in extension data

Types constrained representation. Examples and tests constrained meaning.

One counterexample was worth more than visual review

Reading the generated function did not immediately make the epoch fallback feel dangerous. new Date(value ?? 0) was short and familiar.

The counterexample made the domain consequence visible:

it('rejects an event without observedAt', () => {
  const result = parseAttemptEvent({
    kind: 'adapter-response',
    attemptId: 'a-17',
  })
 
  expect(result).toEqual({
    ok: false,
    issues: [
      expect.objectContaining({ path: ['observedAt'], code: 'required' }),
    ],
  })
})

The test did not prove the parser correct. It encoded the important disagreement between a common fallback and Z29C's evidence model.

I began writing the smallest semantic counterexample before accepting a whole-function suggestion. For a retry classifier, it was an effect accepted with response lost. For a lease transition, it was a stale worker returning late. For authorization, it was a request valid at creation and denied at execution.

Plausible code needs an implausible-looking case that represents the actual domain.

Generated tests could ratify the generated decision

Copilot was also effective at suggesting tests. If I generated tests after accepting the implementation, nearby code strongly suggested what the expected behavior should be.

A completion could produce:

expect(parseAttemptEvent({ attemptId: 'a-17' }).observedAt)
  .toEqual(new Date(0))

The suite would become internally consistent and externally wrong. The implementation had chosen the policy; the generated test preserved it.

I separated semantic cases from mechanical expansion. I wrote or reviewed the behavior table and expected outcomes. Completion could generate fixture setup, repetitive permutations, or assertion scaffolding. It did not get to invent the expected result for a new consequential case.

Property tests offered another independent source when the invariant was real:

  • Serialization round trips preserve attempt identity and unknown fields.
  • A succeeded step never transitions back to pending.
  • Sorting events remains stable under equivalent timestamps.
  • Reusing one idempotency key with changed input never succeeds silently.

Tests are useful when they constrain the suggestion, not when they merely echo it.

The first pause was valuable information

Writing the parser manually would have forced me to pause at the missing timestamp. Should it default, remain optional, or fail? The pause was not wasted typing time. It marked a product decision.

Whole-function completion removed the pause by selecting a conventional answer. That felt productive because the code remained syntactically smooth.

I changed the order:

  1. Name the domain boundary.
  2. Write the type and semantic examples.
  3. Identify the case where a common convention would be wrong.
  4. Accept or request a completion inside that contract.
  5. Run focused tests and inspect the diff.

The tool could still accelerate the interior. It stopped defining the boundary through momentum.

This was the first version of what I later called the assumption budget. A task can tolerate only so many unstated decisions before verification costs more than generation saves.

Repository context was partial context

The preview drew from code near the editor and other available context. Z29C's most important rules were distributed across types, state-transition tests, adapter contracts, and the Work design note.

The parser file alone did not say why epoch zero was a dangerous substitution. The name observedAt looked like an ordinary timestamp. A comment explaining the evidence invariant improved suggestions and human review:

// Missing observation time is invalid evidence; never synthesize an instant.

I did not fill files with prompt-shaped comments solely for the tool. I added comments where a future human reader also needed the non-obvious constraint.

For a substantial task, I brought the relevant contract into the working context through types, named helpers, and focused tests. If the implementation required understanding the whole workflow engine to verify, one large completion was the wrong unit.

Context retrieval remained my responsibility. The tool's suggestion did not prove it had found the right source of truth.

Smaller functions made suggestions safer to judge

The first generated parser handled validation, normalization, timestamp conversion, extension fields, and error formatting in one function. Its compactness made the fallback easy to miss.

I split the boundary:

readObject
requireString
parseObservedTime
preserveExtensions
buildAttemptEvent

Each helper had a narrow result and test. Completion worked well on repetitive field extraction once the error semantics were fixed. The orchestration became readable enough to review without trusting one dense block.

This was not code fragmentation for the sake of generation. The same decomposition improved maintenance and localized policy. parseObservedTime carried the one rule every event parser needed.

Generated code was easier to use safely when the surrounding architecture made invalid assumptions difficult to express.

The tool became a beneficiary of good boundaries rather than the reason for arbitrary abstraction.

Compilation caught invented names, not invented meaning

Some suggestions referenced a plausible helper or API that did not exist. TypeScript and the installed dependency versions caught those quickly.

Other mistakes compiled:

  • Mapping unknown outcome to failure because both prevented progress.
  • Using worker lease expiry as evidence of non-execution.
  • Sorting by source time when response order required ingestion time.
  • Retrying every thrown exception.
  • Omitting an authorization recheck before an external boundary.

These were valid programs under the wrong system model.

I kept the compiler in the loop and stopped treating it as the main verifier. Documentation was required for unfamiliar library APIs. Scenario fixtures were required for Z29C semantics. Security-sensitive suggestions received manual reasoning before code execution.

The confidence gap was largest where the code used familiar constructs to make an unfamiliar decision.

A function can be type-correct and epistemically wrong.

Suggestions were proposals, not provenance

The preview could produce code resembling common patterns. I could not assume a suggestion came with a reliable source, license explanation, or current documentation context.

For unfamiliar algorithms or distinctive larger passages, I did not accept code I could not explain and independently verify. I searched authoritative documentation for APIs and wrote the implementation from the project contract. A suggestion could point toward a term or approach; it was not a citation.

I also checked dependency names and versions rather than allowing a plausible import to create a new package choice. Adding a dependency remained an architectural and supply-chain decision.

The relevant question was not whether the output looked original. It was whether I understood every consequential behavior and had the right to maintain the result.

This kept the early tool in a bounded role. It accelerated code I could own.

I tried completion on a small authorization helper and rejected the first suggestion. It interpreted missing policy data as deny, which was safe, but cached the result without including policy version. A long-running Z29C workflow could keep a stale allow decision.

The code looked cautious and hid a temporal authorization bug.

For authority, privacy, schema migration, and external side effects, I wrote the state transition and threat assumptions before code. Completion could fill exhaustive mappings only after the cases were named.

Examples included:

  • Policy allows at intent creation and denies at execution.
  • Credential expires after request identity is persisted.
  • One account resumes a draft authored under another account.
  • External response contains a secret in an error field.

The shortness of a helper did not increase the budget. Consequence reduced it.

Generated completion was most useful where the contract was explicit and the output cheap to verify.

Once the behavior table existed, Copilot helped with the parts that deserved acceleration:

  • Constructing fixture variants with different missing fields.
  • Mapping stable error codes to display labels.
  • Building exhaustive switch scaffolding.
  • Creating serialization round-trip cases.
  • Expanding worker-termination checkpoints.
  • Writing typed adapters between similar internal shapes.

I reviewed each result and often kept most of it. The time saved was real.

The common property was not that the code was “boilerplate.” Some repetitive boundaries were consequential. The useful property was that expected behavior was already constrained and locally testable.

Generation turned a behavior matrix into code quickly. It was weaker at deciding what the matrix should contain.

That division made the tool feel less magical and more dependable.

I recorded acceptance and correction examples

To evaluate the preview rather than rely on memory, I kept a small private note for several sessions:

  • Task and available context.
  • Size and kind of suggestion.
  • Accepted, edited, or rejected.
  • Hidden assumption found.
  • Verification method.
  • Whether completion saved time after review.

I avoided fabricated productivity percentages. The sample was small and the work varied too much.

Patterns emerged. Typed transformations and fixture expansion were strong. Repository-specific state machines required more correction. Unfamiliar APIs needed documentation regardless of confidence. Generated tests helped after expected outcomes were independently supplied.

The note made the later November assessment more than a first impression. It also preserved failures that successful final code would otherwise erase.

Tool evaluation needed its own evidence.

The red test changed the interaction

The most reliable sessions began with a failing domain test already in the repository. The suggestion then entered a visible constraint system. Accepting it produced immediate evidence instead of a moment of aesthetic satisfaction.

For the timestamp parser, I kept three tests red before requesting implementation: required field missing, invalid time text, and unknown field preservation. The ordinary valid fixture was deliberately not enough. Copilot could fill a first version, and the test output identified which policy it had guessed incorrectly.

This did not mean test-driven work guaranteed a good contract. A wrong expected outcome would constrain the code just as efficiently. I reviewed the cases against the Z29C event model before treating red as authoritative.

The workflow also limited suggestion size. I asked for one helper or one exhausted mapping rather than a complete parser plus tests. Smaller completions failed nearer the decision they contained. When a generated change made several red tests green at once, I inspected which branches achieved it and added mutation-like counterexamples where an overbroad fallback could satisfy the suite accidentally.

I occasionally wrote a deliberately hostile fixture by hand after the code passed: extra whitespace around a timestamp, a numeric value where a string was required, or an extension field named like an internal property. The cases came from the input boundary, not from the shape of the implementation.

The tool made green arrive faster. A good test sequence ensured that green still meant a claim I had chosen.

Acceptance speed was not review speed

Inline completion compressed a large decision into one Tab press. That ergonomic success could make a whole function feel equivalent to accepting a property name.

I introduced a pause proportional to semantic surface, not interaction cost. A one-line completion that changed authorization or fallback behavior received more scrutiny than a twenty-line fixture builder. I read the diff after acceptance instead of continuing immediately into the next completion, because rapid chained suggestions made it difficult to remember which behavior I had consciously chosen.

For larger suggestions I temporarily disabled further completion while reviewing and running the focused tests. This was not a product recommendation for every developer; it was a way to prevent generation and verification from blending into one optimistic flow.

I also checked deletion. A suggestion often added generic error recovery or abstraction that the task did not require. Removing those lines reduced the number of assumptions and made the remaining code easier to verify. Generated code had the same obligation to justify complexity as handwritten code.

The accepted result needed to remain maintainable with Copilot turned off. Names, tests, documentation, and boundaries had to explain the implementation without access to the prompt-like context that produced it.

Keystroke speed was the feature I could feel immediately. Review speed depended on how much unspoken policy the completion had placed behind that keystroke.

A generated function still entered ordinary review

I did not create a separate quality standard for AI-assisted code. The final diff needed the same explanation, tests, formatting, type checks, lint, and scenario validation as code I typed manually.

The review included one extra question: which decisions arrived inside the suggestion without an explicit design step?

I compared the accepted diff to the behavior table, not to the empty file. I checked error paths, input boundaries, dependency changes, and code that looked unusually general for the task.

If auditing took longer than a direct implementation would have, I rejected the completion and used what I had learned to write the smaller solution. Sunk keystrokes were not a reason to keep code.

The tool changed authorship of the first draft. It did not change ownership of the repository.

The first result was promising for the right reason

The parser experiment did not show that Copilot could replace a developer or understand Z29C. It showed that a model trained to complete code could produce a useful, plausible implementation quickly from local cues.

That was valuable. It moved effort from mechanical production toward contract and verification—if I chose to spend the saved effort there.

The epoch fallback became the durable lesson. A conventional answer can be more dangerous than a syntax error because it passes through familiar review. The model had not invented an absurdity. It had selected a pattern that fit many codebases and violated this one.

The function still needed a test, and the test needed an expected outcome that came from the domain rather than the function.

Generated code could begin the implementation. It could not be the reason I believed the implementation was right.