Skip to content
Open
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: 2 additions & 0 deletions .changes/unreleased/+apt-transaction-empty-marker.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
kind: Fixed
body: Accept valid APT install transaction records with one trailing empty marker.
17 changes: 8 additions & 9 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
status: Active
updated: 2026-08-04
updated: 2026-08-14
summary: Active planning surface for Reploy design and implementation gaps.
---

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions internal/providers/apt/resolve_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
50 changes: 50 additions & 0 deletions internal/providers/apt/resolve_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading