@copilotkit/web-inspector 1.62.2 → 1.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@ import type { ThreadDebuggerProvider } from "../index.js";
7
7
  import type { CopilotKitCore } from "@copilotkit/core";
8
8
  import { CopilotKitCoreRuntimeConnectionStatus } from "@copilotkit/core";
9
9
  import type { CopilotKitCoreSubscriber } from "@copilotkit/core";
10
+ import type { Memory } from "@copilotkit/core";
10
11
  import type { AbstractAgent, AgentSubscriber } from "@ag-ui/client";
11
12
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
12
13
 
@@ -23,7 +24,7 @@ type InspectorInternals = {
23
24
 
24
25
  type InspectorThreadViewInternals = {
25
26
  isOpen: boolean;
26
- selectedMenu: "threads";
27
+ selectedMenu: "ag-ui-events" | "threads";
27
28
  selectedThreadId: string | null;
28
29
  _threads: Array<{
29
30
  id: string;
@@ -31,6 +32,15 @@ type InspectorThreadViewInternals = {
31
32
  agentId: string;
32
33
  updatedAt?: string | null;
33
34
  }>;
35
+ _threadsByAgent: Map<
36
+ string,
37
+ Array<{
38
+ id: string;
39
+ name?: string | null;
40
+ agentId: string;
41
+ updatedAt?: string | null;
42
+ }>
43
+ >;
34
44
  };
35
45
 
36
46
  type InspectorContextInternals = {
@@ -101,6 +111,46 @@ function createMockAgent(
101
111
 
102
112
  // --- Mock core factory ---
103
113
 
114
+ // --- Minimal no-op memory store stub ---
115
+ // The inspector calls core.getMemoryStore() lazily, on first Memories-tab
116
+ // activation (NOT on attach). All mock cores still expose this method so that
117
+ // tests which do activate the tab don't hit a TypeError. The stub below seeds
118
+ // the store with empty memories and available=true, which is the right default
119
+ // for tests that don't exercise the memory feature.
120
+
121
+ type MockMemoryStoreState = {
122
+ memories: never[];
123
+ isLoading: boolean;
124
+ isMutating: boolean;
125
+ error: null;
126
+ context: null;
127
+ sessionId: number;
128
+ available: boolean;
129
+ realtimeStatus: "connecting" | "connected" | "unavailable";
130
+ };
131
+
132
+ function createNoopMemoryStore() {
133
+ const state: MockMemoryStoreState = {
134
+ memories: [],
135
+ isLoading: false,
136
+ isMutating: false,
137
+ error: null,
138
+ context: null,
139
+ sessionId: 0,
140
+ available: true,
141
+ realtimeStatus: "connecting",
142
+ };
143
+ return {
144
+ getState: () => state,
145
+ select: <T>(selector: (s: MockMemoryStoreState) => T) => ({
146
+ subscribe: (cb: (v: T) => void) => {
147
+ cb(selector(state));
148
+ return { unsubscribe: () => undefined };
149
+ },
150
+ }),
151
+ };
152
+ }
153
+
104
154
  type MockCore = {
105
155
  agents: Record<string, AbstractAgent>;
106
156
  context: Record<string, unknown>;
@@ -111,6 +161,7 @@ type MockCore = {
111
161
  };
112
162
  getThreadStores: () => Record<string, never>;
113
163
  getThreadStore: (agentId: string) => undefined;
164
+ getMemoryStore: () => ReturnType<typeof createNoopMemoryStore>;
114
165
  };
115
166
 
116
167
  function createMockCore(initialAgents: Record<string, AbstractAgent> = {}) {
@@ -130,6 +181,9 @@ function createMockCore(initialAgents: Record<string, AbstractAgent> = {}) {
130
181
  getThreadStore(_agentId: string) {
131
182
  return undefined;
132
183
  },
184
+ getMemoryStore() {
185
+ return createNoopMemoryStore();
186
+ },
133
187
  };
134
188
 
135
189
  return {
@@ -1401,6 +1455,7 @@ type HeaderMockCore = {
1401
1455
  getThreadStore: (agentId: string) => undefined;
1402
1456
  registerThreadStore: (agentId: string, store: unknown) => void;
1403
1457
  unregisterThreadStore: (agentId: string) => void;
1458
+ getMemoryStore: () => ReturnType<typeof createNoopMemoryStore>;
1404
1459
  };
1405
1460
 
1406
1461
  function createHeaderMockCore(
@@ -1437,6 +1492,9 @@ function createHeaderMockCore(
1437
1492
  },
1438
1493
  registerThreadStore() {},
1439
1494
  unregisterThreadStore() {},
1495
+ getMemoryStore() {
1496
+ return createNoopMemoryStore();
1497
+ },
1440
1498
  };
1441
1499
 
1442
1500
  const asCore = () => core as unknown as CopilotKitCore;
@@ -1481,6 +1539,14 @@ describe("WebInspectorElement owned thread store headers (#5581)", () => {
1481
1539
  properties: Record<string, unknown>;
1482
1540
  };
1483
1541
  });
1542
+ const threadListText = (inspector: WebInspectorElement) =>
1543
+ inspector.shadowRoot?.querySelector("cpk-thread-list")?.shadowRoot
1544
+ ?.textContent ?? "";
1545
+ const expectNoUtmParams = (url: URL) => {
1546
+ expect(url.searchParams.has("utm_source")).toBe(false);
1547
+ expect(url.searchParams.has("utm_medium")).toBe(false);
1548
+ expect(url.searchParams.has("utm_campaign")).toBe(false);
1549
+ };
1484
1550
 
1485
1551
  beforeEach(() => {
1486
1552
  document.body.innerHTML = "";
@@ -1497,6 +1563,7 @@ describe("WebInspectorElement owned thread store headers (#5581)", () => {
1497
1563
  });
1498
1564
 
1499
1565
  afterEach(() => {
1566
+ vi.useRealTimers();
1500
1567
  vi.unstubAllGlobals();
1501
1568
  });
1502
1569
 
@@ -1649,13 +1716,17 @@ describe("WebInspectorElement owned thread store headers (#5581)", () => {
1649
1716
  "Talk to an Engineer",
1650
1717
  "Sign up for Intelligence",
1651
1718
  ]);
1719
+ const engineer = inspector.shadowRoot?.querySelector<HTMLAnchorElement>(
1720
+ 'a[href^="https://www.copilotkit.ai/talk-to-an-engineer"]',
1721
+ );
1722
+ expect(engineer?.closest("#cpk-main-scroll")).toBeNull();
1652
1723
  expect(text).not.toContain("No threads yet");
1653
1724
  expect(
1654
1725
  fetchMock.mock.calls.some((call) => String(call[0]).includes("/threads")),
1655
1726
  ).toBe(false);
1656
1727
  });
1657
1728
 
1658
- it("adds ref and posthog distinct ID attribution to locked-state CTAs", async () => {
1729
+ it("adds inspector attribution to locked-state CTAs", async () => {
1659
1730
  const { agent } = createMockAgent("alpha");
1660
1731
  const harness = createHeaderMockCore(
1661
1732
  { alpha: agent },
@@ -1678,7 +1749,7 @@ describe("WebInspectorElement owned thread store headers (#5581)", () => {
1678
1749
  await inspector.updateComplete;
1679
1750
 
1680
1751
  const signup = inspector.shadowRoot?.querySelector<HTMLAnchorElement>(
1681
- 'a[href^="https://go.copilotkit.ai/intelligence-signup"]',
1752
+ 'a[href^="https://dashboard.operations.copilotkit.ai/sign-in"]',
1682
1753
  );
1683
1754
  const engineer = inspector.shadowRoot?.querySelector<HTMLAnchorElement>(
1684
1755
  'a[href^="https://www.copilotkit.ai/talk-to-an-engineer"]',
@@ -1688,7 +1759,10 @@ describe("WebInspectorElement owned thread store headers (#5581)", () => {
1688
1759
  expect(engineer).not.toBeNull();
1689
1760
 
1690
1761
  const signupUrl = new URL(signup!.href);
1762
+ expect(signupUrl.origin).toBe("https://dashboard.operations.copilotkit.ai");
1763
+ expect(signupUrl.pathname).toBe("/sign-in");
1691
1764
  expect(signupUrl.searchParams.get("ref")).toBe("cpk-inspector");
1765
+ expectNoUtmParams(signupUrl);
1692
1766
  const distinctId = signupUrl.searchParams.get("posthog_distinct_id");
1693
1767
  expect(distinctId).toMatch(/^[0-9a-f-]{36}$/);
1694
1768
 
@@ -1696,6 +1770,7 @@ describe("WebInspectorElement owned thread store headers (#5581)", () => {
1696
1770
  expect(engineerUrl.origin).toBe("https://www.copilotkit.ai");
1697
1771
  expect(engineerUrl.pathname).toBe("/talk-to-an-engineer");
1698
1772
  expect(engineerUrl.searchParams.get("ref")).toBe("cpk-inspector-threads");
1773
+ expectNoUtmParams(engineerUrl);
1699
1774
  expect(engineerUrl.searchParams.get("posthog_distinct_id")).toBe(
1700
1775
  distinctId,
1701
1776
  );
@@ -1746,7 +1821,7 @@ describe("WebInspectorElement owned thread store headers (#5581)", () => {
1746
1821
  }
1747
1822
  });
1748
1823
 
1749
- it("keeps the enabled empty Threads state when thread listing is available", async () => {
1824
+ it("renders example threads and the deselected overview when enabled thread history is empty", async () => {
1750
1825
  const { agent } = createMockAgent("alpha");
1751
1826
  const harness = createHeaderMockCore({ alpha: agent }, {}, {}, true);
1752
1827
 
@@ -1763,12 +1838,1481 @@ describe("WebInspectorElement owned thread store headers (#5581)", () => {
1763
1838
  internals.handleMenuSelect("threads");
1764
1839
  await inspector.updateComplete;
1765
1840
 
1766
- expect(inspector.shadowRoot?.textContent ?? "").toContain("No threads yet");
1841
+ await vi.waitFor(() => {
1842
+ const text = threadListText(inspector);
1843
+ expect(text).toContain("Realtime thread sync");
1844
+ expect(text).toContain("Manage saved conversations");
1845
+ expect(text).toContain("Inspect durable run history");
1846
+ });
1847
+
1848
+ const text = inspector.shadowRoot?.textContent ?? "";
1849
+ expect(text).toContain("Threads are persistent, inspectable conversations");
1850
+ expect(text).toContain(
1851
+ "Take a tour with the example threads in the sidebar.",
1852
+ );
1853
+ const threadsDocs = inspector.shadowRoot?.querySelector<HTMLAnchorElement>(
1854
+ 'a[href^="https://docs.copilotkit.ai/threads"]',
1855
+ );
1856
+ expect(threadsDocs?.textContent?.trim()).toBe("Learn how Threads work");
1857
+ const threadsDocsUrl = new URL(threadsDocs!.href);
1858
+ expect(threadsDocsUrl.origin).toBe("https://docs.copilotkit.ai");
1859
+ expect(threadsDocsUrl.pathname).toBe("/threads");
1860
+ expect(threadsDocsUrl.searchParams.get("ref")).toBe(
1861
+ "cpk-inspector-threads",
1862
+ );
1863
+ expectNoUtmParams(threadsDocsUrl);
1864
+ const selfHosted = inspector.shadowRoot?.querySelector<HTMLAnchorElement>(
1865
+ 'a[href^="https://docs.copilotkit.ai/premium/self-hosting"]',
1866
+ );
1867
+ expect(selfHosted?.textContent?.trim()).toBe(
1868
+ "Explore self-hosted Intelligence",
1869
+ );
1870
+ const selfHostedUrl = new URL(selfHosted!.href);
1871
+ expect(selfHostedUrl.origin).toBe("https://docs.copilotkit.ai");
1872
+ expect(selfHostedUrl.pathname).toBe("/premium/self-hosting");
1873
+ expect(selfHostedUrl.searchParams.get("ref")).toBe("cpk-inspector-threads");
1874
+ expectNoUtmParams(selfHostedUrl);
1875
+ expect(threadListText(inspector)).toContain("Example");
1876
+ expect(text).not.toContain("No threads yet");
1877
+ expect(
1878
+ inspector.shadowRoot?.querySelector("cpk-thread-details"),
1879
+ ).toBeNull();
1880
+ expect(
1881
+ (inspector as unknown as InspectorThreadViewInternals).selectedThreadId,
1882
+ ).toBeNull();
1883
+
1767
1884
  const engineer = inspector.shadowRoot?.querySelector<HTMLAnchorElement>(
1768
1885
  'a[href^="https://www.copilotkit.ai/talk-to-an-engineer"]',
1769
1886
  );
1770
- expect(engineer?.href).toBe(
1771
- "https://www.copilotkit.ai/talk-to-an-engineer?ref=cpk-inspector-threads",
1887
+ const engineerUrl = new URL(engineer!.href);
1888
+ expect(engineerUrl.origin).toBe("https://www.copilotkit.ai");
1889
+ expect(engineerUrl.pathname).toBe("/talk-to-an-engineer");
1890
+ expect(engineerUrl.searchParams.get("ref")).toBe("cpk-inspector-threads");
1891
+ expectNoUtmParams(engineerUrl);
1892
+ expect(engineer?.closest("#cpk-main-scroll")).toBeNull();
1893
+ });
1894
+
1895
+ it("defers loading the empty overview video until after the overview paints", async () => {
1896
+ vi.useFakeTimers();
1897
+ vi.stubGlobal("matchMedia", () => ({ matches: false }));
1898
+
1899
+ const { agent } = createMockAgent("alpha");
1900
+ const harness = createHeaderMockCore({ alpha: agent }, {}, {}, false);
1901
+
1902
+ const inspector = new WebInspectorElement();
1903
+ document.body.appendChild(inspector);
1904
+ inspector.core = harness.core as unknown as WebInspectorElement["core"];
1905
+ harness.emitAgentsChanged();
1906
+
1907
+ const internals = inspector as unknown as {
1908
+ isOpen: boolean;
1909
+ handleMenuSelect: (key: "threads") => void;
1910
+ };
1911
+ internals.isOpen = true;
1912
+ internals.handleMenuSelect("threads");
1913
+ await inspector.updateComplete;
1914
+
1915
+ expect(inspector.shadowRoot?.textContent ?? "").toContain(
1916
+ "Threads are persistent, inspectable conversations",
1917
+ );
1918
+ expect(
1919
+ inspector.shadowRoot?.querySelector(".cpk-threads-overview-video-frame"),
1920
+ ).not.toBeNull();
1921
+ expect(
1922
+ inspector.shadowRoot?.querySelector(".cpk-threads-overview-video"),
1923
+ ).toBeNull();
1924
+
1925
+ await vi.advanceTimersByTimeAsync(450);
1926
+ await inspector.updateComplete;
1927
+
1928
+ const video = inspector.shadowRoot?.querySelector<HTMLVideoElement>(
1929
+ ".cpk-threads-overview-video",
1930
+ );
1931
+ expect(video?.src).toBe(
1932
+ "https://cdn.copilotkit.ai/corp-site/videos/copilotkit-generative-ui-agentic-frontend-demo.webm",
1933
+ );
1934
+ });
1935
+
1936
+ it("does not load the empty overview video when reduced motion is preferred", async () => {
1937
+ vi.useFakeTimers();
1938
+ vi.stubGlobal("matchMedia", () => ({ matches: true }));
1939
+
1940
+ const { agent } = createMockAgent("alpha");
1941
+ const harness = createHeaderMockCore({ alpha: agent }, {}, {}, false);
1942
+
1943
+ const inspector = new WebInspectorElement();
1944
+ document.body.appendChild(inspector);
1945
+ inspector.core = harness.core as unknown as WebInspectorElement["core"];
1946
+ harness.emitAgentsChanged();
1947
+
1948
+ const internals = inspector as unknown as {
1949
+ isOpen: boolean;
1950
+ handleMenuSelect: (key: "threads") => void;
1951
+ };
1952
+ internals.isOpen = true;
1953
+ internals.handleMenuSelect("threads");
1954
+ await inspector.updateComplete;
1955
+
1956
+ expect(inspector.shadowRoot?.textContent ?? "").toContain(
1957
+ "Threads are persistent, inspectable conversations",
1958
+ );
1959
+ expect(
1960
+ inspector.shadowRoot?.querySelector(".cpk-threads-overview-video-frame"),
1961
+ ).not.toBeNull();
1962
+
1963
+ await vi.advanceTimersByTimeAsync(1200);
1964
+ await inspector.updateComplete;
1965
+
1966
+ expect(
1967
+ inspector.shadowRoot?.querySelector(".cpk-threads-overview-video"),
1968
+ ).toBeNull();
1969
+ expect(vi.getTimerCount()).toBe(0);
1970
+ });
1971
+
1972
+ it("clears the deferred video timeout when disconnected before load", async () => {
1973
+ vi.useFakeTimers();
1974
+ vi.stubGlobal("matchMedia", () => ({ matches: false }));
1975
+ Object.defineProperty(window, "requestIdleCallback", {
1976
+ configurable: true,
1977
+ value: undefined,
1978
+ });
1979
+ const clearTimeoutSpy = vi.spyOn(window, "clearTimeout");
1980
+
1981
+ const { agent } = createMockAgent("alpha");
1982
+ const harness = createHeaderMockCore({ alpha: agent }, {}, {}, false);
1983
+
1984
+ const inspector = new WebInspectorElement();
1985
+ document.body.appendChild(inspector);
1986
+ inspector.core = harness.core as unknown as WebInspectorElement["core"];
1987
+ harness.emitAgentsChanged();
1988
+
1989
+ const internals = inspector as unknown as {
1990
+ isOpen: boolean;
1991
+ handleMenuSelect: (key: "threads") => void;
1992
+ };
1993
+ internals.isOpen = true;
1994
+ internals.handleMenuSelect("threads");
1995
+ await inspector.updateComplete;
1996
+
1997
+ expect(vi.getTimerCount()).toBe(1);
1998
+ inspector.remove();
1999
+ expect(clearTimeoutSpy).toHaveBeenCalled();
2000
+ expect(vi.getTimerCount()).toBe(0);
2001
+ });
2002
+
2003
+ it("cancels the deferred video idle callback when disconnected before load", async () => {
2004
+ vi.useFakeTimers();
2005
+ vi.stubGlobal("matchMedia", () => ({ matches: false }));
2006
+ const requestIdleCallback = vi.fn(() => 123);
2007
+ const cancelIdleCallback = vi.fn();
2008
+ Object.defineProperty(window, "requestIdleCallback", {
2009
+ configurable: true,
2010
+ value: requestIdleCallback,
2011
+ });
2012
+ Object.defineProperty(window, "cancelIdleCallback", {
2013
+ configurable: true,
2014
+ value: cancelIdleCallback,
2015
+ });
2016
+
2017
+ const { agent } = createMockAgent("alpha");
2018
+ const harness = createHeaderMockCore({ alpha: agent }, {}, {}, false);
2019
+
2020
+ const inspector = new WebInspectorElement();
2021
+ document.body.appendChild(inspector);
2022
+ inspector.core = harness.core as unknown as WebInspectorElement["core"];
2023
+ harness.emitAgentsChanged();
2024
+
2025
+ const internals = inspector as unknown as {
2026
+ isOpen: boolean;
2027
+ handleMenuSelect: (key: "threads") => void;
2028
+ };
2029
+ internals.isOpen = true;
2030
+ internals.handleMenuSelect("threads");
2031
+ await inspector.updateComplete;
2032
+
2033
+ expect(requestIdleCallback).toHaveBeenCalledTimes(1);
2034
+ inspector.remove();
2035
+ expect(cancelIdleCallback).toHaveBeenCalledWith(123);
2036
+ });
2037
+
2038
+ it("does not render example threads once real threads are present", async () => {
2039
+ const inspector = new WebInspectorElement();
2040
+ document.body.appendChild(inspector);
2041
+ inspector.core = createHeaderMockCore({}, {}, {}, true)
2042
+ .core as unknown as WebInspectorElement["core"];
2043
+
2044
+ const internals = inspector as unknown as InspectorThreadViewInternals;
2045
+ internals.isOpen = true;
2046
+ internals.selectedMenu = "threads";
2047
+ internals._threads = [
2048
+ {
2049
+ id: "real-thread",
2050
+ name: "Real customer thread",
2051
+ agentId: "alpha",
2052
+ updatedAt: "2026-06-25T10:00:00.000Z",
2053
+ },
2054
+ ];
2055
+ internals._threadsByAgent = new Map([["alpha", internals._threads]]);
2056
+ inspector.requestUpdate();
2057
+ await inspector.updateComplete;
2058
+
2059
+ const text = threadListText(inspector);
2060
+ expect(text).toContain("Real customer thread");
2061
+ expect(text).not.toContain("Realtime thread sync");
2062
+ expect(text).not.toContain("Example");
2063
+ });
2064
+
2065
+ it("selects an example thread, shows the tour, and toggles back to the overview on second click", async () => {
2066
+ const { agent } = createMockAgent("alpha");
2067
+ const harness = createHeaderMockCore({ alpha: agent }, {}, {}, true);
2068
+
2069
+ const inspector = new WebInspectorElement();
2070
+ document.body.appendChild(inspector);
2071
+ inspector.core = harness.core as unknown as WebInspectorElement["core"];
2072
+ harness.emitAgentsChanged();
2073
+
2074
+ const internals = inspector as unknown as {
2075
+ isOpen: boolean;
2076
+ handleMenuSelect: (key: "threads") => void;
2077
+ selectedThreadId: string | null;
2078
+ };
2079
+ internals.isOpen = true;
2080
+ internals.handleMenuSelect("threads");
2081
+ await inspector.updateComplete;
2082
+
2083
+ await vi.waitFor(() => {
2084
+ expect(threadListText(inspector)).toContain("Realtime thread sync");
2085
+ });
2086
+
2087
+ const threadList = inspector.shadowRoot?.querySelector("cpk-thread-list");
2088
+ const firstRow =
2089
+ threadList?.shadowRoot?.querySelector<HTMLElement>(".cpk-tl__item");
2090
+ expect(firstRow).toBeDefined();
2091
+
2092
+ firstRow!.click();
2093
+ await inspector.updateComplete;
2094
+ await vi.waitFor(() => {
2095
+ expect(internals.selectedThreadId).toBe("example-realtime-sync");
2096
+ expect(
2097
+ inspector.shadowRoot?.querySelector("cpk-thread-details"),
2098
+ ).not.toBe(null);
2099
+ expect(inspector.shadowRoot?.textContent ?? "").toContain(
2100
+ "Read the run as a story",
2101
+ );
2102
+ });
2103
+
2104
+ firstRow!.click();
2105
+ await inspector.updateComplete;
2106
+
2107
+ await vi.waitFor(() => {
2108
+ expect(internals.selectedThreadId).toBeNull();
2109
+ expect(
2110
+ inspector.shadowRoot?.querySelector("cpk-thread-details"),
2111
+ ).toBeNull();
2112
+ expect(inspector.shadowRoot?.textContent ?? "").toContain(
2113
+ "Threads are persistent, inspectable conversations",
2114
+ );
2115
+ });
2116
+ });
2117
+
2118
+ it("persists example tour dismissal so it does not auto-open again", async () => {
2119
+ const stored = new Map<string, string>();
2120
+ vi.stubGlobal("localStorage", {
2121
+ getItem: (key: string) => stored.get(key) ?? null,
2122
+ setItem: (key: string, value: string) => stored.set(key, value),
2123
+ removeItem: (key: string) => stored.delete(key),
2124
+ clear: () => stored.clear(),
2125
+ get length() {
2126
+ return stored.size;
2127
+ },
2128
+ key: (index: number) => Array.from(stored.keys())[index] ?? null,
2129
+ });
2130
+
2131
+ const { agent } = createMockAgent("alpha");
2132
+ const harness = createHeaderMockCore({ alpha: agent }, {}, {}, false);
2133
+
2134
+ const inspector = new WebInspectorElement();
2135
+ document.body.appendChild(inspector);
2136
+ inspector.core = harness.core as unknown as WebInspectorElement["core"];
2137
+ harness.emitAgentsChanged();
2138
+
2139
+ const internals = inspector as unknown as {
2140
+ isOpen: boolean;
2141
+ handleMenuSelect: (key: "threads") => void;
2142
+ };
2143
+ internals.isOpen = true;
2144
+ internals.handleMenuSelect("threads");
2145
+ await inspector.updateComplete;
2146
+
2147
+ await vi.waitFor(() => {
2148
+ expect(threadListText(inspector)).toContain("Realtime thread sync");
2149
+ });
2150
+
2151
+ const firstRow = inspector.shadowRoot
2152
+ ?.querySelector("cpk-thread-list")
2153
+ ?.shadowRoot?.querySelector<HTMLElement>(".cpk-tl__item");
2154
+ firstRow!.click();
2155
+ await inspector.updateComplete;
2156
+
2157
+ await vi.waitFor(() => {
2158
+ expect(inspector.shadowRoot?.textContent ?? "").toContain(
2159
+ "Read the run as a story",
2160
+ );
2161
+ });
2162
+
2163
+ const skip = Array.from(
2164
+ inspector.shadowRoot?.querySelectorAll<HTMLButtonElement>("button") ?? [],
2165
+ ).find((button) => button.textContent?.trim() === "Skip");
2166
+ expect(skip).toBeDefined();
2167
+ skip!.click();
2168
+ await inspector.updateComplete;
2169
+
2170
+ expect(stored.get("cpk:inspector:threads-example-tour:v1")).toContain(
2171
+ '"dismissed":true',
2172
+ );
2173
+
2174
+ const secondInspector = new WebInspectorElement();
2175
+ document.body.appendChild(secondInspector);
2176
+ secondInspector.core =
2177
+ harness.core as unknown as WebInspectorElement["core"];
2178
+ harness.emitAgentsChanged();
2179
+ const secondInternals = secondInspector as unknown as {
2180
+ isOpen: boolean;
2181
+ handleMenuSelect: (key: "threads") => void;
2182
+ };
2183
+ secondInternals.isOpen = true;
2184
+ secondInternals.handleMenuSelect("threads");
2185
+ await secondInspector.updateComplete;
2186
+
2187
+ await vi.waitFor(() => {
2188
+ expect(threadListText(secondInspector)).toContain("Realtime thread sync");
2189
+ });
2190
+
2191
+ const secondRow = secondInspector.shadowRoot
2192
+ ?.querySelector("cpk-thread-list")
2193
+ ?.shadowRoot?.querySelector<HTMLElement>(".cpk-tl__item");
2194
+ secondRow!.click();
2195
+ await secondInspector.updateComplete;
2196
+
2197
+ await vi.waitFor(() => {
2198
+ expect(
2199
+ secondInspector.shadowRoot?.querySelector("cpk-thread-details"),
2200
+ ).not.toBeNull();
2201
+ });
2202
+ expect(secondInspector.shadowRoot?.textContent ?? "").not.toContain(
2203
+ "Read the run as a story",
2204
+ );
2205
+ expect(secondInspector.shadowRoot?.textContent ?? "").toContain(
2206
+ "Show tour",
1772
2207
  );
2208
+
2209
+ const showTour = Array.from(
2210
+ secondInspector.shadowRoot?.querySelectorAll<HTMLButtonElement>(
2211
+ "button",
2212
+ ) ?? [],
2213
+ ).find((button) => button.textContent?.trim() === "Show tour");
2214
+ expect(showTour).toBeDefined();
2215
+ showTour!.click();
2216
+ await secondInspector.updateComplete;
2217
+
2218
+ await vi.waitFor(() => {
2219
+ expect(secondInspector.shadowRoot?.textContent ?? "").toContain(
2220
+ "Read the run as a story",
2221
+ );
2222
+ });
2223
+ });
2224
+
2225
+ it("tracks example thread selection and tour dismissal telemetry", async () => {
2226
+ const { agent } = createMockAgent("alpha");
2227
+ const harness = createHeaderMockCore({ alpha: agent }, {}, {}, false);
2228
+
2229
+ const inspector = new WebInspectorElement();
2230
+ document.body.appendChild(inspector);
2231
+ inspector.core = harness.core as unknown as WebInspectorElement["core"];
2232
+ harness.emitAgentsChanged();
2233
+
2234
+ const internals = inspector as unknown as {
2235
+ isOpen: boolean;
2236
+ handleMenuSelect: (key: "threads") => void;
2237
+ };
2238
+ internals.isOpen = true;
2239
+ internals.handleMenuSelect("threads");
2240
+ await inspector.updateComplete;
2241
+
2242
+ await vi.waitFor(() => {
2243
+ expect(threadListText(inspector)).toContain("Realtime thread sync");
2244
+ });
2245
+
2246
+ const firstRow = inspector.shadowRoot
2247
+ ?.querySelector("cpk-thread-list")
2248
+ ?.shadowRoot?.querySelector<HTMLElement>(".cpk-tl__item");
2249
+ firstRow!.click();
2250
+ await inspector.updateComplete;
2251
+
2252
+ await vi.waitFor(() => {
2253
+ expect(inspector.shadowRoot?.textContent ?? "").toContain(
2254
+ "Read the run as a story",
2255
+ );
2256
+ });
2257
+
2258
+ const skip = Array.from(
2259
+ inspector.shadowRoot?.querySelectorAll<HTMLButtonElement>("button") ?? [],
2260
+ ).find((button) => button.textContent?.trim() === "Skip");
2261
+ skip!.click();
2262
+ await inspector.updateComplete;
2263
+
2264
+ const posts = telemetryPosts();
2265
+ expect(
2266
+ posts.some(
2267
+ (post) => post.event === "oss.inspector.threads_example_selected",
2268
+ ),
2269
+ ).toBe(true);
2270
+ expect(
2271
+ posts.some(
2272
+ (post) => post.event === "oss.inspector.threads_example_tour_started",
2273
+ ),
2274
+ ).toBe(true);
2275
+ const stepViewed = posts.find(
2276
+ (post) => post.event === "oss.inspector.threads_example_tour_step_viewed",
2277
+ );
2278
+ expect(stepViewed?.properties).toMatchObject({
2279
+ example_thread_id: "example-realtime-sync",
2280
+ tour_step: 1,
2281
+ });
2282
+ const dismissed = posts.find(
2283
+ (post) => post.event === "oss.inspector.threads_example_tour_dismissed",
2284
+ );
2285
+ expect(dismissed?.properties).toMatchObject({
2286
+ example_thread_id: "example-realtime-sync",
2287
+ dismiss_method: "skip",
2288
+ });
2289
+ });
2290
+ });
2291
+
2292
+ // ─────────────────────────────────────────────────────────────────────────
2293
+ // Wave 6 — Memories tab + cpk-memory-list coverage
2294
+ // ─────────────────────────────────────────────────────────────────────────
2295
+ //
2296
+ // 6.1 Helpers: makeCoreWithMemory / makeCoreNoIntelligence / mountMemories
2297
+ // 6.2 Subscription: inspector._memories is seeded from store
2298
+ // 6.3 Tab presence: "Learning" label appears in the rendered menu
2299
+ // 6.4 View states: locked teaser vs. enabled empty vs. enabled with cards
2300
+ // 6.5 cpk-memory-list: cards, kind filter, search filter, empty state
2301
+ // 6.6 Passive guard: inspector reads from core.getMemoryStore(), never creates its own
2302
+
2303
+ // ── 6.1 Helpers ──────────────────────────────────────────────────────────
2304
+
2305
+ type MemoryStoreState = {
2306
+ memories: Memory[];
2307
+ isLoading: boolean;
2308
+ isMutating: boolean;
2309
+ error: Error | null;
2310
+ context: null;
2311
+ sessionId: number;
2312
+ available: boolean;
2313
+ realtimeStatus: "connecting" | "connected" | "unavailable";
2314
+ };
2315
+
2316
+ /**
2317
+ * Returns a minimal mock memory store seeded with the given memories and
2318
+ * availability flag. The `select(selector)` method returns an Observable-like
2319
+ * that calls the subscriber once synchronously with the derived value, then
2320
+ * never again — sufficient for the inspector's subscription wiring.
2321
+ */
2322
+ function makeMockMemoryStore(
2323
+ memories: Memory[],
2324
+ available: boolean,
2325
+ realtimeStatus: MemoryStoreState["realtimeStatus"] = "connected",
2326
+ ): { store: ReturnType<typeof buildStore>; state: MemoryStoreState } {
2327
+ const state: MemoryStoreState = {
2328
+ memories,
2329
+ isLoading: false,
2330
+ isMutating: false,
2331
+ error: null,
2332
+ context: null,
2333
+ sessionId: 0,
2334
+ available,
2335
+ realtimeStatus,
2336
+ };
2337
+
2338
+ function buildStore() {
2339
+ return {
2340
+ getState: () => state,
2341
+ select: <T>(selector: (s: MemoryStoreState) => T) => ({
2342
+ subscribe: (cb: (v: T) => void) => {
2343
+ cb(selector(state));
2344
+ return { unsubscribe: () => undefined };
2345
+ },
2346
+ }),
2347
+ };
2348
+ }
2349
+
2350
+ const store = buildStore();
2351
+ return { store, state };
2352
+ }
2353
+
2354
+ type MemoryMockCore = {
2355
+ agents: Record<string, AbstractAgent>;
2356
+ context: Record<string, unknown>;
2357
+ properties: Record<string, unknown>;
2358
+ telemetryDisabled: boolean;
2359
+ runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus;
2360
+ intelligence: { wsUrl: string } | undefined;
2361
+ subscribe: (subscriber: CopilotKitCoreSubscriber) => {
2362
+ unsubscribe: () => void;
2363
+ };
2364
+ getThreadStores: () => Record<string, never>;
2365
+ getThreadStore: (agentId: string) => undefined;
2366
+ getMemoryStore: () => ReturnType<typeof makeMockMemoryStore>["store"];
2367
+ };
2368
+
2369
+ /**
2370
+ * Returns a mock core with an intelligence property set (so the memories view
2371
+ * is not locked by the intelligence guard) and a memory store seeded with the
2372
+ * supplied memories. Pass `available: false` to simulate memories being
2373
+ * unavailable (which also locks the view).
2374
+ */
2375
+ function makeCoreWithMemory(
2376
+ memories: Memory[],
2377
+ opts: {
2378
+ available?: boolean;
2379
+ telemetryDisabled?: boolean;
2380
+ realtimeStatus?: MemoryStoreState["realtimeStatus"];
2381
+ } = {},
2382
+ ): MemoryMockCore {
2383
+ const available = opts.available ?? true;
2384
+ const { store } = makeMockMemoryStore(
2385
+ memories,
2386
+ available,
2387
+ opts.realtimeStatus ?? "connected",
2388
+ );
2389
+
2390
+ return {
2391
+ agents: {},
2392
+ context: {},
2393
+ properties: {},
2394
+ telemetryDisabled: opts.telemetryDisabled ?? false,
2395
+ runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected,
2396
+ // Intelligence present → locked teaser is NOT shown (unless available=false).
2397
+ intelligence: { wsUrl: "wss://localhost" },
2398
+ subscribe: (_subscriber: CopilotKitCoreSubscriber) => ({
2399
+ unsubscribe: () => undefined,
2400
+ }),
2401
+ getThreadStores: () => ({}),
2402
+ getThreadStore: (_agentId: string) => undefined,
2403
+ getMemoryStore: () => store,
2404
+ };
2405
+ }
2406
+
2407
+ /**
2408
+ * Returns a mock core that has NO intelligence property. Used to assert the
2409
+ * locked teaser regardless of memory availability.
2410
+ */
2411
+ function makeCoreNoIntelligence(): MemoryMockCore {
2412
+ const { store } = makeMockMemoryStore([], true);
2413
+
2414
+ return {
2415
+ agents: {},
2416
+ context: {},
2417
+ properties: {},
2418
+ telemetryDisabled: false,
2419
+ runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected,
2420
+ intelligence: undefined,
2421
+ subscribe: (_subscriber: CopilotKitCoreSubscriber) => ({
2422
+ unsubscribe: () => undefined,
2423
+ }),
2424
+ getThreadStores: () => ({}),
2425
+ getThreadStore: (_agentId: string) => undefined,
2426
+ getMemoryStore: () => store,
2427
+ };
2428
+ }
2429
+
2430
+ /**
2431
+ * Mounts a `<cpk-web-inspector>` with the given core, opens it, and switches
2432
+ * to the memories tab. Returns the element ready for assertion.
2433
+ */
2434
+ async function mountMemories(
2435
+ core: MemoryMockCore,
2436
+ ): Promise<WebInspectorElement> {
2437
+ const el = new WebInspectorElement();
2438
+ document.body.appendChild(el);
2439
+ // CopilotKitCore is a full class — our mock covers what the inspector reads.
2440
+ el.core = core as unknown as WebInspectorElement["core"];
2441
+
2442
+ // Open the inspector window so the tab content is rendered.
2443
+ const internals = el as unknown as {
2444
+ isOpen: boolean;
2445
+ handleMenuSelect: (key: string) => void;
2446
+ };
2447
+ internals.isOpen = true;
2448
+ internals.handleMenuSelect("memories");
2449
+
2450
+ await el.updateComplete;
2451
+ return el;
2452
+ }
2453
+
2454
+ // ── 6.2 Subscription ─────────────────────────────────────────────────────
2455
+
2456
+ describe("WebInspectorElement memories — subscription", () => {
2457
+ beforeEach(() => {
2458
+ document.body.innerHTML = "";
2459
+ vi.stubGlobal("localStorage", {
2460
+ getItem: () => null,
2461
+ setItem: () => undefined,
2462
+ removeItem: () => undefined,
2463
+ clear: () => undefined,
2464
+ get length() {
2465
+ return 0;
2466
+ },
2467
+ key: () => null,
2468
+ });
2469
+ });
2470
+
2471
+ afterEach(() => {
2472
+ vi.unstubAllGlobals();
2473
+ });
2474
+
2475
+ it("seeds _memories from core.getMemoryStore() on Memories-tab activation", async () => {
2476
+ const oneMemory: Memory = {
2477
+ id: "m1",
2478
+ kind: "topical",
2479
+ scope: "user",
2480
+ content: "Likes dogs",
2481
+ sourceThreadIds: [],
2482
+ invalidatedAt: null,
2483
+ };
2484
+
2485
+ const core = makeCoreWithMemory([oneMemory]);
2486
+ const el = await mountMemories(core);
2487
+
2488
+ const ids = (el as unknown as { _memories: Memory[] })._memories.map(
2489
+ (m) => m.id,
2490
+ );
2491
+
2492
+ expect(ids).toEqual(["m1"]);
2493
+ });
2494
+ });
2495
+
2496
+ // ── 6.3 Tab presence ─────────────────────────────────────────────────────
2497
+
2498
+ describe("WebInspectorElement memories — tab presence", () => {
2499
+ beforeEach(() => {
2500
+ document.body.innerHTML = "";
2501
+ vi.stubGlobal("localStorage", {
2502
+ getItem: () => null,
2503
+ setItem: () => undefined,
2504
+ removeItem: () => undefined,
2505
+ clear: () => undefined,
2506
+ get length() {
2507
+ return 0;
2508
+ },
2509
+ key: () => null,
2510
+ });
2511
+ });
2512
+
2513
+ afterEach(() => {
2514
+ vi.unstubAllGlobals();
2515
+ });
2516
+
2517
+ it("renders a Learning tab button in the inspector menu", async () => {
2518
+ const core = makeCoreWithMemory([]);
2519
+ const el = await mountMemories(core);
2520
+
2521
+ const buttons = Array.from(
2522
+ el.shadowRoot?.querySelectorAll<HTMLButtonElement>("button") ?? [],
2523
+ );
2524
+ const memoriesButton = buttons.find((btn) =>
2525
+ btn.textContent?.trim().includes("Learning"),
2526
+ );
2527
+
2528
+ expect(memoriesButton, "Learning tab button should render").toBeDefined();
2529
+ });
2530
+ });
2531
+
2532
+ // ── 6.4 View states ──────────────────────────────────────────────────────
2533
+
2534
+ describe("WebInspectorElement memories — view states", () => {
2535
+ beforeEach(() => {
2536
+ document.body.innerHTML = "";
2537
+ vi.stubGlobal("localStorage", {
2538
+ getItem: () => null,
2539
+ setItem: () => undefined,
2540
+ removeItem: () => undefined,
2541
+ clear: () => undefined,
2542
+ get length() {
2543
+ return 0;
2544
+ },
2545
+ key: () => null,
2546
+ });
2547
+ });
2548
+
2549
+ afterEach(() => {
2550
+ vi.unstubAllGlobals();
2551
+ });
2552
+
2553
+ it("renders the locked teaser when intelligence is absent", async () => {
2554
+ const core = makeCoreNoIntelligence();
2555
+ const el = await mountMemories(core);
2556
+
2557
+ const text = el.shadowRoot?.textContent ?? "";
2558
+ expect(text).toContain("Long-term memory");
2559
+ expect(text).toContain(
2560
+ "Long-term memory isn't enabled on this deployment.",
2561
+ );
2562
+ const memoryList = el.shadowRoot?.querySelector("cpk-memory-list");
2563
+ expect(
2564
+ memoryList,
2565
+ "cpk-memory-list should NOT render when locked",
2566
+ ).toBeNull();
2567
+ });
2568
+
2569
+ it("does not use Threads onboarding UTM attribution for locked memory CTAs", async () => {
2570
+ const core = makeCoreNoIntelligence();
2571
+ const el = await mountMemories(core);
2572
+
2573
+ const talkToEngineer = el.shadowRoot?.querySelector<HTMLAnchorElement>(
2574
+ 'a[href^="https://www.copilotkit.ai/talk-to-an-engineer"]',
2575
+ );
2576
+ const signup = el.shadowRoot?.querySelector<HTMLAnchorElement>(
2577
+ 'a[href^="https://go.copilotkit.ai/intelligence-signup"]',
2578
+ );
2579
+
2580
+ expect(talkToEngineer).not.toBeNull();
2581
+ expect(signup).not.toBeNull();
2582
+
2583
+ for (const href of [talkToEngineer!.href, signup!.href]) {
2584
+ const url = new URL(href);
2585
+ expect(url.searchParams.get("ref")).toBeTruthy();
2586
+ expect(url.searchParams.has("utm_source")).toBe(false);
2587
+ expect(url.searchParams.has("utm_medium")).toBe(false);
2588
+ expect(url.searchParams.has("utm_campaign")).toBe(false);
2589
+ }
2590
+ });
2591
+
2592
+ it("renders the locked teaser when memories are unavailable", async () => {
2593
+ const core = makeCoreWithMemory([], { available: false });
2594
+ const el = await mountMemories(core);
2595
+
2596
+ const text = el.shadowRoot?.textContent ?? "";
2597
+ expect(text).toContain("Long-term memory");
2598
+ const memoryList = el.shadowRoot?.querySelector("cpk-memory-list");
2599
+ expect(
2600
+ memoryList,
2601
+ "cpk-memory-list should NOT render when unavailable",
2602
+ ).toBeNull();
2603
+ });
2604
+
2605
+ it("renders cpk-memory-list with empty state when available and no memories", async () => {
2606
+ const core = makeCoreWithMemory([], { available: true });
2607
+ const el = await mountMemories(core);
2608
+
2609
+ const memoryList = el.shadowRoot?.querySelector("cpk-memory-list");
2610
+ expect(
2611
+ memoryList,
2612
+ "cpk-memory-list should render when enabled",
2613
+ ).not.toBeNull();
2614
+
2615
+ await (memoryList as unknown as { updateComplete: Promise<void> })
2616
+ .updateComplete;
2617
+ const listText = memoryList?.shadowRoot?.textContent ?? "";
2618
+ expect(listText).toContain("No memories yet");
2619
+ });
2620
+
2621
+ it("keeps the list rendered (not the full-screen error) when a mutation error arrives with memories present", async () => {
2622
+ // INSP-2: a failed remove/update sets the store error while a valid list is
2623
+ // already on screen. That must NOT blank the list with the full-screen
2624
+ // "Failed to load memories" state — the error is surfaced inline instead.
2625
+ const oneMemory: Memory = {
2626
+ id: "m1",
2627
+ kind: "topical",
2628
+ scope: "user",
2629
+ content: "Likes dogs",
2630
+ sourceThreadIds: [],
2631
+ invalidatedAt: null,
2632
+ };
2633
+
2634
+ const core = makeCoreWithMemory([oneMemory]);
2635
+ const el = await mountMemories(core);
2636
+
2637
+ // Simulate a mutation failure landing after the list is rendered.
2638
+ (el as unknown as { _memoriesError: Error | null })._memoriesError =
2639
+ new Error("could not delete memory");
2640
+ el.requestUpdate();
2641
+ await el.updateComplete;
2642
+
2643
+ // The list survives.
2644
+ const memoryList = el.shadowRoot?.querySelector("cpk-memory-list");
2645
+ expect(
2646
+ memoryList,
2647
+ "cpk-memory-list must remain rendered on a mutation error",
2648
+ ).not.toBeNull();
2649
+
2650
+ const text = el.shadowRoot?.textContent ?? "";
2651
+ // Inline, non-blocking error with distinct copy.
2652
+ expect(text).toContain("Action failed: could not delete memory");
2653
+ // The full-screen load-failure copy must NOT appear.
2654
+ expect(text).not.toContain("Failed to load memories");
2655
+ });
2656
+
2657
+ it("shows the full-screen load error only when no memories are loaded", async () => {
2658
+ // INSP-2 counterpart: a snapshot-load failure (empty list) still shows the
2659
+ // full-screen "Failed to load memories" state.
2660
+ const core = makeCoreWithMemory([]);
2661
+ const el = await mountMemories(core);
2662
+
2663
+ (el as unknown as { _memoriesError: Error | null })._memoriesError =
2664
+ new Error("network down");
2665
+ el.requestUpdate();
2666
+ await el.updateComplete;
2667
+
2668
+ const text = el.shadowRoot?.textContent ?? "";
2669
+ expect(text).toContain("Failed to load memories");
2670
+ expect(text).toContain("network down");
2671
+ expect(text).not.toContain("Action failed:");
2672
+ const memoryList = el.shadowRoot?.querySelector("cpk-memory-list");
2673
+ expect(memoryList).toBeNull();
2674
+ });
2675
+
2676
+ it("shows the 'live' indicator only when realtime is connected", async () => {
2677
+ const core = makeCoreWithMemory([], {
2678
+ available: true,
2679
+ realtimeStatus: "connected",
2680
+ });
2681
+ const el = await mountMemories(core);
2682
+
2683
+ const text = el.shadowRoot?.textContent ?? "";
2684
+ expect(text).toContain("live");
2685
+ expect(text).not.toContain("offline");
2686
+ expect(text).not.toContain("reconnecting");
2687
+ });
2688
+
2689
+ it("shows a muted 'reconnecting' indicator while realtime is connecting", async () => {
2690
+ const core = makeCoreWithMemory([], {
2691
+ available: true,
2692
+ realtimeStatus: "connecting",
2693
+ });
2694
+ const el = await mountMemories(core);
2695
+
2696
+ const text = el.shadowRoot?.textContent ?? "";
2697
+ expect(text).toContain("reconnecting");
2698
+ // It must NOT claim "live" while still connecting.
2699
+ expect(text).not.toMatch(/>\s*live\s*</);
2700
+ });
2701
+
2702
+ it("shows a muted 'offline' indicator when realtime has permanently given up", async () => {
2703
+ const core = makeCoreWithMemory([], {
2704
+ available: true,
2705
+ realtimeStatus: "unavailable",
2706
+ });
2707
+ const el = await mountMemories(core);
2708
+
2709
+ const text = el.shadowRoot?.textContent ?? "";
2710
+ expect(text).toContain("offline");
2711
+ // The frozen snapshot must NOT be labelled "live".
2712
+ expect(text).not.toMatch(/>\s*live\s*</);
2713
+ });
2714
+
2715
+ it("renders cpk-memory-list with a card when one memory is present", async () => {
2716
+ const oneMemory: Memory = {
2717
+ id: "m1",
2718
+ kind: "topical",
2719
+ scope: "user",
2720
+ content: "Prefers dark mode",
2721
+ sourceThreadIds: [],
2722
+ invalidatedAt: null,
2723
+ };
2724
+
2725
+ const core = makeCoreWithMemory([oneMemory]);
2726
+ const el = await mountMemories(core);
2727
+
2728
+ const memoryList = el.shadowRoot?.querySelector("cpk-memory-list");
2729
+ expect(memoryList, "cpk-memory-list should render").not.toBeNull();
2730
+
2731
+ await (memoryList as unknown as { updateComplete: Promise<void> })
2732
+ .updateComplete;
2733
+ const cards = memoryList?.shadowRoot?.querySelectorAll(".cpk-ml__card");
2734
+ expect(cards?.length).toBe(1);
2735
+ });
2736
+ });
2737
+
2738
+ // ── 6.5 cpk-memory-list ──────────────────────────────────────────────────
2739
+
2740
+ describe("cpk-memory-list", () => {
2741
+ const threeMemories: Memory[] = [
2742
+ {
2743
+ id: "t1",
2744
+ kind: "topical",
2745
+ scope: "user",
2746
+ content: "Likes cats",
2747
+ sourceThreadIds: [],
2748
+ invalidatedAt: null,
2749
+ },
2750
+ {
2751
+ id: "e1",
2752
+ kind: "episodic",
2753
+ scope: "user",
2754
+ content: "First login was on a Monday",
2755
+ sourceThreadIds: [],
2756
+ invalidatedAt: null,
2757
+ },
2758
+ {
2759
+ id: "o1",
2760
+ kind: "operational",
2761
+ scope: "user",
2762
+ content: "Deploys on Thursdays",
2763
+ sourceThreadIds: [],
2764
+ invalidatedAt: null,
2765
+ },
2766
+ ];
2767
+
2768
+ /** Create and mount a standalone cpk-memory-list element. */
2769
+ async function mountList(memories: Memory[]): Promise<Element> {
2770
+ const el = document.createElement("cpk-memory-list");
2771
+ document.body.appendChild(el);
2772
+ // Assign memories via property (same as Lit's .memories=${...} binding).
2773
+ (el as unknown as { memories: Memory[] }).memories = memories;
2774
+ // Trigger update if the element is a Lit element.
2775
+ if ("updateComplete" in el) {
2776
+ await (el as unknown as { updateComplete: Promise<void> }).updateComplete;
2777
+ }
2778
+ return el;
2779
+ }
2780
+
2781
+ beforeEach(() => {
2782
+ document.body.innerHTML = "";
2783
+ });
2784
+
2785
+ it("renders one card per memory in order", async () => {
2786
+ const el = await mountList(threeMemories);
2787
+ const cards = el.shadowRoot?.querySelectorAll(".cpk-ml__card");
2788
+ expect(cards?.length).toBe(3);
2789
+ const contents = Array.from(cards ?? []).map((card) =>
2790
+ card.querySelector(".cpk-ml__content")?.textContent?.trim(),
2791
+ );
2792
+ expect(contents).toEqual([
2793
+ "Likes cats",
2794
+ "First login was on a Monday",
2795
+ "Deploys on Thursdays",
2796
+ ]);
2797
+ });
2798
+
2799
+ it("narrows cards when an operational kind filter is clicked", async () => {
2800
+ const el = await mountList(threeMemories);
2801
+
2802
+ const operationalSeg = el.shadowRoot?.querySelector<HTMLElement>(
2803
+ '[data-kind="operational"]',
2804
+ );
2805
+ expect(
2806
+ operationalSeg,
2807
+ "operational filter segment should exist",
2808
+ ).not.toBeNull();
2809
+
2810
+ operationalSeg!.click();
2811
+ await (el as unknown as { updateComplete: Promise<void> }).updateComplete;
2812
+
2813
+ const cards = el.shadowRoot?.querySelectorAll(".cpk-ml__card");
2814
+ expect(cards?.length).toBe(1);
2815
+ expect(
2816
+ cards?.[0]?.querySelector(".cpk-ml__content")?.textContent?.trim(),
2817
+ ).toBe("Deploys on Thursdays");
2818
+ });
2819
+
2820
+ it("filters cards by search text (case-insensitive)", async () => {
2821
+ const el = await mountList(threeMemories);
2822
+
2823
+ const searchInput = el.shadowRoot?.querySelector<HTMLInputElement>(
2824
+ ".cpk-ml__search-input",
2825
+ );
2826
+ expect(searchInput, "search input should exist").not.toBeNull();
2827
+
2828
+ searchInput!.value = "deploy";
2829
+ searchInput!.dispatchEvent(new Event("input", { bubbles: true }));
2830
+ await (el as unknown as { updateComplete: Promise<void> }).updateComplete;
2831
+
2832
+ const cards = el.shadowRoot?.querySelectorAll(".cpk-ml__card");
2833
+ expect(cards?.length).toBe(1);
2834
+ expect(
2835
+ cards?.[0]?.querySelector(".cpk-ml__content")?.textContent?.trim(),
2836
+ ).toBe("Deploys on Thursdays");
2837
+ });
2838
+
2839
+ it("shows the empty state when memories is empty", async () => {
2840
+ const el = await mountList([]);
2841
+ const empty = el.shadowRoot?.querySelector(".cpk-ml__empty");
2842
+ expect(empty, "empty state should render").not.toBeNull();
2843
+ expect(el.shadowRoot?.textContent ?? "").toContain("No memories yet");
2844
+ const cards = el.shadowRoot?.querySelectorAll(".cpk-ml__card");
2845
+ expect(cards?.length ?? 0).toBe(0);
2846
+ });
2847
+ });
2848
+
2849
+ // ── 6.6 Passive guard ────────────────────────────────────────────────────
2850
+
2851
+ describe("WebInspectorElement memories — passive store guard", () => {
2852
+ beforeEach(() => {
2853
+ document.body.innerHTML = "";
2854
+ vi.stubGlobal("localStorage", {
2855
+ getItem: () => null,
2856
+ setItem: () => undefined,
2857
+ removeItem: () => undefined,
2858
+ clear: () => undefined,
2859
+ get length() {
2860
+ return 0;
2861
+ },
2862
+ key: () => null,
2863
+ });
2864
+ });
2865
+
2866
+ afterEach(() => {
2867
+ vi.unstubAllGlobals();
2868
+ });
2869
+
2870
+ it("calls core.getMemoryStore() on tab activation and reads from the returned store", async () => {
2871
+ const core = makeCoreWithMemory([]);
2872
+ const spy = vi.spyOn(core, "getMemoryStore");
2873
+
2874
+ await mountMemories(core);
2875
+
2876
+ expect(spy).toHaveBeenCalled();
2877
+
2878
+ // The store instance that spy captured is the exact same object that
2879
+ // core.getMemoryStore() returns — inspector reads from it, never wraps it.
2880
+ const returnedStore = spy.mock.results[0]?.value;
2881
+ expect(returnedStore).toBeDefined();
2882
+ // Verify the inspector consumed the store by checking its getState was accessible
2883
+ // (if the inspector had created its own store instead, this reference would differ).
2884
+ expect(typeof returnedStore.getState).toBe("function");
2885
+ expect(typeof returnedStore.select).toBe("function");
2886
+ });
2887
+
2888
+ it("does NOT call core.getMemoryStore() merely by attaching the inspector", async () => {
2889
+ // INSP-1: getMemoryStore() lazily creates + starts the store and opens
2890
+ // realtime, so attaching the inspector must touch nothing. The store is
2891
+ // only created when the user activates the Memories tab.
2892
+ const core = makeCoreWithMemory([]);
2893
+ const spy = vi.spyOn(core, "getMemoryStore");
2894
+
2895
+ const el = new WebInspectorElement();
2896
+ document.body.appendChild(el);
2897
+ el.core = core as unknown as WebInspectorElement["core"];
2898
+ (el as unknown as { isOpen: boolean }).isOpen = true;
2899
+ await el.updateComplete;
2900
+
2901
+ expect(spy).not.toHaveBeenCalled();
2902
+
2903
+ // Activating the Memories tab is what creates + subscribes to the store.
2904
+ (
2905
+ el as unknown as { handleMenuSelect: (k: string) => void }
2906
+ ).handleMenuSelect("memories");
2907
+ await el.updateComplete;
2908
+
2909
+ expect(spy).toHaveBeenCalledTimes(1);
2910
+ });
2911
+
2912
+ it("does not double-subscribe when the Memories tab is re-activated", async () => {
2913
+ const core = makeCoreWithMemory([]);
2914
+ const spy = vi.spyOn(core, "getMemoryStore");
2915
+
2916
+ const el = await mountMemories(core);
2917
+ expect(spy).toHaveBeenCalledTimes(1);
2918
+
2919
+ // Re-activate the Memories tab — the guard must prevent a second
2920
+ // getMemoryStore() call (which would create a second store/realtime).
2921
+ (
2922
+ el as unknown as { handleMenuSelect: (k: string) => void }
2923
+ ).handleMenuSelect("memories");
2924
+ await el.updateComplete;
2925
+
2926
+ expect(spy).toHaveBeenCalledTimes(1);
2927
+ });
2928
+
2929
+ it("re-subscribes after detach when the Memories tab is activated again", async () => {
2930
+ const core = makeCoreWithMemory([]);
2931
+ const spy = vi.spyOn(core, "getMemoryStore");
2932
+
2933
+ const el = await mountMemories(core);
2934
+ expect(spy).toHaveBeenCalledTimes(1);
2935
+
2936
+ // Detach (core = null) must reset the lazy-subscription guard.
2937
+ el.core = null;
2938
+ await el.updateComplete;
2939
+
2940
+ // Re-attach + re-activate the tab → a fresh subscription is created.
2941
+ el.core = core as unknown as WebInspectorElement["core"];
2942
+ (el as unknown as { isOpen: boolean }).isOpen = true;
2943
+ (
2944
+ el as unknown as { handleMenuSelect: (k: string) => void }
2945
+ ).handleMenuSelect("memories");
2946
+ await el.updateComplete;
2947
+
2948
+ expect(spy).toHaveBeenCalledTimes(2);
2949
+ });
2950
+ });
2951
+
2952
+ // ── 6.6.1 Active-on-boot subscription ─────────────────────────────────────
2953
+ //
2954
+ // The memory subscription is normally created on a Memories-tab CLICK
2955
+ // (handleMenuSelect → ensureMemorySubscription). But when the inspector boots
2956
+ // with the Memories tab ALREADY active — e.g. a persisted
2957
+ // `selectedMenu: "memories"` restored by hydrateStateFromStorageEarly — no
2958
+ // click ever fires, so historically no subscription was created: the realtime
2959
+ // indicator stayed stuck on the default "connecting" (rendered "reconnecting")
2960
+ // and the list was empty until the user toggled tabs. The fix subscribes when
2961
+ // the Memories tab is the active tab on boot, gated on the active tab so it
2962
+ // still does not subscribe in apps not viewing memory (INSP-1).
2963
+
2964
+ describe("WebInspectorElement memories — active-on-boot subscription", () => {
2965
+ beforeEach(() => {
2966
+ document.body.innerHTML = "";
2967
+ // Persist `selectedMenu: "memories"` so hydrateStateFromStorageEarly (run in
2968
+ // connectedCallback, before any user interaction) restores the Memories tab
2969
+ // as the active tab — reproducing the stuck-indicator boot scenario.
2970
+ const store: Record<string, string> = {
2971
+ "cpk:inspector:state": JSON.stringify({ selectedMenu: "memories" }),
2972
+ };
2973
+ vi.stubGlobal("localStorage", {
2974
+ getItem: (key: string) => store[key] ?? null,
2975
+ setItem: (key: string, value: string) => {
2976
+ store[key] = value;
2977
+ },
2978
+ removeItem: (key: string) => {
2979
+ delete store[key];
2980
+ },
2981
+ clear: () => {
2982
+ for (const key of Object.keys(store)) delete store[key];
2983
+ },
2984
+ get length() {
2985
+ return Object.keys(store).length;
2986
+ },
2987
+ key: (index: number) => Object.keys(store)[index] ?? null,
2988
+ });
2989
+ });
2990
+
2991
+ afterEach(() => {
2992
+ vi.unstubAllGlobals();
2993
+ });
2994
+
2995
+ it("subscribes to the memory store on boot when the Memories tab is already active (no click)", async () => {
2996
+ // The store reports a live realtime status. If the inspector subscribes on
2997
+ // boot, _memoriesRealtimeStatus reflects "connected"; if it does NOT (the
2998
+ // bug), it stays on the default "connecting".
2999
+ const core = makeCoreWithMemory([], { realtimeStatus: "connected" });
3000
+ const spy = vi.spyOn(core, "getMemoryStore");
3001
+
3002
+ const el = new WebInspectorElement();
3003
+ document.body.appendChild(el);
3004
+ // connectedCallback has already restored selectedMenu = "memories".
3005
+ // Assigning core (the realistic boot path) must trigger the subscription
3006
+ // without any handleMenuSelect click.
3007
+ el.core = core as unknown as WebInspectorElement["core"];
3008
+ (el as unknown as { isOpen: boolean }).isOpen = true;
3009
+ await el.updateComplete;
3010
+
3011
+ expect(
3012
+ (el as unknown as { selectedMenu: string }).selectedMenu,
3013
+ "persisted Memories tab should be the active tab on boot",
3014
+ ).toBe("memories");
3015
+
3016
+ // The store was created + subscribed WITHOUT a tab click.
3017
+ expect(
3018
+ spy,
3019
+ "core.getMemoryStore() must be called on boot when Memories is active",
3020
+ ).toHaveBeenCalled();
3021
+
3022
+ // The live status from the store is reflected — not the stuck default.
3023
+ expect(
3024
+ (el as unknown as { _memoriesRealtimeStatus: string })
3025
+ ._memoriesRealtimeStatus,
3026
+ "realtime status must reflect the store, not the default 'connecting'",
3027
+ ).toBe("connected");
3028
+ });
3029
+
3030
+ it("does not double-subscribe when boot subscription is followed by a Memories-tab click", async () => {
3031
+ // The boot subscription must be idempotent: a later explicit click must not
3032
+ // create a second store/realtime connection.
3033
+ const core = makeCoreWithMemory([], { realtimeStatus: "connected" });
3034
+ const spy = vi.spyOn(core, "getMemoryStore");
3035
+
3036
+ const el = new WebInspectorElement();
3037
+ document.body.appendChild(el);
3038
+ el.core = core as unknown as WebInspectorElement["core"];
3039
+ (el as unknown as { isOpen: boolean }).isOpen = true;
3040
+ await el.updateComplete;
3041
+
3042
+ expect(spy).toHaveBeenCalledTimes(1);
3043
+
3044
+ (
3045
+ el as unknown as { handleMenuSelect: (k: string) => void }
3046
+ ).handleMenuSelect("memories");
3047
+ await el.updateComplete;
3048
+
3049
+ expect(spy).toHaveBeenCalledTimes(1);
3050
+ });
3051
+ });
3052
+
3053
+ // ── 6.7 Older-core compat: missing getMemoryStore ────────────────────────
3054
+ //
3055
+ // An inspector attached to an older @copilotkit/core that predates
3056
+ // getMemoryStore must not throw. The guard added in attachToCore must fall
3057
+ // through to the else branch, set _memoriesAvailable = false, and leave the
3058
+ // memories tab in the locked-teaser state — exactly like a core that defines
3059
+ // the method but returns available=false.
3060
+
3061
+ describe("WebInspectorElement memories — older-core compat (no getMemoryStore)", () => {
3062
+ beforeEach(() => {
3063
+ document.body.innerHTML = "";
3064
+ vi.stubGlobal("localStorage", {
3065
+ getItem: () => null,
3066
+ setItem: () => undefined,
3067
+ removeItem: () => undefined,
3068
+ clear: () => undefined,
3069
+ get length() {
3070
+ return 0;
3071
+ },
3072
+ key: () => null,
3073
+ });
3074
+ });
3075
+
3076
+ afterEach(() => {
3077
+ vi.unstubAllGlobals();
3078
+ });
3079
+
3080
+ it("does not throw when core lacks getMemoryStore, and renders the locked teaser", async () => {
3081
+ // Build a minimal core that does NOT define getMemoryStore — simulating an
3082
+ // older @copilotkit/core package. We deliberately omit the method rather
3083
+ // than setting it to undefined so the typeof guard fires correctly.
3084
+ const olderCore = {
3085
+ agents: {},
3086
+ context: {},
3087
+ properties: {},
3088
+ runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected,
3089
+ // intelligence present so the only lock-cause is the missing store method
3090
+ intelligence: { wsUrl: "wss://localhost" },
3091
+ subscribe: (_subscriber: CopilotKitCoreSubscriber) => ({
3092
+ unsubscribe: () => undefined,
3093
+ }),
3094
+ getThreadStores: () => ({}),
3095
+ getThreadStore: (_agentId: string) => undefined,
3096
+ // getMemoryStore intentionally absent
3097
+ };
3098
+
3099
+ const el = new WebInspectorElement();
3100
+ document.body.appendChild(el);
3101
+
3102
+ // Assigning core must not throw even though getMemoryStore is missing.
3103
+ expect(() => {
3104
+ el.core = olderCore as unknown as WebInspectorElement["core"];
3105
+ }).not.toThrow();
3106
+
3107
+ // Open and switch to the memories tab so the view state is rendered.
3108
+ const internals = el as unknown as {
3109
+ isOpen: boolean;
3110
+ handleMenuSelect: (key: string) => void;
3111
+ };
3112
+ internals.isOpen = true;
3113
+ internals.handleMenuSelect("memories");
3114
+ await el.updateComplete;
3115
+
3116
+ // The locked teaser must render — cpk-memory-list must NOT appear.
3117
+ const text = el.shadowRoot?.textContent ?? "";
3118
+ expect(text).toContain("Long-term memory");
3119
+ const memoryList = el.shadowRoot?.querySelector("cpk-memory-list");
3120
+ expect(
3121
+ memoryList,
3122
+ "cpk-memory-list must not render when getMemoryStore is absent",
3123
+ ).toBeNull();
3124
+ });
3125
+
3126
+ it("shows the SDK-upgrade teaser (distinct from the not-enabled teaser) when getMemoryStore is absent", async () => {
3127
+ // INSP-3: an older @copilotkit/core (no getMemoryStore) must guide an SDK
3128
+ // upgrade, with copy distinct from the genuine "not enabled on this
3129
+ // deployment" teaser shown by a current SDK against a memory-less backend.
3130
+ const olderCore = {
3131
+ agents: {},
3132
+ context: {},
3133
+ properties: {},
3134
+ runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected,
3135
+ intelligence: { wsUrl: "wss://localhost" },
3136
+ subscribe: (_subscriber: CopilotKitCoreSubscriber) => ({
3137
+ unsubscribe: () => undefined,
3138
+ }),
3139
+ getThreadStores: () => ({}),
3140
+ getThreadStore: (_agentId: string) => undefined,
3141
+ // getMemoryStore intentionally absent
3142
+ };
3143
+
3144
+ const el = new WebInspectorElement();
3145
+ document.body.appendChild(el);
3146
+ el.core = olderCore as unknown as WebInspectorElement["core"];
3147
+ const internals = el as unknown as {
3148
+ isOpen: boolean;
3149
+ handleMenuSelect: (key: string) => void;
3150
+ };
3151
+ internals.isOpen = true;
3152
+ internals.handleMenuSelect("memories");
3153
+ await el.updateComplete;
3154
+
3155
+ const text = el.shadowRoot?.textContent ?? "";
3156
+ expect(text).toContain("@copilotkit SDK");
3157
+ expect(text).toContain("Upgrade");
3158
+ // Must NOT show the deployment-not-enabled copy in this case.
3159
+ expect(text).not.toContain(
3160
+ "Long-term memory isn't enabled on this deployment.",
3161
+ );
3162
+ });
3163
+
3164
+ it("shows the not-enabled teaser (distinct from the upgrade teaser) when the current SDK reports memory unavailable", async () => {
3165
+ // INSP-3 counterpart: a current SDK (getMemoryStore present) whose store
3166
+ // reports available=false shows the deployment teaser, NOT upgrade copy.
3167
+ const core = makeCoreWithMemory([], { available: false });
3168
+ const el = await mountMemories(core);
3169
+
3170
+ const text = el.shadowRoot?.textContent ?? "";
3171
+ expect(text).toContain(
3172
+ "Long-term memory isn't enabled on this deployment.",
3173
+ );
3174
+ expect(text).not.toContain("@copilotkit SDK");
3175
+ });
3176
+ });
3177
+
3178
+ // ── 6.8 Memories tab telemetry gating (A7) + detach reset (A8) ────────────
3179
+ //
3180
+ // The memories tab is the only telemetry call site that must honor the host
3181
+ // `core.telemetryDisabled` opt-out and must not re-fire on every click. These
3182
+ // tests mirror the Threads tab-click telemetry test. They also cover that
3183
+ // detachFromCore resets the memory view state so a later attach to an older
3184
+ // core never leaks stale memory counts into telemetry.
3185
+
3186
+ describe("WebInspectorElement memories — tab telemetry + detach reset", () => {
3187
+ let fetchMock: ReturnType<typeof vi.fn>;
3188
+
3189
+ const telemetryPosts = () =>
3190
+ fetchMock.mock.calls
3191
+ .filter(
3192
+ (call) =>
3193
+ String(call[0]) === "https://telemetry.copilotkit.ai/ingest" &&
3194
+ (call[1] as RequestInit | undefined)?.method === "POST",
3195
+ )
3196
+ .map((call) => {
3197
+ const body =
3198
+ ((call[1] as RequestInit | undefined)?.body as string) ?? "{}";
3199
+ return JSON.parse(body) as {
3200
+ event: string;
3201
+ properties: Record<string, unknown>;
3202
+ };
3203
+ });
3204
+
3205
+ const memoriesTabClicks = () =>
3206
+ telemetryPosts().filter(
3207
+ (post) => post.event === "oss.inspector.memories_tab_clicked",
3208
+ );
3209
+
3210
+ beforeEach(() => {
3211
+ document.body.innerHTML = "";
3212
+ // localStorage not opted out → track() proceeds to the fetch sink.
3213
+ vi.stubGlobal("localStorage", {
3214
+ getItem: () => null,
3215
+ setItem: () => undefined,
3216
+ removeItem: () => undefined,
3217
+ clear: () => undefined,
3218
+ get length() {
3219
+ return 0;
3220
+ },
3221
+ key: () => null,
3222
+ });
3223
+ fetchMock = vi.fn(() =>
3224
+ Promise.resolve({
3225
+ ok: true,
3226
+ status: 200,
3227
+ json: () => Promise.resolve({}),
3228
+ }),
3229
+ );
3230
+ vi.stubGlobal("fetch", fetchMock);
3231
+ });
3232
+
3233
+ afterEach(() => {
3234
+ vi.unstubAllGlobals();
3235
+ });
3236
+
3237
+ it("posts memories_tab_clicked when the Memories tab is selected", async () => {
3238
+ const oneMemory: Memory = {
3239
+ id: "m1",
3240
+ kind: "topical",
3241
+ scope: "user",
3242
+ content: "Likes dogs",
3243
+ sourceThreadIds: [],
3244
+ invalidatedAt: null,
3245
+ };
3246
+
3247
+ const core = makeCoreWithMemory([oneMemory]);
3248
+ await mountMemories(core);
3249
+ await Promise.resolve();
3250
+
3251
+ const clicks = memoriesTabClicks();
3252
+ expect(clicks).toHaveLength(1);
3253
+ expect(clicks[0]!.properties).toMatchObject({
3254
+ memory_count: 1,
3255
+ available: true,
3256
+ });
3257
+ });
3258
+
3259
+ it("does NOT post memories_tab_clicked when core.telemetryDisabled is true", async () => {
3260
+ const core = makeCoreWithMemory([], { telemetryDisabled: true });
3261
+ await mountMemories(core);
3262
+ await Promise.resolve();
3263
+
3264
+ expect(memoriesTabClicks()).toHaveLength(0);
3265
+ });
3266
+
3267
+ it("does not double-fire when the already-active Memories tab is re-selected", async () => {
3268
+ const core = makeCoreWithMemory([]);
3269
+ const el = await mountMemories(core);
3270
+ await Promise.resolve();
3271
+
3272
+ // Re-select the already-active Memories tab.
3273
+ const internals = el as unknown as {
3274
+ handleMenuSelect: (key: string) => void;
3275
+ };
3276
+ internals.handleMenuSelect("memories");
3277
+ await el.updateComplete;
3278
+ await Promise.resolve();
3279
+
3280
+ expect(memoriesTabClicks()).toHaveLength(1);
3281
+ });
3282
+
3283
+ it("resets memory view state on detachFromCore (memories empty, count 0)", async () => {
3284
+ const oneMemory: Memory = {
3285
+ id: "m1",
3286
+ kind: "topical",
3287
+ scope: "user",
3288
+ content: "Likes dogs",
3289
+ sourceThreadIds: [],
3290
+ invalidatedAt: null,
3291
+ };
3292
+
3293
+ const core = makeCoreWithMemory([oneMemory]);
3294
+ const el = await mountMemories(core);
3295
+ await Promise.resolve();
3296
+
3297
+ // Sanity: the seeded memory is present before detach.
3298
+ expect(
3299
+ (el as unknown as { _memories: Memory[] })._memories.map((m) => m.id),
3300
+ ).toEqual(["m1"]);
3301
+
3302
+ // Reassigning core triggers detachFromCore(); null means no re-attach.
3303
+ el.core = null;
3304
+ await el.updateComplete;
3305
+
3306
+ const state = el as unknown as {
3307
+ _memories: Memory[];
3308
+ _memoriesLoading: boolean;
3309
+ _memoriesError: Error | null;
3310
+ _memoriesAvailable: boolean;
3311
+ };
3312
+ expect(state._memories).toEqual([]);
3313
+ expect(state._memories).toHaveLength(0);
3314
+ expect(state._memoriesLoading).toBe(false);
3315
+ expect(state._memoriesError).toBeNull();
3316
+ expect(state._memoriesAvailable).toBe(true);
1773
3317
  });
1774
3318
  });