Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4e508f1
test(stack): encode managed stack acceptance contract
jgoux Aug 10, 2026
42a9cc1
test(stack): strengthen managed stack contract
jgoux Aug 10, 2026
0c1b010
test(stack): address managed stack review
jgoux Aug 10, 2026
4b4705d
test(stack): tighten managed contract invariants
jgoux Aug 10, 2026
09cc918
test(stack): align git identity fixtures
jgoux Aug 10, 2026
5360a05
test(stack): pin patched imgproxy contract version
jgoux Aug 10, 2026
1bed79e
test(stack): tighten contract conformance rules
jgoux Aug 10, 2026
a95a8cb
test(stack): encode Git identity scopes
jgoux Aug 10, 2026
c362e15
test(stack): tighten cross-scenario invariants
jgoux Aug 10, 2026
aac1248
test(stack): make contract transitions executable
jgoux Aug 10, 2026
3bce8a1
test(stack): cover credential persistence paths
jgoux Aug 10, 2026
258d6cc
test(stack): close contract validation gaps
jgoux Aug 10, 2026
7fd8bd2
test(stack): bind remaining contract transitions
jgoux Aug 10, 2026
6e31393
test(stack): close transition and target gaps
jgoux Aug 10, 2026
3907ab1
test(stack): exercise remaining contract matrices
jgoux Aug 10, 2026
fd766c1
test(stack): bind destructive targets and matrices
jgoux Aug 10, 2026
91a1d8d
test(stack): bind start and concurrency preconditions
jgoux Aug 10, 2026
480a170
test(stack): bind managed inputs to outcomes
jgoux Aug 10, 2026
e05e825
test(stack): bind public actions to contract results
jgoux Aug 10, 2026
1a0ec2d
test(stack): enforce lifecycle preconditions
jgoux Aug 10, 2026
1d81e8d
test(stack): project runtime unavailability reason
jgoux Aug 10, 2026
336cba8
test(stack): bind lifecycle evidence to outcomes
jgoux Aug 10, 2026
9ffa31b
test(stack): bind contract evidence to results
jgoux Aug 10, 2026
d9f26e4
test(stack): tighten contract decision evidence
jgoux Aug 10, 2026
620f35b
test(stack): bind remaining contract projections
jgoux Aug 10, 2026
d35b51e
test(stack): close contract evidence gaps
jgoux Aug 10, 2026
d1d1941
test(stack): enforce remaining contract invariants
jgoux Aug 10, 2026
de4e22c
test(stack): complete observable contract bindings
jgoux Aug 10, 2026
783bbb1
test(stack): bind remaining lifecycle projections
jgoux Aug 10, 2026
eed459f
test(stack): bind remaining state projections
jgoux Aug 11, 2026
3398c7f
test(stack): close remaining evidence triggers
jgoux Aug 11, 2026
d219a4b
test(stack): tighten managed contract invariants
jgoux Aug 11, 2026
5b20d30
test(stack): simplify managed contract scaffolding
jgoux Aug 11, 2026
2129033
test(stack): tighten fixture data hygiene
jgoux Aug 11, 2026
f47cf7f
test(stack): complete fixture boundary coverage
jgoux Aug 11, 2026
3fc14f6
test(stack): tighten contract fixture lint
jgoux Aug 11, 2026
e419519
test(stack): align contract fixtures with runtime
jgoux Aug 11, 2026
532476f
test(stack): close contract soundness gaps
jgoux Aug 11, 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
169 changes: 169 additions & 0 deletions docs/adr/0015-managed-stack-contract-fixtures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# 0015. Managed Stack Contract Fixtures

**Status**: proposed
**Date**: 2026-08-10

## Problem Statement

The managed local-stack design combines project, checkout, branch, and named-stack identity with
mutable state, host-wide port allocation, runtime selection, legacy bootstrap, credentials, and
reclamation. These decisions affect both the reusable `@supabase/stack` package and the CLI. If each
layer encodes its own behavior matrix, they will drift and tests will eventually validate
implementation details instead of the behavior developers observe.

The persistence technology is intentionally not part of the product contract. A later adapter may
use SQLite or another store, but changing storage must not change identity or lifecycle semantics.

## Decision

The typed fixtures exported from `@supabase/stack/testing` are the normative executable description
of the M1 managed-stack behavior. Each scenario records:

- explicit input state;
- a public CLI, Git, direct-stack API, or managed-stack API action;
- the resolved opaque identities and outcome;
- the complete set of permitted managed-state writes and runtime side effects; and
- human, JSON, or programmatic output, including structured warnings and deterministic recovery
guidance.

Opaque symbolic IDs make the same scenario reusable across an in-memory repository, a persistent
adapter, the managed package, and future CLI integration tests. Linear records the decision history
and links to implementation work; it is not a second source of executable truth.
Each scenario starts from its own isolated `given` state, so a symbolic ID or port has no shared
state across scenarios unless a fixture explicitly references another scenario. Conformance drivers
must reset their repository between scenarios.

Structured error and warning codes follow ADR 0001's `SCREAMING_SNAKE_CASE` convention. A `report`
is always read-only, and an `error` has no state mutation or runtime effect except the explicit
failed-bootstrap rollback, whose only permitted effects remove partial managed state.

`@supabase/stack` has two distinct public responsibilities:

1. Direct `createStack(config)` creates one caller-controlled stack. Omitted stack and runtime roots
are resolved independently as disposable temporary directories and are removed on disposal.
Supplying project, cache, or one state-root path does not make another omitted state root
persistent. Direct usage does not inspect Git, create identity markers, or mutate a global
managed registry.
2. The explicit managed surface owns system-aware discovery, identity, stack selection, ports,
runtime persistence, bootstrap, and reclamation. It accepts an isolated state root or injected
repository so applications and tests can use it without the CLI.

The CLI is a consumer and presentation layer. It translates arguments into managed operations and
projects managed results into human and JSON output. It must not implement a second identity,
selection, port, runtime, or lifecycle decision path.

Git workspaces store project, checkout, and context identities in Git-local metadata, using common
or worktree scope as appropriate. Contract effects record that scope explicitly: project identity
uses common Git config, while checkout and context identities use worktree-local config. Context
writes also declare their owning branch so storage adapters cannot persist an unbound context. A
tracked working-tree identity marker is inert: discovery never trusts or rewrites it. Ordinary
non-Git folders persist a project-local, untracked identity marker on first start and recover that
same project, checkout, and context identity on later starts.

Read-only status remains a successful `report` when it can identify a running stack but finds
unapplied port, credential, or runtime configuration. The report includes a structured warning and
recovery guidance. Conditions that prevent safe identity selection, such as ambiguous ownership,
remain errors.

Persistence sits behind the managed package's repository boundary. Contract fixtures must run
against a storage-independent test repository and then against each selected persistent adapter.
The choice of SQLite, files, or another implementation does not move product policy into the CLI or
change the package boundary.

## Testing Strategy

Tests should be as close as possible to how a developer uses the product:

- Package integration tests invoke public direct or managed APIs and compare their observable
result with the shared fixture.
- CLI integration tests invoke command handlers and assert argument translation plus human/JSON
projection from that same managed result.
- Repository conformance tests execute the same fixtures against the isolated repository and the
selected persistent adapter.
- Unit tests are reserved for genuinely pure algorithms and public export/type checks; they do not
duplicate the behavior matrix through private helpers.
- E2E tests cover a small number of real subprocess/runtime golden paths. Add a targeted E2E test
when a boundary cannot be represented faithfully in an in-process integration test, rather than
mocking away the behavior under test.

CLI-2102 checks in the fixture data and public direct-stack boundary before the managed engine and
persistent adapter exist. The implementation issues it unblocks must attach real drivers to these
fixtures. CLI integration coverage begins when a real command boundary exists; a fixture-presence
test is not evidence that an unimplemented command already satisfies the behavior.

The fixture validator is deliberately fixture lint, not a second implementation of the managed
stack policy. It checks a small set of generic rule families:

- catalog shape and unique scenario identity;
- referential integrity for selected, written, and effected identities;
- state-write and runtime-effect pairing;
- structured diagnostic and read-only outcome shape; and
- consistency between the managed result and its human, JSON, and API projections.

The lint implementation lives separately in `managed-stack-contract-validation.ts` so the contract
module remains centered on types and normative scenario data.

The native qualification matrix derives service names and versions from the package service catalog
so it cannot drift from the shipped manifest. Identity resolution, lifecycle preconditions, port and
runtime selection, bootstrap policy, credential policy, and reclamation semantics belong to the real
managed resolver and engine delivered by the implementation issues below. Further requests to
"validate" those semantics should be covered by running these scenarios against that implementation,
not by expanding this lint into a parallel rule engine. A new lint rule is appropriate only when it
protects a generic fixture-format invariant across behavior areas.

## Implementation Handoff

The downstream implementation issues own the executable drivers, while this ADR and fixture data
own the expected behavior:

- CLI-2106, CLI-2107, and CLI-2108 attach the repository and identity resolver to the identity
fixtures, including ordinary folders, worktrees, branches, and orphan handling.
- CLI-2109 attaches automatic legacy bootstrap and rollback-safe publication.
- CLI-2110 attaches exact and automatic port intent, allocation, stickiness, drift, and collisions.
- CLI-2124 attaches runtime selection, persistence, and strict conflict handling.
- CLI-2114 attaches the experimental CLI handlers and verifies that their human and JSON output is
projected from managed results.
- The selected persistent adapter must run the same repository contract as the isolated test
repository before its implementation issue is complete.

## Rationale

A single typed matrix makes disagreements visible in review and allows every layer to consume the
same expected result. Public-interface integration tests survive refactors because they assert
commands, API calls, outputs, and state transitions rather than internal call graphs. Injected
repositories keep system-aware behavior programmatically reusable while preventing a persistence
choice from leaking into product semantics.

Keeping direct and managed stack creation separate also preserves a simple embedding API for tests:
`createStack()` remains isolated, while callers that want branch/worktree-aware state opt into the
managed surface explicitly.

## Consequences

### Positive

- Package, CLI, and persistence adapters share one reviewed behavioral authority.
- Tests describe developer-visible journeys and remain useful through implementation refactors.
- Programmatic consumers can use managed state without importing CLI code.
- Direct test stacks stay isolated from Git and system-wide state.
- Storage technology can change without changing package ownership or managed semantics.

### Negative / Trade-offs

- The fixture catalog is intentionally large because it records edge cases that otherwise become
implicit behavior.
- New managed behavior requires updating the shared matrix before layer-specific tests.
- Until downstream implementations attach real drivers, fixture catalog tests validate contract
completeness and projection seams, not the future engine itself.

## Alternatives Considered

1. **Duplicate package and CLI test tables**: rejected because identity and lifecycle rules would
drift and reviewers could not identify the authoritative result.
2. **Make CLI tests authoritative**: rejected because managed behavior must be reusable from Node
and Bun without the CLI.
3. **Define behavior through a SQLite schema**: rejected because schemas describe persistence, not
product semantics, and would make a technology choice distort package boundaries.
4. **Put the whole matrix in E2E tests**: rejected because the suite would be slow and failure
diagnosis poor. E2E remains the fallback for boundaries that integration tests cannot exercise
faithfully.
27 changes: 14 additions & 13 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,21 @@ When an ADR becomes outdated, mark it as `deprecated` or reference the supersedi

## ADR index

| ID | Title | Status |
| ---- | ---------------------------------------------------------------------------------------- | -------- |
| 0000 | [Use ADR to Record Decisions](0000-use-adr-to-record-decisions.md) | accepted |
| 0001 | [CLI DX Architecture: The 7 Pillars](0001-cli-dx-architecture-pillars.md) | accepted |
| 0002 | [CLI Product Metrics](0002-cli-product-metrics.md) | accepted |
| 0003 | [Self-Documenting CLI & Documentation Strategy](0003-self-documenting-cli.md) | accepted |
| 0004 | [CLI Design Goals & Development Workflows](0004-cli-design-goals-and-workflows.md) | accepted |
| ID | Title | Status |
| ---- | ------------------------------------------------------------------------------------------ | -------- |
| 0000 | [Use ADR to Record Decisions](0000-use-adr-to-record-decisions.md) | accepted |
| 0001 | [CLI DX Architecture: The 7 Pillars](0001-cli-dx-architecture-pillars.md) | accepted |
| 0002 | [CLI Product Metrics](0002-cli-product-metrics.md) | accepted |
| 0003 | [Self-Documenting CLI & Documentation Strategy](0003-self-documenting-cli.md) | accepted |
| 0004 | [CLI Design Goals & Development Workflows](0004-cli-design-goals-and-workflows.md) | accepted |
| 0005 | [OpenAPI-Driven Code Generation for CRUD Commands](0005-openapi-driven-code-generation.md) | proposed |
| 0006 | [Environment Management & Variable Resolution](0006-environment-management.md) | proposed |
| 0007 | [Real-time Progress in Command Handlers](0007-realtime-progress-in-command-handlers.md) | proposed |
| 0008 | [Authentication & Token Management](0008-authentication-and-token-management.md) | proposed |
| 0009 | [Configuration Schema & Validation](0009-configuration-schema-and-validation.md) | proposed |
| 0011 | [CLI Release & Distribution Strategy](0011-cli-release-and-distribution-strategy.md) | proposed |
| 0013 | [Live E2E Tests Bypass the Replay Server](0013-live-e2e-bypasses-replay-server.md) | proposed |
| 0006 | [Environment Management & Variable Resolution](0006-environment-management.md) | proposed |
| 0007 | [Real-time Progress in Command Handlers](0007-realtime-progress-in-command-handlers.md) | proposed |
| 0008 | [Authentication & Token Management](0008-authentication-and-token-management.md) | proposed |
| 0009 | [Configuration Schema & Validation](0009-configuration-schema-and-validation.md) | proposed |
| 0011 | [CLI Release & Distribution Strategy](0011-cli-release-and-distribution-strategy.md) | proposed |
| 0013 | [Live E2E Tests Bypass the Replay Server](0013-live-e2e-bypasses-replay-server.md) | proposed |
| 0015 | [Managed Stack Contract Fixtures](0015-managed-stack-contract-fixtures.md) | proposed |

## Template

Expand Down
33 changes: 29 additions & 4 deletions packages/stack/src/createStack.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { describe, expect, it } from "vitest";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { existsSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { candidateCleanupTargets } from "./cleanup.ts";
import { basename, dirname, join } from "node:path";
import { candidateCleanupTargets, cleanupAutoManagedPaths } from "./cleanup.ts";
import { dockerContainerName } from "./CleanupTargets.ts";
import { runForegroundOperation, type StackHandle } from "./createStack.ts";
import { StackReadinessError } from "./errors.ts";
import type { AllocatedPorts } from "./PortAllocator.ts";
import { DEFAULT_MANAGED_STACK_NAME, projectKeyForProjectDir } from "./paths.ts";
import {
DEFAULT_MANAGED_STACK_NAME,
projectKeyForProjectDir,
shortTempPrefixRoot,
} from "./paths.ts";
import { stackMetadata } from "./StackMetadata.ts";
import type {
AuthConfig,
Expand Down Expand Up @@ -354,6 +358,27 @@ describe("resolveConfig startup mode", () => {
});
});

describe("resolveConfig state roots", () => {
it("uses disposable temporary roots when direct callers omit them", async () => {
const config = await resolveConfig({ startupMode: "lazy" });

try {
expect(config.autoManagedPaths).toEqual([config.stackRoot, config.runtimeRoot]);
expect(dirname(config.stackRoot)).toBe(shortTempPrefixRoot());
expect(dirname(config.runtimeRoot)).toBe(shortTempPrefixRoot());
expect(basename(config.stackRoot)).toMatch(/^sb-stack-/);
expect(basename(config.runtimeRoot)).toMatch(/^sb-run-/);
expect(existsSync(config.stackRoot)).toBe(true);
expect(existsSync(config.runtimeRoot)).toBe(true);
} finally {
cleanupAutoManagedPaths(config);
}

expect(existsSync(config.stackRoot)).toBe(false);
expect(existsSync(config.runtimeRoot)).toBe(false);
});
});

describe("resolveConfig readiness policy", () => {
it("uses a finite package default", async () => {
const config = await resolveConfig();
Expand Down
8 changes: 7 additions & 1 deletion packages/stack/src/entrypoints.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ describe("@supabase/stack entrypoints", () => {
});

it("isolates consumer test seams in the testing entry", () => {
expect(Object.keys(testing).sort()).toEqual(["DaemonServer", "UnixHttpClient"]);
expect(Object.keys(testing).sort()).toEqual([
"DaemonServer",
"UnixHttpClient",
"managedNativeServiceMatrix",
"managedStackContractFixtures",
"validateManagedStackContractFixtures",
]);
});
});
Loading
Loading