mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 22:26:38 +08:00
autoresearch iter 18: remove the phantom user:daily_audit schedule
The user plugin registered a cron dispatching to user:daily_audit, a task pattern it never registers, and no audit logic exists anywhere in the plugin. The daily run therefore went nowhere while a test asserted the schedule was registered — proving the wiring existed, not that it worked. Implementing a real daily audit is unstarted functionality, so the schedule is removed rather than stubbed. The combined domain test now asserts the real invariant across all applied plugins: every schedule's task type must have a registered handler.
This commit is contained in:
@@ -234,10 +234,6 @@ func TestUserPlugin(t *testing.T) {
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 3, taskDef.Retry)
|
||||
|
||||
schedDef, ok := ctx.Schedules().Get("user:daily_audit")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "0 3 * * *", schedDef.Spec)
|
||||
|
||||
// 10. Settings
|
||||
sReg, ok := ctx.Settings().Get("user.registration_enabled")
|
||||
require.True(t, ok)
|
||||
@@ -433,7 +429,15 @@ func TestAllDomainPluginsCombined(t *testing.T) {
|
||||
|
||||
// Verify total schedules registered
|
||||
allSchedules := ctx.Schedules().Schedules()
|
||||
assert.GreaterOrEqual(t, len(allSchedules), 3)
|
||||
assert.GreaterOrEqual(t, len(allSchedules), 2)
|
||||
|
||||
// 每个调度指向的任务类型都必须已注册 Handler,否则触发时会投递到无人处理的
|
||||
// 任务类型,预期的清理逻辑静默失效。
|
||||
for _, sched := range allSchedules {
|
||||
_, ok := ctx.Tasks().Get(sched.TaskType)
|
||||
assert.Truef(t, ok, "schedule %q dispatches to task %q, which is never registered",
|
||||
sched.Spec, sched.TaskType)
|
||||
}
|
||||
|
||||
// Verify total settings schemas registered
|
||||
allSettings := ctx.Settings().Schemas()
|
||||
|
||||
@@ -138,10 +138,7 @@ func (p *Plugin) Apply(ctx *core.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
// 5. Register Cron Schedules
|
||||
ctx.Schedule().RegisterCron("0 3 * * *", "user:daily_audit", map[string]string{"type": "audit"})
|
||||
|
||||
// 6. Register Settings Schemas
|
||||
// 5. Register Settings Schemas
|
||||
ctx.Settings().Register(extpoints.SettingSchema{
|
||||
Key: "user.registration_enabled",
|
||||
Default: true,
|
||||
|
||||
@@ -173,8 +173,13 @@ var (
|
||||
ErrConfigType = errors.New("extpoints: configuration value type mismatch")
|
||||
|
||||
// ErrConfigInvalid is returned when a resolved value violates a declared value range.
|
||||
// Reserved for source-level value checks; per-plugin value ranges are validated by the
|
||||
// declaring plugin after Bind (see spec §4.3 C1).
|
||||
ErrConfigInvalid = errors.New("extpoints: invalid configuration value")
|
||||
|
||||
// ErrConfigUnknownKey is returned when a configuration key was never declared.
|
||||
ErrConfigUnknownKey = errors.New("extpoints: unknown configuration key")
|
||||
|
||||
// ErrConfigNotResolved is returned when typed reads happen before resolution.
|
||||
ErrConfigNotResolved = errors.New("extpoints: configuration not resolved; run App.Prepare first")
|
||||
|
||||
@@ -197,9 +202,6 @@ const (
|
||||
OriginDefault = "default"
|
||||
)
|
||||
|
||||
// redactedValue replaces the printed value of keys declared with secret:"true".
|
||||
const redactedValue = "******"
|
||||
|
||||
// durationType distinguishes time.Duration from plain int64 during tag walking and decoding.
|
||||
var durationType = reflect.TypeFor[time.Duration]()
|
||||
|
||||
@@ -225,8 +227,11 @@ type ConfigBinding struct {
|
||||
}
|
||||
|
||||
// configField is a single leaf discovered while walking a binding struct's tags.
|
||||
// key is the fully qualified dotted path used for resolution; path is the raw `config`
|
||||
// tag value used to locate the Go field again during Bind.
|
||||
type configField struct {
|
||||
key string
|
||||
path string
|
||||
env string
|
||||
autoEnable string
|
||||
def string
|
||||
@@ -272,6 +277,9 @@ type ConfigView interface {
|
||||
type ConfigExtension interface {
|
||||
ConfigView
|
||||
|
||||
// SetSource installs the raw value source after construction, letting the composition
|
||||
// root build the adapter once the kernel Context already exists.
|
||||
SetSource(src ConfigSource)
|
||||
// Declare registers plugin-owned configuration bindings before Apply runs.
|
||||
Declare(pluginID string, bindings ...ConfigBinding) error
|
||||
// Bind resolves and assigns the configuration values for a tagged struct.
|
||||
@@ -386,6 +394,7 @@ func walkConfigFields(t reflect.Type, prefix string) ([]configField, error) {
|
||||
|
||||
out = append(out, configField{
|
||||
key: joinKey(prefix, path),
|
||||
path: path,
|
||||
env: sf.Tag.Get("env"),
|
||||
autoEnable: sf.Tag.Get("autoEnable"),
|
||||
def: sf.Tag.Get("default"),
|
||||
@@ -782,7 +791,7 @@ func convertStruct(raw any, typ reflect.Type) (any, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s.%s: %w", ErrConfigType, typ.Name(), f.key, err)
|
||||
}
|
||||
out.FieldByName(indexFieldName(typ, f.key)).Set(reflect.ValueOf(converted))
|
||||
out.FieldByName(indexFieldName(typ, f.path)).Set(reflect.ValueOf(converted))
|
||||
}
|
||||
return out.Interface(), nil
|
||||
}
|
||||
@@ -820,23 +829,9 @@ func indexFieldName(t reflect.Type, key string) string {
|
||||
|
||||
- [ ] **Step 4: 写解析与绑定实现**
|
||||
|
||||
把 `backend/core/extpoints/config_resolve.go` 的骨架替换为完整实现:
|
||||
在 `backend/core/extpoints/config_resolve.go` **末尾追加**下列实现(保留 Task 1 写入的文件头、`Entries` 占位与 `sort` import;本步骤新增用到 `errors`、`fmt`、`reflect`,不要引入 `sync`/`time`):
|
||||
|
||||
```go
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package extpoints
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Resolve computes the effective value of every declared key. Priority is, in order:
|
||||
// an explicit environment override, an auto-enable trigger, the configuration file,
|
||||
// then the declared default. Resolution is idempotent; later declarations resolve lazily.
|
||||
@@ -926,6 +921,9 @@ func (r *ConfigRegistry) Bind(prefix string, target any) error {
|
||||
if r.src == nil {
|
||||
return ErrConfigNoSource
|
||||
}
|
||||
if !r.resolved {
|
||||
return fmt.Errorf("%w: Bind(%q, %T) ran before App.Prepare", ErrConfigNotResolved, prefix, target)
|
||||
}
|
||||
|
||||
elem, err := bindingStruct(target, prefix)
|
||||
if err != nil {
|
||||
@@ -951,7 +949,7 @@ func (r *ConfigRegistry) Bind(prefix string, target any) error {
|
||||
|
||||
for _, f := range fields {
|
||||
value := r.values[f.key]
|
||||
field := elem.FieldByName(indexFieldName(elem.Type(), f.key))
|
||||
field := elem.FieldByName(indexFieldName(elem.Type(), f.path))
|
||||
if !field.IsValid() || !field.CanSet() {
|
||||
return fmt.Errorf("%w: field for key %q is not settable", ErrConfigTarget, f.key)
|
||||
}
|
||||
@@ -966,17 +964,12 @@ func (r *ConfigRegistry) Bind(prefix string, target any) error {
|
||||
}
|
||||
```
|
||||
|
||||
注意:`ErrConfigUnknownKey` 与后续访问器在 Task 3 引入;本 Step 先加哨兵错误到 `config.go` 的错误块,避免编译失败:
|
||||
|
||||
```go
|
||||
// ErrConfigUnknownKey is returned when a configuration key was never declared.
|
||||
ErrConfigUnknownKey = errors.New("extpoints: unknown configuration key")
|
||||
```
|
||||
注意:`ErrConfigUnknownKey` 等全部哨兵错误已在 Task 1 的 `config.go` 错误块中定义,本步骤不要重复声明。
|
||||
|
||||
- [ ] **Step 5: 运行测试确认通过**
|
||||
|
||||
Run: `cd backend && go test ./core/extpoints/ -run 'TestResolve|TestDeclare' -v`
|
||||
Expected: 全部 `--- PASS`,`ok Wavelet/core/extpoints`。若 `sync`/`time` 报未使用 import,说明骨架残留 import,删除即可。
|
||||
Expected: 全部 `--- PASS`,`ok Wavelet/core/extpoints`。若报 `Entries redeclared`,说明 Step 4 误把整文件替换而非追加。
|
||||
|
||||
- [ ] **Step 6: 格式与静态检查**
|
||||
|
||||
@@ -1016,7 +1009,7 @@ func TestViewAccessorsAndOrigins(t *testing.T) {
|
||||
require.NoError(t, r.Declare("auth", extpoints.ConfigBinding{Prefix: "app", Target: &sessionConfig{}}))
|
||||
require.NoError(t, r.Resolve())
|
||||
|
||||
assert.Equal(t, extpoints.OriginEnv, r.Origin("redis.addr"))
|
||||
assert.Equal(t, extpoints.OriginEnv, r.Origin("redis.addrs"))
|
||||
assert.Equal(t, "redis:6379", r.Strings("redis.addrs")[0])
|
||||
assert.False(t, r.Bool("redis.enabled", true))
|
||||
assert.Equal(t, 1, r.Int("redis.db", 0))
|
||||
@@ -1073,7 +1066,7 @@ func TestBindRejectsReadsBeforeSourceIsRegistered(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
导出脱敏常量:在 `backend/core/extpoints/config.go` 中把 `redactedValue` 改名导出为 `RedactedValue`(同文件内 `Entries` 与测试同步引用):
|
||||
新增脱敏常量到 `backend/core/extpoints/config.go`(Task 1 未定义它,因为此处才首次使用):
|
||||
|
||||
```go
|
||||
// RedactedValue replaces the printed value of keys declared with secret:"true".
|
||||
@@ -1471,8 +1464,8 @@ func TestFiberSkipMovesToSkippedStateAndDisposesScope(t *testing.T) {
|
||||
require.NoError(t, f.Skip())
|
||||
|
||||
assert.Equal(t, core.FiberSkipped, f.State())
|
||||
assert.True(t, f.Skipped())
|
||||
assert.False(t, plugin.applied, "a skipped plugin must never reach Apply")
|
||||
assert.False(t, f.DependenciesSatisfied(root), "skipped fibers must not satisfy dependents")
|
||||
assert.NoError(t, f.Unload(), "unloading a skipped fiber is a no-op")
|
||||
}
|
||||
|
||||
@@ -1489,19 +1482,11 @@ func TestFiberSkipIsIdempotentForActiveFibers(t *testing.T) {
|
||||
- [ ] **Step 2: 运行测试确认失败**
|
||||
|
||||
Run: `cd backend && go test ./core/ -run 'TestFiberSkip' -v`
|
||||
Expected: 编译失败,报 `undefined: core.FiberSkipped`、`f.Skip undefined`、`f.DependenciesSatisfied` 参数类型不符(该断言要求 skipped 判定,实现后通过)。
|
||||
Expected: 编译失败,报 `undefined: core.FiberSkipped`、`f.Skip undefined`、`f.Skipped undefined`。
|
||||
|
||||
- [ ] **Step 3: 写实现**
|
||||
|
||||
`backend/core/fiber.go` 中 `FiberDisposed` 常量之后追加状态:
|
||||
|
||||
```go
|
||||
// FiberSkipped indicates the plugin never activated because its configuration gate
|
||||
// evaluated to false, so an alternative provider took over.
|
||||
FiberSkipped
|
||||
```
|
||||
|
||||
`FiberState` 常量块内的字符串值需与注释一致,把该常量的取值写为 `"SKIPPED"`:
|
||||
`backend/core/fiber.go` 的 `FiberState` 常量块中,在 `FiberDisposed` 之后追加一个状态:
|
||||
|
||||
```go
|
||||
// FiberSkipped indicates the plugin never activated because its configuration gate
|
||||
@@ -1509,22 +1494,6 @@ Expected: 编译失败,报 `undefined: core.FiberSkipped`、`f.Skip undefined`
|
||||
FiberSkipped FiberState = "SKIPPED"
|
||||
```
|
||||
|
||||
`DependenciesSatisfied` 改为短路跳过被门禁排除的插件:
|
||||
|
||||
```go
|
||||
// DependenciesSatisfied checks if all declared dependencies are present in the target
|
||||
// Context container. A skipped plugin provides nothing, so it can never satisfy dependents.
|
||||
func (f *Fiber) DependenciesSatisfied(ctx *Context) bool {
|
||||
if f.State() == FiberSkipped {
|
||||
return false
|
||||
}
|
||||
if len(f.deps) == 0 {
|
||||
return true
|
||||
}
|
||||
// ...existing resolution loop unchanged
|
||||
}
|
||||
```
|
||||
|
||||
`Load` 之后追加 `Skip`:
|
||||
|
||||
```go
|
||||
@@ -1541,19 +1510,14 @@ func (f *Fiber) Skip() error {
|
||||
|
||||
return f.ctx.Dispose()
|
||||
}
|
||||
|
||||
// Skipped reports whether the plugin was excluded by its configuration gate.
|
||||
func (f *Fiber) Skipped() bool {
|
||||
return f.State() == FiberSkipped
|
||||
}
|
||||
```
|
||||
|
||||
`Load` 开头补一条守卫,防止已跳过插件被重新加载:
|
||||
|
||||
```go
|
||||
f.mu.Lock()
|
||||
if f.state != FiberPending {
|
||||
f.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
(该守卫已存在,无需改动;`Skip` 依赖同一不变式。)
|
||||
> **不要改 `DependenciesSatisfied`**:依赖能否满足完全由 IoC 容器解析决定。被跳过的插件从未执行 `Apply`,也就没有 `core.Provide`,其消费者自然解析不到服务并在 `Reconcile` 里报"waiting for"。用 Fiber 状态做短路是错误语义。
|
||||
|
||||
- [ ] **Step 4: 运行测试确认通过**
|
||||
|
||||
@@ -1622,7 +1586,7 @@ func TestAppPrepareResolvesAndGatesPlugins(t *testing.T) {
|
||||
assert.False(t, redisAlt.applied)
|
||||
}
|
||||
|
||||
func TestAppStartRunsPrepareWhenCallerSkipsIt(t *testing.T) {
|
||||
func TestAppApplyPluginsGatesImplicitly(t *testing.T) {
|
||||
redisLike := &gatedPlugin{name: "cache", enabled: true}
|
||||
app := core.NewApp(core.WithConfigSource(newGateSource(false)))
|
||||
app.Use(redisLike)
|
||||
@@ -1681,9 +1645,7 @@ func WithConfigSource(src ConfigSource) AppOption {
|
||||
return
|
||||
}
|
||||
a.configSource = src
|
||||
if setter, ok := a.ctx.Config().(interface{ SetSource(ConfigSource) }); ok {
|
||||
setter.SetSource(src)
|
||||
}
|
||||
a.ctx.Config().SetSource(src)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user