Files
HomeAgent/internal/kernelboot/runtimedep_test.go
JianFeeeee e70c2b55b4 feat(kernelboot): ID:14 Run 增加执行期依赖校验,顺序被改坏时报可读错而非 SIGSEGV
Build() 里已有构建期校验 diagnoseUnresolvable(查声明:缺依赖/环/重名),
本单**不重复**它。缺的是执行期那一层:声明全对、图也自洽时,plan 的层顺序
仍可能被改坏(手改 Layers / 图与代码漂移 / 有人把某层提前),此时 Build 绿,
模块 Init 拿到 nil 依赖,崩在业务代码里。

实测复现(本单判据的复现形状):把 kernelmod 真实登记表算出的第 1 层
(lua/memory/onnx-parser/…)与第 2 层(llm/text-memory/knowledge-store)交换,
llm.buildProviderManager 收到 0x0 的 LuaVM:
  panic: runtime error: invalid memory address or nil pointer dereference
  [signal SIGSEGV ... addr=0x10]
  internal/lua.(*VM).ConfigureConcurrency(0x0, ...)  vm.go:213
  internal/kernelmod/llm.buildProviderManager(..., 0x0, ...)  mod.go:70

改动:Run 逐层维护 produced(**只有真跑完的层**才贡献设施,刻意不从 plan 预填),
每层 Init **之前**核对本层各模块 Requires ⊆ produced;缺则返回
*DependencyNotReadyError 并**本层一个 Init 都不调**(宁可什么都不装,也不装一半再崩)。
错误文本指名「第 N 层 / 模块 X 缺设施 [...]」并列已实际产出的设施。

判据:
  internal/kernelmod/all/runtime_dep_test.go  —— 用真实登记表算 plan 后做变异
    A 正序前缀不误报
    B 交换两层 ⇒ 判据红(指名 llm 缺 lua / text-memory 缺 memory,onnx /
      knowledge-store 缺 knowledge-migrated),且 ks.LuaVM 仍为 nil(没跑到 llm)
    C 删掉提供者整层 ⇒ 第 0 层就拒绝,IO/EventBus 仍 nil(一个模块都没跑)
  internal/kernelboot/runtimedep_test.go      —— 纯逻辑四形状 + 防 M2 自检

变异证明(4 个,逐个人工注入后跑判据,全部咬住):
  M0 撤掉守卫                → 复现原 SIGSEGV(ConfigureConcurrency(0x0))
  M1 守卫恒放行               → 5 条判据红
  M2 produced 从 plan 预填    → 退化成 Build 重复实现,顺序类判据红
  M3 报错丢模块名             → 指名类判据红

修前红/修后绿:
  修前(M0):panic SIGSEGV @ lua.(*VM).ConfigureConcurrency(0x0)
  修后:DependencyNotReadyError,文本含「第 1 层」「llm」「lua」「缺设施」

go build ./... = 0 error,go vet ./... = 0 error
go test ./internal/kernelboot/... ./internal/kernelmod/... = ok(含 -count=1)
2026-10-24 12:50:57 +08:00

169 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package kernelboot
// 执行期依赖校验(verifyLayerDependencies)的纯逻辑判据(2026-10-24,ID:14)。
//
// ## 为什么与 Build 的判据分开
//
// Build 的判据(TestBuildRejectsMissingDependency / …Cycle …)验的是**声明**:
// 一个模块 Requires 了没人 Provides 的设施,或成环 ⇒ Build 就得拒绝。
//
// 本文件的判据假设**声明完全合法**(Build 能算出图),只把算好的 Plan 的
// 层顺序改坏 —— 这才是 Build 绿而 Run 崩的那类故障(实测 SIGSEGV 见
// internal/kernelmod/all/runtime_dep_test.go)。
//
// ## 覆盖的四类形状
//
// ① 正序(正确开销) ⇒ 不误报
// ② 提供者排到消费者之后 ⇒ 指名模块名 + 缺的设施
// ③ 提供者整层缺失 ⇒ 第 0 层就拒绝,一个 Init 都不跑
// ④ 层内多模块各缺不同依赖 ⇒ 逐个模块报(不是只报第一个)
//
// 另有一条"M2 型变异"的自检:如果 produced 从 plan 预填(拿声明当事实),
// ② ③ 会变成**永不触发**的假绿 —— 判据必须能把它抓出来。
import (
"errors"
"strings"
"testing"
)
// chainPlan 造一个三层的合法计划:c → b → a(c 无依赖,a 最上层)。
func chainPlan(t *testing.T) *Plan {
t.Helper()
mods := []Module{
&fakeModule{name: "a", provides: []string{"fa"}, requires: []string{"fb"}},
&fakeModule{name: "b", provides: []string{"fb"}, requires: []string{"fc"}},
&fakeModule{name: "c", provides: []string{"fc"}},
}
plan, err := Build(mods)
if err != nil {
t.Fatalf("前置:声明必须合法(本文件只测顺序被改坏): %v", err)
}
return plan
}
// ★★★ ① 正序不误报。
func TestRunGuardAcceptsCorrectOrder(t *testing.T) {
if _, err := Run(chainPlan(t), RunOptions{}); err != nil {
t.Fatalf("★ 合法顺序被判成缺依赖(误报): %v", err)
}
}
// ★★★ ② 提供者排到消费者之后 ⇒ 指名道姓报错,且 Init 一个都没跑。
func TestRunGuardRejectsSwappedLayers(t *testing.T) {
plan := chainPlan(t)
// 把 b(提供 fb)与其消费者 a 交换:a 先跑,它要的 fb 还没产出。
swapped := &Plan{Layers: [][]Module{
plan.Layers[0], // c
plan.Layers[2], // a ← 原本在最后
plan.Layers[1], // b
}}
var inited []string
for _, l := range swapped.Layers {
for _, m := range l {
mm := m.(*fakeModule)
inner := mm.onInit
mm.onInit = func() {
inited = append(inited, mm.name)
if inner != nil {
inner()
}
}
}
}
_, err := Run(swapped, RunOptions{})
if err == nil {
t.Fatal("★ 顺序被改坏却装配成功 —— 校验没生效(模块会拿到 nil 依赖)")
}
var dnr *DependencyNotReadyError
if !errors.As(err, &dnr) {
t.Fatalf("★ 期望 *DependencyNotReadyError,实际 %T: %v", err, err)
}
if got, want := strings.Join(dnr.Missing["a"], ","), "fb"; got != want {
t.Errorf("★ 缺依赖报错不准:期望 a 缺 fb,实际 %v", dnr.Missing)
}
if len(inited) != 1 || inited[0] != "c" {
t.Errorf("★ 违规层不该有任何模块被 Init(只应跑过 c 层),实际跑了 %v", inited)
}
for _, sub := range []string{"第 1 层", "模块 a", "fb"} {
if !strings.Contains(err.Error(), sub) {
t.Errorf("★ 报错文本缺少 %q(原文: %s)", sub, err.Error())
}
}
}
// ★★★ ③ 提供者整层缺失 ⇒ 第 0 层就拒绝,produced 为空。
func TestRunGuardRejectsMissingProviderLayer(t *testing.T) {
plan := chainPlan(t)
cut := &Plan{Layers: [][]Module{plan.Layers[1], plan.Layers[2]}}
_, err := Run(cut, RunOptions{})
if err == nil {
t.Fatal("★ 提供者整层缺失却装配成功")
}
var dnr *DependencyNotReadyError
if !errors.As(err, &dnr) {
t.Fatalf("★ 期望 *DependencyNotReadyError,实际 %T: %v", err, err)
}
if dnr.Layer != 0 {
t.Errorf("★ 应在第 0 层拒绝,实际第 %d 层", dnr.Layer)
}
if len(dnr.Produced) != 0 {
t.Errorf("★ 第 0 层之前不该有产出,实际 %v", dnr.Produced)
}
}
// ★★★ ④ 同层多模块各缺不同依赖 ⇒ 逐个报(不是只报第一个,否则要修 N 轮)。
func TestRunGuardReportsEveryMissingModuleInLayer(t *testing.T) {
// 把 a 与 b 放进同一层,且都不提供任何东西 ⇒ 两者都缺依赖。
merged := &Plan{Layers: [][]Module{
{&fakeModule{name: "a", requires: []string{"fb"}},
&fakeModule{name: "b", requires: []string{"fc"}}},
}}
_, err := Run(merged, RunOptions{})
if err == nil {
t.Fatal("★ 缺依赖却装配成功")
}
var dnr *DependencyNotReadyError
if !errors.As(err, &dnr) {
t.Fatalf("★ 期望 *DependencyNotReadyError,实际 %T: %v", err, err)
}
if len(dnr.Missing) != 2 {
t.Fatalf("★ 只报了 %d 个模块缺依赖(应 2 个,否则要修 N 轮才看得全): %v",
len(dnr.Missing), dnr.Missing)
}
if strings.Join(dnr.Missing["a"], ",") != "fb" || strings.Join(dnr.Missing["b"], ",") != "fc" {
t.Errorf("★ 报错内容不准: %v", dnr.Missing)
}
}
// ★★★ 自检(防 M2 型变异):把 produced 预填成"全部声明" ⇒ 上面 ②③ 必须红。
//
// 这条不是测被测代码,而是测**判据具不具备判别力**:
// 若有人把执行期校验改成"从 plan 预填 produced"(等于相信声明),
// 它就退化成 Build 的重复实现,抓不到顺序被改坏 —— 那时本文件的 ②③
// 会变绿(假绿)。所以这里直接把这种实现形态跑一遍,要求它**确实**放行违规,
// 从而证明 ②③ 的红不是"任何实现都会红"。
func TestGuardPrefilledFromPlanIsDetectablyWeaker(t *testing.T) {
plan := chainPlan(t)
prefilled := map[string]bool{}
for _, l := range plan.Layers {
for _, m := range l {
for _, p := range m.Provides() {
prefilled[p] = true
}
}
}
// 违规层:a 在第 0 层,但它要的 fb 由第 2 层的 b 提供。
layer := []Module{plan.Layers[2][0]}
if err := verifyLayerDependencies(0, layer, prefilled); err != nil {
t.Fatalf("★ 预填 produced(拿声明当事实)竟报错 —— 说明本判据的选择无法区分两种实现,需重设: %v", err)
}
// 同一层、同一顺序,用**空** produced(真事实)必须报错 ⇒ 判据有判别力。
if err := verifyLayerDependencies(0, layer, map[string]bool{}); err == nil {
t.Fatal("★ 空 produced 也不报错 —— verifyLayerDependencies 没有判别力")
}
}