diff --git a/.changes/unreleased/+apt-transaction-empty-marker.yaml b/.changes/unreleased/+apt-transaction-empty-marker.yaml new file mode 100644 index 00000000..51755047 --- /dev/null +++ b/.changes/unreleased/+apt-transaction-empty-marker.yaml @@ -0,0 +1,2 @@ +kind: Fixed +body: Accept valid APT install transaction records with one trailing empty marker. diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index dcff50ac..d8f0d6be 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -1,6 +1,6 @@ --- status: Active -updated: 2026-08-04 +updated: 2026-08-14 summary: Active planning surface for Reploy design and implementation gaps. --- @@ -31,14 +31,6 @@ This file is the day-to-day queue for design and implementation gaps. ## Pre-release -- [ ] `P1` Accept APT install transaction records with the optional trailing - empty marker. Current APT versions may emit valid `Inst` records ending - in `) []`, which Reploy currently rejects as malformed. Accept exactly - the known optional ` []` suffix while preserving the existing package, - version, and architecture checks. Add regression coverage for ordinary - installs and upgrades, streaming chunk boundaries, and rejection of - nonempty, duplicated, or otherwise malformed trailing markers. - - [ ] `P2` Add explicit controlled runtime command aliases for validated executable outputs. Keep provider exports non-public by default, and let a blueprint opt into mapping ordinary command names such as `rustc` to @@ -167,6 +159,13 @@ This file is the day-to-day queue for design and implementation gaps. ## Post-v1 +- [ ] `P2` Inventory and organize the repository `temp/` directory. + Identify which files are disposable scratch artifacts, active private + working notes, or durable project documentation. Remove obsolete files + only after explicit review, move durable documents to an appropriate + maintained location, and leave `temp/` with a small documented purpose + and structure that prevents it from becoming an unindexed document store. + - [ ] `P2` Design explicit remote Docker support. Replace today's rejected ambient `DOCKER_HOST` and remote-context behavior with an intentional distributed-runtime contract. Define input snapshot diff --git a/internal/providers/apt/resolve_install.go b/internal/providers/apt/resolve_install.go index 322f0b8f..07c3308b 100644 --- a/internal/providers/apt/resolve_install.go +++ b/internal/providers/apt/resolve_install.go @@ -107,6 +107,7 @@ func (parser *ResolveInstallParserV1) consumeLine(line []byte) error { current = detail[1:end] detail = detail[end+2:] } + detail = strings.TrimSuffix(detail, " []") if len(detail) < 5 || detail[0] != '(' || detail[len(detail)-1] != ')' { return fmt.Errorf("APT install transaction record for package %q is malformed", name) } diff --git a/internal/providers/apt/resolve_plan_test.go b/internal/providers/apt/resolve_plan_test.go index e426388a..2bb1913e 100644 --- a/internal/providers/apt/resolve_plan_test.go +++ b/internal/providers/apt/resolve_plan_test.go @@ -115,6 +115,56 @@ func TestResolveInstallParserCompletesUpgradedDependencyClosure(t *testing.T) { } } +func TestResolveInstallParserAcceptsOptionalEmptyMarkerAcrossChunks(t *testing.T) { + parser, err := NewResolveInstallParserV1("amd64") + if err != nil { + t.Fatal(err) + } + chunks := []string{ + "Inst dovecot-sieve (1:2.4.1-4 Debian:13/stable [amd64]) [", + "]\nInst libssl3t64 [3.0.13-0ubuntu3.11] ", + "(3.0.13-0ubuntu3.12 Ubuntu:24.04/noble-updates [amd64]) []\n", + } + for _, chunk := range chunks { + if _, err := parser.Write([]byte(chunk)); err != nil { + t.Fatal(err) + } + } + changes, err := parser.Finish() + if err != nil { + t.Fatal(err) + } + want := []ResolvePlanPackageV1{ + {Name: "dovecot-sieve", ResolverArchitecture: "amd64", SelectedVersion: "1:2.4.1-4"}, + {Name: "libssl3t64", ResolverArchitecture: "amd64", CurrentVersion: "3.0.13-0ubuntu3.11", SelectedVersion: "3.0.13-0ubuntu3.12"}, + } + if !reflect.DeepEqual(changes, want) { + t.Fatalf("changes = %#v, want %#v", changes, want) + } +} + +func TestResolveInstallParserRejectsMalformedTrailingMarkers(t *testing.T) { + tests := []string{ + "Inst hello (2.10-3 Debian:13/stable [amd64]) [amd64]\n", + "Inst hello (2.10-3 Debian:13/stable [amd64]) [] []\n", + "Inst hello (2.10-3 Debian:13/stable [amd64]) [ ]\n", + "Inst hello (2.10-3 Debian:13/stable [amd64])[]\n", + "Inst hello (2.10-3 Debian:13/stable [amd64]) [] extra\n", + } + for _, input := range tests { + t.Run(input, func(t *testing.T) { + parser, err := NewResolveInstallParserV1("amd64") + if err != nil { + t.Fatal(err) + } + _, _ = parser.Write([]byte(input)) + if _, err := parser.Finish(); err == nil || !strings.Contains(err.Error(), "malformed") { + t.Fatalf("err = %v, want malformed record", err) + } + }) + } +} + func TestCompleteResolvePlanRejectsMissingAndConflictingInstallEvidence(t *testing.T) { marker := ResolvePlanV1{Schema: ResolvePlanSchemaV1, Packages: []ResolvePlanPackageV1{{ Name: "hello", ResolverArchitecture: "amd64", SelectedVersion: "1",