From 783443a46579640959deccb975e9cdb60dca9332 Mon Sep 17 00:00:00 2001 From: Volker Christian Date: Sat, 10 Oct 2026 18:24:18 +0200 Subject: [PATCH 1/3] Consolidate Phase 2 verification and isolated development SDK publication --- .github/workflows/ci.yml | 1 + .github/workflows/publish-dev.yml | 75 ++++++++ .github/workflows/verify.yml | 289 ++++++++++++++++++++++++++---- PHASE2.md | 82 +++++++++ README.md | 127 ++++++++----- templates/repo.yml | 9 +- tests/test_verify.py | 270 ++++++++++++++++++++++++++-- 7 files changed, 766 insertions(+), 87 deletions(-) create mode 100644 .github/workflows/publish-dev.yml create mode 100644 PHASE2.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0218968..6df8107 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,7 @@ jobs: - run: python -m pip install -r requirements.txt - name: Check catalog run: python tools/snodec_sync check + - run: sudo apt-get update && sudo apt-get install -y cmake ninja-build g++ zstd - name: Unit and workflow boundary tests if: ${{ always() && !cancelled() }} run: python -m unittest discover -s tests -v diff --git a/.github/workflows/publish-dev.yml b/.github/workflows/publish-dev.yml new file mode 100644 index 0000000..1f2ac68 --- /dev/null +++ b/.github/workflows/publish-dev.yml @@ -0,0 +1,75 @@ +name: Publish development SDK +on: + workflow_call: +permissions: + contents: write +concurrency: + group: development-sdk-${{ github.repository }} + cancel-in-progress: false +jobs: + publish: + name: Publish tested development SDK + if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} + runs-on: ubuntu-latest + steps: + - run: sudo apt-get update && sudo apt-get install -y zstd + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: development-sdk + path: sdk + - name: Publish current default-branch SDK only + shell: python + env: + GH_TOKEN: ${{ github.token }} + run: | + import json + import os + from pathlib import Path + import subprocess + import tarfile + from urllib.error import HTTPError + from urllib.parse import quote + from urllib.request import Request, urlopen + + repository = os.environ["GITHUB_REPOSITORY"] + sha = os.environ["GITHUB_SHA"] + token = os.environ["GH_TOKEN"] + + def api(path, method="GET", data=None): + body = None if data is None else json.dumps(data).encode() + request = Request("https://api.github.com/" + path, data=body, method=method, + headers={"Authorization": "Bearer " + token, "Accept": "application/vnd.github+json"}) + with urlopen(request, timeout=60) as response: + return json.load(response) + + path = "repos/" + repository + branch = api(path)["default_branch"] + if api(path + "/commits/" + quote(branch, safe=""))["sha"] != sha: + print("Superseded build: no development SDK publication") + else: + archives = list(Path("sdk").glob("*-dev-*.tar.zst")) + if len(archives) != 1: + raise ValueError("expected one tested SDK archive") + archive = archives[0] + unpacked = Path(os.environ["RUNNER_TEMP"]) / "sdk.tar" + subprocess.run(["zstd", "-d", archive, "-o", unpacked], check=True) + with tarfile.open(unpacked) as contents: + identity = json.load(contents.extractfile("./.snodec-sdk.json")) + if identity["repository"] != repository or identity["source_commit"] != sha: + raise ValueError("SDK does not match the successful build") + try: + release = api(path + "/releases/tags/dev") + if not release["prerelease"]: + raise ValueError("dev must remain a prerelease") + except HTTPError as error: + if error.code != 404: + raise + api(path + "/releases", "POST", { + "tag_name": "dev", "target_commitish": sha, + "name": "Development SDK", "body": "Unsigned development SDK from successful default-branch Verify runs. Not a stable product release.", + "prerelease": True, "make_latest": "false", + }) + subprocess.run(["gh", "release", "upload", "dev", str(archive), + "--clobber", "--repo", repository], check=True) + api(path + "/git/refs/tags/dev", "PATCH", {"sha": sha, "force": True}) + print("Published development SDK for " + sha) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 934a7f1..4dee93d 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -5,12 +5,14 @@ permissions: contents: read jobs: prepare: - name: Read C++ manifest + name: Read build manifest runs-on: ubuntu-latest outputs: + profile: ${{ steps.manifest.outputs.profile }} config: ${{ steps.manifest.outputs.config }} container: ${{ steps.manifest.outputs.container }} options: ${{ steps.manifest.outputs.options }} + artifact_paths: ${{ steps.manifest.outputs.artifact_paths }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: @@ -33,44 +35,75 @@ jobs: manifest = yaml.safe_load(Path(".snodec/repo.yml").read_text()) if not isinstance(manifest, dict) or not isinstance(manifest.get("ci"), dict): raise ValueError("manifest must contain a ci mapping") - defaults = { + cpp = { "profile": "cpp", "container": "gcc:15.3.0-trixie", "apt": [], "deps": [], "cmake_options": [], "ctest": True, "ctest_args": ["--output-on-failure"], - "ipv6": False, "runtime_dirs": False, + "ipv6": False, "runtime_dirs": False, "parallel": 2, "sdk": False, "env": {}, "ctest_launcher": [], } + python = { + "profile": "python", "python_version": "3.12", "pip": ".[dev]", + "native_build": [], "smoke": {}, "artifact_globs": [], + } + profile = manifest["ci"].get("profile", "cpp") + if profile not in ("cpp", "python"): + raise ValueError("only cpp and python profiles are supported") + defaults = cpp if profile == "cpp" else python if manifest["ci"].keys() - defaults.keys(): raise ValueError("unsupported ci fields") config = defaults | manifest["ci"] - if config["profile"] != "cpp": - raise ValueError("only the cpp profile is supported") + if profile == "python": + if not isinstance(config["python_version"], str) or not re.fullmatch(r"3[.][0-9]+", config["python_version"]): + raise ValueError("python_version must specify Python 3") + if not isinstance(config["pip"], str) or not config["pip"].startswith("."): + raise ValueError("pip must specify the local project") + if not isinstance(config["smoke"], dict) or set(config["smoke"]) != {"Linux", "Windows", "macOS"}: + raise ValueError("smoke must contain commands for all three native platforms") + for key, values in {"native_build": config["native_build"], "artifact_globs": config["artifact_globs"], **config["smoke"]}.items(): + if not isinstance(values, list) or not values or any(not isinstance(v, str) or not v or any(c in v for c in "\r\n\0") for v in values): + raise ValueError(key + " must be a non-empty argument list") + config["container"] = cpp["container"] if not isinstance(config["container"], str) or not re.fullmatch( r"[A-Za-z0-9][A-Za-z0-9._/:@-]*", config["container"] ): raise ValueError("container must be an image reference") - for key in ("ctest", "ipv6", "runtime_dirs"): + for key in ("ctest", "ipv6", "runtime_dirs", "sdk") if profile == "cpp" else (): if type(config[key]) is not bool: raise ValueError(key + " must be a boolean") - for key in ("apt", "deps", "cmake_options", "ctest_args"): + if profile == "cpp" and (not isinstance(config["env"], dict) or any( + not isinstance(key, str) or not re.fullmatch(r"[A-Z][A-Z0-9_]*", key) or key.startswith(("GITHUB_", "RUNNER_", "GH_")) + or not isinstance(value, str) or any(c in value for c in "\r\n\0") + for key, value in config["env"].items() + )): + raise ValueError("env must contain non-reserved single-line environment values") + if profile == "cpp" and (type(config["parallel"]) is not int or not 1 <= config["parallel"] <= 32): + raise ValueError("parallel must be between 1 and 32") + for key in ("apt", "deps", "cmake_options", "ctest_args", "ctest_launcher") if profile == "cpp" else (): if not isinstance(config[key], list) or any( not isinstance(item, str) or not item or any(c in item for c in "\r\n\0") for item in config[key] ): raise ValueError(key + " must be a list of single-line arguments") - if any(not re.fullmatch(r"[a-z0-9][a-z0-9+.-]*(?::[a-z0-9]+)?", p) for p in config["apt"]): + if profile == "cpp" and any(not re.fullmatch(r"[a-z0-9][a-z0-9+.-]*(?::[a-z0-9]+)?", p) for p in config["apt"]): raise ValueError("apt must contain package names, not options") - if any(not re.fullmatch(r"[A-Za-z0-9_.-]+@source", d) for d in config["deps"]): - raise ValueError("deps supports only SNodeC repository names with @source") - if len(config["deps"]) != len(set(config["deps"])): + if profile == "cpp" and any(not re.fullmatch(r"[A-Za-z0-9_.-]+@(source|dev)", d) for d in config["deps"]): + raise ValueError("deps supports SNodeC repository names with @source or @dev") + if profile == "cpp" and len(config["deps"]) != len(set(config["deps"])): raise ValueError("deps must be unique and ordered") - options = "--cap-add=NET_ADMIN --sysctl net.ipv6.conf.all.disable_ipv6=0" if config["ipv6"] else "" + options = "--cap-add=NET_ADMIN --sysctl net.ipv6.conf.all.disable_ipv6=0" if config.get("ipv6") else "" with open(os.environ["GITHUB_OUTPUT"], "a") as output: + output.write("profile=" + profile + "\n") output.write("config=" + json.dumps(config, separators=(",", ":")) + "\n") output.write("container=" + config["container"] + "\n") output.write("options=" + options + "\n") + if profile == "python": + output.write("artifact_paths<")) + if target.scheme or target.netloc or not target.path: + continue + local = Path(unquote(target.path)) + local = local if local.is_absolute() else path.parent / local + if not local.exists(): + failures.append(filename + ": missing link " + link) + if failures: + raise SystemExit("\n".join(failures)) + + python: + name: snodec / python (${{ matrix.os }}) + needs: prepare + if: ${{ needs.prepare.outputs.profile == 'python' }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, windows-2025, macos-15] + runs-on: ${{ matrix.os }} + env: + CI_CONFIG: ${{ needs.prepare.outputs.config }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: ${{ fromJSON(needs.prepare.outputs.config).python_version }} + - name: Install development dependencies + shell: python + run: | + import json + import os + import subprocess + subprocess.run(["python", "-m", "pip", "install", "-e", json.loads(os.environ["CI_CONFIG"])["pip"]], check=True) + - name: Linux desktop dependencies + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y libegl1 libopengl0 libxcb-cursor0 libxkbcommon-x11-0 libpulse0 + - name: Tests independent of physical audio devices + env: + QT_QPA_PLATFORM: offscreen + run: python -m pytest -q -m "not audio_device" + - name: Build native package + id: package + if: ${{ always() && !cancelled() }} + shell: python + run: | + import json + import os + import subprocess + subprocess.run(json.loads(os.environ["CI_CONFIG"])["native_build"], check=True) + - name: Native packaged launch + if: ${{ always() && !cancelled() && steps.package.outcome == 'success' }} + env: + QT_QPA_PLATFORM: offscreen + shell: python + run: | + import json + import os + import subprocess + subprocess.run(json.loads(os.environ["CI_CONFIG"])["smoke"][os.environ["RUNNER_OS"]], check=True) + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + if: ${{ always() && !cancelled() && steps.package.outcome == 'success' }} + with: + name: ${{ github.event.repository.name }}-${{ runner.os }}-${{ runner.arch }} + path: ${{ needs.prepare.outputs.artifact_paths }} + if-no-files-found: error diff --git a/PHASE2.md b/PHASE2.md new file mode 100644 index 0000000..f488d6e --- /dev/null +++ b/PHASE2.md @@ -0,0 +1,82 @@ +# Phase 2: verification consolidation + +## Governing invariant + +Build/test policy lives once in the control repository. A caller owns build +options in `.snodec/repo.yml` and only genuinely project-specific `x-` jobs. +Nothing grants build/test code a release write token. The separate development +publisher executes control-owned orchestration, not repository build scripts. + +The approved extensions add the missing Python profile and SDK handoff; +reduction alone cannot supply those capabilities. They replace the duplicated +native/source-build jobs rather than leave two CI implementations in service. +Repository READMEs, licence files, package workflows, secrets, stable product +tags and default branch names are outside this change. + +## Preserved behavior and new checks + +| Repository | Shared profile | Repository-specific behavior | +| --- | --- | --- | +| snode.c | cpp, tests/apps enabled | Existing 212 tests, including installed-consumer/policy coverage; separate default-branch SDK publisher | +| mqttsuite | cpp, snode.c SDK | Six new CTest smoke tests: five CLI help contracts and real MQTT CONNECT/SUBSCRIBE/live and retained PUBLISH/PING/DISCONNECT; separate SDK publisher | +| AISuite | cpp, snode.c SDK | Codex tests/apps enabled, IPv6/runtime directories; unchanged protocol-generation command as `x-protocol`; publisher requires both Verify and protocol validation | +| CodexUI | cpp, snode.c/AISuite SDKs | Native qualification with Xvfb/offscreen and existing timing policy; web qualification remains `x-web` (Node 22/24.20.0, checksum-pinned Chrome, profiling, production/browser qualification, diagnostics and uploads) | +| AutoTalk | python | Three native platforms, non-audio-device pytest, native build, packaged smoke launch, archive/checksum artifacts; old native-build workflow removed | + +The snode.c policy test still strictly restricts package triggers and workflow +paths; it additionally recognizes the thin Verify caller and tests inline event +parsing, duplicate events and unexpected events. No existing test is disabled. +MQTTSuite help deliberately returns 2 when framework bootstrap displays help; +its smoke test verifies this established contract and the actual usage output. + +## Evidence scope + +Local C++ qualification uses the requested GCC 15.3.0 / Debian trixie container, +real source trees, isolated runtime directories and unprivileged CTest. SDK +relocation is tested in a separate container with only extracted installs, no +producer staging/build paths: the existing SNode.C installed consumer runs and +CodexUI configures/builds against relocated snode.c and AISuite exports. +The control unit suite executes the actual workflow scripts with real tiny +CMake builds; only network/privileged boundaries are mocked there. + +Default-branch green runs and actual GitHub development publication cannot be +claimed before merging the coordinated PRs. Windows/macOS native packaging and +CodexUI browser qualification must be confirmed by their GitHub jobs. PRs remain +draft where these prerequisites or qualification failures are outstanding. + +## Merge order + +1. Merge the control PR; maintainer publishes its reviewed version and moving + `v1` in **SNodeC/.github**. No tag is created by this implementation task. +2. Merge snode.c; require green default-branch Verify and a published `dev` SDK. +3. Merge mqttsuite and AISuite; require green Verify, AISuite protocol validation + and published SDKs. Stale/missing SDKs fail closed rather than rebuild silently. +4. Merge CodexUI after its prerequisites and qualification are green; AutoTalk + depends only on the shared workflow release. +5. Phase 2 gate: snode.c and mqttsuite are green on their API-reported default + branches. Phase 3 package migration and Phase 4 README alignment are not started. + +## Local results before opening PRs + +- Control: 31 tests pass; catalog validation, deterministic profile drift, + `actionlint 1.7.12`, changed C++/CMake formatting and whitespace checks pass. +- snode.c: all 212 tests pass as an unprivileged user (including the updated + policy test and installed-consumer test). +- mqttsuite: all six new smoke tests pass as an unprivileged user. +- AISuite: all 22 tests pass; the pinned protocol-generation check passes. +- Relocated real SDKs: the existing SNode.C installed consumer runs, and CodexUI + configures/builds with only extracted snode.c/AISuite installs visible. +- CodexUI: full Xvfb/offscreen CTest discovers 84 cases: 16 failures (12 thread + pane semantic-no-op paint gates; four grouped 10,000-row timeouts), four Breeze + cases skipped by their existing availability guard. The same semantic-no-op + failure reproduces under the old root execution model on unchanged sources; + no UI test or implementation is weakened here. Native production build passes. + The preserved web job was structurally compared with its baseline: equal + except replacing AISuite's hard-coded branch with its default-branch checkout. +- AutoTalk Linux/Windows/macOS qualification is not yet claimed green. The + shared matrix preserves the existing jobs; these must pass after release of + the shared workflow. Local Linux qualification uses Python 3.12 and an + isolated container, not the user's desktop or audio services. + +Logs remain in the implementation workspace's `evidence/` directory; they are +not committed build artifacts or substituted for GitHub default-branch gates. diff --git a/README.md b/README.md index 5e48d56..afe13fe 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ rewrite their READMEs, change the package pipeline or create release tags. ## Local checks Python 3 with PyYAML is sufficient for catalog/profile work. CMake, Ninja, Git -and a C++ compiler are also needed for the workflow boundary integration tests. +zstd, tar and a C++ compiler are also needed for the workflow boundary integration tests. ```sh python3 -m venv .venv @@ -51,44 +51,87 @@ In particular, snode.c and mqttsuite still show their current release-package workflow until callers are added in a later phase. The control repository's CI path is included because that workflow is delivered here. -## Reusable C++ Verify - -`.github/workflows/verify.yml` is `workflow_call`-only. A preparation job -validates the caller's manifest and selects the container/capabilities before the -`snodec / cpp` job is scheduled. Checkouts use the exact PR head SHA (otherwise -the event SHA); checkout credentials are not persisted. The C++ checkout includes -submodules. - -The default image is `gcc:15.3.0-trixie`. Base packages are the specified build -dependencies plus Python 3 for stdlib command orchestration. Extra packages must -be individual package names. CMake and CTest arguments are passed as argument -lists, never evaluated as shell fragments. - -Dependencies support `@source` only, in declared order. -The GitHub API supplies each default branch and resolves it to a commit before -checkout; source submodules are included. Each dependency is configured in -Release mode with `BUILD_TESTING=OFF`, built and installed into its own staging -prefix. Previously staged dependencies flow through `CMAKE_PREFIX_PATH` and -`LD_LIBRARY_PATH` (`lib` and `lib64`). The API token is removed from the -environment before any dependency/caller build code runs. - -The caller is configured with Ninja and its `cmake_options`, built, tested -when enabled, and installed into a staging prefix. The workflow owns staging -prefixes and the dependency search path; it sets `CMAKE_INSTALL_LIBDIR=lib`. -Runtime directories and the snodec group are opt-in. IPv6 is also opt-in and -enables NET_ADMIN, the IPv6 sysctl and a dummy interface with `fd00::1/128`. -Whitespace checking runs even after a build/test failure, against the PR base -or the previous push commit (the empty tree for an initial commit). - -Tests execute the actual inline workflow scripts, including a real two-dependency -CMake/CTest/install boundary fixture. They do not duplicate the implementation. -The control CI runs catalog checks, tests and profile drift independently so -one failure does not suppress subsequent checks. - -After merge, **the maintainer** publishes `v1.0.0` and the moving `v1`. -Templates are not installed into other repositories in this phase. Other CI -profiles, organization sync PRs, package/release/site/figure lanes and dev SDK -assets belong to later phases. - -See [evidence](EVIDENCE.md), [brand values](brand/README.md), -[contributing](CONTRIBUTING.md) and [security](SECURITY.md). +## Shared Verify (Phase 2) + +`.github/workflows/verify.yml` remains `workflow_call`-only and read-only. The +manifest is the single authority for build options; the catalog holds identity. +Preparation validates the manifest before selecting the C++ container or native +Python matrix. Every checkout uses the exact PR head SHA, otherwise the event +SHA, with no persisted credentials. C++ includes recursive submodules. + +### C++ + +The default container is `gcc:15.3.0-trixie`. Extra apt packages, CMake arguments, +CTest arguments and optional `ctest_launcher` are argument lists, not shell +fragments. `parallel` controls build workers. Non-secret `env` values reach +builds and tests; reserved GitHub/runner/token variables are rejected. + +Ordered dependencies accept `@source` (API-resolved default branch, +exact commit, Debug build with `BUILD_TESTING=OFF`) or `@dev` (tested +SDK for the same distribution/version/architecture). Both stage installs and +supply `CMAKE_PREFIX_PATH` and `LD_LIBRARY_PATH`. SDK downloads verify the GitHub +asset SHA-256 digest, use safe tar extraction, check repository/target/commit +identity, and reject inconsistent dependency revisions. There is no source +fallback when an SDK is absent or stale. Dependency updates must publish in +order: snode.c, then AISuite, then CodexUI. mqttsuite depends only on snode.c. +The read token is removed before executing any build code. + +The caller configures with Ninja, builds, runs CTest as an unprivileged `snodec` +user when enabled (`--no-tests=error`), then installs into a staging prefix. +`CMAKE_INSTALL_LIBDIR=lib` and dependency paths are owned by the workflow. +Runtime directories and IPv6 remain opt-in; IPv6 adds NET_ADMIN, the sysctl and +`fd00::1/128` on a dummy interface. snode.c enables its existing component, unit, +policy and staged installed-consumer tests explicitly in its manifest. + +### Python + +AutoTalk's existing qualification becomes the shared `python` profile: +Python 3.12 on Ubuntu 24.04, Windows 2025 and macOS 15, editable development +install, pytest excluding physical audio devices, native package build, +packaged smoke launch and archive/checksum upload. The manifest supplies only +local project arguments, build/launch commands and artifact paths. Matrix +failures do not cancel the other platforms. Package qualification can still run +after test failures, without turning a failed test job green. + +### Independent hygiene + +Whitespace checks run against the PR base or previous push commit (empty tree +on the initial commit). An independent job also checks changed C/C++ and CMake +files against existing repository formatting policies and changed Markdown's +local file links. It neither invents a style where none exists nor performs +unreliable external-URL probes. Failures are collected rather than stopping at +the first file. This job still runs when compilation/tests fail. + +### Development SDK publication + +`sdk: true` exports a staged install after successful default-branch C++ tests. +The product's separate `x-sdk` job calls `publish-dev.yml`, with the only +`contents: write` grant. It waits for all shared Verify jobs; AISuite also waits +for `x-protocol`. The publisher never checks out or executes product code. It +checks the current API-resolved default-branch HEAD, rejects superseded runs and +mismatched artifact identities, and creates/updates only the unsigned rolling +`dev` prerelease/tag. Asset names are `-dev---.tar.zst`. +Stable version tags, secrets and package workflows are untouched. + +### Coordinated rollout + +Merge the control workflow change first. The maintainer then publishes the +reviewed shared-workflow version and moving `v1` alias in **SNodeC/.github**. +The existing `v1.0` does not substitute for `@v1`, and predates these extensions. +Merge snode.c next and wait for green default-branch CI plus its `dev` SDK. +Then merge mqttsuite and AISuite; wait for AISuite's green CI/protocol check and +SDK before CodexUI. AutoTalk can follow the control workflow independently. +Caller PRs must not merge while `v1` or their development SDK dependencies are +missing. No duplicate old/native build jobs remain after their migration. + +Tests execute the actual inline workflow scripts, including ordered source +builds, a real SDK export/download/relocation/CTest/install consumer, invalid +SDK identities/digests, Python commands, hygiene aggregation and publication +boundaries. Local boundary tests additionally require **zstd** and tar. + +Repository README frames are **Phase 4**, not Phase 2. Package/release/site, +figure, ecosystem and org-sync lanes remain separate later phases. + +See [Phase 2 evidence and rollout](PHASE2.md), [Phase 1 evidence](EVIDENCE.md), +[brand values](brand/README.md), [contributing](CONTRIBUTING.md) and +[security](SECURITY.md). diff --git a/templates/repo.yml b/templates/repo.yml index a7b7873..036f53c 100644 --- a/templates/repo.yml +++ b/templates/repo.yml @@ -1,13 +1,18 @@ -# Copy to .snodec/repo.yml in a non-fork C++ repository in a later phase. +# Copy to .snodec/repo.yml in a non-fork C++ repository. # Identity (area, kind, summary and ecosystem edges) lives only in catalog.yml. ci: profile: cpp container: gcc:15.3.0-trixie apt: [] # Additional Debian packages; individual package names. deps: [] # Ordered source builds, e.g. [snode.c@source, AISuite@source]. - # @source resolves each dependency's default branch via the API. + # @source resolves the default branch via API; @dev downloads a tested SDK. + # @dev must exist first: merge/publish dependencies before consumers. cmake_options: [] # Individual CMake arguments, e.g. [-DCMAKE_BUILD_TYPE=Release]. ctest: true ctest_args: [--output-on-failure] ipv6: false # NET_ADMIN, IPv6 enabled and dummy interface fd00::1/128. runtime_dirs: false # snodec group; /etc/snode.c, /var/log/snode.c, /var/run/snode.c. + parallel: 2 # Build workers (1–32); ctest parallelism belongs in ctest_args. + sdk: false # Export on default branch; product x-sdk job publishes separately. + env: {} # Non-secret build/test environment, e.g. CODEXUI_TIMING_POLICY. + ctest_launcher: [] # Optional argument-list wrapper, e.g. [xvfb-run, -a]. diff --git a/tests/test_verify.py b/tests/test_verify.py index cace9a4..d362563 100644 --- a/tests/test_verify.py +++ b/tests/test_verify.py @@ -36,7 +36,21 @@ def prepare(path, ci): output.write_text("") with directory(path), patch.dict(os.environ, {"GITHUB_OUTPUT": str(output)}): exec(compile(script("prepare", "Read and validate manifest"), "manifest", "exec"), {}) - return dict(line.split("=", 1) for line in output.read_text().splitlines()) + result = {} + lines = iter(output.read_text().splitlines()) + for line in lines: + if "<<" in line: + key, delimiter = line.split("<<", 1) + values = [] + for value in lines: + if value == delimiter: + break + values.append(value) + result[key] = "\n".join(values) + else: + key, value = line.split("=", 1) + result[key] = value + return result class ManifestTests(unittest.TestCase): @@ -66,11 +80,12 @@ def test_valid_custom_options(self): def test_invalid_manifests_fail_before_container(self): cases = [{"profile": "python"}, {"ipv6": "true"}, {"runtime_dirs": 1}, - {"ctest": None}, {"apt": "--bad"}, {"deps": ["snode.c@dev"]}, + {"ctest": None}, {"apt": "--bad"}, {"deps": ["snode.c@release"]}, {"deps": ["snode.c@source"] * 2}, {"apt": ["--allow-unauthenticated"]}, {"apt": ["libssl-dev;echo bad"]}, {"container": "gcc:15\noptions=bad"}, {"cmake_options": [False]}, {"ctest_args": ["bad\0argument"]}, - {"deps": ["other/snode.c@source"]}, {"unknown": 1}] + {"deps": ["other/snode.c@source"]}, {"unknown": 1}, {"parallel": 0}, {"parallel": True}, + {"sdk": "yes"}, {"env": {"GH_READ_TOKEN": "bad"}}, {"env": {"OK": 1}}] with tempfile.TemporaryDirectory() as directory_name: for ci in cases: with self.subTest(ci=ci), self.assertRaises(ValueError): @@ -128,7 +143,9 @@ def test_runtime_and_ipv6_are_opt_in(self): with patch("subprocess.run") as run, patch.dict(os.environ, {"CI_CONFIG": json.dumps( {"apt": [], "runtime_dirs": False, "ipv6": False})}): exec(code, {}) - run.assert_not_called() + commands = [call.args[0] for call in run.call_args_list] + self.assertTrue(any(c[0] == "useradd" for c in commands)) + self.assertFalse(any(c[0] in ("ip", "install", "apt-get") for c in commands)) with patch("subprocess.run") as run, patch("grp.getgrnam", side_effect=KeyError), \ patch.dict(os.environ, {"CI_CONFIG": json.dumps( {"apt": ["zlib1g-dev"], "runtime_dirs": True, "ipv6": True})}): @@ -143,11 +160,13 @@ def test_disabled_ctest_and_arguments_are_not_shell_code(self): with tempfile.TemporaryDirectory() as directory_name: ci = json.loads(prepare(Path(directory_name), {"ctest": False, "cmake_options": ["-DTEXT=a value;echo not executed"]})["config"]) - with patch("subprocess.run") as run, patch.dict(os.environ, { + with patch("subprocess.run") as run, patch("subprocess.check_output", return_value="a" * 40), patch.dict(os.environ, { "CI_CONFIG": json.dumps(ci), "GITHUB_WORKSPACE": directory_name, "RUNNER_TEMP": directory_name, "GH_READ_TOKEN": "must-not-leak", + "GITHUB_REPOSITORY": "SNodeC/fixture", }): - exec(script("cpp", "Build ordered source dependencies, test and stage caller"), {}) + with patch("platform.freedesktop_os_release", return_value={"ID": "debian", "VERSION_ID": "13"}): + exec(script("cpp", "Build ordered source dependencies, test and stage caller"), {}) commands = [call.args[0] for call in run.call_args_list] self.assertFalse(any(c[0] == "ctest" for c in commands)) self.assertIn("-DTEXT=a value;echo not executed", commands[0]) @@ -207,6 +226,8 @@ def test_real_ordered_dependency_consumer_ctest_and_install(self): add_test(NAME first_success COMMAND consumer) install(TARGETS consumer DESTINATION bin) """) + git(caller, "init", "--initial-branch=caller/default") + commit(caller) ci = json.loads(prepare(caller, {"deps": ["depone@source", "deptwo@source"]})["config"]) requests, commands = [], [] actual_run = subprocess.run @@ -220,18 +241,23 @@ def response(request, **kwargs): return io.BytesIO(json.dumps(data).encode()) def run(args, **kwargs): - commands.append((list(args), dict(kwargs["env"]))) - self.assertNotIn("GH_READ_TOKEN", kwargs["env"]) + commands.append((list(args), dict(kwargs.get("env", os.environ)))) + self.assertNotIn("GH_READ_TOKEN", kwargs.get("env", os.environ)) if args[0] == "git" and "remote" in args: args[-1] = str(root / Path(args[2]).name) + if args[0] == "chown": + return subprocess.CompletedProcess(args, 0) + if args[0] == "runuser": + args = args[args.index("ctest"):] return actual_run(args, **kwargs) with patch.dict(os.environ, {"CI_CONFIG": json.dumps(ci), "GITHUB_WORKSPACE": str(caller), "RUNNER_TEMP": str(root), - "GH_READ_TOKEN": "read-only-fixture"}), \ + "GH_READ_TOKEN": "read-only-fixture", "GITHUB_REPOSITORY": "SNodeC/consumer"}), \ patch("urllib.request.urlopen", side_effect=response), \ patch("subprocess.run", side_effect=run): - exec(script("cpp", "Build ordered source dependencies, test and stage caller"), {}) + with patch("platform.freedesktop_os_release", return_value={"ID": "debian", "VERSION_ID": "13"}): + exec(script("cpp", "Build ordered source dependencies, test and stage caller"), {}) stage = root / "snodec/stage/caller/bin/consumer" self.assertTrue(stage.is_file()) self.assertEqual(subprocess.run([stage]).returncode, 0) @@ -246,12 +272,12 @@ def run(args, **kwargs): self.assertEqual([Path(cmd[2]).name for cmd, _ in configurations], ["depone", "deptwo", "caller"]) self.assertIn(str(root / "snodec/stage/depone/lib"), configurations[1][1]["LD_LIBRARY_PATH"]) self.assertIn(str(root / "snodec/stage/deptwo/lib"), configurations[2][1]["LD_LIBRARY_PATH"]) - tests = [cmd for cmd, _ in commands if cmd[0] == "ctest"] + tests = [cmd for cmd, _ in commands if "ctest" in cmd] self.assertEqual(len(tests), 1) self.assertIn("--output-on-failure", tests[0]) def test_whitespace_initial_push_and_pr_base(self): - code = script("cpp", "Check whitespace against PR base or previous commit") + code = script("hygiene", "Check whitespace against PR base or previous commit") with tempfile.TemporaryDirectory() as directory_name: path = Path(directory_name) git(path, "init", "--initial-branch=fixture/default") @@ -270,3 +296,223 @@ def check(base=""): commit(path) self.assertEqual(check(), 0) self.assertNotEqual(check(initial), 0) + + +class PhaseTwoBoundaryTests(unittest.TestCase): + def test_python_manifest_and_native_commands(self): + ci = {"profile": "python", "native_build": ["python", "tools/build.py"], + "smoke": {system: ["dist/program", "--smoke-test"] for system in ("Linux", "Windows", "macOS")}, + "artifact_globs": ["dist/*.zip", "dist/*.tar.gz"]} + with tempfile.TemporaryDirectory() as name: + output = prepare(Path(name), ci) + config = json.loads(output["config"]) + self.assertEqual(output["profile"], "python") + self.assertEqual(output["artifact_paths"], "dist/*.zip\ndist/*.tar.gz") + with patch("subprocess.run") as run, patch.dict(os.environ, { + "CI_CONFIG": output["config"], "RUNNER_OS": "Windows", + }): + exec(script("python", "Install development dependencies"), {}) + exec(script("python", "Build native package"), {}) + exec(script("python", "Native packaged launch"), {}) + self.assertEqual([call.args[0] for call in run.call_args_list], [ + ["python", "-m", "pip", "install", "-e", ".[dev]"], + ci["native_build"], ci["smoke"]["Windows"], + ]) + self.assertEqual(config["python_version"], "3.12") + for change in ({"smoke": {}}, {"python_version": "2.7"}, {"pip": "remote-package"}, + {"native_build": "python build.py"}, {"artifact_globs": ["bad\npath"]}): + with self.subTest(change=change), self.assertRaises(ValueError): + prepare(Path(name), ci | change) + job = WORKFLOW["jobs"]["python"] + self.assertEqual(job["strategy"]["matrix"]["os"], ["ubuntu-24.04", "windows-2025", "macos-15"]) + self.assertEqual(job["strategy"]["fail-fast"], "false") + self.assertIn('not audio_device', script("python", "Tests independent of physical audio devices")) + + def test_changed_file_hygiene_collects_all_failures(self): + code = script("hygiene", "Check changed source formatting and documentation links") + with tempfile.TemporaryDirectory() as name: + root = Path(name) + git(root, "init", "--initial-branch=hygiene/from-api") + (root / "old.md").write_text("[legacy](missing-old.md)\n") + commit(root) + (root / ".clang-format").write_text("BasedOnStyle: LLVM\n") + (root / ".cmake-format.py").write_text("") + (root / "changed.cpp").write_text("int main() {return 0;}\n") + (root / "CMakeLists.txt").write_text("project( test)\n") + (root / "new.md").write_text("[external](https://example.invalid) [missing](missing-new.md)\n```\n[code](ignored.md)\n```\n") + commit(root) + actual_run = subprocess.run + tools = [] + + def run(args, **kwargs): + if args[0] in ("clang-format", "cmake-format"): + tools.append(args[0]) + return subprocess.CompletedProcess(args, 1) + return actual_run(args, **kwargs) + + with directory(root), patch.dict(os.environ, {"PR_BASE": ""}), patch("subprocess.run", side_effect=run): + with self.assertRaises(SystemExit) as error: + exec(code, {}) + self.assertEqual(set(tools), {"clang-format", "cmake-format"}) + self.assertIn("missing-new.md", str(error.exception)) + self.assertNotIn("missing-old.md", str(error.exception)) + self.assertNotIn("ignored.md", str(error.exception)) + + def test_real_sdk_export_download_relocation_and_consumer(self): + import hashlib + import platform + with tempfile.TemporaryDirectory() as name: + root = Path(name) + source = root / "depone" + sha = dependency(source, "depone", first=True) + producer = root / "producer" + prefix = producer / "snodec/stage/caller" + subprocess.run(["cmake", "-S", source, "-B", root / "producer-build", "-G", "Ninja", + "-DCMAKE_INSTALL_PREFIX=" + str(prefix)], check=True) + subprocess.run(["cmake", "--build", root / "producer-build"], check=True) + subprocess.run(["cmake", "--install", root / "producer-build"], check=True) + target = "debian-13-" + platform.machine() + identity = {"repository": "SNodeC/depone", "source_commit": sha, "target": target, "dependencies": {}} + (prefix / ".snodec-sdk.json").write_text(json.dumps(identity)) + output = root / "sdk-output" + with patch.dict(os.environ, {"RUNNER_TEMP": str(producer), "GITHUB_OUTPUT": str(output)}): + exec(script("cpp", "Export tested development SDK"), {}) + archive = producer / output.read_text().strip().split("=", 1)[1] + data = archive.read_bytes() + # Remove the original prefix: imported CMake targets must really relocate. + import shutil + shutil.rmtree(producer / "snodec") + caller = root / "caller" + caller.mkdir() + git(caller, "init", "--initial-branch=sdk/consumer") + (caller / "main.cpp").write_text('#include "depone.h"\nint main(){return depone() == 41 ? 0 : 1;}\n') + (caller / "CMakeLists.txt").write_text('''cmake_minimum_required(VERSION 3.20) +project(consumer LANGUAGES CXX) +find_package(depone CONFIG REQUIRED) +add_executable(consumer main.cpp) +target_link_libraries(consumer PRIVATE depone::depone) +enable_testing() +add_test(NAME first_success COMMAND consumer) +install(TARGETS consumer DESTINATION bin) +''') + commit(caller) + ci = prepare(caller, {"deps": ["depone@dev"], "env": {"TIMING_POLICY": "report"}})["config"] + actual_run = subprocess.run + commands = [] + digest = "sha256:" + hashlib.sha256(data).hexdigest() + + def response(request, **kwargs): + if isinstance(request, str): + return io.BytesIO(data) + self.assertTrue(request.full_url.endswith("/releases/tags/dev")) + return io.BytesIO(json.dumps({"assets": [{"name": archive.name, + "browser_download_url": "https://example.invalid/sdk", "digest": digest}]}).encode()) + + def run(args, **kwargs): + commands.append(list(args)) + if "env" in kwargs: + self.assertEqual(kwargs["env"]["TIMING_POLICY"], "report") + self.assertNotIn("GH_READ_TOKEN", kwargs["env"]) + if args[0] == "chown": + return subprocess.CompletedProcess(args, 0) + if args[0] == "runuser": + args = args[args.index("ctest"):] + return actual_run(args, **kwargs) + + env = {"CI_CONFIG": ci, "GITHUB_WORKSPACE": str(caller), "RUNNER_TEMP": str(root / "consumer-run"), + "GITHUB_REPOSITORY": "SNodeC/consumer", "GH_READ_TOKEN": "no-leak"} + code = script("cpp", "Build ordered source dependencies, test and stage caller") + with patch.dict(os.environ, env), patch("platform.freedesktop_os_release", return_value={"ID": "debian", "VERSION_ID": "13"}), \ + patch("urllib.request.urlopen", side_effect=response), patch("subprocess.run", side_effect=run): + exec(code, {}) + self.assertFalse(any(cmd[0] == "git" and "fetch" in cmd for cmd in commands)) + self.assertEqual(subprocess.run([root / "consumer-run/snodec/stage/caller/bin/consumer"]).returncode, 0) + caller_identity = json.loads((root / "consumer-run/snodec/stage/caller/.snodec-sdk.json").read_text()) + self.assertEqual(caller_identity["dependencies"], {"depone": sha}) + digest = "sha256:incorrect" + env["RUNNER_TEMP"] = str(root / "bad-digest") + with patch.dict(os.environ, env), patch("platform.freedesktop_os_release", return_value={"ID": "debian", "VERSION_ID": "13"}), \ + patch("urllib.request.urlopen", side_effect=response), self.assertRaisesRegex(ValueError, "digest"): + exec(code, {}) + + for index, change in enumerate(({"repository": "SNodeC/wrong"}, {"target": "wrong-target"}, + {"source_commit": "not-a-commit"}, {"dependencies": {"missing": sha}})): + with self.subTest(change=change): + import tarfile + metadata = json.dumps(identity | change).encode() + buffer = io.BytesIO() + with tarfile.open(fileobj=buffer, mode="w") as contents: + member = tarfile.TarInfo(".snodec-sdk.json") + member.size = len(metadata) + contents.addfile(member, io.BytesIO(metadata)) + data = subprocess.check_output(["zstd", "-q", "-c"], input=buffer.getvalue()) + digest = "sha256:" + hashlib.sha256(data).hexdigest() + env["RUNNER_TEMP"] = str(root / ("invalid-identity-" + str(index))) + with patch.dict(os.environ, env), patch("platform.freedesktop_os_release", return_value={"ID": "debian", "VERSION_ID": "13"}), \ + patch("urllib.request.urlopen", side_effect=response), self.assertRaises(ValueError): + exec(code, {}) + + def test_sdk_publisher_is_default_branch_only_and_never_versions_products(self): + from urllib.error import HTTPError + publisher = yaml.load((ROOT / ".github/workflows/publish-dev.yml").read_text(), Loader=yaml.BaseLoader) + job = publisher["jobs"]["publish"] + self.assertIn("github.event.repository.default_branch", job["if"]) + self.assertEqual(publisher["permissions"], {"contents": "write"}) + self.assertFalse(any("checkout" in step.get("uses", "") for step in job["steps"])) + code = next(step["run"] for step in job["steps"] if step.get("name") == "Publish current default-branch SDK only") + sha = "a" * 40 + with tempfile.TemporaryDirectory() as name: + root = Path(name) + (root / "sdk").mkdir() + stage = root / "stage" + stage.mkdir() + (stage / ".snodec-sdk.json").write_text(json.dumps({"repository": "SNodeC/fixture", "source_commit": sha})) + archive = root / "sdk/fixture-dev-debian-13-x86_64.tar.zst" + subprocess.run(["tar", "--zstd", "-cf", archive, "-C", stage, "."], check=True) + actual_run = subprocess.run + for current, existing in (("b" * 40, False), (sha, False), (sha, True)): + calls, commands = [], [] + temporary = root / ("run-" + current[:1] + str(existing)) + temporary.mkdir() + + def response(request, **kwargs): + path = request.full_url.removeprefix("https://api.github.com/") + data = json.loads(request.data) if request.data else None + calls.append((path, request.method, data)) + if path.endswith("/fixture"): + result = {"default_branch": "arbitrary/default"} + elif "/commits/" in path: + self.assertTrue(path.endswith("arbitrary%2Fdefault")) + result = {"sha": current} + elif path.endswith("/releases/tags/dev"): + if not existing: + raise HTTPError(request.full_url, 404, "not found", {}, None) + result = {"prerelease": True} + else: + result = {} + return io.BytesIO(json.dumps(result).encode()) + + def run(args, **kwargs): + commands.append([str(arg) for arg in args]) + if args[0] == "gh": + return subprocess.CompletedProcess(args, 0) + return actual_run(args, **kwargs) + + with directory(root), patch.dict(os.environ, {"GITHUB_REPOSITORY": "SNodeC/fixture", + "GITHUB_SHA": sha, "GH_TOKEN": "write-fixture", "RUNNER_TEMP": str(temporary)}), \ + patch("urllib.request.urlopen", side_effect=response), patch("subprocess.run", side_effect=run): + exec(code, {}) + if current != sha: + self.assertEqual(commands, []) + self.assertTrue(all(method == "GET" for _, method, _ in calls)) + else: + self.assertIn(["gh", "release", "upload", "dev", "sdk/" + archive.name, + "--clobber", "--repo", "SNodeC/fixture"], commands) + writes = [(path, data) for path, method, data in calls if method != "GET"] + self.assertTrue(all(path.endswith(("/releases", "/git/refs/tags/dev")) for path, _ in writes)) + if not existing: + release = next(data for path, data in writes if path.endswith("/releases")) + self.assertEqual(release["tag_name"], "dev") + self.assertTrue(release["prerelease"]) + self.assertEqual(release["make_latest"], "false") + self.assertEqual(writes[-1][1], {"sha": sha, "force": True}) From fe6df263e8ade679d143f8c8fe6934462383a9a0 Mon Sep 17 00:00:00 2001 From: Volker Christian Date: Sat, 10 Oct 2026 18:24:49 +0200 Subject: [PATCH 2/3] Require one staged authority per dependency repository --- .github/workflows/verify.yml | 2 +- tests/test_verify.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 4dee93d..1fddd30 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -87,7 +87,7 @@ jobs: raise ValueError("apt must contain package names, not options") if profile == "cpp" and any(not re.fullmatch(r"[A-Za-z0-9_.-]+@(source|dev)", d) for d in config["deps"]): raise ValueError("deps supports SNodeC repository names with @source or @dev") - if profile == "cpp" and len(config["deps"]) != len(set(config["deps"])): + if profile == "cpp" and len(config["deps"]) != len({d.split("@")[0] for d in config["deps"]}): raise ValueError("deps must be unique and ordered") options = "--cap-add=NET_ADMIN --sysctl net.ipv6.conf.all.disable_ipv6=0" if config.get("ipv6") else "" with open(os.environ["GITHUB_OUTPUT"], "a") as output: diff --git a/tests/test_verify.py b/tests/test_verify.py index d362563..9317355 100644 --- a/tests/test_verify.py +++ b/tests/test_verify.py @@ -81,7 +81,7 @@ def test_valid_custom_options(self): def test_invalid_manifests_fail_before_container(self): cases = [{"profile": "python"}, {"ipv6": "true"}, {"runtime_dirs": 1}, {"ctest": None}, {"apt": "--bad"}, {"deps": ["snode.c@release"]}, - {"deps": ["snode.c@source"] * 2}, {"apt": ["--allow-unauthenticated"]}, + {"deps": ["snode.c@source"] * 2}, {"deps": ["snode.c@source", "snode.c@dev"]}, {"apt": ["--allow-unauthenticated"]}, {"apt": ["libssl-dev;echo bad"]}, {"container": "gcc:15\noptions=bad"}, {"cmake_options": [False]}, {"ctest_args": ["bad\0argument"]}, {"deps": ["other/snode.c@source"]}, {"unknown": 1}, {"parallel": 0}, {"parallel": True}, From 9a13911a6d752b046cdce91b15fe1bcfa6518a05 Mon Sep 17 00:00:00 2001 From: Volker Christian Date: Sat, 10 Oct 2026 18:31:08 +0200 Subject: [PATCH 3/3] Record coordinated PRs and explicitly defer application test fixes --- PHASE2.md | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/PHASE2.md b/PHASE2.md index f488d6e..7115a21 100644 --- a/PHASE2.md +++ b/PHASE2.md @@ -42,7 +42,8 @@ CMake builds; only network/privileged boundaries are mocked there. Default-branch green runs and actual GitHub development publication cannot be claimed before merging the coordinated PRs. Windows/macOS native packaging and CodexUI browser qualification must be confirmed by their GitHub jobs. PRs remain -draft where these prerequisites or qualification failures are outstanding. +draft where these prerequisites or shared-workflow/SDK prerequisites are outstanding. App test fixes were +explicitly deferred by the maintainer, not silently skipped. ## Merge order @@ -51,8 +52,9 @@ draft where these prerequisites or qualification failures are outstanding. 2. Merge snode.c; require green default-branch Verify and a published `dev` SDK. 3. Merge mqttsuite and AISuite; require green Verify, AISuite protocol validation and published SDKs. Stale/missing SDKs fail closed rather than rebuild silently. -4. Merge CodexUI after its prerequisites and qualification are green; AutoTalk - depends only on the shared workflow release. +4. CodexUI follows its SDK prerequisites; AutoTalk depends only on the shared + workflow release. The maintainer explicitly deferred their existing test + defects to later work; tests and failure gates remain enabled. 5. Phase 2 gate: snode.c and mqttsuite are green on their API-reported default branches. Phase 3 package migration and Phase 4 README alignment are not started. @@ -73,10 +75,26 @@ draft where these prerequisites or qualification failures are outstanding. no UI test or implementation is weakened here. Native production build passes. The preserved web job was structurally compared with its baseline: equal except replacing AISuite's hard-coded branch with its default-branch checkout. -- AutoTalk Linux/Windows/macOS qualification is not yet claimed green. The +- AutoTalk Linux native package build and packaged smoke launch pass locally. + The full pytest run hangs in prepared-playback qualification; Linux/Windows/macOS + full qualification is not claimed green. The shared matrix preserves the existing jobs; these must pass after release of the shared workflow. Local Linux qualification uses Python 3.12 and an isolated container, not the user's desktop or audio services. Logs remain in the implementation workspace's `evidence/` directory; they are not committed build artifacts or substituted for GitHub default-branch gates. + +## Explicitly deferred work + +The maintainer requested on 2026-10-10: defer CodexUI and AutoTalk test fixes. +Their tests remain enabled with unchanged correctness gates. Local investigation +was stopped; no application implementation or audio-test skip was added. The +control, snode.c, mqttsuite and AISuite verification results above are unaffected. + +Coordinated PRs: [control #2](https://github.com/SNodeC/.github/pull/2), +[snode.c #266](https://github.com/SNodeC/snode.c/pull/266), +[mqttsuite #31](https://github.com/SNodeC/mqttsuite/pull/31), +[AISuite #60](https://github.com/SNodeC/AISuite/pull/60), +[CodexUI #76](https://github.com/SNodeC/CodexUI/pull/76), +[AutoTalk #6](https://github.com/SNodeC/AutoTalk/pull/6).