@@ -201,6 +201,123 @@ func writeForeignGlobalReceipt(t *testing.T, home, foreignRepoPath, generatedAt
201201 }
202202}
203203
204+ // TestSnapshotGit_OwnOutputDirIsNotTreeDirt is the regression for enola recording its
205+ // OWN artifacts as working-tree dirt. gitInfo decides dirtiness from any
206+ // `git status --porcelain` output, and --porcelain lists untracked paths — so the
207+ // .enola/ directory the snapshot itself creates (extractor-cache save, engine.go:328,
208+ // which runs BEFORE the receipt reads git state) made a pristine committed repo record
209+ // dirty: true.
210+ //
211+ // The repo here deliberately does NOT gitignore the output dir, which is the case that
212+ // regresses. The second snapshot matters as much as the first: by then .enola/ already
213+ // exists before the run begins, so call ordering is no longer the operative cause and
214+ // only excluding the directory fixes it.
215+ func TestSnapshotGit_OwnOutputDirIsNotTreeDirt (t * testing.T ) {
216+ if _ , err := exec .LookPath ("git" ); err != nil {
217+ t .Skip ("git not available" )
218+ }
219+ repo := t .TempDir ()
220+ writeFile (t , filepath .Join (repo , "go.mod" ), "module dirtmod\n \n go 1.21\n " )
221+ writeFile (t , filepath .Join (repo , "pkg" , "a" , "a.go" ), "package a\n \n func A() {}\n " )
222+ initGitRepo (t , repo )
223+
224+ cfg := config .Default ()
225+ eng , err := engine .New (cfg )
226+ if err != nil {
227+ t .Fatal (err )
228+ }
229+ eng .RegisterExtractor (goextractor .New ())
230+
231+ for _ , run := range []string {"first" , "second" } {
232+ snap , err := eng .GenerateSnapshot (context .Background (), repo , false )
233+ if err != nil {
234+ t .Fatalf ("%s snapshot: %v" , run , err )
235+ }
236+ if err := eng .WriteArtifacts (repo ); err != nil {
237+ t .Fatalf ("%s WriteArtifacts: %v" , run , err )
238+ }
239+ if snap .Meta .Git == nil {
240+ t .Fatalf ("%s run: no git info recorded" , run )
241+ }
242+ if snap .Meta .Git .Dirty {
243+ t .Errorf ("%s run: committed-clean repo recorded dirty:true — enola's own %s/ counted as tree dirt" ,
244+ run , cfg .Output .Dir )
245+ }
246+ }
247+ }
248+
249+ // TestSnapshotGit_RealChangeStillDirty guards the fix above against over-suppressing:
250+ // excluding the output dir must not blind the flag to actual uncommitted source changes.
251+ func TestSnapshotGit_RealChangeStillDirty (t * testing.T ) {
252+ if _ , err := exec .LookPath ("git" ); err != nil {
253+ t .Skip ("git not available" )
254+ }
255+ repo := t .TempDir ()
256+ writeFile (t , filepath .Join (repo , "go.mod" ), "module dirtmod2\n \n go 1.21\n " )
257+ writeFile (t , filepath .Join (repo , "pkg" , "a" , "a.go" ), "package a\n \n func A() {}\n " )
258+ initGitRepo (t , repo )
259+
260+ // A real, uncommitted modification to a TRACKED file.
261+ writeFile (t , filepath .Join (repo , "pkg" , "a" , "a.go" ), "package a\n \n func A() {}\n func B() {}\n " )
262+
263+ cfg := config .Default ()
264+ eng , err := engine .New (cfg )
265+ if err != nil {
266+ t .Fatal (err )
267+ }
268+ eng .RegisterExtractor (goextractor .New ())
269+
270+ snap , err := eng .GenerateSnapshot (context .Background (), repo , false )
271+ if err != nil {
272+ t .Fatal (err )
273+ }
274+ if snap .Meta .Git == nil {
275+ t .Fatal ("no git info recorded" )
276+ }
277+ if ! snap .Meta .Git .Dirty {
278+ t .Error ("a modified tracked file must still record dirty:true" )
279+ }
280+ }
281+
282+ // TestStaleness_DetectsEditAfterCleanSnapshotWithoutGitignore is the payoff. A repo that
283+ // does not gitignore the output dir previously recorded dirty:true at snapshot time,
284+ // which killed the `!r.Git.Dirty && cur.Dirty` arm for its whole life — so a later edit
285+ // produced no staleness signal at all. With the baseline recorded correctly, the arm is
286+ // live again.
287+ func TestStaleness_DetectsEditAfterCleanSnapshotWithoutGitignore (t * testing.T ) {
288+ if _ , err := exec .LookPath ("git" ); err != nil {
289+ t .Skip ("git not available" )
290+ }
291+ t .Setenv ("HOME" , t .TempDir ())
292+
293+ repo := t .TempDir ()
294+ writeFile (t , filepath .Join (repo , "go.mod" ), "module dirtmod3\n \n go 1.21\n " )
295+ writeFile (t , filepath .Join (repo , "pkg" , "a" , "a.go" ), "package a\n \n func A() {}\n " )
296+ initGitRepo (t , repo )
297+
298+ cfg := config .Default ()
299+ eng , err := engine .New (cfg )
300+ if err != nil {
301+ t .Fatal (err )
302+ }
303+ eng .RegisterExtractor (goextractor .New ())
304+ if _ , err := eng .GenerateSnapshot (context .Background (), repo , false ); err != nil {
305+ t .Fatal (err )
306+ }
307+
308+ // Edit a tracked file after the snapshot, leaving it uncommitted.
309+ writeFile (t , filepath .Join (repo , "pkg" , "a" , "a.go" ), "package a\n \n func A() {}\n func Added() {}\n " )
310+
311+ now := time .Now ()
312+ st := eng .Staleness (24 * time .Hour , now )
313+ if st .TooOld {
314+ t .Fatalf ("snapshot is seconds old, TooOld should be false (Age=%s)" , st .Age )
315+ }
316+ if len (st .Changed ) != 1 || st .Changed [0 ].Reason != "uncommitted changes" {
317+ t .Errorf ("got Changed=%+v, want one \" uncommitted changes\" for the edited repo" , st .Changed )
318+ }
319+ }
320+
204321// initGitRepo creates a git repo with one commit and returns its HEAD.
205322func initGitRepo (t * testing.T , repo string ) string {
206323 t .Helper ()
0 commit comments