mirror of
https://github.com/docker/docker-agent.git
synced 2026-11-04 10:02:15 +00:00
Lands two complementary pieces of the eventual file-watcher hot-reload:
1) The diff machinery (pkg/teamloader/diff.go) that classifies a
freshly-loaded latest.Toolset slice against the running one as
Added / Removed / Changed / Unchanged. ToolsetIdentity gives each
entry a stable key (Type+Name when named, else Type+ref/cmd/url),
and ToolsetSignature is a hex SHA-256 over the full Toolset
JSON so adding new fields automatically participates in change
detection without further wiring. HasChanges() is a fast-path for
the future watcher.
2) An immediately-useful capability built on the supervisor: by-name
restart. Most users hit a 'gopls is stuck' or 'I just OAuth'd this
MCP and need it to reconnect' moment within minutes of using the
agent; today they have to restart the whole agent.
New tools.Restartable interface (Restart(ctx) error). Implemented
by mcp.Toolset and *builtin.LSPTool, both of which delegate to
supervisor.RestartAndWait under their respective timeouts.
Runtime gains RestartToolset(ctx, name); the remote runtime
refuses with a clear error since toolset lifecycle is server-side
for that case. App is a thin pass-through.
TUI: new RestartToolsetMsg, /toolset-restart slash command
('/toolset-restart gopls'), and a notification-toast handler that
surfaces success or the supervisor's error.
Tests:
* diff_test.go covers no-changes, added-only, removed-only,
changed-only, mixed (preserves order), identity matching for
named/unnamed toolsets across mcp/lsp/a2a, and signature
stability/discrimination.
* restart_test.go exercises name resolution and Restartable
dispatch in isolation.
Note on full hot-reload (file watcher + diff-driven apply):
Wiring the watcher into the agent so that Stop+Start of a single
toolset works in a live runtime is non-trivial (Agent.toolsets
needs a snapshot pattern, the tool dispatcher needs to swap
references atomically, and the runtime loop needs a quiescent
point). It is intentionally not in this commit. The pieces shipped
here \u2014 diff machinery + per-toolset restart capability \u2014 are the
primitives that flow needs and are independently useful.
Assisted-By: docker-agent
118 lines
3.6 KiB
Go
118 lines
3.6 KiB
Go
package teamloader
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/docker/docker-agent/pkg/config/latest"
|
|
)
|
|
|
|
func mcpCmd(name, command string) latest.Toolset {
|
|
return latest.Toolset{Type: "mcp", Name: name, Command: command}
|
|
}
|
|
|
|
func TestDiffToolsets_NoChanges(t *testing.T) {
|
|
t.Parallel()
|
|
a := mcpCmd("a", "x")
|
|
b := mcpCmd("b", "y")
|
|
diff := DiffToolsets([]latest.Toolset{a, b}, []latest.Toolset{a, b})
|
|
assert.Empty(t, diff.Added)
|
|
assert.Empty(t, diff.Removed)
|
|
assert.Empty(t, diff.Changed)
|
|
assert.Len(t, diff.Unchanged, 2)
|
|
assert.False(t, diff.HasChanges())
|
|
}
|
|
|
|
func TestDiffToolsets_Added(t *testing.T) {
|
|
t.Parallel()
|
|
a := mcpCmd("a", "x")
|
|
b := mcpCmd("b", "y")
|
|
diff := DiffToolsets([]latest.Toolset{a}, []latest.Toolset{a, b})
|
|
assert.Len(t, diff.Added, 1)
|
|
assert.Equal(t, "b", diff.Added[0].Name)
|
|
assert.Empty(t, diff.Removed)
|
|
assert.Empty(t, diff.Changed)
|
|
assert.Len(t, diff.Unchanged, 1)
|
|
assert.True(t, diff.HasChanges())
|
|
}
|
|
|
|
func TestDiffToolsets_Removed(t *testing.T) {
|
|
t.Parallel()
|
|
a := mcpCmd("a", "x")
|
|
b := mcpCmd("b", "y")
|
|
diff := DiffToolsets([]latest.Toolset{a, b}, []latest.Toolset{a})
|
|
assert.Empty(t, diff.Added)
|
|
assert.Len(t, diff.Removed, 1)
|
|
assert.Equal(t, "b", diff.Removed[0].Name)
|
|
assert.Empty(t, diff.Changed)
|
|
assert.Len(t, diff.Unchanged, 1)
|
|
assert.True(t, diff.HasChanges())
|
|
}
|
|
|
|
func TestDiffToolsets_Changed(t *testing.T) {
|
|
t.Parallel()
|
|
a1 := mcpCmd("a", "x")
|
|
a2 := mcpCmd("a", "y") // same identity (mcp/a), different command
|
|
diff := DiffToolsets([]latest.Toolset{a1}, []latest.Toolset{a2})
|
|
assert.Empty(t, diff.Added)
|
|
assert.Empty(t, diff.Removed)
|
|
assert.Len(t, diff.Changed, 1)
|
|
assert.Equal(t, "x", diff.Changed[0].Old.Command)
|
|
assert.Equal(t, "y", diff.Changed[0].New.Command)
|
|
assert.True(t, diff.HasChanges())
|
|
}
|
|
|
|
func TestDiffToolsets_PreservesOrder(t *testing.T) {
|
|
t.Parallel()
|
|
a := mcpCmd("a", "x")
|
|
b := mcpCmd("b", "y")
|
|
c := mcpCmd("c", "z")
|
|
d := mcpCmd("d", "w")
|
|
|
|
oldList := []latest.Toolset{a, b, c}
|
|
newList := []latest.Toolset{c, d, a} // c, d, a — b removed, d added, c & a unchanged
|
|
diff := DiffToolsets(oldList, newList)
|
|
|
|
assert.Len(t, diff.Added, 1)
|
|
assert.Equal(t, "d", diff.Added[0].Name)
|
|
assert.Len(t, diff.Removed, 1)
|
|
assert.Equal(t, "b", diff.Removed[0].Name)
|
|
// Unchanged appears in new-slice order: c then a.
|
|
assert.Equal(t, []string{"c", "a"}, []string{diff.Unchanged[0].Name, diff.Unchanged[1].Name})
|
|
}
|
|
|
|
func TestToolsetIdentity_NamedFirst(t *testing.T) {
|
|
t.Parallel()
|
|
withName := latest.Toolset{Type: "mcp", Name: "n", Command: "c"}
|
|
withoutName := latest.Toolset{Type: "mcp", Command: "c"}
|
|
assert.Equal(t, "mcp/n", ToolsetIdentity(withName))
|
|
assert.Equal(t, "mcp/cmd/c", ToolsetIdentity(withoutName))
|
|
}
|
|
|
|
func TestToolsetIdentity_RefCommandRemoteFallback(t *testing.T) {
|
|
t.Parallel()
|
|
ref := latest.Toolset{Type: "mcp", Ref: "docker:foo"}
|
|
cmd := latest.Toolset{Type: "mcp", Command: "bar"}
|
|
rem := latest.Toolset{Type: "mcp", Remote: latest.Remote{URL: "https://x"}}
|
|
lsp := latest.Toolset{Type: "lsp", Command: "gopls"}
|
|
assert.Equal(t, "mcp/ref/docker:foo", ToolsetIdentity(ref))
|
|
assert.Equal(t, "mcp/cmd/bar", ToolsetIdentity(cmd))
|
|
assert.Equal(t, "mcp/remote/https://x", ToolsetIdentity(rem))
|
|
assert.Equal(t, "lsp/cmd/gopls", ToolsetIdentity(lsp))
|
|
}
|
|
|
|
func TestToolsetSignature_StableAcrossEqualValues(t *testing.T) {
|
|
t.Parallel()
|
|
a := mcpCmd("x", "y")
|
|
b := mcpCmd("x", "y")
|
|
assert.Equal(t, ToolsetSignature(a), ToolsetSignature(b))
|
|
}
|
|
|
|
func TestToolsetSignature_DifferentForDifferentValues(t *testing.T) {
|
|
t.Parallel()
|
|
a := mcpCmd("x", "one")
|
|
b := mcpCmd("x", "two")
|
|
assert.NotEqual(t, ToolsetSignature(a), ToolsetSignature(b))
|
|
}
|