@@ -165,6 +165,159 @@ func TestGlobalJSONForwardsToRunOnceAndPublish(t *testing.T) {
165165 }
166166}
167167
168+ func TestStatusJSONContractContainsStableTopLevelFields (t * testing.T ) {
169+ repoRoot , _ := createTestRepoWithRemote (t )
170+ initRepoForWorker (t , repoRoot )
171+
172+ var stdout bytes.Buffer
173+ var stderr bytes.Buffer
174+ if err := runCLI ([]string {"status" , "--repo" , repoRoot , "--json" }, & stdout , & stderr ); err != nil {
175+ t .Fatalf ("runCLI returned error: %v" , err )
176+ }
177+
178+ var payload map [string ]any
179+ if err := json .Unmarshal (stdout .Bytes (), & payload ); err != nil {
180+ t .Fatalf ("Unmarshal: %v" , err )
181+ }
182+ required := []string {
183+ "repository_root" ,
184+ "state_path" ,
185+ "current_worktree" ,
186+ "current_branch" ,
187+ "protected_branch" ,
188+ "protected_branch_sha" ,
189+ "protected_upstream" ,
190+ "counts" ,
191+ "recent_events" ,
192+ }
193+ for _ , key := range required {
194+ if _ , ok := payload [key ]; ! ok {
195+ t .Fatalf ("expected status json key %q in %+v" , key , payload )
196+ }
197+ }
198+ }
199+
200+ func TestEventsLifecycleJSONContractContainsStableFields (t * testing.T ) {
201+ repoRoot , _ := createTestRepoWithRemote (t )
202+ initRepoForWorker (t , repoRoot )
203+
204+ featurePath := filepath .Join (t .TempDir (), "feature-events-contract" )
205+ runTestCommand (t , repoRoot , "git" , "worktree" , "add" , "-b" , "feature/events-contract" , featurePath )
206+ writeFileAndCommit (t , featurePath , "events.txt" , "events\n " , "events feature" )
207+ submitBranch (t , featurePath )
208+ runOnce (t , repoRoot )
209+
210+ var stdout bytes.Buffer
211+ var stderr bytes.Buffer
212+ if err := runCLI ([]string {"events" , "--repo" , repoRoot , "--json" , "--lifecycle" , "--limit" , "20" }, & stdout , & stderr ); err != nil {
213+ t .Fatalf ("runCLI returned error: %v" , err )
214+ }
215+
216+ lines := strings .Split (strings .TrimSpace (stdout .String ()), "\n " )
217+ if len (lines ) == 0 {
218+ t .Fatalf ("expected lifecycle json lines" )
219+ }
220+
221+ foundIntegrated := false
222+ for _ , line := range lines {
223+ if strings .TrimSpace (line ) == "" {
224+ continue
225+ }
226+ var payload map [string ]any
227+ if err := json .Unmarshal ([]byte (line ), & payload ); err != nil {
228+ t .Fatalf ("Unmarshal line %q: %v" , line , err )
229+ }
230+ for _ , key := range []string {"event" , "repository_root" , "timestamp" } {
231+ if _ , ok := payload [key ]; ! ok {
232+ t .Fatalf ("expected lifecycle key %q in %+v" , key , payload )
233+ }
234+ }
235+ if payload ["event" ] == "integrated" {
236+ foundIntegrated = true
237+ for _ , key := range []string {"branch" , "sha" , "submission_id" } {
238+ if _ , ok := payload [key ]; ! ok {
239+ t .Fatalf ("expected integrated lifecycle key %q in %+v" , key , payload )
240+ }
241+ }
242+ }
243+ }
244+ if ! foundIntegrated {
245+ t .Fatalf ("expected integrated lifecycle event in %q" , stdout .String ())
246+ }
247+ }
248+
249+ func TestDaemonJSONLogContractContainsStableFields (t * testing.T ) {
250+ repoRoot , _ := createTestRepoWithRemote (t )
251+ initRepoForWorker (t , repoRoot )
252+
253+ var stdout bytes.Buffer
254+ opts := daemonOptions {
255+ repoPath : repoRoot ,
256+ interval : time .Millisecond ,
257+ jsonLogs : true ,
258+ idleExit : true ,
259+ }
260+ if err := runDaemonLoop (context .Background (), opts , & stdout ); err != nil {
261+ t .Fatalf ("runDaemonLoop returned error: %v" , err )
262+ }
263+
264+ lines := strings .Split (strings .TrimSpace (stdout .String ()), "\n " )
265+ if len (lines ) < 2 {
266+ t .Fatalf ("expected daemon json log lines, got %q" , stdout .String ())
267+ }
268+ for _ , line := range lines {
269+ var payload map [string ]any
270+ if err := json .Unmarshal ([]byte (line ), & payload ); err != nil {
271+ t .Fatalf ("Unmarshal line %q: %v" , line , err )
272+ }
273+ for _ , key := range []string {"level" , "event" , "repo" , "timestamp" } {
274+ if _ , ok := payload [key ]; ! ok {
275+ t .Fatalf ("expected daemon log key %q in %+v" , key , payload )
276+ }
277+ }
278+ }
279+ }
280+
281+ func TestDaemonProcessesWorkFromBareCloneLinkedWorktree (t * testing.T ) {
282+ bareDir , worktreePath := createBareCloneWorktree (t )
283+
284+ var initOut bytes.Buffer
285+ var initErr bytes.Buffer
286+ if err := runRepoInit ([]string {"--repo" , worktreePath }, & initOut , & initErr ); err != nil {
287+ t .Fatalf ("runRepoInit returned error: %v" , err )
288+ }
289+ featurePath := filepath .Join (t .TempDir (), "bare-feature" )
290+ runTestCommand (t , worktreePath , "git" , "worktree" , "add" , "-b" , "feature/bare-daemon" , featurePath )
291+ writeFileAndCommit (t , featurePath , "bare.txt" , "bare\n " , "bare feature" )
292+ submitBranch (t , featurePath )
293+
294+ var stdout bytes.Buffer
295+ opts := daemonOptions {
296+ repoPath : worktreePath ,
297+ interval : time .Millisecond ,
298+ maxCycles : 1 ,
299+ }
300+ if err := runDaemonLoop (context .Background (), opts , & stdout ); err != nil {
301+ t .Fatalf ("runDaemonLoop returned error: %v" , err )
302+ }
303+
304+ if _ , err := os .Stat (filepath .Join (worktreePath , "bare.txt" )); err != nil {
305+ t .Fatalf ("expected bare.txt after daemon integration: %v" , err )
306+ }
307+
308+ layout , err := git .DiscoverRepositoryLayout (worktreePath )
309+ if err != nil {
310+ t .Fatalf ("DiscoverRepositoryLayout: %v" , err )
311+ }
312+ wantBareDir , err := filepath .EvalSymlinks (bareDir )
313+ if err != nil {
314+ t .Fatalf ("EvalSymlinks(bareDir): %v" , err )
315+ }
316+ if layout .RepositoryRoot != wantBareDir {
317+ t .Fatalf ("expected bare repository root %q, got %q" , wantBareDir , layout .RepositoryRoot )
318+ }
319+ }
320+
168321func TestDaemonProcessesIntegrationAndPublishWork (t * testing.T ) {
169322 repoRoot , remoteDir := createTestRepoWithRemote (t )
170323 initRepoForWorker (t , repoRoot )
0 commit comments