mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
用户澄清推翻了早期设计的三处前提,本提交按新模型重做调度核心(行为有意变化): 1) 类别由注入 API 决定,与通道名无关 - InjectInterrupt* -> TaskInterrupt(带级别,可被严格更高级中断打断) - InjectText*/InjectInputSync*/内核自循环 -> TaskQueued(无级别,可被任何中断打断) - 删除按通道名推断的 taskLevel():qq 走 InjectInterruptTextOpts,本就是中断 2) 级别只属于中断 - 插件在 InjectOptions.Priority 声明 L1-L3(空/非法降级 L1,声明 L4 夹到 L3) - L4 内核独占:新增 raiseKernelInterrupt(panic / selfip);requestKernelPreempt 不夹取 - panic 现在产生一条带 kernel 标记的 L4 中断;L4 自身 panic 不再产生新 L4(防自我放大) 3) 选择结构:四容器固定次序,删除统一比较器 - immediate(抢占者立即运行)-> 中断队列 L4..L1 -> 栈顶(与队头比级别) -> 排队 FIFO - 删除 pickTaskIndex/taskBefore 与“同级 pending 优先”补丁(根因是抢占者进了队列) - 中断栈上界改为结构推论 = 4(= 中断级数);删除“超限转 pendingInterrupts”降级 公开 SDK(feature 分支有意新增,纯追加):InjectOptions.Priority + PriorityL1/2/3; 内核 io / proc 桥 / 插件模板同步透传。 设计稿 §2/§3/§4.1/§6.3/§9/§11/§12/§13/§15 按新模型重写。 验收:go build/vet 干净;go test ./... 37 包 ok 0 FAIL;-race 全绿; e2e(抢占-挂起-恢复)+ 压力(200 排队 + 50 中断,L1/L2/L3 轮转)通过。
207 lines
7.3 KiB
Go
207 lines
7.3 KiB
Go
package core
|
||
|
||
// L4 的内核独占性 + 两类别抢占规则。
|
||
//
|
||
// 模型(用户明确):
|
||
// - 类别由**用哪个注入 API** 决定,与通道名无关;
|
||
// - L1..L3 由插件在 InjectOptions.Priority 声明;
|
||
// - L4 只有内核持有(panic / 内核事件 selfip);
|
||
// - 排队输入无级别,可被**任何**中断打断。
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||
)
|
||
|
||
// 插件声明 L4 必须被夹到 L3:L4 是内核的调度内部属性,不接受外部越权。
|
||
func TestKernel_PluginCannotClaimL4(t *testing.T) {
|
||
if got := clampPluginLevel(LevelCritical); got != LevelInteractive {
|
||
t.Fatalf("插件声明 L4 应被夹到 L3,实际 %v", got)
|
||
}
|
||
cases := []struct {
|
||
declared string
|
||
want Level
|
||
}{
|
||
{"L1", LevelBackground},
|
||
{"L2", LevelMessage},
|
||
{"L3", LevelInteractive},
|
||
{"l2", LevelMessage},
|
||
{"L4", LevelInteractive}, // 越权 → 夹到 L3
|
||
{"L7", DefaultLevel}, // 未知 → 默认级
|
||
{"", DefaultLevel}, // 未声明 → 默认级
|
||
{"紧急", DefaultLevel}, // 拼写错误 → 默认级(不得被静默当成别的级别)
|
||
}
|
||
for _, c := range cases {
|
||
evt := &agentIO.InputEvent{Payload: map[string]interface{}{}}
|
||
if c.declared != "" {
|
||
evt.Payload["priority"] = c.declared
|
||
}
|
||
if got := interruptLevel(evt); got != c.want {
|
||
t.Fatalf("声明 %q → 级别 %v,期望 %v", c.declared, got, c.want)
|
||
}
|
||
}
|
||
if got := interruptLevel(nil); got != DefaultLevel {
|
||
t.Fatalf("无事件应为默认级,实际 %v", got)
|
||
}
|
||
}
|
||
|
||
// 排队任务可被**任何**中断打断——包括最低的 L1。
|
||
func TestKernel_QueuedTaskIsPreemptedByAnyInterrupt(t *testing.T) {
|
||
s := newScheduler(8)
|
||
q := newInputTask(&agentIO.InputEvent{Source: "plugin", OutputChannel: "plugin"})
|
||
s.enqueue(q)
|
||
if task, _, kind := s.nextRef(); task != q || kind != nextReady {
|
||
t.Fatalf("应取到排队任务,kind=%v", kind)
|
||
}
|
||
|
||
evt, _ := textEvent("qq", "最低级中断")
|
||
if !s.registerInterrupt(newInterruptTask(evt, LevelBackground)) {
|
||
t.Fatal("L1 中断也必须能打断排队任务(排队任务无级别)")
|
||
}
|
||
if s.immediate == nil || s.immediate.Level != LevelBackground {
|
||
t.Fatalf("抢占者应进 immediate 槽,实际 %+v", s.immediate)
|
||
}
|
||
}
|
||
|
||
// 排队输入从不抢占——它没有级别,也就没有“比谁高”。
|
||
func TestKernel_QueuedInputNeverPreempts(t *testing.T) {
|
||
s := newScheduler(8)
|
||
if !s.enqueue(newInputTask(&agentIO.InputEvent{Source: "a", OutputChannel: "a"})) {
|
||
t.Fatal("入队失败")
|
||
}
|
||
s.nextRef() // running = 第一个排队任务
|
||
if s.registerInterrupt(newInputTask(&agentIO.InputEvent{Source: "b", OutputChannel: "b"})) {
|
||
t.Fatal("排队输入不得抢占任何任务")
|
||
}
|
||
}
|
||
|
||
// 内核 L4 入口不受夹取影响,且能抢占中断(L3)。
|
||
func TestKernel_RequestKernelPreemptUsesL4(t *testing.T) {
|
||
s := newScheduler(8)
|
||
evt, _ := textEvent("cli", "L3 运行中")
|
||
s.registerInterrupt(newInterruptTask(evt, LevelInteractive))
|
||
s.nextRef() // running = L3 中断
|
||
|
||
kevt, _ := textEvent("kernel", "panic 中断")
|
||
if !s.requestKernelPreempt(kevt) {
|
||
t.Fatal("内核 L4 应能抢占 L3 中断")
|
||
}
|
||
if s.immediate == nil || s.immediate.Level != LevelCritical {
|
||
t.Fatalf("内核中断必须是 L4,实际 %+v", s.immediate)
|
||
}
|
||
}
|
||
|
||
// 任务 panic → 内核 L4 中断(panic 是 L4 的来源之一)。
|
||
func TestKernel_PanicRaisesL4Interrupt(t *testing.T) {
|
||
sp := &scriptProvider{script: []*agentAPI.CompletionResponse{{Content: "已收到内核事件"}}}
|
||
a := newPreemptAgent(t, sp)
|
||
|
||
// Event 为 nil:handleInput 解引用即 panic。
|
||
bad := newInputTask(nil)
|
||
if !a.sched.enqueue(bad) {
|
||
t.Fatal("入队失败")
|
||
}
|
||
task, _, _ := a.sched.nextRef()
|
||
a.executeTask(task) // panic 被隔离
|
||
|
||
snap := a.DumpScheduler()
|
||
if snap.Immediate == nil {
|
||
t.Fatal("panic 必须产生一条内核 L4 中断")
|
||
}
|
||
if snap.Immediate.Level != LevelCritical {
|
||
t.Fatalf("panic 中断级别=%v,期望 L4", snap.Immediate.Level)
|
||
}
|
||
text, _ := snap.Immediate.Event.Payload["content"].(string)
|
||
if !strings.Contains(text, "panic") {
|
||
t.Fatalf("panic 中断应说明发生了什么,实际 %q", text)
|
||
}
|
||
if snap.Immediate.Event.Payload["kernel"] != true {
|
||
t.Fatal("内核中断必须带 kernel 标记,便于与插件中断区分")
|
||
}
|
||
}
|
||
|
||
// 递归保护是结构性的:L4 内核中断自己 panic 时,不再产生新的 L4。
|
||
func TestKernel_PanicInsideL4DoesNotRecurse(t *testing.T) {
|
||
sp := &scriptProvider{}
|
||
a := newPreemptAgent(t, sp)
|
||
|
||
evt, _ := textEvent("kernel", "内核事件")
|
||
l4 := newKernelInterruptTask(evt)
|
||
a.sched.immediate = l4
|
||
task, _, _ := a.sched.nextRef()
|
||
if task != l4 {
|
||
t.Fatal("应取到 L4 内核中断")
|
||
}
|
||
a.executeTask(&Task{ID: task.ID, Class: TaskInterrupt, Level: LevelCritical, Kind: TaskKindInput, Event: nil})
|
||
|
||
snap := a.DumpScheduler()
|
||
if snap.Immediate != nil || len(snap.PendingInterrupts) != 0 {
|
||
t.Fatalf("L4 自身 panic 不得再产生中断(否则自我放大),实际 immediate=%+v pending=%d",
|
||
snap.Immediate, len(snap.PendingInterrupts))
|
||
}
|
||
}
|
||
|
||
// 中断栈的 4 帧上界是**结构推论**:排队(L0) ← I(L1) ← I(L2) ← I(L3) ← I(L4 运行中)。
|
||
func TestKernel_StackBoundIsFullChain(t *testing.T) {
|
||
s := newScheduler(16)
|
||
frame := func() *TaskFrame { return &TaskFrame{} }
|
||
chain := []struct {
|
||
class TaskClass
|
||
lv Level
|
||
}{
|
||
{TaskQueued, 0},
|
||
{TaskInterrupt, LevelBackground},
|
||
{TaskInterrupt, LevelMessage},
|
||
{TaskInterrupt, LevelInteractive},
|
||
}
|
||
for i := 0; i < len(chain)-1; i++ {
|
||
s.suspend(&Task{ID: uint64(i + 1), Class: chain[i].class, Level: chain[i].lv}, frame())
|
||
}
|
||
if !s.canSuspend() {
|
||
t.Fatal("3 帧挂起时仍应容得下 L3(第 4 级)继续下潜")
|
||
}
|
||
s.suspend(&Task{ID: 4, Class: TaskInterrupt, Level: LevelInteractive}, frame())
|
||
if s.canSuspend() {
|
||
t.Fatal("4 帧挂起 = 全链挂起(L4 运行中),不应再有下潜余量")
|
||
}
|
||
}
|
||
|
||
// 端到端:插件声明 Priority → io.applyInjectOpts → payload → interruptLevel → 任务级别。
|
||
// 这条链路断在任何一环,插件声明的级别都会静默失效(降级到 L1)。
|
||
func TestKernel_PriorityFlowsThroughIOLayer(t *testing.T) {
|
||
ioM := agentIO.NewIOManager()
|
||
ioM.InjectInterruptTextOpts("qq", "cli", "通知", agentIO.InjectOptions{Priority: "L3"})
|
||
|
||
select {
|
||
case evt := <-ioM.InputInterruptChan():
|
||
if got := interruptLevel(evt); got != LevelInteractive {
|
||
t.Fatalf("经 io 层后的级别=%v,期望 L3(payload=%v)", got, evt.Payload)
|
||
}
|
||
task := newInterruptTask(evt, interruptLevel(evt))
|
||
if task.Class != TaskInterrupt || task.Level != LevelInteractive {
|
||
t.Fatalf("中断任务类别/级别=%v/%v,期望 interrupt/L3", task.Class, task.Level)
|
||
}
|
||
case <-time.After(2 * time.Second):
|
||
t.Fatal("中断未到达 interruptCh")
|
||
}
|
||
|
||
// 排队路径带 priority 也必须无效:排队输入没有级别。
|
||
ioM.InjectTextOpts("qq", "cli", "普通输入", agentIO.InjectOptions{Priority: "L3"})
|
||
select {
|
||
case evt := <-ioM.InputChan():
|
||
task := newInputTask(evt)
|
||
if task.Class != TaskQueued || task.Level != 0 {
|
||
t.Fatalf("排队任务类别/级别=%v/%v,期望 queued/无级别", task.Class, task.Level)
|
||
}
|
||
if effectiveLevel(task) != 0 {
|
||
t.Fatalf("排队任务有效级=%v,期望 0", effectiveLevel(task))
|
||
}
|
||
case <-time.After(2 * time.Second):
|
||
t.Fatal("排队输入未到达 inputCh")
|
||
}
|
||
}
|