|
| 1 | +// ABOUTME: --checklist/--ac-scan --stage defaulting to the entity's current |
| 2 | +// ABOUTME: frontmatter status when --stage is omitted (AC-1 through AC-5). |
| 3 | +package status |
| 4 | + |
| 5 | +import ( |
| 6 | + "path/filepath" |
| 7 | + "strings" |
| 8 | + "testing" |
| 9 | +) |
| 10 | + |
| 11 | +// TestDefaultStageMatchesExplicitCurrent (AC-1, AC-2) asserts a bare --checklist / |
| 12 | +// --ac-scan call (no --stage) against an entity whose current status is |
| 13 | +// "validation" succeeds and produces byte-identical output to the same call with |
| 14 | +// --stage validation given explicitly, across both text and --json modes. The |
| 15 | +// interleaved fixture's frontmatter status is "validation" and it carries a |
| 16 | +// matching "## Stage Report: validation" section. |
| 17 | +func TestDefaultStageMatchesExplicitCurrent(t *testing.T) { |
| 18 | + root, err := filepath.Abs(filepath.Join("testdata", "seq-workflow")) |
| 19 | + if err != nil { |
| 20 | + t.Fatal(err) |
| 21 | + } |
| 22 | + env := pinnedEnv(t) |
| 23 | + fixture := interleavedFixturePath(t) |
| 24 | + |
| 25 | + cases := []struct { |
| 26 | + name string |
| 27 | + flag string |
| 28 | + }{ |
| 29 | + {"checklist", "--checklist"}, |
| 30 | + {"ac-scan", "--ac-scan"}, |
| 31 | + } |
| 32 | + for _, tc := range cases { |
| 33 | + tc := tc |
| 34 | + t.Run(tc.name+" text", func(t *testing.T) { |
| 35 | + omitted, _, code := runNative(t, root, env, "--workflow-dir", root, "--read", fixture, tc.flag) |
| 36 | + if code != 0 { |
| 37 | + t.Fatalf("bare %s exit=%d, want 0", tc.flag, code) |
| 38 | + } |
| 39 | + if strings.Contains(omitted, "requires --stage") { |
| 40 | + t.Fatalf("bare %s emitted the retired 'requires --stage' wording: %s", tc.flag, omitted) |
| 41 | + } |
| 42 | + explicit, _, code := runNative(t, root, env, "--workflow-dir", root, "--read", fixture, "--stage", "validation", tc.flag) |
| 43 | + if code != 0 { |
| 44 | + t.Fatalf("explicit %s exit=%d, want 0", tc.flag, code) |
| 45 | + } |
| 46 | + if omitted != explicit { |
| 47 | + t.Fatalf("bare %s output != explicit --stage validation output\n--- omitted ---\n%s\n--- explicit ---\n%s", tc.flag, omitted, explicit) |
| 48 | + } |
| 49 | + }) |
| 50 | + t.Run(tc.name+" json", func(t *testing.T) { |
| 51 | + omitted, _, code := runNative(t, root, env, "--workflow-dir", root, "--read", fixture, tc.flag, "--json") |
| 52 | + if code != 0 { |
| 53 | + t.Fatalf("bare %s --json exit=%d, want 0", tc.flag, code) |
| 54 | + } |
| 55 | + explicit, _, code := runNative(t, root, env, "--workflow-dir", root, "--read", fixture, "--stage", "validation", tc.flag, "--json") |
| 56 | + if code != 0 { |
| 57 | + t.Fatalf("explicit %s --json exit=%d, want 0", tc.flag, code) |
| 58 | + } |
| 59 | + if omitted != explicit { |
| 60 | + t.Fatalf("bare %s --json output != explicit --stage validation output\n--- omitted ---\n%s\n--- explicit ---\n%s", tc.flag, omitted, explicit) |
| 61 | + } |
| 62 | + if !strings.Contains(omitted, `"stage":"validation"`) { |
| 63 | + t.Fatalf("bare %s --json envelope stage != validation: %s", tc.flag, omitted) |
| 64 | + } |
| 65 | + }) |
| 66 | + } |
| 67 | +} |
| 68 | + |
| 69 | +// TestExplicitStageOverridesDefault (AC-3) asserts --stage <name> with a name |
| 70 | +// other than the current status still selects that stage's report, unaffected |
| 71 | +// by the current-status default. The interleaved fixture's current status is |
| 72 | +// "validation"; --stage implementation must still select the LATEST |
| 73 | +// implementation cycle, exactly as it does without any default in play. |
| 74 | +func TestExplicitStageOverridesDefault(t *testing.T) { |
| 75 | + root, err := filepath.Abs(filepath.Join("testdata", "seq-workflow")) |
| 76 | + if err != nil { |
| 77 | + t.Fatal(err) |
| 78 | + } |
| 79 | + env := pinnedEnv(t) |
| 80 | + fixture := interleavedFixturePath(t) |
| 81 | + |
| 82 | + out, stderr, code := runNative(t, root, env, "--workflow-dir", root, "--read", fixture, "--stage", "implementation", "--checklist", "--json") |
| 83 | + if code != 0 { |
| 84 | + t.Fatalf("exit=%d stderr=%q", code, stderr) |
| 85 | + } |
| 86 | + if !strings.Contains(out, "rework after the gate") { |
| 87 | + t.Fatalf("explicit --stage implementation did not select the latest implementation cycle: %s", out) |
| 88 | + } |
| 89 | + if strings.Contains(out, "write the first cut") { |
| 90 | + t.Fatalf("explicit --stage implementation leaked the earlier cycle-1 item: %s", out) |
| 91 | + } |
| 92 | + if !strings.Contains(out, `"stage":"implementation"`) { |
| 93 | + t.Fatalf("json envelope stage != implementation despite current status being validation: %s", out) |
| 94 | + } |
| 95 | +} |
| 96 | + |
| 97 | +// TestDefaultStageNoMatchingReport (AC-4) asserts that when --stage is omitted |
| 98 | +// and the entity's current status names a stage with no matching Stage Report, |
| 99 | +// the command exits 1 with a diagnostic naming the stage as the current/defaulted |
| 100 | +// one — never a silent empty emit. The section-reader fixture's status is |
| 101 | +// "implementation" but it only carries a "## Stage Report: ideation" section. |
| 102 | +func TestDefaultStageNoMatchingReport(t *testing.T) { |
| 103 | + root, err := filepath.Abs(filepath.Join("testdata", "seq-workflow")) |
| 104 | + if err != nil { |
| 105 | + t.Fatal(err) |
| 106 | + } |
| 107 | + env := pinnedEnv(t) |
| 108 | + fixture := fixturePath(t) |
| 109 | + |
| 110 | + for _, flag := range []string{"--checklist", "--ac-scan"} { |
| 111 | + flag := flag |
| 112 | + t.Run(flag, func(t *testing.T) { |
| 113 | + out, stderr, code := runNative(t, root, env, "--workflow-dir", root, "--read", fixture, flag) |
| 114 | + if code == 0 { |
| 115 | + t.Fatalf("exit=0 (want non-zero) — silent/partial emit\nstdout=%q", out) |
| 116 | + } |
| 117 | + if strings.TrimSpace(out) != "" { |
| 118 | + t.Fatalf("stdout = %q, want empty on a loud failure", out) |
| 119 | + } |
| 120 | + if !strings.Contains(stderr, `"implementation"`) { |
| 121 | + t.Fatalf("stderr = %q, want it to name the defaulted stage \"implementation\"", stderr) |
| 122 | + } |
| 123 | + if !strings.Contains(stderr, "--stage omitted") { |
| 124 | + t.Fatalf("stderr = %q, want it to note --stage was omitted (defaulting transparency)", stderr) |
| 125 | + } |
| 126 | + }) |
| 127 | + } |
| 128 | +} |
| 129 | + |
| 130 | +// TestDefaultStageNoStatusField (AC-5) asserts that when --stage is omitted and |
| 131 | +// the resolved --read target has no status frontmatter field, the command exits |
| 132 | +// 1 with a diagnostic naming that the stage could not be defaulted — never the |
| 133 | +// retired bare "requires --stage" wording, and never a silent emit. The real |
| 134 | +// docs/dev/README.md has no status: frontmatter field. |
| 135 | +func TestDefaultStageNoStatusField(t *testing.T) { |
| 136 | + root, err := filepath.Abs(filepath.Join("testdata", "seq-workflow")) |
| 137 | + if err != nil { |
| 138 | + t.Fatal(err) |
| 139 | + } |
| 140 | + env := pinnedEnv(t) |
| 141 | + readme := devReadmePath(t) |
| 142 | + |
| 143 | + for _, flag := range []string{"--checklist", "--ac-scan"} { |
| 144 | + flag := flag |
| 145 | + t.Run(flag, func(t *testing.T) { |
| 146 | + out, stderr, code := runNative(t, root, env, "--workflow-dir", root, "--read", readme, flag) |
| 147 | + if code == 0 { |
| 148 | + t.Fatalf("exit=0 (want non-zero) — silent/partial emit\nstdout=%q", out) |
| 149 | + } |
| 150 | + if strings.TrimSpace(out) != "" { |
| 151 | + t.Fatalf("stdout = %q, want empty on a loud failure", out) |
| 152 | + } |
| 153 | + if strings.Contains(stderr, "requires --stage") { |
| 154 | + t.Fatalf("stderr = %q, reused the retired 'requires --stage' wording (misleading now that the flag defaults)", stderr) |
| 155 | + } |
| 156 | + if !strings.Contains(stderr, "--stage omitted") || !strings.Contains(strings.ToLower(stderr), "status") { |
| 157 | + t.Fatalf("stderr = %q, want a diagnostic naming that --stage was omitted and there is no status to default from", stderr) |
| 158 | + } |
| 159 | + }) |
| 160 | + } |
| 161 | +} |
0 commit comments