Skip to content

Commit 9d0e1f2

Browse files
Ivan GorshkovIvan Gorshkov
authored andcommitted
tests and added added git reset --hard HEAD after capturing patches but before clean SDK generation
1 parent 1dc7bcd commit 9d0e1f2

File tree

4 files changed

+327
-26
lines changed

4 files changed

+327
-26
lines changed

cmd/customcode.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818

1919
type RegisterCustomCodeFlags struct {
2020
Show bool `json:"show"`
21-
Resolve bool `json:"resolve"`
2221
Apply bool `json:"apply-only"`
2322
ApplyReverse bool `json:"apply-reverse"`
2423
LatestHash bool `json:"latest-hash"`
@@ -44,10 +43,6 @@ var registerCustomCodeCmd = &model.ExecutableCommand[RegisterCustomCodeFlags]{
4443
Shorthand: "s",
4544
Description: "show custom code patches",
4645
},
47-
flag.BooleanFlag{
48-
Name: "resolve",
49-
Description: "enter conflict resolution mode after a failed generation",
50-
},
5146
flag.BooleanFlag{
5247
Name: "apply-only",
5348
Shorthand: "a",
@@ -103,11 +98,6 @@ func registerCustomCode(ctx context.Context, flags RegisterCustomCodeFlags) erro
10398
return nil
10499
}
105100

106-
// If --resolve flag is provided, enter conflict resolution mode
107-
if flags.Resolve {
108-
return registercustomcode.ResolveCustomCodeConflicts(ctx)
109-
}
110-
111101
// If --apply-only flag is provided, only apply existing patches
112102
if flags.Apply {
113103
wf, _, err := utils.GetWorkflowAndDir()

integration/customcode_singletarget_test.go

Lines changed: 313 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,31 @@ func TestCustomCode(t *testing.T) {
3434
t.Parallel()
3535
testCustomCodeConflictResolutionAcceptOurs(t, speakeasyBinary)
3636
})
37+
38+
t.Run("SequentialPatchesAppliedWithRegenerationBetween", func(t *testing.T) {
39+
t.Parallel()
40+
testCustomCodeSequentialPatchesAppliedWithRegenerationBetween(t, speakeasyBinary)
41+
})
42+
43+
t.Run("SequentialPatchesAppliedWithoutRegenerationBetween", func(t *testing.T) {
44+
t.Parallel()
45+
testCustomCodeSequentialPatchesAppliedWithoutRegenerationBetween(t, speakeasyBinary)
46+
})
47+
48+
t.Run("ConflictDetectionDuringCustomCodeRegistration", func(t *testing.T) {
49+
t.Parallel()
50+
testCustomCodeConflictDetectionDuringRegistration(t, speakeasyBinary)
51+
})
52+
53+
t.Run("NewFilePreservation", func(t *testing.T) {
54+
t.Parallel()
55+
testCustomCodeNewFilePreservation(t, speakeasyBinary)
56+
})
57+
58+
t.Run("NewFileDeletion", func(t *testing.T) {
59+
t.Parallel()
60+
testCustomCodeNewFileDeletion(t, speakeasyBinary)
61+
})
3762
}
3863

3964
// testCustomCodeBasicWorkflow tests basic custom code registration and reapplication
@@ -60,14 +85,14 @@ func testCustomCodeConflictResolution(t *testing.T, speakeasyBinary string) {
6085
specPath := filepath.Join(temp, "customcodespec.yaml")
6186
modifyLineInFile(t, specPath, 477, " description: 'spec change'")
6287

63-
// Run speakeasy run to regenerate - this should produce a conflict
64-
runRegeneration(t, speakeasyBinary, temp, false)
65-
66-
// Run customcode --resolve to enter conflict resolution mode
67-
resolveCmd := exec.Command(speakeasyBinary, "customcode", "--resolve", "--output", "console")
68-
resolveCmd.Dir = temp
69-
resolveOutput, resolveErr := resolveCmd.CombinedOutput()
70-
require.NoError(t, resolveErr, "customcode --resolve should succeed: %s", string(resolveOutput))
88+
// Run speakeasy run to regenerate - this should detect conflict and automatically enter resolution mode
89+
// The process should exit with code 2 after setting up conflict resolution
90+
regenCmd := exec.Command(speakeasyBinary, "run", "-t", "all", "--pinned", "--skip-compile")
91+
regenCmd.Dir = temp
92+
regenOutput, regenErr := regenCmd.CombinedOutput()
93+
require.Error(t, regenErr, "speakeasy run should exit with error after detecting conflicts: %s", string(regenOutput))
94+
require.Contains(t, string(regenOutput), "CUSTOM CODE CONFLICTS DETECTED", "Output should show conflict detection banner")
95+
require.Contains(t, string(regenOutput), "Entering automatic conflict resolution mode", "Output should indicate automatic resolution mode")
7196

7297
// Check for conflict markers in the file
7398
getUserByNameContent, err := os.ReadFile(getUserByNamePath)
@@ -112,14 +137,14 @@ func testCustomCodeConflictResolutionAcceptOurs(t *testing.T, speakeasyBinary st
112137
specPath := filepath.Join(temp, "customcodespec.yaml")
113138
modifyLineInFile(t, specPath, 477, " description: 'spec change'")
114139

115-
// Run speakeasy run to regenerate - this should produce a conflict
116-
runRegeneration(t, speakeasyBinary, temp, false)
117-
118-
// Run customcode --resolve to enter conflict resolution mode
119-
resolveCmd := exec.Command(speakeasyBinary, "customcode", "--resolve", "--output", "console")
120-
resolveCmd.Dir = temp
121-
resolveOutput, resolveErr := resolveCmd.CombinedOutput()
122-
require.NoError(t, resolveErr, "customcode --resolve should succeed: %s", string(resolveOutput))
140+
// Run speakeasy run to regenerate - this should detect conflict and automatically enter resolution mode
141+
// The process should exit with code 2 after setting up conflict resolution
142+
regenCmd := exec.Command(speakeasyBinary, "run", "-t", "all", "--pinned", "--skip-compile")
143+
regenCmd.Dir = temp
144+
regenOutput, regenErr := regenCmd.CombinedOutput()
145+
require.Error(t, regenErr, "speakeasy run should exit with error after detecting conflicts: %s", string(regenOutput))
146+
require.Contains(t, string(regenOutput), "CUSTOM CODE CONFLICTS DETECTED", "Output should show conflict detection banner")
147+
require.Contains(t, string(regenOutput), "Entering automatic conflict resolution mode", "Output should indicate automatic resolution mode")
123148

124149
// Check for conflict markers in the file
125150
getUserByNameContent, err := os.ReadFile(getUserByNamePath)
@@ -173,6 +198,137 @@ func testCustomCodeConflictResolutionAcceptOurs(t *testing.T, speakeasyBinary st
173198
require.NotContains(t, string(finalContent), "// custom code", "Custom code should not be present after accepting ours")
174199
}
175200

201+
// testCustomCodeSequentialPatchesAppliedWithRegenerationBetween tests that patches can be updated
202+
// by registering a first patch, regenerating, then registering a second patch on the same line
203+
func testCustomCodeSequentialPatchesAppliedWithRegenerationBetween(t *testing.T, speakeasyBinary string) {
204+
temp := setupSDKGeneration(t, speakeasyBinary, "customcodespec.yaml")
205+
206+
getUserByNamePath := filepath.Join(temp, "models", "operations", "getuserbyname.go")
207+
208+
// Step 1: Register first patch
209+
registerCustomCode(t, speakeasyBinary, temp, getUserByNamePath, 10, "\t// first custom code")
210+
211+
// Step 2: Verify first patch applies correctly on regeneration
212+
runRegeneration(t, speakeasyBinary, temp, true)
213+
verifyCustomCodePresent(t, getUserByNamePath, "// first custom code")
214+
215+
// Step 2b: Commit the regenerated code with first patch applied
216+
gitCommit(t, temp, "regenerated with first patch")
217+
218+
// Step 3: Modify the same line with different content (second patch)
219+
modifyLineInFile(t, getUserByNamePath, 10, "\t// second custom code - updated")
220+
221+
// Step 4: Register second patch (should update existing patch)
222+
customCodeCmd := exec.Command(speakeasyBinary, "customcode", "--output", "console")
223+
customCodeCmd.Dir = temp
224+
customCodeOutput, customCodeErr := customCodeCmd.CombinedOutput()
225+
require.NoError(t, customCodeErr, "customcode command should succeed for second patch: %s", string(customCodeOutput))
226+
227+
// Step 5: Verify patch file was updated (not appended)
228+
patchFile := filepath.Join(temp, ".speakeasy", "patches", "custom-code.diff")
229+
patchContent, err := os.ReadFile(patchFile)
230+
require.NoError(t, err, "Failed to read patch file")
231+
require.Contains(t, string(patchContent), "second custom code - updated", "Patch should contain second custom code")
232+
require.NotContains(t, string(patchContent), "first custom code", "Patch should not contain first custom code")
233+
234+
// Step 6: Verify second patch applies correctly on final regeneration
235+
runRegeneration(t, speakeasyBinary, temp, true)
236+
237+
// Step 7: Verify final file contains only second patch content
238+
finalContent, err := os.ReadFile(getUserByNamePath)
239+
require.NoError(t, err, "Failed to read getuserbyname.go after final regeneration")
240+
require.Contains(t, string(finalContent), "// second custom code - updated", "File should contain second custom code")
241+
require.NotContains(t, string(finalContent), "// first custom code", "File should not contain first custom code")
242+
}
243+
244+
// testCustomCodeSequentialPatchesAppliedWithoutRegenerationBetween tests that patches can be updated
245+
// by registering a first patch, then immediately registering a second patch on the same line without regenerating
246+
func testCustomCodeSequentialPatchesAppliedWithoutRegenerationBetween(t *testing.T, speakeasyBinary string) {
247+
temp := setupSDKGeneration(t, speakeasyBinary, "customcodespec.yaml")
248+
249+
getUserByNamePath := filepath.Join(temp, "models", "operations", "getuserbyname.go")
250+
251+
// Step 1: Register first patch
252+
registerCustomCode(t, speakeasyBinary, temp, getUserByNamePath, 10, "\t// first custom code")
253+
254+
// Step 2: Immediately modify the same line with different content (NO regeneration between)
255+
modifyLineInFile(t, getUserByNamePath, 10, "\t// second custom code - updated")
256+
257+
// Step 3: Register second patch (should update existing patch)
258+
customCodeCmd := exec.Command(speakeasyBinary, "customcode", "--output", "console")
259+
customCodeCmd.Dir = temp
260+
customCodeOutput, customCodeErr := customCodeCmd.CombinedOutput()
261+
require.NoError(t, customCodeErr, "customcode command should succeed for second patch: %s", string(customCodeOutput))
262+
263+
// Step 4: Verify patch file was updated (not appended)
264+
patchFile := filepath.Join(temp, ".speakeasy", "patches", "custom-code.diff")
265+
patchContent, err := os.ReadFile(patchFile)
266+
require.NoError(t, err, "Failed to read patch file")
267+
require.Contains(t, string(patchContent), "second custom code - updated", "Patch should contain second custom code")
268+
require.NotContains(t, string(patchContent), "first custom code", "Patch should not contain first custom code")
269+
270+
// Step 5: Verify second patch applies correctly on regeneration
271+
runRegeneration(t, speakeasyBinary, temp, true)
272+
273+
// Step 6: Verify final file contains only second patch content
274+
finalContent, err := os.ReadFile(getUserByNamePath)
275+
require.NoError(t, err, "Failed to read getuserbyname.go after final regeneration")
276+
require.Contains(t, string(finalContent), "// second custom code - updated", "File should contain second custom code")
277+
require.NotContains(t, string(finalContent), "// first custom code", "File should not contain first custom code")
278+
}
279+
280+
// testCustomCodeConflictDetectionDuringRegistration tests that conflicts are detected during customcode registration
281+
// when the old patch conflicts with new changes
282+
func testCustomCodeConflictDetectionDuringRegistration(t *testing.T, speakeasyBinary string) {
283+
temp := setupSDKGeneration(t, speakeasyBinary, "customcodespec.yaml")
284+
285+
getUserByNamePath := filepath.Join(temp, "models", "operations", "getuserbyname.go")
286+
287+
// Step 1: Modify the file
288+
modifyLineInFile(t, getUserByNamePath, 10, "\t// first custom code")
289+
290+
// Step 2: Register first patch
291+
customCodeCmd := exec.Command(speakeasyBinary, "customcode", "--output", "console")
292+
customCodeCmd.Dir = temp
293+
customCodeOutput, customCodeErr := customCodeCmd.CombinedOutput()
294+
require.NoError(t, customCodeErr, "customcode command should succeed: %s", string(customCodeOutput))
295+
296+
// Step 3: Run and commit
297+
runRegeneration(t, speakeasyBinary, temp, true)
298+
gitCommit(t, temp, "regenerated with first patch")
299+
300+
// Step 4: Modify the same line again
301+
modifyLineInFile(t, getUserByNamePath, 10, "\t// second custom code - conflicting")
302+
303+
// Step 5: Modify the spec to change the same line (this will cause conflict during registration)
304+
specPath := filepath.Join(temp, "customcodespec.yaml")
305+
modifyLineInFile(t, specPath, 477, " description: 'spec change for conflict'")
306+
307+
// Step 5b: Commit only the spec
308+
gitAddCmd := exec.Command("git", "add", specPath)
309+
gitAddCmd.Dir = temp
310+
gitAddOutput, gitAddErr := gitAddCmd.CombinedOutput()
311+
require.NoError(t, gitAddErr, "git add spec should succeed: %s", string(gitAddOutput))
312+
313+
gitCommitCmd := exec.Command("git", "commit", "-m", "update spec")
314+
gitCommitCmd.Dir = temp
315+
gitCommitOutput, gitCommitErr := gitCommitCmd.CombinedOutput()
316+
require.NoError(t, gitCommitErr, "git commit spec should succeed: %s", string(gitCommitOutput))
317+
318+
// Step 6: Register custom code - should fail with conflict error
319+
customCodeCmd = exec.Command(speakeasyBinary, "customcode", "--output", "console")
320+
customCodeCmd.Dir = temp
321+
customCodeOutput, customCodeErr = customCodeCmd.CombinedOutput()
322+
323+
// Step 7: Validate error - conflict happens when applying existing patch
324+
require.Error(t, customCodeErr, "customcode command should fail due to conflicts: %s", string(customCodeOutput))
325+
outputStr := string(customCodeOutput)
326+
// The conflict occurs when applying the existing patch (not the new patch)
327+
// because the spec changed and the old patch no longer applies cleanly
328+
require.Contains(t, outputStr, "failed to apply existing patch", "Error message should mention failed to apply existing patch")
329+
require.Contains(t, outputStr, "with conflicts", "Error message should mention conflicts")
330+
}
331+
176332
// buildSpeakeasyBinaryOnce builds the speakeasy binary and returns the path to it
177333
func buildSpeakeasyBinaryOnce(t *testing.T) string {
178334
t.Helper()
@@ -424,3 +580,144 @@ func verifyCustomCodePresent(t *testing.T, filePath, expectedContent string) {
424580
require.NoError(t, err, "Failed to read file: %s", filePath)
425581
require.Contains(t, string(content), expectedContent, "Custom code should be present in file")
426582
}
583+
584+
// testCustomCodeNewFilePreservation tests that custom code registration preserves entirely new files
585+
func testCustomCodeNewFilePreservation(t *testing.T, speakeasyBinary string) {
586+
temp := setupSDKGeneration(t, speakeasyBinary, "customcodespec.yaml")
587+
588+
// Create a new file with helper functions
589+
helperFilePath := filepath.Join(temp, "utils", "helper.go")
590+
helperFileContent := `package utils
591+
592+
import "fmt"
593+
594+
// FormatUserID formats a user ID with a prefix
595+
func FormatUserID(id int64) string {
596+
return fmt.Sprintf("user_%d", id)
597+
}
598+
599+
// ValidateUserID validates that a user ID is positive
600+
func ValidateUserID(id int64) bool {
601+
return id > 0
602+
}
603+
`
604+
605+
// Create the utils directory
606+
err := os.MkdirAll(filepath.Join(temp, "utils"), 0o755)
607+
require.NoError(t, err, "Failed to create utils directory")
608+
609+
// Write the helper file
610+
err = os.WriteFile(helperFilePath, []byte(helperFileContent), 0o644)
611+
require.NoError(t, err, "Failed to write helper file")
612+
613+
// Stage the new file so git diff HEAD can capture it
614+
gitAddCmd := exec.Command("git", "add", helperFilePath)
615+
gitAddCmd.Dir = temp
616+
gitAddOutput, gitAddErr := gitAddCmd.CombinedOutput()
617+
require.NoError(t, gitAddErr, "git add should succeed: %s", string(gitAddOutput))
618+
619+
// Register custom code
620+
customCodeCmd := exec.Command(speakeasyBinary, "customcode", "--output", "console")
621+
customCodeCmd.Dir = temp
622+
customCodeOutput, customCodeErr := customCodeCmd.CombinedOutput()
623+
require.NoError(t, customCodeErr, "customcode command should succeed: %s", string(customCodeOutput))
624+
625+
// Verify patch file was created
626+
patchFile := filepath.Join(temp, ".speakeasy", "patches", "custom-code.diff")
627+
_, err = os.Stat(patchFile)
628+
require.NoError(t, err, "patch file should exist at %s", patchFile)
629+
630+
// Verify the file exists after registration (before regeneration)
631+
_, err = os.Stat(helperFilePath)
632+
require.NoError(t, err, "Helper file should exist after registration")
633+
634+
// Run speakeasy run to regenerate the SDK
635+
// This should apply the patch and preserve the new file
636+
runRegeneration(t, speakeasyBinary, temp, true)
637+
638+
// Verify the new file still exists after regeneration
639+
_, err = os.Stat(helperFilePath)
640+
require.NoError(t, err, "Helper file should exist after regeneration")
641+
642+
// Verify the file contents are preserved exactly
643+
verifyCustomCodePresent(t, helperFilePath, "FormatUserID")
644+
verifyCustomCodePresent(t, helperFilePath, "ValidateUserID")
645+
verifyCustomCodePresent(t, helperFilePath, "package utils")
646+
647+
// Read the entire file and verify exact content match
648+
actualContent, err := os.ReadFile(helperFilePath)
649+
require.NoError(t, err, "Failed to read helper file after regeneration")
650+
require.Equal(t, helperFileContent, string(actualContent), "Helper file content should be preserved exactly")
651+
}
652+
653+
// testCustomCodeNewFileDeletion tests that deleting a custom file is properly registered and persisted
654+
func testCustomCodeNewFileDeletion(t *testing.T, speakeasyBinary string) {
655+
temp := setupSDKGeneration(t, speakeasyBinary, "customcodespec.yaml")
656+
657+
// Create a new file with helper functions
658+
helperFilePath := filepath.Join(temp, "utils", "helper.go")
659+
helperFileContent := `package utils
660+
661+
import "fmt"
662+
663+
// FormatUserID formats a user ID with a prefix
664+
func FormatUserID(id int64) string {
665+
return fmt.Sprintf("user_%d", id)
666+
}
667+
`
668+
669+
// Create the utils directory
670+
err := os.MkdirAll(filepath.Join(temp, "utils"), 0o755)
671+
require.NoError(t, err, "Failed to create utils directory")
672+
673+
// Write the helper file
674+
err = os.WriteFile(helperFilePath, []byte(helperFileContent), 0o644)
675+
require.NoError(t, err, "Failed to write helper file")
676+
677+
// Stage the new file so git diff HEAD can capture it
678+
gitAddCmd := exec.Command("git", "add", helperFilePath)
679+
gitAddCmd.Dir = temp
680+
_, err = gitAddCmd.CombinedOutput()
681+
require.NoError(t, err, "git add should succeed")
682+
683+
// Register custom code (registers the new file)
684+
customCodeCmd := exec.Command(speakeasyBinary, "customcode", "--output", "console")
685+
customCodeCmd.Dir = temp
686+
customCodeOutput, customCodeErr := customCodeCmd.CombinedOutput()
687+
require.NoError(t, customCodeErr, "customcode command should succeed: %s", string(customCodeOutput))
688+
689+
// Verify patch file was created
690+
patchFile := filepath.Join(temp, ".speakeasy", "patches", "custom-code.diff")
691+
_, err = os.Stat(patchFile)
692+
require.NoError(t, err, "patch file should exist after registering new file")
693+
694+
// Regenerate and verify the file is preserved
695+
runRegeneration(t, speakeasyBinary, temp, true)
696+
_, err = os.Stat(helperFilePath)
697+
require.NoError(t, err, "Helper file should exist after first regeneration")
698+
699+
// Commit the regeneration so the file becomes part of HEAD
700+
gitCommitCmd := exec.Command("git", "commit", "-am", "regeneration with custom file")
701+
gitCommitCmd.Dir = temp
702+
_, err = gitCommitCmd.CombinedOutput()
703+
require.NoError(t, err, "git commit should succeed after regeneration")
704+
705+
// Now delete the file
706+
err = os.Remove(helperFilePath)
707+
require.NoError(t, err, "Failed to delete helper file")
708+
709+
// Register the deletion
710+
customCodeCmd = exec.Command(speakeasyBinary, "customcode", "--output", "console")
711+
customCodeCmd.Dir = temp
712+
customCodeOutput, customCodeErr = customCodeCmd.CombinedOutput()
713+
require.NoError(t, customCodeErr, "customcode command should succeed after deletion: %s", string(customCodeOutput))
714+
715+
// Verify patch file was removed (no custom code remaining)
716+
_, err = os.Stat(patchFile)
717+
require.True(t, os.IsNotExist(err), "patch file should not exist after deleting the only custom file")
718+
719+
// Regenerate and verify the file remains deleted
720+
runRegeneration(t, speakeasyBinary, temp, true)
721+
_, err = os.Stat(helperFilePath)
722+
require.True(t, os.IsNotExist(err), "Helper file should not exist after regeneration with deletion registered")
723+
}

0 commit comments

Comments
 (0)