A tool runner is no longer restarted by a build of the engine: owned paths name the directory of every build, and a restart names its cause (issue 1966)

The tool runner's unit carries the paths modules own, for the holder of node-files. The engine's own
archive is unpacked into a place named for its build, so every engine replacement changed that word
and restarted the tool runner with nothing of it changed (novox 21:16, 22:25, 22:38 on 2026-11-09).
A resource unpacked into a place named for its build now owns the directory holding every build.
The restart line now names the unit's directive, word and key that differ, never a value.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-09 22:49:20 +01:00
parent 6e461bd24a
commit bcd815a6bb
4 changed files with 199 additions and 1 deletions
+45
View File
@@ -357,6 +357,51 @@ func TestAProcessAppliedAgainIsUnchangedAndNotRestarted(t *testing.T) {
if o := outcomeOf(third, "node-tools.runtime"); o.Action != "unchanged" {
t.Errorf("third apply: %+v, want unchanged", o)
}
// An input of its unit that differs restarts it, and the line says which (issue 1966).
changed := declare(t, `{"id":"node-tools.runtime","type":"process","name":"node-tools","source":"`+serving(t, body)+
`","digest":"`+digest+`","run":["node","main.js"],"env":{"MESH_TOOL_MODULES":"a=/x/index.js,b=/y/index.js"}}`)
fourth, _, err := Apply(context.Background(), archHost(t), changed, state, store.OriginDeclared, run, nil, nil)
if err != nil {
t.Fatal(err)
}
if o := outcomeOf(fourth, "node-tools.runtime"); o.Action != "updated" || !strings.Contains(o.Detail, "its unit's Environment MESH_TOOL_MODULES (b)") {
t.Errorf("fourth apply: %+v, want updated and the word and entry that changed named", o)
}
}
// A restart nobody asked for names its cause (issue 1966): the unit's own words that differ, the key inside a word that
// is a JSON object and the entry of a list, never a value.
func TestAProcessRestartedForAnInputSaysWhichInputChanged(t *testing.T) {
for _, row := range []struct {
name, was, now, says string
}{
{"a word of a module inside a JSON word", `{"files":{"OWNED":"/a"},"mail":{"X":"1"}}`, `{"files":{"OWNED":"/b"},"mail":{"X":"1"}}`,
"Environment TOOL_ENV (files (OWNED))"},
{"an entry of a list", "a=/x/1,b=/y/2", "a=/x/1,b=/y/3", "Environment TOOL_MODULES (b)"},
{"nothing changed", "a=/x/1", "a=/x/1", ""},
} {
t.Run(row.name, func(t *testing.T) {
word := "TOOL_MODULES"
if strings.HasPrefix(row.was, "{") {
word = "TOOL_ENV"
}
mk := func(v string) string {
r := aProcess()
r.Env = map[string]string{word: v, "SECRET": "never-said-" + v}
return "sha256:" + strings.Repeat("a", 64) + " " + unitFor(r)
}
got := unitDifference(mk(row.was), mk(row.now))
if row.says == "" {
if got != "" {
t.Fatalf("said %q for nothing changed", got)
}
return
}
if !strings.Contains(got, row.says) || strings.Contains(got, "never-said") || strings.Contains(got, "/b") {
t.Fatalf("said %q, want it to name %q and no value", got, row.says)
}
})
}
}
// novox/hq ADR 0193: a bundle compiled to a binary runs itself — `./name` is that file in the