From 261f4fcda24d054030c88fd582b3c71bdc64097e Mon Sep 17 00:00:00 2001 From: v-openrig-build Date: Thu, 6 Aug 2026 07:10:24 +0000 Subject: [PATCH] =?UTF-8?q?feat(testbed):=2051-04=20stub-asset=20trio=20+?= =?UTF-8?q?=20census=20list=20=E2=80=94=20unblock=20the=20host=20build=20(?= =?UTF-8?q?L0.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves the VM-side half of the stub census slot: authors the concrete zero-token stub payload and populates docker/testbed/stub-assets.list (was empty => the build verb refused, L0.2 FAIL). Base-image digest slot stays host-resolved (L0.1). - docker/testbed/stub-assets/{rig.yaml, culture.md, agents/worker/agent.yaml}: a runtime:stub, one-pod/one-seat topology. Shape grounded on the PROVEN 51-02 runner fixture (test/fixtures/scenarios/topo-stub-baton.yaml) — member-key + top-key parity MATCH — which the scenario runner settles headlessly in real tmux with zero LLM tokens; both YAML files parse clean. Not guessed from memory (L3.3 grounding). - stub-assets.list: exactly the trio's repo-relative paths (the census scope the manifest hashes + the build verb stages; census-scope-match-code-path). RED-first: scripts/testbed-stub-assets.test.mjs pins the L0.2 check (census non-empty, names exactly the trio, every file exists, receipt each 64-hex) — RED on the empty list (deriveStubAssetsHash loud-fails), GREEN after. 3/3 + full testbed suites 46/46. L0.2 receipt stubAssetsHash: 3b24066fc0cc2e0a12ce03b32c7a18c170e62c394add583ffef0c105d8bd8349 Co-Authored-By: Claude Opus 4.8 --- docker/testbed/stub-assets.list | 11 ++-- .../stub-assets/agents/worker/agent.yaml | 8 +++ docker/testbed/stub-assets/culture.md | 6 ++ docker/testbed/stub-assets/rig.yaml | 23 +++++++ scripts/testbed-stub-assets.test.mjs | 66 +++++++++++++++++++ 5 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 docker/testbed/stub-assets/agents/worker/agent.yaml create mode 100644 docker/testbed/stub-assets/culture.md create mode 100644 docker/testbed/stub-assets/rig.yaml create mode 100644 scripts/testbed-stub-assets.test.mjs diff --git a/docker/testbed/stub-assets.list b/docker/testbed/stub-assets.list index b008bf40e..c39779993 100644 --- a/docker/testbed/stub-assets.list +++ b/docker/testbed/stub-assets.list @@ -5,7 +5,10 @@ # # One repo-relative path per line; comments (#) and blank lines are ignored. # -# UNRESOLVED SLOT — the concrete zero-token stub payload (the container-local stub rig.yaml + its -# agent fixture + culture.md that boot a stub topology for the L3 daemon-in-container leg) is -# finalized in the L3 stub-boot runbook increment, where the exact set is grounded rather than -# guessed. Until populated, the build verb REFUSES (deriveStubAssetsHash loud-fails on an empty set). +# RESOLVED (2026-08-06 VM-side) — the concrete zero-token stub payload: a runtime:stub rig.yaml + +# its agent fixture + culture.md that boot a settled topology for the L3 daemon-in-container leg. +# Shape grounded on the proven 51-02 runner fixture (test/fixtures/scenarios/topo-stub-baton.yaml), +# not guessed. The base-image digest slot (docker/testbed/base-image) remains host-resolved (L0.1). +docker/testbed/stub-assets/rig.yaml +docker/testbed/stub-assets/culture.md +docker/testbed/stub-assets/agents/worker/agent.yaml diff --git a/docker/testbed/stub-assets/agents/worker/agent.yaml b/docker/testbed/stub-assets/agents/worker/agent.yaml new file mode 100644 index 000000000..b0345fdf3 --- /dev/null +++ b/docker/testbed/stub-assets/agents/worker/agent.yaml @@ -0,0 +1,8 @@ +name: worker +version: "1.0.0" +resources: + skills: [] +profiles: + default: + uses: + skills: [] diff --git a/docker/testbed/stub-assets/culture.md b/docker/testbed/stub-assets/culture.md new file mode 100644 index 000000000..b5772e349 --- /dev/null +++ b/docker/testbed/stub-assets/culture.md @@ -0,0 +1,6 @@ +# testbed-stub (51-04 container stub payload) + +A minimal zero-token stub-runtime rig baked into the openrig-testbed image so the L3 +daemon-in-container leg can `rig up` a settled topology without a live LLM. The single +seat runs the deterministic 51-01 stub harness. Not a product rig — it exists only to +prove the container boots a real daemon and settles a real topology headlessly. diff --git a/docker/testbed/stub-assets/rig.yaml b/docker/testbed/stub-assets/rig.yaml new file mode 100644 index 000000000..9d924749e --- /dev/null +++ b/docker/testbed/stub-assets/rig.yaml @@ -0,0 +1,23 @@ +# 51-04 testbed image — the container-local zero-token stub topology (plan §1 layer 4). +# +# One pod (dev) with one stub seat (worker). The stub runtime is the deterministic 51-01 +# node-script harness, so this topology stands up headlessly in real tmux INSIDE the +# testbed container without a live LLM (the L3 daemon-in-container leg: `rig up` settles +# it, zero tokens by construction). Shape mirrors the proven 51-02 runner fixture +# (test/fixtures/scenarios/topo-stub-baton.yaml), which the scenario runner settles +# headlessly end-to-end — not a product rig. +# +# Canonical session of the seat = -@ = dev-worker@testbed-stub. +version: "0.2" +name: testbed-stub +culture_file: culture.md +pods: + - id: dev + label: Dev + members: + - id: worker + agent_ref: "local:agents/worker" + profile: default + runtime: stub + cwd: . + edges: [] diff --git a/scripts/testbed-stub-assets.test.mjs b/scripts/testbed-stub-assets.test.mjs new file mode 100644 index 000000000..a0429de4c --- /dev/null +++ b/scripts/testbed-stub-assets.test.mjs @@ -0,0 +1,66 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync, existsSync } from "node:fs"; +import { dirname, resolve, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { deriveStubAssetsHash } from "./testbed-build-inputs.mjs"; + +// 51-04 stub-asset increment — the L0.2 census check as a durable pin. The build verb +// (scripts/build-testbed-image.sh) stages EXACTLY the paths named in +// docker/testbed/stub-assets.list and feeds that same list to the manifest census +// (census-scope-match-code-path). An EMPTY census is an L0.2 FAIL — deriveStubAssetsHash +// loud-fails and the build verb refuses. This suite pins that the shipped list names the +// concrete zero-token stub trio (a runtime:stub rig.yaml + its agent fixture + culture.md) +// and that the receipt is well-formed, so the list can never silently drift back to empty +// or reference a missing file. + +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const LIST = join(REPO_ROOT, "docker/testbed/stub-assets.list"); + +// The SAME comment-tolerant parse the build verb feeds the census (strip #.* , trim, drop +// blanks — build-testbed-image.sh's node -e / while-read), so the test's census == the +// build's census exactly. +function parseCensus(path) { + return readFileSync(path, "utf8") + .split("\n") + .map((s) => s.replace(/#.*/, "").trim()) + .filter(Boolean); +} + +// The concrete stub trio (sorted — deriveStubAssetsHash sorts its receipt by POSIX path). +const EXPECTED = [ + "docker/testbed/stub-assets/agents/worker/agent.yaml", + "docker/testbed/stub-assets/culture.md", + "docker/testbed/stub-assets/rig.yaml", +]; + +test("stub-assets.list census is populated (empty census = L0.2 FAIL, build verb refuses)", () => { + const files = parseCensus(LIST); + assert.ok(files.length > 0, "stub-assets.list must be populated (an empty census fails L0.2)"); +}); + +test("census names EXACTLY the stub trio and every listed file exists", () => { + const files = parseCensus(LIST); + assert.deepEqual( + [...files].sort(), + EXPECTED, + "census must name exactly the stub trio (rig.yaml + culture.md + agent fixture)", + ); + for (const rel of files) { + assert.ok(existsSync(join(REPO_ROOT, rel)), `listed stub asset must exist: ${rel}`); + } +}); + +test("L0.2 receipt: deriveStubAssetsHash lists exactly the trio, each with a 64-hex sha256", () => { + const files = parseCensus(LIST); + const { hash, receipt } = deriveStubAssetsHash(REPO_ROOT, files); + assert.match(hash, /^[0-9a-f]{64}$/, "census hash is a 64-hex sha256"); + assert.deepEqual( + receipt.files.map((f) => f.path).sort(), + EXPECTED, + "receipt names exactly the trio", + ); + for (const f of receipt.files) { + assert.match(f.sha256, /^[0-9a-f]{64}$/, `each receipt entry carries a 64-hex digest: ${f.path}`); + } +});