Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
172 commits
Select commit Hold shift + click to select a range
6e0a112
gitstate: add NUL-safe worktree family inventory
zzet Aug 20, 2026
c016267
graph: align overlay reads across point, batched, and bulk paths
zzet Aug 20, 2026
2189d15
graphview: add view identity, capabilities, selectors, and leases
zzet Aug 20, 2026
037addd
build: require Go 1.27 and golangci-lint 2.13.1
zzet Aug 20, 2026
1c8c76d
store: add checkout lifecycle catalog tables and accessors
zzet Aug 21, 2026
658d5d1
indexer: add content sources for checkouts and committed trees
zzet Aug 21, 2026
30cf350
reconcile: add checkout lifecycle core with evidence-gated cleanup
zzet Aug 21, 2026
e7c51e4
graph: extend the reader with kind-scoped iteration
zzet Aug 21, 2026
6cf28f2
mcp: serve request reads through the session view
zzet Aug 21, 2026
4ad9d33
store: add edge view generation column
zzet Aug 21, 2026
c7e8158
store: split the handle from the core for generation pinning
zzet Aug 21, 2026
c188fce
mcp: finish routing analysis and review reads through the request view
zzet Aug 21, 2026
cb9a80d
graph: recognize file nodes in overlay ownership checks
zzet Aug 21, 2026
66b5d97
mcp: stamp workspace identity onto overlay layer nodes
zzet Aug 21, 2026
27f27fc
store: key payload sidecars by view generation
zzet Aug 21, 2026
5b64c19
store: give nodes and edges per-generation identity
zzet Aug 21, 2026
9e6fd8f
store: scope payload statements to the handle generation
zzet Aug 21, 2026
6af1303
indexer: route checkout lifecycle through one owner
zzet Aug 21, 2026
95752cd
gitstate: sample checkout dirty state with a stable fingerprint
zzet Aug 21, 2026
853de38
store: add ownership masks and the payload generation lifecycle
zzet Aug 21, 2026
f4ceb0c
graph: compose overlays through a layer contract with uniform node vi…
zzet Aug 21, 2026
f379158
graphview: materialize routed views over persisted generation layers
zzet Aug 21, 2026
f996a60
indexer: read file content through an optional source
zzet Aug 21, 2026
1c1dcde
indexer: build sparse generations for commit and dirty layers
zzet Aug 21, 2026
4eb45b5
store: list view generations with a bounded filter
zzet Aug 21, 2026
6f1e655
indexer: coordinate checkout layer builds with atomic route flips
zzet Aug 21, 2026
9cf697c
mcp: serve routed checkout views by session and selector
zzet Aug 21, 2026
d3ad20e
query: enumerate search candidates from every generation in the view
zzet Aug 21, 2026
a40005a
mcp: bind worktree sessions to their family scope
zzet Aug 21, 2026
d099b92
graph: supersede base edges by their recorded file
zzet Aug 21, 2026
1ff262b
indexer: widen the build closure to a reference-driven fixed point
zzet Aug 21, 2026
d9fede5
indexer: build ref views with coalesced reclaimable claims
zzet Aug 22, 2026
61a224a
mcp: serve committed-tree views for ref and commit selectors
zzet Aug 22, 2026
65dbbf0
indexer: execute promotion, demotion, and primary designation
zzet Aug 22, 2026
b1b2994
mcp: add checkout administration surfaces
zzet Aug 22, 2026
6d9ea65
hooks: resolve probe views through the control socket
zzet Aug 22, 2026
a6792a5
mcp: evaluate operation capabilities against the served view
zzet Aug 22, 2026
ba4612a
indexer: scope text search and language servers to the checkout
zzet Aug 22, 2026
c228aaf
daemon: instrument the view lifecycle with bounded metrics
zzet Aug 22, 2026
1a46dd5
build: give the full test suite headroom in ci
zzet Aug 22, 2026
1749550
reconcile: purge a checkout's layers before withdrawing its route
zzet Aug 22, 2026
8abf7e2
indexer: stop a coordinator's loop before a test drives its cycles
zzet Aug 22, 2026
52f43c4
Take the worst producer state when a view stacks generations
zzet Aug 22, 2026
b42c1d3
Carry the view rider in every response wire format
zzet Aug 22, 2026
2b0d1e8
Render a detached HEAD as its short commit in the families listing
zzet Aug 22, 2026
b37e952
Refuse a working-tree layer built for a head the commit slot does not…
zzet Aug 22, 2026
c1cf3f8
Resolve file paths in the checkout a request's view reads
zzet Aug 22, 2026
52c0343
Keep a ref view build alive for as long as it runs
zzet Aug 22, 2026
0be5449
Bound a generation's unresolved enumeration to its own rows
zzet Aug 23, 2026
e680045
Keep a generation build out of the base topology writer
zzet Aug 23, 2026
67bbd9d
Hold view builds until the daemon has warmed up
zzet Aug 23, 2026
2bd2582
Report a checkout coordinator live while its loop is running
zzet Aug 23, 2026
ee84eec
Merge remote-tracking branch 'origin/main' into HEAD
zzet Aug 24, 2026
9e45ad3
Take the catalog's write gate under the caller's context
zzet Aug 24, 2026
034bce6
Answer a ref-view selection off the read pool while its build runs
zzet Aug 24, 2026
dff5ac1
Give each payload generation its own resolver lane
zzet Aug 24, 2026
2e2ba28
Serve a CLI query run from a linked worktree of a tracked family
zzet Aug 24, 2026
390836d
Resolve a worktree's family without naming the worktree its own main …
zzet Aug 24, 2026
4b9f3b9
Relay the checkout verbs through the family of an unbound worktree
zzet Aug 24, 2026
8351e20
Fail open when the checkout-coverage probe cannot answer in time
zzet Aug 24, 2026
3774939
Pass the path to the path-identity seam
zzet Aug 24, 2026
6a28e63
Compare worktree path fixtures through the production path fold
zzet Aug 24, 2026
7f2bf53
Run the checkout, view and git-state tests on the windows runner
zzet Aug 24, 2026
4dfa218
docs: document worktree views, checkout administration and their conf…
zzet Aug 24, 2026
6b75306
Charge checkout language-server admission by server footprint
zzet Aug 24, 2026
a533421
Bind the checkout workspace weights to the registry's registration
zzet Aug 24, 2026
75324c0
Cover the whole-weight credit on the departed-checkout eviction path
zzet Aug 24, 2026
9fd8df8
Refuse a checkout workspace admission the budget cannot reach before …
zzet Aug 24, 2026
49ee3ba
Answer daemon status from a cached aggregate while the controller is …
zzet Aug 24, 2026
4dca3cf
docs: state that a ref view and a checkout at one commit build separa…
zzet Aug 24, 2026
2fe5435
Key the review-family overlay fixture the way the store spells paths
zzet Aug 24, 2026
2df2c2b
Match a departed worktree's admin directory through the path fold
zzet Aug 25, 2026
8a1f190
Stop the checkout build loops before the binding-chain test seeds a r…
zzet Aug 25, 2026
339f513
Merge remote-tracking branch 'origin/main' into HEAD
zzet Aug 25, 2026
b54762f
Take the resolver lane for contract-edge reconciliation
zzet Aug 25, 2026
44217e3
Assert the real-git reconcile contract per path-evidence platform
zzet Aug 25, 2026
ebdf863
Reach the janitor's removal path by each platform's removal evidence
zzet Aug 25, 2026
fd8fe13
Key the probe fixture's corpus the way the store spells paths
zzet Aug 25, 2026
f8cc8c1
Stop the unrouted probe's reconciliation before the fixture tears down
zzet Aug 25, 2026
68e18a8
Pin the worktree fixture's git isolation to the platform null device
zzet Aug 25, 2026
ce0b549
Stop the catalog server fixture's build loops before its store closes
zzet Aug 25, 2026
eebe4c4
Fix worktree overlay lifecycle and routing
zzet Aug 26, 2026
3fdef4c
Fence graph construction tests from nested worktrees
zzet Aug 26, 2026
1aba704
Close checkout route materialization retirement race
zzet Aug 26, 2026
8ce2cbf
docs: record worktree overlay validation findings
zzet Aug 26, 2026
a9d90dc
test: isolate command query telemetry
zzet Aug 26, 2026
88f50ed
bench: measure generation-scoped reads
zzet Aug 26, 2026
a1cb947
fix(indexer): cancel obsolete checkout builds
zzet Aug 26, 2026
739aecf
perf(store): use generation partial indexes
zzet Aug 26, 2026
ff26281
fix(query): preserve overlay precedence during refill
zzet Aug 26, 2026
3879cf9
fix(mcp): publish structured view selectors
zzet Aug 26, 2026
a50467b
feat(indexer): persist checkout mode transitions
zzet Aug 26, 2026
3414e8a
fix(graphview): honor nonzero base ancestry
zzet Aug 26, 2026
c6f6a2a
fix(indexer): retry transient mutation freshness
zzet Aug 26, 2026
af309b7
fix(mcp): complete structured view selector schemas
zzet Aug 26, 2026
d4147fe
refactor(indexer): support immutable full-tree sources
zzet Aug 26, 2026
3a6ae07
perf(indexer): cancel sparse planning promptly
zzet Aug 27, 2026
79cd5d7
fix(indexer): retain mutations across admission pressure
zzet Aug 27, 2026
80bd7ed
perf(indexer): gate reverse fanout by symbol shape
zzet Aug 27, 2026
466bd63
Merge remote-tracking branch 'origin/main' into feat/worktree-branch-…
zzet Sep 1, 2026
b6587ac
fix(indexer): keep immutable ref reads local
zzet Aug 27, 2026
dce35e0
fix(indexer): make dedicated graph binding owner-idempotent
zzet Aug 27, 2026
47ad906
fix(mcp): scope grace fallback through family primary
zzet Aug 27, 2026
ccfea66
fix(indexer): coalesce sparse generation builds
zzet Aug 28, 2026
c005822
fix(indexer): bound derived view build admission
zzet Aug 28, 2026
8c67bea
test(query): pin overlay shadowing rank semantics
zzet Aug 28, 2026
1be7e75
test(indexer): cover durable inactive ref reuse
zzet Aug 28, 2026
7530502
test(daemon): cover event-driven worktree lifecycle
zzet Aug 28, 2026
444639e
test(mcp): publish readiness in contracts fixture
zzet Aug 28, 2026
2fb34e3
test(indexer): decouple topology probe from build startup
zzet Aug 28, 2026
cce7aca
fix(indexer): retire deleted graph generations
zzet Aug 28, 2026
20b553d
fix(indexer): reclaim abandoned payload generations
zzet Aug 28, 2026
13b1a01
fix(indexer): exhaust retirement generation scans
zzet Aug 28, 2026
948c2a4
fix(indexer): scope full reindex eviction by generation
zzet Aug 28, 2026
eabbb3f
fix(indexer): batch retirement route reads
zzet Aug 28, 2026
23dbeec
fix(indexer): walk sparse sources directly
zzet Aug 28, 2026
dee5efe
fix(indexer): collapse checkout polling git fanout
zzet Aug 28, 2026
613df33
fix(indexer): share Git family topology watches
zzet Aug 28, 2026
93da7bf
fix(gitstate): disambiguate porcelain branch markers
zzet Aug 28, 2026
571c2dd
fix(indexer): drain topology callbacks on teardown
zzet Aug 28, 2026
21f9691
fix(indexer): make repository cleanup retryable
zzet Aug 28, 2026
a8eb927
fix(indexer): complete topology teardown drains
zzet Aug 28, 2026
edfcbe6
test(daemon): attach watcher in lifecycle fixtures
zzet Aug 29, 2026
d2a25e9
test(mcp): cover worktree grace fallback boundaries
zzet Aug 29, 2026
3db2b09
fix(indexer): bound Git topology monitoring
zzet Aug 29, 2026
aeea1b9
fix(indexer): normalize unborn Git baselines
zzet Aug 29, 2026
d69a74f
fix(indexer): join Git reconcile callbacks on stop
zzet Aug 29, 2026
eea01ae
test(mcp): attach watcher to routed text fixture
zzet Aug 29, 2026
3e6840d
test(mcp): attach watcher to lifecycle fixtures
zzet Aug 29, 2026
d617682
fix(indexer): signal accepted checkout head changes
zzet Aug 29, 2026
3699ad1
fix(indexer): probe linked worktree HEAD identities
zzet Aug 29, 2026
ad6fe80
fix(indexer): detect worktree head ABA transitions
zzet Aug 29, 2026
e4aee87
fix go mod tidy
zzet Aug 30, 2026
3952df2
fix daemon startup migration progress
zzet Aug 30, 2026
4f12249
docs teach agents worktree view semantics
zzet Aug 30, 2026
deae731
fixup! docs teach agents worktree view semantics
zzet Aug 30, 2026
da32db9
fix canonical worktree path identity
zzet Aug 30, 2026
80787f7
fix concurrent daemon startup waiting
zzet Aug 30, 2026
52a3fee
fix migration completion durability
zzet Aug 30, 2026
4207c1f
fix authoritative generation content cleanup
zzet Aug 30, 2026
57a4f50
fix sparse reachability invalidation
zzet Aug 30, 2026
fd8c379
fix explicit primary designation
zzet Aug 30, 2026
174dd64
teach agents worktree view routing
zzet Aug 30, 2026
21974e0
test canonical macOS path identity
zzet Aug 30, 2026
a1e3728
benchmark automatic checkout guidance
zzet Aug 30, 2026
1ea75fc
benchmark primary graph designation
zzet Aug 30, 2026
94a74c2
fix bound cold-index WAL growth
zzet Aug 31, 2026
b0ed192
fix retry deferred wal checkpoints
zzet Aug 31, 2026
d5678fe
fix bracket sparse generation runtime activity
zzet Aug 31, 2026
e8dd9ae
fix preserve offline path containment
zzet Aug 31, 2026
37d2c1a
fix release joined test daemon state
zzet Aug 31, 2026
517d325
fix canonicalize checkout path routing
zzet Sep 1, 2026
8692bcc
fix fail closed on unresolved view scopes
zzet Sep 1, 2026
7fc7b83
fix retain routed commits after offline switches
zzet Sep 1, 2026
cad901b
fix(indexer): contain raw storage exhaustion failures
zzet Sep 1, 2026
13d7248
fix(indexer): contain watcher storage failures
zzet Sep 1, 2026
54be9fc
fix(indexer): keep git status probes read-only
zzet Sep 1, 2026
c17161f
fix(watcher): join aggregator shutdown
zzet Sep 1, 2026
ccde352
fix(agents): upgrade shipped worktree guidance
zzet Sep 1, 2026
5bff8f8
fix(mcp): preserve large checkout families
zzet Sep 1, 2026
41bb644
fix(agents): migrate Claude worktree guidance
zzet Sep 1, 2026
950fa47
docs(agents): teach worktree view consistency
zzet Sep 1, 2026
5377bc6
chore: keep the worktree-views working spec untracked
zzet Sep 2, 2026
abf3a44
fix: make rebuilt-branch tests compile
zzet Sep 2, 2026
52fe0aa
fix(store): drop the duplicate write-gate acquisition in addBatchSetO…
zzet Sep 2, 2026
3397fab
test(store): align TestGenerationScopedFileEvict with generation-scop…
zzet Sep 2, 2026
bb1cd09
test(store_sqlite): align eviction receipt + capability checklist wit…
zzet Sep 2, 2026
eb92af5
test(cmd): reconcile rebuilt-branch view tests with model-free tree
zzet Sep 2, 2026
444c2bb
test(mcp): rebase tools/list ceilings and scope grace-code tests to i…
zzet Sep 2, 2026
e6d48a0
test(indexer): reconcile rebuilt-branch tests with the model-free tree
zzet Sep 2, 2026
5655850
fix(store): drop the cold-index WAL ceiling that crashed the daemon
zzet Sep 2, 2026
1ccca35
fix(lint): clear golangci-lint findings on the rebuilt branch
zzet Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
48 changes: 42 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ on:
jobs:
test:
runs-on: ${{ matrix.os }}
# The suite runs ~30m on the ubuntu runners and slower on macos, so the
# per-step budget needs headroom; this job-level cap still bounds a hang.
timeout-minutes: 60
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
go-version: ['1.26']
go-version: ['1.27']
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

Expand All @@ -24,10 +27,10 @@ jobs:
run: go build -o gortex ./cmd/gortex/

- name: Test
run: go test -race -timeout=30m -coverprofile=coverage.out ./...
run: go test -race -timeout=45m -coverprofile=coverage.out ./...

- name: Upload coverage
if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.26'
if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.27'
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6
with:
files: coverage.out
Expand All @@ -37,8 +40,10 @@ jobs:
# CGO build smoke-test on native Windows. tree-sitter needs a C/C++
# compiler — the GitHub windows runner ships mingw-w64 on PATH, so no
# extra toolchain setup is required. `go build ./...` compiles every
# production package; the focused agents and sidecar tests also exercise
# Windows-only paths without opting into the unsupported full test suite.
# production package; the focused test steps below exercise the paths
# whose behaviour actually differs here — separators, volumes, open-file
# semantics, %USERPROFILE%, and the git layouts the checkout catalog is
# built from — without opting into the unsupported full test suite.
runs-on: windows-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -140,6 +145,37 @@ jobs:
./internal/mcp ./internal/resolver ./internal/persistence
./internal/review

# gitstate reads git's own answers and measures them against host paths.
# git spells a worktree root with '/' on every platform while a fixture
# builds one with the host separator, an absolute path needs a volume
# here and not on POSIX, and a repository's admin directory is reached
# by joining. None of those distinctions exists on the linux/macos
# matrix, so the whole package runs here rather than the one test that
# looks path-shaped. The timeout is generous because every fixture
# shells out to git and process spawn is the slow axis on this runner.
- name: Test git state inspection
run: go test -timeout=15m -count=1 ./internal/gitstate

# The reconcile pass turns that inventory into catalog rows: it joins
# admin directories, and it decides which checkout owns a path by
# measuring roots against each other. Both are separator-shaped, so a
# '/'-spelled expectation passes on POSIX whatever the code does.
- name: Test checkout reconciliation
run: go test -timeout=10m -count=1 ./internal/reconcile

# The CLI half of the same code: the checkout verbs, the view router
# that answers a probe from the checkout containing the probed path,
# and the classifier that tells a linked worktree from a main checkout
# by reading its `.git` file. Every one of them decides by comparing
# path spellings, and the git layouts they classify are built by git
# itself. The selector names those tests only — the rest of the package
# is either unrelated or needs a daemon behind an AF_UNIX socket.
- name: Test checkout and view path routing
run: >
go test -timeout=10m -count=1
-run '^(TestRepos(Families|SetPrimary|Forget|Reconcile|ExplainView)|TestUntrack(Previews|Reports)|TestController(TrackUntrack|Reload)DrivesTheCheckoutLifecycle|TestControllerAnswersTheFileCoverageVerb|TestProbeOf|TestProbeMatchesAPathSpelledThroughASymlink|TestRoutedProbe|TestUnroutedProbe|TestSearchSymbolsWithoutAPath|TestViewsStatus|TestReload_(Keeps|Untracks)|TestDispatcher_(RegisteredCheckout|UnregisteredWorktree)|TestLinkedWorktreeAt_)'
./cmd/gortex

build-linux-static:
# The linux release ships a statically linked binary so it runs on any
# distro (see .goreleaser.yml). That only holds while the code stays
Expand Down Expand Up @@ -202,7 +238,7 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9
with:
version: v2.11.4
version: v2.13.1
args: --timeout=10m

# goreleaser used to run `go mod tidy` as a release before-hook, which
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ jobs:
-v "$PWD":/go/src/gortex \
-w /go/src/gortex \
-e GITHUB_TOKEN \
ghcr.io/goreleaser/goreleaser-cross:v1.26 \
ghcr.io/goreleaser/goreleaser-cross:v1.27 \
release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,5 @@ bench/run-linux-rest.sh

# Local CPU-profiling harness (hardcoded local paths; run manually, not in CI)
internal/indexer/bench_vscode_test.go
docs/worktree-branch-overlays.md
docs/specs/
2 changes: 1 addition & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# golangci-lint configuration (schema v2).
# CI pins golangci-lint to v2.11.4 (.github/workflows/ci.yml).
# CI pins golangci-lint to v2.13.1 (.github/workflows/ci.yml).
version: "2"

linters:
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ make lint # golangci-lint run --timeout=5m
```

**A green test run is not the full CI gate.** CI pins golangci-lint to
**v2.11.4** and fails the build on any finding. Run it locally before opening a
**v2.13.1** and fails the build on any finding. Run it locally before opening a
PR — config lives in [`.golangci.yaml`](.golangci.yaml).

```bash
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ eval-setup: build

# Build linux/amd64 binary for container injection (requires podman/docker)
eval-build-linux:
podman run --rm --platform linux/amd64 -v $(CURDIR):/src -w /src golang:1.26 \
podman run --rm --platform linux/amd64 -v $(CURDIR):/src -w /src golang:1.27 \
bash -c "apt-get update -qq && apt-get install -y -qq libtree-sitter-dev && go build -ldflags '$(LDFLAGS)' -o gortex-linux ./cmd/gortex/"
@echo "✓ Built gortex-linux (linux/amd64)"

Expand Down
99 changes: 99 additions & 0 deletions cmd/gortex/canonical_paths_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package main

import (
"io"
"os"
"path/filepath"
"reflect"
"testing"
"time"

"github.com/spf13/cobra"
"github.com/zzet/gortex/internal/config"
"github.com/zzet/gortex/internal/pathkey"
"github.com/zzet/gortex/internal/testenv"
)

func TestResolveEmbeddedIndexCanonicalizesExplicitAlias(t *testing.T) {
base := t.TempDir()
realRoot := filepath.Join(base, "real")
if err := os.MkdirAll(realRoot, 0o755); err != nil {
t.Fatal(err)
}
aliasRoot := filepath.Join(base, "alias")
if err := os.Symlink(realRoot, aliasRoot); err != nil {
t.Skipf("symlinks unavailable: %v", err)
}
want, err := filepath.EvalSymlinks(realRoot)
if err != nil {
t.Fatal(err)
}
want = filepath.Clean(want)

plan := resolveEmbeddedIndex(aliasRoot, "", nil)
if plan.Index != want {
t.Fatalf("index root = %q, want canonical %q", plan.Index, want)
}
if plan.Notebook != want {
t.Fatalf("notebook root = %q, want canonical %q", plan.Notebook, want)
}
}

func TestRunTrackCanonicalizesAliasBeforePersistenceAndRegistration(t *testing.T) {
testenv.Sandbox(t)
base := t.TempDir()
realRoot := filepath.Join(base, "real")
if err := os.MkdirAll(realRoot, 0o755); err != nil {
t.Fatal(err)
}
aliasRoot := filepath.Join(base, "alias")
if err := os.Symlink(realRoot, aliasRoot); err != nil {
t.Skipf("symlinks unavailable: %v", err)
}
want := pathkey.CanonicalExistingRoot(aliasRoot)

oldEnsure := trackEnsureDaemonReadyFn
oldNotify := trackNotifyDaemonTrackFn
oldWait, oldName, oldAsWorktree := trackWait, trackName, trackAsWorktree
t.Cleanup(func() {
trackEnsureDaemonReadyFn = oldEnsure
trackNotifyDaemonTrackFn = oldNotify
trackWait, trackName, trackAsWorktree = oldWait, oldName, oldAsWorktree
})
trackWait, trackName, trackAsWorktree = false, "", false
ensureSlot := reflect.ValueOf(&trackEnsureDaemonReadyFn).Elem()
ensureSlot.Set(reflect.MakeFunc(ensureSlot.Type(), func([]reflect.Value) []reflect.Value {
return []reflect.Value{reflect.ValueOf(daemonReady)}
}))
var registered []string
trackNotifyDaemonTrackFn = func(path string, _ time.Time) error {
registered = append(registered, path)
return nil
}

cmd := &cobra.Command{}
cmd.SetErr(io.Discard)
for _, root := range []string{aliasRoot, realRoot} {
if err := runTrack(cmd, []string{root}); err != nil {
t.Fatal(err)
}
}
if len(registered) != 2 {
t.Fatalf("daemon registrations = %d, want 2", len(registered))
}
for _, got := range registered {
if got != want {
t.Fatalf("daemon registration path = %q, want canonical %q", got, want)
}
}
global, err := config.LoadGlobal()
if err != nil {
t.Fatal(err)
}
if len(global.Repos) != 1 {
t.Fatalf("persisted repos after alias and canonical registration = %d, want 1", len(global.Repos))
}
if got := global.Repos[0].Path; got != want {
t.Fatalf("persisted repo path = %q, want canonical %q", got, want)
}
}
64 changes: 64 additions & 0 deletions cmd/gortex/canonical_routing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package main

import (
"os"
"path/filepath"
"reflect"
"testing"

"github.com/zzet/gortex/internal/daemon"
)

func statusWithTrackedPath(t *testing.T, path string) daemon.StatusResponse {
t.Helper()
var status daemon.StatusResponse
tracked := reflect.ValueOf(&status).Elem().FieldByName("TrackedRepos")
if !tracked.IsValid() || tracked.Kind() != reflect.Slice {
t.Fatal("daemon.StatusResponse.TrackedRepos is not a slice")
}
repo := reflect.New(tracked.Type().Elem()).Elem()
pathField := repo.FieldByName("Path")
if !pathField.IsValid() || !pathField.CanSet() || pathField.Kind() != reflect.String {
t.Fatal("daemon tracked-repository status has no settable Path field")
}
pathField.SetString(path)
tracked.Set(reflect.Append(tracked, repo))
return status
}

func TestTrackedReposReachCanonicalRootAndCWDAliases(t *testing.T) {
base := t.TempDir()
realRoot := filepath.Join(base, "real")
realRepo := filepath.Join(realRoot, "repo")
realCWD := filepath.Join(realRepo, "nested")
if err := os.MkdirAll(realCWD, 0o755); err != nil {
t.Fatal(err)
}
aliasRoot := filepath.Join(base, "alias")
if err := os.Symlink(realRoot, aliasRoot); err != nil {
t.Skipf("symlinks unavailable: %v", err)
}
aliasRepo := filepath.Join(aliasRoot, "repo")
aliasCWD := filepath.Join(aliasRepo, "nested")

for _, tc := range []struct {
name string
root string
cwd string
}{
{name: "canonical root canonical cwd", root: realRepo, cwd: realCWD},
{name: "canonical root alias cwd", root: realRepo, cwd: aliasCWD},
{name: "alias root canonical cwd", root: aliasRepo, cwd: realCWD},
{name: "alias root alias cwd", root: aliasRepo, cwd: aliasCWD},
} {
t.Run(tc.name, func(t *testing.T) {
status := statusWithTrackedPath(t, tc.root)
if !trackedRootContains(status, tc.cwd) {
t.Fatalf("trackedRootContains rejected cwd %q under root %q", tc.cwd, tc.root)
}
if !trackedReposReach(status, tc.cwd) {
t.Fatalf("trackedReposReach rejected cwd %q under root %q", tc.cwd, tc.root)
}
})
}
}
Loading
Loading