Skip to content

Commit d51fe65

Browse files
committed
refactor(integration): split explain_test.go into smaller files
1 parent 0d22f01 commit d51fe65

2 files changed

Lines changed: 216 additions & 203 deletions

File tree

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
//go:build integration
2+
3+
package integration
4+
5+
import (
6+
"encoding/json"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"testing"
11+
12+
"github.com/GrayCodeAI/trace/cli/execx"
13+
"github.com/GrayCodeAI/trace/cli/jsonutil"
14+
"github.com/GrayCodeAI/trace/cli/paths"
15+
"github.com/GrayCodeAI/trace/cli/testutil"
16+
"github.com/stretchr/testify/require"
17+
18+
"github.com/go-git/go-git/v6"
19+
"github.com/go-git/go-git/v6/plumbing"
20+
)
21+
22+
// TestExplain_CheckpointV2SucceedsAfterTreelessFetch is the v2 mirror —
23+
// guards V2GitStore's read path against the same blob-missing regression.
24+
// Required because v2 will be enabled by default soon and reaches the
25+
// same Tree.File() trap as v1.
26+
func TestExplain_CheckpointV2SucceedsAfterTreelessFetch(t *testing.T) {
27+
t.Parallel()
28+
env := NewFeatureBranchEnv(t)
29+
30+
env.PatchSettings(map[string]any{
31+
"strategy_options": map[string]any{
32+
"checkpoints_v2": true,
33+
"push_v2_refs": true,
34+
},
35+
})
36+
37+
bareURL := env.SetupBareRemote()
38+
checkpointID := createAndPushCheckpoint(t, env, "treeless_v2.go", "Treeless v2 prompt")
39+
40+
cloneDir := setupTreelessClone(t, bareURL, "+"+paths.V2MainRefName+":"+paths.V2MainRefName)
41+
writeV2Settings(t, cloneDir)
42+
requireBlobMissing(t, cloneDir, checkpointID, true /* v2 */)
43+
44+
output := runExplainInDir(t, cloneDir, checkpointID)
45+
require.Contains(t, output, "Treeless v2 prompt",
46+
"explain should succeed against v2 with blobs absent locally")
47+
}
48+
49+
// createAndPushCheckpoint runs a session-create-stop cycle in env and
50+
// pushes the resulting checkpoint to origin. Returns the checkpoint ID.
51+
func createAndPushCheckpoint(t *testing.T, env *TestEnv, fileName, prompt string) string {
52+
t.Helper()
53+
session := env.NewSession()
54+
transcriptPath := session.CreateTranscript(prompt, []FileChange{
55+
{Path: fileName, Content: "package treeless"},
56+
})
57+
require.NoError(t, env.SimulateUserPromptSubmitWithPromptAndTranscriptPath(session.ID, prompt, transcriptPath))
58+
env.WriteFile(fileName, "package treeless")
59+
env.GitAdd(fileName)
60+
require.NoError(t, env.SimulateStop(session.ID, transcriptPath))
61+
env.GitCommitWithShadowHooks("Add "+fileName, fileName)
62+
cpID := env.GetLatestCheckpointID()
63+
require.NotEmpty(t, cpID, "expected a checkpoint after condensation")
64+
env.RunPrePush("origin")
65+
return cpID
66+
}
67+
68+
// setupTreelessClone creates a fresh git repo in a fresh TempDir, fetches
69+
// the given refspec from bareURL with --filter=blob:none --depth=1 (so
70+
// trees but no blobs land locally), and writes a minimal trace settings
71+
// file pointing at bareURL as the checkpoint_remote. Returns the new dir.
72+
//
73+
// Note: the bare and the fetch must go through the smart protocol for
74+
// --filter to be honored; the default local-path transport optimization
75+
// copies packs verbatim and ignores filters. We set
76+
// uploadpack.allowFilter=true on the bare and use a file:// URL with
77+
// protocol.file.allow=always to force the smart path.
78+
func setupTreelessClone(t *testing.T, barePath, refspec string) string {
79+
t.Helper()
80+
gitEnv := testutil.GitIsolatedEnv()
81+
enableFilterOnBare(t, barePath, gitEnv)
82+
83+
cloneDir := t.TempDir()
84+
fileURL := "file://" + barePath
85+
86+
for _, args := range [][]string{
87+
{"init", "-q"},
88+
{"-c", "protocol.file.allow=always", "fetch", "--filter=blob:none", "--depth=1", "--no-tags", fileURL, refspec},
89+
} {
90+
cmd := exec.CommandContext(t.Context(), "git", args...)
91+
cmd.Dir = cloneDir
92+
cmd.Env = gitEnv
93+
if out, err := cmd.CombinedOutput(); err != nil {
94+
t.Fatalf("git %v failed: %v\n%s", args, err, out)
95+
}
96+
}
97+
98+
require.NoError(t, writeMinimalTraceSettings(cloneDir, barePath))
99+
return cloneDir
100+
}
101+
102+
// enableFilterOnBare sets uploadpack.allowFilter=true on the bare repo so
103+
// that --filter=blob:none on fetch is honored.
104+
func enableFilterOnBare(t *testing.T, barePath string, gitEnv []string) {
105+
t.Helper()
106+
cmd := exec.CommandContext(t.Context(), "git", "-C", barePath, "config", "uploadpack.allowFilter", "true")
107+
cmd.Env = gitEnv
108+
if out, err := cmd.CombinedOutput(); err != nil {
109+
t.Fatalf("failed to set uploadpack.allowFilter on bare: %v\n%s", err, out)
110+
}
111+
cmd = exec.CommandContext(t.Context(), "git", "-C", barePath, "config", "uploadpack.allowAnySHA1InWant", "true")
112+
cmd.Env = gitEnv
113+
if out, err := cmd.CombinedOutput(); err != nil {
114+
t.Fatalf("failed to set uploadpack.allowAnySHA1InWant on bare: %v\n%s", err, out)
115+
}
116+
}
117+
118+
// writeMinimalTraceSettings writes the smallest valid settings.json that
119+
// configures the manual-commit strategy with filtered_fetches enabled and
120+
// a custom checkpoint_remote URL — the partial-clone setup that triggered
121+
// the original bug.
122+
func writeMinimalTraceSettings(dir, bareURL string) error {
123+
traceDir := filepath.Join(dir, ".trace")
124+
if err := os.MkdirAll(traceDir, 0o755); err != nil {
125+
return err
126+
}
127+
settings := map[string]any{
128+
"enabled": true,
129+
"local_dev": true,
130+
"strategy": "manual-commit",
131+
"strategy_options": map[string]any{
132+
"filtered_fetches": true,
133+
"checkpoint_remote": map[string]any{
134+
"provider": "url",
135+
"url": bareURL,
136+
},
137+
},
138+
}
139+
data, err := jsonutil.MarshalIndentWithNewline(settings, "", " ")
140+
if err != nil {
141+
return err
142+
}
143+
return os.WriteFile(filepath.Join(traceDir, paths.SettingsFileName), data, 0o644)
144+
}
145+
146+
// writeV2Settings overlays checkpoints_v2 enablement on the settings written
147+
// by writeMinimalTraceSettings.
148+
func writeV2Settings(t *testing.T, dir string) {
149+
t.Helper()
150+
settingsPath := filepath.Join(dir, ".trace", paths.SettingsFileName)
151+
data, err := os.ReadFile(settingsPath)
152+
require.NoError(t, err)
153+
154+
var settings map[string]any
155+
require.NoError(t, json.Unmarshal(data, &settings))
156+
157+
opts, _ := settings["strategy_options"].(map[string]any)
158+
opts["checkpoints_v2"] = true
159+
settings["strategy_options"] = opts
160+
161+
updated, err := jsonutil.MarshalIndentWithNewline(settings, "", " ")
162+
require.NoError(t, err)
163+
require.NoError(t, os.WriteFile(settingsPath, updated, 0o644))
164+
}
165+
166+
// runExplainInDir runs `trace explain --checkpoint <id>` in dir and
167+
// returns combined output. Fails the test if the command errors. Uses
168+
// execx.NonInteractive (project rule for spawning the trace binary in
169+
// tests) so the child has no controlling terminal.
170+
func runExplainInDir(t *testing.T, dir, checkpointID string) string {
171+
t.Helper()
172+
cmd := execx.NonInteractive(t.Context(), getTestBinary(), "explain", "--checkpoint", checkpointID)
173+
cmd.Dir = dir
174+
cmd.Env = testutil.GitIsolatedEnv()
175+
out, err := cmd.CombinedOutput()
176+
if err != nil {
177+
t.Fatalf("explain failed: %v\n%s", err, out)
178+
}
179+
return string(out)
180+
}
181+
182+
// requireBlobMissing asserts that at least one metadata blob for the
183+
// checkpoint is genuinely absent from the local object store. Confirms the
184+
// treeless-clone setup actually reproduces the bug-triggering state — if
185+
// every blob were locally available, the test would pass without
186+
// exercising the fix.
187+
func requireBlobMissing(t *testing.T, dir, checkpointID string, isV2 bool) {
188+
t.Helper()
189+
repo, err := git.PlainOpen(dir)
190+
require.NoError(t, err)
191+
192+
var ref *plumbing.Reference
193+
if isV2 {
194+
ref, err = repo.Reference(plumbing.ReferenceName(paths.V2MainRefName), true)
195+
} else {
196+
ref, err = repo.Reference(plumbing.NewBranchReferenceName(paths.MetadataBranchName), true)
197+
}
198+
require.NoError(t, err, "metadata ref should exist after treeless fetch")
199+
200+
commit, err := repo.CommitObject(ref.Hash())
201+
require.NoError(t, err)
202+
rootTree, err := commit.Tree()
203+
require.NoError(t, err)
204+
cpSubtree, err := rootTree.Tree(checkpointID[:2] + "/" + checkpointID[2:])
205+
require.NoError(t, err, "cp subtree should be navigable from local trees")
206+
207+
for _, entry := range cpSubtree.Entries {
208+
if !entry.Mode.IsFile() {
209+
continue
210+
}
211+
if _, err := repo.BlobObject(entry.Hash); err != nil {
212+
return // confirmed: at least one blob is missing
213+
}
214+
}
215+
t.Fatalf("expected at least one metadata blob to be missing in fresh treeless clone (cp=%s, v2=%v)", checkpointID, isV2)
216+
}

0 commit comments

Comments
 (0)