ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

WuKongIM 移除旧版 v1 Runtime:Legacy 代码树清理实施计划与落地源码验证

WuKongIM 移除旧版 v1 Runtime:Legacy 代码树清理实施计划与落地源码验证
  • 即时通讯
  • 后端

【免费下载链接】WuKongIM

More than just IM 不只是即时通讯(IM)

项目地址:https://gitcode.com/gh_mirrors/wu/WuKongIM
点击查看免费下载

本文基于仓库中的实施计划文档 docs/superpowers/plans/2026-07-05-remove-legacy-code.md,并结合当前仓库源码逐一核验落地结果。它完整还原了 WuKongIM 将旧版 v1 runtime 代码(internal/legacy、pkg/legacy、test/legacy)从仓库中安全移除的整套方法论:从测试依赖替换、文档边界更新,到目录删除、引用清扫与多级验证。读者读完可以掌握"在大型 Go 单体仓库中安全退役历史代码树"的完整实操流程:如何界定删除范围、如何先解除测试对旧包的依赖、如何用 import-boundary 测试与文档规则守住"不得重建 legacy"的长期不变量,以及如何用分层go test验证矩阵和分批提交策略控制风险。

WuKongIM 是一个"不止于即时通讯"的开源项目,其代码库在演进中经历了 v1 → v2 的运行时重构。当被提升(promoted)的 v2 代码已经稳定落在规范路径上之后,残留的旧 v1 代码树就成为历史包袱:它们不再被生产代码引用,却仍然被部分测试、文档和目录结构约定所纠缠。本文要解决的核心问题就是:如何干净、彻底、可回滚地移除这些 legacy 代码树,同时保证对外契约(HTTP/API 兼容、Prometheus 指标名)不受影响。


一、任务目标与架构前提

实施计划的核心目标(Goal)非常明确:

移除旧的 v1 runtime 代码树,因为被提升的 v2 代码已经运行在规范路径(canonical paths)上。

所谓"规范路径",是本次清理行动的架构锚点(Architecture):

  • cmd/wukongim—— 服务入口
  • internal—— 内部产品内核(app / access / usecase / runtime / infra)
  • pkg/channel、pkg/cluster、pkg/controller、pkg/transport—— 被提升的公共能力包

执行顺序被严格划分为三个阶段:

  1. 先移除活跃测试中对 legacy 包的非生产引用;
  2. 然后更新边界文档(FLOW 文档)与守护测试(import-boundary 测试);
  3. 最后删除internal/legacy、pkg/legacy、test/legacy三个目录树。

技术栈与约束工具为:Go、go test、仓库 FLOW 文档、AGENTS.md、包级 import-boundary 测试。这本质上是一套"代码迁移 + 契约固化"的组合拳:先用测试把依赖摘干净,再让文档和守卫测试变成永久的"禁止回归"机制,最后才动刀删除。

落地核验:入口已切换到规范路径

计划中的关键证据之一是cmd/wukongim/main.go导入的是internal/app而非internal/legacy。核验当前仓库 cmd/wukongim/main.go 第 12 行,确实为:

"github.com/WuKongIM/WuKongIM/internal/app"

这证明生产入口早已完成 v2 化,legacy 树对生产代码而言已经"无依无靠",只差物理删除。


二、清理前置证据:当前状态审计

在动手之前,计划先做了严格的现状审计(Current Evidence),确保"删得掉、删得对":

1. 聚焦 preflight 测试已通过(计划原文给出的两条命令):

GOWORK=off GOCACHE=/private/tmp/wukongim-go-build-cache go test ./cmd/wukongim -run 'TestImportBoundaryUsesCanonicalInternalApp|TestDependencyBoundaryUsesCanonical|TestDependencyBoundaryDoesNotReachLegacyBenchInternal' -count=1
GOWORK=off GOCACHE=/private/tmp/wukongim-go-build-cache go test ./scripts -run TestPromotedProductionCodeDoesNotImportLegacyPackages -count=1

GOWORK=off用于禁用 Go workspace 聚合,保证测试针对仓库自身模块;GOCACHE指向独立缓存目录,避免共享缓存干扰;-run用正则只跑与 import-boundary 相关的聚焦用例。

2. 非 legacy 目录下的生产代码不再引用旧包:

  • 除internal/legacy、pkg/legacy、test/legacy之外的非测试生产文件,均不导入github.com/WuKongIM/WuKongIM/internal/legacy或github.com/WuKongIM/WuKongIM/pkg/legacy。

3. 剩余的硬引用集中在测试与文档,是本次清理的全部作战目标:

残留引用位置引用内容清理方式
cmd/wkdb/main_test.go仅为了HashSlotForKey导入pkg/legacy/cluster替换为pkg/hashslot
pkg/slot/proxy系列测试把pkg/legacy/cluster、pkg/legacy/transport当作测试 harness替换为本地确定性测试集群
scripts/package_promotion_docs_test.go仍断言 legacy 目录和 slot-proxy fallback 文档存在改为断言"stale 目录必须不存在"
AGENTS.md、internal/FLOW.md、pkg/slot/FLOW.md、pkg/cluster/FLOW.md、pkg/client/FLOW.md、docs/development/PROJECT_KNOWLEDGE.md仍描述 legacy 保留策略改为"已删除 / 规范路径"措辞

说明:计划文档撰写于 2026-07-05,其中提到的cmd/wkdb、internal/FLOW.md等路径在当前仓库中已经历后续演进(如cmd/wkdb目录已不存在)。这属于正常迭代,不影响本文所述清理方法论本身的成立;所有不变量(legacy 不得重建、hash-slot 统一由pkg/hashslot提供、slot-proxy RPC 统一走pkg/cluster.Node.RegisterRPC)均已由当前代码固化。


三、范围界定:删什么、不删什么

任何大删除最怕误伤,计划用一份明确的 Scope 划出红线:

删除(Delete):

  • internal/legacy/
  • pkg/legacy/
  • test/legacy/

本轮不删除、不重命名(Do not delete or rename in this pass):

  1. 由被提升代码实现的 legacy-compatible 公共 HTTP/API 契约——对外协议是兼容面,与旧 Go 源码树无关;
  2. docs/superpowers/reports/下的历史报告——它们是有价值的演进记录,只更新"活跃文档",不触碰历史档案;
  3. Prometheus 指标名(如wukongim_channelv2_*)——这是外部可观测性契约,删除指标名会破坏监控面板和告警,不属于旧 Go 源码树的范畴;
  4. 未跟踪的用户草稿docs/superpowers/plans/2026-07-05-legacy-app-slot-store-adapter.md——不在本次任务范围内。

这个边界原则可以提炼成一句话:"删除的是旧的实现代码树,保留的是对外可观测、可兼容的契约面。"


四、Task 1:摘除最简单的 legacy 测试导入(wkdb hash-slot helper)

Step 1:替换导入

cmd/wkdb/main_test.go中唯一用到pkg/legacy/cluster的地方是 hash-slot 辅助函数。替换方式:

// 替换前 "github.com/WuKongIM/WuKongIM/pkg/legacy/cluster" // 替换后 "github.com/WuKongIM/WuKongIM/pkg/hashslot"

函数调用同样替换:

// 替换前 cluster.HashSlotForKey("u1", 16) // 替换后 hashslot.HashSlotForKey("u1", 16)

这里的关键洞察是:测试只是"借用"了 legacy 包里的一个纯函数,而不是依赖其运行时。HashSlotForKey的本质就是crc32.ChecksumIEEE([]byte(key)) % hashSlotCount,在 pkg/hashslot/hashslottable.go 中可以找到这个 canonical 实现:

func HashSlotForKey(key string, hashSlotCount uint16) uint16 { if hashSlotCount == 0 { return 0 } return uint16(crc32.ChecksumIEEE([]byte(key)) % uint32(hashSlotCount)) }

把这类纯函数从 legacy 包"平迁"到pkg/hashslot,是典型的"先摘依赖、后删目录"手法:被迁移的旧代码树往往藏着少量被测试依赖的纯工具函数,先把它们提升到规范包,测试就能与旧树解耦。

Step 2:验证

GOWORK=off GOCACHE=/private/tmp/wukongim-go-build-cache go test ./cmd/wkdb -count=1

期望结果:PASS。

Step 3:保留字符串级 import-boundary 守卫

计划特别强调:不要因为 legacy 包已不存在,就顺手删掉边界测试里的 legacy 路径字符串。这些字符串守卫(如TestPromotedProductionCodeDoesNotImportLegacyPackages)的价值恰恰在于:即使包被删除了,它们仍然在防回归——任何试图重新引入pkg/legacy/*导入的代码都会立刻被测试拦截。

核验当前仓库 cmd/wukongim/main_test.go,可以清楚看到这类守卫仍然活跃:测试以github.com/WuKongIM/WuKongIM/internal/legacy/、github.com/WuKongIM/WuKongIM/pkg/legacy/cluster、pkg/legacy/controller、pkg/legacy/channel、pkg/legacy/transport等前缀为禁用集,一旦依赖闭包中出现就t.Fatalf。也就是说:目录可以被删除,但"禁止引用 legacy"的哨兵永远不会删除。

补充原则:只有"断言 legacy 目录必须存在"的测试才需要更新;只含 legacy 路径字符串的守卫测试一律保留。


五、Task 2:从 pkg/slot/proxy 测试中移除 legacy harness

这是整个计划中最有技术含量的一步。pkg/slot/proxy(Slot 元数据代理)的单元测试此前把pkg/legacy/cluster和pkg/legacy/transport当作**测试夹具(harness)**使用,而不是被测对象本身。

涉及文件清单

  • pkg/slot/proxy/testutil_test.go
  • pkg/slot/proxy/integration_test.go
  • pkg/slot/proxy/authoritative_rpc_test.go
  • pkg/slot/proxy/channel_migration_rpc_test.go
  • pkg/slot/proxy/plugin_binding_rpc_test.go
  • pkg/slot/proxy/channel_runtime_meta_page_integration_test.go
  • pkg/slot/proxy/channel_page_integration_test.go
  • pkg/slot/proxy/user_page_integration_test.go
  • pkg/slot/proxy/rpc_hashslot_fallback_test.go
  • pkg/slot/proxy/cmd_conversation_state_rpc_test.go

注:这些是计划撰写时点的文件清单;当前仓库中pkg/slot/proxy仍保留authoritative_rpc_test.go、testutil_test.go、rpc_hashslot_fallback_test.go、plugin_binding_rpc_test.go、channel_migration_rpc_test.go等文件(部分集成测试文件在后续演进中发生了合并/重命名),核心替换逻辑保持一致。

Step 1:引入确定性的本地测试集群

将基于raftcluster.Cluster的测试 harness 替换为一个本地测试类型,它实现Cluster接口,并额外实现hashslot_compat.go中定义的可选 proposer 接口。

计划为这个本地集群规定了完整的行为契约:

方法要求行为
SlotIDs()返回配置的物理 Slot 列表
SlotForKey(key)通过HashSlotForKey(key)和本地hashSlotToSlot映射表定位物理 Slot
HashSlotForKey(key)使用pkg/hashslot.HashSlotForKey(key, hashSlotCount)
HashSlotsOf(slotID)返回分配给该物理 Slot 的逻辑 hash slot
HashSlotTableVersion()返回非零的测试修订号
LeaderOf(slotID)返回配置的 leader,否则返回ErrNoLeader
IsLocal(nodeID)与本地节点 ID 比较
PeersForSlot(slotID)返回配置的 peer 集合
RPCService(ctx, nodeID, slotID, serviceID, payload)分发到目标测试节点注册的StoreRPC handler
ProposeWithHashSlot(ctx, slotID, hashSlot, cmd)把 Slot FSM 命令应用到该 hash slot 的权威测试节点 DB
ProposeLocalWithHashSlot(ctx, slotID, hashSlot, cmd)仅当本地节点是配置的 leader 时应用,否则返回ErrNotLeader
ProposeWithHashSlotResult(ctx, slotID, hashSlot, cmd)在需要命令输出的测试中返回 Slot FSM 的 apply 结果

计划特意强调这个设计意图:这是pkg/slot/proxy的专用 harness,而不是被提升的pkg/cluster.Node的 harness。原因很实在——pkg/cluster.Node已经有自己的 slot-proxy 端口测试,而pkg/slot/proxy的测试需要直接访问它播种并断言的metadb.DB分片。测试夹具的抽象层级必须匹配被测单元的边界,而不是照搬生产集群的复杂度。

核验当前仓库 pkg/slot/proxy/hashslot_compat.go,可以看到这套接口契约的真实落地形态:

type hashSlotKeyer interface { HashSlotForKey(string) uint16 } type hashSlotProposer interface { ProposeWithHashSlot(context.Context, multiraft.SlotID, uint16, []byte) error } type hashSlotResultProposer interface { ProposeWithHashSlotResult(context.Context, multiraft.SlotID, uint16, []byte) ([]byte, error) } type localHashSlotProposer interface { ProposeLocalWithHashSlot(context.Context, multiraft.SlotID, uint16, []byte) error }

而 pkg/slot/proxy/testutil_test.go 当前的导入列表只包含pkg/hashslot、pkg/raftlog、pkg/slot/fsm、pkg/slot/multiraft等规范包,已经没有任何pkg/legacy痕迹——这正是 Task 2 执行完毕的直接证据。

Step 2:替换两节点测试 helper

计划要求尽量保留既有 helper 名称,仅替换内部实现:

  • startTwoNodeShardedStores(t):创建两个测试节点、两个 DB、物理 Slot1与2,leader 分别为1 -> node1、2 -> node2;
  • startTwoNodeHashSlotStores(t, hashSlotCount):创建同样的两节点,但带一张确定性的逻辑 hash-slot 表(按 slot 均分);
  • testStoreNode.cluster字段改为新的本地集群类型(或 helper 可接受的接口),不再使用*raftcluster.Cluster;
  • newLegacyTestStore更名为newProxyTestStore,通过Store.RegisterRPCHandlers把 RPC handler 注册进本地测试集群的 handler map。

名称从newLegacyTestStore到newProxyTestStore的变更虽小,却是一个重要的信号:测试工具的名称不该携带被删除代码的标记。

Step 3:替换 legacy 错误断言

proxy 测试中针对raftcluster的错误哨兵需要整体迁移到pkg/slot/proxy自身的错误定义:

// 替换前 raftcluster.ErrNoLeader raftcluster.ErrNotLeader raftcluster.ErrSlotNotFound // 替换后 ErrNoLeader ErrNotLeader ErrSlotNotFound

推荐使用errors.Is(err, ErrNoLeader)风格的断言,以获得最大的兼容性。核验当前仓库 pkg/slot/proxy/errors.go,可以看到这三个哨兵已经以routeError类型落地,并且自带别名兼容机制:

var ( ErrNoLeader = &routeError{ message: "slot/proxy: no leader", aliases: []string{ "cluster: no slot leader", "cluster/routing: no slot leader", }, } ErrNotLeader = &routeError{ message: "slot/proxy: not leader", aliases: []string{ "cluster: not leader", "cluster/propose: not leader", }, } ErrSlotNotFound = &routeError{ message: "slot/proxy: slot not found", aliases: []string{ "cluster: slot not found", }, } )

routeError.Is(target)会同时匹配自身 message 与历史别名,配合routeErrorMatches/containsRouteAlias做子串级兜底。这个设计保证了旧集群返回的错误文本仍能被新哨兵识别——错误契约在迁移过程中不断裂,这比简单替换哨兵变量名更深一层。

Step 4:替换 hash-slot helper

// 替换前 raftcluster.HashSlotForKey(uid, count) // 替换后 hashslot.HashSlotForKey(uid, count)

Step 5:清除 legacy 导入

用 ripgrep 做包级扫描,期望零匹配:

rg -n 'github\.com/WuKongIM/WuKongIM/pkg/legacy|raftcluster|legacytransport' pkg/slot/proxy

计划允许一种例外:注释中解释被删除历史时可以保留文字,但更推荐零匹配——历史解释交给docs/superpowers/reports/这类档案,活跃代码里不留旧词。

Step 6:验证

GOWORK=off GOCACHE=/private/tmp/wukongim-go-build-cache go test ./pkg/slot/proxy -count=1

期望:PASS。如果被修改的文件带//go:build integration构建标签,则在单元包变绿后追加聚焦集成验证:

GOWORK=off GOCACHE=/private/tmp/wukongim-go-build-cache go test -tags=integration ./pkg/slot/proxy -count=1

期望:PASS(仅当确有 integration 标签文件被改动时才需要执行)。


六、Task 3:更新边界测试与文档

删除代码树之前,必须先让"文档和守卫测试"与新世界保持一致,否则删除后文档会指向不存在的路径,守卫测试也会因断言目录存在而失败。

Step 1:更新 package promotion 文档测试

scripts/package_promotion_docs_test.go需要三处调整:

  1. 从"必须存在的目录"清单中移除pkg/legacy/channel、pkg/legacy/cluster、pkg/legacy/controller、pkg/legacy/transport;
  2. 把internal/legacy、pkg/legacy、test/legacy加入"必须不存在的 stale 目录"清单;
  3. 把TestSlotProxyLegacyFallbackIsDocumented替换为 promoted-only 检查:验证pkg/slot/FLOW.md和docs/development/PROJECT_KNOWLEDGE.md提到pkg/cluster.Node.RegisterRPC是 slot-proxy RPC 的唯一注册路径。

核验当前仓库 scripts/package_promotion_docs_test.go 第 77-85 行,可以看到这条守卫已经反转落地:

for _, staleDir := range []string{ "internal/legacy", "pkg/legacy", "test/legacy", } { if _, err := os.Stat(filepath.Join(root, staleDir)); err == nil { t.Fatalf("stale legacy directory %q should not exist", staleDir) } }

同文件第 176-179 行还固化了 Go 导入禁用前缀:

forbiddenPrefixes := []string{ "github.com/WuKongIM/WuKongIM/internal/legacy", "github.com/WuKongIM/WuKongIM/pkg/legacy", }

并配套TestPromotedProductionCodeShouldNotImportLegacyPackages(扫描cmd/wukongim、internal、pkg、test/e2e下所有非测试.go文件),扫描时对internal/legacy、pkg/legacy目录直接SkipDir——既不误伤(目录已删)也不放过(前缀仍在黑名单)。

Step 2:更新 AGENTS.md 分层规则

从目录树中移除以下条目:

  • internal/legacy/
  • pkg/legacy/cluster/
  • pkg/legacy/channel/
  • pkg/legacy/controller/
  • pkg/legacy/transport/
  • test/legacy/e2e/

删除旧层规则:

`internal/legacy/*` 是旧 v1 server runtime 保留区;新增或迁移能力不要依赖 legacy。

替换为简洁的新规则:

旧 v1 runtime 已删除;新增代码不得重新引入 `internal/legacy` 或 `pkg/legacy/*`。

核验当前仓库 AGENTS.md 第 78 行,这条规则以英文固化在代码质量约束中:

- New code MUST NOT recreate `internal/legacy` or `pkg/legacy/*`.

AGENTS.md是仓库给 Agent/开发者的"宪法",把删除决定写进层规则,等于把一次性清理升级为长期架构约定。

Step 3:更新 FLOW 文档

四个 FLOW 文档各有精确的措辞替换:

文档修改要点
internal/FLOW.md删除"旧 v1 runtime 保留在internal/legacy"的陈述;legacy-compatible措辞仅在描述公共 HTTP 兼容性时保留
pkg/slot/FLOW.md删除internal/legacy/app/slot_proxy_rpc.gofallback 语句;只保留被提升的pkg/cluster.Node.RegisterRPC桥接作为唯一注册路径
pkg/cluster/FLOW.md将"帮助 Slot 元数据代理调用方迁移出pkg/legacy/cluster的过渡桥"改为"被pkg/slot/proxy使用的 canonical 兼容端口"
pkg/client/FLOW.md从 helper 使用说明中移除test/legacy/e2e引用

核验当前仓库 pkg/slot/FLOW.md 第 21-23 行的"Boundaries"段落,措辞已经完全切换为规范路径:

- Slot proxy handlers register through the promoted `pkg/cluster.Node.RegisterRPC` bridge; they do not construct a second cluster transport or routing table.

而 pkg/cluster/node_slot_proxy_port.go 第 174 行正是该桥接的实现点:

n.RegisterRPC(serviceID, slotProxyRPCHandlerFunc(handler))

配合 pkg/slot/proxy/promoted_cluster.go 中的promotedRPCRegistrar接口与 pkg/slot/proxy/rpc_registration.go 的Store.RegisterRPCHandlers,形成了完整的"Store 注册 → Node 端口桥接"闭环。文档不再描述历史,只描述当前唯一路径。

Step 4:更新项目知识库

docs/development/PROJECT_KNOWLEDGE.md需要记录清理完成的事实:

  • "former v1 server runtime lives underinternal/legacy" → "former v1 server runtime has been removed";
  • "old implementations live underpkg/legacy/*" → "oldpkg/legacy/*implementations have been removed";
  • 移除对internal/legacy/app/slot_proxy_rpc.go的引用;
  • 保留wukongim_channelv2_*等 Prometheus 原始指标名作为外部兼容别名的说明。

Step 5:验证文档测试

GOWORK=off GOCACHE=/private/tmp/wukongim-go-build-cache go test ./scripts -count=1

期望:PASS。


七、Task 4:物理删除 legacy 代码树

当测试与文档都"无 legacy 依赖"之后,才轮到最终的物理删除:

Step 1:确认受跟踪的删除范围

git ls-files internal/legacy pkg/legacy test/legacy | wc -l

计划记录的执行前证据为933 个受跟踪文件——这是一个规模不小的代码树,印证了删除必须分步进行、而不是一把梭。

Step 2:删除受跟踪文件

git rm -r -- internal/legacy pkg/legacy test/legacy

期望结果:Git 为旧 runtime 树暂存删除操作。git rm -r --的--防止路径被误解析为选项,这是对含特殊字符路径的稳妥写法。

Step 3:检查删除树下的未跟踪残留

git status --short internal/legacy pkg/legacy test/legacy

如果删除树内部残留未跟踪的.DS_Store、.log、.test或临时文件,作为删除清理的一部分一并处理;绝不越界删除这三棵树之外的无关未跟踪文件。这个"只清理范围内残留"的约束与 Scope 红线一脉相承。

核验当前仓库:internal/legacy、pkg/legacy、test/legacy三个目录均已不存在,git工作区中已无对应路径,Task 4 已完整落地。


八、Task 5:Stale 引用清扫

删除之后,要用三道扫描确保"尸骨无存":

Step 1:严格 Go 导入扫描

rg -n 'github\.com/WuKongIM/WuKongIM/(internal/legacy|pkg/legacy)' --glob '*.go'

期望:零匹配。

Step 2:严格活跃文档路径扫描

rg -n '\binternal/legacy\b|\bpkg/legacy\b|\btest/legacy\b|legacy_e2e' AGENTS.md internal pkg cmd scripts docs/development docs/wiki docker test

期望:

  • 对已删除的源码/测试目录零引用;
  • legacy-compatible措辞在描述公共 API 兼容性时可以保留;
  • wukongim_channelv2_*指标名可以保留(不属于本次删除范围)。

核验当前仓库,AGENTS.md 中唯一的legacy目录引用正是禁止性规则("MUST NOT recreate"),而docs/development/PROJECT_KNOWLEDGE.md中保留的legacy字样均属于消息格式兼容、历史指标别名等语义化用途,而非目录路径引用——符合计划的预期语义。

Step 3:目录缺席检查

test ! -e internal/legacy && test ! -e pkg/legacy && test ! -e test/legacy

期望:退出码 0。test ! -e是 POSIX 标准的存在性否定检查,三个目录都不存在才算通过。


九、Task 6:验证矩阵

计划为这次清理设计了一套由窄到宽的验证矩阵,先跑窄检查、后跑全量:

GOWORK=off GOCACHE=/private/tmp/wukongim-go-build-cache go test ./cmd/wukongim ./cmd/wkdb ./cmd/wkbench ./scripts -count=1
GOWORK=off GOCACHE=/private/tmp/wukongim-go-build-cache go test ./pkg/slot/proxy ./pkg/cluster ./pkg/db/message ./pkg/db/transfer ./pkg/slot/fsm -count=1
GOWORK=off GOCACHE=/private/tmp/wukongim-go-build-cache go test ./internal/... -count=1

然后跑全量单元套件:

GOWORK=off GOCACHE=/private/tmp/wukongim-go-build-cache go test ./... -count=1

期望:全部 PASS。

默认不跑全量集成测试,只有当被改动的 integration 标签 proxy 测试确实需要证明时,才追加聚焦集成:

GOWORK=off GOCACHE=/private/tmp/wukongim-go-build-cache go test -tags=integration ./pkg/slot/proxy -count=1

这个矩阵体现了三个原则:先窄后宽(改动包先行)、默认不重(集成测试按需触发)、分层兜底(窄层失败先定位,不盲目动全量)。


十、Task 7:提交策略——三次提交保持可回滚

为了评审和回滚干净,计划把整个改动拆成三个语义独立的提交:

提交消息内容
1test: remove legacy package dependencies from active testscmd/wkdb/main_test.go、pkg/slot/proxy/*_test.go
2docs: mark legacy runtime removedAGENTS.md、FLOW 文档、docs/development/PROJECT_KNOWLEDGE.md、scripts/package_promotion_docs_test.go
3chore: delete legacy runtime treesinternal/legacy、pkg/legacy、test/legacy

每次提交前执行:

git diff --check

期望:无空白错误。这个三段式提交让每次回滚都"颗粒度合理":测试依赖解除、文档契约更新、物理删除各自独立成 commit,评审者可以逐层确认,回滚也可以只回滚删除而保留契约。


十一、风险与控制

计划最后给出了四组风险及其控制手段,这些是大型仓库清理中反复踩坑的经验沉淀:

  1. 测试变得"太假"的风险:pkg/slot/proxy的本地 harness 替代raftcluster后,覆盖率可能意外失真。控制手段:保留所有既有行为断言,并用pkg/cluster现有的node_slot_proxy_port测试与默认集群测试作为Node的真实覆盖来源——本地 harness 只负责 proxy 单元边界,真实集群行为由 pkg/cluster 的正式测试兜底。核验当前仓库确实存在 pkg/cluster/node_slot_proxy_port_test.go 与slot_proxy_port_external_test.go。

  2. 误删历史的风险:文档删得太激进会抹掉有用演进记录。控制手段:只更新活跃文档,docs/superpowers/reports/历史报告一律不动。

  3. 过度删除legacy一词的风险:把legacy这个词从全仓库抹掉是错的——公共 API 兼容性措辞可以保留,要删除的只是已删除实现路径的引用。这个边界在 Task 5 的扫描规则中体现得最清楚。

  4. 全量测试偶发 flake 的风险:go test ./...可能暴露无关的偶发失败。控制手段:先聚焦重跑失败的包,再决定是否动代码——不要因为无关 flake 去"顺手修复"而扩大变更面。


十二、落地全景:从计划到当前仓库的闭环

回看整个计划,它展示了一条清晰的"代码退役流水线":

审计残留引用 → 摘除测试依赖(纯函数提升+本地harness) → 更新文档与守卫测试 → 物理删除 → stale扫描 → 分级验证 → 分批提交

而当前仓库的源码状态,为每一步都提供了可验证的落地证据:

  • 入口:cmd/wukongim/main.go 只导入internal/app;
  • hash-slot 唯一来源:pkg/hashslot/hashslottable.go 提供HashSlotForKey;
  • proxy 本地错误契约:pkg/slot/proxy/errors.go 定义ErrNoLeader/ErrNotLeader/ErrSlotNotFound并兼容旧错误文本;
  • proxy 可选接口:pkg/slot/proxy/hashslot_compat.go 定义 hash-slot keyer 与三类 proposer;
  • RPC 注册唯一路径:pkg/cluster/node_slot_proxy_port.go 的n.RegisterRPC(...),并由 pkg/slot/FLOW.md 固化描述;
  • 禁止回归守卫:AGENTS.md 的 "MUST NOT recreate" 规则、scripts/package_promotion_docs_test.go 的 stale 目录断言、cmd/wukongim/main_test.go 的 legacy 前缀黑名单;
  • 目录物理状态:internal/legacy、pkg/legacy、test/legacy全部不存在。

这套方法论的价值超越 WuKongIM 本身:任何经历大版本重构的 Go 项目,都可以复用"先摘测试依赖、再改契约文档、最后删目录、并以守卫测试固化不变量"的顺序,在不破坏对外契约的前提下完成历史代码树的干净退役。对于想深入了解的读者,原始实施计划全文位于 docs/superpowers/plans/2026-07-05-remove-legacy-code.md,配套的长期项目知识可查阅 docs/development/PROJECT_KNOWLEDGE.md。

  • 即时通讯
  • 后端

【免费下载链接】WuKongIM

More than just IM 不只是即时通讯(IM)

项目地址:https://gitcode.com/gh_mirrors/wu/WuKongIM
点击查看免费下载

相关推荐

上一篇:CANN ops-math 极坐标 Polar 算子(aclnnPolar)自测报告深度解析:从 l0 拼接基线到 Ascend C 单 kernel 融合
下一篇:TiXL Blur 算子完全指南:图像模糊后处理原理、参数详解与实战配置

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进