11package orchestrator
22
33import (
4+ "context"
5+ "strings"
6+ "sync"
47 "testing"
8+ "time"
59
10+ "github.com/UnicoLab/slmcode/pkg/config"
11+ "github.com/UnicoLab/slmcode/pkg/plan"
12+ "github.com/UnicoLab/slmcode/pkg/quality"
613 "github.com/UnicoLab/slmcode/pkg/squads"
714)
815
9- func gatedOrchestrator (t * testing.T , gates ... TeamGate ) * Orchestrator {
16+ // selectiveGate answers each command on its own merits, which is the only way
17+ // to build the shape under test: both TEAM commands green while the run as a
18+ // whole fails.
19+ type selectiveGate struct { green map [string ]bool }
20+
21+ func (g * selectiveGate ) run (_ context.Context , _ , cmd string , _ time.Duration ) quality.SmokeResult {
22+ ok := g .green [cmd ]
23+ sr := quality.SmokeResult {Ran : true , Command : cmd , OK : ok }
24+ if ok {
25+ sr .Summary = quality .SmokePassedMarker + ": " + cmd
26+ } else {
27+ sr .Summary = quality .SmokeFailedMarker
28+ }
29+ return sr
30+ }
31+
32+ func halvesPlan () * squads.Plan {
33+ return & squads.Plan {Squads : []squads.Squad {
34+ {ID : "backend-go" , Owns : []string {"cmd/**" }, Acceptance : "go build ./..." },
35+ {ID : "frontend-react" , Owns : []string {"web/**" }, Acceptance : "npm run build" },
36+ }}
37+ }
38+
39+ func gatedPlanOrchestrator (t * testing.T , gates ... TeamGate ) * Orchestrator {
1040 t .Helper ()
11- o := & Orchestrator {squadPlan : & squads.Plan {Squads : []squads.Squad {
12- {ID : "backend-go" }, {ID : "frontend-react" },
13- }}}
41+ o := & Orchestrator {squadPlan : halvesPlan ()}
1442 for _ , g := range gates {
1543 o .teamGates .set (g )
1644 }
1745 return o
1846}
1947
20- // The measured case: two tasks done, both halves green by their own commands,
21- // and the run reported flat FAILURE because a seam tester escalated after a
22- // local reviewer scored its prose 0. The software was built and each half was
23- // proved; the user was told it failed.
48+ // Two halves, each proved by running its own acceptance command, is measured
49+ // evidence: an exit status on the real tree rather than a model's opinion of a
50+ // write-up.
2451func TestBothHalvesGreenIsMeasuredEvidence (t * testing.T ) {
25- o := gatedOrchestrator (t ,
52+ o := gatedPlanOrchestrator (t ,
2653 TeamGate {Team : "backend-go" , Ran : true , OK : true },
2754 TeamGate {Team : "frontend-react" , Ran : true , OK : true })
2855
@@ -35,9 +62,8 @@ func TestBothHalvesGreenIsMeasuredEvidence(t *testing.T) {
3562}
3663
3764// An UNVERIFIED gate is the case this must never round up. A command that could
38- // not run said nothing about that half, and the whole point of separating
39- // UNVERIFIED from RED is that it is not evidence in either direction —
40- // counting it here would undo that at the one place it decides the verdict.
65+ // not run said NOTHING about that half — that is the entire reason UNVERIFIED
66+ // is separate from RED — so it is not evidence in either direction.
4167func TestAnUnverifiedHalfIsNotEvidence (t * testing.T ) {
4268 for name , gates := range map [string ][]TeamGate {
4369 "one half never ran its check" : {
@@ -52,30 +78,107 @@ func TestAnUnverifiedHalfIsNotEvidence(t *testing.T) {
5278 {Team : "backend-go" , Ran : false },
5379 {Team : "frontend-react" , Ran : false },
5480 },
81+ "only one half reported at all" : {
82+ {Team : "backend-go" , Ran : true , OK : true },
83+ },
5584 } {
56- if gatedOrchestrator (t , gates ... ).allHalvesProved () {
57- t .Errorf ("%s: was counted as proof" , name )
85+ if gatedPlanOrchestrator (t , gates ... ).allHalvesProved () {
86+ t .Errorf ("%s: was counted as proof of both halves " , name )
5887 }
5988 }
6089}
6190
62- // "Both halves proved" needs both halves to exist and be proved. One team doing
63- // all the work is not a teams run, and a single green gate must not license
64- // walking past an escalation.
65- func TestOneProvedHalfIsNotBothHalves (t * testing.T ) {
66- o := gatedOrchestrator (t , TeamGate {Team : "backend-go" , Ran : true , OK : true })
67- if o .allHalvesProved () {
68- t .Error ("a single green half licensed an escalation walk-past" )
69- }
70- }
71-
7291func TestNoSquadPlanProvesNothing (t * testing.T ) {
73- o := & Orchestrator {}
74- if o .allHalvesProved () {
92+ if (& Orchestrator {}).allHalvesProved () {
7593 t .Error ("a run without teams claimed both halves were proved" )
7694 }
7795 var nilO * Orchestrator
7896 if nilO .allHalvesProved () {
7997 t .Error ("a nil orchestrator claimed proof" )
8098 }
8199}
100+
101+ // escalatedTeamsBoard is the measured shape: both teams' implementation tasks
102+ // done, and a seam TESTER escalated to the human backlog.
103+ func escalatedTeamsBoard () * plan.Board {
104+ b := doneBoard ()
105+ b .Tasks [0 ].Squad = "backend-go"
106+ b .Tasks = append (b .Tasks ,
107+ plan.Task {ID : "T2" , Title : "render the todos" , Column : plan .ColDone ,
108+ Role : plan .RoleWorker , Squad : "frontend-react" },
109+ plan.Task {ID : "T3" , Title : "verify the halves meet" , Column : plan .ColToScope ,
110+ Role : plan .RoleTester , Error : "review rejected after max retries" })
111+ return b
112+ }
113+
114+ // A failing teams run must say which halves were nonetheless PROVED.
115+ //
116+ // The verdict is untouched — the run failed and the exit code stands. What a
117+ // person needs is the SHAPE of the failure: "everything is broken" and "both
118+ // halves compile and pass their own tests, and what failed is the join between
119+ // them" call for completely different next moves, and the summary said only the
120+ // first. Measured live: `team backend-go is green`, `team frontend-react is
121+ // green`, reported as a flat failure indistinguishable from a run where nothing
122+ // worked at all.
123+ func TestAFailingRunSaysWhichHalvesWereProved (t * testing.T ) {
124+ // Both halves' own commands pass; everything else — including the QA
125+ // objective — does not. That is the measured shape.
126+ gate := & selectiveGate {green : map [string ]bool {
127+ "go build ./..." : true , "npm run build" : true ,
128+ }}
129+ o := objectiveOrch (t , gate , & countingExec {},
130+ func (c * config.Config ) { c .QAGate = true })
131+ sink := newMsgSink (o )
132+ o .squadPlan = halvesPlan ()
133+
134+ res := finalize (t , o , escalatedTeamsBoard ())
135+
136+ if res .Success {
137+ t .Fatal ("the verdict changed — this must only ADD information to a failure" )
138+ }
139+ joined := sink .text ()
140+ if ! strings .Contains (joined , "backend-go" ) || ! strings .Contains (joined , "frontend-react" ) {
141+ t .Errorf ("a failing run did not name the halves that proved themselves:\n %s" , joined )
142+ }
143+ if ! strings .Contains (joined , "BETWEEN them" ) {
144+ t .Errorf ("the shape of the failure was not stated:\n %s" , joined )
145+ }
146+ }
147+
148+ // Nothing claimed when nothing proved itself.
149+ func TestNoHalvesProvedSaysNothing (t * testing.T ) {
150+ // Nothing green anywhere, so no half can claim to have proved itself.
151+ o := objectiveOrch (t , & selectiveGate {green : map [string ]bool {}}, & countingExec {}, nil )
152+ sink := newMsgSink (o )
153+ o .squadPlan = halvesPlan ()
154+
155+ finalize (t , o , escalatedTeamsBoard ())
156+
157+ if joined := sink .text (); strings .Contains (joined , "proved themselves" ) {
158+ t .Errorf ("claimed a half proved itself when none did:\n %s" , joined )
159+ }
160+ }
161+
162+ // msgSink collects emitted messages. The orchestrator fires events from several
163+ // goroutines, so this needs a lock — without one the race detector fails the
164+ // test rather than the code.
165+ type msgSink struct {
166+ mu sync.Mutex
167+ msgs []string
168+ }
169+
170+ func newMsgSink (o * Orchestrator ) * msgSink {
171+ s := & msgSink {}
172+ o .onEvent = func (e Event ) {
173+ s .mu .Lock ()
174+ defer s .mu .Unlock ()
175+ s .msgs = append (s .msgs , e .Message )
176+ }
177+ return s
178+ }
179+
180+ func (s * msgSink ) text () string {
181+ s .mu .Lock ()
182+ defer s .mu .Unlock ()
183+ return strings .Join (s .msgs , "\n " )
184+ }
0 commit comments