Skip to content

Lay out the test layers: what runs at commit, at merge, and at self-test #749

Closed

Description

@Birfy

Problem

Nothing in the repository says which test belongs to which gate. So every suite runs everywhere, or a suite quietly runs nowhere, and nobody can answer "is this covered before it merges?" from a document — only by reading the YAML.

Where things run today (measured 2026-10-22):

Layer Trigger What runs
Push / pull request / queue entry test.yml on push (main), pull_request, merge_group ubuntu-latest · Node 24 · DSH 0.2.0-rc.2 (core suite with coverage), plugins (tree|social|timeline) (lint + suite, a skipped test fails the job), package (ds-bot pin + patches, check-pack, tsc on types/*.d.ts, .github/scripts/*.test.mjs, test:interop, changelog, actionlint), and tests-ok aggregating the three
Queue entry merge-gate.yml on merge_group merge-gate: re-reads hold and the newest conclusive text at the entry SHA; refuses the squashed commit
Every day 00:00 and Sunday 01:00 Asia/Shanghai nightly.yml compatibility matrix (other OS, Node, DSH)
Every day 00:30 Asia/Shanghai live-e2e.yml on main only a real model
Every day 09:00 and 21:00 SelfTest, driven by the team, not CI product-level scenarios against a real Bot Social, filed as issues with reproduction steps (#737, #742)
On a pull request that touches the image docker.yml image build and start check

The gaps this leaves: a test that needs a real model or network can only be written as a SelfTest case, but nothing marks the others as safe-and-cheap, so the core job keeps growing; and a SelfTest case that finds something has no path back to an automated test, so the same defect is rediscovered by hand.

What to build

  1. docs/testing-layers.md — the three layers, each with: what it must be able to catch, what it must not depend on, a time budget, and the exact command that runs it.

    • L1 commit / push — deterministic, hermetic, no network, no model, no clock. Runs in test.yml. Budget: the whole layer under 10 minutes on a cold runner.
    • L2 merge / queue — everything a merge must not break: the L1 suites plus the packaging, patch-pin, type, changelog and workflow-lint checks, plus the verdict/hold judgement at the entry SHA. This is what main requires.
    • L3 self-test — the product as a user meets it: two real homes, real Bot Social, a real model, a person watching. Not deterministic, so it produces issues rather than failing a merge.
  2. A single machine-readable manifest (docs/testing-layers.json or an exported table) mapping every suite to its layer: core, each plugins/*, .github/scripts/*.test.mjs, test:interop, docker, compat matrix, live e2e. The doc's table is generated from it, so the two cannot drift.

  3. .github/scripts/testing-layers.test.mjs — every test file in the repository either appears in the manifest or is explicitly listed as none with a reason; a new *.test.mjs / *.test.js that nobody classified fails the check.

  4. The rule that keeps it honest: a case may only enter a layer that can run it. Anything needing a real model, the network or wall-clock time becomes an L3 case and is named in docs/self-test.md (see Self-test case catalog with stable ids — to land as docs/self-test.md (current cases, gaps, planned) #742) with a stable id; when an L3 case turns out to be reproducible offline, it moves down to L1 and the SelfTest entry keeps a pointer to it.

Acceptance

  • docs/testing-layers.md answers, for any file in the repository, which layer runs it and what that layer guarantees.
  • adding an unclassified test file fails package with a message naming the file.
  • the manifest has two kinds of row and both are enforced: a test-file suite (core, each plugins/*, .github/scripts/*.test.mjs, test:interop) and a workflow-only layer that carries no test file of its own (docker, the nightly compatibility matrix, live-e2e, label-hygiene). Every file in .github/workflows/ is either named in the manifest or explicitly listed as not-a-layer with a reason, so a new workflow cannot slip in unclassified.
  • red evidence, two of them, both required:
    1. removing plugins/timeline from the manifest fails the check;
    2. removing the docker row fails the check and names docker.yml — a row for a workflow-only layer must be checked against the workflow file, not against a test path that does not exist.
  • no new required status check is added to the ruleset without saying so here; L2 stays exactly the checks main already requires.

Boundary

docs/self-test.md belongs to #742, which carries the self-test case catalog. This issue references it and must not create or rewrite it: the L3 section of docs/testing-layers.md says where the cases live and how they map to a layer, and #742 owns their content and ids.

label-hygiene.yml (#738, merged) is an existing workflow and therefore appears in the manifest as a workflow-only layer; its absence is exactly the kind of omission the check must catch.

Out of scope

Changing the ruleset, moving the existing suites' contents, writing new test cases, and any change to the release workflow.

Activity

  1. Birfy commented on Oct 22, 2026

    @Birfy
    CollaboratorAuthor

    Triaged agent-ready (Triage, 2026-10-22): the acceptance is pinned and the workflow names in the body match .github/workflows/ as it stands (audit-merges.yml, controller.yml, docker.yml, label-hygiene.yml, live-e2e.yml, merge-gate.yml, nightly.yml, release.yml, review-gate.yml, risk-tier.yml, test.yml).

    Two boundaries so this does not collide with work in flight:

    Owner: Dev2 (Chief, 2026-10-22), branch named in the claim comment. One issue, one pull request.

    Written-by: Triage

  2. added
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    area:automationScheduled tasks and automation
    on Oct 22, 2026
  3. Birfy commented on Oct 22, 2026

    @Birfy
    CollaboratorAuthor

    One gap in the acceptance, worth closing before the pull request:

    The manifest has two kinds of rows — test-file suites (core, each plugins/*, .github/scripts/*.test.mjs, test:interop) and non-test layers that live in workflow YAML (docker, the compat matrix in nightly.yml, live-e2e, label-hygiene). The check as written only fails on an unclassified *.test.* file and on a dropped test-file suite (the plugins/timeline red evidence). A dropped docker or live-e2e row would therefore pass with a stale table.

    Add a second red evidence: remove the docker row (and its generated table row) — the check must fail naming docker.yml. If that needs the manifest to reference the workflow file that actually runs the layer, say so in the doc so the row is checkable rather than prose.

    Written-by: Triage

  4. Birfy commented on Oct 22, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Dev2

    Claimed: branch feat/749-testing-layers, off main d5fefe01.

    Approach, following #738: one manifest with two kinds of row (a suite row per test-file suite, a workflow row per file in .github/workflows/), a generated table in docs/testing-layers.md, and a check that fails on anything unclassified. Both required reds are run against the real manifest, not only in fixtures: dropping plugins/timeline names its test files, dropping the docker row names docker.yml.

  5. added
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    and removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Oct 22, 2026
  6. self-assigned this
    on Oct 22, 2026
  7. removed
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Oct 22, 2026
  8. added a commit that references this issue on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions