package loaders import ( "context" "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TC-1112: parent 取消后 detached ctx 仍存活,到 3s 硬超时才 Done。 func TestDetachCacheCleanCtx_ParentCancelDoesNotPropagate(t *testing.T) { parent, cancel := context.WithCancel(context.Background()) cleanCtx, cleanCancel := DetachCacheCleanCtx(parent) defer cleanCancel() cancel() assert.NoError(t, cleanCtx.Err(), "parent 取消后 detached ctx 不应提前终止") select { case <-cleanCtx.Done(): t.Fatal("parent cancel 被传播到了 detached ctx") case <-time.After(100 * time.Millisecond): } } // TC-1113: deadline 必须落在 [now+2.5s, now+3.5s]。 func TestDetachCacheCleanCtx_HasThreeSecondDeadline(t *testing.T) { parent := context.Background() before := time.Now() cleanCtx, cancel := DetachCacheCleanCtx(parent) defer cancel() deadline, ok := cleanCtx.Deadline() require.True(t, ok, "detached ctx 必须挂 timeout deadline") lower := before.Add(2500 * time.Millisecond) upper := before.Add(3500 * time.Millisecond) assert.WithinRange(t, deadline, lower, upper, "deadline 必须在 ~3s 窗口内") } // TC-1114: parent 的 Value 透传不被剥离。 func TestDetachCacheCleanCtx_PreservesValues(t *testing.T) { type ctxKey struct{ name string } k := ctxKey{name: "trace"} parent := context.WithValue(context.Background(), k, "v1") cleanCtx, cancel := DetachCacheCleanCtx(parent) defer cancel() assert.Equal(t, "v1", cleanCtx.Value(k), "trace / tenant 等 Value 必须透传") } // TC-1115: isCtxCanceledErr 分类口径。 func TestIsCtxCanceledErr_Classification(t *testing.T) { assert.True(t, isCtxCanceledErr(context.Canceled)) assert.True(t, isCtxCanceledErr(context.DeadlineExceeded)) assert.True(t, isCtxCanceledErr( // 包装过一层仍应识别 wrapErr(context.Canceled, "clean userDetails: "), )) assert.False(t, isCtxCanceledErr(errors.New("redis down"))) assert.False(t, isCtxCanceledErr(nil)) } type wrappedErr struct { prefix string inner error } func (w *wrappedErr) Error() string { return w.prefix + w.inner.Error() } func (w *wrappedErr) Unwrap() error { return w.inner } func wrapErr(inner error, prefix string) error { return &wrappedErr{prefix: prefix, inner: inner} } // TC-1116: nil 错误时 logCacheInvalidationErr 早退,不触发任何日志写入。 func TestLogCacheInvalidationErr_NilShortCircuit(t *testing.T) { // 无返回值可断言,此用例仅需保证不 panic / 不阻塞即可。 // 若将来接入 logx buffer,可在此注入 TestCollector 断言"零条日志"。 done := make(chan struct{}) go func() { logCacheInvalidationErr(context.Background(), "scope", "detail", nil) close(done) }() select { case <-done: case <-time.After(200 * time.Millisecond): t.Fatal("logCacheInvalidationErr(nil) 不应阻塞") } } // TC-1116 附加:ctx 取消 / 普通错误两条分支都要走通,不允许 panic。 func TestLogCacheInvalidationErr_DoesNotPanic(t *testing.T) { assert.NotPanics(t, func() { logCacheInvalidationErr(context.Background(), "scope", "detail", context.Canceled) }) assert.NotPanics(t, func() { logCacheInvalidationErr(context.Background(), "scope", "detail", context.DeadlineExceeded) }) assert.NotPanics(t, func() { logCacheInvalidationErr(context.Background(), "scope", "detail", errors.New("redis down")) }) }