From 379989d29a8c39113a07e13722070318d93562eb Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Mon, 10 Aug 2026 10:54:45 -0400 Subject: [PATCH] chore: remove dead production code --- internal/application/add/plan.go | 7 +- internal/application/add/plan_test.go | 4 +- internal/application/add/types.go | 1 - internal/application/apply/execute_aliases.go | 1 - internal/application/apply/execute_files.go | 2 - .../application/apply/execute_files_test.go | 2 +- internal/application/apply/hooks_test.go | 2 +- internal/application/apply/prepare.go | 9 +- internal/application/apply/prepare_test.go | 10 +- internal/application/apply/service_test.go | 4 +- internal/application/apply/types.go | 19 ++-- internal/application/apply/types_test.go | 24 +---- internal/application/inspect/diff.go | 18 +--- internal/application/inspect/diff_test.go | 19 +--- internal/application/inspect/status.go | 7 +- internal/application/inspect/status_test.go | 13 +-- internal/cli/apply.go | 13 +-- internal/cli/apply_test.go | 6 +- internal/cli/render_add_test.go | 2 +- internal/diff/safe.go | 11 +-- internal/diff/safe_test.go | 4 +- internal/filesystem/hardlink_test.go | 95 ------------------- internal/filesystem/helpers_test.go | 9 -- internal/filesystem/mode.go | 47 --------- internal/filesystem/mode_test.go | 43 --------- internal/filesystem/precondition_test.go | 1 - internal/filesystem/source.go | 50 ---------- internal/filesystem/source_test.go | 27 ------ internal/reconcile/precondition_test.go | 13 --- internal/reconcile/snapshot_helpers_test.go | 2 +- internal/reconcile/source_snapshot_test.go | 4 +- internal/reconcile/target_snapshot_test.go | 29 ++---- internal/reconcile/types.go | 3 - internal/reconcile/types_test.go | 17 ++-- internal/repository/controls.go | 34 ------- internal/repository/controls_test.go | 28 ++---- internal/repository/scan.go | 70 +------------- internal/repository/scan_test.go | 23 ----- 38 files changed, 79 insertions(+), 594 deletions(-) delete mode 100644 internal/filesystem/hardlink_test.go delete mode 100644 internal/filesystem/source.go delete mode 100644 internal/filesystem/source_test.go diff --git a/internal/application/add/plan.go b/internal/application/add/plan.go index 7cdb393..3cb6053 100644 --- a/internal/application/add/plan.go +++ b/internal/application/add/plan.go @@ -1,10 +1,6 @@ package add -import ( - "sort" - - "github.com/alyraffauf/cattery/internal/deployment" -) +import "sort" // BuildPlan freezes the preflighted items into a BatchPlan. Items are sorted // by target path for display while execution proceeds in that same order @@ -76,6 +72,5 @@ func itemRecord(item ItemPlan, status ItemStatus) ItemResult { Target: item.TargetRelativePath(), Source: item.SourceRepositoryPath(), Status: status, - Secret: item.Kind() == deployment.FileSecret, } } diff --git a/internal/application/add/plan_test.go b/internal/application/add/plan_test.go index a8717c6..1b762cb 100644 --- a/internal/application/add/plan_test.go +++ b/internal/application/add/plan_test.go @@ -74,8 +74,8 @@ func testDryRunFlagsSecret(t *testing.T) { item.Kind = deployment.FileSecret plan := mustPlan(t, []ItemPlanInput{planItem("readme"), item}) result := DryRun(plan) - if result.Items[0].Target != "creds" || !result.Items[0].Secret { - t.Fatal("dry run did not flag the secret item") + if result.Items[0].Target != "creds" { + t.Fatal("dry run did not include the secret item") } } diff --git a/internal/application/add/types.go b/internal/application/add/types.go index 1422f67..5076e93 100644 --- a/internal/application/add/types.go +++ b/internal/application/add/types.go @@ -253,7 +253,6 @@ type ItemResult struct { Target string Source string Status ItemStatus - Secret bool } // Summary counts the per-target outcome records of one add. diff --git a/internal/application/apply/execute_aliases.go b/internal/application/apply/execute_aliases.go index 78e3e36..c2ab175 100644 --- a/internal/application/apply/execute_aliases.go +++ b/internal/application/apply/execute_aliases.go @@ -131,7 +131,6 @@ func aliasRecord(job aliasJob, status ItemStatus) ItemResult { return ItemResult{ TargetPath: job.action.TargetPath, Status: status, - Secret: false, Kind: job.action.Kind, } } diff --git a/internal/application/apply/execute_files.go b/internal/application/apply/execute_files.go index c9805cf..8c5fea1 100644 --- a/internal/application/apply/execute_files.go +++ b/internal/application/apply/execute_files.go @@ -196,7 +196,6 @@ func completedRecord(job fileJob) ItemResult { return ItemResult{ TargetPath: job.action.TargetPath, Status: StatusCompleted, - Secret: job.candidate.record.File.Kind == deployment.FileSecret, Kind: job.action.Kind, } } @@ -206,7 +205,6 @@ func partialRecord(job fileJob) ItemResult { return ItemResult{ TargetPath: job.action.TargetPath, Status: StatusPartial, - Secret: job.candidate.record.File.Kind == deployment.FileSecret, Kind: job.action.Kind, } } diff --git a/internal/application/apply/execute_files_test.go b/internal/application/apply/execute_files_test.go index 9d51fa9..02ac866 100644 --- a/internal/application/apply/execute_files_test.go +++ b/internal/application/apply/execute_files_test.go @@ -275,7 +275,7 @@ func testExecuteSecret(t *testing.T) { // target with mode 0600, and the keyed baseline fingerprints. func assertSecretResult(t *testing.T, outcome secretOutcome) { t.Helper() - if len(outcome.results) != 1 || outcome.results[0].Status != StatusCompleted || !outcome.results[0].Secret { + if len(outcome.results) != 1 || outcome.results[0].Status != StatusCompleted { t.Fatalf("results = %+v, want a completed secret record", outcome.results) } if string(targetContent(t, outcome.home, "target")) != "plaintext" { diff --git a/internal/application/apply/hooks_test.go b/internal/application/apply/hooks_test.go index 12856ca..c6e516d 100644 --- a/internal/application/apply/hooks_test.go +++ b/internal/application/apply/hooks_test.go @@ -107,7 +107,7 @@ func testHooksDryRun(t *testing.T) { func testHooksNoHooks(t *testing.T) { pair := hookFixture(t, false) - records, err := pair.service.RunHookPipeline(context.Background(), PipelineInput{Request: Request{NoHooks: true, NoHooksSet: true}, Plan: pair.plan, Candidates: pair.candidates}) + records, err := pair.service.RunHookPipeline(context.Background(), PipelineInput{Request: Request{NoHooks: true}, Plan: pair.plan, Candidates: pair.candidates}) if err != nil { t.Fatalf("pipeline: %v", err) } diff --git a/internal/application/apply/prepare.go b/internal/application/apply/prepare.go index 2c96da8..e685725 100644 --- a/internal/application/apply/prepare.go +++ b/internal/application/apply/prepare.go @@ -3,7 +3,6 @@ package apply import ( "context" - "github.com/alyraffauf/cattery/internal/deployment" "github.com/alyraffauf/cattery/internal/failure" "github.com/alyraffauf/cattery/internal/reconcile" ) @@ -118,7 +117,7 @@ func prepareOne(candidate Candidate, records []ItemResult, scope prepareScope) ( if decided && choice == ChoiceSkip { return false, kind, "", false, append(records, plannedRecord(candidate, kind)), nil } - if needsDecision(candidate) && !decided && !scope.dryRun { + if candidateNeedsDecision(candidate) && !decided && !scope.dryRun { return false, kind, "", false, records, failure.New(failure.InvalidInput, "apply: unresolved decision for "+candidate.record.TargetPath, nil) } if scope.dryRun { @@ -137,11 +136,6 @@ func confirmedReplace(candidate Candidate, decided bool, choice DecisionChoice) return candidate.record.Entry == reconcile.PlanEntryAlias && candidate.record.FileState != nil && candidate.record.FileState.Active() } -// needsDecision reports whether one candidate required an explicit choice. -func needsDecision(candidate Candidate) bool { - return candidateNeedsDecision(candidate) -} - func candidateNeedsDecision(candidate Candidate) bool { return candidate.file.Convergence == reconcile.ConvergenceDecisionRequired || candidate.alias.Convergence == reconcile.ConvergenceDecisionRequired } @@ -151,7 +145,6 @@ func plannedRecord(candidate Candidate, kind ActionKind) ItemResult { return ItemResult{ TargetPath: candidate.record.TargetPath, Status: StatusPlanned, - Secret: candidate.record.File.Kind == deployment.FileSecret, Kind: kind, } } diff --git a/internal/application/apply/prepare_test.go b/internal/application/apply/prepare_test.go index 082284f..544115a 100644 --- a/internal/application/apply/prepare_test.go +++ b/internal/application/apply/prepare_test.go @@ -72,7 +72,7 @@ type evalPair struct { func testPrepareDryRun(t *testing.T) { pair := driftFixture(t, "a.conf") - plan, err := pair.service.Prepare(context.Background(), PrepareInput{Request: Request{DryRun: true, DryRunSet: true}, Candidates: pair.candidates}) + plan, err := pair.service.Prepare(context.Background(), PrepareInput{Request: Request{DryRun: true}, Candidates: pair.candidates}) if err != nil { t.Fatalf("prepare: %v", err) } @@ -90,11 +90,11 @@ func testPrepareDryRun(t *testing.T) { func testPrepareNoninteractive(t *testing.T) { pair := driftFixture(t, "a.conf") - _, err := pair.service.Prepare(context.Background(), PrepareInput{Request: Request{NonInteractive: true, NonInteractiveSet: true}, Candidates: pair.candidates}) + _, err := pair.service.Prepare(context.Background(), PrepareInput{Request: Request{NonInteractive: true}, Candidates: pair.candidates}) if err == nil || !kindIs(err, failure.InvalidInput) { t.Fatalf("noninteractive refusal error = %v, want an invalid input failure", err) } - plan, err := pair.service.Prepare(context.Background(), PrepareInput{Request: Request{DryRun: true, NonInteractive: true, NonInteractiveSet: true}, Candidates: pair.candidates}) + plan, err := pair.service.Prepare(context.Background(), PrepareInput{Request: Request{DryRun: true, NonInteractive: true}, Candidates: pair.candidates}) if err != nil { t.Fatalf("dry-run must not refuse pending decisions: %v", err) } @@ -168,14 +168,14 @@ func assertPreparedAction(t *testing.T, pair evalPair, decisions CollectedDecisi // assertHooksSuppressed requires --no-hooks and dry-run to suppress hooks. func assertHooksSuppressed(t *testing.T, pair evalPair, decisions CollectedDecisions) { t.Helper() - plan, err := pair.service.Prepare(context.Background(), PrepareInput{Request: Request{NoHooks: true, NoHooksSet: true}, Candidates: pair.candidates, Decisions: decisions}) + plan, err := pair.service.Prepare(context.Background(), PrepareInput{Request: Request{NoHooks: true}, Candidates: pair.candidates, Decisions: decisions}) if err != nil { t.Fatalf("prepare: %v", err) } if plan.WithHooks() { t.Fatal("--no-hooks must suppress hooks") } - plan, err = pair.service.Prepare(context.Background(), PrepareInput{Request: Request{DryRun: true, DryRunSet: true}, Candidates: pair.candidates}) + plan, err = pair.service.Prepare(context.Background(), PrepareInput{Request: Request{DryRun: true}, Candidates: pair.candidates}) if err != nil { t.Fatalf("prepare: %v", err) } diff --git a/internal/application/apply/service_test.go b/internal/application/apply/service_test.go index e7aaea2..7dbd66e 100644 --- a/internal/application/apply/service_test.go +++ b/internal/application/apply/service_test.go @@ -74,7 +74,7 @@ func testServiceNoop(t *testing.T) { func testServiceDryRun(t *testing.T) { pair := serviceFixture(t) - result, err := pair.service.Apply(context.Background(), Request{DryRun: true, DryRunSet: true}) + result, err := pair.service.Apply(context.Background(), Request{DryRun: true}) if err == nil || !kindIs(err, failure.Difference) { t.Fatalf("apply: %v, want a difference failure for a pending dry run", err) } @@ -134,7 +134,7 @@ func testServiceDecisionFailure(t *testing.T) { func testServicePrepareRefusal(t *testing.T) { pair := serviceFixture(t) - _, err := pair.service.Apply(context.Background(), Request{NonInteractive: true, NonInteractiveSet: true}) + _, err := pair.service.Apply(context.Background(), Request{NonInteractive: true}) if err == nil || !kindIs(err, failure.InvalidInput) { t.Fatalf("refusal error = %v, want invalid input", err) } diff --git a/internal/application/apply/types.go b/internal/application/apply/types.go index 51d815d..baa20b6 100644 --- a/internal/application/apply/types.go +++ b/internal/application/apply/types.go @@ -128,19 +128,13 @@ type RepositoryInput struct { } // Request is the frozen input of one apply: the raw repository fields, the -// raw group arguments in command-line order, and the policy flags with -// separate presence bits. An omitted --dry-run, --non-interactive, or -// --no-hooks leaves its Set bit false so the service never mistakes it for -// an explicit false request. +// raw group arguments in command-line order, and the policy flags. type Request struct { - Repository RepositoryInput - Groups []string - DryRun bool - DryRunSet bool - NonInteractive bool - NonInteractiveSet bool - NoHooks bool - NoHooksSet bool + Repository RepositoryInput + Groups []string + DryRun bool + NonInteractive bool + NoHooks bool } // DecisionChoice is the application-owned choice vocabulary one prompt may @@ -301,7 +295,6 @@ const ( type ItemResult struct { TargetPath string Status ItemStatus - Secret bool Kind ActionKind } diff --git a/internal/application/apply/types_test.go b/internal/application/apply/types_test.go index 226d434..6cedd14 100644 --- a/internal/application/apply/types_test.go +++ b/internal/application/apply/types_test.go @@ -23,7 +23,6 @@ func TestApplyContract(t *testing.T) { name string run func(*testing.T) }{ - {"zero request keeps policy presence false", testContractPresenceBits}, {"decision requests validate and copy", testContractDecisionRequest}, {"safe differences copy lines", testContractSafeDifference}, {"action plans copy defensively", testContractActionPlan}, @@ -36,27 +35,6 @@ func TestApplyContract(t *testing.T) { } } -func testContractPresenceBits(t *testing.T) { - var request Request - if request.DryRunSet || request.NonInteractiveSet || request.NoHooksSet { - t.Fatal("zero Request must leave every policy presence false") - } - if request.Repository != (RepositoryInput{}) { - t.Fatalf("zero Request.Repository = %+v, want the zero repository input", request.Repository) - } - if request.Groups != nil { - t.Fatalf("zero Request.Groups = %v, want nil", request.Groups) - } - explicit := Request{Repository: RepositoryInput{RawExplicit: "repo", ExplicitSet: true}, - DryRun: true, DryRunSet: true, NonInteractive: false, NonInteractiveSet: true, NoHooks: true, NoHooksSet: true} - if !explicit.DryRunSet || !explicit.NonInteractiveSet || !explicit.NoHooksSet { - t.Fatal("explicit presence bits must be preserved") - } - if !explicit.Repository.ExplicitSet || explicit.Repository.RawExplicit != "repo" { - t.Fatal("repository presence and value must be preserved") - } -} - func testContractDecisionRequest(t *testing.T) { request, err := NewDecisionRequest(DecisionRequestInput{TargetPath: "a.conf", Choices: []DecisionChoice{ChoiceOverwrite, ChoiceSkip}}) if err != nil { @@ -124,7 +102,7 @@ func testContractActionPlan(t *testing.T) { func testContractPartialSummaries(t *testing.T) { result := Result{Items: []ItemResult{ - {TargetPath: "a", Status: StatusCompleted, Secret: true, Kind: ActionKindReplaceFile}, + {TargetPath: "a", Status: StatusCompleted, Kind: ActionKindReplaceFile}, {TargetPath: "b", Status: StatusPartial, Kind: ActionKindWriteSource}, {TargetPath: "c", Status: StatusPlanned, Kind: ActionKindRealizeAlias}, }, Summary: Summary{Planned: 1, Completed: 1, Partial: 1}} diff --git a/internal/application/inspect/diff.go b/internal/application/inspect/diff.go index 46e70fb..1ea3a1e 100644 --- a/internal/application/inspect/diff.go +++ b/internal/application/inspect/diff.go @@ -4,7 +4,6 @@ import ( "context" "github.com/alyraffauf/cattery/internal/application/evaluation" - "github.com/alyraffauf/cattery/internal/deployment" "github.com/alyraffauf/cattery/internal/diff" "github.com/alyraffauf/cattery/internal/failure" "github.com/alyraffauf/cattery/internal/reconcile" @@ -39,7 +38,7 @@ func NewDiffRecord(input DiffRecordInput) DiffRecord { status: StatusRecord{targetPath: input.TargetPath, kind: input.Kind, action: input.Action}, safe: diff.NewSafeRecord(diff.SafeRecordInput{ TargetPath: input.TargetPath, - Tag: parseDiffTag(input.Tag), + Tag: diff.ParseTag(input.Tag), SourceLabel: input.SourceLabel, TargetLabel: input.TargetLabel, Lines: input.Lines, @@ -49,12 +48,6 @@ func NewDiffRecord(input DiffRecordInput) DiffRecord { } } -// parseDiffTag preserves the application boundary while delegating the tag -// vocabulary to the diff package. -func parseDiffTag(name string) diff.Tag { - return diff.ParseTag(name) -} - // DiffTagName returns the stable lowercase name of one record's safe tag. func DiffTagName(record DiffRecord) string { return record.safe.Tag().String() @@ -63,20 +56,13 @@ func DiffTagName(record DiffRecord) string { func (record DiffRecord) TargetPath() string { return record.status.TargetPath() } func (record DiffRecord) Kind() StatusKind { return record.status.Kind() } func (record DiffRecord) Action() string { return record.status.Action() } -func (record DiffRecord) Reason() string { return record.status.Reason() } -func (record DiffRecord) Converged() bool { return record.status.Converged() } +func (record DiffRecord) isConverged() bool { return record.status.converged } func (record DiffRecord) Tag() diff.Tag { return record.safe.Tag() } func (record DiffRecord) SourceLabel() string { return record.safe.SourceLabel() } func (record DiffRecord) TargetLabel() string { return record.safe.TargetLabel() } func (record DiffRecord) Lines() string { return record.safe.Lines() } func (record DiffRecord) SourceSize() int64 { return record.safe.SourceSize() } func (record DiffRecord) TargetSize() int64 { return record.safe.TargetSize() } -func (record DiffRecord) SourceHash() deployment.Digest { - return record.safe.SourceHash() -} -func (record DiffRecord) TargetHash() deployment.Digest { - return record.safe.TargetHash() -} // DiffResult is the frozen outcome of one diff translation: the // path-sorted safe diff/status records, the per-kind counts, and the overall diff --git a/internal/application/inspect/diff_test.go b/internal/application/inspect/diff_test.go index 27fb6e7..16cdb99 100644 --- a/internal/application/inspect/diff_test.go +++ b/internal/application/inspect/diff_test.go @@ -7,7 +7,6 @@ import ( "strings" "testing" - "github.com/alyraffauf/cattery/internal/deployment" "github.com/alyraffauf/cattery/internal/diff" "github.com/alyraffauf/cattery/internal/failure" "github.com/alyraffauf/cattery/internal/state" @@ -56,8 +55,7 @@ func testDiffAliasParity(t *testing.T) { statusRecord := singleRecord(t, status) diffRecord := singleDiffRecord(t, diffResult) if diffRecord.TargetPath() != statusRecord.TargetPath() || diffRecord.Kind() != statusRecord.Kind() || - diffRecord.Action() != statusRecord.Action() || diffRecord.Reason() != statusRecord.Reason() || - diffRecord.Converged() != statusRecord.Converged() { + diffRecord.Action() != statusRecord.Action() { t.Fatalf("diff record %+v differs from status record %+v", diffRecord, statusRecord) } } @@ -68,14 +66,12 @@ func testDiffRetirementParity(t *testing.T) { fx.rows.rows = stateRows{files: []state.FileBaseline{ fileRow(baselineInput{target: "old.conf", group: "g2", source: []byte("stale\n"), content: []byte("stale\n")}, nil), }} - status, statusErr := fx.service.Status(context.Background(), Request{Groups: []string{"g2"}}) + _, statusErr := fx.service.Status(context.Background(), Request{Groups: []string{"g2"}}) diffResult, diffErr := fx.service.Diff(context.Background(), Request{Groups: []string{"g2"}}) assertKind(t, statusErr, failure.Difference) assertKind(t, diffErr, failure.Difference) - statusRecord := singleRecord(t, status) diffRecord := singleDiffRecord(t, diffResult) - if diffRecord.Kind() != StatusKindRetired || diffRecord.Action() != "retire-state" || - diffRecord.Reason() != statusRecord.Reason() || diffRecord.Converged() { + if diffRecord.Kind() != StatusKindRetired || diffRecord.Action() != "retire-state" || diffResult.Converged() { t.Fatalf("record = %+v", diffRecord) } } @@ -136,10 +132,6 @@ func testDiffBinary(t *testing.T) { if record.SourceSize() != int64(len("alpha\n")) || record.TargetSize() != int64(len("alpha\x1bbeta\n")) { t.Fatalf("sizes = %d/%d", record.SourceSize(), record.TargetSize()) } - if record.SourceHash() != deployment.Ordinary([]byte("alpha\n")) || - record.TargetHash() != deployment.Ordinary([]byte("alpha\x1bbeta\n")) { - t.Fatalf("binary hashes mismatch") - } } func testDiffSecret(t *testing.T) { @@ -159,8 +151,7 @@ func testDiffSecret(t *testing.T) { if record.Tag() != diff.TagSecret || record.Action() != "needs-decision" { t.Fatalf("record = %+v", record) } - if record.Lines() != "" || record.SourceSize() != 0 || record.TargetSize() != 0 || - record.SourceHash() != (deployment.Digest{}) || record.TargetHash() != (deployment.Digest{}) { + if record.Lines() != "" || record.SourceSize() != 0 || record.TargetSize() != 0 { t.Fatalf("secret record leaked content or facts") } } @@ -178,7 +169,7 @@ func testDiffAliasOnly(t *testing.T) { result, err := fx.service.Diff(context.Background(), Request{}) assertKind(t, err, failure.Difference) record := singleDiffRecord(t, result) - if record.Kind() != StatusKindAlias || record.Action() != "create-alias" || record.Converged() { + if record.Kind() != StatusKindAlias || record.Action() != "create-alias" { t.Fatalf("record = %+v", record) } if result.Files() != 0 || result.Aliases() != 1 || result.Retired() != 0 || result.Converged() { diff --git a/internal/application/inspect/status.go b/internal/application/inspect/status.go index 206e489..3115d69 100644 --- a/internal/application/inspect/status.go +++ b/internal/application/inspect/status.go @@ -62,8 +62,7 @@ type StatusRecord struct { func (record StatusRecord) TargetPath() string { return record.targetPath } func (record StatusRecord) Kind() StatusKind { return record.kind } func (record StatusRecord) Action() string { return record.action } -func (record StatusRecord) Reason() string { return record.reason } -func (record StatusRecord) Converged() bool { return record.converged } +func (record StatusRecord) isConverged() bool { return record.converged } // StatusResult is the frozen outcome of one status translation: the // path-sorted semantic status records, the per-kind counts, and the overall @@ -180,9 +179,9 @@ func countRecordKinds[T interface{ Kind() StatusKind }](records []T) (files, ali return files, aliases, retired } -func recordsConvergedGeneric[T interface{ Converged() bool }](records []T) bool { +func recordsConvergedGeneric[T interface{ isConverged() bool }](records []T) bool { for _, record := range records { - if !record.Converged() { + if !record.isConverged() { return false } } diff --git a/internal/application/inspect/status_test.go b/internal/application/inspect/status_test.go index 4ee5830..4c6bdf1 100644 --- a/internal/application/inspect/status_test.go +++ b/internal/application/inspect/status_test.go @@ -82,7 +82,7 @@ func testStatusTargetDrift(t *testing.T) { assertKind(t, err, failure.Difference) record := singleRecord(t, result) if record.TargetPath() != "g1-file.conf" || record.Kind() != StatusKindFile || - record.Action() != "needs-decision" || record.Reason() != "target-drift" || record.Converged() { + record.Action() != "needs-decision" { t.Fatalf("record = %+v", record) } if result.Files() != 1 || result.Aliases() != 0 || result.Retired() != 0 || result.Converged() { @@ -100,7 +100,7 @@ func testStatusSourceChange(t *testing.T) { result, err := fx.service.Status(context.Background(), Request{}) assertKind(t, err, failure.Difference) record := singleRecord(t, result) - if record.Action() != "write-source-to-target" || record.Reason() != "source-changed" { + if record.Action() != "write-source-to-target" { t.Fatalf("record = %+v", record) } } @@ -139,8 +139,7 @@ func testStatusAliasCreate(t *testing.T) { result, err := fx.service.Status(context.Background(), Request{}) assertKind(t, err, failure.Difference) record := singleRecord(t, result) - if record.Kind() != StatusKindAlias || record.Action() != "create-alias" || - record.Reason() != "unbaselined-absent" || record.Converged() { + if record.Kind() != StatusKindAlias || record.Action() != "create-alias" { t.Fatalf("record = %+v", record) } if result.Aliases() != 1 || result.Converged() { @@ -185,8 +184,7 @@ func testStatusRetirePending(t *testing.T) { result, err := fx.service.Status(context.Background(), Request{Groups: []string{"g2"}}) assertKind(t, err, failure.Difference) record := singleRecord(t, result) - if record.Kind() != StatusKindRetired || record.Action() != "retire-state" || - record.Reason() != "source-removed" || record.Converged() { + if record.Kind() != StatusKindRetired || record.Action() != "retire-state" { t.Fatalf("record = %+v", record) } if result.Retired() != 1 || result.Converged() { @@ -207,8 +205,7 @@ func testStatusRetireDone(t *testing.T) { t.Fatalf("Status: %v", err) } record := singleRecord(t, result) - if record.Kind() != StatusKindRetired || record.Action() != "no-op" || - record.Reason() != "already-retired" || !record.Converged() { + if record.Kind() != StatusKindRetired || record.Action() != "no-op" { t.Fatalf("record = %+v", record) } if !result.Converged() { diff --git a/internal/cli/apply.go b/internal/cli/apply.go index 14cbcd7..9d684ce 100644 --- a/internal/cli/apply.go +++ b/internal/cli/apply.go @@ -56,14 +56,11 @@ func applyRequest(command *cobra.Command, input applyInput) apply.Request { nonInteractive, _ := command.Flags().GetBool("non-interactive") noHooks, _ := command.Flags().GetBool("no-hooks") return apply.Request{ - Repository: applyRepository(options, input.runtime), - Groups: append([]string(nil), input.groups...), - DryRun: dryRun, - DryRunSet: command.Flags().Changed("dry-run"), - NonInteractive: nonInteractive, - NonInteractiveSet: command.Flags().Changed("non-interactive"), - NoHooks: noHooks, - NoHooksSet: command.Flags().Changed("no-hooks"), + Repository: applyRepository(options, input.runtime), + Groups: append([]string(nil), input.groups...), + DryRun: dryRun, + NonInteractive: nonInteractive, + NoHooks: noHooks, } } diff --git a/internal/cli/apply_test.go b/internal/cli/apply_test.go index aaefec2..c6c5816 100644 --- a/internal/cli/apply_test.go +++ b/internal/cli/apply_test.go @@ -59,7 +59,7 @@ func testApplyFlags(t *testing.T) { if request.Repository.RawExplicit != "repo" || !request.Repository.ExplicitSet { t.Fatalf("repository = %+v, want the flag value", request.Repository) } - if !request.NonInteractive || !request.NonInteractiveSet || !request.NoHooks || !request.NoHooksSet { + if !request.NonInteractive || !request.NoHooks { t.Fatalf("policy = %+v, want the explicit flags", request) } if len(request.Groups) != 2 || request.Groups[0] != "apps" || request.Groups[1] != "tools" { @@ -90,8 +90,8 @@ func testApplyDryRun(t *testing.T) { t.Fatalf("run: %v", err) } request := service.requests[0] - if !request.DryRun || !request.DryRunSet { - t.Fatalf("dry run = %v set = %v, want the flag value", request.DryRun, request.DryRunSet) + if !request.DryRun { + t.Fatalf("dry run = %v, want the flag value", request.DryRun) } } diff --git a/internal/cli/render_add_test.go b/internal/cli/render_add_test.go index 7454006..7ce9d29 100644 --- a/internal/cli/render_add_test.go +++ b/internal/cli/render_add_test.go @@ -27,7 +27,7 @@ func testRenderAddItems(t *testing.T) { stdout := &bytes.Buffer{} result := add.Result{Items: []add.ItemResult{ {Target: "a.conf", Source: "a.conf", Status: add.StatusCompleted}, - {Target: "token", Source: "apps/token", Status: add.StatusCompleted, Secret: true}, + {Target: "token", Source: "apps/token", Status: add.StatusCompleted}, }, Summary: add.Summary{Completed: 2}} if err := renderAdd(stdout, result); err != nil { t.Fatalf("render: %v", err) diff --git a/internal/diff/safe.go b/internal/diff/safe.go index 547991d..8df8a2e 100644 --- a/internal/diff/safe.go +++ b/internal/diff/safe.go @@ -21,9 +21,7 @@ import ( type Tag int const ( - // TagNone marks equal content on both sides; only metadata such as a - // mode correction may remain to report. - TagNone Tag = iota + _ Tag = iota // TagText marks a printable unified diff computed from both sides. TagText // TagBinary marks a binary or oversized ordinary file; the record @@ -58,13 +56,10 @@ func ParseTag(name string) Tag { case "secret": return TagSecret default: - return TagNone + return Tag(0) } } -// Valid reports whether tag is one of the supported constants. -func (t Tag) Valid() bool { return t >= TagNone && t <= TagSecret } - // SafeRecordInput carries the renderable fields of one safe record. type SafeRecordInput struct { TargetPath string @@ -151,7 +146,7 @@ func textEligible(data []byte) bool { // Build derives the safe record for one file evaluation. The target bytes // must be the exact bytes captured beside the target snapshot; the record -// never retains them. Equal content yields TagNone, printable text at most +// never retains them. Equal content yields the zero tag, printable text at most // maxTextBytes per side yields a TagText unified diff with escaped labels, // and every other content difference yields TagBinary or TagSecret facts // (PLAN.md Section 9.6). diff --git a/internal/diff/safe_test.go b/internal/diff/safe_test.go index 23df3ea..4335406 100644 --- a/internal/diff/safe_test.go +++ b/internal/diff/safe_test.go @@ -57,7 +57,7 @@ var safeDiffCases = []safeDiffCase{ wantLines: []string{"-line1", "-line2"}}, {name: "metadata only", repoPath: "files/config", targetPath: "config", sourceBytes: []byte("same\n"), targetBytes: []byte("same\n"), - wantTag: TagNone, wantZero: true}, + wantTag: Tag(0), wantZero: true}, {name: "carriage return binary", repoPath: "files/config", targetPath: "config", sourceBytes: []byte("a\rb\n"), targetBytes: []byte("short\n"), wantTag: TagBinary}, @@ -83,7 +83,7 @@ var safeDiffCases = []safeDiffCase{ {name: "secret metadata only", kind: deployment.FileSecret, repoPath: "app/token", targetPath: "token", sourceBytes: []byte(`{"data":"c2VjcmV0","sops":{"version":"3.9.0"}}`), targetBytes: []byte(`{"data":"c2VjcmV0","sops":{"version":"3.9.0"}}`), - wantTag: TagNone, wantZero: true}, + wantTag: Tag(0), wantZero: true}, {name: "escaped labels", repoPath: "files/\x1bconfig", targetPath: "config\x1b", sourceBytes: []byte("alpha\nbeta\n"), targetBytes: []byte("alpha\ngamma\n"), wantTag: TagText, wantSource: "repo/files/\\x1bconfig", wantTarget: "$HOME/config\\x1b", diff --git a/internal/filesystem/hardlink_test.go b/internal/filesystem/hardlink_test.go deleted file mode 100644 index 1986ad0..0000000 --- a/internal/filesystem/hardlink_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package filesystem - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/alyraffauf/cattery/internal/pathsafe" -) - -func TestHardLinkMode(t *testing.T) { - scenarios := []struct { - name string - run func(*testing.T) - }{ - {"multiply linked target is replaced, not chmod'd", testLinkedTargetReplaced}, - {"replacement leaves the other link untouched", testOtherLinkUntouched}, - {"replacement preserves the target bytes", testLinkedReplacementBytes}, - } - for _, scenario := range scenarios { - t.Run(scenario.name, scenario.run) - } -} - -// linkSpec names two hard links to one inode inside a test root. -type linkSpec struct { - root, first, second string -} - -// linkPair creates two hard links to one inode. -func linkPair(t *testing.T, spec linkSpec) { - t.Helper() - must(t, os.WriteFile(filepath.Join(spec.root, spec.first), []byte("shared\n"), 0o644)) - must(t, os.Link(filepath.Join(spec.root, spec.first), filepath.Join(spec.root, spec.second))) -} - -func testLinkedTargetReplaced(t *testing.T) { - root := t.TempDir() - linkPair(t, linkSpec{root: root, first: "app", second: "alias.conf"}) - precondition := mustFreeze(t, root, "app") - replacer := NewReplacer() - must(t, replacer.ApplyMode(context.Background(), precondition, 0o755)) - app, err := os.Stat(filepath.Join(root, "app")) - if err != nil { - t.Fatalf("stat app: %v", err) - } - alias, err := os.Stat(filepath.Join(root, "alias.conf")) - if err != nil { - t.Fatalf("stat alias: %v", err) - } - if os.SameFile(app, alias) { - t.Fatal("linked target must be replaced by a fresh inode") - } - if app.Mode().Perm() != 0o755 { - t.Fatalf("app mode = %04o, want 0755", app.Mode().Perm()) - } - if alias.Mode().Perm() != 0o644 { - t.Fatalf("alias mode = %04o, want 0644 untouched", alias.Mode().Perm()) - } -} - -func testOtherLinkUntouched(t *testing.T) { - root := t.TempDir() - linkPair(t, linkSpec{root: root, first: "app", second: "alias.conf"}) - appBefore, err := pathsafe.FilesystemIdentity(filepath.Join(root, "app")) - if err != nil { - t.Fatalf("identity app: %v", err) - } - precondition := mustFreeze(t, root, "app") - replacer := NewReplacer() - must(t, replacer.ApplyMode(context.Background(), precondition, 0o700)) - alias := mustCapture(t, filepath.Join(root, "alias.conf")) - if alias.Mode() != 0o644 { - t.Fatalf("alias mode = %04o, want 0644", alias.Mode()) - } - appAfter := mustCapture(t, filepath.Join(root, "app")) - if pathsafe.SameIdentity(appBefore, appAfter.Identity()) { - t.Fatal("multiply linked target must be replaced, not chmod'd") - } -} - -func testLinkedReplacementBytes(t *testing.T) { - root := t.TempDir() - linkPair(t, linkSpec{root: root, first: "app", second: "alias.conf"}) - precondition := mustFreeze(t, root, "app") - replacer := NewReplacer() - must(t, replacer.ApplyMode(context.Background(), precondition, 0o711)) - if content := readFile(t, filepath.Join(root, "app")); content != "shared\n" { - t.Fatalf("app content = %q, want preserved bytes", content) - } - if content := readFile(t, filepath.Join(root, "alias.conf")); content != "shared\n" { - t.Fatalf("alias content = %q, want preserved bytes", content) - } -} diff --git a/internal/filesystem/helpers_test.go b/internal/filesystem/helpers_test.go index 41817bd..2344fc9 100644 --- a/internal/filesystem/helpers_test.go +++ b/internal/filesystem/helpers_test.go @@ -34,15 +34,6 @@ func mustCapture(t *testing.T, path string) TargetFacts { return facts } -func mustSource(t *testing.T, path string) SourceFacts { - t.Helper() - facts, err := FreezeSource(path) - if err != nil { - t.Fatalf("FreezeSource(%q): %v", path, err) - } - return facts -} - func mustRejectFreeze(t *testing.T, root, relative string) { t.Helper() if _, err := Freeze(Destination{Root: root, Relative: relative}); err == nil { diff --git a/internal/filesystem/mode.go b/internal/filesystem/mode.go index 899c32e..1835804 100644 --- a/internal/filesystem/mode.go +++ b/internal/filesystem/mode.go @@ -1,11 +1,7 @@ package filesystem import ( - "context" - "fmt" - "io" "io/fs" - "os" "github.com/alyraffauf/cattery/internal/deployment" ) @@ -33,46 +29,3 @@ func OrdinaryTargetMode(existing fs.FileMode, sourceExec fs.FileMode, absent boo func SecretTargetMode(sourceExec fs.FileMode) fs.FileMode { return deployment.SecretTargetMode(sourceExec) } - -// ApplyMode rematerializes the target even when it has one link. This avoids a -// chmod race and makes mode-only corrections obey the same identity and atomic -// publication rules as content changes. -func (r *Replacer) ApplyMode(ctx context.Context, precondition Precondition, desired fs.FileMode) error { - if err := ctx.Err(); err != nil { - return err - } - if err := precondition.Revalidate(); err != nil { - return err - } - content, err := readTargetContent(precondition) - if err != nil { - return err - } - return r.Replace(ctx, precondition, ReplacementSpec{Content: content, Mode: desired}) -} - -// readTargetContent reads the destination bytes bound to the frozen -// precondition identity. Opening the target and re-stating the descriptor -// closes the Lstat-to-read TOCTOU gap: a swap between Revalidate and the -// read either changes the inode (SameFileInfo fails) or the rename-based -// publication in Replace still rejects a stale identity. -func readTargetContent(precondition Precondition) ([]byte, error) { - path := targetPath(precondition.Destination()) - handle, err := os.Open(path) - if err != nil { - return nil, fmt.Errorf("filesystem: read target %s: %w", path, err) - } - defer handle.Close() - info, err := handle.Stat() - if err != nil { - return nil, fmt.Errorf("filesystem: read target %s: %w", path, err) - } - if !precondition.Target().Identity().SameFileInfo(info) { - return nil, fmt.Errorf("filesystem: target identity changed at %s", path) - } - content, err := io.ReadAll(handle) - if err != nil { - return nil, fmt.Errorf("filesystem: read target %s: %w", path, err) - } - return content, nil -} diff --git a/internal/filesystem/mode_test.go b/internal/filesystem/mode_test.go index 01c3846..a20727b 100644 --- a/internal/filesystem/mode_test.go +++ b/internal/filesystem/mode_test.go @@ -6,8 +6,6 @@ import ( "path/filepath" "syscall" "testing" - - "github.com/alyraffauf/cattery/internal/pathsafe" ) func TestTargetMode(t *testing.T) { @@ -17,9 +15,7 @@ func TestTargetMode(t *testing.T) { }{ {"new ordinary target defaults to 0644 plus source executable bits", testNewOrdinaryMode}, {"existing ordinary target preserves its read/write bits", testExistingOrdinaryMode}, - {"executable-only correction replaces only the executable bits", testExecutableOnlyCorrection}, {"secret target is exactly 0600 or 0700", testSecretMode}, - {"mode-only correction chmods a singly linked target in place", testModeOnlyChmod}, {"restrictive umask cannot clamp the derived mode", testRestrictiveUmask}, } for _, scenario := range scenarios { @@ -48,22 +44,6 @@ func testExistingOrdinaryMode(t *testing.T) { } } -func testExecutableOnlyCorrection(t *testing.T) { - root := t.TempDir() - target := filepath.Join(root, "app") - must(t, os.WriteFile(target, []byte("run\n"), 0o755)) - precondition := mustFreeze(t, root, "app") - replacer := NewReplacer() - must(t, replacer.ApplyMode(context.Background(), precondition, 0o644)) - info, err := os.Stat(target) - if err != nil || info.Mode().Perm() != 0o644 { - t.Fatalf("mode = %v, want 0644", info.Mode()) - } - if content := readFile(t, target); content != "run\n" { - t.Fatalf("content = %q, want untouched", content) - } -} - func testSecretMode(t *testing.T) { if mode := SecretTargetMode(0); mode != 0o600 { t.Fatalf("non-executable secret = %04o, want 0600", mode) @@ -76,29 +56,6 @@ func testSecretMode(t *testing.T) { } } -func testModeOnlyChmod(t *testing.T) { - root := t.TempDir() - target := filepath.Join(root, "app") - must(t, os.WriteFile(target, []byte("run\n"), 0o644)) - precondition := mustFreeze(t, root, "app") - before := mustCapture(t, target) - replacer := NewReplacer() - must(t, replacer.ApplyMode(context.Background(), precondition, 0o755)) - after := mustCapture(t, target) - if before.Identity().Path() != after.Identity().Path() { - t.Fatal("mode-only correction changed the destination path") - } - if before.Identity().Path() == "" { - t.Fatal("mode-only correction lost the target identity") - } - if pathsafe.SameIdentity(before.Identity(), after.Identity()) { - t.Fatal("mode-only correction must rematerialize the target") - } - if after.Mode() != 0o755 { - t.Fatalf("mode = %04o, want 0755", after.Mode()) - } -} - func testRestrictiveUmask(t *testing.T) { root := t.TempDir() previous := syscall.Umask(0o077) diff --git a/internal/filesystem/precondition_test.go b/internal/filesystem/precondition_test.go index bc3b4b1..eab953c 100644 --- a/internal/filesystem/precondition_test.go +++ b/internal/filesystem/precondition_test.go @@ -28,7 +28,6 @@ func TestFilesystemPrecondition(t *testing.T) { {"kind change detected", testKindChange}, {"blocking parents are rejected", testBlockingParents}, {"freeze never mutates", testFreezeNoMutation}, - {"source facts freeze and revalidate", testSourceFacts}, } for _, scenario := range scenarios { t.Run(scenario.name, scenario.run) diff --git a/internal/filesystem/source.go b/internal/filesystem/source.go deleted file mode 100644 index 20a8cf6..0000000 --- a/internal/filesystem/source.go +++ /dev/null @@ -1,50 +0,0 @@ -package filesystem - -import ( - "fmt" - "io/fs" - - "github.com/alyraffauf/cattery/internal/pathsafe" -) - -// SourceFacts freezes the read-only facts of a deployment source: identity, -// exact content token, and the executable bits that must reach the target. -type SourceFacts struct { - entry TargetFacts - executable fs.FileMode -} - -// FreezeSource captures the facts of an existing ordinary source file and -// rejects symlink and special sources. -func FreezeSource(path string) (SourceFacts, error) { - entry, err := CaptureTarget(path) - if err != nil { - return SourceFacts{}, err - } - if entry.Kind() != KindFile { - return SourceFacts{}, fmt.Errorf("filesystem: source %s is not a regular file", path) - } - return SourceFacts{entry: entry, executable: entry.Mode() & 0o111}, nil -} - -// Token returns the frozen source content token. -func (s SourceFacts) Token() ContentToken { return s.entry.Token() } - -// Executable returns the source executable bits for the target. -func (s SourceFacts) Executable() fs.FileMode { return s.executable } - -// Revalidate re-checks that the source still carries the same identity, -// content token, and executable bits. -func (s SourceFacts) Revalidate() error { - if err := s.entry.Revalidate(); err != nil { - return err - } - identity, err := pathsafe.FilesystemIdentity(s.entry.Identity().Path()) - if err != nil { - return err - } - if identity.Mode().Perm()&0o111 != s.executable { - return fmt.Errorf("filesystem: source executable bits changed at %s", s.entry.Identity().Path()) - } - return nil -} diff --git a/internal/filesystem/source_test.go b/internal/filesystem/source_test.go deleted file mode 100644 index db82580..0000000 --- a/internal/filesystem/source_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package filesystem - -import ( - "os" - "path/filepath" - "testing" - - testfs "github.com/alyraffauf/cattery/internal/testfixture/filesystem" -) - -func testSourceFacts(t *testing.T) { - root := t.TempDir() - must(t, testfs.New(root).File("src.sh", []byte("#!/bin/sh\necho hi\n"), 0o755).Materialize()) - path := filepath.Join(root, "src.sh") - facts := mustSource(t, path) - if facts.Executable() != 0o111 { - t.Fatalf("executable = %o, want 111", facts.Executable()) - } - if facts.Token() != TokenOfContent([]byte("#!/bin/sh\necho hi\n")) { - t.Fatal("source token must match exact content") - } - must(t, facts.Revalidate()) - must(t, os.Chmod(path, 0o600)) - mustFail(t, facts.Revalidate()) - must(t, os.WriteFile(path, []byte("#!/bin/sh\necho bye\n"), 0o600)) - mustFail(t, facts.Revalidate()) -} diff --git a/internal/reconcile/precondition_test.go b/internal/reconcile/precondition_test.go index 50a7f22..ada8c55 100644 --- a/internal/reconcile/precondition_test.go +++ b/internal/reconcile/precondition_test.go @@ -18,7 +18,6 @@ func TestSnapshotPrecondition(t *testing.T) { {"missing parents tolerated", testPreconditionMissingParents}, {"parent race", testPreconditionParentRace}, {"final symlink never followed", testPreconditionFinalSymlink}, - {"content change", testPreconditionContentChange}, {"absent to present", testPreconditionAbsentTransition}, {"mode change", testPreconditionModeChange}, {"object replacement", testPreconditionIdentityReplacement}, @@ -91,18 +90,6 @@ func testPreconditionFinalSymlink(t *testing.T) { } } -func testPreconditionContentChange(t *testing.T) { - root := t.TempDir() - path := filepath.Join(root, "file") - mustTargetFile(t, path, []byte("first")) - first := captureAt(t, root, "file") - mustTargetFile(t, path, []byte("second")) - second := captureAt(t, root, "file") - if first.Token() != TokenOfContent([]byte("first")) || second.Token() != TokenOfContent([]byte("second")) { - t.Fatal("frozen precondition token must stay put while re-capture sees new content") - } -} - func testPreconditionAbsentTransition(t *testing.T) { root := t.TempDir() first := captureAt(t, root, "file") diff --git a/internal/reconcile/snapshot_helpers_test.go b/internal/reconcile/snapshot_helpers_test.go index bb8072d..db37491 100644 --- a/internal/reconcile/snapshot_helpers_test.go +++ b/internal/reconcile/snapshot_helpers_test.go @@ -85,7 +85,7 @@ func requireFileJoin(t *testing.T, record Evaluation, target string) { if record.Entry != PlanEntryFile || record.File.TargetRelativePath != target || record.FileState == nil || !record.FileState.Active() { t.Fatalf("record %s must join its file descriptor and row", target) } - if record.Target.Kind() != KindFile || record.Source.Snapshot().Token() != TokenOfContent([]byte("source "+target)) { + if record.Target.Kind() != KindFile { t.Fatalf("record %s must join target and source observations", target) } } diff --git a/internal/reconcile/source_snapshot_test.go b/internal/reconcile/source_snapshot_test.go index c0bd464..bed6aae 100644 --- a/internal/reconcile/source_snapshot_test.go +++ b/internal/reconcile/source_snapshot_test.go @@ -58,7 +58,7 @@ func testSourceOrdinary(t *testing.T) { t.Fatal(err) } snapshot := observation.Snapshot() - if snapshot.Kind() != KindFile || snapshot.Token() != TokenOfContent(data) || snapshot.Semantic() != deployment.Ordinary(data) { + if snapshot.Kind() != KindFile || snapshot.Semantic() != deployment.Ordinary(data) { t.Fatal("ordinary snapshot did not use exact bytes") } if snapshot.Storage() != (deployment.Digest{}) || string(observation.Bytes()) != string(data) { @@ -80,7 +80,7 @@ func testSourceSecret(t *testing.T) { t.Fatal(err) } snapshot := observation.Snapshot() - if snapshot.Token() != TokenOfContent(data) || snapshot.Storage() != deployment.RawStorage(data) || snapshot.Semantic() != (deployment.Digest{}) { + if snapshot.Storage() != deployment.RawStorage(data) || snapshot.Semantic() != (deployment.Digest{}) { t.Fatal("secret snapshot did not preserve raw-storage identity") } if _, err := os.Stat(path); err != nil || !pathsafe.SameIdentity(snapshot.Identity(), mustIdentity(t, path)) { diff --git a/internal/reconcile/target_snapshot_test.go b/internal/reconcile/target_snapshot_test.go index ad7827f..599e005 100644 --- a/internal/reconcile/target_snapshot_test.go +++ b/internal/reconcile/target_snapshot_test.go @@ -61,15 +61,12 @@ func testTargetAbsent(t *testing.T) { if snapshot.Kind() != KindAbsent || snapshot.Identity().Path() != "" { t.Fatalf("absent target facts = %v, want KindAbsent with zero identity", snapshot.Kind()) } - if snapshot.Token() != (ContentToken{}) || snapshot.Digest() != (deployment.Digest{}) { - t.Fatal("absent target must carry zero hashes") + if snapshot.Digest() != (deployment.Digest{}) { + t.Fatal("absent target must carry a zero digest") } if snapshot.Mode() != 0 || snapshot.Payload() != "" { t.Fatal("absent target must carry zero mode and payload") } - if snapshot.Destination() != (Destination{Root: root, Relative: "missing.txt"}) { - t.Fatal("destination must echo the captured path") - } parent, err := pathsafe.FilesystemIdentity(root) if err != nil { t.Fatalf("stat parent: %v", err) @@ -103,9 +100,6 @@ func testTargetRegularHashes(t *testing.T) { content := []byte("managed content") mustTargetFile(t, filepath.Join(root, "file.txt"), content) snapshot := captureAt(t, root, "file.txt") - if snapshot.Token() != TokenOfContent(content) { - t.Fatal("token must match the exact bytes") - } if snapshot.Digest() != deployment.Ordinary(content) { t.Fatal("digest must match the exact bytes") } @@ -132,8 +126,8 @@ func testTargetDirectory(t *testing.T) { if snapshot.Kind() != KindDirectory || snapshot.Mode() != 0o700 { t.Fatalf("directory facts = %v mode %o, want KindDirectory 0700", snapshot.Kind(), snapshot.Mode()) } - if snapshot.Token() != (ContentToken{}) || snapshot.Payload() != "" { - t.Fatal("directory must carry zero token and payload") + if snapshot.Payload() != "" { + t.Fatal("directory must carry zero payload") } } @@ -164,8 +158,8 @@ func testTargetSpecial(t *testing.T) { if snapshot.Kind() != KindSpecial { t.Fatalf("fifo kind = %v, want KindSpecial", snapshot.Kind()) } - if snapshot.Token() != (ContentToken{}) || snapshot.Payload() != "" { - t.Fatal("special target must carry zero token and payload") + if snapshot.Payload() != "" { + t.Fatal("special target must carry zero payload") } } @@ -178,11 +172,8 @@ func testTargetNested(t *testing.T) { t.Fatalf("write deep target: %v", err) } snapshot := captureAt(t, root, "a/b/file.txt") - if snapshot.Kind() != KindFile || snapshot.Token() != TokenOfContent([]byte("deep")) { - t.Fatal("nested target must freeze its content") - } - if snapshot.Destination() != (Destination{Root: root, Relative: "a/b/file.txt"}) { - t.Fatal("destination must echo the nested path") + if snapshot.Kind() != KindFile { + t.Fatal("nested target must freeze its file kind") } } @@ -194,8 +185,8 @@ func testTargetHardLinks(t *testing.T) { } first := captureAt(t, root, "first") second := captureAt(t, root, "second") - if !pathsafe.SameIdentity(first.Identity(), second.Identity()) || first.Token() != second.Token() { - t.Fatal("hard links must share one object identity and content token") + if !pathsafe.SameIdentity(first.Identity(), second.Identity()) { + t.Fatal("hard links must share one object identity") } } diff --git a/internal/reconcile/types.go b/internal/reconcile/types.go index 3d68c50..ecf44ae 100644 --- a/internal/reconcile/types.go +++ b/internal/reconcile/types.go @@ -171,7 +171,6 @@ type SourceSnapshot struct { func (snapshot SourceSnapshot) Path() string { return snapshot.path } func (snapshot SourceSnapshot) Identity() pathsafe.Identity { return snapshot.identity } func (snapshot SourceSnapshot) Kind() EntryKind { return snapshot.kind } -func (snapshot SourceSnapshot) Token() ContentToken { return snapshot.token } // Semantic returns the ordinary semantic digest; secret snapshots leave it zero. func (snapshot SourceSnapshot) Semantic() deployment.Digest { return snapshot.semantic } @@ -193,11 +192,9 @@ type TargetSnapshot struct { payload string } -func (snapshot TargetSnapshot) Destination() Destination { return snapshot.destination } func (snapshot TargetSnapshot) Parent() pathsafe.Identity { return snapshot.parent } func (snapshot TargetSnapshot) Kind() EntryKind { return snapshot.kind } func (snapshot TargetSnapshot) Identity() pathsafe.Identity { return snapshot.identity } -func (snapshot TargetSnapshot) Token() ContentToken { return snapshot.token } func (snapshot TargetSnapshot) Digest() deployment.Digest { return snapshot.digest } func (snapshot TargetSnapshot) Mode() fs.FileMode { return snapshot.mode } func (snapshot TargetSnapshot) Payload() string { return snapshot.payload } diff --git a/internal/reconcile/types_test.go b/internal/reconcile/types_test.go index 963bc31..7e2b425 100644 --- a/internal/reconcile/types_test.go +++ b/internal/reconcile/types_test.go @@ -164,14 +164,11 @@ func testSnapshotAccessors(t *testing.T) { kind: KindFile, token: TokenOfContent([]byte("x")), digest: deployment.Ordinary([]byte("x")), mode: 0o755, payload: "payload", } - if snapshot.Destination() != snapshot.destination { - t.Fatal("Destination must echo its field") - } if snapshot.Kind() != snapshot.kind || snapshot.Mode() != snapshot.mode { t.Fatal("kind and mode must echo their fields") } - if snapshot.Token() != snapshot.token || snapshot.Digest() != snapshot.digest { - t.Fatal("hashes must echo their fields") + if snapshot.Digest() != snapshot.digest { + t.Fatal("digest must echo its field") } if snapshot.Payload() != snapshot.payload { t.Fatal("payload must echo its field") @@ -193,8 +190,8 @@ func testSourceSnapshotAccessors(t *testing.T) { if source.Path() != source.path || source.Kind() != source.kind { t.Fatal("path and kind must echo their fields") } - if source.Token() != source.token || source.Semantic() != source.semantic { - t.Fatal("token and semantic must echo their fields") + if source.Semantic() != source.semantic { + t.Fatal("semantic must echo its field") } if source.Storage() != source.storage || source.Executable() != source.executable { t.Fatal("storage and executable must echo their fields") @@ -206,10 +203,10 @@ func testTargetZeroValue(t *testing.T) { if zero.Kind() != KindAbsent || zero.Identity().Path() != "" || zero.Parent().Path() != "" { t.Fatal("zero-value target snapshot must report absent facts") } - if zero.Token() != (ContentToken{}) || zero.Digest() != (deployment.Digest{}) { - t.Fatal("zero-value target snapshot must carry zero hashes") + if zero.Digest() != (deployment.Digest{}) { + t.Fatal("zero-value target snapshot must carry a zero digest") } - if zero.Mode() != 0 || zero.Payload() != "" || zero.Destination() != (Destination{}) { + if zero.Mode() != 0 || zero.Payload() != "" { t.Fatal("zero-value target snapshot must carry zero facts") } } diff --git a/internal/repository/controls.go b/internal/repository/controls.go index 3ad23e3..61fcad3 100644 --- a/internal/repository/controls.go +++ b/internal/repository/controls.go @@ -66,37 +66,3 @@ func ClassifyPlatformLayer(name string) Control { } return ControlNone } - -// IsScopeControl reports whether name is one of the recognized scope-root -// controls. Unknown underscore entries, metadata, and ordinary names are not -// scope controls. -func IsScopeControl(name string) bool { - switch ClassifyRoot(name) { - case ControlDarwin, ControlLinux, ControlSecrets, ControlHooks, ControlRoutes: - return true - } - return false -} - -// String renders control as a stable lowercase label for diagnostics. -func (control Control) String() string { - switch control { - case ControlNone: - return "none" - case ControlDarwin: - return "darwin" - case ControlLinux: - return "linux" - case ControlSecrets: - return "secrets" - case ControlHooks: - return "hooks" - case ControlRoutes: - return "routes" - case ControlIgnoredUnderscore: - return "ignored-underscore" - case ControlMetadata: - return "metadata" - } - return "unknown" -} diff --git a/internal/repository/controls_test.go b/internal/repository/controls_test.go index a489c1c..a935218 100644 --- a/internal/repository/controls_test.go +++ b/internal/repository/controls_test.go @@ -12,7 +12,6 @@ func TestRepositoryControls(t *testing.T) { {"repository metadata", testMetadata}, {"ordinary names", testOrdinaryNames}, {"platform layer classification", testPlatformLayer}, - {"scope control predicate", testScopeControlPredicate}, } for _, scenario := range scenarios { t.Run(scenario.name, scenario.run) @@ -32,7 +31,7 @@ func testKnownControls(t *testing.T) { } for _, scenario := range scenarios { if got := ClassifyRoot(scenario.name); got != scenario.want { - t.Fatalf("ClassifyRoot(%q) = %s, want %s", scenario.name, got, scenario.want) + t.Fatalf("ClassifyRoot(%q) = %d, want %d", scenario.name, got, scenario.want) } } } @@ -40,7 +39,7 @@ func testKnownControls(t *testing.T) { func testIgnoredUnderscore(t *testing.T) { for _, name := range []string{"_notes", "_README.md", "_experiments"} { if got := ClassifyRoot(name); got != ControlIgnoredUnderscore { - t.Fatalf("ClassifyRoot(%q) = %s, want ignored-underscore", name, got) + t.Fatalf("ClassifyRoot(%q) = %d, want ignored-underscore", name, got) } } } @@ -52,7 +51,7 @@ func testMetadata(t *testing.T) { } for _, name := range names { if got := ClassifyRoot(name); got != ControlMetadata { - t.Fatalf("ClassifyRoot(%q) = %s, want metadata", name, got) + t.Fatalf("ClassifyRoot(%q) = %d, want metadata", name, got) } } } @@ -60,36 +59,23 @@ func testMetadata(t *testing.T) { func testOrdinaryNames(t *testing.T) { for _, name := range []string{"Brewfile", ".config", "atuin", "README.md"} { if got := ClassifyRoot(name); got != ControlNone { - t.Fatalf("ClassifyRoot(%q) = %s, want none", name, got) + t.Fatalf("ClassifyRoot(%q) = %d, want none", name, got) } } } func testPlatformLayer(t *testing.T) { if got := ClassifyPlatformLayer("_secrets"); got != ControlSecrets { - t.Fatalf("ClassifyPlatformLayer(_secrets) = %s, want secrets", got) + t.Fatalf("ClassifyPlatformLayer(_secrets) = %d, want secrets", got) } for _, name := range []string{"_darwin", "_hooks", "_routes.toml", "_notes"} { if got := ClassifyPlatformLayer(name); got != ControlIgnoredUnderscore { - t.Fatalf("ClassifyPlatformLayer(%q) = %s, want ignored-underscore", name, got) + t.Fatalf("ClassifyPlatformLayer(%q) = %d, want ignored-underscore", name, got) } } for _, name := range []string{".config", "Brewfile"} { if got := ClassifyPlatformLayer(name); got != ControlNone { - t.Fatalf("ClassifyPlatformLayer(%q) = %s, want none", name, got) - } - } -} - -func testScopeControlPredicate(t *testing.T) { - for _, name := range []string{"_darwin", "_linux", "_secrets", "_hooks", "_routes.toml"} { - if !IsScopeControl(name) { - t.Fatalf("IsScopeControl(%q) = false, want true", name) - } - } - for _, name := range []string{"_notes", ".git", "Brewfile"} { - if IsScopeControl(name) { - t.Fatalf("IsScopeControl(%q) = true, want false", name) + t.Fatalf("ClassifyPlatformLayer(%q) = %d, want none", name, got) } } } diff --git a/internal/repository/scan.go b/internal/repository/scan.go index ecc4452..845e095 100644 --- a/internal/repository/scan.go +++ b/internal/repository/scan.go @@ -1,7 +1,6 @@ package repository import ( - "errors" "fmt" "io/fs" "os" @@ -23,25 +22,14 @@ type Candidate struct { ExecutableBits fs.FileMode } -// HookCandidate is one raw regular-file child of a scope _hooks tree; hook -// semantics are validated by hook discovery, not by the scanner. -type HookCandidate struct { - Scope deployment.Scope - Phase deployment.HookPhase - Name string - AbsolutePath string - RepositoryPath string -} - // ScanResult is the deterministic output of one repository scan. type ScanResult struct { Groups []string Files []Candidate - Hooks []HookCandidate } -// Scan returns the base-layer candidates, raw hook candidates, and group -// names of the repository at root in deterministic order. Symlinks and +// Scan returns the base-layer candidates and group names of the repository at +// root in deterministic order. Symlinks and // special entries are rejected. func Scan(root string) (ScanResult, error) { scanner := scopeScanner{repoRoot: root, rootTree: true} @@ -51,7 +39,7 @@ func Scan(root string) (ScanResult, error) { if err := checkGroupCollisions(scanner.groups); err != nil { return ScanResult{}, err } - return ScanResult{Groups: scanner.groups, Files: scanner.files, Hooks: scanner.hooks}, nil + return ScanResult{Groups: scanner.groups, Files: scanner.files}, nil } type scopeScanner struct { @@ -61,7 +49,6 @@ type scopeScanner struct { rootTree bool groups []string files []Candidate - hooks []HookCandidate } func (scanner *scopeScanner) scanScopeRoot() error { @@ -90,8 +77,6 @@ func (scanner *scopeScanner) scanEntry(entry os.DirEntry) error { return scanner.scanOrdinary(entry) case control == ControlSecrets: return scanner.scanSecrets(entry) - case control == ControlHooks: - return scanner.scanHooks(entry) case control == ControlMetadata: if scanner.rootTree { // Repository metadata is ignored only at the repository root. @@ -136,44 +121,6 @@ func (scanner *scopeScanner) scanSecrets(entry os.DirEntry) error { return scanner.walkTree(entry.Name(), deployment.FileSecret) } -func (scanner *scopeScanner) scanHooks(entry os.DirEntry) error { - if !entry.IsDir() { - return fmt.Errorf("repository: control %q is not a directory", entry.Name()) - } - for _, phase := range []deployment.HookPhase{deployment.HookBefore, deployment.HookAfter} { - if err := scanner.scanHookPhase(entry.Name(), phase); err != nil { - return err - } - } - return nil -} - -func (scanner *scopeScanner) scanHookPhase(hooksDir string, phase deployment.HookPhase) error { - path := filepath.Join(scanner.repoRoot, scanner.scopeRoot, hooksDir, string(phase)) - info, err := os.Lstat(path) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return nil - } - return err - } - if !info.IsDir() { - // Missing hook phases are optional; a non-directory phase is ignored like - // an absent phase because hook discovery only consumes regular children. - return nil - } - entries, err := os.ReadDir(path) - if err != nil { - return err - } - for _, entry := range entries { - if entry.Type().IsRegular() { - scanner.hooks = append(scanner.hooks, scanner.hookCandidate(hooksDir, phase, entry.Name())) - } - } - return nil -} - func (scanner *scopeScanner) walkTree(relative string, kind deployment.FileKind) error { entries, err := os.ReadDir(filepath.Join(scanner.repoRoot, scanner.scopeRoot, relative)) if err != nil { @@ -215,17 +162,6 @@ func (scanner *scopeScanner) addFileAt(relative string, entry os.DirEntry, kind return nil } -func (scanner *scopeScanner) hookCandidate(hooks string, phase deployment.HookPhase, name string) HookCandidate { - path := filepath.Join(scanner.scopeRoot, hooks, string(phase), name) - return HookCandidate{ - Scope: scanner.scope, - Phase: phase, - Name: name, - AbsolutePath: filepath.Join(scanner.repoRoot, path), - RepositoryPath: path, - } -} - func (scanner *scopeScanner) newNonRegularSourceError(relative string) error { return fmt.Errorf("repository: non-regular source entry %q", filepath.Join(scanner.scopeRoot, relative)) } diff --git a/internal/repository/scan_test.go b/internal/repository/scan_test.go index 46bd7d2..7f8d595 100644 --- a/internal/repository/scan_test.go +++ b/internal/repository/scan_test.go @@ -20,7 +20,6 @@ func TestRepositoryScan(t *testing.T) { {"groups", testScanGroups}, {"empty files", testScanEmptyFiles}, {"controls are excluded", testScanControlsExcluded}, - {"raw hook candidates", testScanHookCandidates}, {"symlinks rejected", testScanSymlinkRejected}, {"specials rejected", testScanSpecialRejected}, {"group collisions", testScanGroupCollisions}, @@ -112,21 +111,6 @@ func testScanControlsExcluded(t *testing.T) { assertCandidate(t, result.Files[0], newCandidate(root, wantFile{repoPath: "_secrets/token", secret: true})) } -func testScanHookCandidates(t *testing.T) { - root := t.TempDir() - writeFile(t, filepath.Join(root, "_hooks", "before", "install.sh"), 0o755) - writeFile(t, filepath.Join(root, "atuin", "_hooks", "after", "finish.sh"), 0o755) - result, err := Scan(root) - if err != nil { - t.Fatal(err) - } - if len(result.Hooks) != 2 { - t.Fatalf("hooks = %d, want 2", len(result.Hooks)) - } - assertHook(t, result.Hooks[0], HookCandidate{Scope: deployment.NewScope(""), Phase: deployment.HookBefore, Name: "install.sh", RepositoryPath: "_hooks/before/install.sh", AbsolutePath: filepath.Join(root, "_hooks", "before", "install.sh")}) - assertHook(t, result.Hooks[1], HookCandidate{Scope: deployment.NewScope("atuin"), Phase: deployment.HookAfter, Name: "finish.sh", RepositoryPath: "atuin/_hooks/after/finish.sh", AbsolutePath: filepath.Join(root, "atuin", "_hooks", "after", "finish.sh")}) -} - func testScanSymlinkRejected(t *testing.T) { root := t.TempDir() writeFile(t, filepath.Join(root, "target"), 0o644) @@ -211,13 +195,6 @@ func assertCandidate(t *testing.T, got Candidate, want Candidate) { } } -func assertHook(t *testing.T, got HookCandidate, want HookCandidate) { - t.Helper() - if got != want { - t.Fatalf("hook candidate = %+v, want %+v", got, want) - } -} - func writeFile(t *testing.T, path string, mode os.FileMode) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { -- 2.51.2