Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/agentsync/doctor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ func TestRunDoctor_ReadmeIsNeverACapture(t *testing.T) {
// about, so "nothing is wrong" cannot be said about it. Same rule as import and
// link: an incomplete diagnosis must not exit the way a clean one does.
//
// Structural rather than chmod 000 (issue #11), so the CI gate — which runs as
// Structural rather than chmod 000, so the CI gate — which runs as
// root, for whom permission bits are advisory — actually runs it. The chmod
// form skipped there, and a skip prints nothing.
func TestRunDoctor_UnreadableSubtreeIsNotACleanRun(t *testing.T) {
Expand Down
8 changes: 3 additions & 5 deletions cmd/agentsync/home_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,9 @@ func scanArgs(t *testing.T, repoRoot, root string, extra ...string) []string {
return append([]string{"scan", "--" + allFlag}, extra...)
}

// TestRunScan_ValidButWrongHomeWarns is the issue itself. $HOME is a real,
// readable, resolvable directory — it is simply not the home these projects
// live under — so a validity check passes it and, before this change, the scan
// completed with every key silently re-based under _outside-home/ and nothing
// on stderr.
// TestRunScan_ValidButWrongHomeWarns requires a warning when $HOME is valid but
// is not the home these projects live under. A directory validity check alone
// would let the scan silently re-base every key under _outside-home/.
//
// The exit code must not change: warning is the right severity for a
// read-only command, and CI keys off these codes.
Expand Down
6 changes: 3 additions & 3 deletions cmd/agentsync/import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ func TestRunImport_ConflictDoesNotBlockTheOtherProjects(t *testing.T) {
// else in the tree is not a reason to leave a genuinely unprotected file
// unprotected.
//
// The subtree is made unreadable structurally, not with chmod 000 (issue #11).
// The subtree is made unreadable structurally, not with chmod 000.
// The chmod form skipped on the CI gate, which runs as root, and it skipped
// without printing anything — so the assertion that mattered most was verified
// on one laptop and nowhere else. testfs.UnreadableSubtree does not care what
Expand Down Expand Up @@ -636,8 +636,8 @@ func TestRunImport_UnreadableSubtreeBlocksAnOtherwiseEmptyRun(t *testing.T) {

// --- the home guard ---------------------------------------------------------

// TestRunImport_WrongHomeWritesNothing is the write-path guard from issue #3,
// and the reason RequireHome exists.
// TestRunImport_WrongHomeWritesNothing checks that RequireHome blocks writes
// when the configured home would give the projects incorrect capture keys.
//
// $HOME is a real, readable directory that is simply not the one these projects
// live under — the sudo case. Nothing on disk looks wrong; the run would
Expand Down
4 changes: 2 additions & 2 deletions internal/doctor/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
// That is a decision, not an unfinished feature. Every drift below has an
// obvious-looking repair — unlink the broken one, repoint the mismatched one,
// delete the stale capture — and every one of those repairs needs a fact this
// tool does not have. Issue #2's state file is the missing fact: it records
// diagnosis alone cannot establish. The state file records
// every link the tool created and the content hash at link time, which is the
// only thing that separates "the user deleted this on purpose" from "this went
// missing", and "this capture is obsolete" from "this capture is the last copy
// of something". Repairing without that merge base means guessing, and the two
// guesses that look most reasonable — unlinking a broken link and deleting a
// stale capture — are exactly the two that destroy something unrecoverable.
//
// So repair and reconcile are a separate future command, gated on the state
// Reconciliation belongs to the separate reconcile command, gated on the state
// file. doctor's whole contract is: find it, name it, explain why it might be
// there, and leave the judgement to a human. Its exit code is the only thing it
// asserts.
Expand Down
2 changes: 1 addition & 1 deletion internal/doctor/doctor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,7 @@ func TestWalkCaptures_NonRegularCaptureIsAProblem(t *testing.T) {
// treated as empty — an unreadable directory and an empty one must not produce
// the same report.
//
// Structural rather than chmod 000 (issue #11): the CI gate runs as root, and
// Structural rather than chmod 000: the CI gate runs as root, and
// permission bits do not restrict root's walk, so the chmod form of this test
// skipped in CI and covered nothing there. testfs.UnreadableSubtree fails the
// same way for every uid.
Expand Down
7 changes: 3 additions & 4 deletions internal/importer/importer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (f *fixture) commitRepo() {
// noGit makes git unexecutable for the rest of the test, which is how the
// "could not ask" arm is reached without depending on file permissions.
//
// Permissions are the wrong lever here: CI runs as root (issue #11), where a
// Permissions are the wrong lever here: CI runs as root, where a
// chmod-based fixture stops being unreadable and the test quietly passes
// without exercising anything. An empty PATH fails the exec itself, identically
// for every user.
Expand Down Expand Up @@ -657,9 +657,8 @@ func TestApply_UpdateRefusesWhenHEADMovedAfterThePlan(t *testing.T) {
// TestNewPlan_UpdateRefusesWhenGitCannotAnswer is the fail-closed property, and
// the one most likely to regress.
//
// "Not committed" and "git could not be asked" are different facts, and the bug
// class in issue #12 is exactly a caller reading the second as the first — or,
// here, as the more permissive third thing. Only scan.CommittedYes may
// "Not committed" and "git could not be asked" are different facts, but neither
// permits replacing a capture. Only scan.CommittedYes may
// authorise replacing a capture; every other answer, including one the probe
// could not produce at all, refuses.
//
Expand Down
3 changes: 1 addition & 2 deletions internal/importer/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,8 +497,7 @@ func judgeDest(item Item, opts Options) (verdict, error) {
// case rather than the default arm is the point: every way the probe can fail
// to say yes — git missing, a repo git will not talk about under safe.directory,
// a corrupt object store, no repository at all — then lands on the refusing side
// without any call site remembering to handle it. That is the discipline issue
// #12 exists about, applied at the site rather than documented upstream of it.
// without any call site remembering to handle it.
func commitGate(dest string) (Refusal, string) {
switch scan.CommittedInHEAD(dest) {
case scan.CommittedYes:
Expand Down
6 changes: 3 additions & 3 deletions internal/inventory/inventory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1126,9 +1126,9 @@ func TestBuild_EmptyMachineStillEmitsArrays(t *testing.T) {
// The top level is exactly these five keys, asserted as a closed set rather
// than by looking for the fields we do not want.
//
// The issue's own sketch named a generated-at timestamp and a tool version, and
// both were dropped: either one makes every regeneration a diff, which destroys
// the property --check depends on and the reason this file is worth committing.
// A generated-at timestamp or tool version would change the output even when
// the inventory content is unchanged, undermining --check and creating noise
// in a committed inventory file.
// Their absence is a contract, so adding a sixth key has to fail here and be
// argued for, not slip in.
func TestMarshal_TopLevelIsSchemaAndContentOnly(t *testing.T) {
Expand Down
13 changes: 5 additions & 8 deletions internal/linker/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,7 @@ func (u Unresolved) String() string {
// The zero value covers every project the scan found and provisions only what
// is already on disk there. Detection — deciding which agents a project
// *should* have — is deliberately not part of this package; link provisions
// what it is told to provision, and the manifest from issue #2 will feed the
// same Agents field when it exists.
// what it is told to provision through the Agents field.
type Options struct {
// Projects restricts the run to these project keys, as produced by
// scan.ProjectKey. Empty means every project found under the scan root.
Expand Down Expand Up @@ -700,9 +699,8 @@ func atRisk(entries []scan.Entry) []scan.Entry {

// Judge maps one classified entry onto the action link takes for it.
//
// The mapping is the state table from issue #2 and nothing else: states 3, 4,
// 6 and 7 are reported and left alone, state 2 is a no-op, and state 5 is the
// only on-disk state link writes for.
// States 3, 4, 6 and 7 are reported and left alone, state 2 is a no-op, and
// state 5 is the only on-disk state link writes for.
//
// Exported for doctor, which reports on the same link states. "Is this link
// healthy, and is it pointing at this project's counterpart?" is one question,
Expand Down Expand Up @@ -914,9 +912,8 @@ func deriveDir(repo *scan.Repo, key string) (string, error) {
// an ordinary layout. The Lstat rule governs agent *files*, where
// following a link is what turns a healthy link into a false adoptable.
//
// This does fold "not there" together with "could not look" — audited
// under issue #12 and kept, because here both answers are the same
// answer. Every non-nil error returns, so an unreadable path refuses to
// "Not there" and "could not look" both refuse provisioning here.
// Every non-nil error returns, so an unreadable path refuses to
// provision exactly as a missing one does, and refusing is the safe arm
// on a write path. The wording is the only casualty: "no project at X"
// is imprecise for a permission error. It is not misleading in the way
Expand Down
18 changes: 9 additions & 9 deletions internal/reconcile/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,8 +542,8 @@ func (p *Plan) refuse(op Op, selector, path string, guard Guard, format string,
}

// judgeRecords walks what this tool says it created and compares each record
// against what is on disk now. It is where the three-row table from issue #13
// is decided, and where the prune gate is evaluated.
// against what is on disk now: a symlink, a regular file, or an absent path.
// It also evaluates the prune gate for recorded links.
func (p *Plan) judgeRecords(repo *scan.Repo, records []state.Record, opts Options) {
for _, rec := range records {
item := Item{
Expand Down Expand Up @@ -572,15 +572,15 @@ func (p *Plan) judgeRecords(repo *scan.Repo, records []state.Record, opts Option
}
}

// judgeAbsent separates row three of the table from a project that is no longer
// at this path at all.
// judgeAbsent distinguishes a deleted link from a project that is no longer
// at this path.
//
// Row three is one link deleted out of a directory that is still there, which
// is evidence about that agent. A missing agents directory is evidence about
// the project — an ordinary `mv` produces it — and reading it as row three
// One link deleted out of a directory that is still there is evidence about
// that agent. A missing agents directory is evidence about
// the project — an ordinary `mv` produces it — and treating it as a deleted link
// writes a declined marker for every agent in the project, suppressing agents
// nobody declined. That is the false-positive direction #13 names, and the
// decline is the one write no flag guards.
// nobody declined. Declines are recorded without an apply flag, so this
// distinction must hold even in a dry run.
func (p *Plan) judgeAbsent(item Item, rec state.Record) {
dir := filepath.Dir(rec.Path)

Expand Down
6 changes: 3 additions & 3 deletions internal/reconcile/reconcile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,9 @@ func noErrs(t *testing.T, errs []error) {

// --- the record-against-disk table ------------------------------------------

// TestNewPlan_RecordAgainstDisk is the three-row table from issue #13 plus the
// link states that surround it. It is the whole of what reconcile decides about
// a link it has a record for, so every arm is exercised from one fixture shape.
// TestNewPlan_RecordAgainstDisk covers recorded paths that are now symlinks,
// regular files, or absent. Every reconciliation outcome for a recorded link
// is exercised from one fixture shape.
func TestNewPlan_RecordAgainstDisk(t *testing.T) {
const project = "Web/proj"

Expand Down
23 changes: 10 additions & 13 deletions internal/scan/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,8 @@ func probeGitRoot(dir string) gitRoot {
// and nothing upstream can catch it. Reading the 128 as "unborn" made
// this gitNo, hence TrackedNo, hence CommittedNo, hence import --update
// telling the user to commit project-agents/ in a repository whose refs
// are unreadable — the same wrong remedy this issue was opened over,
// reached by a different route. `git pack-refs --all` is an ordinary
// are unreadable, where committing cannot fix the failure.
// `git pack-refs --all` is an ordinary
// maintenance command and `git gc` runs it automatically, so a packed
// refs file is the normal state of any repository that has been alive
// for a while, not an exotic one.
Expand All @@ -232,12 +232,10 @@ func probeGitRoot(dir string) gitRoot {
// rev-parse --verify --quiet HEAD -> 1
//
// So a repository whose refs cannot be read still lands on gitNo here
// and reports as "nothing is committed". It is pre-existing, it is
// identical in InspectDir below, and it is recorded on issue #12 rather
// than fixed: the only disambiguator that survives is filesystem
// corroboration in the style of hasGitEntry — asking whether refs
// actually exist — and that is a larger change than this audit should
// make on a path that already fails closed for every other reason.
// and reports as "nothing is committed". This remains a limitation here
// and in InspectDir below: distinguishing unreadable refs from an unborn
// HEAD requires filesystem corroboration in the style of hasGitEntry,
// rather than relying on the exit code alone.
_, ans, code := runGitCode(top, "rev-parse", "--verify", "--quiet", "HEAD")
switch {
case ans == gitYes:
Expand Down Expand Up @@ -414,9 +412,8 @@ func (c Committed) String() string {
// The exit codes are read the way the rest of this file reads them: 0 is a yes,
// a documented 1 is a no, and anything else is git failing rather than
// answering. Only a clean yes to all three questions returns CommittedYes;
// every other outcome refuses. That is the discipline issue #12 exists about,
// and making CommittedUnknown the zero value is what keeps a forgotten case on
// the refusing side.
// every other outcome refuses. Making CommittedUnknown the zero value keeps
// a forgotten case on the refusing side.
//
// Moving this into the calling package was rejected: it would duplicate
// runGitCode's exit-code handling into a second place, which is exactly how
Expand Down Expand Up @@ -674,8 +671,8 @@ func InspectDir(dir string) DirGit {
// git reports an unreadable refs/heads/ with its documented negative
// rather than with 128. A repository whose refs cannot be read therefore
// reaches the ignore check as though it had no commits, and lands on
// DirVisible or DirIgnored — the permissive side. Pre-existing, recorded
// on issue #12, not fixed here.
// DirVisible or DirIgnored — the permissive side. Distinguishing these
// cases remains an unresolved limitation of this exit-code check.
_, ans, code := runGitCode(top, "rev-parse", "--verify", "--quiet", "HEAD")
switch {
case ans == gitYes:
Expand Down
26 changes: 11 additions & 15 deletions internal/scan/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,9 @@ func requireGit(t *testing.T) {
}
}

// This file used to carry its own git() and gitOutput(), each building the
// isolated environment the assertions depend on. They now live in testfs
// alongside the breakage fixtures, for the reason issue #12 is an audit rather
// than six fixes: a second copy of a rule drifts from the first, silently, and
// this bug class is what drift in a git probe looks like. One definition of
// "a git command under test" means a fixture written next year gets the same
// isolation and the same self-checks as the ones written today.
// Git helpers live in testfs alongside the breakage fixtures so every test
// uses the same isolated environment and fixture self-checks. Keeping that
// setup in one place prevents git-probe tests from drifting apart.

// initGitRepo makes a repository at dir with no commits yet.
func initGitRepo(t *testing.T, dir string) {
Expand Down Expand Up @@ -497,8 +493,8 @@ func TestCommittedInHEAD_FailsClosed(t *testing.T) {

t.Run("git unavailable is unknown, never a no", func(t *testing.T) {
// The two answers mean different things to a caller: one has a fix
// the user can carry out and the other does not. Collapsing them is
// the bug class issue #12 tracks.
// the user can carry out and the other does not. An unavailable git
// command must return unknown rather than claim the file is uncommitted.
repo := filepath.Join(t.TempDir(), "project")
mustMkdir(t, repo)
p := filepath.Join(repo, "agent.md")
Expand Down Expand Up @@ -540,8 +536,8 @@ func TestCommittedInHEAD_FailsClosed(t *testing.T) {
// The recipe is deliberate rather than incidental: `ls-tree` reads only tree
// objects, so deleting the blob leaves it answering perfectly well, while every
// question about content has to go to the object that is gone. No permissions
// and no privilege are involved, so this runs identically for root — the same
// property issue #11 required of the walk fixtures.
// and no privilege are involved, so this runs identically for root and
// non-root users.
func TestCommittedInHEAD_DamagedObjectStoreIsNeverAYes(t *testing.T) {
requireGit(t)

Expand Down Expand Up @@ -586,8 +582,8 @@ func TestCommittedInHEAD_DamagedObjectStoreIsNeverAYes(t *testing.T) {
})
}

// The regression guards for issue #12's audit: three probes in probeGitRoot and
// gitProber.tracked that read a non-zero git exit as a clean negative.
// These regression guards ensure probes in probeGitRoot and gitProber.tracked
// distinguish failed git commands from clean negative answers.
//
// Every one of these passed before the fix, because every one of them was
// unreachable from a repository that works. That is the property of this bug
Expand Down Expand Up @@ -709,8 +705,8 @@ func TestProbeGitRoot_UnbornHeadIsTheOnlyNo(t *testing.T) {
// upstream can catch it because --show-toplevel still exits 0. Read as
// "unborn" it became TrackedNo, then CommittedNo, then import --update
// telling the user to commit project-agents/ in a repository whose refs
// git cannot parse — the wrong remedy this issue exists over, reached by
// a route the first fix did not cover.
// git cannot parse. A corrupt refs file must be reported as a probe failure,
// since committing cannot repair it.
t.Run("a corrupt packed-refs is not an unborn HEAD", func(t *testing.T) {
project := filepath.Join(t.TempDir(), "project")
testfs.GitRepo(t, project, "agent.md", "content\n")
Expand Down
8 changes: 4 additions & 4 deletions internal/scan/home_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ func agentsDirIn(projectDir string) string {
return filepath.Join(projectDir, ".claude", "agents")
}

// TestCheckHome_Statuses is the heart of the issue: /var/root is a valid,
// readable, resolvable directory, so a validity check passes it and the wrong
// TestCheckHome_Statuses guards against a valid but wrong home: /var/root is
// readable and resolvable, so a validity check passes it and the wrong
// keys go unreported. Only the keys the projects actually get separate a wrong
// home from a right one.
func TestCheckHome_Statuses(t *testing.T) {
Expand Down Expand Up @@ -130,8 +130,8 @@ func TestCheckHome_Statuses(t *testing.T) {
//
// Under `sudo -i` with --root / or --root /Users, home sits UNDER the root, so
// any root-relative test concludes "fine" — while every project key is
// silently re-based under _outside-home/. That is precisely the invocation
// shape the issue is about, and it must warn.
// silently re-based under _outside-home/. This must warn even though the
// scan root contains the configured home.
func TestCheckHome_RootIsNotTheSignal(t *testing.T) {
base := resolvePath(t.TempDir())
realHome := filepath.Join(base, "home")
Expand Down
Loading
Loading