Complete diagnostics retention semantics
This commit is contained in:
@@ -307,7 +307,9 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
|
|||||||
|
|
||||||
if runDir != nil {
|
if runDir != nil {
|
||||||
_ = runDir.WriteReport(report)
|
_ = runDir.WriteReport(report)
|
||||||
_ = runDir.ApplyRetention(false)
|
_ = runDir.ApplyRetention(diagnostics.RetentionDecisionInput{
|
||||||
|
RunSucceeded: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(stderr, "audita process: %v\n", runErr)
|
fmt.Fprintf(stderr, "audita process: %v\n", runErr)
|
||||||
@@ -320,7 +322,9 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
|
|||||||
if err := reporting.WriteProcessReport(inv.ReportJSONPath, report); err != nil {
|
if err := reporting.WriteProcessReport(inv.ReportJSONPath, report); err != nil {
|
||||||
if runDir != nil {
|
if runDir != nil {
|
||||||
_ = runDir.WriteErrorLog(fmt.Sprintf("report_write: %v", err))
|
_ = runDir.WriteErrorLog(fmt.Sprintf("report_write: %v", err))
|
||||||
_ = runDir.ApplyRetention(false)
|
_ = runDir.ApplyRetention(diagnostics.RetentionDecisionInput{
|
||||||
|
RunSucceeded: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
fmt.Fprintf(stderr, "audita process: %v\n", err)
|
fmt.Fprintf(stderr, "audita process: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
@@ -329,7 +333,10 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
|
|||||||
|
|
||||||
if runDir != nil {
|
if runDir != nil {
|
||||||
_ = runDir.WriteReport(report)
|
_ = runDir.WriteReport(report)
|
||||||
if err := runDir.ApplyRetention(true); err != nil {
|
if err := runDir.ApplyRetention(diagnostics.RetentionDecisionInput{
|
||||||
|
RunSucceeded: true,
|
||||||
|
HasSkippedCorrections: false, // Hook for future module-level skipped-correction reporting.
|
||||||
|
}); err != nil {
|
||||||
fmt.Fprintf(stderr, "audita process: failed to apply work-dir retention: %v\n", err)
|
fmt.Fprintf(stderr, "audita process: failed to apply work-dir retention: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -478,6 +478,98 @@ func TestRunProcessFailedRunRetainedWithNeverRetention(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunProcessSuccessfulRunAutoRetentionRemovesRunDir(t *testing.T) {
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
|
workDir := t.TempDir()
|
||||||
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
||||||
|
|
||||||
|
exitCode := Run([]string{
|
||||||
|
"process",
|
||||||
|
fixturePath("tiny_transcript.json"),
|
||||||
|
"--glossary",
|
||||||
|
fixturePath("tiny_glossary.yaml"),
|
||||||
|
"--work-dir",
|
||||||
|
workDir,
|
||||||
|
"--work-dir-retention",
|
||||||
|
"auto",
|
||||||
|
"--report-json",
|
||||||
|
reportPath,
|
||||||
|
}, &stdout, &stderr)
|
||||||
|
if exitCode != 0 {
|
||||||
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(workDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read work dir: %v", err)
|
||||||
|
}
|
||||||
|
if len(entries) != 0 {
|
||||||
|
t.Fatalf("expected auto retention to remove successful clean run dir, found %d entries", len(entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit report path must still exist even if run dir is removed.
|
||||||
|
if _, err := os.Stat(reportPath); err != nil {
|
||||||
|
t.Fatalf("expected --report-json output to survive run-dir removal: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunProcessSuccessfulRunAlwaysRetentionKeepsRunDir(t *testing.T) {
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
|
workDir := t.TempDir()
|
||||||
|
|
||||||
|
exitCode := Run([]string{
|
||||||
|
"process",
|
||||||
|
fixturePath("tiny_transcript.json"),
|
||||||
|
"--glossary",
|
||||||
|
fixturePath("tiny_glossary.yaml"),
|
||||||
|
"--work-dir",
|
||||||
|
workDir,
|
||||||
|
"--work-dir-retention",
|
||||||
|
"always",
|
||||||
|
}, &stdout, &stderr)
|
||||||
|
if exitCode != 0 {
|
||||||
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
runPath := onlyRunDir(t, workDir)
|
||||||
|
if _, err := os.Stat(runPath); err != nil {
|
||||||
|
t.Fatalf("expected run dir retained under always: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunProcessFailedRunRetainedWithAutoRetention(t *testing.T) {
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
|
workDir := t.TempDir()
|
||||||
|
|
||||||
|
exitCode := Run([]string{
|
||||||
|
"process",
|
||||||
|
schemaFixturePath("transcript_empty_speaker.json"),
|
||||||
|
"--glossary",
|
||||||
|
fixturePath("tiny_glossary.yaml"),
|
||||||
|
"--work-dir",
|
||||||
|
workDir,
|
||||||
|
"--work-dir-retention",
|
||||||
|
"auto",
|
||||||
|
}, &stdout, &stderr)
|
||||||
|
if exitCode == 0 {
|
||||||
|
t.Fatalf("expected nonzero exit code")
|
||||||
|
}
|
||||||
|
if stdout.Len() != 0 {
|
||||||
|
t.Fatalf("expected empty stdout on failure, got %q", stdout.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
runPath := onlyRunDir(t, workDir)
|
||||||
|
if _, err := os.Stat(filepath.Join(runPath, "error.log")); err != nil {
|
||||||
|
t.Fatalf("expected error.log in retained failed run under auto: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunProcessReportJSONIncludesChunkingSummary(t *testing.T) {
|
func TestRunProcessReportJSONIncludesChunkingSummary(t *testing.T) {
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
|
|||||||
72
internal/core/diagnostics/retention_test.go
Normal file
72
internal/core/diagnostics/retention_test.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package diagnostics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestShouldRetainRunDirectoryMatrix(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input RetentionDecisionInput
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "always success keeps", input: RetentionDecisionInput{RetentionMode: "always", RunSucceeded: true}, want: true},
|
||||||
|
{name: "always failure keeps", input: RetentionDecisionInput{RetentionMode: "always", RunSucceeded: false}, want: true},
|
||||||
|
{name: "never success keeps", input: RetentionDecisionInput{RetentionMode: "never", RunSucceeded: true}, want: true},
|
||||||
|
{name: "never failure keeps", input: RetentionDecisionInput{RetentionMode: "never", RunSucceeded: false}, want: true},
|
||||||
|
{name: "auto success no skips removes", input: RetentionDecisionInput{RetentionMode: "auto", RunSucceeded: true, HasSkippedCorrections: false}, want: false},
|
||||||
|
{name: "auto success skips keeps", input: RetentionDecisionInput{RetentionMode: "auto", RunSucceeded: true, HasSkippedCorrections: true}, want: true},
|
||||||
|
{name: "auto failure keeps", input: RetentionDecisionInput{RetentionMode: "auto", RunSucceeded: false}, want: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := ShouldRetainRunDirectory(tc.input)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Fatalf("unexpected retain decision: got=%v want=%v", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyRetentionRemovesWhenDecisionSaysRemove(t *testing.T) {
|
||||||
|
workDir := t.TempDir()
|
||||||
|
runPath := filepath.Join(workDir, "run-test")
|
||||||
|
if err := os.Mkdir(runPath, 0o755); err != nil {
|
||||||
|
t.Fatalf("mkdir run path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
runDir := &RunDirectory{path: runPath, retention: "auto"}
|
||||||
|
err := runDir.ApplyRetention(RetentionDecisionInput{RunSucceeded: true, HasSkippedCorrections: false})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ApplyRetention failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(runPath); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("expected run directory removed, stat err=%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyRetentionReturnsRemovalError(t *testing.T) {
|
||||||
|
parent := t.TempDir()
|
||||||
|
runPath := filepath.Join(parent, "run-test")
|
||||||
|
if err := os.Mkdir(runPath, 0o755); err != nil {
|
||||||
|
t.Fatalf("mkdir run path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make parent non-writable so removing child fails.
|
||||||
|
if err := os.Chmod(parent, 0o500); err != nil {
|
||||||
|
t.Fatalf("chmod parent: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = os.Chmod(parent, 0o700)
|
||||||
|
})
|
||||||
|
|
||||||
|
runDir := &RunDirectory{path: runPath, retention: "auto"}
|
||||||
|
err := runDir.ApplyRetention(RetentionDecisionInput{RunSucceeded: true, HasSkippedCorrections: false})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected removal error, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,31 @@ type RunDirectory struct {
|
|||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RetentionDecisionInput struct {
|
||||||
|
RetentionMode string
|
||||||
|
RunSucceeded bool
|
||||||
|
HasSkippedCorrections bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShouldRetainRunDirectory(input RetentionDecisionInput) bool {
|
||||||
|
// Failed runs are always retained.
|
||||||
|
if !input.RunSucceeded {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch input.RetentionMode {
|
||||||
|
case "always":
|
||||||
|
return true
|
||||||
|
case "never":
|
||||||
|
return true
|
||||||
|
case "auto":
|
||||||
|
return input.HasSkippedCorrections
|
||||||
|
default:
|
||||||
|
// Be conservative for unknown values.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// InvocationMetadata captures non-secret invocation details for diagnostics.
|
// InvocationMetadata captures non-secret invocation details for diagnostics.
|
||||||
type InvocationMetadata struct {
|
type InvocationMetadata struct {
|
||||||
Operation string `json:"operation"`
|
Operation string `json:"operation"`
|
||||||
@@ -187,20 +212,14 @@ func (r *RunDirectory) WriteChunkingSummary(summary *chunking.DetailedSummary) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: In later phases, implement full "auto" semantics based on skip/report outcomes.
|
func (r *RunDirectory) ApplyRetention(input RetentionDecisionInput) error {
|
||||||
func (r *RunDirectory) ApplyRetention(runSucceeded bool) error {
|
decision := input
|
||||||
if !runSucceeded {
|
if decision.RetentionMode == "" {
|
||||||
// Failed runs are always retained.
|
decision.RetentionMode = r.retention
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.retention {
|
if ShouldRetainRunDirectory(decision) {
|
||||||
case "always":
|
|
||||||
return nil
|
|
||||||
case "never", "auto":
|
|
||||||
return os.RemoveAll(r.path)
|
|
||||||
default:
|
|
||||||
// Retain by default for unknown modes; config validation should prevent this.
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
return os.RemoveAll(r.path)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user