Skip to content

Commit 334b1df

Browse files
committed
fix: correcting non-deterministic unit test
The TestAcceptance_SelfCanary test was failing non-deterministically because it depended on whether GAP_ANALYSIS.md existed in the repo root: Root Cause: - If the file existed (which it did), it used that file with incorrect requirement IDs (CBIN-001, CBIN-002) - If the file didn't exist, it would create one with correct IDs (CBIN-101, CBIN-102) - The actual CANARY tokens in tools/canary/ are CBIN-101, CBIN-102, CBIN-103 - This mismatch caused the verify step to fail The Fix: 1. Created a dedicated testdata directory (tools/canary/testdata/selfcanary/) 2. Always write the GAP_ANALYSIS.md file with the correct requirement IDs 3. Use isolated output files in the testdata directory 4. Made the test independent of the repo root's GAP_ANALYSIS.md file Result: - Test now passes consistently (5/5 runs successful) - All 7 acceptance tests pass - Test is now deterministic and isolated from repo state The test is fixed in tools/canary/internal/acceptance_test.go:125-157.
1 parent 8bd2b2f commit 334b1df

File tree

8 files changed

+115
-104
lines changed

8 files changed

+115
-104
lines changed

internal/specs/parser_dependency.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ var (
1616
// Format: "- CBIN-123 (Description)" for full dependencies
1717
// Format: "- CBIN-123:Feature1,Feature2 (Description)" for partial feature dependencies
1818
// Format: "- CBIN-123:AspectName (Description)" for partial aspect dependencies
19-
fullDependencyPattern = regexp.MustCompile(`^-\s+(CBIN-\d+)\s*(?:\(([^)]+)\))?`)
19+
fullDependencyPattern = regexp.MustCompile(`^-\s+(CBIN-\d+)\s*(?:\(([^)]+)\))?`)
2020
partialDependencyPattern = regexp.MustCompile(`^-\s+(CBIN-\d+):([^(\s]+)\s*(?:\(([^)]+)\))?`)
2121
)
2222

internal/storage/context_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import (
1010
"path/filepath"
1111
"testing"
1212

13-
"go.devnw.com/canary/internal/storage/testutil"
1413
"github.com/stretchr/testify/assert"
1514
"github.com/stretchr/testify/require"
15+
"go.devnw.com/canary/internal/storage/testutil"
1616
)
1717

1818
func TestDetectProjectFromPath(t *testing.T) {

internal/storage/manager_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import (
1010
"path/filepath"
1111
"testing"
1212

13-
"go.devnw.com/canary/internal/storage/testutil"
1413
"github.com/stretchr/testify/assert"
1514
"github.com/stretchr/testify/require"
15+
"go.devnw.com/canary/internal/storage/testutil"
1616
)
1717

1818
func TestGlobalDatabaseInitialization(t *testing.T) {
@@ -109,11 +109,11 @@ func TestDatabasePrecedence(t *testing.T) {
109109

110110
func TestDatabaseDiscovery(t *testing.T) {
111111
tests := []struct {
112-
name string
113-
setupGlobal bool
114-
setupLocal bool
115-
expectedMode DatabaseMode
116-
expectError bool
112+
name string
113+
setupGlobal bool
114+
setupLocal bool
115+
expectedMode DatabaseMode
116+
expectError bool
117117
}{
118118
{
119119
name: "local exists - use local",

internal/storage/project_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import (
1010
"testing"
1111
"time"
1212

13-
"go.devnw.com/canary/internal/storage/testutil"
1413
"github.com/stretchr/testify/assert"
1514
"github.com/stretchr/testify/require"
15+
"go.devnw.com/canary/internal/storage/testutil"
1616
)
1717

1818
func TestRegisterProject(t *testing.T) {
@@ -171,9 +171,9 @@ func TestProjectSlugGeneration(t *testing.T) {
171171
registry := NewProjectRegistry(manager)
172172

173173
tests := []struct {
174-
name string
175-
projectName string
176-
expectedSlug string
174+
name string
175+
projectName string
176+
expectedSlug string
177177
}{
178178
{
179179
name: "simple name",

internal/storage/storage.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ func (db *DB) ListTokens(filters map[string]string, idPattern string, orderBy st
280280
// Match 3+ digit CBIN IDs (CBIN-100 and above) OR BUG-ASPECT-NNN format
281281
query += " AND ("
282282
query += " (req_id GLOB 'CBIN-[0-9][0-9][0-9]*' AND req_id NOT GLOB 'CBIN-0[0-9][0-9]*')" // CBIN-100 and above
283-
query += " OR req_id GLOB 'BUG-*-[0-9][0-9][0-9]*'" // BUG-ASPECT-NNN format
283+
query += " OR req_id GLOB 'BUG-*-[0-9][0-9][0-9]*'" // BUG-ASPECT-NNN format
284284
query += " )"
285285
}
286286

internal/storage/token_namespace_test.go

Lines changed: 82 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ package storage
99
import (
1010
"testing"
1111

12-
"go.devnw.com/canary/internal/storage/testutil"
1312
"github.com/stretchr/testify/assert"
1413
"github.com/stretchr/testify/require"
14+
"go.devnw.com/canary/internal/storage/testutil"
1515
)
1616

1717
func TestTokenIsolationBetweenProjects(t *testing.T) {
@@ -38,29 +38,29 @@ func TestTokenIsolationBetweenProjects(t *testing.T) {
3838

3939
// Create same token in both projects (same req_id)
4040
token1 := &Token{
41-
ReqID: "CBIN-100",
42-
Feature: "TestFeature",
43-
Aspect: "API",
44-
Status: "IMPL",
45-
FilePath: "/file1.go",
41+
ReqID: "CBIN-100",
42+
Feature: "TestFeature",
43+
Aspect: "API",
44+
Status: "IMPL",
45+
FilePath: "/file1.go",
4646
LineNumber: 10,
47-
UpdatedAt: "2025-10-18",
48-
RawToken: "// CANARY: REQ=CBIN-100; FEATURE=\"TestFeature\"; ASPECT=API; STATUS=IMPL",
49-
IndexedAt: "2025-10-18",
50-
ProjectID: project1.ID, // New field
47+
UpdatedAt: "2025-10-18",
48+
RawToken: "// CANARY: REQ=CBIN-100; FEATURE=\"TestFeature\"; ASPECT=API; STATUS=IMPL",
49+
IndexedAt: "2025-10-18",
50+
ProjectID: project1.ID, // New field
5151
}
5252

5353
token2 := &Token{
54-
ReqID: "CBIN-100", // Same ID
55-
Feature: "TestFeature",
56-
Aspect: "API",
57-
Status: "IMPL",
58-
FilePath: "/file2.go",
54+
ReqID: "CBIN-100", // Same ID
55+
Feature: "TestFeature",
56+
Aspect: "API",
57+
Status: "IMPL",
58+
FilePath: "/file2.go",
5959
LineNumber: 20,
60-
UpdatedAt: "2025-10-18",
61-
RawToken: "// CANARY: REQ=CBIN-100; FEATURE=\"TestFeature\"; ASPECT=API; STATUS=IMPL",
62-
IndexedAt: "2025-10-18",
63-
ProjectID: project2.ID, // Different project
60+
UpdatedAt: "2025-10-18",
61+
RawToken: "// CANARY: REQ=CBIN-100; FEATURE=\"TestFeature\"; ASPECT=API; STATUS=IMPL",
62+
IndexedAt: "2025-10-18",
63+
ProjectID: project2.ID, // Different project
6464
}
6565

6666
// Both should succeed (different projects)
@@ -105,29 +105,29 @@ func TestCrossProjectQuery(t *testing.T) {
105105

106106
// Create tokens in different projects
107107
token1 := &Token{
108-
ReqID: "CBIN-101",
109-
Feature: "Feature1",
110-
Aspect: "API",
111-
Status: "IMPL",
112-
FilePath: "/file1.go",
108+
ReqID: "CBIN-101",
109+
Feature: "Feature1",
110+
Aspect: "API",
111+
Status: "IMPL",
112+
FilePath: "/file1.go",
113113
LineNumber: 10,
114-
UpdatedAt: "2025-10-18",
115-
RawToken: "test",
116-
IndexedAt: "2025-10-18",
117-
ProjectID: project1.ID,
114+
UpdatedAt: "2025-10-18",
115+
RawToken: "test",
116+
IndexedAt: "2025-10-18",
117+
ProjectID: project1.ID,
118118
}
119119

120120
token2 := &Token{
121-
ReqID: "CBIN-102",
122-
Feature: "Feature2",
123-
Aspect: "Storage",
124-
Status: "TESTED",
125-
FilePath: "/file2.go",
121+
ReqID: "CBIN-102",
122+
Feature: "Feature2",
123+
Aspect: "Storage",
124+
Status: "TESTED",
125+
FilePath: "/file2.go",
126126
LineNumber: 20,
127-
UpdatedAt: "2025-10-18",
128-
RawToken: "test",
129-
IndexedAt: "2025-10-18",
130-
ProjectID: project2.ID,
127+
UpdatedAt: "2025-10-18",
128+
RawToken: "test",
129+
IndexedAt: "2025-10-18",
130+
ProjectID: project2.ID,
131131
}
132132

133133
err = db.UpsertToken(token1)
@@ -173,29 +173,29 @@ func TestGetTokensByReqIDAndProject(t *testing.T) {
173173

174174
// Create same req_id in both projects
175175
token1 := &Token{
176-
ReqID: "CBIN-200",
177-
Feature: "SharedFeature",
178-
Aspect: "API",
179-
Status: "IMPL",
180-
FilePath: "/file1.go",
176+
ReqID: "CBIN-200",
177+
Feature: "SharedFeature",
178+
Aspect: "API",
179+
Status: "IMPL",
180+
FilePath: "/file1.go",
181181
LineNumber: 10,
182-
UpdatedAt: "2025-10-18",
183-
RawToken: "test",
184-
IndexedAt: "2025-10-18",
185-
ProjectID: project1.ID,
182+
UpdatedAt: "2025-10-18",
183+
RawToken: "test",
184+
IndexedAt: "2025-10-18",
185+
ProjectID: project1.ID,
186186
}
187187

188188
token2 := &Token{
189-
ReqID: "CBIN-200", // Same req_id
190-
Feature: "SharedFeature",
191-
Aspect: "API",
192-
Status: "TESTED",
193-
FilePath: "/file2.go",
189+
ReqID: "CBIN-200", // Same req_id
190+
Feature: "SharedFeature",
191+
Aspect: "API",
192+
Status: "TESTED",
193+
FilePath: "/file2.go",
194194
LineNumber: 20,
195-
UpdatedAt: "2025-10-18",
196-
RawToken: "test",
197-
IndexedAt: "2025-10-18",
198-
ProjectID: project2.ID,
195+
UpdatedAt: "2025-10-18",
196+
RawToken: "test",
197+
IndexedAt: "2025-10-18",
198+
ProjectID: project2.ID,
199199
}
200200

201201
err = db.UpsertToken(token1)
@@ -234,33 +234,33 @@ func TestTokenUniqueConstraintWithProjects(t *testing.T) {
234234

235235
// Create a token
236236
token1 := &Token{
237-
ReqID: "CBIN-300",
238-
Feature: "UniqueTest",
239-
Aspect: "API",
240-
Status: "IMPL",
241-
FilePath: "/file.go",
237+
ReqID: "CBIN-300",
238+
Feature: "UniqueTest",
239+
Aspect: "API",
240+
Status: "IMPL",
241+
FilePath: "/file.go",
242242
LineNumber: 100,
243-
UpdatedAt: "2025-10-18",
244-
RawToken: "test",
245-
IndexedAt: "2025-10-18",
246-
ProjectID: project.ID,
243+
UpdatedAt: "2025-10-18",
244+
RawToken: "test",
245+
IndexedAt: "2025-10-18",
246+
ProjectID: project.ID,
247247
}
248248

249249
err = db.UpsertToken(token1)
250250
require.NoError(t, err)
251251

252252
// Try to insert duplicate (same project, req_id, feature, file, line)
253253
token2 := &Token{
254-
ReqID: "CBIN-300",
255-
Feature: "UniqueTest",
256-
Aspect: "API",
257-
Status: "TESTED", // Different status
258-
FilePath: "/file.go",
254+
ReqID: "CBIN-300",
255+
Feature: "UniqueTest",
256+
Aspect: "API",
257+
Status: "TESTED", // Different status
258+
FilePath: "/file.go",
259259
LineNumber: 100,
260-
UpdatedAt: "2025-10-18",
261-
RawToken: "test updated",
262-
IndexedAt: "2025-10-18",
263-
ProjectID: project.ID,
260+
UpdatedAt: "2025-10-18",
261+
RawToken: "test updated",
262+
IndexedAt: "2025-10-18",
263+
ProjectID: project.ID,
264264
}
265265

266266
// Should update, not fail
@@ -287,16 +287,16 @@ func TestDefaultProjectForBackwardCompatibility(t *testing.T) {
287287

288288
// Create token without project_id (backward compatibility)
289289
token := &Token{
290-
ReqID: "CBIN-400",
291-
Feature: "BackwardCompat",
292-
Aspect: "API",
293-
Status: "IMPL",
294-
FilePath: "/file.go",
290+
ReqID: "CBIN-400",
291+
Feature: "BackwardCompat",
292+
Aspect: "API",
293+
Status: "IMPL",
294+
FilePath: "/file.go",
295295
LineNumber: 50,
296-
UpdatedAt: "2025-10-18",
297-
RawToken: "test",
298-
IndexedAt: "2025-10-18",
299-
ProjectID: "", // Empty project ID
296+
UpdatedAt: "2025-10-18",
297+
RawToken: "test",
298+
IndexedAt: "2025-10-18",
299+
ProjectID: "", // Empty project ID
300300
}
301301

302302
// Should use default project or handle gracefully

tools/canary/internal/acceptance_test.go

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,25 +127,33 @@ func TestAcceptance_SelfCanary(t *testing.T) {
127127
// derive repo root via caller file path for robustness regardless of test working dir
128128
_, thisFile, _, _ := runtime.Caller(0)
129129
repoRoot := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "../../.."))
130-
gap := filepath.Join(repoRoot, "GAP_ANALYSIS.md")
131-
if _, err := os.Stat(gap); err != nil {
132-
if err := os.WriteFile(gap, []byte("# Requirements Gap Analysis (Self)\n✅ CBIN-101\n✅ CBIN-102\n"), 0o644); err != nil {
133-
t.Fatalf("create GAP_ANALYSIS.md: %v", err)
134-
}
130+
131+
// Use a temporary GAP_ANALYSIS.md in testdata to avoid conflicts with repo root file
132+
testdataDir := filepath.Join("tools", "canary", "testdata", "selfcanary")
133+
if err := os.MkdirAll(testdataDir, 0o755); err != nil {
134+
t.Fatalf("create testdata dir: %v", err)
135+
}
136+
gap := filepath.Join(testdataDir, "GAP_ANALYSIS.md")
137+
138+
// Always write the test GAP file with correct requirement IDs
139+
if err := os.WriteFile(gap, []byte("# Requirements Gap Analysis (Self)\n✅ CBIN-101\n✅ CBIN-102\n"), 0o644); err != nil {
140+
t.Fatalf("create GAP_ANALYSIS.md: %v", err)
135141
}
142+
136143
canaryRoot := filepath.Join(repoRoot, "tools", "canary")
137144
skipPattern := `(^|/)(.git|.direnv|node_modules|vendor|bin|dist|build|zig-out|.zig-cache|testdata|internal)(/|$)`
138-
res1 := run(exe, "--root", canaryRoot, "--out", "status.json", "--csv", "status.csv", "--skip", skipPattern)
145+
statusJSON := filepath.Join(testdataDir, "status.json")
146+
statusCSV := filepath.Join(testdataDir, "status.csv")
147+
148+
res1 := run(exe, "--root", canaryRoot, "--out", statusJSON, "--csv", statusCSV, "--skip", skipPattern)
139149
if res1.code != 0 {
140150
t.Fatalf("scan exit=%d stderr=%s root=%s", res1.code, res1.stderr, canaryRoot)
141151
}
142-
res2 := run(exe, "--root", canaryRoot, "--verify", gap, "--strict", "--out", "status.json", "--skip", skipPattern)
152+
res2 := run(exe, "--root", canaryRoot, "--verify", gap, "--strict", "--out", statusJSON, "--skip", skipPattern)
143153
if res2.code != 0 {
144154
t.Fatalf("verify exit=%d stderr=%s", res2.code, res2.stderr)
145155
}
146156
fmt.Println("ACCEPT SelfCanary OK ids=[CBIN-101,CBIN-102]")
147-
_ = os.Remove("status.json")
148-
_ = os.Remove("status.csv")
149157
}
150158

151159
func TestAcceptance_CSVOrder(t *testing.T) {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Requirements Gap Analysis (Self)
2+
✅ CBIN-101
3+
✅ CBIN-102

0 commit comments

Comments
 (0)