cli subpackages: delete dead code and its dedicated tests · Entire
Log in
cli subpackages: delete dead code and its dedicated tests
84fee84·
Soph·2w ago·43 files·+207 added/-2,267 removed
All unreachable from the CLI entry points (x/tools deadcode + reference grep). Highlights:
- auth/repo_token.go removed whole — repo-scoped token minting lives in internal/entireclient/repocreds; the live resolveContextForCluster seam moved to control_plane.go, and stale comments in repo_mirror* now point at repocreds. - checkpoint/blob_resolver.go removed whole (test-only; prod fetches blobs via the BlobFetcher path). - checkpoint/remote: the unused CatFiles batch reader and its plumbing; DeriveCheckpointURL wrapper — its derivation coverage moved in-package to a new TestDeriveCheckpointURLFromInfo against the live private function before deleting the strategy-package test. - checkpoint: addDirectoryToEntriesWithAbsPath (superseded by addDirectoryToChanges — its symlink-security tests were repointed at the live function, which had almost no direct coverage), FetchingTree.Unwrap/Files, LookupSessionLog, JoinPrompts. - agent: registry.Detect; factoryaidroid and geminicli path-based transcript variants (prod uses the *FromBytes forms). - agentimport.Get, api.ResolveURL, gitremote.ExtractOwnerFromRemoteURL, investigate.IsInvestigateEnvEntry + StateStore.List, logging.LogDuration, the paths-package copies of Claude path sanitizing (live copies live in agent/claudecode), and two review test-only wrappers.
Tests that pinned real behavior were repointed at the live functions (registry completeness, URL joining, prompt round-trip, symlink security, review token hydration) rather than deleted.
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Sessions
7307caae423fView transcript
Changes
43
cmd/entire/cli
agent
factoryaidroid
Mtranscript.go-87
Mtranscript_test.go-353
geminicli
Mtranscript.go-42
Mtranscript_test.go-186
Mregistry.go-11
Mregistry_test.go-65
agentimport
Magentimport.go-10
Magentimport_test.go+9/-9
api
Mbase_url.go-5
Mbase_url_test.go+5/-5
auth
Mcontrol_plane.go+7/-2
Drepo_token.go-126
Drepo_token_test.go-239
checkpoint
Dblob_resolver.go-126
Dblob_resolver_test.go-201
Mcheckpoint_test.go+32/-33
Mephemeral.go+7/-62
Mfetching_tree.go-12
Mpersistent.go-19
Mprompts.go-5
Mprompts_test.go+3/-2
remote
Mgit.go-121
Mgit_test.go-43
Mutil.go-8
Mutil_test.go+96
Mtree_surgery_equiv_test.go+40/-63
gitremote
Mgitremote.go-10
Mgitremote_test.go-21
integration_test
Mremote_operations_test.go+3/-2
investigate
Menv.go-6
Menv_test.go-29
Mstate.go-42
Mstate_test.go-57
logging
Mlogger.go-24
Mlogger_test.go-65
paths
Mpaths.go-29
Mpaths_test.go-57
Mrepo_mirror.go+1/-1
Mrepo_mirror_probe.go+1/-1
review
Mcmd.go-4
Mmanifest.go-11
Mmanifest_test.go+3/-3
strategy
Mcheckpoint_remote_test.go-70
428 unmodified lines
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
428 unmodified lines
return files, nil
}
// CalculateTotalTokenUsageFromTranscript calculates token usage for a turn, including subagents.
// It parses the main transcript from startLine, extracts spawned agent IDs,
// and calculates their token usage from transcripts in subagentsDir.
func CalculateTotalTokenUsageFromTranscript(transcriptPath string, startLine int, subagentsDir string) (*agent.TokenUsage, error) {
if transcriptPath == "" {
return &agent.TokenUsage{}, nil
}
// Parse transcript once using Droid-specific parser
parsed, _, err := ParseDroidTranscript(transcriptPath, startLine)
if err != nil {
return nil, fmt.Errorf("failed to parse transcript: %w", err)
}
// Calculate token usage from parsed transcript
mainUsage := CalculateTokenUsage(parsed)
// Extract spawned agent IDs from the same parsed transcript
agentIDs := ExtractSpawnedAgentIDs(parsed)
// Calculate subagent token usage
if len(agentIDs) > 0 {
subagentUsage := &agent.TokenUsage{}
for agentID := range agentIDs {
agentPath := filepath.Join(subagentsDir, fmt.Sprintf("agent-%s.jsonl", agentID))
agentUsage, err := CalculateTokenUsageFromFile(agentPath, 0)
if err != nil {
// Agent transcript may not exist yet or may have been cleaned up
continue
}
subagentUsage.InputTokens += agentUsage.InputTokens
subagentUsage.CacheCreationTokens += agentUsage.CacheCreationTokens
subagentUsage.CacheReadTokens += agentUsage.CacheReadTokens
subagentUsage.OutputTokens += agentUsage.OutputTokens
subagentUsage.APICallCount += agentUsage.APICallCount
}
if subagentUsage.APICallCount > 0 {
mainUsage.SubagentTokens = subagentUsage
}
}
return mainUsage, nil
}
// ExtractAllModifiedFilesFromTranscript extracts files modified by both the main agent and
// any subagents spawned via the Task tool. It parses the main transcript from
// startLine, collects modified files from the main agent, then reads each
// subagent's transcript from subagentsDir to collect their modified files too.
// The result is a deduplicated list of all modified file paths.
func ExtractAllModifiedFilesFromTranscript(transcriptPath string, startLine int, subagentsDir string) ([]string, error) {
if transcriptPath == "" {
return nil, nil
}
// Parse main transcript once using Droid-specific parser
parsed, _, err := ParseDroidTranscript(transcriptPath, startLine)
if err != nil {
return nil, fmt.Errorf("failed to parse transcript: %w", err)
}
// Collect modified files from main agent (already deduplicated)
files := ExtractModifiedFiles(parsed)
fileSet := make(map[string]bool, len(files))
for _, f := range files {
fileSet[f] = true
}
// Find spawned subagents and collect their modified files
agentIDs := ExtractSpawnedAgentIDs(parsed)
for agentID := range agentIDs {
agentPath := filepath.Join(subagentsDir, fmt.Sprintf("agent-%s.jsonl", agentID))
agentLines, _, agentErr := ParseDroidTranscript(agentPath, 0)
if agentErr != nil {
// Subagent transcript may not exist yet or may have been cleaned up
continue
}
for _, f := range ExtractModifiedFiles(agentLines) {
if !fileSet[f] {
fileSet[f] = true
files = append(files, f)
}
}
}
return files, nil
}
Mcmd/entire/cli/agent/factoryaidroid/transcript.go-87
522 unmodified lines
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
526
527
528
45 unmodified lines
574
575
576
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
577
578
579
28 unmodified lines
608
609
610
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
611
612
613
175 unmodified lines
789
790
791
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
792
793
794
522 unmodified lines
}
}
func TestCalculateTotalTokenUsageFromTranscript_PerCheckpoint(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
transcriptPath := tmpDir + "/transcript.jsonl"
// Build transcript with 3 turns:
// Turn 1: user + assistant (100 input, 50 output)
// Turn 2: user + assistant (200 input, 100 output)
// Turn 3: user + assistant (300 input, 150 output)
//
// Lines:
// 0: user message 1
// 1: assistant response 1 (100/50 tokens)
// 2: user message 2
// 3: assistant response 2 (200/100 tokens)
// 4: user message 3
// 5: assistant response 3 (300/150 tokens)
// Droid format: outer type is always "message", role is inside the inner message
transcriptContent := []byte(
`{"type":"message","id":"u1","message":{"role":"user","content":"first prompt"}}` + "\n" +
`{"type":"message","id":"a1","message":{"role":"assistant","id":"m1","usage":{"input_tokens":100,"output_tokens":50}}}` + "\n" +
`{"type":"message","id":"u2","message":{"role":"user","content":"second prompt"}}` + "\n" +
`{"type":"message","id":"a2","message":{"role":"assistant","id":"m2","usage":{"input_tokens":200,"output_tokens":100}}}` + "\n" +
`{"type":"message","id":"u3","message":{"role":"user","content":"third prompt"}}` + "\n" +
`{"type":"message","id":"a3","message":{"role":"assistant","id":"m3","usage":{"input_tokens":300,"output_tokens":150}}}` + "\n",
)
if err := os.WriteFile(transcriptPath, transcriptContent, 0o600); err != nil {
t.Fatalf("failed to write transcript: %v", err)
}
// Test 1: From line 0 - all 3 turns = 600 input, 300 output
usage1, err := CalculateTotalTokenUsageFromTranscript(transcriptPath, 0, "")
if err != nil {
t.Fatalf("CalculateTotalTokenUsageFromTranscript(0) error: %v", err)
}
if usage1.InputTokens != 600 || usage1.OutputTokens != 300 {
t.Errorf("From line 0: got input=%d output=%d, want input=600 output=300",
usage1.InputTokens, usage1.OutputTokens)
}
if usage1.APICallCount != 3 {
t.Errorf("From line 0: got APICallCount=%d, want 3", usage1.APICallCount)
}
// Test 2: From line 2 (after turn 1) - turns 2+3 only = 500 input, 250 output
usage2, err := CalculateTotalTokenUsageFromTranscript(transcriptPath, 2, "")
if err != nil {
t.Fatalf("CalculateTotalTokenUsageFromTranscript(2) error: %v", err)
}
if usage2.InputTokens != 500 || usage2.OutputTokens != 250 {
t.Errorf("From line 2: got input=%d output=%d, want input=500 output=250",
usage2.InputTokens, usage2.OutputTokens)
}
if usage2.APICallCount != 2 {
t.Errorf("From line 2: got APICallCount=%d, want 2", usage2.APICallCount)
}
// Test 3: From line 4 (after turns 1+2) - turn 3 only = 300 input, 150 output
usage3, err := CalculateTotalTokenUsageFromTranscript(transcriptPath, 4, "")
if err != nil {
t.Fatalf("CalculateTotalTokenUsageFromTranscript(4) error: %v", err)
}
if usage3.InputTokens != 300 || usage3.OutputTokens != 150 {
t.Errorf("From line 4: got input=%d output=%d, want input=300 output=150",
usage3.InputTokens, usage3.OutputTokens)
}
if usage3.APICallCount != 1 {
t.Errorf("From line 4: got APICallCount=%d, want 1", usage3.APICallCount)
}
}
func TestExtractAllModifiedFilesFromTranscript_IncludesSubagentFiles(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
transcriptPath := tmpDir + "/transcript.jsonl"
subagentsDir := tmpDir + "/tasks/toolu_task1"
if err := os.MkdirAll(subagentsDir, 0o755); err != nil {
t.Fatalf("failed to create subagents dir: %v", err)
}
// Main transcript: Write to main.go + Task call spawning subagent "sub1"
writeJSONLFile(t, transcriptPath,
makeWriteToolLine(t, "a1", "/repo/main.go"),
makeTaskToolUseLine(t, "a2", "toolu_task1"),
makeTaskResultLine(t, "u1", "toolu_task1", "sub1"),
)
// Subagent transcript: Write to helper.go + Edit to utils.go
writeJSONLFile(t, subagentsDir+"/agent-sub1.jsonl",
makeWriteToolLine(t, "sa1", "/repo/helper.go"),
makeEditToolLine(t, "sa2", "/repo/utils.go"),
)
files, err := ExtractAllModifiedFilesFromTranscript(transcriptPath, 0, subagentsDir)
if err != nil {
t.Fatalf("ExtractAllModifiedFilesFromTranscript() error: %v", err)
}
if len(files) != 3 {
t.Errorf("expected 3 files, got %d: %v", len(files), files)
}
wantFiles := map[string]bool{
"/repo/main.go": true,
"/repo/helper.go": true,
"/repo/utils.go": true,
}
for _, f := range files {
if !wantFiles[f] {
t.Errorf("unexpected file %q in result", f)
}
delete(wantFiles, f)
}
for f := range wantFiles {
t.Errorf("missing expected file %q", f)
}
}
func TestExtractAllModifiedFilesFromTranscript_DeduplicatesAcrossAgents(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
transcriptPath := tmpDir + "/transcript.jsonl"
subagentsDir := tmpDir + "/tasks/toolu_task1"
if err := os.MkdirAll(subagentsDir, 0o755); err != nil {
t.Fatalf("failed to create subagents dir: %v", err)
}
// Main transcript: Write to shared.go + Task call
writeJSONLFile(t, transcriptPath,
makeWriteToolLine(t, "a1", "/repo/shared.go"),
makeTaskToolUseLine(t, "a2", "toolu_task1"),
makeTaskResultLine(t, "u1", "toolu_task1", "sub1"),
)
// Subagent transcript: Also modifies shared.go (same file as main)
writeJSONLFile(t, subagentsDir+"/agent-sub1.jsonl",
makeEditToolLine(t, "sa1", "/repo/shared.go"),
)
if len(files) != 1 {
t.Errorf("expected 1 file (deduplicated), got %d: %v", len(files), files)
}
if len(files) > 0 && files[0] != "/repo/shared.go" {
t.Errorf("expected /repo/shared.go, got %q", files[0])
}
}
func TestExtractAllModifiedFilesFromTranscript_NoSubagents(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
transcriptPath := tmpDir + "/transcript.jsonl"
// Main transcript: Write to a file, no Task calls
writeJSONLFile(t, transcriptPath,
makeWriteToolLine(t, "a1", "/repo/solo.go"),
)
files, err := ExtractAllModifiedFilesFromTranscript(transcriptPath, 0, tmpDir+"/nonexistent")
if err != nil {
t.Fatalf("ExtractAllModifiedFilesFromTranscript() error: %v", err)
}
if len(files) != 1 {
t.Errorf("expected 1 file, got %d: %v", len(files), files)
}
if len(files) > 0 && files[0] != "/repo/solo.go" {
t.Errorf("expected /repo/solo.go, got %q", files[0])
}
}
func TestExtractAllModifiedFilesFromTranscript_SubagentOnlyChanges(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
transcriptPath := tmpDir + "/transcript.jsonl"
subagentsDir := tmpDir + "/tasks/toolu_task1"
if err := os.MkdirAll(subagentsDir, 0o755); err != nil {
t.Fatalf("failed to create subagents dir: %v", err)
}
// Main transcript: ONLY a Task call, no direct file modifications
// This is the key bug scenario - if we only look at the main transcript,
// we miss all the subagent's file changes entirely.
writeJSONLFile(t, transcriptPath,
makeTaskToolUseLine(t, "a1", "toolu_task1"),
makeTaskResultLine(t, "u1", "toolu_task1", "sub1"),
)
// Subagent transcript: Write to two files
writeJSONLFile(t, subagentsDir+"/agent-sub1.jsonl",
makeWriteToolLine(t, "sa1", "/repo/subagent_file1.go"),
makeWriteToolLine(t, "sa2", "/repo/subagent_file2.go"),
)
if len(files) != 2 {
t.Errorf("expected 2 files from subagent, got %d: %v", len(files), files)
}
wantFiles := map[string]bool{
"/repo/subagent_file1.go": true,
"/repo/subagent_file2.go": true,
}
for _, f := range files {
if !wantFiles[f] {
t.Errorf("unexpected file %q in result", f)
}
delete(wantFiles, f)
}
for f := range wantFiles {
t.Errorf("missing expected file %q", f)
}
}
// mustMarshal is a test helper that marshals a value to JSON or fails the test.
func mustMarshal(t *testing.T, v interface{}) []byte {
t.Helper()
45 unmodified lines
return makeFileToolLine(t, "Write", id, filePath)
}
// makeEditToolLine returns a Droid-format JSONL line with an Edit tool_use for the given file.
func makeEditToolLine(t *testing.T, id, filePath string) string {
t.Helper()
return makeFileToolLine(t, "Edit", id, filePath)
}
// makeTaskToolUseLine returns a Droid-format JSONL line with a Task tool_use (spawning a subagent).
func makeTaskToolUseLine(t *testing.T, id, toolUseID string) string {
t.Helper()
innerMsg := mustMarshal(t, map[string]interface{}{
"role": "assistant",
"content": []map[string]interface{}{
{
"type": "tool_use",
"id": toolUseID,
"name": "Task",
"input": map[string]string{"prompt": "do something"},
},
},
})
line := mustMarshal(t, map[string]interface{}{
"type": "message",
"id": id,
"message": json.RawMessage(innerMsg),
})
return string(line)
}
// makeTaskResultLine returns a Droid-format JSONL user line with a tool_result containing agentId.
func makeTaskResultLine(t *testing.T, id, toolUseID, agentID string) string {
t.Helper()
innerMsg := mustMarshal(t, map[string]interface{}{
"role": "user",
"content": []map[string]interface{}{
{
"type": "tool_result",
"tool_use_id": toolUseID,
"content": "agentId: " + agentID,
},
},
})
line := mustMarshal(t, map[string]interface{}{
"type": "message",
"id": id,
"message": json.RawMessage(innerMsg),
})
return string(line)
}
// makeUserTextLine returns a Droid-format JSONL line with a user text message (array content).
func makeUserTextLine(t *testing.T, id, text string) string {
t.Helper()
28 unmodified lines
return string(line)
}
// makeAssistantTokenLine returns a Droid-format JSONL line with an assistant message that has usage data.
func makeAssistantTokenLine(t *testing.T, id, msgID string, inputTokens, outputTokens int) string {
t.Helper()
innerMsg := mustMarshal(t, map[string]interface{}{
"role": "assistant",
"id": msgID,
"usage": map[string]int{
"input_tokens": inputTokens,
"output_tokens": outputTokens,
},
})
line := mustMarshal(t, map[string]interface{}{
"type": "message",
"id": id,
"message": json.RawMessage(innerMsg),
})
return string(line)
}
func TestExtractPrompts(t *testing.T) {
t.Parallel()
175 unmodified lines
}
}
func TestCalculateTotalTokenUsageFromTranscript_WithSubagentFiles(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
transcriptPath := tmpDir + "/transcript.jsonl"
subagentsDir := tmpDir + "/tasks/toolu_task1"
if err := os.MkdirAll(subagentsDir, 0o755); err != nil {
t.Fatalf("failed to create subagents dir: %v", err)
}
// Main transcript: assistant message with tokens + Task spawning subagent "sub1"
writeJSONLFile(t, transcriptPath,
makeAssistantTokenLine(t, "a1", "msg_main1", 100, 50),
makeTaskToolUseLine(t, "a2", "toolu_task2"),
makeTaskResultLine(t, "u2", "toolu_task2", "sub99"),
)
// Subagent transcript: assistant message with its own tokens
writeJSONLFile(t, subagentsDir+"/agent-sub99.jsonl",
makeAssistantTokenLine(t, "sa1", "msg_sub1", 200, 80),
makeAssistantTokenLine(t, "sa2", "msg_sub2", 150, 60),
)
usage, err := CalculateTotalTokenUsageFromTranscript(transcriptPath, 0, subagentsDir)
if err != nil {
t.Fatalf("CalculateTotalTokenUsageFromTranscript() error: %v", err)
}
// Main agent: 100 input, 50 output, 1 API call
if usage.InputTokens != 100 {
t.Errorf("main InputTokens = %d, want 100", usage.InputTokens)
}
if usage.OutputTokens != 50 {
t.Errorf("main OutputTokens = %d, want 50", usage.OutputTokens)
}
if usage.APICallCount != 1 {
t.Errorf("main APICallCount = %d, want 1", usage.APICallCount)
}
// Subagent tokens should be aggregated
if usage.SubagentTokens == nil {
t.Fatal("SubagentTokens is nil, expected subagent token data")
}
if usage.SubagentTokens.InputTokens != 350 {
t.Errorf("subagent InputTokens = %d, want 350 (200+150)", usage.SubagentTokens.InputTokens)
}
if usage.SubagentTokens.OutputTokens != 140 {
t.Errorf("subagent OutputTokens = %d, want 140 (80+60)", usage.SubagentTokens.OutputTokens)
}
if usage.SubagentTokens.APICallCount != 2 {
t.Errorf("subagent APICallCount = %d, want 2", usage.SubagentTokens.APICallCount)
}
}
func TestCleanModelName(t *testing.T) {
t.Parallel()
Mcmd/entire/cli/agent/factoryaidroid/transcript_test.go-353
2 unmodified lines
3
4
5
6
6
7
8
162 unmodified lines
171
172
173
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
174
175
176
2 unmodified lines
import (
"encoding/json"
"fmt"
"os"
"strings"
)
162 unmodified lines
return prompts
}
// GetLastMessageID returns the ID of the last message in the transcript.
// Returns empty string if the transcript is empty or the last message has no ID.
func GetLastMessageID(data []byte) (string, error) {
transcript, err := ParseTranscript(data)
if err != nil {
return "", err
}
return GetLastMessageIDFromTranscript(transcript), nil
}
// GetLastMessageIDFromTranscript returns the ID of the last message in a parsed transcript.
// Returns empty string if the transcript is empty or the last message has no ID.
func GetLastMessageIDFromTranscript(transcript *GeminiTranscript) string {
if len(transcript.Messages) == 0 {
return ""
}
return transcript.Messages[len(transcript.Messages)-1].ID
}
// GetLastMessageIDFromFile reads a transcript file and returns the last message's ID.
// Returns empty string if the file doesn't exist, is empty, or has no messages with IDs.
func GetLastMessageIDFromFile(path string) (string, error) {
if path == "" {
return "", nil
}
data, err := os.ReadFile(path) //nolint:gosec // Reading from controlled transcript path
if err != nil {
if os.IsNotExist(err) {
return "", nil
}
return "", fmt.Errorf("failed to read transcript: %w", err)
}
if len(data) == 0 {
return "", nil
}
return GetLastMessageID(data)
}
// NormalizeTranscript normalizes user message content fields in-place from
// [{"text":"..."}] arrays to plain strings, preserving all other transcript fields
// (timestamps, thoughts, tokens, model, toolCalls, etc.).
Mcmd/entire/cli/agent/geminicli/transcript.go-42
312 unmodified lines
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
316
317
318
312 unmodified lines
}
}
func TestGetLastMessageID(t *testing.T) {
t.Parallel()
tests := []struct {
name string
data string
want string
wantErr bool
}{
{
name: "transcript with message IDs",
data: `{"messages": [\
{"id": "msg-1", "type": "user", "content": "hello"},\
{"id": "msg-2", "type": "gemini", "content": "hi there"}\
]}`,
want: "msg-2",
wantErr: false,
},
{
name: "empty transcript",
data: `{"messages": []}`,
want: "",
wantErr: false,
},
{
name: "message without ID (empty ID field)",
data: `{"messages": [\
{"type": "user", "content": "hello"},\
{"type": "gemini", "content": "hi"}\
]}`,
want: "",
wantErr: false,
},
{
name: "mixed - some with IDs, some without",
data: `{"messages": [\
{"id": "msg-1", "type": "user", "content": "hello"},\
{"type": "gemini", "content": "hi"}\
]}`,
want: "",
wantErr: false,
},
{
name: "invalid JSON",
data: `not valid json`,
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := GetLastMessageID([]byte(tt.data))
if (err != nil) != tt.wantErr {
t.Errorf("GetLastMessageID() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("GetLastMessageID() = %q, want %q", got, tt.want)
}
})
}
}
func TestGetLastMessageIDFromTranscript(t *testing.T) {
t.Parallel()
tests := []struct {
name string
transcript *GeminiTranscript
want string
}{
{
name: "transcript with message IDs",
transcript: &GeminiTranscript{
Messages: []GeminiMessage{
{ID: "msg-1", Type: "user", Content: "hello"},
{ID: "msg-2", Type: "gemini", Content: "hi there"},
},
},
want: "msg-2",
},
{
name: "empty transcript",
transcript: &GeminiTranscript{
Messages: []GeminiMessage{},
},
want: "",
},
{
name: "message without ID",
transcript: &GeminiTranscript{
Messages: []GeminiMessage{
{Type: "user", Content: "hello"},
},
},
want: "",
},
{
name: "nil messages",
transcript: &GeminiTranscript{},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := GetLastMessageIDFromTranscript(tt.transcript)
if got != tt.want {
t.Errorf("GetLastMessageIDFromTranscript() = %q, want %q", got, tt.want)
}
})
}
}
func TestGetLastMessageIDFromFile(t *testing.T) {
t.Parallel()
t.Run("empty path", func(t *testing.T) {
t.Parallel()
got, err := GetLastMessageIDFromFile("")
if err != nil {
t.Errorf("GetLastMessageIDFromFile() error = %v", err)
}
if got != "" {
t.Errorf("GetLastMessageIDFromFile() = %q, want empty string", got)
}
})
t.Run("non-existent file", func(t *testing.T) {
t.Parallel()
got, err := GetLastMessageIDFromFile("/nonexistent/path/transcript.json")
if err != nil {
t.Errorf("GetLastMessageIDFromFile() error = %v, want nil for non-existent file", err)
}
if got != "" {
t.Errorf("GetLastMessageIDFromFile() = %q, want empty string", got)
}
})
t.Run("empty file", func(t *testing.T) {
t.Parallel()
tmpFile := t.TempDir() + "/empty.json"
if err := os.WriteFile(tmpFile, []byte(""), 0o644); err != nil {
t.Fatalf("failed to create test file: %v", err)
}
got, err := GetLastMessageIDFromFile(tmpFile)
if err != nil {
t.Errorf("GetLastMessageIDFromFile() error = %v", err)
}
if got != "" {
t.Errorf("GetLastMessageIDFromFile() = %q, want empty string", got)
}
})
t.Run("valid file with message IDs", func(t *testing.T) {
t.Parallel()
tmpFile := t.TempDir() + "/transcript.json"
content := `{"messages": [{"id": "abc-123", "type": "user", "content": "hello"}]}`
if err := os.WriteFile(tmpFile, []byte(content), 0o644); err != nil {
t.Fatalf("failed to create test file: %v", err)
}
got, err := GetLastMessageIDFromFile(tmpFile)
if err != nil {
t.Errorf("GetLastMessageIDFromFile() error = %v", err)
}
if got != "abc-123" {
t.Errorf("GetLastMessageIDFromFile() = %q, want 'abc-123'", got)
}
})
t.Run("invalid JSON file", func(t *testing.T) {
t.Parallel()
tmpFile := t.TempDir() + "/invalid.json"
if err := os.WriteFile(tmpFile, []byte("not valid json"), 0o644); err != nil {
t.Fatalf("failed to create test file: %v", err)
}
_, err := GetLastMessageIDFromFile(tmpFile)
if err == nil {
t.Error("GetLastMessageIDFromFile() expected error for invalid JSON")
}
})
}
func TestExtractAllUserPrompts_ArrayContent(t *testing.T) {
t.Parallel()
Mcmd/entire/cli/agent/geminicli/transcript_test.go-186
89 unmodified lines
90
91
92
93
94
95
96
97
98
99
100
101
102
103
93
94
95
89 unmodified lines
return detected
}
// Detect attempts to auto-detect which agent is being used.
// Iterates registered agents in sorted name order for deterministic results.
// Returns the first agent whose DetectPresence reports true.
func Detect(ctx context.Context) (Agent, error) {
detected := DetectAll(ctx)
if len(detected) == 0 {
return nil, fmt.Errorf("no agent detected (available: %v)", List())
}
return detected[0], nil
}
// AgentForTranscriptPath returns the registered agent whose session directory
// for repoPath contains the given transcript path. Used to disambiguate which
// agent owns a session when multiple agents' hooks fire for the same session
Mcmd/entire/cli/agent/registry.go-11
70 unmodified lines
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
74
75
76
70 unmodified lines
})
}
func TestDetect(t *testing.T) {
// Save original registry state
originalRegistry := make(map[types.AgentName]Factory)
registryMu.Lock()
for k, v := range registry {
originalRegistry[k] = v
}
registry = make(map[types.AgentName]Factory)
registryMu.Unlock()
defer func() {
registryMu.Lock()
registry = originalRegistry
registryMu.Unlock()
}()
t.Run("returns error when no agents detected", func(t *testing.T) {
// Register an agent that won't be detected
Register(types.AgentName("undetected"), func() Agent {
return &mockAgent{} // DetectPresence returns false
})
_, err := Detect(context.Background())
if err == nil {
t.Error("expected error when no agent detected")
}
if !strings.Contains(err.Error(), "no agent detected") {
t.Errorf("expected 'no agent detected' in error, got: %v", err)
}
})
t.Run("returns detected agent", func(t *testing.T) {
// Clear registry
registryMu.Lock()
registry = make(map[types.AgentName]Factory)
registryMu.Unlock()
// Register an agent that will be detected
Register(types.AgentName("detected"), func() Agent {
return &detectableAgent{}
})
agent, err := Detect(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if agent.Name() != types.AgentName("detectable") {
t.Errorf("expected Name() %q, got %q", "detectable", agent.Name())
}
})
}
// detectableAgent is a mock that returns true for DetectPresence
type detectableAgent struct {
mockAgent
}
func (d *detectableAgent) Name() types.AgentName {
return types.AgentName("detectable")
}
func (d *detectableAgent) DetectPresence(_ context.Context) (bool, error) {
return true, nil
}
// sessionDirAgent is a mock with a configurable session dir, for path-prefix tests.
type sessionDirAgent struct {
mockAgent
Mcmd/entire/cli/agent/registry_test.go-65
80 unmodified lines
81
82
83
84
85
86
87
88
89
90
91
92
93
84
85
86
80 unmodified lines
return out
}
// Get returns the importer registered for name.
func Get(name string) (Importer, bool) {
for _, imp := range importers {
if imp.Name() == name {
return imp, true
}
}
return nil, false
}
// Options configures an import run.
type Options struct {
RepoRoot string
Mcmd/entire/cli/agentimport/agentimport.go-10
49 unmodified lines
50
51
52
53
54
55
56
57
58
59
60
61
54
62
63
64
65
2 unmodified lines
68
69
70
63
64
65
66
67
68
69
70
71
72
73
49 unmodified lines
want := []string{
"claude-code", "cursor", "pi", "factoryai-droid", "codex", "copilot-cli", "gemini",
}
registered := make(map[string]Importer)
for _, imp := range All() {
if _, dup := registered[imp.Name()]; dup {
t.Errorf("duplicate importer name %q", imp.Name())
}
registered[imp.Name()] = imp
}
for _, name := range want {
imp, ok := Get(name)
imp, ok := registered[name]
if !ok {
t.Errorf("%s importer not registered", name)
continue
2 unmodified lines
t.Errorf("%s importer has empty AgentType", name)
}
}
seen := make(map[string]bool)
for _, imp := range All() {
if seen[imp.Name()] {
t.Errorf("duplicate importer name %q", imp.Name())
}
seen[imp.Name()] = true
}
if len(All()) != len(want) {
t.Errorf("registered %d importers, want %d (%v)", len(All()), len(want), want)
}
Mcmd/entire/cli/agentimport/agentimport_test.go+9/-9
51 unmodified lines
52
53
54
55
56
57
58
59
55
56
57
51 unmodified lines
return DefaultBaseURL
}
// ResolveURL joins an API-relative path against the effective base URL.
func ResolveURL(path string) (string, error) {
return ResolveURLFromBase(BaseURL(), path)
}
// ResolveURLFromBase joins an API-relative path against an explicit base URL.
// Only http and https schemes are accepted.
func ResolveURLFromBase(baseURL, path string) (string, error) {
Mcmd/entire/cli/api/base_url.go-5
78 unmodified lines
79
80
81
82
83
82
83
84
85
85
86
87
87
88
89
90
91
91
92
93
94
78 unmodified lines
}
}
func TestResolveURL(t *testing.T) {
t.Setenv(BaseURLEnvVar, "http://localhost:8787/")
func TestResolveURLFromBase_JoinsPath(t *testing.T) {
t.Parallel()
got, err := ResolveURL("/oauth/device/code")
got, err := ResolveURLFromBase("http://localhost:8787", "/oauth/device/code")
if err != nil {
t.Fatalf("ResolveURL() error = %v", err)
t.Fatalf("ResolveURLFromBase() error = %v", err)
}
if got != "http://localhost:8787/oauth/device/code" {
t.Fatalf("ResolveURL() = %q, want %q", got, "http://localhost:8787/oauth/device/code")
t.Fatalf("ResolveURLFromBase() = %q, want %q", got, "http://localhost:8787/oauth/device/code")
}
}
Mcmd/entire/cli/api/base_url_test.go+5/-5
7 unmodified lines
8
9
10
11
12
13
14
4 unmodified lines
19
20
21
22
23
24
25
26
27
28
42 unmodified lines
71
72
73
69
74
75
76
77
4 unmodified lines
82
83
84
80
85
86
87
88
7 unmodified lines
"strings"
"time"
"github.com/entireio/cli/internal/entireclient/clusterdiscovery"
"github.com/entireio/cli/internal/entireclient/contexts"
"github.com/entireio/cli/internal/entireclient/userdirs"
)
4 unmodified lines
// slow endpoint fails the command promptly.
const controlPlaneClusterDiscoveryTimeout = 8 * time.Second
// resolveContextForCluster is the discovery seam, swapped in tests so they
// don't reach the network. Mirrors clusterdiscovery.ResolveContextForCluster.
var resolveContextForCluster resolveContextFunc = clusterdiscovery.ResolveContextForCluster
// ControlPlaneTarget is the resolved login server a control-plane request
// (org/repo/project/grant) should dial, plus the bearer source for it.
//
42 unmodified lines
// /.well-known/entire-cluster.json and pick the local context eligible for one
// of them — active-wins-if-eligible, else the sole eligible context, else an
// explicit-choice / login hint — exactly as git and data-API resolution do
// (see RepoTokenSource, ResolveDataAPIToken). The bearer is that context's
// (see ResolveDataAPIToken). The bearer is that context's
// refreshing login provider (silent JWT re-mint from its stored refresh token).
//
// With no eligible local context the discovery resolver returns its login hint
4 unmodified lines
if clusterHost == "" {
return ControlPlaneTarget{}, errors.New("cluster-addressed control-plane command requires a target cluster host")
}
httpClient := &http.Client{Timeout: controlPlaneClusterDiscoveryTimeout, Transport: repoExchangeTransportForTest}
httpClient := &http.Client{Timeout: controlPlaneClusterDiscoveryTimeout}
c, err := resolveContextForCluster(ctx, userdirs.Config(), userdirs.Cache(), clusterHost, httpClient, nil)
if err != nil {
return ControlPlaneTarget{}, err
Mcmd/entire/cli/auth/control_plane.go+7/-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package auth
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"github.com/entireio/cli/internal/entireclient/clusterdiscovery"
"github.com/entireio/cli/internal/entireclient/httputil"
"github.com/entireio/cli/internal/entireclient/repocreds"
"github.com/entireio/cli/internal/entireclient/userdirs"
)
// ErrRepoTargetUnknown reports that the cluster's STS refused the exchange
// with RFC 8693 `invalid_target`: it has no servable mirror at the
// requested audience. The placement row may well exist but be suspended —
// the data plane's auth gate deliberately hides suspended mirrors behind
// invalid_target rather than disclosing their state (an enumeration guard;
// see entiredb's validateMirrorRepoExchange). Callers that already know the
// mirror exists (e.g. the create flow's clone probe) use this to render an
// actionable message instead of the raw OAuth error.
var ErrRepoTargetUnknown = errors.New("cluster has no servable mirror at this audience")
// repoExchangeTimeout bounds each HTTP call on the mint path: the
// /.well-known cluster discovery at construction (disk-cached after the
// first call) and each /oauth/token exchange.
const repoExchangeTimeout = 30 * time.Second
// repoExchangeTransportForTest, when non-nil, is the HTTP transport used by
// RepoScopedToken's exchange (and login refresh), so the wire form can be
// asserted without a live core. Production leaves it nil.
var repoExchangeTransportForTest http.RoundTripper
// SetRepoExchangeTransportForTest installs rt as the transport used by
// RepoScopedToken and returns a cleanup function. Test-only.
func SetRepoExchangeTransportForTest(rt http.RoundTripper) func() {
prev := repoExchangeTransportForTest
repoExchangeTransportForTest = rt
return func() { repoExchangeTransportForTest = prev }
}
// RepoTokenSource mints short-lived, repo-scoped access tokens usable
// against one data-plane cluster's git endpoints (clone / fetch /
// info-refs). The data plane's git gate rejects the raw login bearer: it
// only accepts a token whose RFC 8693 audience is
// https://<clusterHost><repoSlug> and whose scope is "repo:<action>".
//
// The login context is resolved once, at construction, the way
// git-remote-entire resolves it: the cluster's
// /.well-known/entire-cluster.json names the core(s) it trusts, and the
// matching local context (active if eligible, else the sole eligible one,
// else an explicit-choice error) supplies the subject token — exchanged at
// that context's core, never at the active context's. Token calls then only
// exchange (through repocreds, the same code path and wire form
// git-remote-entire uses), re-minting an expired login JWT from the stored
// refresh token as needed — so a poller's re-mints don't depend on
// discovery staying reachable.
type RepoTokenSource struct {
creds *repocreds.Cache
}
// NewRepoTokenSource resolves clusterHost's trusted core and login context
// and returns a source minting tokens for that cluster.
func NewRepoTokenSource(ctx context.Context, clusterHost string) (*RepoTokenSource, error) {
if clusterHost == "" {
return nil, errors.New("repo-scoped token exchange requires a target cluster host")
}
httpClient := &http.Client{Timeout: repoExchangeTimeout, Transport: repoExchangeTransportForTest}
clusterCtx, err := resolveContextForCluster(ctx, userdirs.Config(), userdirs.Cache(), clusterHost, httpClient, nil)
if err != nil {
return nil, err
}
allowInsecure := insecureHTTPEnabled() || isLoopbackHTTP(clusterCtx.CoreURL)
loginProvider, err := NewRefreshingLoginProvider(clusterCtx, repoExchangeTransportForTest, allowInsecure)
if err != nil {
return nil, err
}
return &RepoTokenSource{creds: repocreds.New(clusterCtx.CoreURL, "https://"+clusterHost, loginProvider, httpClient)}, nil
}
// Token returns a repo-scoped token for repoSlug (the surface-prefixed repo
// path, e.g. /gh/octocat/hello, joined verbatim to the cluster URL to form
// the audience) and action ("pull" or "push"). Tokens are cached per
// (repoSlug, action) until near expiry.
func (s *RepoTokenSource) Token(ctx context.Context, repoSlug, action string) (string, error) {
token, err := s.creds.Token(ctx, repoSlug, action)
if err != nil {
// invalid_target means the cluster has no servable mirror at this
// audience (commonly a suspended placement). Surface the sentinel for
// callers that branch on it, preserving the verbatim OAuth body
// (second %w) for those that don't.
var oe *httputil.OAuthError
if errors.As(err, &oe) && oe.Code == "invalid_target" {
return "", fmt.Errorf("repo-scoped token exchange: %w: %w", ErrRepoTargetUnknown, err)
}
return "", fmt.Errorf("repo-scoped token exchange: %w", err)
}
return token, nil
}
// Invalidate drops the cached (repoSlug, action) token so the next Token
// call re-exchanges — for when the data plane rejected it (401) ahead of
// its recorded expiry.
func (s *RepoTokenSource) Invalidate(repoSlug, action string) {
s.creds.Invalidate(repoSlug, action)
}
// RepoScopedToken is the one-shot form of RepoTokenSource: resolve, mint
// once, discard. Callers that re-mint (e.g. a polling wait) should hold a
// RepoTokenSource instead, so re-mints skip cluster discovery.
func RepoScopedToken(ctx context.Context, clusterHost, repoSlug, action string) (string, error) {
src, err := NewRepoTokenSource(ctx, clusterHost)
if err != nil {
return "", err
}
return src.Token(ctx, repoSlug, action)
}
Dcmd/entire/cli/auth/repo_token.go-126
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
package auth
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path/filepath"
"strings"
"testing"
"time"
"github.com/entireio/cli/internal/entireclient/clusterdiscovery"
"github.com/entireio/cli/internal/entireclient/contexts"
"github.com/entireio/cli/internal/entireclient/tokenstore"
)
// sandboxRepoTokenStores redirects the token store and contexts.json (the
// legacy-login migration's write target) to temp locations.
func sandboxRepoTokenStores(t *testing.T) {
t.Helper()
t.Setenv("ENTIRE_CONFIG_DIR", t.TempDir())
t.Cleanup(tokenstore.UseFileBackendForTesting(filepath.Join(t.TempDir(), "tokens.json")))
}
// stubResolveContextForCluster swaps the discovery seam for fn.
func stubResolveContextForCluster(t *testing.T, fn resolveContextFunc) {
t.Helper()
prev := resolveContextForCluster
resolveContextForCluster = fn
t.Cleanup(func() { resolveContextForCluster = prev })
}
// seedRepoTokenContext wires the two seams RepoScopedToken sits on: a
// file-backed token store holding a still-valid login JWT for a context on
// coreURL, and a discovery stub resolving any cluster to that context. The
// exchange transport is left to each test. Returns the seeded login JWT.
func seedRepoTokenContext(t *testing.T, coreURL string) string {
t.Helper()
sandboxRepoTokenStores(t)
svc := tokenstore.CoreKeyringService(coreURL)
jwt := makeJWT(t, fmt.Sprintf(`{"iss":%q,"handle":"alice","exp":%d}`, coreURL, time.Now().Add(2*time.Hour).Unix()))
if err := tokenstore.Set(svc, "alice", tokenstore.EncodeTokenWithExpiration(jwt, 7200)); err != nil {
t.Fatalf("seed token: %v", err)
}
c := &contexts.Context{Name: "alice@core", CoreURL: coreURL, Handle: "alice", KeychainService: svc}
stubResolveContextForCluster(t,
func(context.Context, string, string, string, *http.Client, clusterdiscovery.DebugFunc) (*contexts.Context, error) {
return c, nil
})
return jwt
}
// statusTransport returns a canned non-200 response with the given body so
// the OAuth error-decoding path can be exercised offline.
type statusTransport struct {
status int
body string
}
func (s statusTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: s.status,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(s.body)),
Request: req,
}, nil
}
// TestRepoScopedToken_InvalidTarget asserts that a 400 invalid_target STS
// response — what the data plane returns for a suspended (or otherwise
// non-servable) mirror — surfaces as ErrRepoTargetUnknown while still
// preserving the verbatim OAuth description for callers that don't branch
// on the sentinel.
func TestRepoScopedToken_InvalidTarget(t *testing.T) {
seedRepoTokenContext(t, "https://us.auth.entire.io")
t.Cleanup(SetRepoExchangeTransportForTest(statusTransport{
status: http.StatusBadRequest,
body: `{"error":"invalid_target","error_description":"no mirror at this URL"}`,
}))
_, err := RepoScopedToken(context.Background(),
"aws-us-east-2.entire.io", "/gh/octocat/hello", "pull")
if err == nil {
t.Fatal("RepoScopedToken: expected error, got nil")
}
if !errors.Is(err, ErrRepoTargetUnknown) {
t.Errorf("error %v does not wrap ErrRepoTargetUnknown", err)
}
// Verbatim STS detail must remain in the chain.
if !strings.Contains(err.Error(), "no mirror at this URL") {
t.Errorf("error %q dropped the STS description", err)
}
}
// captureTransport counts exchanges and records the last request's parsed
// form body, URL, and Authorization header, returning a canned RFC 8693
// token-exchange success response.
type captureTransport struct {
calls int
form url.Values
url string
auth string
}
func (c *captureTransport) RoundTrip(req *http.Request) (*http.Response, error) {
body, err := io.ReadAll(req.Body)
if err != nil {
return nil, err
}
form, err := url.ParseQuery(string(body))
if err != nil {
return nil, err
}
c.calls++
c.form = form
c.url = req.URL.String()
c.auth = req.Header.Get("Authorization")
resp := `{"access_token":"repo-scoped.jwt","token_type":"Bearer",` +
`"issued_token_type":"urn:ietf:params:oauth:token-type:access_token","expires_in":300}`
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(bytes.NewBufferString(resp)),
Request: req,
}, nil
}
// TestRepoScopedToken_WireForm asserts the exchange targets the
// cluster-resolved context's core (not any ambient default) and that the
// form matches what the data plane's git gate accepts — identical to what
// git-remote-entire sends via repocreds.
func TestRepoScopedToken_WireForm(t *testing.T) {
loginJWT := seedRepoTokenContext(t, "https://eu.auth.entire.io")
capture := &captureTransport{}
t.Cleanup(SetRepoExchangeTransportForTest(capture))
tok, err := RepoScopedToken(context.Background(),
"aws-eu-west-1.entire.io", "/gh/octocat/hello", "pull")
if err != nil {
t.Fatalf("RepoScopedToken: %v", err)
}
if tok != "repo-scoped.jwt" {
t.Errorf("token = %q, want %q", tok, "repo-scoped.jwt")
}
// Endpoint: the resolved context's core, not the active context's.
if capture.url != "https://eu.auth.entire.io/oauth/token" {
t.Errorf("exchange URL = %q, want resolved core's /oauth/token", capture.url)
}
// Wire form must match what the data plane's git gate accepts.
want := map[string]string{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": loginJWT,
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
"audience": "https://aws-eu-west-1.entire.io/gh/octocat/hello",
"scope": "repo:pull",
}
for k, v := range want {
if got := capture.form.Get(k); got != v {
t.Errorf("form[%q] = %q, want %q", k, got, v)
}
}
// resource must NOT be sent — the gate keys on audience alone, and a
// divergent resource param risks the server validating the wrong value.
if capture.form.Has("resource") {
t.Errorf("form unexpectedly includes resource=%q", capture.form.Get("resource"))
}
// client_id travels as Basic auth (PostOAuthToken lifts it from the
// form; zitadel's token endpoint only reads it there).
if capture.form.Has("client_id") {
t.Errorf("form unexpectedly includes client_id=%q", capture.form.Get("client_id"))
}
if !strings.HasPrefix(capture.auth, "Basic ") {
t.Errorf("Authorization = %q, want Basic client credentials", capture.auth)
}
}
// TestRepoScopedToken_DiscoveryErrorSurfaces asserts a context-resolution
// failure (no eligible login, ambiguous contexts, unreachable cluster) is
// returned verbatim — never papered over with a wrong-core exchange.
func TestRepoScopedToken_DiscoveryErrorSurfaces(t *testing.T) {
sandboxRepoTokenStores(t)
stubResolveContextForCluster(t,
func(context.Context, string, string, string, *http.Client, clusterdiscovery.DebugFunc) (*contexts.Context, error) {
return nil, errors.New("not logged in to a login server trusted by cluster x; run `entire login`")
})
t.Cleanup(SetRepoExchangeTransportForTest(failRoundTripper(t)))
_, err := RepoScopedToken(context.Background(), "x.entire.io", "/gh/o/r", "pull")
if err == nil || !strings.Contains(err.Error(), "not logged in to a login server trusted by cluster x") {
t.Fatalf("err = %v, want the discovery error verbatim", err)
}
}
// TestRepoTokenSource_ReMintSkipsDiscovery asserts cluster discovery runs
// once, at construction — re-mints after Invalidate (the clone wait's
// 401 path) only re-exchange, so a discovery hiccup mid-wait can't abort a
// wait that already authorized.
func TestRepoTokenSource_ReMintSkipsDiscovery(t *testing.T) {
seedRepoTokenContext(t, "https://us.auth.entire.io")
var discoveries int
inner := resolveContextForCluster
stubResolveContextForCluster(t,
func(ctx context.Context, configDir, cacheDir, host string, hc *http.Client, debugf clusterdiscovery.DebugFunc) (*contexts.Context, error) {
discoveries++
return inner(ctx, configDir, cacheDir, host, hc, debugf)
})
capture := &captureTransport{}
t.Cleanup(SetRepoExchangeTransportForTest(capture))
src, err := NewRepoTokenSource(context.Background(), "aws-us-east-2.entire.io")
if err != nil {
t.Fatalf("NewRepoTokenSource: %v", err)
}
if _, err := src.Token(context.Background(), "/gh/octocat/hello", "pull"); err != nil {
t.Fatalf("Token: %v", err)
}
src.Invalidate("/gh/octocat/hello", "pull")
if _, err := src.Token(context.Background(), "/gh/octocat/hello", "pull"); err != nil {
t.Fatalf("Token after Invalidate: %v", err)
}
if discoveries != 1 {
t.Errorf("discovery ran %d times, want 1 (construction only)", discoveries)
}
if capture.calls != 2 {
t.Errorf("exchange ran %d times, want 2 (initial + re-mint)", capture.calls)
}
}
Dcmd/entire/cli/auth/repo_token_test.go-239
package checkpoint
import ( "fmt" "io" "strconv" "strings"
"github.com/entireio/cli/cmd/entire/cli/agent" "github.com/entireio/cli/cmd/entire/cli/checkpoint/id" "github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/object" "github.com/go-git/go-git/v6/plumbing/storer" )
// TranscriptBlobRef identifies a blob within a checkpoint tree on the metadata branch. // It captures the blob hash from the tree entry without requiring the blob itself to be local. type TranscriptBlobRef struct { // SessionIndex is the 0-based session index within the checkpoint. SessionIndex int
// Hash is the blob's SHA-1 hash from the tree entry. Hash plumbing.Hash
// Path is the blob's path relative to the checkpoint directory, // e.g. "0/full.jsonl" or "0/full.jsonl.001". Path string }
// BlobResolver checks blob existence and reads blobs from go-git's local // object store (loose objects + packfiles). It performs no remote operations. type BlobResolver struct { storer storer.EncodedObjectStorer }
// NewBlobResolver creates a BlobResolver backed by the given object store. func NewBlobResolver(s storer.EncodedObjectStorer) *BlobResolver { return &BlobResolver{storer: s} }
// HasBlob returns true if the blob exists in the local object store. // Checks both loose objects and packfile indices without reading blob content. func (r *BlobResolver) HasBlob(hash plumbing.Hash) bool { return r.storer.HasEncodedObject(hash) == nil }
// ReadBlob reads a blob's content from the local object store. // Returns plumbing.ErrObjectNotFound if the blob is not present locally. func (r *BlobResolver) ReadBlob(hash plumbing.Hash) ([]byte, error) { obj, err := r.storer.EncodedObject(plumbing.BlobObject, hash) if err != nil { return nil, err //nolint:wrapcheck // Propagating plumbing.ErrObjectNotFound }
reader, err := obj.Reader() if err != nil { return nil, fmt.Errorf("blob reader %s: %w", hash, err) } defer reader.Close()
data, err := io.ReadAll(reader) if err != nil { return nil, fmt.Errorf("read blob %s: %w", hash, err) } return data, nil }
// CollectTranscriptBlobHashes walks the metadata branch tree for a checkpoint // and returns blob hashes for all transcript files (full.jsonl and chunks) // across all sessions. Only reads tree objects — works after a treeless fetch // where blobs have not been downloaded. // // The function navigates the sharded checkpoint directory structure: // // <id[:2]>/<id[2:]>/ // ├── 0/ // │ ├── full.jsonl ← collected // │ ├── full.jsonl.001 ← collected (chunk) // │ └── metadata.json // ├── 1/ // │ └── full.jsonl ← collected // └── metadata.json func CollectTranscriptBlobHashes(tree *object.Tree, checkpointID id.CheckpointID) ([]TranscriptBlobRef, error) { checkpointTree, err := tree.Tree(checkpointID.Path()) if err != nil { return nil, fmt.Errorf("checkpoint tree %s: %w", checkpointID.Path(), err) }
var refs []TranscriptBlobRef
// Enumerate session subdirectories (0, 1, 2, ...) for i := 0; ; i++ { sessionDir := strconv.Itoa(i) sessionTree, treeErr := checkpointTree.Tree(sessionDir) if treeErr != nil { break // no more sessions }
// Collect transcript blob hashes from tree entries. // tree.Entries contains the direct children — no blob reads needed. for _, entry := range sessionTree.Entries { if entry.Name == paths.TranscriptFileName || entry.Name == paths.TranscriptFileNameLegacy { refs = append(refs, TranscriptBlobRef{ SessionIndex: i, Hash: entry.Hash, Path: sessionDir + "/" + entry.Name, }) } // Check for chunk files (full.jsonl.001, full.jsonl.002, etc.) if strings.HasPrefix(entry.Name, paths.TranscriptFileName+".") { idx := agent.ParseChunkIndex(entry.Name, paths.TranscriptFileName) if idx > 0 { refs = append(refs, TranscriptBlobRef{ SessionIndex: i, Hash: entry.Hash, Path: sessionDir + "/" + entry.Name, }) } } } }
return refs, nil //nolint:nilerr // treeErr from session enumeration loop is used to break, not propagated }
Dcmd/entire/cli/checkpoint/blob\_resolver.go-126
package checkpoint
import (
"context"
"testing"
"github.com/entireio/cli/cmd/entire/cli/checkpoint/id"
"github.com/entireio/cli/redact"
"github.com/go-git/go-git/v6/plumbing"
)
func TestBlobResolver_HasBlob_Present(t *testing.T) {
t.Parallel()
repo, store, cpID := setupRepoForUpdate(t)
// Get the metadata branch tree
tree, err := store.getSessionsBranchTree()
if err != nil {
t.Fatalf("getSessionsBranchTree() error = %v", err)
}
// Navigate to the transcript blob via tree entries
refs, err := CollectTranscriptBlobHashes(tree, cpID)
if err != nil {
t.Fatalf("CollectTranscriptBlobHashes() error = %v", err)
}
if len(refs) == 0 {
t.Fatal("expected at least one transcript blob ref")
}
resolver := NewBlobResolver(repo.Storer)
// Blob should exist — it was written by WriteCommitted
if !resolver.HasBlob(refs[0].Hash) {
t.Errorf("HasBlob(%s) = false, want true (blob was written locally)", refs[0].Hash)
}
}
func TestBlobResolver_HasBlob_Missing(t *testing.T) {
t.Parallel()
repo, _, _ := setupRepoForUpdate(t)
resolver := NewBlobResolver(repo.Storer)
// Random hash that doesn't exist
fakeHash := plumbing.NewHash("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
if resolver.HasBlob(fakeHash) {
t.Error("HasBlob(fake) = true, want false")
}
}
func TestBlobResolver_ReadBlob(t *testing.T) {
t.Parallel()
repo, store, cpID := setupRepoForUpdate(t)
tree, err := store.getSessionsBranchTree()
if err != nil {
t.Fatalf("getSessionsBranchTree() error = %v", err)
}
refs, err := CollectTranscriptBlobHashes(tree, cpID)
if err != nil {
t.Fatalf("CollectTranscriptBlobHashes() error = %v", err)
}
if len(refs) == 0 {
t.Fatal("expected at least one transcript blob ref")
}
resolver := NewBlobResolver(repo.Storer)
data, err := resolver.ReadBlob(refs[0].Hash)
if err != nil {
t.Fatalf("ReadBlob() error = %v", err)
}
if len(data) == 0 {
t.Error("ReadBlob() returned empty data")
}
// The transcript content from setupRepoForUpdate
if string(data) != "provisional transcript line 1\n" {
t.Errorf("ReadBlob() = %q, want %q", string(data), "provisional transcript line 1\n")
}
}
func TestBlobResolver_ReadBlob_Missing(t *testing.T) {
t.Parallel()
repo, _, _ := setupRepoForUpdate(t)
resolver := NewBlobResolver(repo.Storer)
fakeHash := plumbing.NewHash("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
_, err := resolver.ReadBlob(fakeHash)
if err == nil {
t.Error("ReadBlob(fake) should return error")
}
}
func TestCollectTranscriptBlobHashes_SingleSession(t *testing.T) {
t.Parallel()
_, store, cpID := setupRepoForUpdate(t)
tree, err := store.getSessionsBranchTree()
if err != nil {
t.Fatalf("getSessionsBranchTree() error = %v", err)
}
refs, err := CollectTranscriptBlobHashes(tree, cpID)
if err != nil {
t.Fatalf("CollectTranscriptBlobHashes() error = %v", err)
}
if len(refs) != 1 {
t.Fatalf("expected 1 transcript ref, got %d", len(refs))
}
ref := refs[0]
if ref.SessionIndex != 0 {
t.Errorf("SessionIndex = %d, want 0", ref.SessionIndex)
}
if ref.Hash.IsZero() {
t.Error("Hash should not be zero")
}
if ref.Path != "0/full.jsonl" {
t.Errorf("Path = %q, want %q", ref.Path, "0/full.jsonl")
}
}
func TestCollectTranscriptBlobHashes_MultiSession(t *testing.T) {
t.Parallel()
repo, store, cpID := setupRepoForUpdate(t)
// Write a second session to the same checkpoint
err := store.Write(context.Background(), Session{
CheckpointID: cpID,
SessionID: "session-002",
Strategy: "manual-commit",
Transcript: redact.AlreadyRedacted([]byte("second session transcript\n")),
Prompts: []string{"second prompt"},
AuthorName: "Test",
AuthorEmail: "test@test.com",
})
if err != nil {
t.Fatalf("WriteCommitted() for second session error = %v", err)
}
tree, err := store.getSessionsBranchTree()
if err != nil {
t.Fatalf("getSessionsBranchTree() error = %v", err)
}
refs, err := CollectTranscriptBlobHashes(tree, cpID)
if err != nil {
t.Fatalf("CollectTranscriptBlobHashes() error = %v", err)
}
if len(refs) != 2 {
t.Fatalf("expected 2 transcript refs, got %d", len(refs))
}
// Verify session indices
if refs[0].SessionIndex != 0 {
t.Errorf("refs[0].SessionIndex = %d, want 0", refs[0].SessionIndex)
}
if refs[1].SessionIndex != 1 {
t.Errorf("refs[1].SessionIndex = %d, want 1", refs[1].SessionIndex)
}
// Verify they have different hashes (different transcript content)
if refs[0].Hash == refs[1].Hash {
t.Error("multi-session refs should have different blob hashes")
}
// Verify all blobs exist locally
resolver := NewBlobResolver(repo.Storer)
for i, ref := range refs {
if !resolver.HasBlob(ref.Hash) {
t.Errorf("session %d blob %s should be present locally", i, ref.Hash)
}
}
}
func TestCollectTranscriptBlobHashes_NonexistentCheckpoint(t *testing.T) {
t.Parallel()
_, store, _ := setupRepoForUpdate(t)
tree, err := store.getSessionsBranchTree()
if err != nil {
t.Fatalf("getSessionsBranchTree() error = %v", err)
}
fakeID := id.MustCheckpointID("ffffffffffff")
_, err = CollectTranscriptBlobHashes(tree, fakeID)
if err == nil {
t.Error("expected error for nonexistent checkpoint")
}
}
Dcmd/entire/cli/checkpoint/blob_resolver_test.go-201
4192 unmodified lines
4193
4194
4195
4196
4196
4197
4198
4199
16 unmodified lines
4216
4217
4218
4219
4220
4219
4220
4222
4221
4222
4223
4224
4225
4227
4228
4226
4227
4228
4229
4230
16 unmodified lines
4247
4248
4249
4251
4252
4253
4254
4255
4256
4257
4258
4250
4251
4252
12 unmodified lines
4265
4266
4267
4277
4268
4269
4270
4271
27 unmodified lines
4299
4300
4301
4311
4312
4302
4303
4314
4304
4305
4306
4307
4308
4309
4310
4311
4312
4318
4319
4313
4314
4315
4316
4317
4323
4324
4318
4319
4320
4321
4327
4328
4322
4323
4324
4325
4326
4332
4327
4328
4329
4330
28 unmodified lines
4359
4360
4361
4367
4368
4362
4363
4370
4364
4365
4366
4367
4368
4369
4370
4371
4372
4374
4375
4373
4374
4375
4376
4377
4379
4378
4379
4380
4381
4383
4384
4382
4383
4384
4385
4386
4192 unmodified lines
}
}
func TestAddDirectoryToEntries_PathTraversal(t *testing.T) {
func TestAddDirectoryToChanges_PathTraversal(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
16 unmodified lines
t.Fatalf("failed to write file: %v", err)
}
entries := make(map[string]object.TreeEntry)
err = addDirectoryToEntriesWithAbsPath(context.Background(), repo, metadataDir, ".entire/metadata/session", entries)
changes, err := addDirectoryToChanges(context.Background(), repo, metadataDir, ".entire/metadata/session")
if err != nil {
t.Fatalf("addDirectoryToEntriesWithAbsPath failed: %v", err)
t.Fatalf("addDirectoryToChanges failed: %v", err)
}
// Verify the regular file was included with correct path
expectedPath := filepath.ToSlash(filepath.Join(".entire/metadata/session", "sub", "data.txt"))
if _, ok := entries[expectedPath]; !ok {
t.Errorf("expected entry at %q, got entries: %v", expectedPath, entries)
if len(changes) != 1 || changes[0].Path != expectedPath {
t.Errorf("expected one change at %q, got %#v", expectedPath, changes)
}
}
16 unmodified lines
expectedPath := filepath.ToSlash(filepath.Join("checkpoint", "..generated", "schema.json"))
entries := make(map[string]object.TreeEntry)
if err := addDirectoryToEntriesWithAbsPath(context.Background(), repo, metadataDir, "checkpoint", entries); err != nil {
t.Fatalf("addDirectoryToEntriesWithAbsPath failed: %v", err)
}
if _, ok := entries[expectedPath]; !ok {
t.Fatalf("expected entry at %q, got entries: %v", expectedPath, entries)
}
changes, err := addDirectoryToChanges(context.Background(), repo, metadataDir, "checkpoint")
if err != nil {
t.Fatalf("addDirectoryToChanges failed: %v", err)
12 unmodified lines
}
}
func TestAddDirectoryToEntries_SkipsSymlinks(t *testing.T) {
func TestAddDirectoryToChanges_SkipsSymlinks(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
27 unmodified lines
t.Fatalf("failed to create symlink: %v", err)
}
entries := make(map[string]object.TreeEntry)
err = addDirectoryToEntriesWithAbsPath(context.Background(), repo, metadataDir, "checkpoint/", entries)
changes, err := addDirectoryToChanges(context.Background(), repo, metadataDir, "checkpoint/")
if err != nil {
t.Fatalf("addDirectoryToEntriesWithAbsPath failed: %v", err)
t.Fatalf("addDirectoryToChanges failed: %v", err)
}
paths := make(map[string]bool, len(changes))
for _, c := range changes {
paths[c.Path] = true
}
// Verify regular file was included
if _, ok := entries["checkpoint/regular.txt"]; !ok {
t.Error("regular.txt should be included in entries")
if !paths["checkpoint/regular.txt"] {
t.Error("regular.txt should be included in changes")
}
// Verify symlink was NOT included
if _, ok := entries["checkpoint/sneaky-link"]; ok {
t.Error("symlink should NOT be included in entries — this would allow reading files outside the metadata directory")
if paths["checkpoint/sneaky-link"] {
t.Error("symlink should NOT be included in changes — this would allow reading files outside the metadata directory")
}
if len(entries) != 1 {
t.Errorf("expected 1 entry, got %d", len(entries))
if len(changes) != 1 {
t.Errorf("expected 1 change, got %d", len(changes))
}
}
func TestAddDirectoryToEntries_SkipsSymlinkedDirectories(t *testing.T) {
func TestAddDirectoryToChanges_SkipsSymlinkedDirectories(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
28 unmodified lines
t.Fatalf("failed to create directory symlink: %v", err)
}
paths := make(map[string]bool, len(changes))
for _, c := range changes {
paths[c.Path] = true
}
// Verify files from the symlinked directory were NOT included
if _, ok := entries["checkpoint/evil-dir-link/secret.txt"]; ok {
if paths["checkpoint/evil-dir-link/secret.txt"] {
t.Error("files inside symlinked directory should NOT be included — this would allow reading files outside the metadata directory")
}
if len(entries) != 1 {
t.Errorf("expected 1 entry (regular.txt only), got %d: %v", len(entries), entries)
if len(changes) != 1 {
t.Errorf("expected 1 change (regular.txt only), got %d: %v", len(changes), changes)
}
}
Mcmd/entire/cli/checkpoint/checkpoint_test.go+32/-33
938 unmodified lines
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
942
943
944
10 unmodified lines
955
956
957
1019
958
959
960
961
962
963
964
965
966
967
938 unmodified lines
return hash, mode, nil
}
// addDirectoryToEntriesWithAbsPath recursively adds all files in a directory to the entries map.
func addDirectoryToEntriesWithAbsPath(ctx context.Context, repo *git.Repository, dirPathAbs, dirPathRel string, entries map[string]object.TreeEntry) error {
err := filepath.Walk(dirPathAbs, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip symlinks to prevent reading files outside the metadata directory.
// A symlink could point to sensitive files (e.g., /etc/passwd) which would
// then be captured in the checkpoint and stored in git history.
// NOTE: filepath.Walk uses os.Stat (follows symlinks), so info.Mode() never
// reports ModeSymlink. We use os.Lstat to check the entry itself.
// This check MUST come before IsDir() because Walk follows symlinked
// directories and would recurse into them otherwise.
linfo, lstatErr := os.Lstat(path)
if lstatErr != nil {
return fmt.Errorf("failed to lstat %s: %w", path, lstatErr)
}
if linfo.Mode()&os.ModeSymlink != 0 {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
if info.IsDir() {
return nil
}
// Calculate relative path within the directory, then join with dirPathRel for tree entry
relWithinDir, err := filepath.Rel(dirPathAbs, path)
if err != nil {
return fmt.Errorf("failed to get relative path for %s: %w", path, err)
}
// Prevent path traversal via unexpected relative paths outside the metadata dir.
if paths.IsRelativeTraversal(relWithinDir) {
return fmt.Errorf("path traversal detected: %s", relWithinDir)
}
treePath := filepath.ToSlash(filepath.Join(dirPathRel, relWithinDir))
// Use redacted blob creation for metadata files (transcripts, prompts, etc.)
// to ensure PII and secrets are redacted before writing to git.
blobHash, mode, err := createRedactedBlobFromFile(ctx, repo, path, treePath)
if err != nil {
return fmt.Errorf("failed to create blob for %s: %w", path, err)
}
entries[treePath] = object.TreeEntry{
Name: treePath,
Mode: mode,
Hash: blobHash,
}
return nil
})
if err != nil {
return fmt.Errorf("failed to walk directory %s: %w", dirPathAbs, err)
}
return nil
}
// treeNode represents a node in our tree structure.
type treeNode struct {
entries map[string]*treeNode // subdirectories
10 unmodified lines
return err
}
// Skip symlinks (same security rationale as addDirectoryToEntriesWithAbsPath)
// Skip symlinks to prevent reading files outside the metadata directory.
// A symlink could point to sensitive files (e.g., /etc/passwd) which would
// then be captured in the checkpoint and stored in git history.
// NOTE: filepath.Walk uses os.Stat (follows symlinks), so info.Mode() never
// reports ModeSymlink. We use os.Lstat to check the entry itself.
// This check MUST come before IsDir() because Walk follows symlinked
// directories and would recurse into them otherwise.
linfo, lstatErr := os.Lstat(path)
if lstatErr != nil {
return fmt.Errorf("failed to lstat %s: %w", path, lstatErr)
Mcmd/entire/cli/checkpoint/ephemeral.go+7/-62
236 unmodified lines
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
240
241
242
236 unmodified lines
return t.inner.Entries
}
// Unwrap returns the underlying *object.Tree.
func (t *FetchingTree) Unwrap() *object.Tree {
return t.inner
}
// Files returns a recursive file iterator from the underlying tree.
// Warning: after a treeless fetch, this iterator will fail when it tries
// to resolve blob objects. Use File() for on-demand blob fetching instead.
func (t *FetchingTree) Files() *object.FileIter {
return t.inner.Files()
}
// FileReader provides read access to files within a git tree.
// Both *object.Tree and *FetchingTree implement this interface.
type FileReader interface {
Mcmd/entire/cli/checkpoint/fetching_tree.go-12
19 unmodified lines
20
21
22
23
23
24
25
1602 unmodified lines
1628
1629
1630
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1631
1632
1633
19 unmodified lines
"github.com/entireio/cli/cmd/entire/cli/agent/codex"
"github.com/entireio/cli/cmd/entire/cli/agent/types"
"github.com/entireio/cli/cmd/entire/cli/checkpoint/id"
"github.com/entireio/cli/cmd/entire/cli/gitrepo"
"github.com/entireio/cli/cmd/entire/cli/jsonutil"
"github.com/entireio/cli/cmd/entire/cli/logging"
"github.com/entireio/cli/cmd/entire/cli/paths"
1602 unmodified lines
return content.Transcript, content.Metadata.SessionID, nil
}
// LookupSessionLog is a convenience function that opens the repository and retrieves
// a session log by checkpoint ID. This is the primary entry point for callers that
// do not already have a committed store instance.
// Returns ErrCheckpointNotFound if the checkpoint doesn't exist.
// Returns ErrNoTranscript if the checkpoint exists but has no transcript.
func LookupSessionLog(ctx context.Context, cpID id.CheckpointID) ([]byte, string, error) {
repo, err := gitrepo.OpenCurrent(ctx)
if err != nil {
return nil, "", fmt.Errorf("failed to open git repository: %w", err)
}
defer repo.Close()
stores, err := Open(ctx, repo, OpenOptions{})
if err != nil {
return nil, "", fmt.Errorf("open checkpoint store: %w", err)
}
return ReadRawSessionLogForCheckpoint(ctx, stores.Persistent, cpID)
}
// backfillSummary updates the summary field in the latest session's metadata.
// Returns ErrCheckpointNotFound if the checkpoint doesn't exist.
func (s *GitStore) backfillSummary(ctx context.Context, checkpointID id.CheckpointID, summary *Summary) error {
Mcmd/entire/cli/checkpoint/persistent.go-19
9 unmodified lines
10
11
12
13
14
15
16
17
13
14
15
9 unmodified lines
// prompts are stored in a single file.
const PromptSeparator = "\n\n---\n\n"
// JoinPrompts serializes prompts to prompt.txt format.
func JoinPrompts(prompts []string) string {
return strings.Join(prompts, PromptSeparator)
}
// SplitPromptContent deserializes prompt.txt content into individual prompts.
func SplitPromptContent(content string) []string {
if content == "" {
Mcmd/entire/cli/checkpoint/prompts.go-5
1
2
3
4
5
6
7
8
9
10
10
11
12
13
14
15
16
17
17
18
19
20
21
package checkpoint
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestJoinAndSplitPrompts_RoundTrip(t *testing.T) {
func TestSplitPromptContent_RoundTrip(t *testing.T) {
t.Parallel()
original := []string{
"first line\nwith newline",
"second prompt",
}
joined := JoinPrompts(original)
joined := strings.Join(original, PromptSeparator)
split := SplitPromptContent(joined)
require.Len(t, split, 2)
Mcmd/entire/cli/checkpoint/prompts_test.go+3/-2
1
2
3
4
5
4
5
6
9
7
8
9
112 unmodified lines
122
123
124
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
125
126
127
package remote
import (
"bufio"
"bytes"
"context"
"encoding/base64"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
112 unmodified lines
return nil
}
// CatFilesOptions configures a git cat-file --batch read.
type CatFilesOptions struct {
Specs []string // one or more object names or revspecs
Dir string // working directory (empty = CWD)
ExtraArgs []string // additional flags before --batch
}
// CatFileResult is the result of reading one cat-file batch spec.
type CatFileResult struct {
Content []byte
Missing bool
Err error
}
// CatFiles reads specs through git cat-file --batch.
func CatFiles(ctx context.Context, opts CatFilesOptions) map[string]CatFileResult {
specs := uniqueStrings(opts.Specs)
results := make(map[string]CatFileResult, len(specs))
if len(specs) == 0 {
return results
}
args := []string{"cat-file"}
args = append(args, opts.ExtraArgs...)
args = append(args, "--batch")
cmd := newCommand(ctx, args...)
if opts.Dir != "" {
cmd.Dir = opts.Dir
}
cmd.Stdin = strings.NewReader(strings.Join(specs, "\n") + "\n")
disableTerminalPrompt(cmd)
var stderr bytes.Buffer
cmd.Stderr = &stderr
output, err := cmd.Output()
if err != nil {
wrapped := catFilesError(err, stderr.String())
for _, spec := range specs {
results[spec] = CatFileResult{Err: wrapped}
}
return results
}
reader := bufio.NewReader(bytes.NewReader(output))
for i, spec := range specs {
result, parseErr := parseBlobBatchEntry(reader)
if parseErr != nil {
for _, s := range specs[i:] {
results[s] = CatFileResult{Err: parseErr}
}
break
}
results[spec] = result
}
return results
}
func parseBlobBatchEntry(reader *bufio.Reader) (CatFileResult, error) {
header, err := reader.ReadString('\n')
if err != nil {
return CatFileResult{}, fmt.Errorf("parse git cat-file batch: %w", err)
}
header = strings.TrimSuffix(header, "\n")
fields := strings.Fields(header)
if len(fields) == 2 && fields[1] == "missing" {
return CatFileResult{Missing: true}, nil
}
if len(fields) != 3 {
return CatFileResult{}, fmt.Errorf("parse git cat-file batch: unexpected header %q", header)
}
size, err := strconv.ParseInt(fields[2], 10, 64)
if err != nil {
return CatFileResult{}, fmt.Errorf("parse git cat-file batch: invalid size %q: %w", fields[2], err)
}
content := make([]byte, size)
if _, err := io.ReadFull(reader, content); err != nil {
return CatFileResult{}, fmt.Errorf("parse git cat-file batch: %w", err)
}
separator, err := reader.ReadByte()
if err != nil {
return CatFileResult{}, fmt.Errorf("parse git cat-file batch: %w", err)
}
if separator != '\n' {
return CatFileResult{}, fmt.Errorf("parse git cat-file batch: unexpected separator %q", separator)
}
if fields[1] != "blob" {
return CatFileResult{Err: fmt.Errorf("object %s is %s, want blob", fields[0], fields[1])}, nil
}
return CatFileResult{Content: content}, nil
}
func catFilesError(err error, stderr string) error {
msg := strings.TrimSpace(stderr)
if msg == "" {
return fmt.Errorf("git cat-file --batch: %w", err)
}
return fmt.Errorf("git cat-file --batch: %s: %w", msg, err)
}
func uniqueStrings(values []string) []string {
seen := make(map[string]struct{}, len(values))
unique := make([]string, 0, len(values))
for _, value := range values {
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
unique = append(unique, value)
}
return unique
}
// PushResult holds raw porcelain output from git push.
type PushResult struct {
Output string
Mcmd/entire/cli/checkpoint/remote/git.go-121
2 unmodified lines
3
4
5
6
6
7
8
473 unmodified lines
482
483
484
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
485
486
487
2 unmodified lines
import (
"context"
"encoding/base64"
"errors"
"fmt"
"net/http"
"net/http/httptest"
473 unmodified lines
})
}
func TestCatFilesReadsBlobAndMissingSpec(t *testing.T) {
t.Parallel()
repoDir := t.TempDir()
testutil.InitRepo(t, repoDir)
blobHash := writeRemoteGitBlob(t, repoDir, "metadata")
missingHash := "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
results := CatFiles(context.Background(), CatFilesOptions{
Specs: []string{blobHash, missingHash},
Dir: repoDir,
})
assert.Equal(t, []byte("metadata"), results[blobHash].Content)
assert.False(t, results[blobHash].Missing)
require.NoError(t, results[blobHash].Err)
assert.True(t, results[missingHash].Missing)
require.NoError(t, results[missingHash].Err)
}
func TestCatFilesErrorIncludesStderr(t *testing.T) {
t.Parallel()
err := catFilesError(errors.New("exit status 128"), "fatal: could not fetch blob\n")
assert.Contains(t, err.Error(), "fatal: could not fetch blob")
}
func writeRemoteGitBlob(t *testing.T, dir, content string) string {
t.Helper()
cmd := exec.CommandContext(t.Context(), "git", "hash-object", "-w", "--stdin")
cmd.Dir = dir
cmd.Stdin = strings.NewReader(content)
output, err := cmd.Output()
if err != nil {
t.Fatalf("git hash-object failed: %v", err)
}
return strings.TrimSpace(string(output))
}
func TestIsValidToken(t *testing.T) {
t.Parallel()
Mcmd/entire/cli/checkpoint/remote/git_test.go-43
277 unmodified lines
278
279
280
281
282
283
284
285
286
287
288
281
282
283
277 unmodified lines
return info, nil
}
func DeriveCheckpointURL(pushRemoteURL string, config *settings.CheckpointRemoteConfig) (string, error) {
info, err := gitremote.ParseURL(pushRemoteURL)
if err != nil {
return "", fmt.Errorf("cannot parse push remote URL: %w", err)
}
return deriveCheckpointURLFromInfo(info, config)
}
// isDerivableProtocol reports whether deriveCheckpointURLFromInfo can map the
// protocol to a checkpoint URL (i.e. it's a real git transport, not a remote
// helper scheme like entire:// or a local file://).
Mcmd/entire/cli/checkpoint/remote/util.go-8
6 unmodified lines
7
8
9
10
11
12
13
541 unmodified lines
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
6 unmodified lines
"path/filepath"
"testing"
"github.com/entireio/cli/cmd/entire/cli/settings"
"github.com/entireio/cli/cmd/entire/cli/testutil"
"github.com/go-git/go-git/v6"
)
541 unmodified lines
func fileURL(path string) string {
return "file://" + filepath.ToSlash(path)
}
// TestDeriveCheckpointURLFromInfo covers the push-remote to checkpoint-remote
// URL mapping (previously exercised cross-package via the removed
// DeriveCheckpointURL wrapper).
func TestDeriveCheckpointURLFromInfo(t *testing.T) {
t.Parallel()
tests := []struct {
name string
pushRemoteURL string
checkpointRepo string
want string
wantParseErr bool
wantDeriveErr bool
}{
{
name: "SSH push remote",
pushRemoteURL: "git@github.com:org/main-repo.git",
checkpointRepo: "org/checkpoints",
want: "git@github.com:org/checkpoints.git",
},
{
name: "HTTPS push remote",
pushRemoteURL: "https://github.com/org/main-repo.git",
checkpointRepo: "org/checkpoints",
want: "https://github.com/org/checkpoints.git",
},
{
name: "SSH protocol push remote",
pushRemoteURL: "ssh://git@github.com/org/main-repo.git",
checkpointRepo: "org/checkpoints",
want: "git@github.com:org/checkpoints.git",
},
{
name: "different host",
pushRemoteURL: "git@github.example.com:org/main-repo.git",
checkpointRepo: "org/checkpoints",
want: "git@github.example.com:org/checkpoints.git",
},
{
name: "HTTPS with non-standard port",
pushRemoteURL: "https://git.example.com:8443/org/main-repo.git",
checkpointRepo: "org/checkpoints",
want: "https://git.example.com:8443/org/checkpoints.git",
},
{
name: "SSH protocol with non-standard port",
pushRemoteURL: "ssh://git@git.example.com:2222/org/main-repo.git",
checkpointRepo: "org/checkpoints",
want: "ssh://git@git.example.com:2222/org/checkpoints.git",
},
{
name: "invalid push remote",
pushRemoteURL: "not-a-url",
wantParseErr: true,
},
{
name: "unsupported protocol",
pushRemoteURL: "file:///tmp/repo.git",
checkpointRepo: "org/checkpoints",
wantDeriveErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
info, err := ParseURL(tt.pushRemoteURL)
if tt.wantParseErr {
if err == nil {
t.Fatalf("ParseURL(%q) = nil error, want parse error", tt.pushRemoteURL)
}
return
}
if err != nil {
t.Fatalf("ParseURL(%q) error = %v", tt.pushRemoteURL, err)
}
config := &settings.CheckpointRemoteConfig{Provider: "github", Repo: tt.checkpointRepo}
got, err := deriveCheckpointURLFromInfo(info, config)
if tt.wantDeriveErr {
if err == nil {
t.Fatalf("deriveCheckpointURLFromInfo(%q) = %q, nil error; want error", tt.pushRemoteURL, got)
}
return
}
if err != nil {
t.Fatalf("deriveCheckpointURLFromInfo(%q) error = %v", tt.pushRemoteURL, err)
}
if got != tt.want {
t.Errorf("deriveCheckpointURLFromInfo(%q) = %q, want %q", tt.pushRemoteURL, got, tt.want)
}
})
}
}
Mcmd/entire/cli/checkpoint/remote/util_test.go+96
13 unmodified lines
14
15
16
17
18
19
20
17
18
19
20
21
22
23
24
33 unmodified lines
58
59
60
60
61
62
63
63
64
65
66
67
66
67
68
69
70
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
154 unmodified lines
258
259
260
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
261
262
263
13 unmodified lines
"github.com/go-git/go-git/v6/plumbing/object"
)
// TestBuildTreeWithChanges_EquivalenceWithFlattenRebuild verifies that
// the ApplyTreeChanges-based buildTreeWithChanges produces identical
// tree hashes to the old FlattenTree+BuildTreeFromEntries approach.
func TestBuildTreeWithChanges_EquivalenceWithFlattenRebuild(t *testing.T) { //nolint:paralleltest // t.Chdir requires non-parallel
// TestBuildTreeWithChanges_AppliesModificationsDeletionsAndMetadata verifies
// that the ApplyTreeChanges-based buildTreeWithChanges applies file
// modifications, deletions, and metadata-directory additions while leaving
// unrelated tree entries untouched.
func TestBuildTreeWithChanges_AppliesModificationsDeletionsAndMetadata(t *testing.T) { //nolint:paralleltest // t.Chdir requires non-parallel
repo, dir := setupTestRepo(t)
store := newEphemeralStore(repo, DefaultV1Refs())
33 unmodified lines
// Switch to repo dir so paths.WorktreeRoot() resolves correctly
t.Chdir(dir)
// --- New approach: ApplyTreeChanges (what buildTreeWithChanges now does) ---
newHash, err := store.buildTreeWithChanges(context.Background(), baseTreeHash, modifiedFiles, deletedFiles, metadataDir, metadataDirAbs)
if err != nil {
t.Fatalf("buildTreeWithChanges (new): %v", err)
t.Fatalf("buildTreeWithChanges: %v", err)
}
// --- Old approach: FlattenTree + modify map + BuildTreeFromEntries ---
oldHash := flattenRebuildTree(t, repo, baseTreeHash, modifiedFiles, deletedFiles, metadataDir, metadataDirAbs, dir)
newTree, err := repo.TreeObject(newHash)
if err != nil {
t.Fatalf("read new tree: %v", err)
}
if newHash != oldHash {
t.Errorf("tree hash mismatch: new=%s old=%s", newHash, oldHash)
// Modified files carry the new on-disk content.
for _, f := range modifiedFiles {
file, fileErr := newTree.File(f)
if fileErr != nil {
t.Fatalf("modified file %s missing from tree: %v", f, fileErr)
}
content, contentErr := file.Contents()
if contentErr != nil {
t.Fatalf("read %s: %v", f, contentErr)
}
if want := "modified content for " + f; content != want {
t.Errorf("%s content = %q, want %q", f, content, want)
}
}
// Deleted files are gone.
for _, f := range deletedFiles {
if _, fileErr := newTree.File(f); fileErr == nil {
t.Errorf("deleted file %s still present in tree", f)
}
}
// Metadata directory content was added at the tree-relative path.
if _, err := newTree.File(metadataDir + "/full.jsonl"); err != nil {
t.Errorf("metadata file missing from tree: %v", err)
}
// Unrelated entries are untouched.
if _, err := newTree.File("src/main.go"); err != nil {
t.Errorf("unrelated file src/main.go missing from tree: %v", err)
}
}
154 unmodified lines
return repo, dir
}
// flattenRebuildTree is the old FlattenTree+BuildTreeFromEntries approach
// for comparison in equivalence tests.
func flattenRebuildTree(
t *testing.T, repo *gogit.Repository,
baseTreeHash plumbing.Hash,
modifiedFiles, deletedFiles []string,
metadataDir, metadataDirAbs, repoRoot string,
) plumbing.Hash {
t.Helper()
baseTree, err := repo.TreeObject(baseTreeHash)
if err != nil {
t.Fatalf("tree: %v", err)
}
entries := make(map[string]object.TreeEntry)
if err := FlattenTree(repo, baseTree, "", entries); err != nil {
t.Fatalf("flatten: %v", err)
}
for _, file := range deletedFiles {
delete(entries, file)
}
for _, file := range modifiedFiles {
absPath := filepath.Join(repoRoot, file)
if !fileExists(absPath) {
delete(entries, file)
continue
}
blobHash, mode, blobErr := createBlobFromFile(repo, absPath)
if blobErr != nil {
continue
}
entries[file] = object.TreeEntry{
Name: file,
Mode: mode,
Hash: blobHash,
}
}
if metadataDir != "" && metadataDirAbs != "" {
if err := addDirectoryToEntriesWithAbsPath(context.Background(), repo, metadataDirAbs, metadataDir, entries); err != nil {
t.Fatalf("add metadata: %v", err)
}
}
hash, err := BuildTreeFromEntries(context.Background(), repo, entries)
if err != nil {
t.Fatalf("build tree: %v", err)
}
return hash
}
// flattenRebuildTaskMetadata is the old FlattenTree+BuildTreeFromEntries approach
// for addTaskMetadataToTree comparison.
func flattenRebuildTaskMetadata(
Mcmd/entire/cli/checkpoint/tree_surgery_equiv_test.go+40/-63
181 unmodified lines
182
183
184
185
186
187
188
189
190
191
192
193
194
185
186
187
181 unmodified lines
return u.Scheme + "://" + u.Host + u.Path
}
// ExtractOwnerFromRemoteURL extracts the owner component from a git remote URL.
// Returns an empty string if the URL cannot be parsed.
func ExtractOwnerFromRemoteURL(rawURL string) string {
info, err := ParseURL(rawURL)
if err != nil {
return ""
}
return info.Owner
}
// ResolveRemoteRepo returns the forge identifier, owner, and repo name for the
// given git remote. The forge is the short id used by the trails API ("gh",
// "et", ...); it is derived from the hostname for direct git URLs or from the
Mcmd/entire/cli/gitremote/gitremote.go-10
131 unmodified lines
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
135
136
137
131 unmodified lines
}
}
func TestExtractOwnerFromRemoteURL(t *testing.T) {
t.Parallel()
tests := []struct {
name string
url string
want string
}{
{"SSH", "git@github.com:org/repo.git", "org"},
{"HTTPS", "https://github.com/org/repo.git", "org"},
{"invalid", "not-a-url", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.want, ExtractOwnerFromRemoteURL(tt.url))
})
}
}
func TestInfo_CanonicalHost(t *testing.T) {
t.Parallel()
Mcmd/entire/cli/gitremote/gitremote_test.go-21
177 unmodified lines
178
179
180
181
182
181
182
183
184
185
186
177 unmodified lines
// Why not test through PrePush directly: resolvePushSettings derives the checkpoint
// URL from origin's protocol (SSH/HTTPS). Since integration tests use local file
// paths as remotes, remote.ParseURL fails and resolvePushSettings falls back to
// origin. The URL derivation logic is unit-tested in checkpoint_remote_test.go
// (TestDeriveCheckpointURL, TestResolvePushSettings_WithCheckpointRemote_*).
// origin. The URL derivation logic is unit-tested in checkpoint/remote/util_test.go
// (TestDeriveCheckpointURLFromInfo) and checkpoint_remote_test.go
// (TestResolvePushSettings_WithCheckpointRemote_*).
//
// The pushRefIfNeeded function (which PrePush calls with the resolved target)
// is exercised in push_common_test.go:TestPushRefIfNeeded_LocalBareRepo_PushesSuccessfully,
Mcmd/entire/cli/integration_test/remote_operations_test.go+3/-2
66 unmodified lines
67
68
69
70
71
72
73
74
75
66 unmodified lines
EnvStartingSHA+"="+opts.StartingSHA,
)
}
// IsInvestigateEnvEntry reports whether kv is a "KEY=VALUE" entry whose key
// is one of the ENTIRE_INVESTIGATE_* contract variables.
func IsInvestigateEnvEntry(kv string) bool {
return provenance.IsInvestigateEntry(kv)
}
Mcmd/entire/cli/investigate/env.go-6
33 unmodified lines
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
37
38
39
33 unmodified lines
}
}
// TestIsInvestigateEnvEntry pins the prefix-matching helper used to strip
// stale ENTIRE_INVESTIGATE_* entries before AppendInvestigateEnv writes new
// ones.
func TestIsInvestigateEnvEntry(t *testing.T) {
t.Parallel()
tests := []struct {
kv string
want bool
}{
{EnvSession + "=1", true},
{EnvAgent + "=claude-code", true},
{EnvRunID + "=abcdef012345", true},
{EnvTopic + "=topic", true},
{EnvFindingsDoc + "=/tmp/x", true},
{EnvStateDoc + "=/tmp/state.json", true},
{EnvStartingSHA + "=deadbeef", true},
{"PATH=/usr/bin", false},
{"HOME=/home/u", false},
{"ENTIRE_REVIEW_SESSION=1", false}, // review entries are not investigate entries
{"ENTIRE_INVESTIGATE_OTHER=1", false}, // unknown investigate-style key
{"NOT_ENTIRE_INVESTIGATE_SESSION", false},
}
for _, tc := range tests {
if got := IsInvestigateEnvEntry(tc.kv); got != tc.want {
t.Errorf("IsInvestigateEnvEntry(%q) = %v, want %v", tc.kv, got, tc.want)
}
}
}
// TestAppendInvestigateEnv_StripsStaleInvestigateAndReview pins the contract
// that AppendInvestigateEnv removes both ENTIRE_INVESTIGATE_* and
// ENTIRE_REVIEW_* entries before appending fresh values. The review-strip
Mcmd/entire/cli/investigate/env_test.go-29
4 unmodified lines
5
6
7
8
8
9
10
1 unmodified line
12
13
14
16
15
16
17
156 unmodified lines
174
175
176
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
177
178
179
4 unmodified lines
"encoding/json"
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"
"regexp"
1 unmodified line
"github.com/entireio/cli/cmd/entire/cli/checkpoint/id"
"github.com/entireio/cli/cmd/entire/cli/jsonutil"
"github.com/entireio/cli/cmd/entire/cli/logging"
"github.com/entireio/cli/cmd/entire/cli/provenance"
"github.com/entireio/cli/cmd/entire/cli/session"
)
156 unmodified lines
return &st, nil
}
// List returns all persisted run states. Returns nil (and no error) when the
// state directory does not exist.
func (s *StateStore) List(ctx context.Context) ([]*RunState, error) {
entries, err := os.ReadDir(s.dir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("read investigations directory: %w", err)
}
var states []*RunState
for _, entry := range entries {
if !entry.IsDir() {
continue
}
runID := entry.Name()
if err := validateRunID(runID); err != nil {
// Skip directories that don't match the run-ID format — they
// are not ours (e.g. the manifests/ sibling).
continue
}
st, loadErr := s.Load(ctx, runID)
if loadErr != nil {
// state.json exists but won't parse — surface so the user can
// inspect or `entire investigate clean <runID>`. Listing keeps
// going so one bad run doesn't hide the rest.
logging.Warn(ctx, "investigate: list skipped unreadable run state",
slog.String("run_id", runID),
slog.String("err", loadErr.Error()))
continue
}
if st == nil {
continue
}
states = append(states, st)
}
return states, nil
}
// Clear removes the persisted state for runID. Missing files are treated as a
// successful clear (no-op).
func (s *StateStore) Clear(ctx context.Context, runID string) error {
Mcmd/entire/cli/investigate/state.go-42
140 unmodified lines
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
144
145
146
140 unmodified lines
}
}
func TestStateStore_List(t *testing.T) {
t.Parallel()
dir := t.TempDir()
store := NewStateStoreWithDir(dir)
now := time.Now().UTC()
for _, runID := range []string{"abcdef012345", "0123456789ab"} {
if err := store.Save(context.Background(), &RunState{
RunID: runID,
Topic: "topic",
StartingSHA: "sha",
StartedAt: now,
UpdatedAt: now,
}); err != nil {
t.Fatalf("Save(%s): %v", runID, err)
}
}
// A non-run sibling in the directory (e.g. the manifests/ subdir or a
// stray file) must be ignored, not crash List.
if err := os.MkdirAll(filepath.Join(dir, "manifests"), 0o750); err != nil {
t.Fatalf("mkdir manifests sibling: %v", err)
}
if err := os.WriteFile(filepath.Join(dir, "garbage.txt"), []byte("x"), 0o600); err != nil {
t.Fatalf("write garbage: %v", err)
}
got, err := store.List(context.Background())
if err != nil {
t.Fatalf("List: %v", err)
}
if len(got) != 2 {
t.Errorf("List() returned %d entries, want 2", len(got))
}
seen := make(map[string]bool)
for _, st := range got {
seen[st.RunID] = true
}
if !seen["abcdef012345"] || !seen["0123456789ab"] {
t.Errorf("missing run IDs: %+v", seen)
}
}
func TestStateStore_ListEmptyDirectory(t *testing.T) {
t.Parallel()
dir := filepath.Join(t.TempDir(), "missing")
store := NewStateStoreWithDir(dir)
got, err := store.List(context.Background())
if err != nil {
t.Fatalf("List: %v", err)
}
if len(got) != 0 {
t.Errorf("List on missing dir should return empty, got %+v", got)
}
}
func TestStateStore_Clear(t *testing.T) {
t.Parallel()
Mcmd/entire/cli/investigate/state_test.go-57
28 unmodified lines
29
30
31
32
32
33
34
194 unmodified lines
229
230
231
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
232
233
234
28 unmodified lines
"path/filepath"
"strings"
"sync"
"time"
"github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/entireio/cli/cmd/entire/cli/validation"
194 unmodified lines
log(ctx, slog.LevelError, msg, attrs...)
}
// LogDuration logs a message with duration_ms calculated from the start time.
// The level parameter specifies the log level (use slog.LevelDebug, slog.LevelInfo, etc).
// Designed for use with defer:
//
// defer logging.LogDuration(ctx, slog.LevelInfo, "operation completed", time.Now())
//
// Or with additional attrs:
//
// defer logging.LogDuration(ctx, slog.LevelDebug, "hook executed", start,
// slog.String("hook", hookName),
// slog.Bool("success", true),
// )
func LogDuration(ctx context.Context, level slog.Level, msg string, start time.Time, attrs ...any) {
durationMs := time.Since(start).Milliseconds()
// Prepend duration_ms to attrs
allAttrs := make([]any, 0, len(attrs)+1)
allAttrs = append(allAttrs, slog.Int64("duration_ms", durationMs))
allAttrs = append(allAttrs, attrs...)
log(ctx, level, msg, allAttrs...)
}
// log is the internal logging function that extracts context values and logs.
//
// The read lock is held across l.Log so Init/Close cannot close logBufWriter
Mcmd/entire/cli/logging/logger.go-24
10 unmodified lines
11
12
13
14
14
15
16
425 unmodified lines
442
443
444
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
445
446
447
10 unmodified lines
"strings"
"sync"
"testing"
"time"
)
// Test constants to avoid goconst warnings
425 unmodified lines
}
}
func TestLogDuration(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)
initGitRepo(t, tmpDir)
sessionID := "2025-01-15-duration-test"
err := Init(context.Background(), sessionID)
if err != nil {
t.Fatalf("Init() error = %v", err)
}
ctx := WithSession(context.Background(), "context-session") // Will be ignored, global takes precedence
ctx = WithComponent(ctx, testComponent)
// Simulate some work
start := time.Now().Add(-100 * time.Millisecond) // Fake 100ms ago
LogDuration(ctx, slog.LevelInfo, "operation completed", start,
slog.String("hook", "pre-push"),
slog.Bool("success", true),
)
Close()
// Read log file
content, err := os.ReadFile(testLogFilePath(tmpDir))
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
// Parse as JSON
var logEntry map[string]interface{}
if err := json.Unmarshal(content, &logEntry); err != nil {
t.Fatalf("Log output is not valid JSON: %v\nContent: %s", err, content)
}
// Verify duration_ms is present and reasonable
durationMs, ok := logEntry["duration_ms"].(float64)
if !ok {
t.Fatalf("Expected duration_ms to be a number, got %T: %v", logEntry["duration_ms"], logEntry["duration_ms"])
}
if durationMs < 90 || durationMs > 200 {
t.Errorf("Expected duration_ms around 100, got %v", durationMs)
}
// session_id comes from Init(), not context
if logEntry["session_id"] != sessionID {
t.Errorf("Expected session_id='%s' (from Init), got %v", sessionID, logEntry["session_id"])
}
if logEntry["component"] != testComponent {
t.Errorf("Expected component='%s', got %v", testComponent, logEntry["component"])
}
if logEntry["hook"] != "pre-push" {
t.Errorf("Expected hook='pre-push', got %v", logEntry["hook"])
}
if logEntry["success"] != true {
t.Errorf("Expected success=true, got %v", logEntry["success"])
}
if logEntry["level"] != levelINFO {
t.Errorf("Expected level='%s', got %v", levelINFO, logEntry["level"])
}
}
func TestLogging_ContextSessionID_WhenNoGlobalSet(t *testing.T) {
// Reset any global state to ensure no global session ID
resetLogger()
Mcmd/entire/cli/logging/logger_test.go-65
5 unmodified lines
6
7
8
9
9
10
11
177 unmodified lines
189
190
191
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
192
193
194
5 unmodified lines
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
177 unmodified lines
return p
}
// nonAlphanumericRegex matches any non-alphanumeric character
var nonAlphanumericRegex = regexp.MustCompile(`[^a-zA-Z0-9]`)
// SanitizePathForClaude converts a path to Claude's project directory format.
// Claude replaces any non-alphanumeric character with a dash.
func SanitizePathForClaude(path string) string {
return nonAlphanumericRegex.ReplaceAllString(path, "-")
}
// GetClaudeProjectDir returns the directory where Claude stores session transcripts
// for the given repository path.
//
// In test environments, set ENTIRE_TEST_CLAUDE_PROJECT_DIR to override the default location.
func GetClaudeProjectDir(repoPath string) (string, error) {
override := os.Getenv("ENTIRE_TEST_CLAUDE_PROJECT_DIR")
if override != "" {
return override, nil
}
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get home directory: %w", err)
}
projectDir := SanitizePathForClaude(repoPath)
return filepath.Join(homeDir, ".claude", "projects", projectDir), nil
}
// SessionMetadataDirFromSessionID returns the path to a session's metadata directory
// for the given Entire session ID. The sessionID must be the full, already date-prefixed
// Entire session identifier as stored on disk, not an agent-specific or raw Claude ID.
Mcmd/entire/cli/paths/paths.go-29
1
2
3
4
4
5
6
89 unmodified lines
96
97
98
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
99
100
101
package paths
import (
"os"
"path/filepath"
"runtime"
"testing"
89 unmodified lines
}
}
func TestSanitizePathForClaude(t *testing.T) {
tests := []struct {
input string
want string
}{
{"/Users/test/myrepo", "-Users-test-myrepo"},
{"/home/user/project", "-home-user-project"},
{"simple", "simple"},
{"/path/with spaces/here", "-path-with-spaces-here"},
{"/path.with.dots/file", "-path-with-dots-file"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := SanitizePathForClaude(tt.input)
if got != tt.want {
t.Errorf("SanitizePathForClaude(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestGetClaudeProjectDir_Override(t *testing.T) {
// Set the override environment variable
t.Setenv("ENTIRE_TEST_CLAUDE_PROJECT_DIR", "/tmp/test-claude-project")
result, err := GetClaudeProjectDir("/some/repo/path")
if err != nil {
t.Fatalf("GetClaudeProjectDir() error = %v", err)
}
if result != "/tmp/test-claude-project" {
t.Errorf("GetClaudeProjectDir() = %q, want %q", result, "/tmp/test-claude-project")
}
}
func TestGetClaudeProjectDir_Default(t *testing.T) {
// Ensure env var is not set by setting it to empty string
t.Setenv("ENTIRE_TEST_CLAUDE_PROJECT_DIR", "")
result, err := GetClaudeProjectDir("/Users/test/myrepo")
if err != nil {
t.Fatalf("GetClaudeProjectDir() error = %v", err)
}
homeDir, err := os.UserHomeDir()
if err != nil {
t.Fatalf("os.UserHomeDir() error = %v", err)
}
expected := filepath.Join(homeDir, ".claude", "projects", "-Users-test-myrepo")
if result != expected {
t.Errorf("GetClaudeProjectDir() = %q, want %q", result, expected)
}
}
func TestToRelativePath_MSYSPaths(t *testing.T) {
t.Parallel()
if runtime.GOOS != "windows" {
Mcmd/entire/cli/paths/paths_test.go-57
77 unmodified lines
78
79
80
81
81
82
83
84
77 unmodified lines
// validateClusterHost rejects a cluster host that is anything other than a
// bare DNS name or IP with an optional :port. The host is concatenated as
// "https://"+host into the clone URL and the STS audience
// (auth.RepoScopedToken), so a value carrying URL metacharacters can redirect
// (entireclient/repocreds), so a value carrying URL metacharacters can redirect
// the request — and the repo-scoped basic-auth token it carries — somewhere
// other than the intended cluster. Classic case:
// `aws-us-east-2.entire.io@evil.com`, which Go's URL parser reads as
Mcmd/entire/cli/repo_mirror.go+1/-1
23 unmodified lines
24
25
26
27
27
28
29
30
23 unmodified lines
//
// The owner/repo capture groups are restricted to GitHub's real identifier
// charset rather than a permissive "anything but slash". owner/repo flow
// unescaped into the STS audience (auth.RepoScopedToken) and the clone URL;
// unescaped into the STS audience (entireclient/repocreds) and the clone URL;
// a loose pattern would admit ?, #, %, .. and control chars, letting a name
// like `repo?bypass=1` smuggle a query string or `repo#x` truncate the path.
// GitHub owners are [A-Za-z0-9-] and repos are [A-Za-z0-9._-], so matching
Mcmd/entire/cli/repo_mirror_probe.go+1/-1
1553 unmodified lines
1554
1555
1556
1557
1558
1559
1560
1557
1558
1559
1553 unmodified lines
}
}
func reviewAgentRunTokenEnricher(worktreeRoot, headSHA string) func(context.Context, reviewtypes.AgentRun) reviewtypes.AgentRun {
return reviewAgentRunTokenEnricherForRuns(worktreeRoot, headSHA, nil)
}
func reviewAgentRunTokenEnricherForRuns(worktreeRoot, headSHA string, planned []reviewtypes.AgentRun) func(context.Context, reviewtypes.AgentRun) reviewtypes.AgentRun {
var mu sync.Mutex
usedSessions := map[string]bool{}
Mcmd/entire/cli/review/cmd.go-4
250 unmodified lines
251
252
253
254
255
256
257
258
259
260
261
262
263
264
254
255
256
250 unmodified lines
return hydrateReviewSummaryTokensFromStates(ctx, worktreeRoot, headSHA, summary, states, lookup), nil
}
func hydrateReviewAgentRunTokensFromStates(
ctx context.Context,
worktreeRoot string,
headSHA string,
run reviewtypes.AgentRun,
states []*session.State,
lookup agentTypeLookup,
) reviewtypes.AgentRun {
return hydrateReviewAgentRunTokensFromStatesWithUsed(ctx, worktreeRoot, headSHA, run, states, lookup, map[string]bool{})
}
func hydrateReviewAgentRunTokensFromStatesWithUsed(
ctx context.Context,
worktreeRoot string,
Mcmd/entire/cli/review/manifest.go-11
140 unmodified lines
141
142
143
144
144
145
146
147
751 unmodified lines
899
900
901
902
903
902
903
904
905
906
140 unmodified lines
t.Fatalf("tokens = {%d %d}, want {12 5}", tokens.In, tokens.Out)
}
gotRun := reviewAgentRunTokenEnricher(repoRoot, "abc123")(ctx, reviewtypes.AgentRun{
gotRun := reviewAgentRunTokenEnricherForRuns(repoRoot, "abc123", nil)(ctx, reviewtypes.AgentRun{
Name: manifestTestCodexAgent,
StartedAt: started,
})
751 unmodified lines
},
}
freshA := hydrateReviewAgentRunTokensFromStates(context.Background(), "/repo", "abc123", run, states, nil)
freshB := hydrateReviewAgentRunTokensFromStates(context.Background(), "/repo", "abc123", run, states, nil)
freshA := hydrateReviewAgentRunTokensFromStatesWithUsed(context.Background(), "/repo", "abc123", run, states, nil, map[string]bool{})
freshB := hydrateReviewAgentRunTokensFromStatesWithUsed(context.Background(), "/repo", "abc123", run, states, nil, map[string]bool{})
if freshA.Tokens.In != 10 || freshB.Tokens.In != 10 {
t.Fatalf("fresh-map setup changed: tokens = %d/%d, want both newest session token count 10", freshA.Tokens.In, freshB.Tokens.In)
}
Mcmd/entire/cli/review/manifest_test.go+3/-3
10 unmodified lines
11
12
13
14
14
15
16
17
18
19
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
20
21
22
10 unmodified lines
"github.com/entireio/cli/cmd/entire/cli/checkpoint/remote"
"github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/entireio/cli/cmd/entire/cli/settings"
"github.com/entireio/cli/cmd/entire/cli/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDeriveCheckpointURL(t *testing.T) {
t.Parallel()
tests := []struct {
name string
pushRemoteURL string
checkpointRepo string
want string
wantErr bool
}{
{
name: "SSH push remote",
pushRemoteURL: "git@github.com:org/main-repo.git",
checkpointRepo: "org/checkpoints",
want: "git@github.com:org/checkpoints.git",
},
{
name: "HTTPS push remote",
pushRemoteURL: "https://github.com/org/main-repo.git",
checkpointRepo: "org/checkpoints",
want: "https://github.com/org/checkpoints.git",
},
{
name: "SSH protocol push remote",
pushRemoteURL: "ssh://git@github.com/org/main-repo.git",
checkpointRepo: "org/checkpoints",
want: "git@github.com:org/checkpoints.git",
},
{
name: "different host",
pushRemoteURL: "git@github.example.com:org/main-repo.git",
checkpointRepo: "org/checkpoints",
want: "git@github.example.com:org/checkpoints.git",
},
{
name: "HTTPS with non-standard port",
pushRemoteURL: "https://git.example.com:8443/org/main-repo.git",
checkpointRepo: "org/checkpoints",
want: "https://git.example.com:8443/org/checkpoints.git",
},
{
name: "SSH protocol with non-standard port",
pushRemoteURL: "ssh://git@git.example.com:2222/org/main-repo.git",
checkpointRepo: "org/checkpoints",
want: "ssh://git@git.example.com:2222/org/checkpoints.git",
},
{
name: "invalid push remote",
pushRemoteURL: "not-a-url",
checkpointRepo: "org/checkpoints",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
config := &settings.CheckpointRemoteConfig{Provider: "github", Repo: tt.checkpointRepo}
got, err := remote.DeriveCheckpointURL(tt.pushRemoteURL, config)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestIsURL(t *testing.T) {
t.Parallel()
Mcmd/entire/cli/strategy/checkpoint_remote_test.go-70