[agent/codex:gpt-5.4-mini] Read ~/spec/code/core/go/log/RFC.md fully. Find ONE feature ... #15

Merged
Virgil merged 1 commit from agent/read---spec-code-core-go-log-rfc-md-full into dev 2026-04-01 04:41:56 +00:00
4 changed files with 67 additions and 11 deletions

View file

@ -211,11 +211,11 @@ func inheritRecovery(dst *Err, err error) {
//
// retryAfter, ok := log.RetryAfter(err)
func RetryAfter(err error) (*time.Duration, bool) {
var wrapped *Err
if As(err, &wrapped) {
if wrapped.RetryAfter != nil {
for err != nil {
if wrapped, ok := err.(*Err); ok && wrapped.RetryAfter != nil {
return wrapped.RetryAfter, true
}
err = errors.Unwrap(err)
}
return nil, false
}
@ -235,13 +235,25 @@ func IsRetryable(err error) bool {
//
// next := log.RecoveryAction(err)
func RecoveryAction(err error) string {
var wrapped *Err
if As(err, &wrapped) {
return wrapped.NextAction
for err != nil {
if wrapped, ok := err.(*Err); ok && wrapped.NextAction != "" {
return wrapped.NextAction
}
err = errors.Unwrap(err)
}
return ""
}
func retryableHint(err error) bool {
for err != nil {
if wrapped, ok := err.(*Err); ok && wrapped.Retryable {
return true
}
err = errors.Unwrap(err)
}
return false
}
// --- Standard Library Wrappers ---
// Is reports whether any error in err's tree matches target.

View file

@ -286,6 +286,16 @@ func TestRetryAfter_Good(t *testing.T) {
assert.Equal(t, retryAfter, *got)
}
func TestRetryAfter_Good_NestedChain(t *testing.T) {
retryAfter := 42 * time.Second
inner := &Err{Msg: "typed", RetryAfter: &retryAfter}
outer := &Err{Msg: "outer", Err: inner}
got, ok := RetryAfter(outer)
assert.True(t, ok)
assert.Equal(t, retryAfter, *got)
}
func TestIsRetryable_Good(t *testing.T) {
err := &Err{Msg: "typed", Retryable: true}
assert.True(t, IsRetryable(err))
@ -296,6 +306,13 @@ func TestRecoveryAction_Good(t *testing.T) {
assert.Equal(t, "inspect", RecoveryAction(err))
}
func TestRecoveryAction_Good_NestedChain(t *testing.T) {
inner := &Err{Msg: "typed", NextAction: "inspect"}
outer := &Err{Msg: "outer", Err: inner}
assert.Equal(t, "inspect", RecoveryAction(outer))
}
func TestMessage_Good(t *testing.T) {
err := E("op", "the message", errors.New("base"))
assert.Equal(t, "the message", Message(err))

10
log.go
View file

@ -259,18 +259,18 @@ func (l *Logger) log(level Level, prefix, msg string, keyvals ...any) {
if As(err, &logErr) {
if _, hasRetryable := existing["retryable"]; !hasRetryable {
existing["retryable"] = struct{}{}
keyvals = append(keyvals, "retryable", logErr.Retryable)
keyvals = append(keyvals, "retryable", retryableHint(err))
}
if logErr.RetryAfter != nil {
if retryAfter, ok := RetryAfter(err); ok {
if _, hasRetryAfter := existing["retry_after_seconds"]; !hasRetryAfter {
existing["retry_after_seconds"] = struct{}{}
keyvals = append(keyvals, "retry_after_seconds", logErr.RetryAfter.Seconds())
keyvals = append(keyvals, "retry_after_seconds", retryAfter.Seconds())
}
}
if logErr.NextAction != "" {
if nextAction := RecoveryAction(err); nextAction != "" {
if _, hasNextAction := existing["next_action"]; !hasNextAction {
existing["next_action"] = struct{}{}
keyvals = append(keyvals, "next_action", logErr.NextAction)
keyvals = append(keyvals, "next_action", nextAction)
}
}
}

View file

@ -121,6 +121,33 @@ func TestLogger_ErrorContextIncludesRecovery_Good(t *testing.T) {
}
}
func TestLogger_ErrorContextIncludesNestedRecovery_Good(t *testing.T) {
var buf bytes.Buffer
l := New(Options{Output: &buf, Level: LevelInfo})
retryAfter := 30 * time.Second
inner := &Err{
Msg: "inner failure",
Retryable: true,
RetryAfter: &retryAfter,
NextAction: "retry later",
}
outer := &Err{Msg: "outer failure", Err: inner}
l.Error("request failed", "err", outer)
output := buf.String()
if !strings.Contains(output, "retryable=true") {
t.Errorf("expected output to contain retryable=true, got %q", output)
}
if !strings.Contains(output, "retry_after_seconds=30") {
t.Errorf("expected output to contain retry_after_seconds=30, got %q", output)
}
if !strings.Contains(output, "next_action=\"retry later\"") {
t.Errorf("expected output to contain next_action=\"retry later\", got %q", output)
}
}
func TestLogger_Redaction_Good(t *testing.T) {
var buf bytes.Buffer
l := New(Options{