@@ -4,13 +4,27 @@ import (
44 "encoding/json"
55 "net/http"
66 "net/http/httptest"
7+ "os"
8+ "path/filepath"
79 "sync/atomic"
810 "testing"
911 "time"
1012
1113 "github.com/RandomCodeSpace/ctm/internal/serve/git"
1214)
1315
16+ // gitWorkdir returns a tempdir with a .git directory so the handler's
17+ // isGitWorkdir check passes — otherwise the handler short-circuits
18+ // before calling the lister.
19+ func gitWorkdir (t * testing.T ) string {
20+ t .Helper ()
21+ dir := t .TempDir ()
22+ if err := os .MkdirAll (filepath .Join (dir , ".git" ), 0o700 ); err != nil {
23+ t .Fatal (err )
24+ }
25+ return dir
26+ }
27+
1428func TestCheckpoints_404OnUnknownSession (t * testing.T ) {
1529 h := Checkpoints (func (name string ) (string , bool ) { return "" , false }, nil )
1630 rec := httptest .NewRecorder ()
@@ -42,7 +56,8 @@ func TestCheckpoints_CacheHitWithin5s(t *testing.T) {
4256 return want , nil
4357 }
4458
45- h := Checkpoints (func (name string ) (string , bool ) { return "/fake/wd" , true }, nil )
59+ wd := gitWorkdir (t )
60+ h := Checkpoints (func (name string ) (string , bool ) { return wd , true }, nil )
4661
4762 for i := 0 ; i < 5 ; i ++ {
4863 rec := httptest .NewRecorder ()
@@ -52,11 +67,14 @@ func TestCheckpoints_CacheHitWithin5s(t *testing.T) {
5267 if rec .Code != http .StatusOK {
5368 t .Fatalf ("call %d: status = %d" , i , rec .Code )
5469 }
55- var got []git. Checkpoint
70+ var got checkpointsResp
5671 if err := json .NewDecoder (rec .Body ).Decode (& got ); err != nil {
5772 t .Fatalf ("decode: %v" , err )
5873 }
59- if len (got ) != 1 || got [0 ].SHA != "abc" {
74+ if ! got .GitWorkdir {
75+ t .Fatalf ("call %d: git_workdir = false, want true" , i )
76+ }
77+ if len (got .Checkpoints ) != 1 || got .Checkpoints [0 ].SHA != "abc" {
6078 t .Errorf ("call %d: payload = %+v" , i , got )
6179 }
6280 }
@@ -74,7 +92,8 @@ func TestCheckpoints_CacheKeyedOnLimit(t *testing.T) {
7492 atomic .AddInt32 (& calls , 1 )
7593 return nil , nil
7694 }
77- h := Checkpoints (func (name string ) (string , bool ) { return "/wd" , true }, nil )
95+ wd := gitWorkdir (t )
96+ h := Checkpoints (func (name string ) (string , bool ) { return wd , true }, nil )
7897
7998 for _ , q := range []string {"" , "?limit=10" , "?limit=10" , "?limit=20" } {
8099 rec := httptest .NewRecorder ()
@@ -94,14 +113,56 @@ func TestCheckpoints_NilListEncodedAsEmptyArray(t *testing.T) {
94113 checkpointsLister = func (workdir string , limit int ) ([]git.Checkpoint , error ) {
95114 return nil , nil
96115 }
97- h := Checkpoints (func (name string ) (string , bool ) { return "/wd" , true }, nil )
116+ wd := gitWorkdir (t )
117+ h := Checkpoints (func (name string ) (string , bool ) { return wd , true }, nil )
98118 rec := httptest .NewRecorder ()
99119 req := httptest .NewRequest (http .MethodGet , "/api/sessions/s/checkpoints" , nil )
100120 req .SetPathValue ("name" , "s" )
101121 h (rec , req )
102- body := rec .Body .String ()
103- if body != "[]\n " {
104- t .Errorf ("body = %q, want \" []\\ n\" " , body )
122+ var got checkpointsResp
123+ if err := json .NewDecoder (rec .Body ).Decode (& got ); err != nil {
124+ t .Fatalf ("decode: %v" , err )
125+ }
126+ if ! got .GitWorkdir {
127+ t .Fatalf ("git_workdir = false, want true" )
128+ }
129+ if got .Checkpoints == nil || len (got .Checkpoints ) != 0 {
130+ t .Errorf ("checkpoints = %+v, want empty non-nil slice" , got .Checkpoints )
131+ }
132+ }
133+
134+ func TestCheckpoints_NotGitWorkdir (t * testing.T ) {
135+ // No .git dir — handler must short-circuit and return
136+ // git_workdir:false without calling the lister.
137+ prev := checkpointsLister
138+ t .Cleanup (func () { checkpointsLister = prev })
139+ var calls int32
140+ checkpointsLister = func (workdir string , limit int ) ([]git.Checkpoint , error ) {
141+ atomic .AddInt32 (& calls , 1 )
142+ return nil , nil
143+ }
144+
145+ wd := t .TempDir () // no .git subdir
146+ h := Checkpoints (func (name string ) (string , bool ) { return wd , true }, nil )
147+ rec := httptest .NewRecorder ()
148+ req := httptest .NewRequest (http .MethodGet , "/api/sessions/s/checkpoints" , nil )
149+ req .SetPathValue ("name" , "s" )
150+ h (rec , req )
151+ if rec .Code != http .StatusOK {
152+ t .Fatalf ("status = %d, want 200" , rec .Code )
153+ }
154+ var got checkpointsResp
155+ if err := json .NewDecoder (rec .Body ).Decode (& got ); err != nil {
156+ t .Fatalf ("decode: %v" , err )
157+ }
158+ if got .GitWorkdir {
159+ t .Errorf ("git_workdir = true, want false for non-git workdir" )
160+ }
161+ if len (got .Checkpoints ) != 0 {
162+ t .Errorf ("checkpoints = %+v, want empty" , got .Checkpoints )
163+ }
164+ if c := atomic .LoadInt32 (& calls ); c != 0 {
165+ t .Errorf ("lister called %d times for non-git workdir, want 0" , c )
105166 }
106167}
107168
@@ -118,8 +179,6 @@ func TestCheckpointsCache_IsCheckpointFullSHAOnly(t *testing.T) {
118179 if ! cache .IsCheckpoint ("/wd" , "name" , fullSHA ) {
119180 t .Error ("full SHA must be allowed" )
120181 }
121- // Abbreviated SHAs (UI might naively round-trip a 7-char display
122- // SHA) must be rejected to keep allowlist surface area minimal.
123182 if cache .IsCheckpoint ("/wd" , "name" , fullSHA [:7 ]) {
124183 t .Error ("7-char abbreviated SHA must be rejected" )
125184 }
@@ -140,13 +199,9 @@ func TestCheckpoints_CacheExpiresAfterTTL(t *testing.T) {
140199 atomic .AddInt32 (& calls , 1 )
141200 return nil , nil
142201 }
143- h := Checkpoints (func (name string ) (string , bool ) { return "/wd" , true }, nil )
202+ wd := gitWorkdir (t )
203+ h := Checkpoints (func (name string ) (string , bool ) { return wd , true }, nil )
144204
145- // First call populates cache; manually expire the entry by reaching
146- // into the closure-captured cache via a real handler call before
147- // stale time, then asserting we miss after TTL by directly poking
148- // time isn't possible without a clock seam — instead we verify the
149- // hit-then-miss path by relying on TTL only when CTM_LONG_TESTS=1.
150205 rec1 := httptest .NewRecorder ()
151206 r1 := httptest .NewRequest (http .MethodGet , "/api/sessions/s/checkpoints" , nil )
152207 r1 .SetPathValue ("name" , "s" )
@@ -158,5 +213,5 @@ func TestCheckpoints_CacheExpiresAfterTTL(t *testing.T) {
158213 if c := atomic .LoadInt32 (& calls ); c != 1 {
159214 t .Fatalf ("cache miss within TTL: calls = %d" , c )
160215 }
161- _ = time .Now () // keep import even if long-test branch removed
216+ _ = time .Now ()
162217}
0 commit comments