Repository navigation
Description
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
-
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
mainrequires. - 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.
- L1 commit / push — deterministic, hermetic, no network, no model, no clock. Runs in
-
A single machine-readable manifest (
docs/testing-layers.jsonor an exported table) mapping every suite to its layer:core, eachplugins/*,.github/scripts/*.test.mjs,test:interop,docker, compat matrix, live e2e. The doc's table is generated from it, so the two cannot drift. -
.github/scripts/testing-layers.test.mjs— every test file in the repository either appears in the manifest or is explicitly listed asnonewith a reason; a new*.test.mjs/*.test.jsthat nobody classified fails the check. -
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.mdanswers, for any file in the repository, which layer runs it and what that layer guarantees.- adding an unclassified test file fails
packagewith a message naming the file. - the manifest has two kinds of row and both are enforced: a test-file suite (
core, eachplugins/*,.github/scripts/*.test.mjs,test:interop) and a workflow-only layer that carries no test file of its own (docker, thenightlycompatibility 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:
- removing
plugins/timelinefrom the manifest fails the check; - removing the
dockerrow fails the check and namesdocker.yml— a row for a workflow-only layer must be checked against the workflow file, not against a test path that does not exist.
- removing
- no new required status check is added to the ruleset without saying so here; L2 stays exactly the checks
mainalready 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
Birfy commentedon Oct 22, 2026
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:
docs/self-test.mdbelongs to Self-test case catalog with stable ids — to land as docs/self-test.md (current cases, gaps, planned) #742 (the case catalog with stable ids). This issue references it and must not create or rewrite it; if Self-test case catalog with stable ids — to land as docs/self-test.md (current cases, gaps, planned) #742 has not landed yet, link to it and leave the file alone.label-hygiene.ymland its script already exist (from Strip stale agent-* labels mechanically instead of by hand #738) and are part of what the manifest must classify — add them to the list rather than treating them as new.Owner: Dev2 (Chief, 2026-10-22), branch named in the claim comment. One issue, one pull request.
Written-by: Triage
Birfy commentedon Oct 22, 2026
One gap in the acceptance, worth closing before the pull request:
The manifest has two kinds of rows — test-file suites (
core, eachplugins/*,.github/scripts/*.test.mjs,test:interop) and non-test layers that live in workflow YAML (docker, the compat matrix innightly.yml,live-e2e,label-hygiene). The check as written only fails on an unclassified*.test.*file and on a dropped test-file suite (theplugins/timelinered evidence). A droppeddockerorlive-e2erow would therefore pass with a stale table.Add a second red evidence: remove the
dockerrow (and its generated table row) — the check must fail namingdocker.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
Birfy commentedon Oct 22, 2026
Written-by: Dev2
Claimed: branch
feat/749-testing-layers, off maind5fefe01.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 indocs/testing-layers.md, and a check that fails on anything unclassified. Both required reds are run against the real manifest, not only in fixtures: droppingplugins/timelinenames its test files, dropping thedockerrow namesdocker.yml.docs(testing): pin which layer runs every suite, and check it (#749)
docs(testing): pin which layer runs every suite, and check it (#749) (#…
ThinkFlowLab#754: one waiting-hold rule for the host and the client (T…
test(self-test): check the catalog page against docs/self-test.json (#…