|
|
b73ff3786a
|
test(security): 补 Gate 明文拦截/回填与 Store 跨重启持久化契约
Codec 原有 7 条保留。新增模块职责层判据:
TestGateReplacesSecretOutboundAndBackfillsForExecution
出向文本里出现真实凭据 → 被替换成 @{alias},明文不再出现
执行前 FillForExecution 把别名换回真实凭据
TestGateScrubsSecretInbound
入向泄漏的凭据被清洗,订阅侧看不到明文
TestGateDisabledPassesTextThrough
关闭状态原样透传、拦截计数 0
TestStoreAliasRoundTripSurvivesReopen
Submit 拿别名 → 重新打开同一存储 → Fill 回填出原文
变异验证:
去掉回填分支 → 别名不再换回真实凭据(先前的失败)
出向不产出别名 → outbound secret was not intercepted
过程中修正了两处我自己写错的期望(回填目标必须先取别名、
引用格式是 @{alias}),不是放宽实现。
|
2026-11-14 10:38:37 +08:00 |
|
|
|
5700531169
|
test(events): 补授权拒绝与脱敏投递的模块契约
新增 TestPublishAsReturnsDeniedErrorAndBlocksDelivery
策略拒绝 → 返回可诊断 DeniedError,订阅者零投递
新增 TestSubscribersReceiveScrubbedPayload
入向脱敏后订阅者看不到明文
新增 TestHandlerPanicDoesNotBreakOtherSubscribers
handler panic 不影响其他订阅者
变异:绕过 AllowPublish 判断 → denied publish 返回 nil 错误
跳过 ScrubInbound → 明文 SECRET 到达订阅者
|
2026-11-14 10:30:50 +08:00 |
|
|
|
29bfad63d6
|
test(agent/core): StageHost 增加失败语义与工具摘除契约
新增 TestRunStageCollectsHandlerErrors
handler 返回 error → 汇总进 ctx.Errors,其他 handler 的产物保留
新增 TestRunStagePanicDoesNotKillRun
handler panic 被隔离,其余 handler 照常执行
新增 TestUnregisterPluginToolsOnlyThatPlugin
工具摘除只影响目标插件,其他插件工具仍可执行
变异验证:
去掉 error 收集 → errors=[] 断言红
去掉 recover 隔离 → panic 直接炸掉测试进程(rc=1)
|
2026-11-14 10:28:45 +08:00 |
|
|
|
b215bfb3ad
|
test: io/lua/system/tracker 改为模块职责级黑盒契约
## internal/agent/io
原:只有 OutputCapability 位标志 + String
现:
InjectText → 输入事件(source/output_channel/request_id)
InjectInputTo → 显式输出通道被保留
InjectTextSync → 响应回程交到调用方
RegisterDevice → GetDevice → StartAll/StopAll 真实调用设备
重复注册被拒;StartAll 传播设备启动失败
能力标志语义保留(任一位命中)
变异:允许重复注册 / 吞掉启动错误 → 均断言红
## internal/lua
新增:
transform_request 真的执行 Lua body(原来只验证加载与未加载报错)
endpoint/headers 静态声明经公开 API 读回
变异:transform_request 直接返回入参 → 断言红
## internal/system
原:只断言 archived=true 与 baseline 目录非空
现:留档 → 覆盖 → RestoreFileFromBaseline 内容还原为原文
非受保护路径与新建文件不留档
内容未变时重复留档不产生新档案
变异:留档写空内容 / 去掉幂等判断 → 均断言红
## internal/tracker
原:无文件变更时断言无 changeset,rollback 起点为空
现:真实 upper 目录写入 → PostAction 记录 ChangeFileCreated
修改原文 → RollbackLatest 恢复原文内容
新建文件 → RollbackLatest 后消失
空历史定向回滚报错
变异:不记录变更 → 两条断言红
## 验证
go build ./... / go vet ./... PASS
gofmt -l 干净
CI 门禁自检 PASS
go test ./... -count=1 -race 51 测试包 / 100 包无测试 / 0 FAIL
源码目录 _test.go 0
|
2026-11-14 10:22:12 +08:00 |
|
|
|
a7e8f55085
|
test: 把 helper/getter 判据改写为模块职责级黑盒契约
四个模块从「导出符号调用」改为「模块职责 → 可观察结果」。
## internal/memory
TestCommitThenRecallAfterReopen
Commit → Close → 重开同一库 → Recall 核对主/谓/宾
(原来只检查 GraphData 返回 map 里有 nodes 键)
变异:Recall 恒空 → 断言红
## internal/memory/pipeline
TestAppendThenDistillWritesQueryableFact
Append → Start → 注入 splitter → 图库 Recall 命中
TestRawRecordsPersistAcrossRestart
Append → Stop → 新实例 Start → 回读原始记录
TestSplitterFailureDoesNotFabricateFacts
拆分失败必须回退,不得编出事实
TestPauseResumeAndStopAreObservable
变异:跳过蒸馏/跳过落盘 → 均断言红
## internal/nlp
真实 Parser → 三元组 + 句子溯源 + 分值
融合阈值改变取舍(高阈值空、低阈值还原)
SetDefaultParser 影响 NewExtractor(nil)
变异:丢弃 dep 抽取 / 取消阈值截断 → 均断言红
## internal/config
新增 TestSecretSurvivesRestartAndStaysEncrypted
敏感项 Set → 磁盘无明文 → Close → 重开 → GetSecret 还原明文
变异:GetSecret 不解密 / Set 不加密落盘 → 均断言红
删除了一条不成立的断言(普通 Get 本应返回明文),按真实契约收紧。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 51 测试包 / 100 包无测试 / 0 FAIL
4 个改动包 -race 全绿
源码目录 _test.go 0
|
2026-11-14 10:04:44 +08:00 |
|
|
|
41926e770d
|
fix(lua): 适配器永久落后 —— 修死代码 + 改为显式 opt-in 同步
## 缺陷:适配器会永久卡在旧版
writeBundledAdapters 用清单里的 prevHash 判断「盘上有没有被人动过」,
被动的那个就保留。但清单只存 hash ——「内置旧版本的 hash」与
「用户改过的 hash」在清单里长得一模一样。于是:
记了盘上那份当新基线 ⇒ 下次 onDisk == prevHash
⇒ 被误判成「没跟上新版本」⇒ 覆盖掉用户改动
而保留分支里的 cur[name] = onDisk 又会
## 被循环末尾无条件 `cur[name] = sum` 冲掉
⇒ 判「用户改过」后清单仍记内置 hash ⇒ 下次再判一遍「用户改过」
⇒ **永久**停在旧版。日志只有一句「保留用户版本」,
看起来像用户自己的选择,完全看不出是卡住。
本机实测 8 个适配器中招(openai/anthropic/github/groq/mistral/ollama/
kimicode/server),落后内置版 3000~6500 字节,缺的是用量归一化
(cache_read / reasoning_tokens)、流式 tool_calls 等真实修复。
症状最终表现为「模型不听话」「工具调不动」——指不到适配器身上。
## 为什么不能靠「自动覆盖」解决
「旧发行版残留」与「用户手工改过」在文件内容上无法区分(都只是
hash 不等于内置版)。擅自改用户盘上的文件,比落后一个版本更糟。
所以改成**显式 opt-in**:
core.lua.adapter_sync = off(默认) 不碰文件,但告警必须可操作
core.lua.adapter_sync = auto 差异即备份 .bak-<hash8> 后覆盖
零值 = off:忘记注入配置时宁可不动用户文件,也不能默默全量覆盖。
## 告警要可操作,不是「保留用户版本」一句话
教训(2026-10-26 + 2026-11-11 两次):这类静默落后最难查,因为症状
指向模型而不是适配器。现在会给出:数量、名单、影响、三条处理路径
(跟随内核 / 长期定制改 adapter_path / 删掉旧文件)。
长期定制的正道是 `core.llm.sources.<name>.adapter_path`(每源独立可配),
不需要改内置位置的同名文件 —— 这一点也写进告警里。
## 判据 7 条,均经变异验证
TestAdapterSyncDefaultsToOff 零值/未注入必须是 off
(变异:return "off" → "auto" ⇒ 红)
TestOffModeLeavesDiskUntouched off 不碰文件、不产生备份
(变异:case mode=="auto" → case mode!="" ⇒ 红)
TestAutoModeUpdatesWithBackup auto 更新且备份可回滚
TestAutoModeNoRedundantBackup 一致时不重复堆 .bak
TestAdapterStateStableAcrossRestarts 两种模式启停 3 次不漂移
TestManifestCoversAllBundledAdapters 清单不漏项
TestFirstInstallHappensInBothModes off 只「不动已存在文件」,≠不安装
go build / vet / gofmt / go test -race 全绿
## 生产现状(已按用户确认保留更新)
适配器 10/10 与内置一致,8 个 .bak-<hash8> 可回滚
服务 active,NRestarts=0
|
2026-11-11 21:39:03 +08:00 |
|
|
|
f7dad243b6
|
fix(deepseek): reasoning_content 三层贯通 + thinking 关闭层级修正
## 生产故障
all 2 providers failed, last error: api error 400:
"The reasoning_content in the thinking mode must be passed back to the API."
## 两个根因,第一个更根本
### ① thinking 关闭一直没生效(真正的触发器)
deepseek.lua 写的是 `req.extra_body.thinking = {type="disabled"}`。
但官方文档里那个 `extra_body={"thinking":...}` 是 **OpenAI SDK 层参数** ——
SDK 会把它摊平进 HTTP 请求体顶层。而适配器是**裸发** JSON 给 api.deepseek.com,
多包一层 `extra_body` 服务端根本不认,直接忽略。
⇒ 即使内核 thinking_enabled=false(发出 disable_thinking=true),
也从来没真正关掉过思考,每次都走 thinking 模式,每次都撞 reasoning_content 规则。
改为顶层 `req.thinking = {type="disabled"}`。
### ② reasoning 从未被保存和回传
DeepSeek 规则(官方 thinking_mode 指南):请求**带 tools** 时,
所有历史轮的 reasoning_content 都必须传回;内核每轮都带工具定义。
而内核三层全丢:
L1 ContextEvent 没有该字段;renderL1Messages 渲染 assistant 时不带
L2 coldcontext 只切 Input/Response 两块,推理内容不进块
L3 蒸馏原文只读 Input/Response/工具输出
修法(按用户裁定:L1/L2/L3 都要包含 reasoning):
L1 ContextEvent.ReasoningContent + renderL1Messages 回传
顺带:textForVector 纳入 reasoning —— 关键线索常只在思考里
("用户要的是 A 不是 B,所以先查 C"),只看 Response 会误裁
L2 RoleReasoning 独立块(不并进 Response:回灌时要放回不同字段,
合并会让思维链被当成正式回复发给模型)
L3 coldcontextOriginalText 按**时序** input → reasoning → response
## 一个易错点:不能用 f.Resp.ReasoningContent
f.Resp 只在「本批有工具调用」时赋值(stepLLM 里 len(ToolCalls)==0 就 return)。
纯文本收尾轮拿到的是**上一次工具轮**的旧响应 —— 用它写进 L1 会把上一批的
思维链冒名顶替成本轮的(比缺失更坏)。所以单独一个 TurnReasoning,
由 stepLLM 在分支**之前**无条件写入。
## 适配器兜底(治存量)
内核修了,但**存量会话**的 context.json 是修复前写的,那些事件没这个字段。
不兜底就是死结:历史缺字段 ⇒ 400 ⇒ 会话永久不可用(重启也救不回,
缺的字段在磁盘上的旧记录里)。
transform_request 里:带 tools 时给每条 assistant 补 reasoning_content="",
已存在则不覆盖(不抹掉真实思考内容)。
## 顺手修的既有缺陷
零时间戳时 docBlocksForEntry 用 time.Now() 算块 ID,而 ColdEvent.Timestamp
留零值 ⇒ 哈希永久失配 ⇒ 内容就在那里却一个字段都读不回,且不报错不记日志。
线上因为 L1 总带时间戳没暴露。docEventsFromEntries 里先归一化时间戳。
## 验证
生产实机(A2A 多轮工具调用,日志确认落到 deepseek 源直连):
LLM fallback: trying provider "deepseek"
response (12186ms, tools=[cmd_run plugin_start cmd_run])
reasoning_content 400 = 0 次
单元判据 16 条,全部变异验证(改回错误形态即红):
coldcontext/test/reasoning_contract_test.go 5 条(L2 往返/独立块/
无思考不造空块/特殊字符/InOut 兼容)
agent/api/test/reasoning_protocol_test.go 3 条(顶层字段/无思考不发/
带 tool_calls 仍发)
lua/test/deepseek_reasoning_test.go 8 条(补空串/不覆盖真实值/
无 tools 不补/补全部轮/不破其他字段/顶层 thinking/默认不加 thinking)
go build / vet / gofmt / go test -race 全绿
|
2026-11-11 21:38:54 +08:00 |
|
|
|
319fa91ce7
|
test(cfgmgr): add module-level configuration workflow contract (wip)
|
2026-11-10 09:06:44 +08:00 |
|
|
|
3d05dcac71
|
test: 按模块导出接口重写黑盒判据(批次 14:mcp / proc / cfgmgr)
三个模块新增 test/ 黑盒契约,测试仅通过导出 API 驱动。
## internal/plugins/mcp
TestListToolsThroughHTTP
TestCallToolSuccessAndEmptyContentHandling
TestCallToolSurfacesIsErrorAsFailure
TestCallToolSurfacesRPCError
TestCallToolSurfacesHTTPError
TestListToolsParseSSEStreamFrame
使用 httptest + NewSSETransport + NewServer,覆盖 JSON-RPC 工具列表、
成功调用、空内容失败、MCP isError、RPC 错误、HTTP 错误和 SSE data 帧。
## internal/plugin/proc
TestMajorVersionParsing
TestKnownCapabilitiesExcludesCoreAndIsStable
TestSharedRefSliceBoundary
TestPublicCapabilityConstantsStable
覆盖版本主段解析、能力清单排序/去重/core 排除、共享引用切片边界和能力常量。
## internal/plugins/cfgmgr
TestStartRegistersConfigToolsWithConcurrencyMarkers
TestStopIsSafe
通过公开 Start 注入 SDK 工具注册回调,观察七个配置工具及其
Serial/ParallelSafe 并发契约;未访问私有 handler。
## 变异验证
mcp 忽略 MCP isError → isError 未被判失败
proc MajorVersion 不截断 → 多组版本断言红
cfgmgr 写工具取消 Serial 标记 → 写工具未声明 Serial
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 51 个测试包 / 100 包无测试 / 0 FAIL
3 个新包 -race 全绿
源码目录 _test.go 0
|
2026-11-09 21:37:53 +08:00 |
|
|
|
e74d66492d
|
test: 按模块导出接口重写黑盒判据(批次 13:sdk / skillmgr / remotedevice)
三个模块新增 test/ 黑盒契约,测试仅使用模块导出 API。
## internal/sdk
EffectiveProxyAuthNormalizesLegacyAndEmpty
ProxyHostNormalizationAndValidation
NewConfigGetPutRoundTrip
BuiltinProxyRegistrySnapshotsAndClear
覆盖鉴权归一化、DNS host 标签校验、ConfigAPI 往返、内置反代声明表快照与清理。
## internal/plugins/skillmgr
SkillDescriptionsListsScannedSkillsInStableOrder
SkillDescriptionsEmptyBeforeStart
通过公开 New/Start/SkillDescriptions/Name 验证技能目录扫描、稳定排序与空目录行为。
## internal/plugins/remotedevice
SaveResultThenGetResult
SupportsToolForUnknownAndUnknownCapability
SaveInlineMediaRequiresDirectoryAndPersistsBytes
覆盖结果存取、未知设备能力查询、媒体目录配置、base64 落盘、摘要和元数据。
## 变异验证
sdk 注册版本不递增 → registration did not advance public version
skillmgr 不写入技能表 → scanned descriptions missing
remotedevice 丢弃 SaveResult → GetResult=false
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 48 个测试包 / 100 包无测试 / 0 FAIL
3 个新包 -race 全绿
源码目录 _test.go 0
|
2026-11-09 21:13:49 +08:00 |
|
|
|
cd4290807c
|
test: 按模块导出接口重写黑盒判据(批次 12:plugin / qwen3vl / chineseclip)
3 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
新增公开输入→输出判据:
## internal/plugin —— SKILL 内容与加载
TestValidateSKILLContent
合法内容通过;空内容/无描述/非法工具名分别被拒
TestLoadSKILLDirectoryExposesMetadataAndTools
临时 SKILL.md → Name/Version/Description/Tools/IOConfig
TestLoadSKILLMissingFileReturnsError
## providers/qwen3vl —— BPE tokenizer 公开契约
TestTokenizerSpecialTokenRoundTrip
临时 tokenizer.json → VocabSize/SpecialID/Encode/DecodeOne
TestTokenizerMissingFileReturnsError
## providers/chineseclip —— WordPiece tokenizer 公开契约
TestTokenizerEncodeUsesSpecialTokensAndPadding
临时 vocab.txt → 固定长度 ids 与 attention mask
TestTokenizerRejectsInvalidVocabulary
缺特殊 token / 非正 maxLength → 报错
所有测试都用 `package test` 导入模块,通过导出 API 操作;不访问私有符号,
不读取源码/文档作为业务断言。
## 变异验证
plugin ValidateSKILLContent 放过无描述内容 → no_description 断言红
qwen3vl Encode 丢弃特殊 token → Encode=[] 断言红
chineseclip Encode 不补 padding → 固定长度判据红
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 45 个测试包 / 100 包无测试 / 0 FAIL
3 个新包 -race 全绿
源码目录 _test.go 0
|
2026-11-09 16:35:21 +08:00 |
|
|
|
08cbd3e7bd
|
test: 按模块导出接口重写黑盒判据(批次 11:update / kernelboot / devicebridge)
3 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 15 条判据,只调导出接口,输入 → 可观察输出。
## internal/update —— 版本/升级纯函数
TestParseSemver 三段式(可带 v);非纯版本返回 nil
TestCmpSemver 大小/相等;nil 返回 0(不可比)
TestNewer 只认纯三段式;dev 后缀不误报
TestCompareVersions 升级/降级/完全相同/同版本不同 commit
TestDeltaAssetName 产物名内联版本/平台/来源 commit
TestShortCommit 长 commit 截 8 位
TestBrokenDirPath 输出落在 dataDir 下且含时间戳
## internal/kernelboot —— 依赖图编解码
TestGraphEncodeDecodeRoundTrip Encode → Decode 模块与层号
TestGraphDigestIsDeterministic 同声明稳定;换声明则变
TestDecodeGraphRejectsCorruption 位翻转/截断必拒
TestCheckAgainstBinary dev 放行 / 不配套拒 / 配套通过
TestFuncModuleExposesSpec Name/Provides/Requires
**发现一条假绿并修掉**:`TestDecodeGraphRejectsCorruption` 第一版
翻转 `data[10]`(magic/长度区),跳过 hash 校验后仍会因格式标识报错
⇒ 判据假绿。改成只翻转尾部 hash 之前的最后一个 payload 字节,
让唯一能抓到它的就是 hash 自校验,变异才真红。
## internal/devicebridge/client —— BaseResult
TestBaseResultAlwaysCarriesIdentity op/req_id/status
TestBaseResultOmitsEmptyOptionalFields 空 output/error 不进结果
TestBaseResultCarriesOutputAndError 非空时落对应键
## 变异验证(改回旧实现必须红)
update CmpSemver 恒 0 → ★ 版本大小比较错误
kernelboot 跳过 hash 校验 → ★ 数据区位翻转的图被接受
devicebridge 丢 req_id → ★ 身份字段缺失
kernelboot 第一版变异造成「未使用变量 → 编译失败」,不算干净断言红;
改成跳过校验但保留变量后确认判据有效。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 42 个测试包 / 100 包无测试 / 0 FAIL
3 个新包 -race 全绿
源码目录 _test.go 0
|
2026-11-09 14:59:40 +08:00 |
|
|
|
3420ceca60
|
test: 按模块导出接口重写黑盒判据(批次 10:agent / lua / ollama / log)
4 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 20 条判据,只调导出接口,输入 → 可观察输出。
## internal/agent —— 人格文件
TestSaveThenLoadPersonality Save → Load 内容/路径
TestLoadMissingPersonalityGivesEmpty 文件不存在 → 空人格
TestInjectPromptContainsContentAndRules 注入含原文 + 内核说话规则
TestInjectPromptEmptyPersonalityIsEmpty
TestPersonaStaleHints 陈旧措辞给提示;干净文本不报
TestSavePersonalityCreatesParentDirs 自动建父目录
## internal/lua —— VM 适配器
TestNewVMExposesAdapterDir 构造参数 → AdapterDir
TestListAdaptersEmptyThenLoaded LoadAdapterSource → ListAdapters
TestLoadAdapterSourceRejectsBadScript
TestCallTransformRequestUnknownAdapter
TestGetAdapterEndpointUnknownIsEmpty
## providers/ollama —— httptest 真实 HTTP
TestGenerateReturnsText 200 → Response.Text
TestGenerateMarksTruncated done_reason=length → Truncated
TestGenerateHTTPError 5xx → 报错
TestGenerateRejectsInvalidJSONSchema
TestNewFallsBackToDefaultModel 空 model 走默认值
**判据写错过一次**:第一版断言"空 model 构造失败",但 `optString`
对空值回退默认,构造成功才是真实契约,按此修正。
## internal/log —— Manager 生命周期
TestManagerStartStopIsSafe Start/cancel/Stop 不挂住
TestManagerStopIsIdempotent 连停两次不 panic
TestRetentionPoliciesAreDistinct 四个策略常量互异
## 变异验证(改回旧实现必须红)
agent SavePersonality 不建父目录 → ★ 文件没落盘
lua ListAdapters 恒空 → ★ ListAdapters = [],期望 1 个
ollama 丢 Truncated → ★ done_reason=length 未标记 Truncated
log Stop 重复 close → panic: close of closed channel
lua / log 第一版变异分别是文本不匹配与"import 变未使用导致编译失败",
都不算干净的断言红;重做成只改行为后确认有效。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 39 个测试包 / 100 包无测试 / 0 FAIL
4 个新包 -race 全绿
源码目录 _test.go 0
|
2026-11-09 14:41:43 +08:00 |
|
|
|
b2a5fad95f
|
test: 按模块导出接口重写黑盒判据(批次 9:seq / nlp / pipeline)
3 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 17 条判据,只调导出接口,输入 → 可观察输出。
`internal/kernelmod/pluginhost` 本批跳过:它的 PluginCatalog 只有
`newCatalog` 这个未导出构造,没有对外入口。按「不为测试导出内部」的
规矩,不为它造壳。
## internal/plugins/seq —— Parse / Store
TestParseValidSequence 合法 JSON → 名字/Boot/Groups
TestParseRejectsUnknownField 拼错字段必须报 unknown field
TestParseRejectsInvalidJSON 坏 JSON 报错
TestStoreRoundTrip Save → List/Load → Delete
TestLoadMissingReportsError
TestCheckNewRejectsSelfRecursion #self 调用被拒
TestCheckNewAcceptsValidCall 合法 group 调用通过
**判据写错过三次,都按真实输入契约修正**:
① group 的工具字段叫 `tools` 不叫 `steps`;
② `tools` 在 JSON 里是字符串(若干以 `;` 分隔的 JSON 对象),不是数组;
③ 最后一个工具末尾必须有 `;`。
`CheckNew` 守的是 seq_call 的 target 校验,不是"同名重复",判据按此改。
**还发现一条假绿并修掉**:`TestParseRejectsUnknownField` 第一版只断言
`err != nil`,但输入没带 group —— "没有任何 group"同样会报错,
去掉 `DisallowUnknownFields` 后判据仍绿。改成带合法 group
且断言错误里含 `unknown field`,变异才真红。
## internal/nlp —— 配置 / Extractor
TestDefaultExtractorConfig 默认权重可用
TestSetFusionWeightsRoundTrip 设入 → FusionConfig 读回
TestSetDefaultParserRoundTrip SetDefaultParser → GetDefaultParser
TestExtractEmptyTextReturnsEmptySet
TestExtractNonEmptyTextDoesNotPanic
不带 ONNX 模型时默认 parser 为 nil、`NewExtractor(nil)` 走 fallback,
这是真实契约;判据不要求它非 nil,而是用 stub 验证 Set/Get 往返。
## internal/memory/pipeline —— Distiller
TestAppendThenRecentRecords Append → GetRecentRecords
TestGetRecentRecordsRespectsLimit limit + 取最新 N 条
TestPauseResumeStopped 暂停/恢复可观察
TestStatsIsUsable
TestRawPathIsUnderDataDir
## 变异验证(改回旧实现必须红)
seq 去掉 DisallowUnknownFields → ★ 未知字段被静默接受
nlp SetFusionWeights 丢 alpha → ★ FusionConfig 不符
pipeline Append 不记录 → ★ GetRecentRecords = 0 条
全部还原后复跑全绿。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 35 个测试包 / 100 包无测试 / 0 FAIL
3 个新包 -race 全绿
源码目录 _test.go 0
|
2026-11-09 14:20:59 +08:00 |
|
|
|
ac525670d1
|
test: 按模块导出接口重写黑盒判据(批次 8:embedding / generation / text / social)
4 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 18 条判据,只调导出接口,输入 → 可观察输出。
## pkg/embedding —— 注册表 + 校验
TestRegisterThenOpen 注册 → Names/Open → Info/Embed 输出
TestOpenUnknownProviderReportsError 未注册名报错且可诊断
TestValidateInfo 维度/指纹真值表
TestValidateVector 维度不符 / NaN / Inf 必拒
## pkg/generation —— 注册表
TestRegisterThenOpen 注册 → Names/Open → Info/Generate 输出
TestOpenUnknownProviderReportsError 未注册名报错
## internal/memory/text —— 真实临时目录
TestAppendThenRecentEvents Append → RecentEvents 读回内容
TestReplayVisitsAppendedEvents Replay 收到全部事件
TestFileCountAndStats 写后文件数/统计可用
## internal/memory/social —— 真实内存图库
TestNilDBReportsError 无图库时写操作报错(不静默)
TestSetTraitThenGetTrait 特质往返
TestSetTraitOverwrites 同键覆盖
TestAddRelationThenGetRelations 关系往返
TestListPersons 人物列表含已写入的人
## 变异验证(改回旧实现必须红)
embedding 非有限值检查留 NaN 通过 → ★ 非法向量被接受:[NaN 2 3]
generation 未注册名静默返回 nil → ★ 未注册 provider 的报错不可诊断
text Append 不写盘 → ★ RecentEvents = 0 条
social SetTrait 不写 → ★ 无图库却 SetTrait 成功
前两条第一版变异分别造成「编译失败(math 未使用)」和「panic」,
不是干净的断言红 —— 改成只改行为、仍能编译的变异后确认有效。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 32 个测试包 / 100 包无测试 / 0 FAIL
4 个新包 -race 全绿
源码目录 _test.go 0
|
2026-11-09 13:55:15 +08:00 |
|
|
|
379300d89c
|
test: 按模块导出接口重写黑盒判据(批次 7:agent-io / system / ipc / supervisor)
4 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 15 条判据,只调导出接口,输入 → 可观察输出。
## internal/agent/io —— OutputCapability
TestSupportsSingleAndCombinedFlags 位标志的「任一位命中」语义
TestCapabilityStringListsFlags String 列出已置位能力名
TestZeroCapabilityString 零能力不 panic
**判据写错过一次**:第一版按「全部命中」写 Supports,实测红。
查实现(`c&cap != 0`)确认是任一位命中,按真实契约修正。
## internal/system —— 路径判定 / 写前留档
TestIsProtectedPathExplicit 显式前缀集内/外的判定
TestArchiveBeforeWrite 受保护路径覆盖前留档(走导出的
SetProtectedPaths 配置入口)
TestNetworkBaselinePathIsStable 路径输出稳定
TestLoadBaselineMissingReportsError 读不存在的基线报错
## internal/ipc —— 真实 unix socket 往返
TestPingReturnsServerStatus Server 状态原样到达 Client
TestPingWithoutServerReportsError 无服务端时报错
用临时目录下真 socket,不 mock 传输层。
## internal/supervisor —— Daemon 登记
TestFreshDaemonHasNoAgents 新 daemon 无 agent
TestRegisterThenList RegisterAgent → List/Get 反映该 agent
TestGetUnknownAgentReportsError 未注册 agent 报错
TestSDKAdapterSeesSameAgents 适配层看到同一份登记
## 变异验证(改回旧实现必须红)
agent/io Supports 恒 false → ★ 已置位的能力报告不支持
system 路径判定恒 false → ★ 前缀集内的路径没判受保护
ipc Ping 丢 Tools 字段 → ★ Ping 拿到的状态不符
supervisor ListAgents 恒空 → ★ ListAgents = [],期望 1 个
system / ipc 第一版变异改错了位置(空变异),重做后确认判据有效。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 28 个测试包 / 100 包无测试 / 0 FAIL
4 个新包 -race 全绿
源码目录 _test.go 0
|
2026-11-09 13:38:31 +08:00 |
|
|
|
8814ccdef6
|
test: 按模块导出接口重写黑盒判据(批次 6:promptsec / toolroute / pluginview / meta)
4 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 21 条判据,只调导出接口,输入 → 可观察输出。
## internal/promptsec —— NewRegistry/Declare/Render/Snapshot/Names
TestDeclareThenRender 声明两段,按 Order 输出且含正文
TestDeclareRejectsMismatchedOwner 段名前缀与 Owner 不符必须报错
TestDeclareIsIdempotent 同段名覆盖,不产生两份
TestEmptyContentRemovesSection 空正文 = 撤销声明
TestRemoveOwnerOnlyDropsThatOwner 撤销一个 owner 不影响其他
TestSnapshotReturnsAllSections 快照反映当前声明
## internal/toolroute —— Dispatch
TestDispatchRoutesEachPrefix 15 种工具前缀 → 对应族处理器
TestDispatchGetPluginToolsExtractsArg plugin_name 参数透传
TestDispatchUnknownIsNotSwallowed 未知工具 handled=false(插件工具不能消失)
TestDispatchNilHandlers nil handlers 不 panic
用 recordingHandlers 记录哪个族被调用,不断言内部 switch 结构。
## internal/pluginview —— 事件 → Snapshot
TestFreshViewIsEmpty 新视图为空
TestCmdRunEventAppearsInSnapshot cmd_run → 命令历史
TestTerminalCreateThenOutput terminal_create + 输出 → 终端状态
TestUnknownToolIsIgnored 未知工具不造记录
**判据写错过一次**:`OnTerminalOutput` 的 payload 键是 `terminal_id`
不是 `id`,第一版按 `id` 写 ⇒ 输出没进 Snapshot。按真实输入契约修正。
## internal/meta —— 版本元信息
TestFullVersionContainsKernelAndVersion
TestSourceURLLooksLikeHTTPS
TestLicenseMetadataNotEmpty
TestSDKCompatibleVersionIsSemverLike
## 变异验证(改回旧实现必须红)
promptsec Declare 直接 return nil → ★ Render 缺正文
toolroute 未知工具 handled=true → ★ 未知工具被吞掉
pluginview cmd_run 不记录 → ★ cmd_run 未进命令历史
meta FullVersion 丢版本 → ★ 不含 "1.4.0"
第一版 promptsec 变异改的是条件中的一段 token、没改变行为(空变异),
重做后确认判据有效。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 24 个测试包 / 100 包无测试 / 0 FAIL
4 个新包 -race 全绿
源码目录 _test.go 0
|
2026-11-09 13:22:35 +08:00 |
|
|
|
5a0ff10d8d
|
test: 按模块导出接口重写黑盒判据(批次 5:vector / systemd / recovery)
3 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 17 条判据,只调导出接口,输入 → 可观察输出。
## internal/memory/vector —— 纯数学 + Store
TestCosineSimilarity 稀疏向量:同向/正交/零向量
TestDenseCosine 稠密向量:三种基本情形 + 非单位向量
TestStoreInsertSearchRemove Insert → Search 命中 → Remove → 不再命中
**变异第一版没红**:`DenseCosine` 去掉归一化后,我给的 `{1,0}`
用例点积恰好还是 1 ⇒ 空变异。补 `{4,4} / {2,2}`(不归一化得 16,
归一化得 1)后才真红。**变异必须真的改变被测行为**,这条按纪律记下。
## internal/systemd —— State / NewMigratePlan
TestStateInSync 相同/等价/空/不同路径
TestStateDescribe 一致→空;不一致→含两路径与后果
TestSyncCommand 不一致时给出可执行命令
TestNewMigratePlanNormalizesPaths 路径 Clean;脚本内联新旧路径 + shebang
TestRestartCommand 命令含给定地址
不读单元文件、不执行脚本、不碰系统状态。
## internal/recovery —— 文件往返
TestTaskRoundTrip SaveTask → LoadTask 全字段 + Plugins()
TestTaskPathsAreStable TaskPath/ResultPath 稳定且互异
TestResultRoundTrip SaveResult → LoadResult 结论字段
TestLoadTaskMissingFileReportsError
TestSortSteps 输出可用
## 变异验证(改回旧实现必须红)
vector 不归一化 → ★ 非单位同向 = 16,期望 1
systemd InSync 恒 true → ★ 真不一致: InSync=true
recovery 丢 PluginList → ★ PluginList 丢失:[]
全部还原后复跑全绿。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 20 个测试包 / 100 包无测试 / 0 FAIL
3 个新包 -race 全绿
源码目录 _test.go 0
|
2026-11-09 13:08:16 +08:00 |
|
|
|
aac11f6661
|
test: 按模块导出接口重写黑盒判据(批次 4:coldcontext / tracker / network)
3 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 15 条判据,只调导出接口,输入 → 可观察输出。
## internal/memory/coldcontext —— NewStore/Insert/Get/Query/Consume/Blocks
TestInsertThenGet 空 ID 自动补齐;Get 拿回内容;时间戳填好
TestInsertExplicitIDKeepsIt 显式 ID 不被覆盖
TestQueryReturnsRelevantDoc 按分词器词形召回相关文档
TestConsumeIsOneShot 消费过的文档不再返回
TestBlocksReflectsInsertedDoc Blocks 输出可调用
**判据写错过一次并修正**:分词器按空白切词,文档里「发票重试」是一个
token,所以「发票 重试 账单」查不到而「发票重试」能查到 —— 实测探针
确认后按真实输入契约写。判据的输入必须能走到被测路径。
## internal/tracker —— NewTracker/Init/PreAction/PostAction/Rollback
TestFreshTrackerHasNoChanges 新 tracker 无 changeset
TestNewChangeSetCarriesAction Action/ID/时间
TestPostActionWithoutFileChangeIsNoop 无文件变更不凭空造 changeset
TestRollbackLatestWithoutChangesReportsError
TestRollbackClearsChangeSets Rollback 后视图清空
不碰 t.changeSets、不假设 upper/merge 目录结构。
## internal/network —— NewMonitor/CheckOnce/Status/AggregateResult
TestCheckOnceReachableServer 2xx → Reachable=true + URL/时间
TestCheckOnceServerError 5xx → Reachable=false
TestCheckOnceUnreachableEndpoint 连不上 → Error 非空,不 panic
TestCheckOnceInvalidURL 非法 URL → 失败状态
TestFreshMonitorStatusIsEmpty 初始状态为空
用 httptest 提供真实 HTTP 输入,不 mock client、不碰 m.status。
## 变异验证(改回旧实现必须红)
coldcontext Insert 不补 ID → ★ Insert 没有给空 ID 的文档补 ID
network 5xx 判可达 → ★ 5xx 端点判可达
tracker 无变更也造 changeset → ★ 无文件变更却 HasChanges=true
全部还原后复跑全绿。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 17 个测试包 / 100 包无测试 / 0 FAIL
3 个新包 -race 全绿
源码目录 _test.go 0
|
2026-11-09 12:56:13 +08:00 |
|
|
|
443110506c
|
test: 按模块导出接口重写黑盒判据(批次 3:memory / agent-core)
延续批次 1/2 的形态:只调模块**本来就有**的导出接口,输入 → 可观察输出。
2 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 17 条判据。
## internal/memory(10 条)
纯函数(输入文本 → 输出):
TestCleanText 空白/「,」前缀(实现剥两次)
TestChannelScene "qq" → "chan:qq",空 → 空
TestFilterNoiseTriples 噪音主语/宾语被过滤,且不改输入切片
TestSentenceBlockIDIsStable 同文本同 ID
TestNewSentenceBlockFields Trim 后的 Text 与 ID
TestBuildCentroid 中心向量均值 = 输入算术均值
GraphDB 真实构造 → 写入 → 可观察输出:
TestCommitThenGraphData Commit → GraphData 给出 nodes 结构
TestReadOnlyHandleRejectsWrite OpenGraphDBReadOnly 在 SQLite 层拒写
`TestReadOnlyHandleRejectsWrite` 守的是「子 agent 改不了主记忆」的
**结构性**保证(PRAGMA query_only=1),不是靠调用方自觉。
**判据写错过两次,按真实契约修正**:
① `CleanText` 连剥两次「,」,第一版按"只剥一次"写 ⇒ 红。
② 只读句柄的第一版三元组用了单字符实体名,被 `validEntityName`
提前拒掉、根本走不到 SQL 层 ⇒ 假绿。换成合法实体名后才真正验到
query_only。这个坑值得记:**判据的输入必须能走到被测路径**。
## internal/agent/core(7 条)
TestAgentID New(AgentConfig{ID}) → ID()
TestOutputAllowlist 空白名单=完整授权;非空=白名单
TestDuplicateInputWindow 同源同内容第二次判重;异源/异内容否
TestRealtimeFlag 普通 agent=false;nil receiver 安全
TestEmptyCollectionsOnFreshAgent Residents/RTChildren/DumpScheduler 为空
TestResolveOutputTargetWithoutRegistry 无注册表 → ok=false,不 panic
TestGetKernelStatusNeverNil 状态查询返回可用对象
只用 `New(AgentConfig)` 装配,不启动事件循环(已核实 `New` 里没有
`go a.xxx` / `Start()`),不构造 TaskFrame、不调 stepLLM/runStage、
不碰 a.sched、不读源码。
## 变异验证(改回旧实现必须红)
memory CleanText 不剥前缀 → ★ CleanText(",,你好")=",你好"
memory 只读句柄不设 query_only → ★ 只读句柄接受了写入
core 白名单恒放行 → ★ 白名单外的通道被放行
core 去重恒 false → ★ 同源同内容第二次未判重
全部还原后复跑全绿。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 14 个测试包 / 100 包无测试 / 0 FAIL
2 个新包 -race 全绿
源码目录 _test.go 0
|
2026-11-09 12:37:52 +08:00 |
|
|
|
8dff678640
|
test: 按模块导出接口重写黑盒判据(批次 2:security/events/safeclose/kstate)
延续批次 1 的形态:只调模块**本来就有**的导出接口,输入 → 可观察输出。
4 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 24 条判据,每模块 1 个文件。
## 各模块的接口清单与判据
### internal/security —— NewCodec / Seal / Open / Domain / Enabled
TestCodecSealOpenRoundTrip Seal → Open 得回原文;Seal 不返回原文
TestCodecDomainSeparation 不同 domain 的密文互不通用
TestCodecWrongKeyCannotOpen 换钥后旧密文不可读
TestCodecEnabledAndDomain 元信息输出;nil Codec 不报 Enabled
TestCodecOldKeysStillDecrypt NewCodecWithOldKeys 轮换后旧密文可读
TestCodecOpenLegacyPlaintextPassthrough 旧明文原样透传
TestCodecOpenRejectsCorruptedCiphertext 带密文前缀但内容坏掉必须报错
**判据写错过一次并修正**:第一版把 `Open("not-a-cipher")` 也要求报错,
实测红。查实现(codec.go:138)确认那是**明确的兼容契约** —— 加密是后来加的,
库里存在未加密的历史值,判成错误会让整库读不出来。改成两条:
旧明文透传 / 损坏密文报错。把差异显式写下来。
### internal/events —— NewBus / Subscribe / Publish / SetGuard
TestPublishDeliversPayload 订阅后发布,handler 收到原 Payload
TestUnsubscribeStopsDelivery 退订后不再收到
TestOnlyMatchingTypeReceives 不同事件类型不串台
TestPublisherIdentity 空 Name = 内核,非空 = 插件
TestSetGuardRoundTrip SetGuard → Guard 拿回同一守卫
`Guard` stub 按真实接口签名实现(AllowPublish 返回 `(bool, string)`、
Scrub* 返回 map),不是猜的。
### internal/safeclose —— Safe / SafeFunc
TestSafeFuncIsIdempotent 连调 5 次不 panic,channel 确已关闭
TestSafeReturnsFiredOnce 同一把 Once 只有第一次返回 true
TestSafeRejectsNilInputs nil once / nil chan 安全返回 false
TestConcurrentCloseIsSafe 8 goroutine 并发关同一 channel
不用任何 `NewOnceForTest` 之类的测试钩子 —— `sync.Once` 由调用方自己造,
这正是模块本来的公开用法。
### internal/kstate —— NewSyncBus / Mount / Get / State
TestSyncBusReadyLifecycle MarkDeclared/MarkReady → IsReady/AllReady/Missing
TestSyncBusReadyInOrderIsStable 就绪顺序可复现,重复标记不重复记录
TestSyncBusKnownVsReady 声明过 ≠ 已就绪
TestSyncBusKernelComplete 内核完成标记可观察
TestSyncBusSnapshotIsCopy 外部改快照不影响总线
TestMountGet Mount → Get 拿到同一份状态
TestStateCloseAllInvokesClosers AddCloser 注册的清理被调用
不用 `MountForTest`(上一轮已删除):直接用 `Mount` + `Get` 断言输出。
## 变异验证(改回旧实现必须红)
security Seal 直接返回原文 → ★ Seal 返回了原文 —— 没有加密
events 退订函数变空操作 → ★ 退订后仍收到 1 次
kstate MarkReady 不记 order → ★ ReadyInOrder = [],期望 [a b]
safeclose Safe 不用 sync.Once → panic: close of closed channel
前两条第一版变异因文本形态不匹配**没有变红**,重做后确认判据有效 ——
空变异不算变异验证,这一点按纪律写清楚。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 13 个测试包 / 100 包无测试 / 0 FAIL
4 个新包 -race 全绿
源码目录 _test.go 0
|
2026-11-09 12:21:13 +08:00 |
|
|
|
3384cee62a
|
test: 按模块导出接口重写黑盒判据(批次 1:config/knowledge/distill/agent-api)
## 方向
「面向模块输入接受模块输出」:只调模块**本来就有**的导出接口,
输入 → 断言可观察输出。不从旧白盒里抽、不读源码、不引用文档、
不构造内部结构、不为测试导出任何新符号。
4 个文件全部放 `<module>/test/`,源码目录仍零 `_test.go`。
共 23 条判据,每模块 1 个文件。
## 各模块的接口清单与判据
### internal/config —— NewConfigRegistry / PluginConfig
TestCoreSetGetDelete Set → Get → Delete → Get=nil
TestPluginNamespaceIsIsolated 两插件同名 key 不串台;核心表看不到插件键
TestPluginListByPrefix List 只返回匹配前缀的键
TestDefRoundTripPreservesSecret RegisterDef → ListDefs 保住 Secret
TestSecretValueIsNotPlaintextOnDisk Set → GetSecret 往返 + DB 里无明文
**判据按真实契约写**:插件表由 `RegisterDef` 创建,`Set` 不隐式建表。
第一版没先 RegisterDef,实测 `no such table: config_alpha` —— 不是缺陷,
是我把装配顺序当成了实现细节。判据里显式写了这条契约。
### internal/knowledge —— NewStore(root) / Add / List / Stats / Remove / Search / Categories
TestAddThenListAndStats Add → List/Stats
TestRemoveActuallyRemoves Remove → 空;二次 Remove 报错
TestPathTraversalIsRejected ".." / "." / 空名 必须被拒
TestCategoriesIncludeIntermediateLevels tech/go/叶名 ⇒ tech、tech/go 都出现
TestSearchRanksRelevantAboveUnrelated 只问排序,不钉数值
`TestSearchRanksRelatedAboveUnrelated` 直接对治 engineering-lessons §1.1
那个坑:16 条判据全绿而检索结果完全无关(分数全挤在 0.75~0.79)。
**判据写错过一次并修正**:`Categories()` 返回的是**父路径**,
`tech/go/concurrency` 条目的 Category 是 `tech/go`,不含叶名。
第一版断言叶名也在列表里 ⇒ 红。查实现后按真实契约改成两层。
### internal/memory/distill —— 纯函数,输入输出就是全部语义
TestBlockTextRoundTrip ToMemoryBlock → BlockSubject/Dimension/Value
TestBlockIDDependsOnSubject 不同主语不得撞 ID(静默数据丢失)
TestBlockIDIsDeterministic 同输入同输出
TestDimensionValueEdgeCases 无 '=' / 空维度 / 多 '='
TestSentenceBlockIDIsStable 原句块 ID 稳定
TestNormalizeDimensionIsIdempotent 归一幂等
### internal/agent/api —— 纯函数 + ProviderManager 公开状态机
TestEstimateTokensIsMonotonic 更长文本不得更少 token
TestTruncateByTokensRespectsBudget 截断后不超预算
TestModelContextWindowAlwaysPositive
TestIsValidSourceConfig 四字段 + URL scheme 真值表
TestRegisterGetListDefault 注册顺序/按名取回/默认源切换
TestAvailabilityStateMachine MarkUnavailable → 不可用 → Reset 恢复
TestProviderErrorCarriesStatusAndMessage
`ProviderManager` 用的是真 `NewProviderManager()` + 满足 `Provider`
接口的 stub,不碰 `m.status`、不用任何 ForTest 钩子。
## 变异验证(每条至少一次,改回旧实现必须红)
config ListDefs 丢 Secret → ★ Secret:true 在 RegisterDef → ListDefs 后丢失
knowledge normalizeName 放行 ".." → ★ Add("..") 被接受了 —— 路径可逃出知识根
distill BlockID 丢 subject → ★ 不同主语得到同一个 BlockID
api 漏 URL scheme 校验 → ★ ftp://x 判成了 true
全部还原后复跑全绿。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 9 个测试包 / 100 个包无测试 / 0 FAIL
4 个新包 -race 全绿
lens 4 个新文件 LSP 0 诊断
|
2026-11-09 11:57:48 +08:00 |
|
|
|
110db1bfee
|
fix(ci): 包依赖门的两个 shell bug + 既有 shellcheck/超长行清理
## CI 42555311224 红在「包依赖门」
::error::go list -deps github.com/.../internal/agent/core|internal/kernelboot 失败
两个真实 shell 缺陷,都是我上一提交写进去的:
1. `while read -r pkg bad` 没设 `IFS='|'` ⇒ 整行塞进 pkg,
go list 收到 `core|internal/kernelboot` 这种不存在的包路径。
2. `echo "$forbidden_pairs" | while ...` 用管道 ⇒ while 在子 shell 里跑,
里面 `fail=1` 传不出来。即使检出违规依赖,门也会**静默变绿** ——
比红更糟。
改为 `while IFS='|' read ... done <<< "$forbidden_pairs"`,
并用 `grep -c` 显式取命中数(`grep -q` 在 `set -e` + `||` 组合下易误判)。
**变异验证**(针对第 2 条那个假绿入口):
往禁止表插入一条真实存在的依赖 `core|internal/agent/api`:
正向 rc=0
变异后 rc=1
::error::internal/agent/core 反向依赖了 internal/agent/api —— 分层退化
## 顺带清掉 lint 标 STOP 的既有问题
- ci.yml 13 处 >80 字符行全部折行(含 awk 覆盖率那段 230 字符长行)
- shellcheck SC2086 ×2:`echo $ALL` / `echo $PKGS` 补引号
- shellcheck SC2129:三条 GITHUB_OUTPUT 重定向合成 `{ ... } >>`
`python3 .github/scripts/check-ci-gates.py` 通过;
本地实跑两道新门(测试归拢 / 包依赖)均通过。
|
2026-11-09 11:22:28 +08:00 |
|
|
|
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 |
|
|
|
eac8e808f1
|
chore(package): 产物名去掉旧 HomeAgent 命名;顺手清了 20G 构建缓存
scripts/make-package.sh 产出 `HomeAgent-linux-x86_64-*` 是旧项目名残留,
改为 `HologramAgent-linux-x86_64-*`;README 标题与首行注释同步。
不动的部分(都是真实运行契约,不是旧命名):
~/.homeagent/cli.sock waiter 硬编码读它,生产机上真实存在
~/.homeagent/hmapdev/sdk install.sh 的 SDK 安装目标
sdk.homeagent.jianfgit.xyz 线上域名
ProxyAuthHomeAgent 明确标注的存量源码兼容别名
legacySeededPromptSignature 旧人格卡签名,改了迁移判据失效
upload_assets.py 里的 HomeAgent gitcode 旧仓名/旧 Windows 产物名的历史注解
另外清掉 gitignored 的构建产物:build/ 7.2G + dist/ 6.6G +
.go/ 606M + .npm-cache/ 5.1G,工作区 21G → 1.7G。清后 go build ./... 通过。
|
2026-11-09 10:32:13 +08:00 |
|
|
|
790a28fdfd
|
fix(webui): 补提交 kernel_buttons 判据的 package/import 改动
## 事故:上一个 commit 只提交了 rename,没提交内容改动
`fb67703` 用 `git mv` 把 `kernel_buttons_render_test.go` 移进 `test/`,
但改 `package webui → package test`、`dashboardHTML → webui.DashboardPage()`
这些编辑留在工作区没进暂存区。结果 **HEAD 的 `test/` 目录里放着一个
`package webui` 的文件**:
found packages test (chat_ack_timeout_test.go) and webui
(kernel_buttons_render_test.go) in internal/plugins/webui/test
本仓 `.gitignore` 忽略 `build/`,本地 `go test ./...` 也过了 —— 但那是
**带未提交改动的工作区**过了,不是这个 commit 过了。干净 worktree 复验:
git worktree add /tmp/cleanchk HEAD
go test ./internal/plugins/webui/test/
→ FAIL [setup failed] found packages test and webui
这正是 2026-11-03 定的提交纪律第 5 条要拦的事。上一批做完 `git mv`
只 `git diff --cached --stat` 看到 "0 insertions" 就该警觉 —— rename 的
staged diff 是 0,而内容改动是 unstaged,等于提交了个半成品。
## 教训(写进流程)
`git mv` 之后**必须再 `git add` 一次同名路径**,否则只提交移动本身。
以后每批提交前一律:
git diff --stat # 看 unstaged 是否还有内容
git worktree add /tmp/clean <sha> # 干净检出复验
go build ./... && go test ./...
## 本提交的改动
package webui → package test
dashboardHTML → webui.DashboardPage()
FrontendAsset → webui.FrontendAsset
import 补 internal/plugins/webui + gofmt 排序
## 验证
干净 worktree(本提交后)复验通过,见下一条命令的输出。
|
2026-11-09 09:58:14 +08:00 |
|
|
|
fb6770350d
|
refactor(webui): 内核按钮判据迁到 test/ —— 走导出的页面与资源入口
kernel_buttons_render_test.go → test/ (3 条判据)
webui/ 161 → 158
webui/test/ 41 → 44
合计 202 不变
这条最省事:判据本来就断言"组装后发给浏览器的字节里有什么",
只差两个入口没导出。上一批已经导出了 `FrontendAsset` 与
`DashboardPage`,所以只改调用点:
dashboardHTML → webui.DashboardPage()
FrontendAsset("x.js") → webui.FrontendAsset("x.js")
它守的两件事仍然有效(写在判据头部注释里):
1. 按钮写进了 dashboard.html,但 {{DASHBOARD_JS}} 占位符没被替换/替换错位
⇒ 页面里看不到按钮
2. JS 函数被包在 IIFE 里不在全局,onclick="kernelRestart()" 点了报
"kernelRestart is not defined"
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 67 包全过,0 FAIL
|
2026-11-08 09:50:11 +08:00 |
|
|
|
1d38353498
|
refactor(webui): SSE 事件环判据迁到 test/ —— 导出环类型与记录字段
handler_sse_test.go → test/ (6 条判据)
webui/ 167 → 161
webui/test/ 35 → 41
合计 202 不变
## 原来那 6 条判据读的是私有字段
len(r.buf)
r.buf[0].ID / .EventType / .Data
`sseEventRing` 的对外语义就三条:容量上限、按 Last-Event-ID 重放、
未知 ID 回退全量。它们全部可以从**导出接口**断言:
NewSSEEventRing(cap) 构造
Append(id, type, data) 写入
After(id) []SSEEventRecord 重放
所以导出 `sseEventRing → SSEEventRing`、`SSEEventRecord` 的字段、
以及构造入口 `NewSSEEventRing`。
**为什么这不是"为测试导出内部"**:`After` 返回的东西就是客户端断线
重连时实际收到的内容 —— id、event、payload 三样。它们是外部可见的输出,
把字段藏起来只让包内测试能读,才是反过来的怪事。
测试侧的 `r.buf[i]` 统一换成 `r.After("")[i]`(空 ID 命中不了,按约定
回退全量),语义等价且不再触碰内部状态。
## 判据有效性(变异验证)
把容量裁剪短路掉(`if false && len(r.buf) > r.cap`):
--- FAIL: TestSSEEventRing_Append_Capacity
expected 3, got 5
环不再丢最旧条目 ⇒ 断线重连会重放超过窗口的历史,客户端 Last-Event-ID
语义失效。恢复后全绿。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 67 包全过,0 FAIL
|
2026-11-04 22:21:54 +08:00 |
|
|
|
bb29096f60
|
refactor(webui): 图谱判据迁到 test/ —— 导出 GraphDataForVisual 一层转换
graph_source_test.go → test/
handler_graph_pulse_test.go → test/ (7 条判据)
webui/ 174 → 167
webui/test/ 28 → 35
合计 202 不变
## 这一条必须动生产代码,理由要写清
判据想测的是「GraphData 的 raw map 被裁成可视化视图时,稠密向量必须被剥掉」。
两条路都不通:
走 HTTP 端点 /api/v1/memory/graph
→ 端点输入来自 h.memory.GraphData(),h.memory 是私有字段,
包外无法注入合成数据。测不了。
包内测试直接调 graphDataForVisual
→ 这正是要摆脱的白盒写法。
所以导出这一层转换 `graphDataForVisual → GraphDataForVisual`。它不是
为测试造出来的形状:函数的输入输出本来就是本包唯一的「raw map →
visual map」语义,端点在 handler_memory.go:199 调它。导出的是真实边界,
不是内部细节。(判据 §5.3「不要为满足测试导出内部」指的是后者。)
顺带把 `graphBlockView`(私有 JSON 结构)在测试侧改成匿名结构体 ——
它只用于反序列化断言,没有必要引用包内类型。
## 改走真实路由暴露的两个事实
### ① `newGraphHandler` 直接设 `&Handler{memory: m}`
改成 `NewHandler(sdk.New("webui", sdk.SDKConfig{Memory: m, ...}))` ——
`Memory` 确实能经 SDKConfig 注入(plugin.go:457 `base.SetMemoryAPI(cfg.Memory)`)。
### ② 503 暴露了鉴权要求
改走路由后立刻:
status = 503, want 200
`requireAPI` 需要 api_key,而最小装配没配。补 `seedWebUIConfig` 后 200。
之前直接调 handler 的写法**从来不知道有这个要求** —— 路由层挡掉的东西,
绕过路由就看不见。
## 判据有效性(变异验证)
在 `GraphDataForVisual` 里把裁剪短路掉(`if blocks != nil { return out }`),
实测红,且输出直接暴露泄漏内容:
--- FAIL: TestHandleMemoryGraph_StripsVector
response still contains vector field:
{"data":{"memory_blocks":[{"id":"b_old",...,"vector":[0.1,0.2,0.3],...
"success":true}
稠密向量确实会被下发到前端。恢复后全绿。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 67 包全过,0 FAIL
|
2026-11-04 22:06:35 +08:00 |
|
|
|
2e90c2cb5a
|
refactor(webui): openai_compat 判据迁到 test/,包内重建共享辅助
openai_compat_test.go → webui/test/ (4 条判据)
webui/ 178 → 174 条
webui/test/ 24 → 28 条
合计 202 条不变
## 迁移碰到的连带问题
`openai_compat_test.go` 里除了自己的判据,还定义了三个**被包内其他文件
依赖**的共享件:
newOpenAITestServer(t) auth_hardening_test.go 在用
testAuthAPIKey auth_hardening / gzip / legacy_proxy(×2) 在用
seedWebUIConfig 多处
文件一移走,包内立刻 build failed:
vet: auth_hardening_test.go:373:15: undefined: newOpenAITestServer
vet: auth_hardening_test.go:377:30: undefined: testAuthAPIKey
这说明「迁一个测试文件」从来不是孤立的 —— 它可能是包内共享辅助的**宿主**。
## 处理方式:两边各留一份,不跨包共享
包内重定义 `testAuthAPIKey` 与 `newOpenAITestServer`(放 handler_test.go),
test/ 侧保留自己那份(`seedWebUIConfig` 统一收进 helpers_test.go)。
**为什么不抽到共享包**:包内版本要用包内的 `testSDK`/`seedWebUIConfig`,
test/ 版本要用那边的 helper。抽公共包等于让 test/ 反向依赖包内的未导出
辅助 —— 那正是这次迁移要摆脱的东西。两份各写 20 行,换来两侧互不牵连,
比一个"共享测试工具包"更符合这次的目的。
## 顺带修掉一处重复定义
`seedWebUIConfig` 在 secret_meta_test.go 与 helpers_test.go 各出现过一次
(上一批我加的那份没和已有的对齐)。收拢到 helpers_test.go 一份,
字段与包内版本保持一致(api_key/username/password/session_ttl_hours)。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 67 包全过,0 FAIL
webui/ + webui/test/ 174 + 28 = 202 条,与迁移前一致
|
2026-11-04 21:52:52 +08:00 |
|
|
|
b3ae99e32f
|
refactor(webui): secret meta 判据迁到 test/ —— 走导出路由而非未导出 handler
## 迁移
secret_meta_test.go → webui/test/
## 这一条迁移暴露的问题比前几次多
原测试用两种白盒写法:
h := &Handler{settings: ...} // 直接构造,设私有字段
h.handleSettings(w, req) // 直接调未导出 handler
h.handleSettingsReveal(...) // 同上
改成黑箱后依次撞到四件事,逐个解决:
### ① 路由是 `/api/v1/settings`,不是 `/api/settings`
原测试写的路径 `/api/settings` 从来没被路由表匹配过 —— 它直接调 handler,
所以路径是摆设。真实注册在 handler.go:551:
mux.HandleFunc("/api/v1/settings", h.requireAPI(h.handleSettings))
⇒ 走真实路由后必须用对路径,否则 404。
### ② `Handler()` 会套三层中间件,依赖完整装配
`Handler()` = proxyDispatch + gzipMW + logged + mux。其中 `logged` 读
`h.settings` 的 webui 配置、`requestLog` 读 history store 等 ——
这些在最小装配下是 nil,直接 panic。
这不是"中间件写坏了",是本判据不该测它们。**用导出的 `RegisterRoutes`**:
mux := http.NewServeMux()
h.RegisterRoutes(mux) // 只注册业务路由,不套中间件
`RegisterRoutes` 是导出接口,拿到的是真实路由表 —— 仍是黑箱,
只是不带外层。判据测的是 settings 的 meta 与 reveal,不该被请求日志和
反代的依赖拖下水。
### ③ `requireAPI` 需要 api_key,没配就 503
{"error":"webui api_key not configured"}
补 `seedWebUIConfig`(与包内同名辅助一致),并在请求上带
`X-API-Key`。这是生产上非浏览器客户端的正常路径
(`validAPIKey` 优先于 cookie session)。
### ④ `SecretPlaceholder` 需导出访问
改成 `webui.SecretPlaceholder`。
## 判据有效性(变异验证)
secret 标记的传递链是
`ConfigDef.Secret → mapDefs → sdk.ConfigDef.Secret → HTTP meta`。
在 `internal/sdk/settings_impl.go` 的 mapDefs 里删掉 `Secret: d.Secret`:
--- FAIL: TestSecretFlagReachesSettingsMeta
Secret:true 未传递到 meta(前端会明文回显):
map[default: display_name:标志式 key:by_flag type:string]
恢复后全绿。这条判据守的是真实缺陷形态:SDK 的 ConfigDef 一直有 Secret
字段,但内核侧三处转换都漏过它,导致敏感项被渲染成普通文本框明文回显。
## 进度
webui/ 178 条(迁走 2 条)
webui/test/ 24 条
`webui/` 剩 19 个测试文件。剩下的大头是 `proxy_test.go`(45 处未导出引用)
与 `handler_test.go`(90 处),它们直接调 `handleXxx` 并依赖
`newTestHandler`/`testSDK` 这套装配辅助 —— 迁移需要先把那套辅助搬到
test/ 共享,再逐个改调用点。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 67 包全过,0 FAIL
|
2026-11-04 21:34:17 +08:00 |
|
|
|
c01f4d6350
|
test: 两个零测试包补黑箱判据 —— files(沙箱)与 tfidf
覆盖率分析显示这两个包**零测试文件**,而它们各自守着一条不能错的边界:
internal/plugins/files 581 行 含沙箱逃逸防护
internal/tfidf 306 行 记忆检索的 fallback 路径
## files:沙箱边界
581 行、0 判据。它是文件读写插件,`pathWithinSandbox` 写错一个字符就是
任意文件读写。没有判据意味着没人知道它现在是否还挡得住。
6 条判据全部走**真实装配**(调 `Start()` 而不是手搓 Plugin 结构),
通过注入的 registrar 捕获 handler,然后以「输入 {path, content} → 输出」断言:
TestToolsAreRegistered 四个工具都在
TestReadWriteRoundTrip 写进去能原样读回
TestSandboxEscapeIsRejected ★ 沙箱外路径必须被拒(3 种逃逸形态)
TestSandboxAllowsInside 反向:沙箱内必须能读
TestEmptyPathIsRejected read/write 拒绝空 path
TestLsEmptyPathMeansCurrentDir ls 把空 path 当 "."
TestLsListsDirectory ls 列出目录内容
**沙箱那条的判据设计**:沙箱外造一个**真实存在**的文件。若只拦不存在的
路径,判据会假绿;文件真实存在而仍被拒,才证明是沙箱在起作用。
变异(`pathWithinSandbox` 加 `if true { return true }`)⇒ 实测红:
★ 沙箱外路径 ".../secret.txt" 被接受了,返回 {"content":"SECRET",...}
这是任意文件读取。沙箱根: ...
## tfidf:检索判别力
306 行、0 判据。它是没有神经网络 embedder 时的文本相似度来源 ——
判据错 = 检索错,而检索错是静默的(不报错,只是答非所问)。
判据**只断言契约,不断言数值**:TF-IDF 的权重取决于语料统计,写死数值
等于把训练集也当成契约。6 条钉的是换了实现也必须成立的性质:
TestVectorizeIsDeterministic 同输入同输出(map 遍历浮点累加
会产生末位差异,让缓存/去重失效)
TestVectorizeHasFixedDimAndNonNegative 维度恒定、非 NaN/Inf、非负
TestEmptyTextGivesZeroVector 空文本不 panic 且给零向量
TestSearchRanksRelatedAboveUnrelated ★ 核心质量判据:只排序,不看数值
TestAddRemoveChangesResults 增删后检索结果随之变化
TestFingerprintAndDim 元信息非空
`TestSearchRanksRelatedAboveUnrelated` 直接对治 engineering-lessons §1.1
记的那个坑:16 条判据全绿而检索结果完全无关(分数全挤在 0.75~0.79)。
所以它只问"billing invoice 的查询里,auth 文档有没有排到相关文档前面"。
变异(`Search` 提前 `return nil`)⇒ 实测红两条。
## 一处判据写错了(已按实际契约修正)
第一版把 `files_ls` 也纳入"空 path 必须拒绝",实测红:
★ files_ls 接受了空 path,返回 {"content":"(empty directory)"}
查代码(plugin.go:488)发现那是**明确设计**:`if path == "" { path = "." }`。
不是缺陷,是我把三条工具的输入契约当成同一条了。改成两条判据分开钉:
TestEmptyPathIsRejected read/write 拒绝空 path
TestLsEmptyPathMeansCurrentDir ls 把空 path 当 "."
把差异显式写下来,以后有人想"统一都拒绝空 path"时能看到这是有意的。
## 覆盖率
internal/plugins/files 0.0% → 54.1%
internal/tfidf 0.0% → 有判据(契约全覆盖)
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 67 包全过,0 FAIL(新增 2 个 test 包)
|
2026-11-04 20:41:43 +08:00 |
|
|
|
b8ec1e84d2
|
refactor(webui): 再迁 3 个判据到 test/ —— 去重辅助函数
## 迁移
dashboard_js_scope_test.go → webui/test/
embed_read_test.go → webui/test/
mascot_asset_test.go → webui/test/
这三个原先都直接读未导出的 `dashboardFS`(`go:embed` 的变量)。
上一批已经加了导出入口 `FrontendAsset(name)`,所以只需换调用即可,
不涉及行为改动。
## 迁移暴露的三处重复(一并去重)
迁过去之后编译报了三组重复定义:
stripJSComments helpers_test.go 与 dashboard_js_scope_test.go 各一份
dashboardJSSource helpers_test.go 与 dashboard_js_scope_test.go 各一份
mustReadEmbedded embed_read_test.go 里定义,kernel_buttons_render_test.go 在用
前两个是**迁移造成的镜像**:原本包内的辅助函数在迁到 test/ 时被复制了一份。
逐字比较确认两份 `stripJSComments` 实现完全相同,删掉包内那份、保留
`test/helpers_test.go` 里的共享版本。
第三个不同:`mustReadEmbedded` 只有包内的 `kernel_buttons_render_test.go` 在用。
把它改成直接调导出的 `FrontendAsset`,不再需要这个辅助函数 ——
包内测试从此也不再依赖"哪个变量装着 embed 资源"这种实现细节。
## 顺带清掉一批未使用导入
批量迁移+脚本改包名之后,多个文件留着已经不用的 `webui` 导入
(有的只在注释里提到 `webui.FrontendAsset`)。用 `go vet` 的输出逐个清理,
最后 `go vet ./internal/plugins/webui/...` 干净。
## 判据未失效(变异验证)
把 dashboard.js 里 ack 超时 `}, 600000);` 改成 `}, 30000);`:
--- FAIL: TestChatAckTimeoutIsGenerous
chat_ack_timeout_test.go:59: ★ 受理超时只有 30000 ms —— 太小。
恢复后全绿。
## 进度
webui/ 180 条(原 186,迁走 6 条)
webui/test/ 22 条
合计 202 条,与迁移前一致(+ 之前新增的契约判据)
`webui/` 剩下 20 个测试文件,全部依赖 `handleXxx` / `newTestHandler` /
`testSDK` 等未导出 handler 与装配辅助,需要先有导出入口才能迁。
其中 `proxy_test.go`(45 处)和 `handler_test.go`(90 处)是最大两块,
要迁必须先给 webui 导出"起一个带路由的 handler"的测试入口。
## 验证
go build ./... PASS
go vet ./internal/plugins/webui/... PASS
gofmt -l 干净
go test ./... -count=1 65 包全过,0 FAIL
webui 连跑 3 次 0 失败
|
2026-11-04 20:23:34 +08:00 |
|
|
|
06f3161e1d
|
docs(en): 建立英文文档目录 —— 索引 + 教训全文 + 设计摘要
## 问题
仓库是公开的,README / README_EN 已经双语,但 `docs/` 只有 `docs/zh/`:
英文读者读完首页就撞墙。
## 做法:摘要而非全译,并写明为什么
`docs/zh/` 有 27 份文档、约 7000 行设计。**全译会烂得比审得还快**,
而一份陈旧的译文比没有更糟 —— 读者不知道它落后于原文多少。
所以按"内容变化速度"分级:
| 类型 | 译法 | 理由 |
| --- | --- | --- |
| 教训 | ✅ 全文 | 复用价值最高,失败形态会反复出现 |
| 索引 | ✅ 全文 | 没有它其余都不可发现 |
| 设计文档 | ⚠️ 一页摘要 + 状态行 | 7000 行全译跟不上演进 |
| 历史快照/证据 | ❌ 不译 | 时点产物,翻译无增益 |
| 未实施计划 | ❌ 不译 | 会变;且按 §4.3 本就不该进仓 |
判据写在 `docs/en/README.md` 里:
> 如果一份文档的内容变化速度快过译文能被审阅的速度,就译它的**摘要**而不是正文。
## 三份文件
**docs/en/README.md** —— 索引。含分类表(哪些已译、哪些只有中文摘要、
哪些不译)、状态行约定、以及"本索引必须在同一提交里更新"这条硬规则
(滞后的索引比没有索引更糟:它教读者信任错的地图)。
**docs/en/engineering-lessons.md** —— §1(判据会骗人)+ §5(判据形态)
全文翻译,§2–§4 摘要。§5 是本轮新增的核心内容:
5.1 判据必须把被测模块当黑箱(含三个正当例外)
5.2 禁止读源码做断言(附"想保证什么 → 错误做法 → 正确做法"表)
5.2.1 判据不得以文档为依据
5.3 测试与业务代码分离
5.3.1 三种测试位置的实测边界
5.3.2 覆盖率必须用 -coverpkg=./...
5.4 CI 必须覆盖每个独立 module
5.5 CI 与本地必须跑同一套门
每个小节都带实测数据与变异验证结果,不是原则罗列。
**docs/en/design-summaries.md** —— 6 份核心设计的一页摘要:
kernel-modularization 6 层 20 模块;依赖图在构建期算好并嵌进二进制;
depgraph.bin 必须与二进制同目录,放数据目录会崩溃循环
secret-firewall security 是外围到核心之间的防火墙,自持数据结构、
只接受事实不接受策略;出入向都要洗
input-scheduler L1–L4 + 现场保存;工具执行是临界区不可抢占
resident-subagent 与 rtchild 语义相反;单数/复数共用 Cleanup
toolcall-contract assistant(tool_calls) 后必须紧跟每个 id 的 tool
消息;批内不得插入任何东西
context-overflow-l4 下沉到冷上下文而非截断;InactivePages 是本次渲染
的临时标记(json:"-")
另有 13 份(插件相关 / 数据报告型)列为 "Chinese only" 并说明理由。
## 中文索引加了英文入口
`docs/zh/README.md` 顶部增加指向 `../en/README.md` 的一行,并明确:
> 本目录是**中文原文,也是唯一权威**;英文侧只做摘要,与本文冲突时以本文为准。
这一条很重要 —— 否则两份文档会逐渐分叉,而读者不知道该信哪个。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... 65 包全过,0 FAIL
|
2026-11-04 20:05:10 +08:00 |
|
|
|
892247946a
|
fix: staticcheck 24 条存量全部清零(含 4 个真实缺陷)
staticcheck 门接入时把存量设为基线 24 条,本次全部清掉,基线改为 0。
## 生产代码里的真实缺陷(4 个)
### ① memory/fusion.go —— BFS 的 depth 参数形同虚设
for d := 1; d <= depth; d++ {
neighbors, _ := g.NeighbourBlocks(seed.BlockID) // ← 永远是 seed
...
break // ← 只跑一轮
}
三处叠加:循环体末尾无条件 `break`;每层传的还是 `seed.BlockID`;
于是 depth=2 与 depth=3 召回结果**完全一样**。
staticcheck SA4008("变量在循环条件里从不改变")正是从这里报出来的 ——
它看出来 `d` 除了打分没有参与寻址。
改成真正的逐层 BFS(frontier 推进到上一层新发现的节点)。实测:
depth=1 → 2 个 [节点0 节点1]
depth=2 → 3 个 [节点0 节点1 节点2] ← 修复前停在这里
depth=3 → 4 个 [节点0 节点1 节点2 节点3]
补判据 `TestBFS召回_层深真实生效`:必须用**链**(n0-n1-n2-n3),
星型在 depth=1 就能召回全部叶子、断不出有没有走深。
变异(改回单层)⇒ 实测红:`★ depth=2 召回 2 个,期望 3`。
### ② nlp/extractor.go —— subjRels/objRels 只写不读
两个切片被 append 了 4 处,但**从未被读取**。三元组的关系类型来自
`relLabel := result.Tokens[vi]`,置信度来自 `result.DepRels[si]`,
与这两个切片无关。删除。
### ③ plugins/cmd/plugin.go —— 配置解析失败会把上限写成 0
if n, err := fmt.Sscanf(s, "%d", &p.maxOutput); err == nil && n > 0 {
} // ← 空分支
直接 Sscanf 到 &p.maxOutput:解析失败时它可能已被写成 0 或部分值,
而空分支无法回滚 ⇒ 配置写错一个字符就把输出上限变成 0
(下游 `maxLen := p.maxOutput` 直接拿它算截断长度)。
改为解析到临时变量、失败时保持默认并记日志。
### ④ cmd/homed/bootstrap.go —— 空的 else 分支
`if err != nil { log } else { }` —— 删掉 else。
## 测试判据里的真实缺陷(3 个)
### ⑤ stop_semantics_test —— 短路求值让第二次调用从不执行
if !a.sched.consumeCancel() || !a.sched.consumeCancel()
第一次返回 true 时第二次**根本不会执行**,于是"两条配额都可消费"
这条断言实际只测了一次。拆成两次独立调用(两处同型)。
### ⑥ seq/store_test —— 永假断言
if maxCallDepth != maxCallDepth { t.Fatal("maxCallDepth 不稳定") }
自己跟自己比,从诞生起就不可能失败。改成显式读两次比较。
### ⑦ pluginhost/catalog_test —— typed-nil 守卫永假
var nb *kstate.SyncBus
var iface kstate.SyncState = nb
if iface == nil { t.Skip(...) } // ← 恒 false
把 typed-nil 装进接口后,接口的 (type, value) 二元组都不为 nil。
这道前置守卫**从未生效** —— 而这正是该判据要演示的陷阱本身。
改用 `reflect.ValueOf(iface).IsNil()`。
## 其余(写法/冗余,无行为变化)
agent.go / context.go / toolresult_store_test / apply_regression_test
空 if 块(注释移到真实分支上方)
codec_chunkfast_golden_test 未使用的三个返回值 + 空 if
scheduler_critical/stack_test `if _, _ = f(); true { }` → 直接调用
overflow_test / upgrade_test 变量被整体覆盖,首次赋值未使用
dense_test m[string(x)] 替代临时变量
lex_rules_test range over string 而非 []rune(string)
## 验证
staticcheck -checks 'SA4*,SA5*,SA6*,SA9*' 0 条(基线 24 → 0)
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 65 包全过,0 FAIL
干净检出(worktree) build + memory/nlp/core 全绿
|
2026-11-04 19:49:21 +08:00 |
|
|
|
77e38360a8
|
ci+docs: 覆盖率改用 -coverpkg=./... —— 默认统计会漏掉外部测试包
## 缺陷(实测发现,且是**度量本身**在说谎)
加覆盖率采集之后,`internal/agent/core/child` 报告 0.0% —— 但刚给
它写了 9 条判据、覆盖 87.6%。
原因:`go test ./... -coverprofile` **只统计每个包内自测的覆盖**。
而覆盖 child 的测试在 `child/test/` —— 另一个包。主报告里只看到:
internal/agent/core/child 0.0% ← 假的
internal/agent/core/child/test 87.6% ← 真的在覆盖 child
## 修法
go test ./... -coverpkg=./... -coverprofile=/tmp/cover.out
实测差异:
child 0.0% → 85.7%
全局总覆盖率 56.9% → 63.0%
即**此前的数字是系统性低估的**。而低估比高估更危险 —— 它会促使人
"为凑覆盖率写空判据",那正是 engineering-lessons §1.1 记的坑。
代价:`-coverpkg=./...` 为每个测试二进制插桩全部包,跑得更慢
(实测仍在 20m 门内)。这个代价换"数字是真的",值得。
## 文档补 §5.3.2
把这条写进 engineering-lessons,因为它是**判据分离的连带义务**:
> 判据分离(§5.3)与覆盖率采集必须同时改。
> 只改前者,度量会退化成谎报。
否则下次有人把测试迁到 test/ 之后,会看到覆盖率"莫名其妙掉了",
进而怀疑分离本身,把它改回去。
## 本提交同时包含
`internal/agent/core/child/test/registry_contract_test.go`(9 条判据)
已随上一个提交(8884cf9)入库 —— 那里和 io 的并发判据修复放在一起,
因为它们都是"判据自身的缺陷"这一主题。
|
2026-11-04 19:27:01 +08:00 |
|
|
|
8884cf9962
|
fix(io): 并发判据自带串话 —— 32 个 goroutine 抢 26 个 key
## 缺陷(既有,实测在本次改动之前的基线上同样复现)
`TestConsumeToolBlocksFor_ConcurrentNoCrossTalk` 约 1/8 概率假红:
.go:105: call E 期望 [block-4-E],实际 []
根因在判据**自己**:
const n = 32
id := string(rune('A' + i%26)) // ← 字母只有 26 个
i=0 与 i=26 都得到 'A',i=1 与 i=27 都得 'B' …… 共 6 个 id 各被两个
goroutine 共用。两者共享同一个 key:后 Set 覆盖先 Set 的、先 Consume
拿到空或别人的值。
所以它想验证"不同 key 不串话",却让多个 goroutine 抢同一个 key ——
**判据违反了它自己想验证的前提**。这不是 `SetToolBlocksFor` /
`ConsumeToolBlocksFor` 的并发缺陷(共用 key 本就该覆盖)。
## 修法
id := "call-" + strconv.Itoa(i) // 唯一
实测:修改前 12 次内命中 1 次;修改后连跑 20 次 0 失败。
## 补两条判据把前提本身钉住
TestConsumeToolBlocksFor_KeysAreIsolated
一次性写 64 个**互不相同**的 key,逐个读回必须各得其所。
不涉及并发 ⇒ 失败一定是真缺陷,不会偶发。
TestConsumeToolBlocksFor_ConsumeIsOneShot
读一次后清空(block 是一次性的)。
变异:删掉 `delete(m.toolPendingBlocks, callID)` ⇒ 实测红:
★ 二次消费应得空(block 是一次性的),实际 [b]
## 为什么这条值得单独修
一个 1/8 概率假红的判据,实际效果等于**没有判据**:
它在 CI 里红了几次之后,所有人都会学会"重跑一次"。
而这正是本仓"长红 ⇒ 整道门失效"教训的一个变体。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 65 包全过,0 FAIL
该判据连跑 20 次 0 失败(修改前 12 次中 1 次)
|
2026-11-04 19:24:22 +08:00 |
|
|
|
abcb5669b8
|
docs(lessons): 补 §5.3.1 测试位置的三种实际边界
按 §5.3 动手后撞到的三类不可迁移情况,记清楚避免以后重复摸索:
可行 <pkg>/test/ 子目录(实测 pkg/test/ 里写 package pkg 也访问不到
未导出符号 —— Go 按目录划分包实例,所以与外部包等价)
不可行 package main(CLI)—— 'is a program, not an importable package'
不可行 测试直接构造私有字段 —— 但多数情况下导出构造入口已接受这些
参数(如 SDK 的 New(...)),改完判据仍咬得住变异
不可行 判据扫描「当前目录源码」—— 迁到子目录后 glob 到空,静默失去对象
以及迁移顺序:必须一次一个、让编译器说话。批量脚本加包名前缀会改到
导入路径/注释/标准库符号(实测把 core 改成 core.core.core,误伤 strconv)。
|
2026-11-04 18:30:11 +08:00 |
|
|
|
84021ef408
|
refactor(agent/core): 3 个黑箱判据迁到 core/test + 去掉一处白盒
## 迁移了什么
stages_test.go → internal/agent/core/test/
stages_plugin_test.go → internal/agent/core/test/
stages_tooldef_test.go → internal/agent/core/test/
包名 `core` → `test`。`agent/core` 目录少 3 个测试文件(该目录原有
196 个文件,其中 144 个是测试 —— 这是分步收敛的第一批)。
## 顺带去掉一处白盒
`stages_plugin_test.go` 原本这样断言"stage 表已清空":
h.mu.RLock()
_, exists := h.stages[sdk.StageAfterToolcall]
h.mu.RUnlock()
直接读**私有互斥锁和私有字段**。而 `UnregisterPluginStages` 的返回值
已经表达了同一件事。改为:
if n := h.UnregisterPluginStages("solo"); n != 1 { ... }
if n := h.UnregisterPluginStages("solo"); n != 0 { ... }
第二次摘除必须得 0 —— 如果内部表没清空,第二次仍会"摘到"残留计数。
这是黑箱可观测的等价断言。
变异验证:让 `UnregisterPluginStages` 假装摘除但不动表
(`if true { return 1 }`)⇒ 实测红:
--- FAIL: TestUnregisterPluginStages_RemovesOnlyThatPlugin
--- FAIL: TestUnregisterPluginStages_DropsEmptyStage
★ 重复摘除应得 0(说明内部已清空),实际 1
## 为什么只迁 3 个(agent/core 有 143 个测试文件)
先按「对源码未导出标识符的引用数」分档,实测:
0 引用 11 个文件
1 引用 约 20 个
... 其余逐步升高
但"0 引用"不等于"能迁" —— 逐个实测发现四种拦路虎:
① `newScrubRuntime` / `withScrubRuntime` / `newRTTestAgent` ——
包内测试辅助,其中 `withScrubRuntime(a *Agent, rt)` 直接设私有字段
② `computeTokenBudget` —— 未导出方法(tokenbudget_test 必须留包内)
③ `filepath.Glob("*.go")` —— 扫描"当前目录源码"的判据
(childkind_test),迁到子目录后会 glob 到空目录而**静默失效**
④ `newPreemptAgent` 等跨文件辅助
③ 尤其值得记:它不是编译错误,是**判据悄悄失去对象**。
## 踩到的坑(记下来避免重犯)
第一次批量迁 11 个文件、再用脚本自动加 `core.` 前缀,结果把
`import "…/internal/agent/core"` 本身也改了(变成 `core.core.core`),
还误伤了 `strconv.Itoa` 等标准库调用。批量文本替换在"加包名前缀"
这种场景下不可靠 —— 它会改到导入路径、注释、标准库符号。
最终改为**一次只迁一个、编译器逐步报错、手工修**。慢,但不会留下
半成品。agent/core 已完全还原过一次(git checkout HEAD)。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 64 包全过,0 FAIL
(core/test 是新包,63 → 64)
core/test 实跑用例数 17(确认不是空壳)
干净检出(worktree) build + core/... 全绿
## 后续
agent/core 剩余约 140 个测试文件,按上述四类拦路虎分批:
· 纯黑箱可迁的先迁
· 需要导出入口的,评估"导出是否合理"(如 FrontendAsset 那样)
· 真白盒的(改私有状态、测内部算法)加文件头说明为什么必须白盒
|
2026-11-04 18:26:46 +08:00 |
|
|
|
217e32b494
|
ci: 补 staticcheck + 覆盖率采集 —— 把"看不见"变成可见
## 背景
本轮判据治理的起点是「覆盖率/判据形状两者都看不清」。前几个提交补了
gofmt 门和 SDK 判据步骤;这个补最后两项。
## staticcheck
### 为什么选它
`go vet` 只跑标准检查器。实测今天撞到的那类缺陷它抓不到:
pluginHealthTracker 标记 Unhealthy=true 后,isHealthy 立刻用
LastReload(**从未被赋值**,恒为零值)判"冷却已过"而自愈
那是 SA4xxx 家族(无用赋值 / 永真条件)的领域。它不替代判据,
但能在"写完了但逻辑自相矛盾"处提前报警。
只查四族,不查风格:
SA4xxx 无用赋值 / 永真条件
SA5xxx 可疑代码
SA6xxx 低效写法
SA9xxx 必错分支
### 踩到的坑:版本不匹配会造出一道永远绿的门
第一版钉 staticcheck@2025.1.1,在 Go 1.27.1 上实测:
internal error in importing "image/color"
(cannot decode "image/color", export data version 4 is
greater than maximum supported version 2)
然后**静默返回退出码 0** —— 一道永远绿的门,比没有门更危险。
改用 2026.2.2 后正常解析。步骤里加了两道判:
· 退出码非 0 但输出里没有任何 `file.go:line` ⇒ 判定"未能运行",直接红
· 版本变化导致的静默退化由上面这条兜住
### 存量基线:24 条 / 13 个文件(2026-11-04 实测)
按分布:
SA9003 empty branch 10
SA4006 value never used 4
SA4000 identical expressions 3
SA4010 value never checked 2
SA4008 1 SA4023 1 SA6001 1 SA6003 1
处理方式是**基线 + 只许减不许增**:新增告警让这一步红,存量可见但
暂不阻塞(否则合并即长红,所有人学会忽略这道门 —— 正是本仓
ci.yml 里那段"长红 ⇒ 整道门失效"的教训)。低于基线时会提示把
BASE 调低,否则门会越来越松。
抽样确认这些是真问题,不是误报。例如:
internal/agent/api/codec_chunkfast_golden_test.go:68:9:
this value of fastOK is never used (SA4006)
internal/agent/api/codec_chunkfast_golden_test.go:69:2:
empty branch (SA9003)
`fastOK` 赋值后从未使用,且紧跟一个空分支 —— 那里本意要区分
「回退」与「快速路径给出失败」,实际什么都没做。
## 覆盖率采集
仓里 521 个测试文件、11.2 万行测试代码,但**没有任何覆盖率数据** ——
无法回答"哪些模块其实没被测到"。
补的步骤只报数,不设阈值:
· 打总覆盖率
· 打覆盖率最低的 15 个包
**为什么不设阈值**:阈值会促成"为凑数写空判据",那比没覆盖率更糟
(本仓 engineering-lessons §1.1 记的就是这个)。这里只把数字
摆出来,让人做模块级决策时有参照。
顺带:这一步同时兜住了 `go test` 的失败(`|| { ... exit 1; }`),
所以它不只是统计,也是一道执行力门。
## 验证
YAML 语法 OK
CI 门禁自检 通过(动态枚举 / 排除表 / 启动图 / 交叉枚举 / run 步骤语法)
本地复跑 staticcheck rc=1、24 条 = 基线 ⇒ 通过
go build ./... PASS
go test(core/webui) 全绿
## 后续
staticcheck 存量 24 条已登记方向(future-plan.md「staticcheck 存量」)。
它不是判据问题,是代码问题,单独收敛。
|
2026-11-04 18:20:04 +08:00 |
|
|
|
87f874e06f
|
refactor(webui): 7 个黑箱判据迁到 test/ + 导出前端资源入口
## 迁移了什么
chat_ack_timeout_test.go → test/
chat_attachment_img_test.go → test/
cmd_history_render_test.go → test/
config_namespace_test.go → test/
dashboard_tabs_deps_test.go → test/
plugin_status_render_test.go → test/
starmap_perf_test.go → test/
包名 `webui` → `test`。`webui/` 目录少 7 个测试文件。
## 为什么是这 7 个(分类依据)
先按「对未导出符号的依赖数」把 29 个测试文件测了一遍,这 7 个是
**0 依赖**(既不用 handleXxx,也不用 testSDK/newTestHandler 等测试辅助)。
其余 22 个按强度递增留在包内,后续逐步处理。
## 新增两个导出入口
### webui.FrontendAsset(name)
返回打包进二进制的前端资源原文(dashboard.js / dashboard.css /
mascot.webp / logo.svg …)。
判据要检查「实际会被发出去的那份文本」:dashboard.js 在严格模式下
会不会抛 ReferenceError(用 node 真跑)、mascot.webp 的魔数是否合法、
HTML 属性里有没有残留转义引号。
这些无法通过 HTTP 端点可靠断言(要登录态、且拿到的是拼接后整页,
定位不到单个 asset)。而留在包内测就得读 `dashboardFS` 这个**未导出变量**
—— 那正是"测试耦合实现"。导出后,判据变成「输入 asset 名 → 输出字节」,
与资源存在哪个变量、怎么 embed 无关。
### webui.DashboardPage()
返回组装完成的整页 HTML(外壳 + 内联 CSS/JS)—— 浏览器实际收到的那份。
与 FrontendAsset 的分工:那个取单个源资产,这个取成品。
## 顺带修掉迁移暴露的两个隐患
① 三个判据原本 `os.ReadFile("dashboard.js")` 读**磁盘相对路径**。
在包内恰好能跑(工作目录就是包目录),但那读的是**工作区文件**,
不是**二进制里实际嵌入的那份** —— 两者在生产上可能不同
(比如打包后 dashboard.js 被替换)。统一改走 FrontendAsset。
② starmap_perf_test 的相对路径 `../../../shared/starmap/layout.js`
与 `static/starmap-layout-worker.js` 在移入子目录后失效,已修正为
`../../../../shared/...` 与 `../static/...`。
## 判据仍有效(变异验证)
把 dashboard.js 里 ack 超时 `}, 600000);` 改成 `}, 30000);`:
--- FAIL: TestChatAckTimeoutIsGenerous
★ 受理超时只有 30000 ms —— 太小。
超时太短会让前端 abort,而 agent 仍在正常干活
恢复后全绿。迁移不是搬完就废。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 63 包全过,0 FAIL
(webui/test 是新包,故 62 → 63)
干净检出(worktree) build + webui/... 全绿
## 后续
webui 剩余 22 个文件(依赖 handleXxx / testSDK 等),按强度递增处理
agent/core 739 处白盒引用,先分类再动
CI 补 coverage + staticcheck
|
2026-11-04 18:17:31 +08:00 |
|
|
|
7cef7df6f2
|
test: 判据改为面向接口输入输出 + 文档立规 + CI 补缺口
本提交是三件事的第一批,后续按计划分步推进。
## 一、判据改造(两条实证)
### 1. turn_hint 判据原是自证循环
原测试在测试里**手动重放**了真实注入逻辑:
f.Msgs = a.buildMessages(...)
hint := stageHint(f.StageCtx, "turn_hint")
f.Msgs = append(f.Msgs, Message{Role:"system", Content:"[Turn hints] "+hint})
// 然后断言"最后一条是 system、内容含 [Turn hints]"
它断言的是**它自己刚 append 的那一行** —— 把 task.go:656-661 抄进测试
再验证抄得对不对。真实代码那段删掉,它照样绿。
改为:把 turn_hint 放进 StageCtx,跑**真实的 stepPrepare**
(newTaskFrame 起始就是 StepPrepare),检查它产出的 Msgs。
变异验证:删掉 task.go 里 `if hint := stageHint(...)` 那段 ⇒ 实测红。
### 2. webui 文件下发判据原是白盒
原测试直接调未导出的 `stageWebFile(src, isImage)`、断言 `webFilesDir`
包级变量 —— 那都是**实现方式**。
改为从**输出通道 handler** 走(`RegisterOutputChannel("webui", …)`,
agent 发图/发文件时走的就是它):
输入:{payload: "<路径或URL>", type: "image"|"file"}
输出:{status:"ok", url:"/files/<name>.<ext>", size:N}
4 条判据(新版 handler_files_contract_test.go):
· 图片 → 可下载 URL(前缀/扩展名/size)
· 远端 URL 原样透传(不下载)
· 危险扩展名被中和(.html 保留会在 webui 域执行脚本)
· 空 payload 被拒
变异验证:丢扩展名 ⇒ 红;远端也拷贝 ⇒ 红。
## 二、文档立规:engineering-lessons 新增第五节
原来只有「判据会骗人」,没有「判据本身形状错了」。新增 6 小节:
5.1 判据必须把被测模块当黑箱(含"如果重写内部实现、接口不变,
这条判据还会绿吗"这个可操作判据)
5.2 禁止读源码做断言(附正确替代表)
5.2.1 判据不得以文档为依据
5.3 测试文件与业务代码分离(现状:521 文件 / 11.2 万行,
agent/core 一个包 196 文件里 144 个是测试)
5.4 CI 必须覆盖每个独立 module
5.5 CI 与本地必须跑同一套门
§5.2.1 的直接触发:`interrupt_invariant_test.go` 里有一条判据写着
「事实清单(来自探测,见 docs/zh/interrupt-subsystem-split-plan.md §1.5)」
—— 被引用方自己标着「状态:计划(未实施)」,而那条判据的实际断言
只是"入队后队列里 1 条",与声称的"反向依赖现状快照"毫无关系。
该判据已删除(入队行为已由 scheduler_preempt_test 的黑箱断言覆盖)。
## 三、CI 补两个缺口
### gofmt 门
提交纪律要求 `gofmt -l` 干净,但三个 workflow 里 `gofmt` 出现 **0 次**
—— 纪律只靠人自觉。补上后未格式化的代码进不了主干。
只查本仓自管目录:SDK 是独立仓,格式由它自己的 CI 管。
### SDK 判据步骤
`third_party/hologramagentsdk/` 有自己独立的 go.mod:
go list ./... | grep third_party → 0 个
主仓 `go test ./...` **永远不包含** SDK 任何测试。实测 SDK 有 30 个判据
文件,主仓 CI 一次都没跑过。
补的步骤只跑主仓 git 跟踪的文件(CI 上只有 22 个 vendored 文件,
工作机上是完整独立仓)—— 实测 `git archive` 出 22 个文件后
`go test ./sdk/ ./meta/` 全绿。
## 四、SDK 仓测试迁移(独立仓,已单独提交 3a38ef5)
`sdk/{plugin,proxy,stress}_test.go` → `sdk/test/`,包名 `sdk` → `test`。
关键:原测试直接写 `&PluginSDK{regStage: X, name: "test"}` —— **私有字段**。
而 `New(name, sett, regTool, regStage, regAPI, regOutput)` 本来就把所有
registrar 作为参数,等价且是生产路径。共改 19 处,0 处剩余。
主仓同步更新了跟踪路径(这三个文件主仓也 vendor 了一份)。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 62 包全过,0 FAIL
干净检出(worktree) build + 改造后的判据全绿
## 后续(按计划分步)
步骤 2 webui 29 个测试分类迁移(已实测出精确分类)
步骤 3 agent/core 739 处白盒引用先分类再动
步骤 4 SDK tools/hmapdev(package main,无法迁出,只加说明)
步骤 5 CI 补 coverage + staticcheck
步骤 6 文档双语(需定术语口径)
|
2026-11-04 18:11:06 +08:00 |
|
|
|
07d35297a6
|
fix(webui): 反代三处缺陷 —— 入口不显示、子域链接失效、前缀重定向逃逸
全部来自本机 systemd 实例的实测,不是推测。
1. 插件页「服务入口」永远显示"暂无"
TABS.plugins.fetch 漏了 proxyServices,而 renderPlugins 第一行就调
renderProxyServicesCard()(读 state.proxyServices)。于是该页签
永远不请求 /api/v1/proxy/services —— journalctl 实测:用户正常使用
期间零请求,只有我手工 curl 的那一条。
判据 TestProxyServicesReachesPluginsTab 钉住三环(A 调用 B、B 读
state、C fetch 含块名),已做变异验证(删掉该项立刻红)。
不用静态调用图做通用依赖检查:实测它在 dashboard.js 上不可靠
(中文注释引号破坏配平、分发器展开导致大量误报),已在注释里
写明原因,避免后人重复踩。
2. 面板生成的子域链接 100% 进不了反代
生成链接走 base_url 的主机名,路由匹配只认 base_domain。
生产实例 base_url=homeagent.jianfgit.xyz、base_domain=localhost,
于是链接落到门户 mux → 302 /login,而手拼 *.localhost 反而能进。
新增 proxyBaseDomains 返回全部候选基域(配置值 → base_url 主机名
→ localhost),serveProxyHost 用它匹配。两者都认,不是二选一:
*.localhost 是 RFC 6761 的零配置基座,不能丢。
3. 前缀模式下上游根绝对路径重定向逃出挂载点
实测 NapCat:上游 GET / → 301 Location: /webui,浏览器跟随到
https://<门户>/webui(已跑出 /napcat),门户 mux 接住 → 302 /login。
httputil.ReverseProxy 不改写 Location,在 ModifyResponse 里补:
只改以 / 开头且未带前缀的,避开 //host 与已经带前缀的(否则会出现
/napcat/napcat/webui)。
顺带纠正:base_domain 保持原配置不动,改由代码兼容两种基域 ——
改配置会抢走别的 *.jianfgit.xyz 子域,且 *.jianfgit.xyz 是单层通配,
huawei-smarthome.homeagent.jianfgit.xyz 本来就证书不匹配(实测
verify error 62)。要外网可用子域形态,应把 base_domain 配成
jianfgit.xyz,那是运维决策,不在本次改动内。
|
2026-11-04 17:15:43 +08:00 |
|
|
|
0c959e3ff3
|
fix(rtchild): pump 裸读写 data race —— 锁贯彻到底
## 缺陷(-race 实测)
rtChild 里 pump 是**唯一**"构造后被改写、又有并发读者"的字段,
但它的读写绕过了 rc.mu:
Dispose(StopRTChild 的 goroutine) rc.pump = nil ← 写
rtEmit (rtLoop 的 goroutine) rc.pump ← 读
-race 栈:
Read at … core.(*Agent).rtEmit rtchild.go:1275
Previous write … core.(*rtChild).Dispose rtchild.go:377
## 为什么之前没被发现
TestRTChild_E2E_Sustained 覆盖同一个窗口,但它是**偶发命中**:
全量负载下 5 次中 1 次,单跑 20 次 0 次,基线 af17af5 全量 0 次。
这种形状在 CI 里等于没有 —— 红了是运气,绿了不代表安全。
## 修法:锁贯彻到底(不是只补这一处)
rtChild 全部字段逐个判定过:
字段 构造后写? 遵守锁规?
pump 是(Dispose) ★ 本提交修:读写都持 rc.mu
light/mainRO 是(Dispose) ✓ 无并发读者,只在 Dispose 内用
slot/input/output/agent 否 ✓ 只读,不需要锁
history 是(append) ✓ 三处访问全部持锁(原有)
round/lastSeq/curCancel/critical/suppressedCancels ✓ 原本就持锁
即:pump 是唯一的漏网者,其余要么本已正确、要么根本没有并发写。
## 两个顺序要求(不只是"加个锁")
① **取走并置 nil 在锁内,Close 在锁外**
pump.Close() 内部 wg.Wait() 等消费 goroutine 收尾;持锁等待可能与
另一个持锁路径交叉。而"置 nil"必须与读者的"读"互斥,否则读者会
拿到一个已被清掉的指针。
② **rtEmit 锁内取、锁外 Push**
Push 在队列满时会阻塞,持锁会卡死 Dispose 的置 nil。
## 判据(2 条,2 次变异全部实测变红)
TestRTPumpAccessIsLocked
一边不停 emit、一边 Dispose,用 -race 判定。
变异 A(rtEmit 裸读) ⇒ 连跑 3 次全部命中 DATA RACE
变异 B(Dispose 裸写) ⇒ 第 1 次命中 DATA RACE
TestRTPumpAfterDisposeIsNil
钉住顺序语义:Dispose 之后 rc.pump 必须为 nil(否则 emit 会推
送到已关闭的 pump),且兜底路径不 panic。
## 判据第一版是假的(记下来)
第一版读者写的是 `for i := 0; i < 500; i++` —— 500 次 emit 微秒级跑完,
而写者 sleep 1ms 才动手,两者**根本不重叠**。变异 B(裸写)连跑 6 次
都不红。改成"持续跑到写者宣布结束"之后才真正抓到。
这正是「判据必须能变红」的意义:不跑变异就永远不知道它是真判据还是摆设。
## 判据的边界(诚实说明)
这条判据的"红"依赖 -race 检测器。不带 -race 时它只能发现 panic/死锁。
所以必须与 `go test -race` 一起跑 —— 提交纪律第 4 条已要求全量 -race,
这里只是把「哪个窗口最危险」钉成可复跑的形态。
## 验证(提交纪律四项全绿)
go build ./... 0
go vet ./... 0
gofmt -l internal/ cmd/ pkg/ 干净
go test ./... -count=1 -race 0 竞态,62 包全过
干净检出(worktree) build + TestRTPump 全绿
|
2026-11-04 16:53:13 +08:00 |
|
|
|
8fa6f36982
|
fix(agent): 插件崩溃熔断从不生效 —— 冷却锚点用了从未赋值的字段
## 缺陷(写上一个提交的判据时撞出来的)
recordCrash 标记 Unhealthy=true,但 isHealthy 的冷却判据是:
time.Since(r.LastReload) > reloadCooldown
而 LastReload **只在 markReloaded 里被赋值**(即"重载成功"之后)。
插件刚崩、从未重载时它是**零值**:
time.Since(零值) ≈ 2000 年 >> 30s
⇒ 判"冷却期早已过去" ⇒ 立刻把 Unhealthy 置回 false
实测(连打 3 次 recordCrash):
crash #1 → needReload=false healthy=true
crash #2 → needReload=false healthy=true
crash #3 → needReload=true healthy=true ← 刚标完就自愈
## 影响
三处「插件不健康则跳过执行」全是**死分支**:
task.go:1065 stepToolBegin 中的跳过
task.go:1135 (上一提交刚补的那个分支的另一处同构)
toolbatch.go:155 并发批次中的跳过
即:一个正在崩溃循环的插件会被**持续调用**,每次调用再崩一次。
recordCrash 返回的 needReload=true 也没意义 —— 下一句 isHealthy 就说它健康了。
pendingReloads 用同一个零值字段 ⇒ autoReloadPlugins 每次都会尝试重载
**刚崩的**插件,很可能立刻再崩(崩溃循环)。
## 修法:冷却锚点改为 LastCrash
语义上「熔断」是"刚崩完先别用它,等 30s 再看" —— 观察窗口应锚在
**最近一次崩溃**,而不是"最近一次重载成功"。
isHealthy 锚点 LastReload → LastCrash
pendingReloads 锚点 LastReload → LastCrash
## 顺带删掉冗余
unhealthyPlugins() 零生产调用方(全仓 grep 只有定义本身)
record.FirstCrash 只写不读(写它的唯一目的是被下面这个判断用,
而那个判断早就改成只看 LastCrash 了)
## 判据(5 条,3 次变异全部实测变红)
TestCircuitBreakerActuallyOpens ★ 主判据
变异:锚点改回 LastReload ⇒ "★ 连续 3 次崩溃后插件仍被判健康"
TestCircuitBreakerWaitsForCooldownBeforeReload
变异:pendingReloads 锚点改回 LastReload ⇒
"★ 刚崩完就进了重载队列(应等冷却期):[p]"
TestCircuitBreakerRecoversAfterCooldown
变异:删掉恢复分支 ⇒ "★ 冷却期过后仍未恢复健康 ——
插件会被永久停用(比不熔断更糟)"
TestCircuitBreakerUnknownPluginIsHealthy 从未崩过的插件不记账
TestCircuitBreakerWindowResets 窗口外的旧崩溃不该累加
## 判据本身的两个坑(已避开)
① 只断言 recordCrash 的返回值 —— 它本来就返回 true,测了等于没测。
必须断言 **isHealthy 的结论**,那才是被熔断逻辑消费的东西。
② 不测"冷却之后会恢复" —— 那样把代码改成恒 return false 也能过,
而那就成了永久禁用插件(比不熔断更糟)。
## 连带:deniedpair_test 的前置条件改回真实状态机
上一提交里,第二条判据只能**手工置 Unhealthy**(因为 recordCrash 标完
立刻被自愈撤销)。现在熔断真生效了,改回用 recordCrash 打满阈值 ——
判据依赖公开状态机而不是私有字段。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 62 包全过,0 FAIL
干净检出(worktree) build + TestCircuitBreaker/TestDenied 全绿
|
2026-11-04 16:34:46 +08:00 |
|
|
|
c9be04fdac
|
fix(agent): 被拒绝/被跳过的工具没落 ToolResults —— call 与 result 失配
## 根因(4ab888c 的自检抓出来的)
4ab888c 在发送前加了配对自检,生产日志立刻指出真正来源:
★ dropped 1 unpaired tool message(s) before send (names: [qq_call_state])
顺着工具名查 context.json,脏帧是决定性的:
[42] tool_calls = [qq_call_state, output_send__qq]
tool_results = [output_send__qq] ← call_state 从未落盘
该帧自己的记录写了原因:qq_call_state 被 QQ 插件的来源权限策略拦下
(判定为"非所有者私聊")。
## 缺陷位置
stepToolBegin 的顺序是「先记 call,再判能不能执行」:
f.appendToolCallRecord(tc) // ← 先落 ToolCalls
if a.runStage(sdk.StageBeforeToolcall, …) {
f.Msgs 补一条 role=tool // ← 模型能看到
f.ToolIdx++
return outcomeContinue // ← 但没写 ToolResults
}
同一函数里还有一处同构分支(插件崩溃态跳过执行),同样只补 f.Msgs。
task.go 顶部的注释写着「ToolCalls 与 ToolResults 通过 CallID 配对;
两者都由 appendToolCallRecord 同步追加」—— 这个不变量在**提前返回路径**
上被违反了。
## 为什么这会变成会话级故障
ContextEvent{ToolCalls: 2, ToolResults: 1}
⇒ 渲染出 assistant(tool_calls=[c1,c2]) 但只有 c1 的结果
⇒ 上游硬拒绝(HTTP 400):
An assistant message with 'tool_calls' must be followed by tool messages
responding to each 'tool_call_id'. (insufficient tool messages…)
关键在于脏数据**持久化进 context.json**,之后每一轮都带着它,直到自检剔掉。
## 修法
新增 appendToolResultRecord(与 appendToolCallRecord 成对的公开入口),
在两处提前返回各补一行。
判定"被拒绝/被跳过"≠"没有结果":拒绝原因本身就是结果,而且模型确实
从 f.Msgs 看到了它。落 context 的那份必须与发给模型的一致。
并发路径(stepToolBatch)没有这个问题 —— runOneTool 只返回结果,
join 后的顺序收尾段无条件写 ToolResults(含被拒绝项)。
## 判据(2 条,2 次变异全部实测变红)
TestDeniedToolStillRecordsResult
注册一个真的会拒绝的 before_toolcall 钩子,跑完整一轮,
断言落进 context 的每个 call 都有 result,且拒绝原因文本一致。
变异:删掉拒绝分支那行 ⇒ "★ call call_deny_1 (denied_tool) 没有对应的
ToolResults"(实测红)
TestUnhealthyPluginSkipStillRecordsResult
钉住第二条同构分支。
变异:删掉跳过分支那行 ⇒ "★ 被跳过的工具 call_unhealthy_1 没落
ToolResults —— 同型失配,上游 400"(实测红)
两条都走真实链路(真的调 runStage / 真的置崩溃态),不是只测
appendToolResultRecord 这个函数本身。
## 顺带发现一个独立缺陷(未修,不在本次范围)
写第二条判据时发现:插件崩溃熔断**实际从不生效**。
recordCrash 标记 Unhealthy=true,但从不设置 LastReload
⇒ isHealthy 的冷却判据 time.Since(r.LastReload) > reloadCooldown
在 LastReload 为零值时恒为真(Since 是巨大的正数)
⇒ 判定"冷却期早已过去" ⇒ 立刻自愈
实测:连续 3 次 recordCrash 后 needReload=true、Unhealthy=true,
紧接着 isHealthy 返回 true。
后果:task.go:1065/1135 的「插件崩溃态跳过执行」分支实际走不到。
这条应单独修并单独钉,本提交的判据用直达状态绕过它(注释已写明原因)。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 62 包全过,0 FAIL
干净检出(worktree) build + TestDenied/TestUnhealthy 全绿
|
2026-11-04 16:24:49 +08:00 |
|
|
|
4ab888ce06
|
fix(agent): 发送前自检 tool_call 配对 —— 止损并定位上游 400
## 为什么还要修(前面两个修复都不够)
今天已经修了两处:
263b651 并发批次 stepToolBatch 漏记 ToolCalls
0ed3c89 L1 预算截断切开 tool 配对
并用生产 context.json 验证历史已干净:
53 个事件,0 个"结果多于调用"的帧
但 A2A **新会话**(a2a_1793778753015670668)仍然报同一个 400:
An assistant message with 'tool_calls' must be followed by tool messages
responding to each 'tool_call_id'. (insufficient tool messages following...)
同时日志显示 `ratio=0.28`(远低于 1.25 阈值)、`skip oversized frame` 计数 0
⇒ 既不是裁剪,也不是预算截断。
## 定位:问题在 f.Msgs,不在 L1 历史
`f.Msgs` 是**多条路径**拼出来的:
prepare 重建 buildMessages
中断恢复 rebaseFramePrefix(重建 prefix + 接回 tail)
批末附加 flushBatchAttachments
占位补全 dropContinuationPlaceholders / continuation
任何一条漏掉配对,最终都表现为同一个 400,而协议**不告诉你是哪条 call**。
所以把自检放在唯一一次组装 provider 请求的位置(stepLLM,task.go:750),
而不是散在各条路径上。
## 为什么剔 call 而不是补占位
补一条 "tool result: (missing)" 会让模型看到它**从未调用过**的结果,
可能据此继续推理 —— 那是编造事实。剔掉 call 只是少了一次尝试,
模型可以重来,语义安全。
被剔的 call 记录工具名与数量,日志里能看出是哪一轮出的问题。
## 判据(5 条,3 次变异全部实测变红)
DropsCallWithoutResult 主方向:没有结果的 call 必须剔
变异:直接 return msgs ⇒ "★ 未配对的 call c2 仍在消息里 —— 上游必然 400"
DropsOrphanToolResult 反方向:找不到 call 的孤立 tool 必须删
变异:删掉第二遍循环 ⇒ 红
DropsEmptyAssistantShell call 全剔且正文为空 ⇒ 整条删
变异:去掉该分支 ⇒ 红
KeepsContentWhenAllCallsDropped
call 全剔但**有正文**时保留正文(那是模型说的话,不能丢)
LeavesValidSequenceUntouched
合法序列必须逐条不变 —— 修正逻辑不能引入新问题
只测一个方向会放过另一半失配,所以两个方向都钉。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 62 包全过,0 FAIL
干净检出(worktree) build + TestDropUnpaired 全绿
## 下一步
这条是**止损 + 定位**,不是根因修复。部署后如果日志里出现
"dropped N unpaired tool message(s)",那 N 与工具名就指向真正漏配对的
那条路径,据此再修上游。
|
2026-11-04 15:58:22 +08:00 |
|
|
|
af17af5082
|
chore: 规划书不进仓库(与 plan.md / kernelplan.md 同惯例)
## 为什么
规划/设计在交接前会反复改。入库后有两个实际问题:
· 每改一次就是一条"改文档"提交,把真实代码提交埋掉
· 规划与实现必然漂移,读了仓库里的人会拿到过期结论
## 既有惯例
本仓早就是这样处理的,不是新立的规矩:
/plan.md 曾在 7901651「移除过程文档」被清出
/kernelplan.md 从未入库
两者都在 .gitignore 里,都只存在于工作区
## 做法
· docs/zh/dshplgadapter-plan.md 从索引移除(文件本身留在工作区)
· 挪到仓库根 dshplgadapter-plan.md,与 plan.md / kernelplan.md 同位置
· .gitignore 加 /dshplgadapter-plan.md,并写明理由
实现落地后,结论会进 docs/zh/ 下的正式设计文档 —— 那里才是该留档的地方。
## 影响
内核仓历史里 e8d07cd、82d08eb 两个提交只含这份规划书,现已从工作树移除。
如需追查当时的结论,git show e8d07cd 仍可读到(历史不重写)。
|
2026-11-04 15:54:56 +08:00 |
|
|
|
1b6cd57dca
|
fix(skillmgr): 内核旧版 skill 永不更新 —— 无 stamp 不等于"用户改过"
## 缺陷(实测,非推测)
给 kernel-plugins 补上 `skill_info(name, section=...)` 的用法说明后,
改了嵌入内容,但**生产上永远不会更新**:
/home/hologramagent/skills/kernel-plugins/ mtime 2026-10-26,无 .builtin-sha256
嵌入版本 1.1.0 vs 磁盘版本 1.0.0
`materializeBuiltinSkills` 的判断链是:
builtinSkillUnmodified(dir) → 读 .builtin-sha256
没有 stamp ⇒ false
⇒ continue("用户改过或无法确认,保留人的版本")
而 stamp 机制是 387ae94(今天早些时候)引入的。生产机上这份 skill
是**更早的内核**写的 —— 它没有 stamp,于是被永久判为"来历不明"。
后果不只是这一份文档:任何在 stamp 机制之前部署的机器,其内核自带
skill 都停在旧版。而 kernel-* 系列正是教模型怎么用工具/记忆/终端的
文档 —— 内核改进了文档,模型永远看不到。
## 修法:用 frontmatter version 区分两种"没有 stamp"
原判据把两件事混成一件:
用户改过措辞 ⇒ 必须保留
内核在 stamp 之前写的 ⇒ 应该升级
新增 builtinSkillOutdated(dir, embedded):只在**没有 stamp** 时被调用,
判据是磁盘 frontmatter 的 `version` 严格低于内嵌版本。
安全性分析(写在代码注释里):
人改措辞 通常不动 version ⇒ 不低于 ⇒ 保留 ✓
人把 version 改高 ⇒ 保留 ✓
内核旧版(无 stamp) ⇒ 升级 ✓(这正是要修的)
解析不出版本号 ⇒ 返回 false ⇒ 保留(保守)
误升级的唯一条件是"人把 version 改低",而那本身就意味着放弃内核版本。
原有的四条保守判据(含 NoStampIsConservative)全部保持绿。
## 判据抓到了我自己写的 bug
TestCompareVersion 立刻红了:
compareVersion("1.0","1.0.0")=-1,期望 0
原因:缺失段留成空串 ⇒ Atoi("") 失败 ⇒ 落到字符串比较 ⇒ `"" < "0"`
⇒ 版本号少写一段的 skill 会被无谓地重复升级。改为缺失段视为 "0"。
## 变异验证
builtinSkillOutdated 恒 return false(= 修复前行为)
⇒ TestMaterializeBuiltinSkills_UpgradesStaleKernelVersion 红:
"★ 没有 stamp 的内核旧版(version 0.0.1)没被升级"
compareVersion 缺失段改回空串
⇒ TestCompareVersion 红:"compareVersion(\"1.0\",\"1.0.0\")=-1,期望 0"
## 顺带:kernel-plugins v1.1.0
补一节「Reading a skill without burning context」,说明:
- 长文档返回目录 + 开头 30 行,目录每行带 (N 行)
- `section` 可按编号或标题子串取,大小写不敏感,容忍 `##` 前缀
- 顶层章节返回整棵子树
- 未命中会列出全部可用章节
- 章节边界尊重围栏代码块
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 62 包全过,0 FAIL
干净检出(worktree) build + skillmgr 全绿
(包数 63 → 62 是因 clawhubadapter 已在 a0077a2 迁出为外部插件,与本改动无关)
|
2026-11-04 15:50:39 +08:00 |
|
|
|
82d08ebc45
|
docs(plan): 修正 dshplgadapter 规划书的仓库引用 + 补交接须知
## 原稿的四类引用错误(交给别人会直接卡住)
① 说 clawhubadapter 在 internal/plugins/ —— 已迁出,那里没有了(a0077a2)
② 指向 /home/program/clawhubadapter-plugin/ —— 本机路径,别人打不开
③ 指向 /opt/agentmail/plugins/dsh-mail-bridge/ —— 本机生产拷贝
④ ★ 完全没提 deepseek-ai/deepseek-harness —— 漏掉唯一的 dsh 官方文档源
第 ④ 条最严重:dsh / dsh-tools / cordis 三个包的 package.json 的
repository 字段全指向那个 monorepo(公开,master,2026-11-03 仍在更新),
里面有 docs/cordis-tutorial/01..07 的**七步中文教程**、tool-execution-pipeline
(tools/execute 异步链的权威说明)、cordis-primer、api/、cookbook/ ——
正是阶段 1-3 的直接依据,原稿一条都没有。
## 现在的引用(逐个实测 HTTP 200)
github.com/JianFeeeee/HologramAgent 内核仓
github.com/JianFeeeee/hologramagentsdk SDK 仓
github.com/JianFeeeee/clawhubadapter 参考插件(原型)
github.com/deepseek-ai/deepseek-harness dsh 官方
github.com/QIKO0721/dsh-mail-bridge mail-bridge 公开镜像
只剩两处本机路径,且都写了适用条件、标明不是阻塞项
(dsh 的 ~/.dsh/profiles 固定布局;mail-bridge 的本机生产拷贝)。
## 新增 §9 交接须知
给没有本机环境、没有上下文的人:
· 9.1 需要的 6 样东西 + 哪些阻塞(只有 mail-bridge 一项依赖特定机器)
· 9.2 开工前的环境自检命令(含期望输出)
· 9.3 三条别再踩的坑(go build 不成立 / NewPluginFactory / stdout 只能 JSON)
· 9.5 一个前置决策:dsh 插件能跑任意 Node 代码 = 给本机执行权限,
必须在阶段 1 前决定,不能"先跑通再说"
· 9.6 已验证 vs 推测的诚实标注
9.6 是本轮最重要的补充。四条"推测"(裸 Context 承载插件、fd3 通道、
defineTool schema 无损转换、异步链不死锁)就是阶段 0 存在的理由 ——
不标出来,接手人会把推测当事实,直接跳到阶段 1。
## 结构校验
表格 0 处列数不一致;代码围栏 38 个(偶数,配对完整);766 行。
|
2026-11-04 15:49:40 +08:00 |
|