mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 17:56:37 +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)
|
require.True(t, ok)
|
||||||
assert.Equal(t, 3, taskDef.Retry)
|
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
|
// 10. Settings
|
||||||
sReg, ok := ctx.Settings().Get("user.registration_enabled")
|
sReg, ok := ctx.Settings().Get("user.registration_enabled")
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
@@ -433,7 +429,15 @@ func TestAllDomainPluginsCombined(t *testing.T) {
|
|||||||
|
|
||||||
// Verify total schedules registered
|
// Verify total schedules registered
|
||||||
allSchedules := ctx.Schedules().Schedules()
|
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
|
// Verify total settings schemas registered
|
||||||
allSettings := ctx.Settings().Schemas()
|
allSettings := ctx.Settings().Schemas()
|
||||||
|
|||||||
@@ -138,10 +138,7 @@ func (p *Plugin) Apply(ctx *core.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// 5. Register Cron Schedules
|
// 5. Register Settings Schemas
|
||||||
ctx.Schedule().RegisterCron("0 3 * * *", "user:daily_audit", map[string]string{"type": "audit"})
|
|
||||||
|
|
||||||
// 6. Register Settings Schemas
|
|
||||||
ctx.Settings().Register(extpoints.SettingSchema{
|
ctx.Settings().Register(extpoints.SettingSchema{
|
||||||
Key: "user.registration_enabled",
|
Key: "user.registration_enabled",
|
||||||
Default: true,
|
Default: true,
|
||||||
|
|||||||
@@ -173,8 +173,13 @@ var (
|
|||||||
ErrConfigType = errors.New("extpoints: configuration value type mismatch")
|
ErrConfigType = errors.New("extpoints: configuration value type mismatch")
|
||||||
|
|
||||||
// ErrConfigInvalid is returned when a resolved value violates a declared value range.
|
// 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")
|
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 is returned when typed reads happen before resolution.
|
||||||
ErrConfigNotResolved = errors.New("extpoints: configuration not resolved; run App.Prepare first")
|
ErrConfigNotResolved = errors.New("extpoints: configuration not resolved; run App.Prepare first")
|
||||||
|
|
||||||
@@ -197,9 +202,6 @@ const (
|
|||||||
OriginDefault = "default"
|
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.
|
// durationType distinguishes time.Duration from plain int64 during tag walking and decoding.
|
||||||
var durationType = reflect.TypeFor[time.Duration]()
|
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.
|
// 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 {
|
type configField struct {
|
||||||
key string
|
key string
|
||||||
|
path string
|
||||||
env string
|
env string
|
||||||
autoEnable string
|
autoEnable string
|
||||||
def string
|
def string
|
||||||
@@ -272,6 +277,9 @@ type ConfigView interface {
|
|||||||
type ConfigExtension interface {
|
type ConfigExtension interface {
|
||||||
ConfigView
|
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 registers plugin-owned configuration bindings before Apply runs.
|
||||||
Declare(pluginID string, bindings ...ConfigBinding) error
|
Declare(pluginID string, bindings ...ConfigBinding) error
|
||||||
// Bind resolves and assigns the configuration values for a tagged struct.
|
// 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{
|
out = append(out, configField{
|
||||||
key: joinKey(prefix, path),
|
key: joinKey(prefix, path),
|
||||||
|
path: path,
|
||||||
env: sf.Tag.Get("env"),
|
env: sf.Tag.Get("env"),
|
||||||
autoEnable: sf.Tag.Get("autoEnable"),
|
autoEnable: sf.Tag.Get("autoEnable"),
|
||||||
def: sf.Tag.Get("default"),
|
def: sf.Tag.Get("default"),
|
||||||
@@ -782,7 +791,7 @@ func convertStruct(raw any, typ reflect.Type) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: %s.%s: %w", ErrConfigType, typ.Name(), f.key, err)
|
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
|
return out.Interface(), nil
|
||||||
}
|
}
|
||||||
@@ -820,23 +829,9 @@ func indexFieldName(t reflect.Type, key string) string {
|
|||||||
|
|
||||||
- [ ] **Step 4: 写解析与绑定实现**
|
- [ ] **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
|
```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:
|
// Resolve computes the effective value of every declared key. Priority is, in order:
|
||||||
// an explicit environment override, an auto-enable trigger, the configuration file,
|
// an explicit environment override, an auto-enable trigger, the configuration file,
|
||||||
// then the declared default. Resolution is idempotent; later declarations resolve lazily.
|
// 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 {
|
if r.src == nil {
|
||||||
return ErrConfigNoSource
|
return ErrConfigNoSource
|
||||||
}
|
}
|
||||||
|
if !r.resolved {
|
||||||
|
return fmt.Errorf("%w: Bind(%q, %T) ran before App.Prepare", ErrConfigNotResolved, prefix, target)
|
||||||
|
}
|
||||||
|
|
||||||
elem, err := bindingStruct(target, prefix)
|
elem, err := bindingStruct(target, prefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -951,7 +949,7 @@ func (r *ConfigRegistry) Bind(prefix string, target any) error {
|
|||||||
|
|
||||||
for _, f := range fields {
|
for _, f := range fields {
|
||||||
value := r.values[f.key]
|
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() {
|
if !field.IsValid() || !field.CanSet() {
|
||||||
return fmt.Errorf("%w: field for key %q is not settable", ErrConfigTarget, f.key)
|
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` 的错误块,避免编译失败:
|
注意:`ErrConfigUnknownKey` 等全部哨兵错误已在 Task 1 的 `config.go` 错误块中定义,本步骤不要重复声明。
|
||||||
|
|
||||||
```go
|
|
||||||
// ErrConfigUnknownKey is returned when a configuration key was never declared.
|
|
||||||
ErrConfigUnknownKey = errors.New("extpoints: unknown configuration key")
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 5: 运行测试确认通过**
|
- [ ] **Step 5: 运行测试确认通过**
|
||||||
|
|
||||||
Run: `cd backend && go test ./core/extpoints/ -run 'TestResolve|TestDeclare' -v`
|
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: 格式与静态检查**
|
- [ ] **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.Declare("auth", extpoints.ConfigBinding{Prefix: "app", Target: &sessionConfig{}}))
|
||||||
require.NoError(t, r.Resolve())
|
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.Equal(t, "redis:6379", r.Strings("redis.addrs")[0])
|
||||||
assert.False(t, r.Bool("redis.enabled", true))
|
assert.False(t, r.Bool("redis.enabled", true))
|
||||||
assert.Equal(t, 1, r.Int("redis.db", 0))
|
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
|
```go
|
||||||
// RedactedValue replaces the printed value of keys declared with secret:"true".
|
// 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())
|
require.NoError(t, f.Skip())
|
||||||
|
|
||||||
assert.Equal(t, core.FiberSkipped, f.State())
|
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, 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")
|
assert.NoError(t, f.Unload(), "unloading a skipped fiber is a no-op")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1489,19 +1482,11 @@ func TestFiberSkipIsIdempotentForActiveFibers(t *testing.T) {
|
|||||||
- [ ] **Step 2: 运行测试确认失败**
|
- [ ] **Step 2: 运行测试确认失败**
|
||||||
|
|
||||||
Run: `cd backend && go test ./core/ -run 'TestFiberSkip' -v`
|
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: 写实现**
|
- [ ] **Step 3: 写实现**
|
||||||
|
|
||||||
`backend/core/fiber.go` 中 `FiberDisposed` 常量之后追加状态:
|
`backend/core/fiber.go` 的 `FiberState` 常量块中,在 `FiberDisposed` 之后追加一个状态:
|
||||||
|
|
||||||
```go
|
|
||||||
// FiberSkipped indicates the plugin never activated because its configuration gate
|
|
||||||
// evaluated to false, so an alternative provider took over.
|
|
||||||
FiberSkipped
|
|
||||||
```
|
|
||||||
|
|
||||||
`FiberState` 常量块内的字符串值需与注释一致,把该常量的取值写为 `"SKIPPED"`:
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// FiberSkipped indicates the plugin never activated because its configuration gate
|
// FiberSkipped indicates the plugin never activated because its configuration gate
|
||||||
@@ -1509,22 +1494,6 @@ Expected: 编译失败,报 `undefined: core.FiberSkipped`、`f.Skip undefined`
|
|||||||
FiberSkipped FiberState = "SKIPPED"
|
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`:
|
`Load` 之后追加 `Skip`:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -1541,19 +1510,14 @@ func (f *Fiber) Skip() error {
|
|||||||
|
|
||||||
return f.ctx.Dispose()
|
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` 开头补一条守卫,防止已跳过插件被重新加载:
|
> **不要改 `DependenciesSatisfied`**:依赖能否满足完全由 IoC 容器解析决定。被跳过的插件从未执行 `Apply`,也就没有 `core.Provide`,其消费者自然解析不到服务并在 `Reconcile` 里报"waiting for"。用 Fiber 状态做短路是错误语义。
|
||||||
|
|
||||||
```go
|
|
||||||
f.mu.Lock()
|
|
||||||
if f.state != FiberPending {
|
|
||||||
f.mu.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
(该守卫已存在,无需改动;`Skip` 依赖同一不变式。)
|
|
||||||
|
|
||||||
- [ ] **Step 4: 运行测试确认通过**
|
- [ ] **Step 4: 运行测试确认通过**
|
||||||
|
|
||||||
@@ -1622,7 +1586,7 @@ func TestAppPrepareResolvesAndGatesPlugins(t *testing.T) {
|
|||||||
assert.False(t, redisAlt.applied)
|
assert.False(t, redisAlt.applied)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppStartRunsPrepareWhenCallerSkipsIt(t *testing.T) {
|
func TestAppApplyPluginsGatesImplicitly(t *testing.T) {
|
||||||
redisLike := &gatedPlugin{name: "cache", enabled: true}
|
redisLike := &gatedPlugin{name: "cache", enabled: true}
|
||||||
app := core.NewApp(core.WithConfigSource(newGateSource(false)))
|
app := core.NewApp(core.WithConfigSource(newGateSource(false)))
|
||||||
app.Use(redisLike)
|
app.Use(redisLike)
|
||||||
@@ -1681,9 +1645,7 @@ func WithConfigSource(src ConfigSource) AppOption {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.configSource = src
|
a.configSource = src
|
||||||
if setter, ok := a.ctx.Config().(interface{ SetSource(ConfigSource) }); ok {
|
a.ctx.Config().SetSource(src)
|
||||||
setter.SetSource(src)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user