Skip to content

Commit 95de853

Browse files
committed
test: refactor flywheel e2e canary
1 parent 9379d38 commit 95de853

1 file changed

Lines changed: 59 additions & 13 deletions

File tree

cli/cmd/ao/flywheel_e2e_test.go

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,26 @@ import (
1515
// TestFlywheelE2E_CreateHarvestPromoteRetrieveInject validates the full flywheel loop:
1616
// create learning → harvest extract → catalog + promote → retrieve → quality gate.
1717
func 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

Comments
 (0)