|
6 | 6 | "github.com/stretchr/testify/assert" |
7 | 7 | "github.com/stretchr/testify/require" |
8 | 8 |
|
| 9 | + "github.com/docker/docker-agent/pkg/tools" |
| 10 | + "github.com/docker/docker-agent/pkg/tools/lifecycle" |
9 | 11 | "github.com/docker/docker-agent/pkg/tui/messages" |
10 | 12 | ) |
11 | 13 |
|
@@ -277,3 +279,122 @@ func TestSettingsCommandLabel(t *testing.T) { |
277 | 279 | assert.Equal(t, "settings.open", settings.ID) |
278 | 280 | assert.Equal(t, "Settings", settings.Label) |
279 | 281 | } |
| 282 | + |
| 283 | +// stubToolsetStatusSource is a minimal toolsetStatusSource for exercising |
| 284 | +// toolsetRestartCandidates without a real *app.App. |
| 285 | +type stubToolsetStatusSource struct { |
| 286 | + statuses []tools.ToolsetStatus |
| 287 | +} |
| 288 | + |
| 289 | +func (s stubToolsetStatusSource) CurrentAgentToolsetStatuses() []tools.ToolsetStatus { |
| 290 | + return s.statuses |
| 291 | +} |
| 292 | + |
| 293 | +func TestToolsetRestartCandidates_AllShownWithDisabledFlag(t *testing.T) { |
| 294 | + t.Parallel() |
| 295 | + |
| 296 | + source := stubToolsetStatusSource{statuses: []tools.ToolsetStatus{ |
| 297 | + {Name: "github", Kind: "MCP", State: lifecycle.StateReady, Restartable: true}, |
| 298 | + {Name: "filesystem", State: lifecycle.StateReady, Restartable: false}, |
| 299 | + }} |
| 300 | + |
| 301 | + candidates := toolsetRestartCandidates(source) |
| 302 | + require.Len(t, candidates, 2) |
| 303 | + |
| 304 | + assert.Equal(t, "github", candidates[0].Label) |
| 305 | + assert.False(t, candidates[0].Disabled) |
| 306 | + assert.Contains(t, candidates[0].Description, "MCP") |
| 307 | + |
| 308 | + assert.Equal(t, "filesystem", candidates[1].Label) |
| 309 | + assert.True(t, candidates[1].Disabled, "non-restartable toolsets must be shown, not hidden") |
| 310 | + assert.Contains(t, candidates[1].Description, "Built-in") |
| 311 | +} |
| 312 | + |
| 313 | +func TestToolsetRestartCandidates_DedupePrefersRestartable(t *testing.T) { |
| 314 | + t.Parallel() |
| 315 | + |
| 316 | + source := stubToolsetStatusSource{statuses: []tools.ToolsetStatus{ |
| 317 | + {Name: "dup", State: lifecycle.StateStopped, Restartable: false}, |
| 318 | + {Name: "dup", State: lifecycle.StateReady, Restartable: true}, |
| 319 | + }} |
| 320 | + |
| 321 | + candidates := toolsetRestartCandidates(source) |
| 322 | + require.Len(t, candidates, 1, "duplicate names must be deduplicated") |
| 323 | + assert.False(t, candidates[0].Disabled, "the restartable entry must win over the non-restartable duplicate") |
| 324 | +} |
| 325 | + |
| 326 | +func TestToolsetRestartCandidates_EmptyNameSkipped(t *testing.T) { |
| 327 | + t.Parallel() |
| 328 | + |
| 329 | + source := stubToolsetStatusSource{statuses: []tools.ToolsetStatus{{Name: "", Restartable: true}}} |
| 330 | + assert.Empty(t, toolsetRestartCandidates(source)) |
| 331 | +} |
| 332 | + |
| 333 | +func TestAttachToolsetRestartCompletion(t *testing.T) { |
| 334 | + t.Parallel() |
| 335 | + |
| 336 | + items := builtInSessionCommands() |
| 337 | + source := stubToolsetStatusSource{statuses: []tools.ToolsetStatus{ |
| 338 | + {Name: "github", Restartable: true}, |
| 339 | + }} |
| 340 | + attachToolsetRestartCompletion(items, source) |
| 341 | + |
| 342 | + var restart, other *Item |
| 343 | + for i := range items { |
| 344 | + switch items[i].ID { |
| 345 | + case "session.toolset.restart": |
| 346 | + restart = &items[i] |
| 347 | + case "session.exit": |
| 348 | + other = &items[i] |
| 349 | + } |
| 350 | + } |
| 351 | + require.NotNil(t, restart) |
| 352 | + require.NotNil(t, restart.CompleteArgument, "the restart item must get a completer") |
| 353 | + candidates := restart.CompleteArgument() |
| 354 | + require.Len(t, candidates, 1) |
| 355 | + assert.Equal(t, "github", candidates[0].Label) |
| 356 | + |
| 357 | + require.NotNil(t, other) |
| 358 | + assert.Nil(t, other.CompleteArgument, "commands without a provider keep a nil CompleteArgument") |
| 359 | +} |
| 360 | + |
| 361 | +// TestAttachToolsetRestartCompletion_QueriesFreshOnEachCall is a regression |
| 362 | +// test for bug 2 (PR #3728): the attached CompleteArgument closure must query |
| 363 | +// the toolset status source live on every call, not capture a snapshot at |
| 364 | +// attach time. Otherwise the first Tab after "/toolset-restart " shows |
| 365 | +// whatever toolsets existed when the command list was built, not the |
| 366 | +// current ones. |
| 367 | +func TestAttachToolsetRestartCompletion_QueriesFreshOnEachCall(t *testing.T) { |
| 368 | + t.Parallel() |
| 369 | + |
| 370 | + items := builtInSessionCommands() |
| 371 | + source := &stubToolsetStatusSource{statuses: []tools.ToolsetStatus{ |
| 372 | + {Name: "github", State: lifecycle.StateReady, Restartable: true}, |
| 373 | + }} |
| 374 | + attachToolsetRestartCompletion(items, source) |
| 375 | + |
| 376 | + var restart *Item |
| 377 | + for i := range items { |
| 378 | + if items[i].ID == "session.toolset.restart" { |
| 379 | + restart = &items[i] |
| 380 | + } |
| 381 | + } |
| 382 | + require.NotNil(t, restart) |
| 383 | + |
| 384 | + first := restart.CompleteArgument() |
| 385 | + require.Len(t, first, 1) |
| 386 | + assert.Equal(t, "github", first[0].Label) |
| 387 | + |
| 388 | + // The toolset status source changes after the completer was attached |
| 389 | + // (e.g. the agent restarted with a different toolset set). A second call |
| 390 | + // must reflect that change rather than replaying the first snapshot. |
| 391 | + source.statuses = []tools.ToolsetStatus{ |
| 392 | + {Name: "filesystem", State: lifecycle.StateReady, Restartable: true}, |
| 393 | + {Name: "github", State: lifecycle.StateReady, Restartable: true}, |
| 394 | + } |
| 395 | + |
| 396 | + second := restart.CompleteArgument() |
| 397 | + require.Len(t, second, 2, "must reflect the current toolset set, not the one captured at attach time") |
| 398 | + assert.Equal(t, "filesystem", second[0].Label) |
| 399 | + assert.Equal(t, "github", second[1].Label) |
| 400 | +} |
0 commit comments