detached 81538e1

fix(subagents): preserve projection portability

commit
81538e1cca9697d3b0b44c2505cbbd9166442d29
author
brice · · 2026-08-18 15:16 UTC
parent
5bcc0f8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +39 -4
M packages/agent/subagents/resident_journal.go +8 -4
@@ -9,6 +9,7 @@ import (
99 "io"
1010 "os"
1111 "path/filepath"
12+ "runtime"
1213 "sort"
1314 "strings"
1415 "sync"
@@ -1129,10 +1130,13 @@ func writeResidentProjection(dir, name string, value any) error {
11291130 }
11301131
11311132 func residentProjectionCurrent(path string, data []byte) bool {
1132- current, err := os.ReadFile(path)
1133- if err != nil || !bytes.Equal(current, data) {
1133+ info, err := os.Lstat(path)
1134+ if err != nil || !info.Mode().IsRegular() {
1135+ return false
1136+ }
1137+ if runtime.GOOS != "windows" && info.Mode().Perm() != 0o600 {
11341138 return false
11351139 }
1136- info, err := os.Stat(path)
1137- return err == nil && info.Mode().Perm() == 0o600
1140+ current, err := os.ReadFile(path)
1141+ return err == nil && bytes.Equal(current, data)
11381142 }
@@ -9,6 +9,7 @@ import (
9 "io"9 "io"
10 "os"10 "os"
11 "path/filepath"11 "path/filepath"
12+ "runtime"
12 "sort"13 "sort"
13 "strings"14 "strings"
14 "sync"15 "sync"
@@ -1129,10 +1130,13 @@ func writeResidentProjection(dir, name string, value any) error {
1129 }1130 }
1130 1131
1131 func residentProjectionCurrent(path string, data []byte) bool {1132 func residentProjectionCurrent(path string, data []byte) bool {
1132- current, err := os.ReadFile(path)1133+ info, err := os.Lstat(path)
1133- if err != nil || !bytes.Equal(current, data) {1134+ if err != nil || !info.Mode().IsRegular() {
1135+ return false
1136+ }
1137+ if runtime.GOOS != "windows" && info.Mode().Perm() != 0o600 {
1134 return false1138 return false
1135 }1139 }
1136- info, err := os.Stat(path)1140+ current, err := os.ReadFile(path)
1137- return err == nil && info.Mode().Perm() == 0o6001141+ return err == nil && bytes.Equal(current, data)
1138 }1142 }
M packages/agent/subagents/resident_journal_test.go +31 -0
@@ -218,6 +218,37 @@ func TestResidentProjectionSkipsIdenticalRewrite(t *testing.T) {
218218 }
219219 }
220220
221+func TestResidentProjectionReplacesSymlinkInsteadOfFollowingIt(t *testing.T) {
222+ if runtime.GOOS == "windows" {
223+ t.Skip("symlink creation may require elevated privileges on Windows")
224+ }
225+ dir := t.TempDir()
226+ metadata := ResidentMetadata{Version: residentJournalVersion, ID: "projection-child", SessionID: "projection-session", State: ResidentIdle}
227+ data, err := json.Marshal(metadata)
228+ if err != nil {
229+ t.Fatal(err)
230+ }
231+ target := filepath.Join(dir, "target.json")
232+ if err := os.WriteFile(target, data, 0o600); err != nil {
233+ t.Fatal(err)
234+ }
235+ path := filepath.Join(dir, residentMetadataName)
236+ if err := os.Symlink(target, path); err != nil {
237+ t.Fatal(err)
238+ }
239+
240+ if err := writeResidentMetadata(dir, metadata); err != nil {
241+ t.Fatal(err)
242+ }
243+ info, err := os.Lstat(path)
244+ if err != nil {
245+ t.Fatal(err)
246+ }
247+ if !info.Mode().IsRegular() {
248+ t.Fatalf("projection mode = %v, want regular file", info.Mode())
249+ }
250+}
251+
221252 func TestResidentProjectionRepairsPermissionsForIdenticalData(t *testing.T) {
222253 if runtime.GOOS == "windows" {
223254 t.Skip("POSIX permission bits are not enforced on Windows")
@@ -218,6 +218,37 @@ func TestResidentProjectionSkipsIdenticalRewrite(t *testing.T) {
218 }218 }
219 }219 }
220 220
221+func TestResidentProjectionReplacesSymlinkInsteadOfFollowingIt(t *testing.T) {
222+ if runtime.GOOS == "windows" {
223+ t.Skip("symlink creation may require elevated privileges on Windows")
224+ }
225+ dir := t.TempDir()
226+ metadata := ResidentMetadata{Version: residentJournalVersion, ID: "projection-child", SessionID: "projection-session", State: ResidentIdle}
227+ data, err := json.Marshal(metadata)
228+ if err != nil {
229+ t.Fatal(err)
230+ }
231+ target := filepath.Join(dir, "target.json")
232+ if err := os.WriteFile(target, data, 0o600); err != nil {
233+ t.Fatal(err)
234+ }
235+ path := filepath.Join(dir, residentMetadataName)
236+ if err := os.Symlink(target, path); err != nil {
237+ t.Fatal(err)
238+ }
239+
240+ if err := writeResidentMetadata(dir, metadata); err != nil {
241+ t.Fatal(err)
242+ }
243+ info, err := os.Lstat(path)
244+ if err != nil {
245+ t.Fatal(err)
246+ }
247+ if !info.Mode().IsRegular() {
248+ t.Fatalf("projection mode = %v, want regular file", info.Mode())
249+ }
250+}
251+
221 func TestResidentProjectionRepairsPermissionsForIdenticalData(t *testing.T) {252 func TestResidentProjectionRepairsPermissionsForIdenticalData(t *testing.T) {
222 if runtime.GOOS == "windows" {253 if runtime.GOOS == "windows" {
223 t.Skip("POSIX permission bits are not enforced on Windows")254 t.Skip("POSIX permission bits are not enforced on Windows")

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close