F-fixtures

Workstream F — Fixtures & declarative expectation tests

Goal

Lock the draft-checks contract (detect / validate / next-step / extract) into declarative YAML expectation files, the same pattern used by validate_format2.yml, lint_format2.yml, normalized_format2.yml. Each expectation case is fixture → operation → assertions[] — same runner, same vocabulary. The expectations become the language-neutral contract shared upstream with gxformat2 PR #219.

The CLI command tests (draft-validate.test.ts, draft-next-step.test.ts, _draft-extract.test.ts) stay as they are — thin smoke tests for I/O wiring, exit codes, and CLI-only concerns (text/JSON/HTML output mode routing, stdout-sink collisions). Behavior assertions move to the declarative tier where they’re richer and cheaper.

Why not goldens

The metaplan F (INDEX.md L185–203) originally hand-waved at “golden output” tests. The codebase strongly prefers declarative assertion files:

Inputs

Locked decisions (this subplan)

DecisionOutcome
Testing patternDeclarative YAML expectation files, not goldens. One operation per file, mirroring the validate_format2.yml / lint_format2.yml style.
Fixture namingsynthetic- prefix kept. Established gxformat2 convention; metaplan’s draft-valid-simple.yml-style names were hand-wave naming. New fixtures: synthetic-draft-<descriptor>.gxwf.yml.
Fixture locationpackages/schema/test/fixtures/workflows/format2/draft/ — new draft/ subdir under format2/ for hygiene. Existing 3 fixtures move out of packages/cli/test/fixtures/draft/.
Negative-case shapeAssert on result paths directly (lint pattern), no expect_error flag. expect_error stays reserved for operations that throw. validateDraft is documented as “Collects all diagnostics — does not throw” (draft-checks.ts L251), so validate_draft.yml follows lint_format2.yml: [error_count], [errors, 0]: value_contains "...".
Extract-result shapeAssert on full surviving workflow shape via per-key paths, not just step-label sets. Catches structural regressions on survivors (lost state: blocks, mangled in: entries) that set assertions miss. Consistent with how normalized_format2.yml already asserts.
Snake-case wrapperYes — wrap validateDraft and detectDraft in the OPERATIONS registry so paths in expectation files are snake_case across all four draft ops. NextStepResult and ExtractResult are already snake_case; only DraftValidationResult and DraftSurvey are camelCase. Tiny toSnakeCaseKeys helper lives in declarative-test-utils.ts. (Alternative refactor — change the result types to snake_case directly — flagged as open question, not done in F.)
next_draft_step_work_lines op variantPunt. Revisit if [work, 0]: value_contains paths feel awkward when writing F5. Not in v1.
UpstreamingBundle into existing gxformat2 PR #219. Fixtures + expectation YAMLs + Python helpers for the ops gxformat2 needs (detect_draft, validate_draft). next_draft_step / extract_draft_subset stay TS-only (locked decision: Python parity deferred).
_ts_extras.yml suffixNot used. Expectations are canonical from day one. If gxformat2 PR #219 lags, check-sync-workflow-expectations is allowed to fail locally until the upstream PR catches up.
Cross-check (draft ⊇ concrete)Dropped. F7 removed. validate_format2 (lax) injects class: GalaxyWorkflow over the draft class and drops _plan_* via onExcessProperty: ignore — the cross-check fixture would prove nothing about the lax decoder. validate_format2_strict rejects class: GalaxyWorkflowDraft outright. No single fixture satisfies both validators meaningfully.
_plan_* on a step with no TODO sentinelsvalidateDraft emits semanticError. Locked decision in INDEX.md L284 was inaccurate (“schema-enforced”). DraftWorkflowStep accepts _plan_* unconditionally and validateDraft had no per-step check — only the extract layer dropped via step_has_plan_field. F4 adds the missing semanticError so the validate / extract contracts agree.
CLI command testsStay as they are. Thin smoke tests for exit codes, text/JSON/HTML routing, and CLI-only failure modes (--report-json to stdout collision, etc.). Behavior assertions move to the declarative tier.

Steps

F1. Fixture audit (done)

#Metaplan nameStatusTarget fixture (under format2/draft/)
1draft-valid-simpledonesynthetic-draft-tool-step.gxwf.yml (existing)
2draft-valid-chainmissingsynthetic-draft-chain.gxwf.yml
3draft-valid-subworkflow (inner draft)needs-extract-from-inlinesynthetic-draft-subworkflow-inner-draft.gxwf.yml
4draft-invalid-todo-labelneeds-extract-from-inlinesynthetic-draft-bad-todo-label.gxwf.yml
5draft-invalid-dangling-edgeneeds-extract-from-inlinesynthetic-draft-bad-dangling-edge.gxwf.yml
6draft-invalid-plan-on-concretemissingsynthetic-draft-bad-plan-on-concrete.gxwf.yml
7draft-valid-plan-on-subworkflowdone (extract-drops; encode in expectation)synthetic-draft-plan-subworkflow.gxwf.yml (existing)
8draft-extract-cascadeneeds-extract-from-inlinesynthetic-draft-extract-cascade.gxwf.yml
9draft-fully-concreteneeds-extract-from-inlinesynthetic-draft-fully-concrete.gxwf.yml
10draft-next-step-topological-tiebreakneeds-extract-from-inlinesynthetic-draft-next-step-tiebreak.gxwf.yml

Plus existing synthetic-draft-plan-top-level.gxwf.yml (warning case, not in the 10) — keep and move.

Inline heredocs that intentionally stay inline (CLI-only concerns): survey-line pluralization (validate.test L52–67), parse-failure exit codes, --format native rejection, --json + --report-html stdout collision, hidden-command help filtering, markdown output rendering.

F2. Move + reorganize fixtures (both sides — TS + upstream gxformat2)

The 3 existing draft fixtures already exist upstream (gxformat2 commit 58c8e95, in PR #219) flat in gxformat2/examples/format2/. To keep sync valid with the draft/ subdir convention we want, both repos move in step.

Upstream gxformat2 (carry-along on the existing #219 branch):

TS side:

F3. Fill missing fixtures

Per F1 audit. All in format2/draft/, all with synthetic- prefix:

Positive cases:

Negative cases (lifted from inline heredocs):

Each fixture is YAML-only. No code change.

F4. Register draft operations in the runner + close the plan-on-concrete enforcement gap

B-tier addition (must land before F5 expectation entries can pass): in packages/schema/src/workflow/draft-checks.ts, extend validateDraft to emit a semanticError when a step carries any _plan_* field but no TODO sentinels. Path: the step’s StepPath. Message: step "<label>" has _plan_<field> but no TODO sentinels — planning fields belong on drafty steps only. This closes the contradiction flagged in the locked-decisions table (INDEX.md L284 was inaccurate). Add a vitest case in draft-checks.test.ts mirroring the new behavior. Update report-models-draft.test.ts if the surface changes.

In packages/schema/test/declarative-normalized.test.ts, extend OPERATIONS:

detect_draft: (raw) => toSnakeCaseKeys(detectDraft(raw)),
validate_draft: (raw) => toSnakeCaseKeys(validateDraft(raw)),
next_draft_step: (raw) => nextDraftStep(raw),
extract_draft_subset: (raw) => extractConcreteSubset(raw),

Add toSnakeCaseKeys to declarative-test-utils.ts — deep, simple recursive helper, only used by these two ops. Punt on next_draft_step_work_lines variant.

expect_error is not used for any of these — all four ops return structured results.

F5. Write expectation YAMLs

Four new files under packages/schema/test/fixtures/expectations/:

Naming convention test_draft_<descriptor>_<expectation>: so the test id reads like a contract sentence.

F6. Bundle into gxformat2 PR #219 (minimal scope)

In worktree ~/projects/worktrees/gxformat2/branch/abstraction_applications, on top of PR #219:

F7. Subplan doc + (TBD) removal (this section)

(Original F7 was a cross-check expectation; dropped because no single fixture satisfies both validate_draft and validate_format2 meaningfully — see locked decision row.)

Final state — landed inventory

Fixtures under packages/schema/test/fixtures/workflows/format2/draft/ (mirrored at gxformat2/examples/format2/draft/):

Existing (3, moved): synthetic-draft-tool-step.gxwf.yml, synthetic-draft-plan-top-level.gxwf.yml, synthetic-draft-plan-subworkflow.gxwf.yml.

New (8): synthetic-draft-chain.gxwf.yml, synthetic-draft-subworkflow-inner-draft.gxwf.yml, synthetic-draft-extract-cascade.gxwf.yml, synthetic-draft-fully-concrete.gxwf.yml, synthetic-draft-next-step-tiebreak.gxwf.yml, synthetic-draft-bad-todo-label.gxwf.yml, synthetic-draft-bad-dangling-edge.gxwf.yml, synthetic-draft-bad-plan-on-concrete.gxwf.yml.

Expectation files under packages/schema/test/fixtures/expectations/ (TS authoritative; detect_draft.yml + validate_draft.yml mirrored upstream):

FileCasesWhat it locks
detect_draft.yml7survey shape (is_draft, todos, plan_fields); subworkflow path prefixing; per-step-only contract
validate_draft.yml11ok flag + per-category counts; top-level _plan_* warning; v1 carveout for non-tool steps; 3 negatives (todo-label, dangling-edge, plan-on-concrete)
next_draft_step.yml8locked work-order; topological pick + alphabetical tiebreak; subworkflow descent; {draft: false} on non-draft and fully-resolved cases
extract_draft_subset.yml8step survival shape; cascade ordering; subworkflow inner shrink + outer-output cascade; step_has_plan_field drop

Metaplan named case → expectation test id mapping:

Metaplan nameFixtureAsserted by
draft-valid-simplesynthetic-draft-tool-steptest_detect_draft_tool_step_*, test_validate_draft_tool_step_clean, test_next_draft_step_tool_step_*, test_extract_draft_tool_step_*
draft-valid-chainsynthetic-draft-chaintest_detect_draft_chain_*, test_validate_draft_chain_clean, test_next_draft_step_chain_*, test_extract_draft_chain_*
draft-valid-subworkflowsynthetic-draft-subworkflow-inner-drafttest_detect_draft_subworkflow_*, test_validate_draft_subworkflow_inner_draft_clean, test_next_draft_step_subworkflow_*, test_extract_draft_subworkflow_*
draft-invalid-todo-labelsynthetic-draft-bad-todo-labeltest_validate_draft_bad_todo_label_topology_error
draft-invalid-dangling-edgesynthetic-draft-bad-dangling-edgetest_validate_draft_bad_dangling_edge_two_topology_errors
draft-invalid-plan-on-concretesynthetic-draft-bad-plan-on-concretetest_validate_draft_bad_plan_on_concrete_semantic_error, test_extract_draft_bad_plan_on_concrete_drops_step
draft-valid-plan-on-subworkflowsynthetic-draft-plan-subworkflowtest_detect_draft_plan_subworkflow_*, test_validate_draft_plan_on_subworkflow_step_clean_v1_carveout, test_next_draft_step_plan_subworkflow_*, test_extract_draft_plan_subworkflow_drops_outer_plan_step
draft-extract-cascadesynthetic-draft-extract-cascadetest_validate_draft_extract_cascade_clean, test_extract_draft_cascade_*
draft-fully-concretesynthetic-draft-fully-concretetest_detect_draft_fully_concrete_*, test_validate_draft_fully_concrete_clean, test_next_draft_step_fully_concrete_*, test_extract_draft_fully_concrete_*
draft-next-step-topological-tiebreaksynthetic-draft-next-step-tiebreaktest_validate_draft_tiebreak_clean, test_next_draft_step_tiebreak_alphabetical, test_extract_draft_tiebreak_*

Plus the synthetic-draft-plan-top-level.gxwf.yml warning fixture is locked by test_detect_draft_plan_top_level_*, test_validate_draft_plan_top_level_passes_with_warning, test_next_draft_step_plan_top_level_*.

gxformat2 PR #219 carry-along status:

Open items left for follow-up (not in F’s scope):

Sequencing

F1 (audit, done — above)
  └─→ F2 (move fixtures, update loadWorkflow + sync-manifest)
       └─→ F3 (fill missing fixtures — 5 positive + 3 negative)
            └─→ F4 (register ops + toSnakeCaseKeys + add plan-on-concrete semanticError to validateDraft)
                 └─→ F5 (write 4 expectation YAMLs, parallelizable per file)
                      └─→ F7 (subplan doc finalization)

  Parallel from F3 onward:
  └─→ F6 (gxformat2 PR #219 carry-along)

F2 is the only structural commit; F4 has a small B-tier code addition; F3, F5, F6 are mostly YAML.

Pipeline / commands

Acceptance criteria (status)

Open questions (resolved)

All F open questions resolved during execution. Outstanding cross-workstream follow-ups are listed under F7’s “Open items left for follow-up” section.