diff --git a/Makefile b/Makefile index 82a25e869..be4e0b64d 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ build-release \ standalone-artifact-smoke \ native-only-startup-contract \ + acceptance-fixtures-check \ command-matrix-drift-check \ schema-drift-check \ parity-harness \ @@ -19,7 +20,7 @@ RUST_MANIFEST := cmd/devcontainer/Cargo.toml RELEASE_BINARY := ./cmd/devcontainer/target/release/devcontainer -tests: rust-fmt rust-clippy rust-check rust-tests build-release standalone-artifact-smoke native-only-startup-contract command-matrix-drift-check schema-drift-check parity-harness no-node-runtime check-parity-inventory check-cli-metadata check-todo-args check-compatibility-dashboard upstream-compatibility +tests: rust-fmt rust-clippy rust-check rust-tests build-release standalone-artifact-smoke native-only-startup-contract acceptance-fixtures-check command-matrix-drift-check schema-drift-check parity-harness no-node-runtime check-parity-inventory check-cli-metadata check-todo-args check-compatibility-dashboard upstream-compatibility rust-fmt: cargo fmt --manifest-path $(RUST_MANIFEST) --all -- --check @@ -42,6 +43,9 @@ standalone-artifact-smoke: build-release native-only-startup-contract: node build/check-native-only.js +acceptance-fixtures-check: + node build/check-acceptance-fixtures.js + command-matrix-drift-check: node build/generate-command-matrix.js --check diff --git a/README.md b/README.md index 0f98a9d4e..401f6a4b8 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ The shipped runtime is the Rust binary in `cmd/devcontainer`. Node is kept only - `cmd/devcontainer/`: native Rust CLI crate. - `cmd/devcontainer/src/runtime/`: native runtime subsystems for container-backed commands. +- `acceptance/`: repo-owned manual acceptance scenarios and suite manifest. - `upstream/`: canonical upstream `devcontainers/cli` baseline. - `spec/`: canonical upstream `devcontainers/spec` schemas and docs. - `build/`: repo-owned compatibility guard scripts. @@ -44,6 +45,12 @@ Compatibility/tooling validation: npm test ``` +Manual acceptance suite shape: + +```bash +make acceptance-fixtures-check +``` + The Node-based checks do not require installing project dependencies; they use built-in Node modules only. Node 20+ is still required to run them. Generated command reference: diff --git a/acceptance/README.md b/acceptance/README.md new file mode 100644 index 000000000..d4f525e4c --- /dev/null +++ b/acceptance/README.md @@ -0,0 +1,82 @@ +# Acceptance Fixtures + +This directory holds repo-owned manual acceptance scenarios for contributor +checks against the native CLI. The suite manifest lives in +`acceptance/scenarios.json`. + +Prerequisites: + +- a `devcontainer` binary on `PATH`, either from a local build or a release + artifact +- a working container engine + +## image-lifecycle + +Path: `acceptance/image-lifecycle` + +Use this as the baseline lifecycle workspace. + +```bash +devcontainer read-configuration --workspace-folder acceptance/image-lifecycle +devcontainer up --workspace-folder acceptance/image-lifecycle +devcontainer exec --workspace-folder acceptance/image-lifecycle /bin/cat /workspace/.acceptance/post-create +devcontainer run-user-commands --workspace-folder acceptance/image-lifecycle +devcontainer set-up --workspace-folder acceptance/image-lifecycle +``` + +## dockerfile-build + +Path: `acceptance/dockerfile-build` + +Use this to verify Dockerfile-based build args and post-create marker flow. + +```bash +devcontainer read-configuration --workspace-folder acceptance/dockerfile-build +devcontainer build --workspace-folder acceptance/dockerfile-build --image-name acceptance/dockerfile-build:manual +devcontainer up --workspace-folder acceptance/dockerfile-build +devcontainer exec --workspace-folder acceptance/dockerfile-build /bin/cat /workspace/.acceptance/dockerfile-message +``` + +## template-node-mongo + +Path: `acceptance/template-node-mongo` + +This is the template scenario. Apply +`ghcr.io/devcontainers/templates/node-mongo:latest` into the generated +workspace at `acceptance/template-node-mongo/workspace`, then run the normal +runtime checks there. The tracked `workspace/.gitignore` keeps generated files +out of version control. + +```bash +devcontainer templates apply --workspace-folder acceptance/template-node-mongo/workspace --template-id ghcr.io/devcontainers/templates/node-mongo:latest +devcontainer read-configuration --workspace-folder acceptance/template-node-mongo/workspace +devcontainer up --workspace-folder acceptance/template-node-mongo/workspace +devcontainer exec --workspace-folder acceptance/template-node-mongo/workspace /bin/sh -lc 'ls /workspaces/workspace/.devcontainer' +``` + +## local-feature + +Path: `acceptance/local-feature` + +Use this to verify repo-local Feature resolution and installation. + +```bash +devcontainer read-configuration --workspace-folder acceptance/local-feature +devcontainer build --workspace-folder acceptance/local-feature --image-name acceptance/local-feature:manual +devcontainer up --workspace-folder acceptance/local-feature +devcontainer exec --workspace-folder acceptance/local-feature /usr/local/bin/acceptance-local-feature +``` + +## published-feature + +Path: `acceptance/published-feature` + +Use this as the published-Feature scenario. It is the suite's only scenario +that depends on published devcontainer collection resolution. + +```bash +devcontainer read-configuration --workspace-folder acceptance/published-feature --include-features-configuration +devcontainer build --workspace-folder acceptance/published-feature --image-name acceptance/published-feature:manual +devcontainer up --workspace-folder acceptance/published-feature +devcontainer exec --workspace-folder acceptance/published-feature /bin/cat /workspace/.acceptance/published-feature +``` diff --git a/acceptance/dockerfile-build/.devcontainer/Dockerfile b/acceptance/dockerfile-build/.devcontainer/Dockerfile new file mode 100644 index 000000000..68a179232 --- /dev/null +++ b/acceptance/dockerfile-build/.devcontainer/Dockerfile @@ -0,0 +1,6 @@ +FROM alpine:3.20 + +ARG MESSAGE=unset + +RUN mkdir -p /usr/local/etc \ + && printf '%s\n' "$MESSAGE" > /usr/local/etc/acceptance-dockerfile-message diff --git a/acceptance/dockerfile-build/.devcontainer/devcontainer.json b/acceptance/dockerfile-build/.devcontainer/devcontainer.json new file mode 100644 index 000000000..f12848a3c --- /dev/null +++ b/acceptance/dockerfile-build/.devcontainer/devcontainer.json @@ -0,0 +1,13 @@ +{ + "name": "acceptance-dockerfile-build", + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "args": { + "MESSAGE": "from-build-arg" + } + }, + "workspaceFolder": "/workspace", + "updateRemoteUserUID": false, + "postCreateCommand": "mkdir -p /workspace/.acceptance && cat /usr/local/etc/acceptance-dockerfile-message > /workspace/.acceptance/dockerfile-message" +} diff --git a/acceptance/dockerfile-build/README.md b/acceptance/dockerfile-build/README.md new file mode 100644 index 000000000..847b36fd9 --- /dev/null +++ b/acceptance/dockerfile-build/README.md @@ -0,0 +1,4 @@ +# Dockerfile Build + +This workspace validates Dockerfile-based builds, build args, and a +post-create marker copied from the built image into the mounted workspace. diff --git a/acceptance/image-lifecycle/.devcontainer/devcontainer.json b/acceptance/image-lifecycle/.devcontainer/devcontainer.json new file mode 100644 index 000000000..62e36754d --- /dev/null +++ b/acceptance/image-lifecycle/.devcontainer/devcontainer.json @@ -0,0 +1,17 @@ +{ + "name": "acceptance-image-lifecycle", + "image": "alpine:3.20", + "workspaceFolder": "/workspace", + "updateRemoteUserUID": false, + "containerEnv": { + "ACCEPTANCE_SCENARIO": "image-lifecycle" + }, + "remoteEnv": { + "MANUAL_CHECK": "image-lifecycle-ready" + }, + "onCreateCommand": "mkdir -p /workspace/.acceptance && printf on-create > /workspace/.acceptance/on-create", + "updateContentCommand": "printf update-content > /workspace/.acceptance/update-content", + "postCreateCommand": "printf post-create > /workspace/.acceptance/post-create", + "postStartCommand": "printf post-start > /workspace/.acceptance/post-start", + "postAttachCommand": "printf post-attach > /workspace/.acceptance/post-attach" +} diff --git a/acceptance/image-lifecycle/README.md b/acceptance/image-lifecycle/README.md new file mode 100644 index 000000000..1badf04fd --- /dev/null +++ b/acceptance/image-lifecycle/README.md @@ -0,0 +1,4 @@ +# Image Lifecycle + +This workspace is the baseline manual check for lifecycle hook execution and +follow-up commands like `exec`, `run-user-commands`, and `set-up`. diff --git a/acceptance/local-feature/.devcontainer/devcontainer.json b/acceptance/local-feature/.devcontainer/devcontainer.json new file mode 100644 index 000000000..3ae778f4c --- /dev/null +++ b/acceptance/local-feature/.devcontainer/devcontainer.json @@ -0,0 +1,12 @@ +{ + "name": "acceptance-local-feature", + "image": "debian:bookworm", + "workspaceFolder": "/workspace", + "updateRemoteUserUID": false, + "features": { + "./local-feature": { + "message": "local-feature-ready" + } + }, + "postCreateCommand": "mkdir -p /workspace/.acceptance && /usr/local/bin/acceptance-local-feature > /workspace/.acceptance/local-feature" +} diff --git a/acceptance/local-feature/.devcontainer/local-feature/devcontainer-feature.json b/acceptance/local-feature/.devcontainer/local-feature/devcontainer-feature.json new file mode 100644 index 000000000..83f599e2e --- /dev/null +++ b/acceptance/local-feature/.devcontainer/local-feature/devcontainer-feature.json @@ -0,0 +1,12 @@ +{ + "id": "local-feature", + "name": "Local Feature", + "version": "1.0.0", + "description": "Adds a deterministic marker command for acceptance checks.", + "options": { + "message": { + "type": "string", + "default": "local-feature-default" + } + } +} diff --git a/acceptance/local-feature/.devcontainer/local-feature/install.sh b/acceptance/local-feature/.devcontainer/local-feature/install.sh new file mode 100644 index 000000000..dee3a505f --- /dev/null +++ b/acceptance/local-feature/.devcontainer/local-feature/install.sh @@ -0,0 +1,14 @@ +#!/bin/sh +set -eu + +message="${MESSAGE:-local-feature-default}" + +mkdir -p /usr/local/bin /usr/local/share/acceptance-local-feature + +cat > /usr/local/bin/acceptance-local-feature < /usr/local/share/acceptance-local-feature/message diff --git a/acceptance/local-feature/README.md b/acceptance/local-feature/README.md new file mode 100644 index 000000000..5e5171589 --- /dev/null +++ b/acceptance/local-feature/README.md @@ -0,0 +1,4 @@ +# Local Feature + +This workspace uses a repo-local Feature that installs a deterministic command +for manual verification after build or `up`. diff --git a/acceptance/published-feature/.devcontainer/devcontainer.json b/acceptance/published-feature/.devcontainer/devcontainer.json new file mode 100644 index 000000000..0aeec8109 --- /dev/null +++ b/acceptance/published-feature/.devcontainer/devcontainer.json @@ -0,0 +1,10 @@ +{ + "name": "acceptance-published-feature", + "image": "debian:bookworm", + "workspaceFolder": "/workspace", + "updateRemoteUserUID": false, + "features": { + "ghcr.io/devcontainers/features/git:1.0": {} + }, + "postCreateCommand": "mkdir -p /workspace/.acceptance && printf published-feature > /workspace/.acceptance/published-feature" +} diff --git a/acceptance/published-feature/README.md b/acceptance/published-feature/README.md new file mode 100644 index 000000000..0e03b71ac --- /dev/null +++ b/acceptance/published-feature/README.md @@ -0,0 +1,4 @@ +# Published Feature + +This workspace keeps a published Feature identifier in the config path so +manual checks can verify feature-aware configuration and build flows. diff --git a/acceptance/scenarios.json b/acceptance/scenarios.json new file mode 100644 index 000000000..f55aa4a9e --- /dev/null +++ b/acceptance/scenarios.json @@ -0,0 +1,105 @@ +[ + { + "id": "image-lifecycle", + "kind": "workspace", + "description": "Image-based workspace that exercises lifecycle hooks and follow-up commands.", + "path": "acceptance/image-lifecycle", + "workspacePath": "acceptance/image-lifecycle", + "requiresNetwork": false, + "expectedFiles": [ + "README.md", + ".devcontainer/devcontainer.json" + ], + "checks": [ + "read-configuration", + "up", + "exec", + "run-user-commands", + "set-up" + ] + }, + { + "id": "dockerfile-build", + "kind": "workspace", + "description": "Dockerfile-based workspace with a build argument and a post-create marker.", + "path": "acceptance/dockerfile-build", + "workspacePath": "acceptance/dockerfile-build", + "requiresNetwork": false, + "expectedFiles": [ + "README.md", + ".devcontainer/devcontainer.json", + ".devcontainer/Dockerfile" + ], + "checks": [ + "read-configuration", + "build", + "up", + "exec" + ] + }, + { + "id": "template-node-mongo", + "kind": "template", + "description": "Template-driven compose workspace generated from the embedded node-mongo template.", + "path": "acceptance/template-node-mongo", + "workspacePath": "acceptance/template-node-mongo/workspace", + "requiresNetwork": false, + "expectedFiles": [ + "README.md", + "workspace/.gitignore" + ], + "postApplyFiles": [ + ".devcontainer/devcontainer.json", + ".devcontainer/docker-compose.yml" + ], + "template": { + "id": "ghcr.io/devcontainers/templates/node-mongo:latest", + "args": {}, + "features": [] + }, + "checks": [ + "templates-apply", + "read-configuration", + "up", + "exec" + ] + }, + { + "id": "local-feature", + "kind": "workspace", + "description": "Image-based workspace with a repo-local Feature that installs a deterministic marker command.", + "path": "acceptance/local-feature", + "workspacePath": "acceptance/local-feature", + "requiresNetwork": false, + "expectedFiles": [ + "README.md", + ".devcontainer/devcontainer.json", + ".devcontainer/local-feature/devcontainer-feature.json", + ".devcontainer/local-feature/install.sh" + ], + "checks": [ + "read-configuration", + "build", + "up", + "exec" + ] + }, + { + "id": "published-feature", + "kind": "workspace", + "description": "Image-based workspace that keeps one published Feature in the configuration path for manual checks.", + "path": "acceptance/published-feature", + "workspacePath": "acceptance/published-feature", + "requiresNetwork": true, + "expectedFiles": [ + "README.md", + ".devcontainer/devcontainer.json" + ], + "checks": [ + "read-configuration", + "build", + "up", + "exec" + ] + } +] diff --git a/acceptance/template-node-mongo/README.md b/acceptance/template-node-mongo/README.md new file mode 100644 index 000000000..d4b5128eb --- /dev/null +++ b/acceptance/template-node-mongo/README.md @@ -0,0 +1,4 @@ +# Template Node Mongo + +This scenario is driven by `devcontainer templates apply` and uses the embedded +`ghcr.io/devcontainers/templates/node-mongo:latest` template as its source. diff --git a/acceptance/template-node-mongo/workspace/.gitignore b/acceptance/template-node-mongo/workspace/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/acceptance/template-node-mongo/workspace/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/build/check-acceptance-fixtures.js b/build/check-acceptance-fixtures.js new file mode 100644 index 000000000..658ab92bd --- /dev/null +++ b/build/check-acceptance-fixtures.js @@ -0,0 +1,263 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) devcontainer-rs contributors. + * Licensed under the MIT License. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +const repositoryRoot = path.join(__dirname, '..'); +const acceptanceRoot = path.join(repositoryRoot, 'acceptance'); +const manifestPath = path.join(acceptanceRoot, 'scenarios.json'); +const readmePath = path.join(acceptanceRoot, 'README.md'); +const repoReadmePath = path.join(repositoryRoot, 'README.md'); +const architecturePath = path.join(repositoryRoot, 'docs', 'architecture.md'); +const makefilePath = path.join(repositoryRoot, 'Makefile'); + +const expectedScenarioIds = [ + 'image-lifecycle', + 'dockerfile-build', + 'template-node-mongo', + 'local-feature', + 'published-feature', +]; + +const allowedChecks = new Set([ + 'templates-apply', + 'read-configuration', + 'build', + 'up', + 'exec', + 'run-user-commands', + 'set-up', +]); + +function assertExists(targetPath, message) { + assert(fs.existsSync(targetPath), message); +} + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +function validateScenarioCommon(scenario) { + assert.equal(typeof scenario.id, 'string', 'scenario.id must be a string'); + assert.equal(typeof scenario.kind, 'string', `scenario ${scenario.id} must declare kind`); + assert.equal(typeof scenario.description, 'string', `scenario ${scenario.id} must declare description`); + assert.equal(typeof scenario.path, 'string', `scenario ${scenario.id} must declare path`); + assert.equal( + typeof scenario.workspacePath, + 'string', + `scenario ${scenario.id} must declare workspacePath`, + ); + assert.equal( + typeof scenario.requiresNetwork, + 'boolean', + `scenario ${scenario.id} must declare requiresNetwork`, + ); + assert( + Array.isArray(scenario.checks) && scenario.checks.length > 0, + `scenario ${scenario.id} must declare at least one check`, + ); + assert( + Array.isArray(scenario.expectedFiles) && scenario.expectedFiles.length > 0, + `scenario ${scenario.id} must declare expectedFiles`, + ); + + for (const check of scenario.checks) { + assert( + allowedChecks.has(check), + `scenario ${scenario.id} uses an unsupported check token: ${check}`, + ); + } + + assertExists( + path.join(repositoryRoot, scenario.path), + `scenario path must exist: ${scenario.path}`, + ); + assertExists( + path.join(repositoryRoot, scenario.workspacePath), + `scenario workspace path must exist: ${scenario.workspacePath}`, + ); + + for (const expectedFile of scenario.expectedFiles) { + assertExists( + path.join(repositoryRoot, scenario.path, expectedFile), + `scenario ${scenario.id} is missing expected file: ${expectedFile}`, + ); + } +} + +function validateWorkspaceScenario(scenario, expectation) { + const configPath = path.join(repositoryRoot, scenario.path, '.devcontainer', 'devcontainer.json'); + assertExists(configPath, `workspace scenario ${scenario.id} must include .devcontainer/devcontainer.json`); + const config = readJson(configPath); + + if (expectation.image) { + assert.equal(typeof config.image, 'string', `${scenario.id} must be image-based`); + assert(!config.build, `${scenario.id} must not declare a build block`); + } + + if (expectation.build) { + assert(config.build, `${scenario.id} must declare a build block`); + assert.equal( + config.build.dockerfile, + 'Dockerfile', + `${scenario.id} must build from .devcontainer/Dockerfile`, + ); + } + + if (expectation.lifecycle) { + for (const key of [ + 'onCreateCommand', + 'updateContentCommand', + 'postCreateCommand', + 'postStartCommand', + 'postAttachCommand', + ]) { + assert.equal( + typeof config[key], + 'string', + `${scenario.id} must declare ${key}`, + ); + } + } + + if (expectation.localFeature) { + assertExists( + path.join(repositoryRoot, scenario.path, '.devcontainer', 'local-feature', 'devcontainer-feature.json'), + `${scenario.id} must include a local feature manifest`, + ); + assertExists( + path.join(repositoryRoot, scenario.path, '.devcontainer', 'local-feature', 'install.sh'), + `${scenario.id} must include a local feature install script`, + ); + assert( + config.features && Object.prototype.hasOwnProperty.call(config.features, './local-feature'), + `${scenario.id} must reference ./local-feature`, + ); + } + + if (expectation.publishedFeature) { + const featureIds = Object.keys(config.features || {}); + assert(featureIds.length > 0, `${scenario.id} must declare at least one published feature`); + assert( + featureIds.some((featureId) => featureId.startsWith('ghcr.io/devcontainers/features/')), + `${scenario.id} must use a ghcr.io/devcontainers/features/* identifier`, + ); + } +} + +function validateTemplateScenario(scenario) { + assert.equal(scenario.kind, 'template', `${scenario.id} must be a template scenario`); + assert( + Array.isArray(scenario.postApplyFiles) && scenario.postApplyFiles.length > 0, + `${scenario.id} must declare postApplyFiles`, + ); + assert(scenario.template, `${scenario.id} must declare template metadata`); + assert.equal( + scenario.template.id, + 'ghcr.io/devcontainers/templates/node-mongo:latest', + `${scenario.id} must use the embedded node-mongo template`, + ); + assert.deepStrictEqual( + scenario.template.args, + {}, + `${scenario.id} must keep template args empty for the baseline fixture`, + ); + assert.deepStrictEqual( + scenario.template.features, + [], + `${scenario.id} must keep template extra features empty for the baseline fixture`, + ); + assert( + scenario.checks.includes('templates-apply'), + `${scenario.id} must include templates-apply in its checks`, + ); +} + +function validateTemplateReadmeCommands(scenario, acceptanceReadme) { + const workspaceBasename = path.basename(scenario.workspacePath); + const workspaceFolder = `/workspaces/${workspaceBasename}`; + const expectedExecCommand = `devcontainer exec --workspace-folder ${scenario.workspacePath} /bin/sh -lc 'ls ${workspaceFolder}/.devcontainer'`; + assert( + acceptanceReadme.includes(expectedExecCommand), + `acceptance/README.md must use ${workspaceFolder} for the ${scenario.id} exec check`, + ); +} + +function main() { + assertExists(acceptanceRoot, 'acceptance/ must exist'); + assertExists(readmePath, 'acceptance/README.md must exist'); + assertExists(manifestPath, 'acceptance/scenarios.json must exist'); + assertExists(repoReadmePath, 'README.md must exist'); + assertExists(architecturePath, 'docs/architecture.md must exist'); + assertExists(makefilePath, 'Makefile must exist'); + + const manifest = readJson(manifestPath); + const acceptanceReadme = fs.readFileSync(readmePath, 'utf8'); + const repoReadme = fs.readFileSync(repoReadmePath, 'utf8'); + const architecture = fs.readFileSync(architecturePath, 'utf8'); + const makefile = fs.readFileSync(makefilePath, 'utf8'); + assert(Array.isArray(manifest), 'acceptance/scenarios.json must contain a JSON array'); + assert.deepStrictEqual( + manifest.map((scenario) => scenario.id), + expectedScenarioIds, + 'acceptance/scenarios.json must list the expected scenarios in a stable order', + ); + + const templateScenarios = manifest.filter((scenario) => scenario.kind === 'template'); + assert.equal(templateScenarios.length, 1, 'acceptance suite must include exactly one template scenario'); + + const networkScenarios = manifest.filter((scenario) => scenario.requiresNetwork); + assert.equal(networkScenarios.length, 1, 'acceptance suite must include exactly one network scenario'); + assert.equal(networkScenarios[0].id, 'published-feature', 'published-feature must be the only network scenario'); + + for (const scenarioId of expectedScenarioIds) { + assert( + acceptanceReadme.includes(scenarioId), + `acceptance/README.md must describe ${scenarioId}`, + ); + } + assert( + acceptanceReadme.includes('devcontainer templates apply'), + 'acceptance/README.md must document the template apply command', + ); + assert( + acceptanceReadme.includes('ghcr.io/devcontainers/templates/node-mongo:latest'), + 'acceptance/README.md must mention the embedded node-mongo template id', + ); + assert( + acceptanceReadme.includes('acceptance/template-node-mongo/workspace'), + 'acceptance/README.md must mention the generated template workspace path', + ); + assert(repoReadme.includes('acceptance/'), 'README.md must mention acceptance/'); + assert( + architecture.includes('acceptance/'), + 'docs/architecture.md must mention acceptance/', + ); + assert( + /^tests:.*acceptance-fixtures-check/m.test(makefile), + 'Makefile tests target must include acceptance-fixtures-check', + ); + + for (const scenario of manifest) { + validateScenarioCommon(scenario); + } + + validateWorkspaceScenario(manifest[0], { image: true, lifecycle: true }); + validateWorkspaceScenario(manifest[1], { build: true }); + validateTemplateScenario(manifest[2]); + validateTemplateReadmeCommands(manifest[2], acceptanceReadme); + validateWorkspaceScenario(manifest[3], { image: true, localFeature: true }); + validateWorkspaceScenario(manifest[4], { image: true, publishedFeature: true }); + + console.log( + `[acceptance-fixtures] suite layout looks current (${manifest.length} scenario(s)).`, + ); +} + +main(); diff --git a/docs/architecture.md b/docs/architecture.md index d6bf1bf41..345e7bc16 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -46,6 +46,7 @@ Dependency direction: `commands/*` may depend on `runtime/*`, but `runtime/*` sh - Rust integration tests live under `cmd/devcontainer/tests/`. - `cmd/devcontainer/tests/support/runtime_harness.rs` is the shared fake-engine harness for runtime integration coverage. - The runtime smoke suite is split by concern: build, container lifecycle, context resolution, exec behavior, and lifecycle behavior. +- `acceptance/` holds repo-owned manual acceptance fixtures and the suite manifest for contributor-guided runtime checks. - Repo-owned compatibility fixtures live under `src/test/parity/`. - Node guard scripts in `build/` cover upstream/spec drift, command-matrix drift, native-only startup, no-node-runtime regressions, and the parity harness.