package recovery import ( "context" "encoding/json" "errors" "os" "path/filepath" "strings" "testing" "time" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport" ) func TestCrossPlatformCoverageRecoveryModelEdges(t *testing.T) { typedCases := []struct { name string err error raw string want string }{ {"validation required", apperrors.NewValidation("required value"), "", cliMissingParamCode}, {"validation invalid", apperrors.NewValidation("bad json"), "", cliInvalidJSONCode}, {"auth permission reason", apperrors.NewAuth("denied", apperrors.WithReason("http_403")), "", cliPermissionCode}, {"auth forbidden", apperrors.NewAuth("forbidden"), "", cliPermissionCode}, {"auth expired", apperrors.NewAuth("expired"), "", cliAuthExpiredCode}, {"api rate reason", apperrors.NewAPI("limited", apperrors.WithReason("http_429")), "", cliRateLimitCode}, {"api rate message", apperrors.NewAPI("rate limit", apperrors.WithRetryable(true)), "", cliRateLimitCode}, {"api timeout reason", apperrors.NewAPI("failed", apperrors.WithReason("request_timeout")), "", cliTimeoutCode}, {"api timeout message", apperrors.NewDiscovery("connection reset"), "", cliTimeoutCode}, {"api invalid params", apperrors.NewAPI("failed", apperrors.WithReason("invalid_params")), "", cliInvalidJSONCode}, {"api method missing", apperrors.NewAPI("failed", apperrors.WithReason("method_not_found")), "", cliResourceNotFoundCode}, {"api 404", apperrors.NewAPI("failed", apperrors.WithReason("http_404")), "", cliResourceNotFoundCode}, {"raw auth", nil, "user_token_illegal", cliAuthExpiredCode}, {"raw permission", nil, "permission denied", cliPermissionCode}, {"raw rate", nil, "too many requests", cliRateLimitCode}, {"raw timeout", nil, "deadline exceeded", cliTimeoutCode}, {"raw missing", nil, "base_not_found", cliResourceNotFoundCode}, {"unknown", nil, "strange", ""}, } for _, tt := range typedCases { t.Run(tt.name, func(t *testing.T) { if got := canonicalCLIErrorCode(tt.err, tt.raw); got != tt.want { t.Fatalf("canonicalCLIErrorCode = %q, want %q", got, tt.want) } }) } for _, name := range []string{"get_x", "list_x", "search_x", "query_x", "status_x", "download_x"} { if InferOperationKind(name) != OperationRead { t.Errorf("%q should be read", name) } } for _, name := range []string{"create_x", "update_x", "delete_x", "send_x", "generate_image", "edit_image", "upscale", "isolate"} { if InferOperationKind(name) != OperationWrite { t.Errorf("%q should be write", name) } } if InferOperationKind("") != OperationUnknown || InferOperationKind("execute") != OperationUnknown { t.Fatal("unknown operation inference failed") } input := map[string]any{ "short": "value", "long": strings.Repeat("x", 121), "text": "secret body", "names": []string{"a", "b"}, "textItems": []string{"a"}, "items": []any{"a", map[string]any{"x": 1}}, "records": []map[string]any{{"name": "n"}}, "objects": []map[string]any{{"name": "n", "token": "drop"}}, "payload": map[string]any{"name": "n", "token": "drop"}, "fields": map[string]any{"z": 1, "a": 2}, "stringMap": map[string]string{"name": "n", "token": "drop"}, "jsonMap": map[string]string{"z": "1", "a": "2"}, "number": 7, "token": "drop", } summary := SummarizeArgs(input) if len(summary) == 0 || summary["number"] != 7 { t.Fatalf("summary = %#v", summary) } replayed := sanitizeReplayMap(input) if _, ok := replayed["token"]; ok { t.Fatal("sensitive replay field retained") } for _, value := range []any{ map[string]any{"safe": "x", "token": "drop"}, map[string]string{"safe": "x", "token": "drop"}, []any{map[string]any{"safe": "x"}}, []string{"a"}, []map[string]any{{"safe": "x"}}, 42, } { if sanitizeReplayValue(value) == nil { t.Fatalf("sanitizeReplayValue(%T) returned nil", value) } } if sanitizeReplayMap(nil) != nil || sanitizeReplayStringMap(nil) != nil { t.Fatal("empty replay maps should be nil") } if got := sanitizeReplayField("data", map[string]string{"safe": "x"}); got == nil { t.Fatal("string map sanitization failed") } if got := sanitizeReplayField("data", []map[string]any{{"safe": "x"}}); got == nil { t.Fatal("map slice sanitization failed") } if got := sanitizeReplayField("data", strings.Repeat("x", 121)); got == nil { t.Fatal("long string sanitization failed") } argv := sanitizeArgv([]string{ "dws", "--token=one", "--auth-code=two", "--refresh-token=three", "--auth-code", "four", "--refresh-token", "five", "--plain", }) if len(argv) != 9 || strings.Contains(strings.Join(argv, " "), "one") { t.Fatalf("sanitized argv = %#v", argv) } if sanitizeArgv(nil) != nil { t.Fatal("nil argv should remain nil") } if value, ok := redactSensitiveArg("--plain=x"); ok || value != "" { t.Fatalf("plain arg redacted: %q %v", value, ok) } if keys := sortedKeys(map[string]any{"b": 1, "a": 2}); strings.Join(keys, ",") != "a,b" { t.Fatalf("sorted keys = %#v", keys) } if keys := sortedStringKeys(map[string]string{"b": "1", "a": "2"}); strings.Join(keys, ",") != "a,b" { t.Fatalf("sorted string keys = %#v", keys) } normalized := normalizeRawError(" HTTPS://example.test/x?q=secret 123456 2026-07-13T01:02:03Z abcdefghijklmnopqrstuvwxyz ") if strings.Contains(normalized, "secret") || strings.Contains(normalized, "123456") { t.Fatalf("normalizeRawError = %q", normalized) } ctx := BuildContext(CaptureInput{ToolName: "get_x"}) if ctx.OperationKind != OperationRead || ctx.RawError != "" { t.Fatalf("BuildContext defaults = %#v", ctx) } replay := BuildReplay(CaptureInput{ToolName: "create_x"}) if replay.OperationKind != OperationWrite || replay.RedactedCommand != "" { t.Fatalf("BuildReplay defaults = %#v", replay) } } func TestCrossPlatformCoveragePlannerRuleAndSearchEdges(t *testing.T) { rules := []struct { name string rc RecoveryContext category string retry bool }{ {"input", RecoveryContext{CLIErrorCode: cliInvalidJSONCode}, "input", false}, {"resource suffix", RecoveryContext{CLIErrorCode: "THING_NOT_FOUND"}, "resource", false}, {"permission", RecoveryContext{HTTPStatus: 403}, "permission", false}, {"rate read", RecoveryContext{HTTPStatus: 429, OperationKind: OperationRead}, "rate_limit", true}, {"rate write http", RecoveryContext{HTTPStatus: 429, OperationKind: OperationWrite, CallStage: "http"}, "rate_limit", true}, {"rate write non-http", RecoveryContext{HTTPStatus: 429, OperationKind: OperationWrite, CallStage: "decode"}, "rate_limit", false}, {"network read", RecoveryContext{HTTPStatus: 503, OperationKind: OperationRead}, "network", true}, {"network write", RecoveryContext{RawError: "timeout", OperationKind: OperationWrite, CallStage: "http"}, "network", false}, } for _, tt := range rules { t.Run(tt.name, func(t *testing.T) { plan := NewPlanner(nil).PlanWithOptions(context.Background(), tt.rc, PlanOptions{EnableDocSearch: false}) if plan.Category != tt.category || plan.ShouldRetry != tt.retry { t.Fatalf("plan = %#v", plan) } }) } if safeToRetry(RecoveryContext{OperationKind: OperationUnknown}, "network") { t.Fatal("unknown operation should not retry") } if normalizeDecisionOwner(RecoveryPlan{DecisionOwner: DecisionOwnerAgent}) != DecisionOwnerAgent || normalizeDecisionOwner(RecoveryPlan{Category: "unknown"}) != DecisionOwnerAgent || normalizeDecisionOwner(RecoveryPlan{Category: "auth"}) != DecisionOwnerBuiltinRule { t.Fatal("decision owner normalization failed") } queryRC := RecoveryContext{CLIErrorCode: "CODE", ToolName: "get_x", CommandPath: []string{"product", "get"}, RawError: "Error alpha alpha beta 123456"} if query := BuildFallbackQuery(queryRC); !strings.Contains(query, "CODE") || !strings.Contains(query, "alpha") { t.Fatalf("fallback query = %q", query) } if got := stableKeywords(""); got != nil { t.Fatalf("empty stable keywords = %#v", got) } many := stableKeywords("one two three four five six seven eight") if len(many) != 6 { t.Fatalf("stable keyword cap = %#v", many) } if containsAny("Hello WORLD", "none", "world") != true || containsAny("hello", "x") { t.Fatal("containsAny failed") } if got := normalizeSafeActions([]string{"wait_and_retry", "unsafe", "wait_and_retry"}); len(got) != 1 { t.Fatalf("safe actions = %#v", got) } hit := KBHit{ Source: "docs", Title: "errors", URL: "https://docs", Snippet: "| httpCode | code | message | reason | action |\n| --- | --- | --- | --- | --- |\n| 403 | Forbidden | denied | permission | Request access |\n| 404 | Missing | gone | deleted | %s |\n| bad | short | row |", } retrieval := KnowledgeRetrieval{ KBHits: []KBHit{hit, hit}, DocSearch: DocSearch{ Request: &ToolCallRecord{ServerID: "docs", ToolName: "search", Arguments: map[string]any{"q": "x"}}, Response: &ToolResponse{Content: []ToolResponseBlock{{Type: "text", Text: "ok"}}}, Items: []DocSearchItem{{Title: "one"}}, }, } plan := NewPlanner(fakeRetriever{retrieval: retrieval}).Plan(context.Background(), RecoveryContext{RawError: "unknown issue"}) if plan.DocSearch.Status != "success" || len(plan.KBHits) != 1 || len(plan.DocActions) != 1 { t.Fatalf("retrieved plan = %#v", plan) } if !plan.DecisionHints.PermissionSensitive || !plan.DecisionHints.ResourceStateRelated { t.Fatalf("decision hints = %#v", plan.DecisionHints) } emptyPlan := NewPlanner(fakeRetriever{}).Plan(context.Background(), RecoveryContext{RawError: "unknown"}) if emptyPlan.DocSearch.Status != "empty" { t.Fatalf("empty search = %#v", emptyPlan.DocSearch) } errPlan := NewPlanner(fakeRetriever{err: errors.New("docs down")}).Plan(context.Background(), RecoveryContext{RawError: "unknown"}) if errPlan.DocSearch.Status != "error" || errPlan.DocSearch.Error != "docs down" { t.Fatalf("error search = %#v", errPlan.DocSearch) } customErr := NewPlanner(fakeRetriever{ retrieval: KnowledgeRetrieval{DocSearch: DocSearch{Status: "error", Error: "custom"}}, err: errors.New("docs down"), }).Plan(context.Background(), RecoveryContext{RawError: "unknown"}) if customErr.DocSearch.Error != "custom" { t.Fatalf("custom search error = %#v", customErr.DocSearch) } emptyErr := NewPlanner(fakeRetriever{ retrieval: KnowledgeRetrieval{DocSearch: DocSearch{Status: "error"}}, }).Plan(context.Background(), RecoveryContext{RawError: "unknown"}) if emptyErr.DocSearch.Status != "error" { t.Fatalf("empty search error = %#v", emptyErr.DocSearch) } skipped := NewPlanner(nil).Plan(context.Background(), RecoveryContext{RawError: "unknown"}) if skipped.DocSearch.Status != "skipped" { t.Fatalf("nil retriever = %#v", skipped.DocSearch) } var nilPlanner *Planner if got := nilPlanner.searchKnowledge(context.Background(), "query", RecoveryContext{}); got.DocSearch.Status != "skipped" { t.Fatalf("nil planner search = %#v", got) } if got := NewPlanner(nil).searchKnowledge(context.Background(), "", RecoveryContext{}); got.DocSearch.Status != "skipped" { t.Fatalf("empty query search = %#v", got) } actions := extractDocActions([]KBHit{{Snippet: ""}, hit}) if len(actions) != 1 || len(splitTableRow("| a | b |")) != 2 { t.Fatalf("doc actions = %#v", actions) } if len(dedupeDocActions(append(actions, actions...))) != 1 || len(dedupeKBHits([]KBHit{hit, hit})) != 1 { t.Fatal("dedupe failed") } if cloneDocActions(nil) != nil || cloneKBHits(nil) != nil { t.Fatal("nil clone should remain nil") } if got := cloneDocSearch(retrieval.DocSearch); got.Request == retrieval.DocSearch.Request || got.Response == retrieval.DocSearch.Response { t.Fatal("doc search clone retained pointers") } if got := stableKeywords("a ok https://example.test/path?q=x valid"); len(got) != 1 || got[0] != "valid" { t.Fatalf("placeholder/small keyword filtering = %#v", got) } if got := uniqueStrings([]string{" ", "one", "one"}); len(got) != 1 { t.Fatalf("unique strings = %#v", got) } if got := extractDocActions([]KBHit{{Snippet: "ordinary text\n| 1 | 2 | 3 | 4 | act |"}}); len(got) != 1 { t.Fatalf("non-table doc line handling = %#v", got) } var nilPlan *RecoveryPlan HydratePlanForEvent("evt", RecoveryContext{}, Replay{}, nilPlan) } type fakeRecoveryInvoker struct { result *transport.ToolCallResult err error } func (f fakeRecoveryInvoker) CallToolDirect(context.Context, string, string, map[string]any) (*transport.ToolCallResult, error) { return f.result, f.err } func TestCrossPlatformCoverageExecutorAndProbeEdges(t *testing.T) { last := LastError{ EventID: "evt-1", Context: RecoveryContext{ CommandPath: []string{"aitable", "base", "get"}, ServerID: "aitable", ToolName: "get_base", OperationKind: OperationRead, ArgsSummary: map[string]any{"baseId": "base_1"}, RawError: "unknown", }, Replay: Replay{ ServerID: "aitable", ToolName: "get_base", OperationKind: OperationRead, ToolArgs: map[string]any{"baseId": "base_1"}, RedactedCommand: "dws aitable base get", }, } if got := (*Executor)(nil).Execute(context.Background(), last); got.Status != "analysis_failed" { t.Fatalf("nil executor = %#v", got) } if got := (&Executor{}).Execute(context.Background(), last); got.Status != "analysis_failed" { t.Fatalf("nil planner executor = %#v", got) } result := &transport.ToolCallResult{Blocks: []transport.ContentBlock{{Type: "text", Text: "{\"bases\":[1]}"}}} executor := NewExecutor(NewPlanner(fakeRetriever{}), fakeRecoveryInvoker{result: result}) bundle := executor.Execute(context.Background(), last) if bundle.Status != "needs_agent_action" || len(bundle.ProbeResults) != 2 || bundle.Plan.AgentRoute.Payload == nil || len(bundle.Plan.AgentRoute.Payload.ProbeResults) != 2 { t.Fatalf("successful bundle = %#v", bundle) } if bundle.FinalizeHint.Command == "" || len(bundle.AgentTask.MustReadRefs) != 4 { t.Fatalf("agent handoff metadata = %#v %#v", bundle.FinalizeHint, bundle.AgentTask) } failing := NewExecutor(NewPlanner(fakeRetriever{err: errors.New("docs")}), fakeRecoveryInvoker{err: errors.New("probe")}) failedBundle := failing.Execute(context.Background(), LastError{EventID: "evt-2", Context: last.Context}) if failedBundle.Status != "analysis_failed" || !strings.Contains(failedBundle.AnalysisError, "replay data missing") || !strings.Contains(failedBundle.AnalysisError, "doc search failed") { t.Fatalf("failed bundle = %#v", failedBundle) } executor.probes = nil if got := executor.runProbes(context.Background(), last.Context, last.Replay, RecoveryPlan{}); got != nil { t.Fatalf("empty probes = %#v", got) } executor.probes = []Probe{ func(context.Context, ToolInvoker, RecoveryContext, Replay, RecoveryPlan) *ProbeResult { return nil }, func(context.Context, ToolInvoker, RecoveryContext, Replay, RecoveryPlan) *ProbeResult { return &ProbeResult{Name: "custom", Status: "success"} }, } if got := executor.runProbes(context.Background(), last.Context, last.Replay, RecoveryPlan{}); len(got) != 1 { t.Fatalf("custom probes = %#v", got) } if got := probeUnknownContextAudit(context.Background(), nil, RecoveryContext{}, Replay{}, RecoveryPlan{Category: "auth"}); got != nil { t.Fatalf("built-in plan audit = %#v", got) } audit := probeUnknownContextAudit(context.Background(), nil, RecoveryContext{}, Replay{}, RecoveryPlan{Category: "unknown"}) if audit == nil || !strings.Contains(audit.Summary, "no identifier") { t.Fatalf("empty audit = %#v", audit) } fullAudit := probeUnknownContextAudit( context.Background(), nil, RecoveryContext{ CommandPath: []string{"x"}, ArgsSummary: map[string]any{"empty_id": "", "name": "n", "record_id": "r1"}, }, Replay{ToolArgs: map[string]any{"uuid": "u1"}, RedactedCommand: "dws x"}, RecoveryPlan{ Category: "unknown", DocSearch: DocSearch{Status: "success"}, KBHits: []KBHit{{Title: "hit"}}, DocActions: []DocAction{{Action: "act"}}, }, ) if fullAudit == nil || !strings.Contains(fullAudit.Summary, "found 2") { t.Fatalf("full audit = %#v", fullAudit) } if got := probeAITableBaseCatalog(context.Background(), nil, RecoveryContext{}, Replay{}, RecoveryPlan{}); got != nil { t.Fatalf("unrelated probe = %#v", got) } if got := probeAITableBaseCatalog(context.Background(), nil, RecoveryContext{CommandPath: []string{"aitable", "base", "list"}}, Replay{}, RecoveryPlan{}); got.Status != "skipped" { t.Fatalf("unsupported aitable probe = %#v", got) } if got := probeAITableBaseCatalog(context.Background(), nil, last.Context, last.Replay, RecoveryPlan{}); got.Status != "skipped" { t.Fatalf("nil invoker probe = %#v", got) } outputs := []struct { result *transport.ToolCallResult summary string }{ {nil, "no output"}, {&transport.ToolCallResult{Blocks: []transport.ContentBlock{{Type: "image"}, {Type: "text", Text: " plain "}}}, "text payload"}, {&transport.ToolCallResult{Content: map[string]any{"x": 1}}, "content payload"}, {&transport.ToolCallResult{StructuredContent: map[string]any{"x": 1}}, "non-text payload"}, } for _, tt := range outputs { _, summary := summarizeProbeOutput(tt.result) if !strings.Contains(summary, tt.summary) { t.Errorf("summarizeProbeOutput = %q, want %q", summary, tt.summary) } } if len(buildMustReadRefs(nil)) != 3 || productReferenceFor("unknown") != "" { t.Fatal("must-read references failed") } for _, product := range []string{"aitable", "attendance", "calendar", "chat", "contact", "devdoc", "ding", "report", "todo", "workbench", "approval"} { if productReferenceFor(product) == "" { t.Errorf("missing product reference for %q", product) } } if cloneProbeResults(nil) != nil { t.Fatal("nil probe clone should remain nil") } } func TestCrossPlatformCoverageStoreRuntimeAndPruningEdges(t *testing.T) { ResetRuntimeState() if LatestCapture() != nil { t.Fatal("latest capture should start nil") } setLatestCapture(nil) last := &LastError{ EventID: "evt", Context: RecoveryContext{ CommandPath: []string{"x"}, ArgsSummary: map[string]any{"nested": map[string]any{"x": 1}}, }, Replay: Replay{ ToolArgs: map[string]any{"slice": []any{map[string]any{"x": 1}}, "strings": []string{"x"}}, RedactedArgv: []string{"dws"}, }, } setLatestCapture(last) got := LatestCapture() if got == last || got.Context.ArgsSummary == nil || got.Replay.ToolArgs == nil { t.Fatalf("LatestCapture clone = %#v", got) } ResetRuntimeState() if cloneMap(nil) != nil || cloneSlice(nil) != nil { t.Fatal("nil clones should remain nil") } if cloneValue(7) != 7 { t.Fatal("scalar clone changed") } if !isEmptyReplay(Replay{}) || isEmptyReplay(Replay{ToolName: "x"}) { t.Fatal("empty replay detection failed") } var disabled *Store if disabled.Enabled() || NewStore("").Enabled() { t.Fatal("disabled store reported enabled") } if captured, err := disabled.Capture(RecoveryContext{}); err != nil || captured != nil { t.Fatalf("disabled capture = %#v, %v", captured, err) } for name, call := range map[string]func() error{ "load": func() error { _, err := disabled.LoadLastError(); return err }, "event": func() error { _, err := disabled.LoadErrorByEvent("evt"); return err }, "ensure": disabled.ensureDir, } { if err := call(); err == nil { t.Errorf("disabled %s unexpectedly succeeded", name) } } if err := disabled.SavePlan("evt", RecoveryPlan{}); err != nil { t.Fatal(err) } if err := disabled.SaveAnalysis("evt", RecoveryPlan{}, RecoveryBundle{}); err != nil { t.Fatal(err) } if err := disabled.Finalize("evt", "failed", nil); err != nil { t.Fatal(err) } if err := disabled.pruneExpiredArtifacts(time.Now()); err != nil { t.Fatal(err) } dir := t.TempDir() store := NewStore(dir) if _, err := store.LoadErrorByEvent(""); err == nil { t.Fatal("empty event id succeeded") } if _, err := store.LoadErrorByEvent("missing"); err == nil { t.Fatal("missing event id succeeded") } if _, err := store.LoadLastError(); err == nil { t.Fatal("missing last error succeeded") } badLast := store.lastErrorPath() if err := os.MkdirAll(store.dir(), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(badLast, []byte("{"), 0o600); err != nil { t.Fatal(err) } if _, err := store.LoadLastError(); err == nil { t.Fatal("malformed last error succeeded") } cutoff := time.Now().UTC().Add(-recoveryMaxAge) old := cutoff.Add(-time.Hour).Format(time.RFC3339Nano) recent := cutoff.Add(time.Hour).Format(time.RFC3339Nano) if _, ok := parseRecordedAt(""); ok { t.Fatal("empty timestamp parsed") } if _, ok := parseRecordedAt("invalid"); ok { t.Fatal("invalid timestamp parsed") } if _, ok := parseRecordedAt(time.Now().UTC().Format(time.RFC3339)); !ok { t.Fatal("RFC3339 timestamp did not parse") } if err := os.WriteFile(badLast, []byte("{\"recorded_at\":\""+old+"\"}"), 0o600); err != nil { t.Fatal(err) } if err := store.pruneLastError(cutoff); err != nil { t.Fatal(err) } if _, err := os.Stat(badLast); !os.IsNotExist(err) { t.Fatalf("old last error remains: %v", err) } if err := os.WriteFile(badLast, []byte("{\"recorded_at\":\""+recent+"\"}"), 0o600); err != nil { t.Fatal(err) } if err := store.pruneLastError(cutoff); err != nil { t.Fatal(err) } events := strings.Join([]string{ "", "{", "{\"event_id\":\"old\",\"recorded_at\":\"" + old + "\"}", "{\"event_id\":\"recent\",\"recorded_at\":\"" + recent + "\"}", }, "\n") + "\n" if err := os.WriteFile(store.eventsPath(), []byte(events), 0o600); err != nil { t.Fatal(err) } if err := store.pruneEvents(cutoff); err != nil { t.Fatal(err) } data, err := os.ReadFile(store.eventsPath()) if err != nil || strings.Contains(string(data), "old") || !strings.Contains(string(data), "recent") { t.Fatalf("pruned events = %q, %v", data, err) } if err := os.WriteFile(store.eventsPath(), []byte("{\"event_id\":\"old\",\"recorded_at\":\""+old+"\"}\n"), 0o600); err != nil { t.Fatal(err) } if err := store.pruneEvents(cutoff); err != nil { t.Fatal(err) } if _, err := os.Stat(store.eventsPath()); !os.IsNotExist(err) { t.Fatalf("empty events file remains: %v", err) } oldArtifact := filepath.Join(store.dir(), "old-artifact") recentArtifact := filepath.Join(store.dir(), "recent-artifact") if err := os.MkdirAll(oldArtifact, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(recentArtifact, []byte("x"), 0o600); err != nil { t.Fatal(err) } oldTime := cutoff.Add(-time.Hour) if err := os.Chtimes(oldArtifact, oldTime, oldTime); err != nil { t.Fatal(err) } if err := store.pruneOtherArtifacts(cutoff); err != nil { t.Fatal(err) } if _, err := os.Stat(oldArtifact); !os.IsNotExist(err) { t.Fatalf("old artifact remains: %v", err) } if _, err := os.Stat(recentArtifact); err != nil { t.Fatalf("recent artifact removed: %v", err) } emptyReplay, err := store.Capture(RecoveryContext{RawError: "x"}) if err != nil || emptyReplay == nil { t.Fatalf("empty replay capture = %#v, %v", emptyReplay, err) } if _, err := store.LoadErrorByEvent(emptyReplay.EventID); err != nil { t.Fatal(err) } } func TestCrossPlatformCoverageStoreMarshalErrors(t *testing.T) { store := NewStore(t.TempDir()) if err := store.writeJSON(filepath.Join(store.dir(), "bad.json"), map[string]any{"bad": func() {}}); err == nil { t.Fatal("expected writeJSON marshal error") } event := RecoveryEvent{ EventID: "evt", Bundle: &RecoveryBundle{ ProbeResults: []ProbeResult{{Output: func() {}}}, }, } if err := store.appendEvent(event); err == nil { t.Fatal("expected appendEvent marshal error") } } func TestCrossPlatformCoverageLoadErrorByEventFallback(t *testing.T) { store := NewStore(t.TempDir()) last := LastError{EventID: "fallback", RecordedAt: time.Now().UTC().Format(time.RFC3339Nano)} data, err := json.Marshal(last) if err != nil { t.Fatal(err) } if err := os.MkdirAll(store.dir(), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(store.lastErrorPath(), data, 0o600); err != nil { t.Fatal(err) } if got, err := store.LoadErrorByEvent("fallback"); err != nil || got.EventID != "fallback" { t.Fatalf("fallback event = %#v, %v", got, err) } otherEvent := RecoveryEvent{ EventID: "other", Phase: "planned", RecordedAt: time.Now().UTC().Format(time.RFC3339Nano), } eventData, err := json.Marshal(otherEvent) if err != nil { t.Fatal(err) } if err := os.WriteFile(store.eventsPath(), append(eventData, '\n'), 0o600); err != nil { t.Fatal(err) } if got, err := store.LoadErrorByEvent("fallback"); err != nil || got.EventID != "fallback" { t.Fatalf("EOF fallback event = %#v, %v", got, err) } } func TestCrossPlatformCoverageStoreSystemCallEdges(t *testing.T) { oldReadFile := recoveryReadFile oldWriteFile := recoveryWriteFile oldOpen := recoveryOpen oldOpenFile := recoveryOpenFile oldMkdirAll := recoveryMkdirAll oldReadDir := recoveryReadDir oldStat := recoveryStat oldRemove := recoveryRemove oldRemoveAll := recoveryRemoveAll oldFileWrite := recoveryFileWrite t.Cleanup(func() { recoveryReadFile = oldReadFile recoveryWriteFile = oldWriteFile recoveryOpen = oldOpen recoveryOpenFile = oldOpenFile recoveryMkdirAll = oldMkdirAll recoveryReadDir = oldReadDir recoveryStat = oldStat recoveryRemove = oldRemove recoveryRemoveAll = oldRemoveAll recoveryFileWrite = oldFileWrite }) reset := func() { recoveryReadFile = oldReadFile recoveryWriteFile = oldWriteFile recoveryOpen = oldOpen recoveryOpenFile = oldOpenFile recoveryMkdirAll = oldMkdirAll recoveryReadDir = oldReadDir recoveryStat = oldStat recoveryRemove = oldRemove recoveryRemoveAll = oldRemoveAll recoveryFileWrite = oldFileWrite } store := NewStore(t.TempDir()) recoveryWriteFile = func(string, []byte, os.FileMode) error { return errors.New("write") } if _, err := store.Capture(RecoveryContext{}); err == nil { t.Fatal("expected capture snapshot write error") } reset() recoveryOpenFile = func(string, int, os.FileMode) (*os.File, error) { return nil, errors.New("open event") } if _, err := store.Capture(RecoveryContext{}); err == nil { t.Fatal("expected capture event append error") } reset() recoveryReadFile = func(string) ([]byte, error) { return nil, errors.New("prune") } if _, err := store.LoadLastError(); err == nil { t.Fatal("expected LoadLastError prune error") } if _, err := store.LoadErrorByEvent("evt"); err == nil { t.Fatal("expected LoadErrorByEvent prune error") } if err := store.pruneExpiredArtifacts(time.Now()); err == nil { t.Fatal("expected last-error prune failure") } reset() recoveryReadFile = func(path string) ([]byte, error) { if path == store.lastErrorPath() { return nil, os.ErrNotExist } if path == store.eventsPath() { return nil, errors.New("events") } return oldReadFile(path) } if err := store.pruneExpiredArtifacts(time.Now()); err == nil { t.Fatal("expected event prune failure") } reset() recoveryOpen = func(string) (*os.File, error) { return nil, errors.New("open") } if _, err := store.LoadErrorByEvent("evt"); err == nil { t.Fatal("expected event open error") } reset() if err := os.MkdirAll(store.dir(), 0o755); err != nil { t.Fatal(err) } if err := os.Mkdir(store.eventsPath(), 0o755); err != nil { t.Fatal(err) } recoveryReadFile = func(path string) ([]byte, error) { if path == store.lastErrorPath() || path == store.eventsPath() { return nil, os.ErrNotExist } return oldReadFile(path) } if _, err := store.LoadErrorByEvent("evt"); err == nil || os.IsNotExist(err) { t.Fatalf("expected non-EOF reader error, got %v", err) } if err := os.RemoveAll(store.eventsPath()); err != nil { t.Fatal(err) } reset() recoveryMkdirAll = func(string, os.FileMode) error { return errors.New("mkdir") } if err := store.writeJSON(store.lastErrorPath(), map[string]any{"x": 1}); err == nil { t.Fatal("expected writeJSON ensure error") } if err := store.appendEvent(RecoveryEvent{}); err == nil { t.Fatal("expected appendEvent ensure error") } if err := store.ensureDir(); err == nil { t.Fatal("expected ensureDir mkdir error") } reset() recoveryOpenFile = func(string, int, os.FileMode) (*os.File, error) { return nil, errors.New("open file") } if err := store.appendEvent(RecoveryEvent{}); err == nil { t.Fatal("expected appendEvent open error") } reset() recoveryFileWrite = func(*os.File, []byte) (int, error) { return 0, errors.New("write event") } if err := store.appendEvent(RecoveryEvent{}); err == nil { t.Fatal("expected appendEvent write error") } reset() cutoff := time.Now().UTC().Add(-recoveryMaxAge) if err := os.MkdirAll(store.dir(), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(store.lastErrorPath(), []byte("{"), 0o600); err != nil { t.Fatal(err) } oldTime := cutoff.Add(-time.Hour) if err := os.Chtimes(store.lastErrorPath(), oldTime, oldTime); err != nil { t.Fatal(err) } if err := store.pruneLastError(cutoff); err != nil { t.Fatal(err) } reset() if err := os.WriteFile(store.lastErrorPath(), []byte("{"), 0o600); err != nil { t.Fatal(err) } recoveryStat = func(string) (os.FileInfo, error) { return nil, os.ErrNotExist } if err := store.pruneLastError(cutoff); err != nil { t.Fatal(err) } recoveryStat = func(string) (os.FileInfo, error) { return nil, errors.New("stat") } if err := store.pruneLastError(cutoff); err == nil { t.Fatal("expected last-error stat error") } reset() recoveryReadDir = func(string) ([]os.DirEntry, error) { return nil, errors.New("read dir") } if err := store.pruneOtherArtifacts(cutoff); err == nil { t.Fatal("expected artifact read error") } reset() artifact := filepath.Join(store.dir(), "artifact") if err := os.WriteFile(artifact, []byte("x"), 0o600); err != nil { t.Fatal(err) } recoveryStat = func(path string) (os.FileInfo, error) { if path == artifact { return nil, os.ErrNotExist } return oldStat(path) } if err := store.pruneOtherArtifacts(cutoff); err != nil { t.Fatal(err) } recoveryStat = func(path string) (os.FileInfo, error) { if path == artifact { return nil, errors.New("stat") } return oldStat(path) } if err := store.pruneOtherArtifacts(cutoff); err == nil { t.Fatal("expected artifact stat error") } reset() if err := os.Chtimes(artifact, oldTime, oldTime); err != nil { t.Fatal(err) } recoveryRemoveAll = func(string) error { return errors.New("remove") } if err := store.pruneOtherArtifacts(cutoff); err == nil { t.Fatal("expected artifact removal error") } }