From cdf8ffd57cc716d306a4e2705d57d427b2d0d065 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 07:34:17 +0000 Subject: [PATCH] fix(live-e2e): derive the showcase-app commit from the release tag at boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `e2e/live/ci/backend.env` carried two pins that a header MUST said must agree: `OBJECTSTACK_VERSION` (checked against `pnpm-lock.yaml` since objectui#7689) and `OBJECTSTACK_REF`, a hand-moved sha whose stated rule — "always the commit the `@objectstack/cli@${OBJECTSTACK_VERSION}` release tag points at" — nothing could check. The file said so in as many words: "NOT machine-checked, and deliberately so", on the reasoning that resolving the tag needs the objectstack repository and the unit lane cannot reach it. True of the unit lane, and beside the point: the live lane clones that very repository to get the app. Retire the pin instead of adding a check nobody would read (the lane is `informational`). `start-backend.sh` now resolves the commit at boot from the release tag named by `OBJECTSTACK_VERSION` — `git ls-remote --tags`, taking the peeled `^{}` sha of the annotated tag — and refuses to start, naming the tag, when it does not resolve to a 40-character sha. `git ls-remote` reports a missing tag as empty output and exit 0, so that shape check is the guard, not a formality. The resolved sha stays in the reuse stamp so a re-pointed tag cannot be served from a cached fixture in a reused scratch dir. The pair the header describes then cannot disagree by construction: the sparse-checkout of `examples/app-showcase` and the installed published packages always come from the same release, and there is one value to move, not two. Tests: the `keeps OBJECTSTACK_REF in the one shape start-backend.sh can fetch` pin is retired deliberately — it asserted the shape of a value that no longer exists — and replaced by three pins that now hold: backend.env declares no `OBJECTSTACK_REF`; start-backend.sh still resolves the tag, still builds it from `OBJECTSTACK_VERSION`, and still refuses on a bad shape; and backend.env still explains the absence, so nobody restores the pin thinking it was dropped by accident. Prose updated in the CI/CD page, `better-auth-pin.mjs` and the live saved-view spec, all of which described a two-pin pair. Refs objectui#7964, objectui#7689. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr --- content/docs/guide/ci-cd-pipeline.md | 12 +- e2e/live/ci/backend.env | 40 +++++-- e2e/live/ci/better-auth-pin.mjs | 8 +- e2e/live/ci/start-backend.sh | 61 ++++++++-- e2e/live/saved-view-filter.spec.ts | 17 +-- scripts/__tests__/ci-cd-pipeline-doc.test.ts | 117 ++++++++++++++----- 6 files changed, 199 insertions(+), 56 deletions(-) diff --git a/content/docs/guide/ci-cd-pipeline.md b/content/docs/guide/ci-cd-pipeline.md index b82888cade..004cc7fa29 100644 --- a/content/docs/guide/ci-cd-pipeline.md +++ b/content/docs/guide/ci-cd-pipeline.md @@ -420,7 +420,15 @@ comment), so every promotion would stale a hand-copied enumeration here; it alre Backend pins live in `e2e/live/ci/backend.env` and must match the `@objectstack/spec` version in `pnpm-lock.yaml` — bump both in the same PR, or the run proves nothing. -That file carries a **third** pin, `BETTER_AUTH_VERSION`, and it is a different kind of thing: +`OBJECTSTACK_VERSION` is the only `@objectstack` value that file declares, and moving it is the +only move. The showcase-app commit the lane sparse-checks-out is **derived**, not pinned: +`start-backend.sh` resolves the `@objectstack/cli@$OBJECTSTACK_VERSION` release tag at boot +(`git ls-remote --tags`, peeled sha) and refuses to start when it does not resolve. It used to be +a second key, `OBJECTSTACK_REF`, moved by hand under a MUST that nothing could check — the same +shape as the version drift above, and retired for the same reason (objectui#7964). Deriving it +means the app source and the published packages it runs on come from one release by construction. + +That file carries a **second** pin, `BETTER_AUTH_VERSION`, and it is a different kind of thing: not a matched-pair pin but a workaround for a break inside the published packages themselves. `@objectstack/plugin-auth` imports `createLocalAccountIssuer` from `@better-auth/core/db` and declares `@better-auth/core` with a caret; `@better-auth/core@1.7.3` removed that export in a @@ -431,7 +439,7 @@ its 300-second readiness timeout having run zero specs (objectstack#16186, objec pinning a dependency to turn a lane green is a gate weakening — `e2e/live/ci/better-auth-pin.mjs` runs on **every** start, cache hits included, and fails by name if the override was not declared, did not resolve, or resolved and still lacks the export. ⛔ It is not, and must not become, a -repair of the two pins above: objectui#7689's triage forbids repairing this lane by moving those. +repair of the version pin above: objectui#7689's triage forbids repairing this lane by moving it. Retire the pin and its guard together in the PR that bumps `OBJECTSTACK_VERSION` past the upstream fix. diff --git a/e2e/live/ci/backend.env b/e2e/live/ci/backend.env index f4800f4f69..bf2478c3a1 100644 --- a/e2e/live/ci/backend.env +++ b/e2e/live/ci/backend.env @@ -12,16 +12,38 @@ # had drifted two minor versions (17.0.0-rc.2 against a 17.2.0 lockfile) # before anything compared the two numbers — objectui#7689. # -# OBJECTSTACK_REF — the objectstack-ai/objectstack commit the showcase app -# metadata is checked out from. Always the commit the release tag -# `@objectstack/cli@${OBJECTSTACK_VERSION}` points at, so the app source and -# the published packages it runs on come from the same tree. (Mirror image -# of the framework repo's `.objectui-sha` console pin.) -# NOT machine-checked, and deliberately so: resolving that tag needs the -# objectstack repository, which the unit lane cannot reach. Move this by -# hand whenever the version above moves, from the tag's own commit. +# This file declares exactly ONE @objectstack pin, and that is the whole +# design: the commit is derived from it, see below. OBJECTSTACK_VERSION=17.2.0 -OBJECTSTACK_REF=e7d2cc67fdef7fee9d2c6d65d7363fe1c78ce6a4 + +# There is deliberately no OBJECTSTACK_REF key here. The objectstack-ai/objectstack +# commit the showcase app metadata is sparse-checked-out from is DERIVED at +# boot by start-backend.sh, from the release tag +# `@objectstack/cli@${OBJECTSTACK_VERSION}` — `git ls-remote --tags`, taking +# the peeled `^{}` sha of the annotated tag — and the script refuses to start +# when that tag does not resolve to a 40-character sha. The app source and the +# published packages it runs on therefore come from the same release BY +# CONSTRUCTION; there is no second value to move, so the two cannot disagree. +# (Mirror image of the framework repo's `.objectui-sha` console pin.) +# +# It used to be a hand-moved sha here, carrying a stated MUST — "always the +# commit the release tag points at" — that nothing could check, on the +# reasoning that resolving the tag needs the objectstack repository and the +# unit lane cannot reach it. True of the unit lane, and beside the point: the +# live lane clones that very repository at boot to get the app, so the lane +# that CONSUMES the value could always resolve it. objectui#7964 retired the +# pin rather than add a check inside an `informational` lane nobody reads. +# +# ⚠️ One consequence, stated rather than papered over. The workflow caches the +# prepared fixture under `live-backend--`, and the +# resolved sha is no longer in this file. A version bump still moves the key, +# because the version is what the sha is derived from. But if the +# `@objectstack/cli@${OBJECTSTACK_VERSION}` tag were ever re-pointed at another +# commit after a fixture had been cached, that stale fixture would still be +# served until OBJECTSTACK_VERSION itself moved. start-backend.sh's own stamp +# carries the RESOLVED sha, so a re-point is caught in a reused scratch dir; +# the Actions cache is the one path it is not caught on. Published release tags +# are not expected to move, and moving one is the anomaly, not this lane. # BETTER_AUTH_VERSION — the version the whole floating `better-auth` family is # pinned to in the backend's install path, via an npm `overrides` block that diff --git a/e2e/live/ci/better-auth-pin.mjs b/e2e/live/ci/better-auth-pin.mjs index 316a495db4..4bcecfd63e 100755 --- a/e2e/live/ci/better-auth-pin.mjs +++ b/e2e/live/ci/better-auth-pin.mjs @@ -32,9 +32,11 @@ * binds the port, loads 42 plugins and prints `Server is ready` while auth is * absent (objectui#8084). * - * ⛔ The pin is NOT a repair of `OBJECTSTACK_VERSION` / `OBJECTSTACK_REF`. - * objectui#7689's triage forbids repairing this lane by moving those, and this - * does not move them. It pins a TRANSITIVE dependency of the published artifact + * ⛔ The pin is NOT a repair of `OBJECTSTACK_VERSION` (nor of the showcase-app + * commit start-backend.sh derives from it — objectui#7964 retired the second, + * hand-moved `OBJECTSTACK_REF` key that used to sit beside it). + * objectui#7689's triage forbids repairing this lane by moving that, and this + * does not move it. It pins a TRANSITIVE dependency of the published artifact * to the version that artifact's own manifest was authored against, restoring * the resolution the publisher intended. When objectstack#16186 lands upstream * — plugin-auth pinning `@better-auth/core` itself, or moving off the removed diff --git a/e2e/live/ci/start-backend.sh b/e2e/live/ci/start-backend.sh index a83a48c403..c1661ebdce 100755 --- a/e2e/live/ci/start-backend.sh +++ b/e2e/live/ci/start-backend.sh @@ -3,8 +3,10 @@ # @objectstack/* packages, serving the showcase app the live specs target. # # What it does (idempotent; a stamp file skips prepare when pins are unchanged): -# 1. Sparse-checkout `examples/app-showcase` from objectstack-ai/objectstack -# at the pinned OBJECTSTACK_REF (backend.env). +# 1. Resolve the `@objectstack/cli@$OBJECTSTACK_VERSION` release tag to a +# commit, and sparse-checkout `examples/app-showcase` from +# objectstack-ai/objectstack at it. The commit is DERIVED, never pinned: +# see the note above OBJECTSTACK_VERSION in backend.env (objectui#7964). # 2. Rewrite its package.json: every `@objectstack/*` workspace dep -> the # pinned published OBJECTSTACK_VERSION; dev-only tooling dropped. # 3. `npm install` (published tarballs only — nothing is built from source, @@ -37,10 +39,53 @@ STAMP="$BACKEND_DIR/.prepared" # floating family is the exact outcome this pin exists to prevent, and it costs # 300 seconds to discover downstream (objectstack#16186). : "${BETTER_AUTH_VERSION:?backend.env must declare BETTER_AUTH_VERSION — see its header and objectstack#16186}" -# The pin is IN the stamp: without it, a fixture prepared before a pin change -# satisfies the reuse test and the new pin is never installed. The workflow's -# fixture cache key is the hash of backend.env, which carries the pin for the -# same reason. + +# The showcase-app commit is DERIVED here, and is declared nowhere: it is +# whatever the `@objectstack/cli@$OBJECTSTACK_VERSION` release tag points at, so +# the metadata checked out below and the published packages installed on top of +# it cannot come from different trees. backend.env used to carry it as a second, +# hand-moved pin whose stated MUST ("always the commit the release tag points +# at") nothing could check — and a pair that can disagree eventually does +# (objectui#7964, objectui#7689 for the version half). +# +# Annotated tags resolve in two lines: the tag object, then the peeled `^{}` +# commit. Take the peeled sha when present, else the tag's own (lightweight tags +# do not peel). Both patterns are passed explicitly — asking for the tag alone +# does NOT return its peeled line. +OBJECTSTACK_TAG="@objectstack/cli@$OBJECTSTACK_VERSION" +# GIT_TERMINAL_PROMPT=0 so an unreachable/private remote fails in seconds with +# the message below instead of blocking the job on a credential prompt. +if ! TAG_LINES="$(GIT_TERMINAL_PROMPT=0 git ls-remote --tags "$REPO_URL" \ + "refs/tags/$OBJECTSTACK_TAG" "refs/tags/$OBJECTSTACK_TAG^{}" 2>&1)"; then + echo "[live-backend] cannot reach $REPO_URL to resolve the release tag" >&2 + echo "[live-backend] $TAG_LINES" >&2 + exit 1 +fi +OBJECTSTACK_REF="$(printf '%s\n' "$TAG_LINES" | awk -v t="refs/tags/$OBJECTSTACK_TAG" ' + $2 == t "^{}" { peeled = $1 } + $2 == t { plain = $1 } + END { print (peeled != "" ? peeled : plain) } +')" +# A missing tag is not an error to `git ls-remote` — it prints nothing and exits +# 0 — so the shape check below is the actual guard, not a formality. Refuse +# loudly and by name: the alternative is `git fetch --depth 1 origin ""` failing +# 300 seconds downstream, in a log nobody reads until the job goes red. +if [[ ! "$OBJECTSTACK_REF" =~ ^[0-9a-f]{40}$ ]]; then + echo "[live-backend] release tag '$OBJECTSTACK_TAG' does not resolve to a commit in" >&2 + echo "[live-backend] $REPO_URL" >&2 + echo "[live-backend] OBJECTSTACK_VERSION=$OBJECTSTACK_VERSION (backend.env) names a version" >&2 + echo "[live-backend] with no published release tag, or the tag scheme moved. Refusing to" >&2 + echo "[live-backend] start: there is no showcase-app commit matching that release." >&2 + exit 1 +fi +echo "[live-backend] resolved $OBJECTSTACK_TAG -> $OBJECTSTACK_REF" + +# The RESOLVED sha, not the version, is in the stamp: without it, a fixture +# prepared before a pin change satisfies the reuse test and the new pin is never +# installed — and a re-pointed tag is exactly such a change with no key of its +# own to hash. The workflow's fixture cache key is the hash of backend.env, +# which carries the version for the same reason; see the ⚠️ note there for the +# one path that key can no longer see. WANT_STAMP="$OBJECTSTACK_REF $OBJECTSTACK_VERSION better-auth@$BETTER_AUTH_VERSION" mkdir -p "$BACKEND_DIR" @@ -50,10 +95,10 @@ prepare() { echo "[live-backend] prepare: pins unchanged ($WANT_STAMP), reusing $APP_DIR" return fi - echo "[live-backend] prepare: showcase@${OBJECTSTACK_REF:0:12} on published @objectstack/*@$OBJECTSTACK_VERSION" + echo "[live-backend] prepare: showcase@${OBJECTSTACK_REF:0:12} (from $OBJECTSTACK_TAG) on published @objectstack/*@$OBJECTSTACK_VERSION" rm -rf "$APP_DIR" "$BACKEND_DIR/src" "$STAMP" - # Shallow, sparse fetch of the pinned commit — metadata source only. + # Shallow, sparse fetch of the resolved commit — metadata source only. git init -q "$BACKEND_DIR/src" git -C "$BACKEND_DIR/src" remote add origin "$REPO_URL" git -C "$BACKEND_DIR/src" sparse-checkout set examples/app-showcase diff --git a/e2e/live/saved-view-filter.spec.ts b/e2e/live/saved-view-filter.spec.ts index 44375b7dc9..94a201c63a 100644 --- a/e2e/live/saved-view-filter.spec.ts +++ b/e2e/live/saved-view-filter.spec.ts @@ -7,14 +7,15 @@ import { test, expect } from '@playwright/test'; * This is the empirical pin for the defect, run against the REAL stack because * only the server can answer the question the issue asked: does it accept a * rule array in `$filter`? It does not. Measured against the published backend - * pair this lane pins — `OBJECTSTACK_VERSION` and `OBJECTSTACK_REF` in - * `e2e/live/ci/backend.env`, which move together and are now held to the - * lockfile by `scripts/__tests__/ci-cd-pipeline-doc.test.ts` (objectui#7689). - * That pair has moved since this measurement was first taken, which is why it - * is named by FILE and not by value: a version literal written here goes stale - * behind the pin silently, and the whole point of objectui#7689 is that it did - * — for two minor versions, in the pin itself. The showcase app is the one that - * ref checks out: + * backend this lane pins — `OBJECTSTACK_VERSION` in `e2e/live/ci/backend.env`, + * held to the lockfile by `scripts/__tests__/ci-cd-pipeline-doc.test.ts` + * (objectui#7689), with the showcase-app commit derived from it at boot from + * the matching `@objectstack/cli` release tag (objectui#7964). That pin has + * moved since this measurement was first taken, which is why it is named by + * FILE and not by value: a version literal written here goes stale behind the + * pin silently, and the whole point of objectui#7689 is that it did — for two + * minor versions, in the pin itself. The showcase app is the one that release + * checks out: * * GET /api/v1/data/showcase_task * ?$filter=[{"field":"status","operator":"equals","value":"in_progress"}] diff --git a/scripts/__tests__/ci-cd-pipeline-doc.test.ts b/scripts/__tests__/ci-cd-pipeline-doc.test.ts index f0f56577e6..921c218642 100644 --- a/scripts/__tests__/ci-cd-pipeline-doc.test.ts +++ b/scripts/__tests__/ci-cd-pipeline-doc.test.ts @@ -1292,13 +1292,25 @@ describe('ci-cd-pipeline.md — contexts that can never be required (#4170)', () * `@objectstack/spec` versions has no single number for the pin to match, and naming that * is more useful than picking one of them and comparing to it. * - * NOT asserted: the file's other pin, `OBJECTSTACK_REF`, whose stated rule is that it is - * the commit the `@objectstack/cli@${OBJECTSTACK_VERSION}` release tag points at. Reading - * that tag needs the objectstack repository over the network, which this lane has not got, - * so the pairing moves by hand and this file says so rather than implying coverage it has - * not got. What IS checked is the shape a hand move can still get wrong in a way the lane - * only discovers 300 seconds later: `start-backend.sh` fetches the ref with - * `git fetch --depth 1 origin "$OBJECTSTACK_REF"`, which needs a full object name. + * ALSO ASSERTED, since objectui#7964: that the file's other pin is GONE. `OBJECTSTACK_REF` + * used to be a second, hand-moved sha here, carrying a MUST — "always the commit the + * `@objectstack/cli@${OBJECTSTACK_VERSION}` release tag points at" — that nothing could + * check, because reading that tag needs the objectstack repository over the network and + * this unit lane has not got it. That was a true statement about what a reader should do + * and, again, an unenforceable one: the same shape #7689 is about, one file down. + * + * The fix was not another check. It was to delete the second value: `start-backend.sh` + * now RESOLVES the commit at boot from the release tag named by `OBJECTSTACK_VERSION` + * (`git ls-remote --tags`, peeled `^{}` sha preferred) and refuses to start when the tag + * does not resolve to a 40-character sha. The lane that consumes the commit is the lane + * that can reach the repository, and it always could. So the pair the header describes + * cannot disagree by construction, and there is nothing left here to hand-move. + * + * What this file pins about that half is therefore the ABSENCE and the DERIVATION, by + * content: no `OBJECTSTACK_REF=` key in backend.env, and the resolution + refusal still in + * start-backend.sh. Either one alone would be vacuous — an absent key is fine only while + * something derives the value, and a derivation is only load-bearing while no pin + * overrides it. * * ## Anti-vacuity * @@ -1310,8 +1322,10 @@ describe('ci-cd-pipeline.md — contexts that can never be required (#4170)', () * teaching this page. */ const backendEnvPath = path.join(repoRoot, 'e2e/live/ci/backend.env'); +const startBackendPath = path.join(repoRoot, 'e2e/live/ci/start-backend.sh'); const lockfilePath = path.join(repoRoot, 'pnpm-lock.yaml'); const backendEnv = fs.readFileSync(backendEnvPath, 'utf8'); +const startBackend = fs.readFileSync(startBackendPath, 'utf8'); /** The page's statement of the rule, whitespace-normalised because the source wraps it. */ const PIN_RULE_SENTENCE = @@ -1404,34 +1418,85 @@ describe('ci-cd-pipeline.md — live-e2e backend pin (#7689)', () => { '(informational)` against this pair therefore carries no information, green or red — ' + 'it exercises one published backend against a console built for another.\n\n' + 'Fix it in whichever direction the change came from: a lockfile bump must move ' + - 'OBJECTSTACK_VERSION (and, by hand, OBJECTSTACK_REF to the commit the matching ' + - '`@objectstack/cli` release tag points at), and a pin bump must be a lockfile bump. ' + + 'OBJECTSTACK_VERSION, and a pin bump must be a lockfile bump. That is the only ' + + 'value to move — the showcase-app commit follows it on its own, because ' + + 'start-backend.sh resolves the `@objectstack/cli@$OBJECTSTACK_VERSION` release tag ' + + 'at boot (objectui#7964). ' + '⛔ Do not resolve this by reverting the pin to whatever was green last: a failing ' + 'matched pair carries strictly more information than a green mismatched one ' + '(objectui#7689).', ).toBe(versions[0]); }); - it('keeps OBJECTSTACK_REF in the one shape start-backend.sh can fetch', () => { - const ref = readEnvKey('OBJECTSTACK_REF'); + // Retired here, deliberately, with objectui#7964: `keeps OBJECTSTACK_REF in the one shape + // start-backend.sh can fetch`. It asserted that a hand-moved sha was 40 hex characters — + // the only half of that pin a lane with no network could read. There is no hand-moved sha + // any more, so the shape pin has nothing to hold; the three below hold what replaced it. + // ⛔ Do not restore it by re-adding the key: a pin that overrides the derivation brings + // back the exact pair that could silently disagree. + + it('declares no OBJECTSTACK_REF — the commit is derived, not pinned', () => { expect( - ref, - `${path.relative(repoRoot, backendEnvPath)} declares no OBJECTSTACK_REF, which ` + - '`start-backend.sh` needs to sparse-checkout the showcase app.', - ).not.toBeNull(); + readEnvKey('OBJECTSTACK_REF'), + `${path.relative(repoRoot, backendEnvPath)} declares an OBJECTSTACK_REF again. That ` + + 'key was retired by objectui#7964: it was a second, hand-moved sha whose stated MUST ' + + '— always the commit the `@objectstack/cli@$OBJECTSTACK_VERSION` release tag points ' + + 'at — nothing could check, and a pair that CAN disagree eventually does (it is the ' + + 'same failure #7689 found in the version half). start-backend.sh resolves the commit ' + + 'from the tag at boot instead, so the app source and the published packages come ' + + 'from one release by construction.\n\n' + + 'If the derivation genuinely cannot serve some case, that is a decision to take on ' + + 'the record — reintroducing the pin here restores the drift, and this lane is ' + + '`informational`, so nothing else would notice.', + ).toBeNull(); + }); + + it('derives the commit from the @objectstack/cli release tag in start-backend.sh', () => { + const rel = path.relative(repoRoot, startBackendPath); + + // Pinned by CONTENT, not by behaviour: this lane cannot run the script (it needs the + // network the whole #7964 argument turns on). What it can read is that the resolution + // is still there and still keyed off OBJECTSTACK_VERSION — the two things whose loss + // would leave the absent key above vacuous. + expect( + /git ls-remote --tags/.test(startBackend), + `${rel} no longer resolves the release tag with \`git ls-remote --tags\`. The test ` + + 'above requires backend.env to carry NO OBJECTSTACK_REF, on the understanding that ' + + 'this script derives it. Without a resolution here, that absence is not a design — ' + + 'it is a missing value, and the lane fetches nothing.', + ).toBe(true); - // Which COMMIT it should be is the half this file cannot read (see the header): that - // needs the objectstack release tag. The shape it must have is readable here, and it - // is the one a hand move gets wrong — `git fetch --depth 1 origin ` wants a full - // object name, and an abbreviated one fails 300 seconds into the lane, in a log nobody - // reads until the job goes red. expect( - ref, - `OBJECTSTACK_REF is ${JSON.stringify(ref)}. start-backend.sh fetches it with ` + - '`git fetch --depth 1 origin "$OBJECTSTACK_REF"`, which needs a full 40-character ' + - 'commit sha — an abbreviated one, a branch name or a tag is refused by the remote and ' + - 'the lane only says so once the backend fails to boot.', - ).toMatch(/^[0-9a-f]{40}$/); + /refs\/tags\/\$OBJECTSTACK_TAG/.test(startBackend) && + /OBJECTSTACK_TAG="@objectstack\/cli@\$OBJECTSTACK_VERSION"/.test(startBackend), + `${rel} no longer builds the tag it resolves from OBJECTSTACK_VERSION as ` + + '`@objectstack/cli@$OBJECTSTACK_VERSION`. That coupling is the entire guarantee: it ' + + 'is what makes the checked-out app source and the installed published packages the ' + + 'same release. A tag derived from anything else — a branch, a literal, another ' + + "package's tag — reopens the gap objectui#7964 closed.", + ).toBe(true); + + // The refusal is half the ruling: `git ls-remote` prints nothing and exits 0 for a tag + // that does not exist, so without a shape check the script would carry an empty ref into + // `git fetch --depth 1 origin ""` and fail 300 seconds later, in a log nobody reads. + expect( + /\[\[ ! "\$OBJECTSTACK_REF" =~ \^\[0-9a-f\]\{40\}\$ \]\]/.test(startBackend), + `${rel} no longer refuses to start when the release tag fails to resolve to a ` + + '40-character sha. `git ls-remote` reports a missing tag as empty output and exit 0, ' + + 'so this check is the only thing standing between a typo in OBJECTSTACK_VERSION and ' + + 'a 300-second timeout with no explanation. Keep a refusal that names the tag.', + ).toBe(true); + }); + + it('documents the derivation in backend.env, where the pin used to be', () => { + expect( + /no OBJECTSTACK_REF key here/.test(backendEnv) && /DERIVED at\s*\n#\s*boot/.test(backendEnv), + `${path.relative(repoRoot, backendEnvPath)} no longer explains that the showcase-app ` + + 'commit is derived at boot rather than pinned. The key is absent from this file; a ' + + 'reader who finds no OBJECTSTACK_REF and no note saying why will conclude the pin was ' + + 'dropped by accident and restore it — which is exactly the regression the test above ' + + 'forbids. The absence has to be legible as a decision.', + ).toBe(true); }); });