Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5344570589 | |||
| baa1029b90 | |||
| 9c7135a61d | |||
| b7d8918d60 | |||
| e7dbbcde87 | |||
| bbf7b137da |
@@ -61,10 +61,10 @@ func NewWithCommand(
|
||||
t testing.TB, cmd *serpent.Command, args ...string,
|
||||
) (*serpent.Invocation, config.Root) {
|
||||
configDir := config.Root(t.TempDir())
|
||||
// Keyring usage is disabled here because many existing tests expect the session token
|
||||
// to be stored on disk and is not properly instrumented for parallel testing against
|
||||
// the actual operating system keyring.
|
||||
invArgs := append([]string{"--global-config", string(configDir), "--use-keyring=false"}, args...)
|
||||
// Keyring usage is disabled here when --global-config is set because many existing
|
||||
// tests expect the session token to be stored on disk and is not properly instrumented
|
||||
// for parallel testing against the actual operating system keyring.
|
||||
invArgs := append([]string{"--global-config", string(configDir)}, args...)
|
||||
return setupInvocation(t, cmd, invArgs...), configDir
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ func setupKeyringTestEnv(t *testing.T, clientURL string, args ...string) keyring
|
||||
|
||||
serviceName := keyringTestServiceName(t)
|
||||
root.WithKeyringServiceName(serviceName)
|
||||
root.UseKeyringWithGlobalConfig()
|
||||
|
||||
inv, cfg := clitest.NewWithDefaultKeyringCommand(t, cmd, args...)
|
||||
|
||||
@@ -169,6 +170,7 @@ func TestUseKeyring(t *testing.T) {
|
||||
logoutCmd, err := logoutRoot.Command(logoutRoot.AGPL())
|
||||
require.NoError(t, err)
|
||||
logoutRoot.WithKeyringServiceName(env.serviceName)
|
||||
logoutRoot.UseKeyringWithGlobalConfig()
|
||||
|
||||
logoutInv, _ := clitest.NewWithDefaultKeyringCommand(t, logoutCmd,
|
||||
"logout",
|
||||
|
||||
+23
-9
@@ -483,9 +483,9 @@ func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, err
|
||||
Flag: varUseKeyring,
|
||||
Env: envUseKeyring,
|
||||
Description: "Store and retrieve session tokens using the operating system " +
|
||||
"keyring. Enabled by default. If the keyring is not supported on the " +
|
||||
"current platform, file-based storage is used automatically. Set to " +
|
||||
"false to force file-based storage.",
|
||||
"keyring. This flag is ignored and file-based storage is used when " +
|
||||
"--global-config is set or keyring usage is not supported on the current " +
|
||||
"platform. Set to false to force file-based storage on supported platforms.",
|
||||
Default: "true",
|
||||
Value: serpent.BoolOf(&r.useKeyring),
|
||||
Group: globalGroup,
|
||||
@@ -536,11 +536,12 @@ type RootCmd struct {
|
||||
disableDirect bool
|
||||
debugHTTP bool
|
||||
|
||||
disableNetworkTelemetry bool
|
||||
noVersionCheck bool
|
||||
noFeatureWarning bool
|
||||
useKeyring bool
|
||||
keyringServiceName string
|
||||
disableNetworkTelemetry bool
|
||||
noVersionCheck bool
|
||||
noFeatureWarning bool
|
||||
useKeyring bool
|
||||
keyringServiceName string
|
||||
useKeyringWithGlobalConfig bool
|
||||
}
|
||||
|
||||
// InitClient creates and configures a new client with authentication, telemetry,
|
||||
@@ -721,8 +722,14 @@ func (r *RootCmd) createUnauthenticatedClient(ctx context.Context, serverURL *ur
|
||||
// flag.
|
||||
func (r *RootCmd) ensureTokenBackend() sessionstore.Backend {
|
||||
if r.tokenBackend == nil {
|
||||
// Checking for the --global-config directory being set is a bit wonky but necessary
|
||||
// to allow extensions that invoke the CLI with this flag (e.g. VS code) to continue
|
||||
// working without modification. In the future we should modify these extensions to
|
||||
// either access the credential in the keyring (like Coder Desktop) or some other
|
||||
// approach that doesn't rely on the session token being stored on disk.
|
||||
assumeExtensionInUse := r.globalConfig != config.DefaultDir() && !r.useKeyringWithGlobalConfig
|
||||
keyringSupported := runtime.GOOS == "windows" || runtime.GOOS == "darwin"
|
||||
if r.useKeyring && keyringSupported {
|
||||
if r.useKeyring && !assumeExtensionInUse && keyringSupported {
|
||||
serviceName := sessionstore.DefaultServiceName
|
||||
if r.keyringServiceName != "" {
|
||||
serviceName = r.keyringServiceName
|
||||
@@ -742,6 +749,13 @@ func (r *RootCmd) WithKeyringServiceName(serviceName string) {
|
||||
r.keyringServiceName = serviceName
|
||||
}
|
||||
|
||||
// UseKeyringWithGlobalConfig enables the use of the keyring storage backend
|
||||
// when the --global-config directory is set. This is only intended as an override
|
||||
// for tests, which require specifying the global config directory for test isolation.
|
||||
func (r *RootCmd) UseKeyringWithGlobalConfig() {
|
||||
r.useKeyringWithGlobalConfig = true
|
||||
}
|
||||
|
||||
type AgentAuth struct {
|
||||
// Agent Client config
|
||||
agentToken string
|
||||
|
||||
Vendored
+4
-3
@@ -111,9 +111,10 @@ variables or flags.
|
||||
|
||||
--use-keyring bool, $CODER_USE_KEYRING (default: true)
|
||||
Store and retrieve session tokens using the operating system keyring.
|
||||
Enabled by default. If the keyring is not supported on the current
|
||||
platform, file-based storage is used automatically. Set to false to
|
||||
force file-based storage.
|
||||
This flag is ignored and file-based storage is used when
|
||||
--global-config is set or keyring usage is not supported on the
|
||||
current platform. Set to false to force file-based storage on
|
||||
supported platforms.
|
||||
|
||||
-v, --verbose bool, $CODER_VERBOSE
|
||||
Enable verbose output.
|
||||
|
||||
@@ -23751,6 +23751,7 @@ SET
|
||||
WHERE
|
||||
template_id = $3
|
||||
AND dormant_at IS NOT NULL
|
||||
AND deleted = false
|
||||
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
|
||||
-- should not have their dormant or deleting at set, as these are handled by the
|
||||
-- prebuilds reconciliation loop.
|
||||
|
||||
@@ -846,6 +846,7 @@ SET
|
||||
WHERE
|
||||
template_id = @template_id
|
||||
AND dormant_at IS NOT NULL
|
||||
AND deleted = false
|
||||
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
|
||||
-- should not have their dormant or deleting at set, as these are handled by the
|
||||
-- prebuilds reconciliation loop.
|
||||
|
||||
@@ -38,4 +38,4 @@ Where relevant, both streaming and non-streaming requests are supported.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
To report a bug, file a feature request, or view a list of known issues, please visit our [GitHub repository for AI Bridge](https://github.com/coder/aibridge). If you encounter issues with AI Bridge during early access, please reach out to us via [Discord](https://discord.gg/coder).
|
||||
To report a bug, file a feature request, or view a list of known issues, please visit our [GitHub repository for AI Bridge](https://github.com/coder/aibridge). If you encounter issues with AI Bridge, please reach out to us via [Discord](https://discord.gg/coder).
|
||||
|
||||
+1
-1
@@ -935,7 +935,7 @@
|
||||
"description": "Centralized LLM and MCP proxy for platform teams",
|
||||
"path": "./ai-coder/ai-bridge/index.md",
|
||||
"icon_path": "./images/icons/api.svg",
|
||||
"state": ["premium", "early access"],
|
||||
"state": ["premium", "beta"],
|
||||
"children": [
|
||||
{
|
||||
"title": "Setup",
|
||||
|
||||
Generated
+1
-1
@@ -179,7 +179,7 @@ Disable network telemetry. Network telemetry is collected when connecting to wor
|
||||
| Environment | <code>$CODER_USE_KEYRING</code> |
|
||||
| Default | <code>true</code> |
|
||||
|
||||
Store and retrieve session tokens using the operating system keyring. Enabled by default. If the keyring is not supported on the current platform, file-based storage is used automatically. Set to false to force file-based storage.
|
||||
Store and retrieve session tokens using the operating system keyring. This flag is ignored and file-based storage is used when --global-config is set or keyring usage is not supported on the current platform. Set to false to force file-based storage on supported platforms.
|
||||
|
||||
### --global-config
|
||||
|
||||
|
||||
+4
-3
@@ -70,9 +70,10 @@ variables or flags.
|
||||
|
||||
--use-keyring bool, $CODER_USE_KEYRING (default: true)
|
||||
Store and retrieve session tokens using the operating system keyring.
|
||||
Enabled by default. If the keyring is not supported on the current
|
||||
platform, file-based storage is used automatically. Set to false to
|
||||
force file-based storage.
|
||||
This flag is ignored and file-based storage is used when
|
||||
--global-config is set or keyring usage is not supported on the
|
||||
current platform. Set to false to force file-based storage on
|
||||
supported platforms.
|
||||
|
||||
-v, --verbose bool, $CODER_VERBOSE
|
||||
Enable verbose output.
|
||||
|
||||
@@ -460,7 +460,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
})
|
||||
r.Route("/templates/{template}/prebuilds", func(r chi.Router) {
|
||||
r.Use(
|
||||
api.templateRBACEnabledMW,
|
||||
api.RequireFeatureMW(codersdk.FeatureWorkspacePrebuilds),
|
||||
apiKeyMiddleware,
|
||||
httpmw.ExtractTemplateParam(api.Database),
|
||||
)
|
||||
|
||||
@@ -737,6 +737,105 @@ func TestNotifications(t *testing.T) {
|
||||
require.Contains(t, sent[i].Targets, dormantWs.OwnerID)
|
||||
}
|
||||
})
|
||||
|
||||
// Regression test for https://github.com/coder/coder/issues/20913
|
||||
// Deleted workspaces should not receive dormancy notifications.
|
||||
t.Run("DeletedWorkspacesNotNotified", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
db, _ = dbtestutil.NewDB(t)
|
||||
ctx = testutil.Context(t, testutil.WaitLong)
|
||||
user = dbgen.User(t, db, database.User{})
|
||||
file = dbgen.File(t, db, database.File{
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
templateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
||||
FileID: file.ID,
|
||||
InitiatorID: user.ID,
|
||||
Tags: database.StringMap{
|
||||
"foo": "bar",
|
||||
},
|
||||
})
|
||||
timeTilDormant = time.Minute * 2
|
||||
templateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
||||
CreatedBy: user.ID,
|
||||
JobID: templateJob.ID,
|
||||
OrganizationID: templateJob.OrganizationID,
|
||||
})
|
||||
template = dbgen.Template(t, db, database.Template{
|
||||
ActiveVersionID: templateVersion.ID,
|
||||
CreatedBy: user.ID,
|
||||
OrganizationID: templateJob.OrganizationID,
|
||||
TimeTilDormant: int64(timeTilDormant),
|
||||
TimeTilDormantAutoDelete: int64(timeTilDormant),
|
||||
})
|
||||
)
|
||||
|
||||
// Create a dormant workspace that is NOT deleted.
|
||||
activeDormantWorkspace := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||
OwnerID: user.ID,
|
||||
TemplateID: template.ID,
|
||||
OrganizationID: templateJob.OrganizationID,
|
||||
LastUsedAt: time.Now().Add(-time.Hour),
|
||||
})
|
||||
_, err := db.UpdateWorkspaceDormantDeletingAt(ctx, database.UpdateWorkspaceDormantDeletingAtParams{
|
||||
ID: activeDormantWorkspace.ID,
|
||||
DormantAt: sql.NullTime{
|
||||
Time: activeDormantWorkspace.LastUsedAt.Add(timeTilDormant),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a dormant workspace that IS deleted.
|
||||
deletedDormantWorkspace := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||
OwnerID: user.ID,
|
||||
TemplateID: template.ID,
|
||||
OrganizationID: templateJob.OrganizationID,
|
||||
LastUsedAt: time.Now().Add(-time.Hour),
|
||||
Deleted: true, // Mark as deleted
|
||||
})
|
||||
_, err = db.UpdateWorkspaceDormantDeletingAt(ctx, database.UpdateWorkspaceDormantDeletingAtParams{
|
||||
ID: deletedDormantWorkspace.ID,
|
||||
DormantAt: sql.NullTime{
|
||||
Time: deletedDormantWorkspace.LastUsedAt.Add(timeTilDormant),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Setup dependencies
|
||||
notifyEnq := notificationstest.NewFakeEnqueuer()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
const userQuietHoursSchedule = "CRON_TZ=UTC 0 0 * * *" // midnight UTC
|
||||
userQuietHoursStore, err := schedule.NewEnterpriseUserQuietHoursScheduleStore(userQuietHoursSchedule, true)
|
||||
require.NoError(t, err)
|
||||
userQuietHoursStorePtr := &atomic.Pointer[agplschedule.UserQuietHoursScheduleStore]{}
|
||||
userQuietHoursStorePtr.Store(&userQuietHoursStore)
|
||||
templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr, notifyEnq, logger, nil)
|
||||
|
||||
// Lower the dormancy TTL to ensure the schedule recalculates deadlines and
|
||||
// triggers notifications.
|
||||
_, err = templateScheduleStore.Set(dbauthz.AsNotifier(ctx), db, template, agplschedule.TemplateScheduleOptions{
|
||||
TimeTilDormant: timeTilDormant / 2,
|
||||
TimeTilDormantAutoDelete: timeTilDormant / 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// We should only receive a notification for the non-deleted dormant workspace.
|
||||
sent := notifyEnq.Sent()
|
||||
require.Len(t, sent, 1, "expected exactly 1 notification for the non-deleted workspace")
|
||||
require.Equal(t, sent[0].UserID, activeDormantWorkspace.OwnerID)
|
||||
require.Equal(t, sent[0].TemplateID, notifications.TemplateWorkspaceMarkedForDeletion)
|
||||
require.Contains(t, sent[0].Targets, activeDormantWorkspace.ID)
|
||||
|
||||
// Ensure the deleted workspace was NOT notified
|
||||
for _, notification := range sent {
|
||||
require.NotContains(t, notification.Targets, deletedDormantWorkspace.ID,
|
||||
"deleted workspace should not receive notifications")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplateTTL(t *testing.T) {
|
||||
|
||||
@@ -378,7 +378,12 @@ func (api *API) postInvalidateTemplatePresets(rw http.ResponseWriter, r *http.Re
|
||||
slog.F("preset_count", len(invalidatedPresets)),
|
||||
)
|
||||
|
||||
invalidated := db2sdk.InvalidatedPresets(invalidatedPresets)
|
||||
if invalidated == nil {
|
||||
invalidated = []codersdk.InvalidatedPreset{} // need to avoid nil value
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.InvalidatePresetsResponse{
|
||||
Invalidated: db2sdk.InvalidatedPresets(invalidatedPresets),
|
||||
Invalidated: invalidated,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package coderd_test
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"slices"
|
||||
"testing"
|
||||
@@ -2154,7 +2155,7 @@ func TestInvalidateTemplatePrebuilds(t *testing.T) {
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
codersdk.FeatureWorkspacePrebuilds: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -2208,3 +2209,121 @@ func TestInvalidateTemplatePrebuilds(t *testing.T) {
|
||||
require.Equal(t, codersdk.InvalidatedPreset{TemplateName: template.Name, TemplateVersionName: version2.Name, PresetName: presetWithParameters2.Name}, invalidated.Invalidated[0])
|
||||
require.Equal(t, codersdk.InvalidatedPreset{TemplateName: template.Name, TemplateVersionName: version2.Name, PresetName: presetWithParameters3.Name}, invalidated.Invalidated[1])
|
||||
}
|
||||
|
||||
func TestInvalidateTemplatePrebuilds_RegularUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given the following parameters and presets...
|
||||
templateVersionParameters := []*proto.RichParameter{
|
||||
{Name: "param1", Type: "string", Required: false, DefaultValue: "default1"},
|
||||
}
|
||||
presetWithParameters1 := &proto.Preset{
|
||||
Name: "Preset With Parameters 1",
|
||||
Parameters: []*proto.PresetParameter{
|
||||
{Name: "param1", Value: "value1"},
|
||||
},
|
||||
}
|
||||
|
||||
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureWorkspacePrebuilds: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
regularUserClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||
|
||||
// Given
|
||||
version1 := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: []*proto.Response{
|
||||
{
|
||||
Type: &proto.Response_Plan{
|
||||
Plan: &proto.PlanComplete{
|
||||
Presets: []*proto.Preset{presetWithParameters1},
|
||||
Parameters: templateVersionParameters,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ProvisionApply: echo.ApplyComplete,
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, version1.ID)
|
||||
template := coderdtest.CreateTemplate(t, ownerClient, owner.OrganizationID, version1.ID)
|
||||
|
||||
// When
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
_, err := regularUserClient.InvalidateTemplatePresets(ctx, template.ID)
|
||||
|
||||
// Then
|
||||
require.Error(t, err, "regular user cannot invalidate presets")
|
||||
var sdkError *codersdk.Error
|
||||
require.True(t, errors.As(err, &sdkError))
|
||||
require.ErrorAs(t, err, &sdkError)
|
||||
require.Equal(t, http.StatusNotFound, sdkError.StatusCode())
|
||||
}
|
||||
|
||||
func TestInvalidateTemplatePrebuilds_NoPresets(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given the template versions and template...
|
||||
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureWorkspacePrebuilds: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
templateAdminClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
|
||||
version1 := coderdtest.CreateTemplateVersion(t, templateAdminClient, owner.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete, ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ApplyComplete,
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, version1.ID)
|
||||
template := coderdtest.CreateTemplate(t, templateAdminClient, owner.OrganizationID, version1.ID)
|
||||
|
||||
// When
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
invalidated, err := templateAdminClient.InvalidateTemplatePresets(ctx, template.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then
|
||||
require.NotNil(t, invalidated.Invalidated)
|
||||
require.Len(t, invalidated.Invalidated, 0)
|
||||
}
|
||||
|
||||
func TestInvalidateTemplatePrebuilds_LicenseFeatureDisabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given the template versions and template...
|
||||
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{},
|
||||
})
|
||||
templateAdminClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
|
||||
version1 := coderdtest.CreateTemplateVersion(t, templateAdminClient, owner.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete, ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ApplyComplete,
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, version1.ID)
|
||||
template := coderdtest.CreateTemplate(t, templateAdminClient, owner.OrganizationID, version1.ID)
|
||||
|
||||
// When
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
_, err := templateAdminClient.InvalidateTemplatePresets(ctx, template.ID)
|
||||
|
||||
// Then
|
||||
require.Error(t, err, "license feature prebuilds is required")
|
||||
var sdkError *codersdk.Error
|
||||
require.True(t, errors.As(err, &sdkError))
|
||||
require.ErrorAs(t, err, &sdkError)
|
||||
require.Equal(t, http.StatusForbidden, sdkError.StatusCode())
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { withAuthProvider, withProxyProvider } from "testHelpers/storybook";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { API } from "api/api";
|
||||
import { MockUsers } from "pages/UsersPage/storybookData/users";
|
||||
import { expect, spyOn, userEvent, within } from "storybook/test";
|
||||
import { expect, spyOn, userEvent, waitFor, within } from "storybook/test";
|
||||
import { getTemplatesQueryKey } from "../../api/queries/templates";
|
||||
import TasksPage from "./TasksPage";
|
||||
|
||||
@@ -145,6 +145,29 @@ export const LoadedTasksWaitingForInputTab: Story = {
|
||||
spyOn(API, "getTasks").mockResolvedValue([
|
||||
{
|
||||
...firstTask,
|
||||
id: "active-idle-task",
|
||||
display_name: "Active Idle Task",
|
||||
status: "active",
|
||||
current_state: {
|
||||
...firstTask.current_state,
|
||||
state: "idle",
|
||||
},
|
||||
},
|
||||
{
|
||||
...firstTask,
|
||||
id: "paused-idle-task",
|
||||
display_name: "Paused Idle Task",
|
||||
status: "paused",
|
||||
current_state: {
|
||||
...firstTask.current_state,
|
||||
state: "idle",
|
||||
},
|
||||
},
|
||||
{
|
||||
...firstTask,
|
||||
id: "error-idle-task",
|
||||
display_name: "Error Idle Task",
|
||||
status: "error",
|
||||
current_state: {
|
||||
...firstTask.current_state,
|
||||
state: "idle",
|
||||
@@ -161,6 +184,23 @@ export const LoadedTasksWaitingForInputTab: Story = {
|
||||
name: /waiting for input/i,
|
||||
});
|
||||
await userEvent.click(waitingForInputTab);
|
||||
|
||||
// Wait for the table to update after tab switch
|
||||
await waitFor(async () => {
|
||||
const table = canvas.getByRole("table");
|
||||
const tableContent = within(table);
|
||||
|
||||
// Active idle task should be visible
|
||||
expect(tableContent.getByText("Active Idle Task")).toBeInTheDocument();
|
||||
|
||||
// Only active idle tasks should be visible in the table
|
||||
expect(
|
||||
tableContent.queryByText("Paused Idle Task"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
tableContent.queryByText("Error Idle Task"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ const TasksPage: FC = () => {
|
||||
refetchInterval: 10_000,
|
||||
});
|
||||
const idleTasks = tasksQuery.data?.filter(
|
||||
(task) => task.current_state?.state === "idle",
|
||||
(task) => task.status === "active" && task.current_state?.state === "idle",
|
||||
);
|
||||
const displayedTasks =
|
||||
tab.value === "waiting-for-input" ? idleTasks : tasksQuery.data;
|
||||
|
||||
Reference in New Issue
Block a user