fix(agent): 卸载时指出该去哪删节点,而不是一律说 agents.json

agent 不持有运维令牌——节点不是运维,一个能删掉邻居的被控节点比它解决的问题更糟——所以
它不能自己从中继上摘掉自己。此前卸载的结束语对所有节点都写"the relay's agents.json still
lists this node",而自注册的节点根本不在那里:它在中继的状态文件里,要从手机上删。运维照着
那句话去翻一个没有这条记录的文件。

现在节点在登记时把来历写进配置(`selfEnrolled`,带 `--code` 为真),卸载结束语据此给出
正确的那一句:

- 自注册的 → "在中继的登记状态里,去 App 的「中继节点」移除"
- 手工登记的 → "在 agents.json 里,删掉那条并重启中继"
- 配置读不出来 → 两句都提,因为读不到不是让卸载失败的理由

测试两项:卸载打印的是它被告知的那一句(且不再对自注册节点说 agents.json)、登记时正确
记录来历(带码为真、不带码不写这个字段)。

Go 全包、Node 141 项、Swift 121 项通过。
This commit is contained in:
chick
2026-09-17 19:12:32 +08:00
parent 934760f8e2
commit 7d174a0261
6 changed files with 96 additions and 1 deletions
+8 -1
View File
@@ -81,6 +81,10 @@ type Options struct {
// NotifyRelay retires this node's device bearers at the relay, and is called before anything is
// removed. Nil skips it, which is right for a node that was never paired.
NotifyRelay func() error
// RemovalHint is the sentence printed at the end telling the operator what is left at the relay.
// It depends on how the node got there — self-enrolled or written into agents.json — and the caller
// is what knows that, from the config the node wrote at enrollment time.
RemovalHint string
Runner Runner
Log func(format string, args ...any)
}
@@ -164,7 +168,10 @@ func Uninstall(opts Options) error {
opts.Log("dry run: nothing was removed")
} else {
opts.Log("msa-agent removed")
opts.Log("the relay's agents.json still lists this node; remove that entry unless you are reinstalling")
// The one thing this command cannot do for the operator, and it cannot by design: the agent holds
// no admin token, because a node is not an operator, and a compromised node that could remove its
// neighbours would be a worse problem than the one this hint solves.
opts.Log("%s", opts.RemovalHint)
}
return nil
}
+25
View File
@@ -401,3 +401,28 @@ func TestInstallDryRunReportsPreconditionsInsteadOfRefusing(t *testing.T) {
t.Fatalf("the dry run printed no plan:\n%s", output)
}
}
// The hint at the end has to name the right place, because the two records live in different places and
// are removed by different people. Telling the operator the wrong one sends them to the wrong place.
func TestTheRemovalHintIsTheOneItWasGiven(t *testing.T) {
paths := installed(t)
var lines []string
err := Uninstall(Options{
Paths: paths,
Runner: &recorder{},
Log: func(format string, args ...any) { lines = append(lines, fmt.Sprintf(format, args...)) },
RemovalHint: "the relay still lists this node in its enrollment state; remove it in the app",
})
if err != nil {
t.Fatalf("Uninstall: %v", err)
}
output := strings.Join(lines, "\n")
if !strings.Contains(output, "remove it in the app") {
t.Fatalf("the hint was not printed:\n%s", output)
}
// And the old hard-coded sentence is gone: it named agents.json for every node, including the
// self-enrolled ones that are not there.
if strings.Contains(output, "agents.json still lists this node") {
t.Fatalf("the uninstall still claims every node is in agents.json:\n%s", output)
}
}