Skip to main content
Glama
ActionQueueListItem.test.ts9.07 kB
import { beforeEach, describe, expect, test, vi } from "vitest"; import { ref } from "vue"; import { mount } from "@vue/test-utils"; import { ActionKind, ActionState } from "@/api/sdf/dal/action"; import { ActionProposedView } from "./types"; import ActionQueueList from "./ActionQueueList.vue"; /** * NOTE: I broke this out into a separate file because once the `ActionQueueListItem` * gets mocked, even per test, it doesn't want to get unmocked for this test. */ vi.mock("./api_composables", () => ({ routes: { ActionRetry: "action-retry", }, useApi: () => ({ endpoint: vi.fn(() => ({ put: vi.fn(), })), }), })); vi.mock("si/vue-lib/design-system/DropdownMenu.vue", () => ({ default: { name: "DropdownMenu", template: '<div class="dropdownmenu">Dropdown</div>', }, })); vi.mock("vue-router", async (importOriginal) => { const actual = await importOriginal(); return { ...(actual as object), useRoute: () => ({ params: { workspacePk: "test-workspace", changeSetId: "test-changeset" }, }), }; }); beforeEach(() => { vi.clearAllMocks(); }); describe("Test that the ActionQueueListItem correctly displays children", () => { test("Circular dependency between two components and a third that depends on one of them. (from prod)", () => { /** * We are not *actively* supporting circular dependencies, e.g. they will * not display circularly——but we do at least want to not die if we get data * shaped like this (be defensive!!!) * * NOTE: the current decision is that each action will be displayed once * one of the circular dependency becomes "the parent", and the 3rd action * slides underneath that one. * * But an equally valid decision could be that 4 actions show up, because * we've changed how nesting works... so if this test case fails because 3 != 4 * its possible that it should just be changed to 4. */ const actionViewList = ref([ { id: "01KCME2ZTRRZYPBDNDB1EZJDFN", prototypeId: "01KCM5WWD3V79WH3HT9BQEYE6K", componentId: "01KCM5WWDEEC6HRS55XKPXVS03", componentSchemaName: "Microsoft.Resources/resourceGroups", componentName: "hello-world-rg", name: "Destroy", description: "Delete Resource Group", kind: "Destroy", state: "Queued", originatingChangeSetId: "01KCME0RBFNA6Q0YNHTRPWWDHP", myDependencies: [ "01KCME2ZTRRZYPBDNDB1EZJDFN", "01KCME6FWAH3GH1WY2KGA70Q9M", "01KCME70ANXAXDDSNZ19CAZY3G", ], dependentOn: ["01KCME6FWAH3GH1WY2KGA70Q9M"], holdStatusInfluencedBy: [], }, { id: "01KCME6FWAH3GH1WY2KGA70Q9M", prototypeId: "01KCM62JGYXTFJG50RYDYX93NR", componentId: "01KCME6C82XYSMSRBETGXFJ8GV", componentSchemaName: "Microsoft.Web/sites", componentName: "hello-world-func-20251216", name: "Create", description: null, kind: "Create", state: "Queued", originatingChangeSetId: "01KCME0RBFNA6Q0YNHTRPWWDHP", myDependencies: [ "01KCME2ZTRRZYPBDNDB1EZJDFN", "01KCME6FWAH3GH1WY2KGA70Q9M", "01KCME70ANXAXDDSNZ19CAZY3G", ], dependentOn: ["01KCME2ZTRRZYPBDNDB1EZJDFN"], holdStatusInfluencedBy: [], }, { id: "01KCME70ANXAXDDSNZ19CAZY3G", prototypeId: "01KCM89B8B0G5G03FG35T4S1VS", componentId: "01KCME6ZFTZT25BZ9JRK4WHXGE", componentSchemaName: "Microsoft.Authorization/roleAssignments", componentName: "func-app-storage-blob-data-owner", name: "Create", description: null, kind: "Create", state: "Queued", originatingChangeSetId: "01KCME0RBFNA6Q0YNHTRPWWDHP", myDependencies: [], dependentOn: ["01KCME6FWAH3GH1WY2KGA70Q9M"], holdStatusInfluencedBy: [], }, ] as ActionProposedView[]); const highlightedActionIds = ref(new Set<string>()); const wrapper = mount(ActionQueueList, { props: { actionViewList: actionViewList.value, highlightedActionIds: highlightedActionIds.value, }, }); // Then: Only grandparent should appear at top level // ActionQueueListItem will recursively render children, and with the fix, const actionItems = wrapper.findAllComponents({ name: "ActionQueueListItem", }); expect(actionItems.length).toBe(3); }); // NOTE THESE ARE FROM ActionQueueList.test.ts // IF WE CHANGE THOSE, CHANGE THESE TOO test("deeply nested actions with transitive dependencies", () => { // Given: A three-level dependency chain where myDependencies contains ALL descendants // This simulates the actual backend behavior that caused the bug const grandparentAction: ActionProposedView = { id: "grandparent-1", prototypeId: "proto-9", componentId: "comp-9", name: "Grandparent Create Action", description: "Create grandparent resource", kind: ActionKind.Create, originatingChangeSetId: "cs-5", state: ActionState.Queued, myDependencies: ["parent-5", "child-5"], // Backend includes ALL descendants! dependentOn: [], holdStatusInfluencedBy: [], componentSchemaName: "GrandparentSchema", componentName: "grandparent-component", }; const parentAction: ActionProposedView = { id: "parent-5", prototypeId: "proto-10", componentId: "comp-10", name: "Parent Create Action", description: "Create parent resource", kind: ActionKind.Create, originatingChangeSetId: "cs-5", state: ActionState.Queued, myDependencies: ["child-5"], // Backend includes all descendants dependentOn: ["grandparent-1"], // Only lists direct parent holdStatusInfluencedBy: [], componentSchemaName: "ParentSchema", componentName: "parent-component", }; const childAction: ActionProposedView = { id: "child-5", prototypeId: "proto-11", componentId: "comp-11", name: "Child Create Action", description: "Create child resource", kind: ActionKind.Create, originatingChangeSetId: "cs-5", state: ActionState.Queued, myDependencies: [], dependentOn: ["parent-5"], // Only lists direct parent holdStatusInfluencedBy: [], componentSchemaName: "ChildSchema", componentName: "child-component", }; const actionViewList = ref([grandparentAction, parentAction, childAction]); const highlightedActionIds = ref(new Set<string>()); // When: Rendering the ActionQueueList const wrapper = mount(ActionQueueList, { props: { actionViewList: actionViewList.value, highlightedActionIds: highlightedActionIds.value, }, }); // Then: Only grandparent should appear at top level // ActionQueueListItem will recursively render children, and with the fix, // it will only render direct children (not all descendants from myDependencies) const actionItems = wrapper.findAllComponents({ name: "ActionQueueListItem", }); expect(actionItems.length).toBe(3); }); test("child action is hidden even when parent is Queued and child is OnHold (different states)", () => { // Given: A parent action that is Queued and a child action that is OnHold // This was part of the bug - the old code required exact state match const parentAction: ActionProposedView = { id: "parent-2", prototypeId: "proto-3", componentId: "comp-3", name: "Parent Create Action", description: "Create parent resource", kind: ActionKind.Create, originatingChangeSetId: "cs-2", state: ActionState.Queued, myDependencies: ["child-2"], dependentOn: [], holdStatusInfluencedBy: [], componentSchemaName: "ParentSchema", componentName: "parent-component", }; const childAction: ActionProposedView = { id: "child-2", prototypeId: "proto-4", componentId: "comp-4", name: "Child Create Action", description: "Create child resource", kind: ActionKind.Create, originatingChangeSetId: "cs-2", state: ActionState.OnHold, // Different state from parent! myDependencies: [], dependentOn: ["parent-2"], holdStatusInfluencedBy: ["parent-2"], componentSchemaName: "ChildSchema", componentName: "child-component", }; const actionViewList = ref([parentAction, childAction]); const highlightedActionIds = ref(new Set<string>()); // When: Rendering the ActionQueueList const wrapper = mount(ActionQueueList, { props: { actionViewList: actionViewList.value, highlightedActionIds: highlightedActionIds.value, }, }); // Then: Only parent should appear at top level, child should not appear in OnHold section const actionItems = wrapper.findAllComponents({ name: "ActionQueueListItem", }); expect(actionItems.length).toBe(2); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server