Redact invalid chunk plan lookup diagnostics
This commit is contained in:
@@ -351,7 +351,7 @@ feature and its per-user and system-wide deployment guidance.
|
||||
|
||||
## Stage 9: Redact invalid cache-record diagnostics
|
||||
|
||||
**Status:** Pending
|
||||
**Status:** Complete
|
||||
|
||||
### Objective
|
||||
|
||||
|
||||
@@ -255,6 +255,22 @@ func TestRunRecordsExplicitChunkCacheOverrideAndEffectiveMode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunChunkPlanDiagnosticsRedactStoreDecisionReason(t *testing.T) {
|
||||
diagnosticsDir := t.TempDir()
|
||||
configPath := writeTestConfig(t, cacheTestConfig("auto", t.TempDir(), diagnosticsDir))
|
||||
factory := &recordingChunkPlanFactory{store: &recordingChunkPlanStore{
|
||||
decision: pipeline.ChunkPlanDecision{Status: pipeline.ChunkPlanInvalid, Reason: "SENTINEL_INVALID_RECORD_CONTENT"},
|
||||
}}
|
||||
code, stderr := runCacheCommand(t, cacheRunArgs(t, configPath), cacheTestOptions(t, nil, factory))
|
||||
if code != 0 || stderr != "" {
|
||||
t.Fatalf("code=%d stderr=%q", code, stderr)
|
||||
}
|
||||
summary := string(readFile(t, filepath.Join(onlyChildDir(t, diagnosticsDir), diagnostics.ArtifactChunkPlan)))
|
||||
if strings.Contains(summary, "SENTINEL_INVALID_RECORD_CONTENT") || !strings.Contains(summary, `"lookup_reason": "stored chunk plan is invalid"`) {
|
||||
t.Fatalf("chunk plan diagnostic = %s", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAutoReusesPlanAcrossIndependentInvocations(t *testing.T) {
|
||||
cacheBase := filepath.Join(t.TempDir(), "cache")
|
||||
workspaceDir := filepath.Join(t.TempDir(), "workspace")
|
||||
|
||||
@@ -40,7 +40,7 @@ func (s *filesystemStore) Load(sourceDigest string) (pipeline.ChunkPlanRecord, p
|
||||
data, err := os.ReadFile(target)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return pipeline.ChunkPlanRecord{}, pipeline.ChunkPlanDecision{Status: pipeline.ChunkPlanMissing, Reason: "chunk plan not found"}, nil
|
||||
return pipeline.ChunkPlanRecord{}, pipeline.ChunkPlanDecision{Status: pipeline.ChunkPlanMissing, Reason: lookupReason(pipeline.ChunkPlanMissing)}, nil
|
||||
}
|
||||
return pipeline.ChunkPlanRecord{}, pipeline.ChunkPlanDecision{}, fmt.Errorf("read chunk plan: %w", err)
|
||||
}
|
||||
@@ -50,19 +50,16 @@ func (s *filesystemStore) Load(sourceDigest string) (pipeline.ChunkPlanRecord, p
|
||||
decoder.DisallowUnknownFields()
|
||||
decoder.UseNumber()
|
||||
if err := decoder.Decode(&record); err != nil {
|
||||
return invalidDecision(fmt.Sprintf("decode stored chunk plan: %v", err))
|
||||
return invalidDecision()
|
||||
}
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); err != io.EOF {
|
||||
if err == nil {
|
||||
return invalidDecision("stored chunk plan contains trailing JSON")
|
||||
}
|
||||
return invalidDecision(fmt.Sprintf("decode stored chunk plan trailer: %v", err))
|
||||
return invalidDecision()
|
||||
}
|
||||
if err := validateRecord(record, sourceDigest); err != nil {
|
||||
return invalidDecision(err.Error())
|
||||
return invalidDecision()
|
||||
}
|
||||
return record, pipeline.ChunkPlanDecision{Status: pipeline.ChunkPlanHit, Reason: "stored chunk plan is valid"}, nil
|
||||
return record, pipeline.ChunkPlanDecision{Status: pipeline.ChunkPlanHit, Reason: lookupReason(pipeline.ChunkPlanHit)}, nil
|
||||
}
|
||||
|
||||
func (s *filesystemStore) Save(record pipeline.ChunkPlanRecord) error {
|
||||
@@ -165,8 +162,21 @@ func validateRecord(record pipeline.ChunkPlanRecord, requestedDigest string) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func invalidDecision(reason string) (pipeline.ChunkPlanRecord, pipeline.ChunkPlanDecision, error) {
|
||||
return pipeline.ChunkPlanRecord{}, pipeline.ChunkPlanDecision{Status: pipeline.ChunkPlanInvalid, Reason: reason}, nil
|
||||
func invalidDecision() (pipeline.ChunkPlanRecord, pipeline.ChunkPlanDecision, error) {
|
||||
return pipeline.ChunkPlanRecord{}, pipeline.ChunkPlanDecision{Status: pipeline.ChunkPlanInvalid, Reason: lookupReason(pipeline.ChunkPlanInvalid)}, nil
|
||||
}
|
||||
|
||||
func lookupReason(status pipeline.ChunkPlanStatus) string {
|
||||
switch status {
|
||||
case pipeline.ChunkPlanHit:
|
||||
return "stored chunk plan is valid"
|
||||
case pipeline.ChunkPlanMissing:
|
||||
return "chunk plan not found"
|
||||
case pipeline.ChunkPlanInvalid:
|
||||
return "stored chunk plan is invalid"
|
||||
default:
|
||||
return "chunk plan lookup skipped"
|
||||
}
|
||||
}
|
||||
|
||||
type atomicWriteHooks struct {
|
||||
|
||||
@@ -122,14 +122,13 @@ func TestFilesystemStoreReportsInvalidRecordsAsRecoverable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func([]byte) []byte
|
||||
want string
|
||||
}{
|
||||
{name: "unknown field", mutate: func(data []byte) []byte {
|
||||
return bytes.Replace(data, []byte(`{"schema_version"`), []byte(`{"unknown":true,"schema_version"`), 1)
|
||||
}, want: "unknown"},
|
||||
{name: "truncated JSON", mutate: func(data []byte) []byte { return data[:len(data)/2] }, want: "decode"},
|
||||
{name: "schema mismatch", mutate: replaceJSON(`notarius.chunk-plan.v1`, `notarius.chunk-plan.v2`), want: "schema_version"},
|
||||
{name: "source mismatch", mutate: replaceJSON(testSourceDigest, "sha256:"+strings.Repeat("b", 64)), want: "source_digest"},
|
||||
return bytes.Replace(data, []byte(`{"schema_version"`), []byte(`{"SENTINEL_UNKNOWN_FIELD":true,"schema_version"`), 1)
|
||||
}},
|
||||
{name: "truncated JSON", mutate: func(data []byte) []byte { return data[:len(data)/2] }},
|
||||
{name: "schema mismatch", mutate: replaceJSON(`notarius.chunk-plan.v1`, `SENTINEL_SCHEMA_VALUE`)},
|
||||
{name: "source mismatch", mutate: replaceJSON(testSourceDigest, "sha256:"+strings.Repeat("b", 64))},
|
||||
{name: "plan digest mismatch", mutate: func(data []byte) []byte {
|
||||
prefix := []byte(`"plan_digest":"sha256:`)
|
||||
index := bytes.Index(data, prefix)
|
||||
@@ -137,14 +136,15 @@ func TestFilesystemStoreReportsInvalidRecordsAsRecoverable(t *testing.T) {
|
||||
data[index+len(prefix)] = '0'
|
||||
}
|
||||
return data
|
||||
}, want: "plan_digest"},
|
||||
}},
|
||||
{name: "noncanonical annotation", mutate: func(data []byte) []byte {
|
||||
return bytes.Replace(data, []byte(`{"value":1}`), []byte(`{ "value": 1 }`), 1)
|
||||
}, want: "canonical JSON"},
|
||||
return bytes.Replace(data, []byte(`"test/value"`), []byte(`"SENTINEL_ANNOTATION_NAMESPACE"`), 1)
|
||||
}},
|
||||
{name: "bad boundary", mutate: func(data []byte) []byte {
|
||||
return bytes.Replace(data, []byte(`"start_unit_id":1`), []byte(`"start_unit_id":0`), 1)
|
||||
}, want: "boundaries must be positive"},
|
||||
{name: "trailing JSON", mutate: func(data []byte) []byte { return append(data, []byte(` {}`)...) }, want: "trailing"},
|
||||
}},
|
||||
{name: "timestamp", mutate: replaceJSON(`2026-07-18T12:00:00Z`, `SENTINEL_TIMESTAMP`)},
|
||||
{name: "trailing JSON", mutate: func(data []byte) []byte { return append(data, []byte(` {"SENTINEL_TRAILING":true}`)...) }},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -163,9 +163,14 @@ func TestFilesystemStoreReportsInvalidRecordsAsRecoverable(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, decision, err := store.Load(testSourceDigest)
|
||||
if err != nil || decision.Status != pipeline.ChunkPlanInvalid || !reflect.DeepEqual(got, pipeline.ChunkPlanRecord{}) || !strings.Contains(decision.Reason, tc.want) {
|
||||
if err != nil || decision.Status != pipeline.ChunkPlanInvalid || !reflect.DeepEqual(got, pipeline.ChunkPlanRecord{}) || decision.Reason != "stored chunk plan is invalid" {
|
||||
t.Fatalf("record=%#v decision=%#v error=%v", got, decision, err)
|
||||
}
|
||||
for _, sentinel := range []string{"SENTINEL_UNKNOWN_FIELD", "SENTINEL_SCHEMA_VALUE", "SENTINEL_ANNOTATION_NAMESPACE", "SENTINEL_TIMESTAMP", "SENTINEL_TRAILING"} {
|
||||
if strings.Contains(decision.Reason, sentinel) {
|
||||
t.Fatalf("decision leaked %q: %#v", sentinel, decision)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,11 +47,11 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
||||
if mode == ChunkCacheAuto {
|
||||
record, decision, err := input.ChunkPlans.Load(doc.Digest)
|
||||
result.lookup = decision
|
||||
result.summary.LookupStatus = string(decision.Status)
|
||||
result.summary.LookupReason = decision.Reason
|
||||
result.summary.LookupStatus = chunkPlanLookupStatus(decision.Status)
|
||||
result.summary.LookupReason = chunkPlanLookupReason(decision.Status)
|
||||
if err != nil {
|
||||
result.summary.LookupStatus = "skipped"
|
||||
result.summary.LookupReason = "chunk plan lookup failed"
|
||||
result.summary.LookupReason = chunkPlanLookupReason("")
|
||||
return result, fmt.Errorf("load chunk plan: %w", err)
|
||||
}
|
||||
switch decision.Status {
|
||||
@@ -68,13 +68,13 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
||||
result.setValidation(validationWarnings, rejection, err)
|
||||
return result, err
|
||||
}
|
||||
result.lookup = ChunkPlanDecision{Status: ChunkPlanInvalid, Reason: "stored chunk plan cannot be materialized against the current source"}
|
||||
result.lookup = ChunkPlanDecision{Status: ChunkPlanInvalid, Reason: chunkPlanLookupReason(ChunkPlanInvalid)}
|
||||
result.summary.LookupStatus = "invalid"
|
||||
result.summary.LookupReason = result.lookup.Reason
|
||||
result.summary.LookupReason = chunkPlanLookupReason(ChunkPlanInvalid)
|
||||
case ChunkPlanMissing, ChunkPlanInvalid:
|
||||
// Generate below.
|
||||
default:
|
||||
return result, fmt.Errorf("load chunk plan returned unsupported status %q", decision.Status)
|
||||
return result, fmt.Errorf("load chunk plan returned unsupported decision status")
|
||||
}
|
||||
}
|
||||
if mode == ChunkCacheAuto || mode == ChunkCacheRefresh {
|
||||
@@ -171,6 +171,32 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func chunkPlanLookupStatus(status ChunkPlanStatus) string {
|
||||
switch status {
|
||||
case ChunkPlanHit:
|
||||
return "hit"
|
||||
case ChunkPlanMissing:
|
||||
return "missing"
|
||||
case ChunkPlanInvalid:
|
||||
return "invalid"
|
||||
default:
|
||||
return "skipped"
|
||||
}
|
||||
}
|
||||
|
||||
func chunkPlanLookupReason(status ChunkPlanStatus) string {
|
||||
switch status {
|
||||
case ChunkPlanHit:
|
||||
return "stored chunk plan is valid"
|
||||
case ChunkPlanMissing:
|
||||
return "chunk plan not found"
|
||||
case ChunkPlanInvalid:
|
||||
return "stored chunk plan is invalid"
|
||||
default:
|
||||
return "chunk plan lookup skipped"
|
||||
}
|
||||
}
|
||||
|
||||
func (result *chunkPlanExecution) setCandidate(record ChunkPlanRecord, action string) {
|
||||
cloned := cloneChunkPlanRecord(record)
|
||||
result.record = &cloned
|
||||
|
||||
@@ -143,6 +143,43 @@ func TestRunnerChunkPlanModeMatrix(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerUsesStatusDerivedChunkPlanSummaryReasons(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
status ChunkPlanStatus
|
||||
wantStatus string
|
||||
wantReason string
|
||||
}{
|
||||
{name: "hit", status: ChunkPlanHit, wantStatus: "hit", wantReason: "stored chunk plan is valid"},
|
||||
{name: "missing", status: ChunkPlanMissing, wantStatus: "missing", wantReason: "chunk plan not found"},
|
||||
{name: "invalid", status: ChunkPlanInvalid, wantStatus: "invalid", wantReason: "stored chunk plan is invalid"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
prepared, plan := preparedTerminalDebugPipeline(t)
|
||||
store := &recordingChunkPlanStore{
|
||||
record: chunkPlanRecord(t, prepared, plan),
|
||||
decision: ChunkPlanDecision{Status: tc.status, Reason: "SENTINEL_STORE_REASON"},
|
||||
}
|
||||
output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), ChunkCacheMode: ChunkCacheAuto, ChunkPlans: store})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if output.ChunkPlan.LookupStatus != tc.wantStatus || output.ChunkPlan.LookupReason != tc.wantReason || strings.Contains(output.ChunkPlan.LookupReason, "SENTINEL_STORE_REASON") {
|
||||
t.Fatalf("summary = %#v", output.ChunkPlan)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
prepared, _ := preparedTerminalDebugPipeline(t)
|
||||
output, err := New().Run(context.Background(), RunInput{
|
||||
Prepared: prepared, RawInput: []byte("input"), ChunkCacheMode: ChunkCacheAuto,
|
||||
ChunkPlans: &recordingChunkPlanStore{loadErr: errors.New("read failed")},
|
||||
})
|
||||
if err == nil || output.ChunkPlan == nil || output.ChunkPlan.LookupStatus != "skipped" || output.ChunkPlan.LookupReason != "chunk plan lookup skipped" {
|
||||
t.Fatalf("operational lookup output=%#v error=%v", output.ChunkPlan, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerChunkPlanHitUsesStoredProducerProvenance(t *testing.T) {
|
||||
prepared, plan := preparedTerminalDebugPipeline(t)
|
||||
prepared.resolved.Chunk.Module = "chunk/requested"
|
||||
|
||||
Reference in New Issue
Block a user