fix: make Codex hooks transactional

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 03:22:58 -04:00
1 parent 415a884efe
commit f0e4694da2
17 files changed
+2550 -506

No files matched your search

@@ -0,0 +1,106 @@
package plugins
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
)
func transactionTestChanges(t *testing.T) (string, string, []*fileChange) {
t.Helper()
directory := t.TempDir()
firstPath := filepath.Join(directory, "first.json")
secondPath := filepath.Join(directory, "second.json")
for _, item := range []struct {
path string
content string
}{
{firstPath, "first-original"},
{secondPath, "second-original"},
} {
if err := os.WriteFile(item.path, []byte(item.content), 0600); err != nil {
t.Fatalf("write transaction fixture: %v", err)
}
}
first, err := prepareFileChange(firstPath, "first test file", []byte("first-next"), 0600)
if err != nil {
t.Fatalf("prepare first transaction change: %v", err)
}
second, err := prepareFileChange(secondPath, "second test file", []byte("second-next"), 0600)
if err != nil {
t.Fatalf("prepare second transaction change: %v", err)
}
return firstPath, secondPath, []*fileChange{first, second}
}
func requirePreservedRollback(t *testing.T, directory string) {
t.Helper()
entries, err := os.ReadDir(directory)
if err != nil {
t.Fatalf("read transaction directory: %v", err)
}
var rollbackPath string
for _, entry := range entries {
if strings.HasSuffix(entry.Name(), ".rollback") {
if rollbackPath != "" {
t.Fatal("multiple rollback files remain")
}
rollbackPath = filepath.Join(directory, entry.Name())
}
}
if rollbackPath == "" {
t.Fatal("original rollback file was removed")
}
content, err := os.ReadFile(rollbackPath)
if err != nil {
t.Fatalf("read preserved rollback file: %v", err)
}
if string(content) != "first-original" {
t.Fatalf("preserved rollback content = %q", content)
}
}
func TestTransactionPreservesBackupAfterConcurrentCommittedChange(t *testing.T) {
firstPath, secondPath, changes := transactionTestChanges(t)
rename := func(source, destination string) error {
if destination == secondPath && strings.HasSuffix(source, ".tmp") {
if err := os.Remove(firstPath); err != nil {
return err
}
if err := os.WriteFile(firstPath, []byte("external"), 0600); err != nil {
return err
}
return errors.New("simulated second commit failure")
}
return os.Rename(source, destination)
}
err := writeChanges(changes, "Test transaction", rename)
if err == nil || !strings.Contains(err.Error(), "original content preserved at") {
t.Fatalf("transaction error = %v", err)
}
current, readErr := os.ReadFile(firstPath)
if readErr != nil || string(current) != "external" {
t.Fatalf("external content = %q, %v", current, readErr)
}
requirePreservedRollback(t, filepath.Dir(firstPath))
}
func TestTransactionPreservesBackupAfterRollbackRenameFailure(t *testing.T) {
firstPath, secondPath, changes := transactionTestChanges(t)
rename := func(source, destination string) error {
if destination == secondPath && strings.HasSuffix(source, ".tmp") {
return errors.New("simulated second commit failure")
}
if destination == firstPath && strings.HasSuffix(source, ".rollback") {
return errors.New("simulated rollback failure")
}
return os.Rename(source, destination)
}
err := writeChanges(changes, "Test transaction", rename)
if err == nil || !strings.Contains(err.Error(), "original content preserved at") {
t.Fatalf("transaction error = %v", err)
}
requirePreservedRollback(t, filepath.Dir(firstPath))
}