Skip to content

apply and apply --dry-run hang forever on a non-regular destination #241

Description

@spxrogers

Found during review of #240, which fixes the same defect class for status / diff / reconcile's drift walk. This one is in internal/render and is not fixed by that PR.

os.ReadFile on a FIFO does not fail — it blocks in the open waiting for a writer that never comes — so the read's own error path never runs and the command never returns.

Measured

Built at d4fb233 (i.e. with #240's gate in place), clean apply, then the rendered destination ~/.claude/skills/demo/SKILL.md replaced by a 0600 FIFO, run from a neutral cwd:

command rc
status 0
diff 0
reconcile --auto-safe 0
apply --dry-run 124 — hung
apply 124 — hung
reconcile --auto-override 124 — hung

apply --dry-run is advertised as read-only.

reconcile --auto-override (and the interactive [o] keystroke) belongs here rather than in #240: [o] re-applies the item through render.NewWriterApply → the same unguarded read, so it shares apply's bug rather than the drift walk's. #240 therefore withholds [o]verride from writeBackFileItem's refusal message — recommending it would route a user out of a clean refusal into this wedge. When this issue is fixed, that suggestion can be restored (there is a test asserting the message does not currently offer it, which will need updating in the same commit).

Where

internal/render/writer.go:200 — the convergence read, the first statement of Writer.Write:

if cur, err := os.ReadFile(op.Path); err == nil && bytes.Equal(cur, finalBytes) {

Its own comment notes "The same read drives the dry-run preview: it is what lets apply --dry-run label a converged destination 'synced' rather than 'write'." So both the real apply and the preview go through it.

A false doc claim to fix alongside

internal/render/writer.go, the doc comment on isRegularOrAbsent, currently says:

… without this, apply --dry-run (advertised as read-only) and the real apply's pre-delete read would both hang forever on a FIFO left at a destination path.

The predicate exists and is correct, but Writer.Write's convergence read does not call it, so the claim about apply --dry-run is false. Whatever fixes the hang should make that sentence true rather than merely plausible.

Suggested shape

Guard the convergence read with the package's own isRegularOrAbsent. On a non-regular destination the convergence check should be skipped rather than attempted — falling through to the normal write path, whose atomic temp-then-rename replaces the FIFO with a real file, which is the outcome the user wants from apply. --dry-run must report rather than block.

internal/render/state_apply.go:310 and :353 also read op.Path directly; they run after the write, so they are likely reached only once the path is regular again, but they should be checked as part of this.

Test note

internal/cli/dest_fifo_e2e_unix_test.go already carries ready-made rows for apply, apply --dry-run and reconcile --auto-override, currently t.Skipped citing this issue. Deleting the skip: string on each is enough to inherit the assertion — they were verified to fail with the correct BLOCKED diagnostic (8s per row, both destination shapes) when un-skipped against the current code.

Any regression test must be timeout-bounded. This class does not fail, it hangs, so a plain call wedges the suite with no diagnostic.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions