Preserve transcript destinations that change type
This commit is contained in:
@@ -27,8 +27,10 @@ consumed application and PromptKit sources, and effective host message files. An
|
||||
intentional in-place transcript replacement is allowed. A regular transcript
|
||||
output is prepared in a sibling temporary file, leaving the final destination
|
||||
untouched until successful publication. Pipes and devices are opened only for
|
||||
delivery; a pipe can wait for its reader. Parent directories for required outputs
|
||||
must already exist.
|
||||
delivery, and a pipe can wait for its reader. If a transcript destination changes
|
||||
between a regular file and a pipe or device during processing, Audita can report a
|
||||
delivery failure while preserving the new destination. Parent directories for
|
||||
required outputs must already exist.
|
||||
|
||||
Enabled reports are initialized as `in_progress` before processing. The
|
||||
transcript is published before final reports; the files do not form a single
|
||||
|
||||
@@ -384,6 +384,8 @@ keep their native behavior.
|
||||
|
||||
## R11 — P2: Preserve Pipes That Appear After Output Preparation
|
||||
|
||||
**Status:** Completed. Delivery checks the destination type before opening a prepared stream or replacing a file. Tests cover both type changes, preservation of the replacement destination, and cleanup of staged output.
|
||||
|
||||
**Category**
|
||||
|
||||
The new Stage 7 preparation changes the existing special-file delivery behavior.
|
||||
|
||||
@@ -327,6 +327,13 @@ func (s *OutputSession) writeTranscript(payload []byte) error {
|
||||
return err
|
||||
}
|
||||
if s.direct {
|
||||
info, err := os.Stat(s.transcript)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() || info.Mode().IsRegular() {
|
||||
return fmt.Errorf("transcript output is no longer a stream destination")
|
||||
}
|
||||
file, err := os.OpenFile(s.transcript, os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -349,6 +356,13 @@ func (s *OutputSession) writeTranscript(payload []byte) error {
|
||||
if err := errors.Join(writeErr, closeErr); err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := os.Stat(s.publishPath)
|
||||
if err == nil && !info.Mode().IsRegular() {
|
||||
return fmt.Errorf("transcript output changed type after preparation")
|
||||
}
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(s.tempPath, s.publishPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -418,14 +418,8 @@ func TestTranscriptWriteFailureCleansTemporaryResource(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNamedPipeIsNotOpenedDuringPreparation(t *testing.T) {
|
||||
tool, err := exec.LookPath("mkfifo")
|
||||
if err != nil {
|
||||
t.Skip("mkfifo is unavailable")
|
||||
}
|
||||
pipe := filepath.Join(t.TempDir(), "output-pipe")
|
||||
if output, err := exec.Command(tool, pipe).CombinedOutput(); err != nil {
|
||||
t.Fatalf("mkfifo: %v %s", err, output)
|
||||
}
|
||||
makeNamedPipe(t, pipe)
|
||||
session, err := PrepareOutput(OutputSelection{TranscriptPath: pipe})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -437,3 +431,60 @@ func TestNamedPipeIsNotOpenedDuringPreparation(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLateNamedPipeIsPreserved(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "output")
|
||||
session, err := PrepareOutput(OutputSelection{TranscriptPath: path})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer session.Close()
|
||||
makeNamedPipe(t, path)
|
||||
err = session.Finish(nil, []byte("transcript"), ReportBundle{}, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "changed type") {
|
||||
t.Fatalf("expected clear delivery failure, got %v", err)
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.Mode()&os.ModeNamedPipe == 0 {
|
||||
t.Fatalf("late pipe was lost: %v %v", info, err)
|
||||
}
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil || len(entries) != 1 {
|
||||
t.Fatalf("unpublished output resources remain: %v %v", entries, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamChangedToRegularFileIsPreserved(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "output")
|
||||
makeNamedPipe(t, path)
|
||||
session, err := PrepareOutput(OutputSelection{TranscriptPath: path})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer session.Close()
|
||||
if err := os.Remove(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte("existing content"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = session.Finish(nil, []byte("new"), ReportBundle{}, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "no longer a stream") {
|
||||
t.Fatalf("expected clear delivery failure, got %v", err)
|
||||
}
|
||||
if data, err := os.ReadFile(path); err != nil || string(data) != "existing content" {
|
||||
t.Fatalf("replacement file changed: %q %v", data, err)
|
||||
}
|
||||
}
|
||||
|
||||
func makeNamedPipe(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
tool, err := exec.LookPath("mkfifo")
|
||||
if err != nil {
|
||||
t.Skip("mkfifo is unavailable")
|
||||
}
|
||||
if output, err := exec.Command(tool, path).CombinedOutput(); err != nil {
|
||||
t.Fatalf("mkfifo: %v %s", err, output)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user