Files
HomeAgent/internal/kstate/mount.go
JianFeeeee 53cdc7f2a6 refactor(test): 肃清全部白盒测试 —— 源码目录零 _test.go,判据只走模块导出接口
## 用户裁定(本次唯一方向)

1. 「测试不应该面向过程而是面向模块的输入和输出」
2. 「每个模块下应该使用 test 目录搜集所有测试文件,而不是和源码混在一起」
3. 「现在 test 都超过源码数量了,这很明显不符合模块接口黑盒化的要求」
4. 「ast判包依赖?这不是工具链该做的吗?为什么在测试里」
5. 「抽行为判据?只需要面向模块输入接受模块输出就行」
6. 「不管ci,直接把所有白盒测试全清理了再回去一鼓作气改ci」

## 实测起点

    537 个测试文件 / 377 个源码文件 / 2853 条 Test
    523 / 537(97%)直接调用私有函数
    514 个是 in-package 白盒(package xxx,与源码同目录)
     88 个读 .go/.md 源码做断言
     94 个断言私有字段(f.Step / r.buf / a.sched …)
     38 个直调私有函数(stepLLM / runStage / classifyProviderError …)
    internal/agent/core: 140 test / 52 src

## 做了什么

### ① 删除 514 个 in-package 白盒测试

不做「抽行为判据」,也不做逐文件搬运。理由:Go 按目录分包,
`pkg/test/` 里写 `package pkg` 也看不到 unexported 符号 —— 所以
「挪进 test/」和「改成黑盒」是同一件事的两面。把 514 个白盒文件
搬走只会得到一堆编译不过的文件,或者为了迁就它们把内部全导出。

用户明确否定「抽」这个动作后,剩下 9 个 test/ 里的判据也一并
重新审视:凡是靠读源码/前端资源文本做断言的(dashboardJSSource /
FrontendAsset / GraphDataForVisual / SSEEventRing …)全部删除,
只留走模块**本来就有**的公开 API 的输入→输出判据:

    internal/agent/core/child/test/     Registry 的 Add/Get/Stop/Remove 输出
    internal/agent/core/test/           StageHost 的 Register/Run 输出
    internal/plugins/files/test/        工具 handler 的 {path} → {content/error}
    internal/plugins/webui/test/        HTTP 请求 → 响应(NewHandler/RegisterRoutes)
    internal/tfidf/test/                文本 → 向量/检索结果

现存 9 个测试文件全部在 `<module>/test/`,源码目录零 `_test.go`。

### ② 回退「为测试而导出」的接口

这些是前几轮为了把白盒判据挪出源码目录而导出的,属于白盒残留,
现在没有调用方了,全部收回私有:

    webui.FrontendAsset / DashboardPage        删除
    webui.GraphDataForVisual → graphDataForVisual
    webui.SSEEventRing/Record → sseEventRing/sseEventRecord(含字段)
    webui.NewSSEEventRing                       删除
    kstate.MountForTest                         删除
    security.ClassifyPrincipalForTest           删除

生产代码对它们的内部调用同步改回私有名,不改任何行为。

### ③ CI 一鼓作气改到位(用户指定顺序)

**删**:`整机启动图判据` 单列步骤(它跑的 bootgraph_test.go 已删),
以及 `check-ci-gates.py` 里盯着那个步骤的断言 —— 继续盯着一个
不存在的步骤就是永远红的空转。

**加两道构建期静态门**(原先这些约束错放在 go test 里):

- `测试归拢(源码目录不得有 _test.go)`
  `find internal cmd pkg providers deploy -name '*_test.go' -not -path '*/test/*'`
  非空即红。物理上挡住白盒回潮。
- `包依赖门(kstate 为叶子 / 底层包不反向依赖装配器)`
  用 `go list -deps` 判 8 组禁止依赖 + `go build core` 确认环不存在。
  这是**工具链**的职责,不该由 `go test` 兼职。

`check-ci-gates.py` 同步改为盯这两道门;ci.yml 里 13 处既有
>80 字符行一并折行(门禁标了 STOP,不留尾巴)。

### ④ 顺带(上一批未提交的)

`scripts/make-package.sh` 产物名 `HomeAgent-linux-*` →
`HologramAgent-linux-*`;清理 gitignored 构建产物
`build/ dist/ .go/ .npm-cache/` 共 20G(工作区 21G → 1.7G)。
运行契约(~/.homeagent/cli.sock、线上域名、兼容别名)未动。

## 验证

    go build ./...                     PASS
    go vet ./...                       PASS
    gofmt -l                           干净
    python3 .github/scripts/check-ci-gates.py   PASS
    go test ./... -count=1             5 个测试包全过 / 100 个包无测试 / 0 FAIL
    源码目录 _test.go 数              0
    测试文件数                         9(全部在 <module>/test/)

## 后续

按模块导出接口清单重写判据(不是从旧白盒里抽):
internal/agent/api、internal/memory、internal/security、internal/plugin、
internal/knowledge、cmd/* 等目前 0 判据,需要逐个列接口 → 写输入→输出判据
→ 变异验证。这是新一轮工作,本提交不含。
2026-11-09 11:13:56 +08:00

78 lines
3.0 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 kstate
// 内核统一挂载点:本进程唯一的公共数据持有者 + 它的存取入口。
//
// ## 用户设计(本包存在的理由)
//
// > 内核通过一个结构体收集全部包级共享变量,其他模块通过这个结构体访问数据,
// > 各个模块的公共数据也统一注册到内核这个公共结构体中。
//
// > 结构体成员初始置 nil 加读取前检查不就好了
//
// > 所有共享数据统一挂载一个 kernel 提供的挂载点上暴露。
//
// ## 为什么本包必须与装配引擎分开(2026-11-04 拆分)
//
// 拆分前 `KernelState` 与装配引擎同居 `internal/kernelboot`。数据结构要认识
// 各业务类型(`*security.Runtime`、`*plugin.Registry`、`*agentCore.StageHost`…),
// 于是 `kernelboot` 被迫依赖 **19 个业务包**:
//
// kernelboot ──▶ agent/core
// ──▶ plugin
// ──▶ security
// ──▶ memory / knowledge / text / coldcontext / …
//
// 而装配引擎本身(kernelboot.go / scheduler.go / funcmodule.go)
// **一行业务代码都不需要** —— 依赖全在数据结构这一个文件里。
//
// 后果是所有被依赖的包都无法反向引用 kernelboot,于是它们各自造通道:
//
// core → Agent 字段(靠每种 agent 构造点记得传)
// plugin → 12 个 SetXxx
// sdk → SDKConfig 传参
// security → 包级单例 Global() / RuntimeGlobal() / ConfigCodec()
//
// 四条通道都指向同一个根因。把数据结构搬出来(本包),环就断了:
//
// kstate ← 纯数据,谁都能 import(它就是挂载点本身)
// kernelboot ← 装配引擎,零业务包
//
// ## 并发约定
//
// 状态结构体上的字段**不加锁**(用户设计):同层模块并行 Init 时各自只写
// **自己那一个字段**,字段互不相同 ⇒ 无数据竞争。读方直接读字段 + 判 nil。
//
// 但**挂载点本身**要加锁:它是全部调用方共同读的单一变量,而装配期写入
// 与运行期读取必然跨 goroutine(`Mount` 在启动时一次,`Get` 在热路径)。
import "sync"
var (
mountMu sync.RWMutex
mount *State
)
// Mount 装入本进程的挂载点。**装配期调用一次**(由 kernelboot 的 Run 完成)。
//
// 重复调用会覆盖:便于测试与热重启路径(后装的生效)。
func Mount(s *State) {
mountMu.Lock()
mount = s
mountMu.Unlock()
}
// Get 取挂载点。未装配时返回 **nil**(不 panic)。
//
// ★ 为什么返回 nil 而不是空结构体:调用点散布在内核热路径上,未装配
// (单元测试、独立工具如 memgc)时必须**零成本跳过**。所有调用方写成:
//
// if ks := kstate.Get(); ks != nil && ks.Xxx != nil { … }
//
// 返回空结构体看似更"安全",实则让"未装配"与"装配了但字段为 nil"两种
// 完全不同的情况混为一谈 —— 前者是环境问题,后者是装配缺陷。
func Get() *State {
mountMu.RLock()
defer mountMu.RUnlock()
return mount
}