@@ -15,8 +15,26 @@ import (
1515// TestFlywheelE2E_CreateHarvestPromoteRetrieveInject validates the full flywheel loop:
1616// create learning → harvest extract → catalog + promote → retrieve → quality gate.
1717func TestFlywheelE2E_CreateHarvestPromoteRetrieveInject (t * testing.T ) {
18+ fixture := setupFlywheelE2ECanary (t )
19+ artifacts := extractFlywheelE2ECanaryArtifacts (t , fixture )
20+ assertFlywheelE2ECanaryArtifact (t , artifacts [0 ])
21+ promotedPath := promoteFlywheelE2ECanaryArtifact (t , fixture , artifacts )
22+
23+ assertFlywheelE2EPromotedMetadata (t , promotedPath )
24+ assertFlywheelE2EOriginalLearningRetrievable (t , fixture .learningFile )
25+ assertFlywheelE2EQualityGate (t , fixture .learningFile )
26+ }
27+
28+ type flywheelE2ECanaryFixture struct {
29+ rigBase string
30+ learningFile string
31+ promoteDir string
32+ }
33+
34+ func setupFlywheelE2ECanary (t * testing.T ) flywheelE2ECanaryFixture {
35+ t .Helper ()
36+
1837 t .Setenv ("HOME" , t .TempDir ()) // isolate HOME per Wave 1 check-home-isolation gate
19- // Stage 1: Create a learning with proper metadata in a temp rig structure
2038 tmpDir := t .TempDir ()
2139 rigBase := filepath .Join (tmpDir , "testproject" , "crew" , "testcrew" )
2240 learningsDir := filepath .Join (rigBase , ".agents" , "learnings" )
@@ -43,9 +61,18 @@ The content is long enough to pass the quality gate minimum of 50 characters.
4361 t .Fatalf ("writing learning file: %v" , err )
4462 }
4563
46- // Stage 2: Harvest — extract artifacts from the test rig
64+ return flywheelE2ECanaryFixture {
65+ rigBase : rigBase ,
66+ learningFile : learningFile ,
67+ promoteDir : filepath .Join (tmpDir , "global-learnings" ),
68+ }
69+ }
70+
71+ func extractFlywheelE2ECanaryArtifacts (t * testing.T , fixture flywheelE2ECanaryFixture ) []harvest.Artifact {
72+ t .Helper ()
73+
4774 rig := harvest.RigInfo {
48- Path : filepath .Join (rigBase , ".agents" ),
75+ Path : filepath .Join (fixture . rigBase , ".agents" ),
4976 Project : "testproject" ,
5077 Crew : "testcrew" ,
5178 Rig : "testproject-testcrew" ,
@@ -63,7 +90,12 @@ The content is long enough to pass the quality gate minimum of 50 characters.
6390 t .Fatalf ("expected 1 artifact, got %d" , len (artifacts ))
6491 }
6592
66- art := artifacts [0 ]
93+ return artifacts
94+ }
95+
96+ func assertFlywheelE2ECanaryArtifact (t * testing.T , art harvest.Artifact ) {
97+ t .Helper ()
98+
6799 if art .Type != "learning" {
68100 t .Errorf ("expected type=learning, got %q" , art .Type )
69101 }
@@ -74,19 +106,21 @@ The content is long enough to pass the quality gate minimum of 50 characters.
74106 if art .SourceRig != "testproject-testcrew" {
75107 t .Errorf ("expected source_rig=testproject-testcrew, got %q" , art .SourceRig )
76108 }
109+ }
110+
111+ func promoteFlywheelE2ECanaryArtifact (t * testing.T , fixture flywheelE2ECanaryFixture , artifacts []harvest.Artifact ) string {
112+ t .Helper ()
77113
78- // Stage 3: Catalog + Promote
79114 catalog := harvest .BuildCatalog (artifacts , 0.5 )
80115 if len (catalog .Promoted ) != 1 {
81116 t .Fatalf ("expected 1 promoted artifact, got %d" , len (catalog .Promoted ))
82117 }
83118
84- promoteDir := filepath .Join (tmpDir , "global-learnings" )
85- if err := os .MkdirAll (promoteDir , 0o755 ); err != nil {
119+ if err := os .MkdirAll (fixture .promoteDir , 0o755 ); err != nil {
86120 t .Fatalf ("creating promotion dir: %v" , err )
87121 }
88122
89- promoted , err := harvest .Promote (catalog , promoteDir , false )
123+ promoted , err := harvest .Promote (catalog , fixture . promoteDir , false )
90124 if err != nil {
91125 t .Fatalf ("Promote: %v" , err )
92126 }
@@ -95,12 +129,18 @@ The content is long enough to pass the quality gate minimum of 50 characters.
95129 }
96130
97131 // Find the promoted file
98- promotedFiles , err := filepath .Glob (filepath .Join (promoteDir , "learning" , "*.md" ))
132+ promotedFiles , err := filepath .Glob (filepath .Join (fixture . promoteDir , "learning" , "*.md" ))
99133 if err != nil || len (promotedFiles ) == 0 {
100- t .Fatalf ("no promoted files found in %s/learning/" , promoteDir )
134+ t .Fatalf ("no promoted files found in %s/learning/" , fixture . promoteDir )
101135 }
102136
103- promotedContent , err := os .ReadFile (promotedFiles [0 ])
137+ return promotedFiles [0 ]
138+ }
139+
140+ func assertFlywheelE2EPromotedMetadata (t * testing.T , promotedPath string ) {
141+ t .Helper ()
142+
143+ promotedContent , err := os .ReadFile (promotedPath )
104144 if err != nil {
105145 t .Fatalf ("reading promoted file: %v" , err )
106146 }
@@ -116,8 +156,11 @@ The content is long enough to pass the quality gate minimum of 50 characters.
116156 if ! strings .Contains (pc , "utility: 0.8" ) {
117157 t .Error ("promoted file lost utility metadata" )
118158 }
159+ }
160+
161+ func assertFlywheelE2EOriginalLearningRetrievable (t * testing.T , learningFile string ) {
162+ t .Helper ()
119163
120- // Stage 4: Retrieve — verify the ORIGINAL learning is findable via processLearningFile.
121164 // Note: promoted files are intentionally skipped by inject (isPromoted → Superseded=true)
122165 // to avoid double-counting. The inject pipeline reads local .agents/learnings/, not the
123166 // global promoted store. So we validate retrieval against the original source file.
@@ -131,8 +174,11 @@ The content is long enough to pass the quality gate minimum of 50 characters.
131174 if l .Title == "" {
132175 t .Error ("parsed learning has empty title" )
133176 }
177+ }
178+
179+ func assertFlywheelE2EQualityGate (t * testing.T , learningFile string ) {
180+ t .Helper ()
134181
135- // Stage 5: Quality gate — verify the learning passes injection quality standards.
136182 // Note: learnings without source_bead get a 0.3x utility penalty in processLearningFile,
137183 // so we check the gate BEFORE that penalty (which is applied during scoring, not parsing).
138184 // The quality gate itself checks the raw parsed values.
0 commit comments