shadow-claw 1.52.0 → 1.52.1

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.
Files changed (112) hide show
  1. package/.well-known/mcp/server-card.json +1 -1
  2. package/.well-known/mcp.json +1 -1
  3. package/README.md +9 -9
  4. package/dist/cli/build/build.js +1 -1
  5. package/dist/cli/{chunk-DYq-orHK.js → chunk-BgTv9w0K.js} +1 -1
  6. package/dist/cli/{chunk-C6jii2Fl.js → chunk-d5hIhj8t.js} +1 -1
  7. package/dist/cli/cli.js +1 -1
  8. package/dist/lib/components/index.js +1 -1
  9. package/dist/lib/components/shadow-claw-pages.js +1 -1
  10. package/dist/lib/index.js +1 -1
  11. package/dist/lib/{shadow-claw-pages-kA2qa-4B.js → shadow-claw-pages-CjHS50HN.js} +2 -2
  12. package/dist/lib/{shadow-claw-pages-kA2qa-4B.js.map → shadow-claw-pages-CjHS50HN.js.map} +1 -1
  13. package/dist/public/.well-known/mcp/server-card.json +1 -1
  14. package/dist/public/.well-known/mcp.json +1 -1
  15. package/dist/public/AGENTS.md +1 -1
  16. package/dist/public/README.md +9 -9
  17. package/dist/public/docs/README.md +1 -1
  18. package/dist/public/docs/architecture/orchestrator.md +2 -2
  19. package/dist/public/docs/architecture/overview.md +3 -3
  20. package/dist/public/docs/architecture/worker-protocol.md +29 -29
  21. package/dist/public/docs/example/article/index.html +1 -1
  22. package/dist/public/docs/example/article.html +1 -1
  23. package/dist/public/docs/publishing/index.html +1 -1
  24. package/dist/public/docs/publishing.html +1 -1
  25. package/dist/public/docs/skill-creator/index.html +1 -1
  26. package/dist/public/docs/skill-creator.html +1 -1
  27. package/dist/public/docs/subsystems/local-models.md +1 -1
  28. package/dist/public/docs/subsystems/providers.md +11 -4
  29. package/dist/public/e2e/README.md +2 -2
  30. package/dist/public/index.html +1 -1
  31. package/dist/public/index.js +1 -1
  32. package/dist/public/llms.txt +1 -1
  33. package/dist/public/main/index.html +1 -1
  34. package/dist/public/main/memory/index.html +1 -1
  35. package/dist/public/main/memory.html +1 -1
  36. package/dist/public/main.html +1 -1
  37. package/dist/public/service-worker.js +1 -1
  38. package/dist/public/{shadow-claw-Ba2ZFE9m.js → shadow-claw-T9oPpqxT.js} +1 -1
  39. package/dist/public/{shadow-claw-pages-Cy6T0G9y.js → shadow-claw-pages-IW_LNEL_.js} +1 -1
  40. package/package.json +1 -1
  41. package/src/cli/build/build.test.ts +95 -0
  42. package/src/cli/build/build.ts +5 -1
  43. package/src/cli/commands/agent.test.ts +134 -0
  44. package/src/cli/commands/backup.test.ts +85 -0
  45. package/src/cli/commands/clients.test.ts +32 -0
  46. package/src/cli/commands/mcp.test.ts +260 -1
  47. package/src/cli/commands/send-file.test.ts +73 -0
  48. package/src/cli/commands/send.test.ts +58 -0
  49. package/src/cli/commands/tasks.test.ts +78 -0
  50. package/src/cli/commands/webrtc-listen.test.ts +74 -0
  51. package/src/cli/tools/assert-version-bump.test.ts +30 -1
  52. package/src/cli/tools/file-search-replace.test.ts +33 -1
  53. package/src/cli/tools/patch-service-worker-trusted-types.test.ts +24 -1
  54. package/src/cli/tools/touch-nojekyll.test.ts +12 -0
  55. package/src/cli/tools/version.test.ts +109 -5
  56. package/src/cli/utils/agent-core.test.ts +40 -0
  57. package/src/cli/utils/control-client.test.ts +239 -2
  58. package/src/cli/utils/load-workspace-config.test.ts +63 -0
  59. package/src/cli/utils/local-models.test.ts +98 -0
  60. package/src/cli/utils/progress-bar.test.ts +41 -2
  61. package/src/cli/utils/resolve-cache-dir.test.ts +61 -0
  62. package/src/cli/utils/resolve-project-root.test.ts +17 -0
  63. package/src/cli/utils/stdin.test.ts +38 -0
  64. package/src/cli/utils/stdio.test.ts +51 -1
  65. package/src/cli/utils/suppress-warnings.test.ts +9 -0
  66. package/src/cli/utils/webrtc-control-client.test.ts +569 -0
  67. package/src/common/utils/frontmatter.test.ts +117 -0
  68. package/src/components/common/help/providers.test.ts +67 -0
  69. package/src/components/common/shadow-claw-provider-model-picker/shadow-claw-provider-model-picker.test.ts +121 -0
  70. package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +131 -0
  71. package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +130 -0
  72. package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +12 -0
  73. package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +328 -0
  74. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +530 -8
  75. package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +459 -0
  76. package/src/components/settings/shadow-claw-networking/shadow-claw-networking.test.ts +88 -0
  77. package/src/components/settings/shadow-claw-notifications/shadow-claw-notifications.test.ts +92 -0
  78. package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +127 -0
  79. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +173 -0
  80. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +62 -0
  81. package/src/components/settings/shadow-claw-webvm/shadow-claw-webvm.test.ts +104 -0
  82. package/src/components/shadow-claw/shadow-claw.test.ts +112 -0
  83. package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +109 -0
  84. package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +361 -0
  85. package/src/components/shadow-claw-dialog/shadow-claw-dialog.test.ts +76 -0
  86. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +307 -0
  87. package/src/components/shadow-claw-files/shadow-claw-files.test.ts +398 -1
  88. package/src/components/shadow-claw-pages/shadow-claw-pages.test.ts +85 -0
  89. package/src/components/shadow-claw-pages/shadow-claw-pages.ts +2 -1
  90. package/src/components/shadow-claw-pdf-viewer/shadow-claw-pdf-viewer.test.ts +229 -0
  91. package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +233 -0
  92. package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +223 -1
  93. package/src/components/shadow-claw-toast/shadow-claw-toast.test.ts +79 -0
  94. package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +229 -1
  95. package/src/server/logger.test.ts +57 -0
  96. package/src/server/peer.test.ts +47 -0
  97. package/src/server/routes/gemini.test.ts +244 -0
  98. package/src/server/server-peer.test.ts +245 -11
  99. package/src/server/services/llamafile-manager.test.ts +668 -0
  100. package/src/share-target/pending-shares.test.ts +90 -0
  101. package/src/shell/fs.test.ts +125 -0
  102. package/src/shell/shell.test.ts +8 -0
  103. package/src/subsystems/git/credentials.test.ts +260 -0
  104. package/src/subsystems/git/git.test.ts +64 -0
  105. package/src/subsystems/providers/executeNativeAiTask.test.ts +78 -0
  106. package/src/subsystems/providers/utils/parseLocalModelToolCall.test.ts +57 -0
  107. package/src/worker/tools/builtin-ai/builtin-ai.test.ts +88 -0
  108. package/src/worker/tools/email/email.test.ts +358 -0
  109. package/src/worker/tools/email/utils/resolveIntegrationEmailAuth.test.ts +120 -0
  110. package/src/worker/tools/email/utils/resolveUniqueAttachmentPath.test.ts +80 -0
  111. package/src/worker/tools/fetch-file/fetch-file.test.ts +111 -0
  112. package/src/worker/tools/fetch-url/fetch-url.test.ts +295 -0
@@ -1,5 +1,16 @@
1
1
  import { jest } from "@jest/globals";
2
2
 
3
+ jest.unstable_mockModule("../../storage/readGroupFileBytes.js", () => ({
4
+ readGroupFileBytes: jest.fn(
5
+ async (_db: any, _groupId: string, filePath: string) => {
6
+ if (filePath.includes("error")) {
7
+ throw new Error("File read error");
8
+ }
9
+ return new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
10
+ },
11
+ ),
12
+ }));
13
+
3
14
  import { setDB } from "../../db/db.js";
4
15
  import { registerBasicFunctions } from "../../ui/a2ui/registries/basicFunctions.js";
5
16
  import { A2UI_BASIC_CATALOG_ID } from "../../ui/a2ui/types.js";
@@ -1791,4 +1802,102 @@ describe("Basic catalog components unit tests", () => {
1791
1802
 
1792
1803
  document.body.removeChild(el);
1793
1804
  });
1805
+
1806
+ it("resolves workspace media and poster paths to blob URLs", async () => {
1807
+ const el = document.createElement("shadow-claw-a2ui") as any;
1808
+ el.groupId = "peer:media-group";
1809
+ document.body.appendChild(el);
1810
+
1811
+ await (el as any).onTemplateReady;
1812
+
1813
+ el.applyEnvelope({
1814
+ version: "v1.0" as const,
1815
+ type: "createSurface",
1816
+ surfaceId: "media-blob-surface",
1817
+ catalogId: A2UI_BASIC_CATALOG_ID,
1818
+ components: [
1819
+ {
1820
+ id: "root",
1821
+ component: "Column",
1822
+ children: ["img-png", "img-gif", "img-webp", "vid-mp4", "aud-mp3"],
1823
+ },
1824
+ {
1825
+ id: "img-png",
1826
+ component: "Image",
1827
+ url: "sample.png",
1828
+ },
1829
+ {
1830
+ id: "img-gif",
1831
+ component: "Image",
1832
+ url: "sample.gif",
1833
+ },
1834
+ {
1835
+ id: "img-webp",
1836
+ component: "Image",
1837
+ url: "sample.webp",
1838
+ },
1839
+ {
1840
+ id: "vid-mp4",
1841
+ component: "Video",
1842
+ url: "sample.mp4",
1843
+ posterUrl: "poster.jpg",
1844
+ },
1845
+ {
1846
+ id: "aud-mp3",
1847
+ component: "AudioPlayer",
1848
+ url: "sample.mp3",
1849
+ },
1850
+ ] as any,
1851
+ dataModel: {},
1852
+ });
1853
+
1854
+ // Wait for async media resolution
1855
+ await new Promise((resolve) => setTimeout(resolve, 80));
1856
+
1857
+ const root = el.shadowRoot;
1858
+ const imgs = root?.querySelectorAll("img");
1859
+ const video = root?.querySelector("video");
1860
+ const audio = root?.querySelector("audio");
1861
+
1862
+ expect(imgs?.length).toBeGreaterThanOrEqual(3);
1863
+ imgs?.forEach((img: HTMLImageElement) => {
1864
+ expect(img.src).toContain("mock-blob");
1865
+ expect(img.hasAttribute("data-a2ui-workspace-src")).toBe(false);
1866
+ });
1867
+
1868
+ expect(video?.src).toContain("mock-blob");
1869
+ expect(video?.poster).toContain("mock-blob");
1870
+ expect(audio?.src).toContain("mock-blob");
1871
+
1872
+ // Test error handling in resolveSingleMediaToBlobUrl and resolveVideoPosterToBlobUrl
1873
+ el.applyEnvelope({
1874
+ version: "v1.0" as const,
1875
+ type: "createSurface",
1876
+ surfaceId: "media-error-surface",
1877
+ catalogId: A2UI_BASIC_CATALOG_ID,
1878
+ components: [
1879
+ {
1880
+ id: "root",
1881
+ component: "Column",
1882
+ children: ["img-err", "vid-err"],
1883
+ },
1884
+ {
1885
+ id: "img-err",
1886
+ component: "Image",
1887
+ url: "error.png",
1888
+ },
1889
+ {
1890
+ id: "vid-err",
1891
+ component: "Video",
1892
+ url: "error.mp4",
1893
+ posterUrl: "error-poster.jpg",
1894
+ },
1895
+ ] as any,
1896
+ dataModel: {},
1897
+ });
1898
+
1899
+ await new Promise((resolve) => setTimeout(resolve, 80));
1900
+
1901
+ document.body.removeChild(el);
1902
+ });
1794
1903
  });
@@ -1943,4 +1943,365 @@ describe("shadow-claw-conversations dialog submission methods", () => {
1943
1943
 
1944
1944
  document.body.removeChild(el);
1945
1945
  });
1946
+
1947
+ it("covers keyboard reordering, resize handling, autoscroll, peer participants, and dialog helpers", async () => {
1948
+ const el = new ShadowClawConversations() as any;
1949
+ document.body.appendChild(el);
1950
+ el.db = {} as any;
1951
+ await el.render();
1952
+
1953
+ el.shadowRoot?.querySelectorAll("dialog").forEach((d: any) => {
1954
+ d.showModal = jest.fn();
1955
+ d.close = jest.fn();
1956
+ });
1957
+
1958
+ // 1. Keyboard navigation and reordering
1959
+ console.log("CP1: start");
1960
+ mockOrchStore.groups = [
1961
+ { groupId: "g1", name: "Group 1" },
1962
+ { groupId: "g2", name: "Group 2" },
1963
+ { groupId: "g3", name: "Group 3" },
1964
+ ];
1965
+ el.render();
1966
+
1967
+ const targetEl = document.createElement("div");
1968
+ targetEl.classList.add("conversation-item");
1969
+
1970
+ // Grab g2 with 'm'
1971
+ el._handleKeyboard(
1972
+ { key: "m", preventDefault: jest.fn(), target: targetEl } as any,
1973
+ "g2",
1974
+ "Group 2",
1975
+ );
1976
+ expect(el._keyboardGrabbedId).toBe("g2");
1977
+
1978
+ // ArrowUp swaps g2 with g1
1979
+ el._handleKeyboard(
1980
+ { key: "ArrowUp", preventDefault: jest.fn(), target: targetEl } as any,
1981
+ "g2",
1982
+ "Group 2",
1983
+ );
1984
+ expect(mockOrchStore.reorderConversations).toHaveBeenCalledWith(
1985
+ expect.anything(),
1986
+ ["g2", "g1", "g3"],
1987
+ );
1988
+
1989
+ // ArrowDown swaps g2 with g3
1990
+ mockOrchStore.groups = [
1991
+ { groupId: "g1", name: "Group 1" },
1992
+ { groupId: "g2", name: "Group 2" },
1993
+ { groupId: "g3", name: "Group 3" },
1994
+ ];
1995
+ el._keyboardGrabbedId = "g2";
1996
+ el._handleKeyboard(
1997
+ { key: "ArrowDown", preventDefault: jest.fn(), target: targetEl } as any,
1998
+ "g2",
1999
+ "Group 2",
2000
+ );
2001
+ expect(mockOrchStore.reorderConversations).toHaveBeenCalledWith(
2002
+ expect.anything(),
2003
+ ["g1", "g3", "g2"],
2004
+ );
2005
+
2006
+ // ArrowUp at top edge
2007
+ mockOrchStore.groups = [
2008
+ { groupId: "g1", name: "Group 1" },
2009
+ { groupId: "g2", name: "Group 2" },
2010
+ ];
2011
+ el._keyboardGrabbedId = "g1";
2012
+ el._handleKeyboard(
2013
+ { key: "ArrowUp", preventDefault: jest.fn(), target: targetEl } as any,
2014
+ "g1",
2015
+ "Group 1",
2016
+ );
2017
+
2018
+ // Escape cancels
2019
+ el._handleKeyboard(
2020
+ { key: "Escape", preventDefault: jest.fn(), target: targetEl } as any,
2021
+ "g1",
2022
+ "Group 1",
2023
+ );
2024
+ expect(el._keyboardGrabbedId).toBeNull();
2025
+
2026
+ // Enter / Space drops
2027
+ el._keyboardGrabbedId = "g1";
2028
+ el._handleKeyboard(
2029
+ { key: "Enter", preventDefault: jest.fn(), target: targetEl } as any,
2030
+ "g1",
2031
+ "Group 1",
2032
+ );
2033
+ expect(el._keyboardGrabbedId).toBeNull();
2034
+
2035
+ el._keyboardGrabbedId = "g1";
2036
+ el._handleKeyboard(
2037
+ { key: " ", preventDefault: jest.fn(), target: targetEl } as any,
2038
+ "g1",
2039
+ "Group 1",
2040
+ );
2041
+ expect(el._keyboardGrabbedId).toBeNull();
2042
+
2043
+ // When not grabbed: navigation keys
2044
+ el._keyboardGrabbedId = null;
2045
+ el._handleKeyboard(
2046
+ { key: "ArrowDown", preventDefault: jest.fn(), target: targetEl } as any,
2047
+ "g1",
2048
+ "Group 1",
2049
+ );
2050
+ el._handleKeyboard(
2051
+ { key: "ArrowUp", preventDefault: jest.fn(), target: targetEl } as any,
2052
+ "g1",
2053
+ "Group 1",
2054
+ );
2055
+ el._handleKeyboard(
2056
+ { key: "ArrowRight", preventDefault: jest.fn(), target: targetEl } as any,
2057
+ "g1",
2058
+ "Group 1",
2059
+ );
2060
+ el._handleKeyboard(
2061
+ { key: "ArrowLeft", preventDefault: jest.fn(), target: targetEl } as any,
2062
+ "g1",
2063
+ "Group 1",
2064
+ );
2065
+ const navListener = jest.fn();
2066
+ document.addEventListener("shadow-claw-navigate", navListener);
2067
+ el._handleKeyboard(
2068
+ { key: "Enter", preventDefault: jest.fn(), target: targetEl } as any,
2069
+ "g1",
2070
+ "Group 1",
2071
+ );
2072
+ expect(navListener).toHaveBeenCalledWith(
2073
+ expect.objectContaining({
2074
+ detail: expect.objectContaining({ groupId: "g1" }),
2075
+ }),
2076
+ );
2077
+ document.removeEventListener("shadow-claw-navigate", navListener);
2078
+
2079
+ // 2. Resize handle pointer events & dblclick
2080
+ el._initResizeHandle();
2081
+ const handle = el.shadowRoot?.querySelector(
2082
+ ".resize-handle",
2083
+ ) as HTMLElement;
2084
+ if (handle) {
2085
+ handle.setPointerCapture = jest.fn() as any;
2086
+ // non-primary mouse button ignored
2087
+ handle.dispatchEvent(
2088
+ new PointerEvent("pointerdown", { pointerType: "mouse", button: 2 }),
2089
+ );
2090
+
2091
+ // primary pointerdown
2092
+ handle.dispatchEvent(
2093
+ new PointerEvent("pointerdown", { pointerId: 1, button: 0 }),
2094
+ );
2095
+ expect(handle.classList.contains("active")).toBe(true);
2096
+
2097
+ // pointermove
2098
+ document.dispatchEvent(
2099
+ new PointerEvent("pointermove", { pointerId: 1, clientY: 100 }),
2100
+ );
2101
+
2102
+ // pointerup
2103
+ handle.dispatchEvent(new PointerEvent("pointerup", { pointerId: 1 }));
2104
+ expect(handle.classList.contains("active")).toBe(false);
2105
+
2106
+ // pointercancel
2107
+ handle.dispatchEvent(
2108
+ new PointerEvent("pointerdown", { pointerId: 2, button: 0 }),
2109
+ );
2110
+ handle.dispatchEvent(new PointerEvent("pointercancel"));
2111
+ expect(handle.classList.contains("active")).toBe(false);
2112
+
2113
+ // dblclick resets
2114
+ handle.dispatchEvent(new MouseEvent("dblclick"));
2115
+ expect(mockSetConfig).toHaveBeenCalledWith(
2116
+ el.db,
2117
+ "conversations_height",
2118
+ 0,
2119
+ );
2120
+ }
2121
+
2122
+ // 3. Autoscroll
2123
+ jest.spyOn(window, "requestAnimationFrame").mockImplementation(() => 1);
2124
+ (global.fetch as any) = jest.fn<any>().mockResolvedValue({
2125
+ ok: true,
2126
+ json: async () => [],
2127
+ });
2128
+
2129
+ const listEl = el.shadowRoot?.querySelector(
2130
+ ".conversation-list",
2131
+ ) as HTMLElement;
2132
+ if (listEl) {
2133
+ listEl.getBoundingClientRect = () => ({ top: 100, bottom: 500 }) as any;
2134
+ }
2135
+
2136
+ el._startAutoScroll();
2137
+ el._startAutoScroll(); // duplicate branch test
2138
+ el._updateAutoScrollSpeed(110); // near top
2139
+ expect(el._autoScrollSpeed).toBeLessThan(0);
2140
+ el._updateAutoScrollSpeed(490); // near bottom
2141
+ expect(el._autoScrollSpeed).toBeGreaterThan(0);
2142
+ el._updateAutoScrollSpeed(300); // middle
2143
+ expect(el._autoScrollSpeed).toBe(0);
2144
+ el._stopAutoScroll();
2145
+ expect(el._autoScrollActive).toBe(false);
2146
+
2147
+ // 4. itemAtPoint
2148
+ const itemEl = el.shadowRoot?.querySelector(".conversation-item");
2149
+ (el.shadowRoot as any).elementFromPoint = jest.fn().mockReturnValue(itemEl);
2150
+ expect(el._itemAtPoint(10, 10)).toBe(itemEl);
2151
+
2152
+ // 5. Dialog submissions & cancel
2153
+ // Create dialog
2154
+ const createInput = el.shadowRoot?.querySelector(
2155
+ ".conversations__create-dialog .conversations__input",
2156
+ ) as HTMLInputElement;
2157
+ if (createInput) {
2158
+ createInput.value = "";
2159
+ await el._submitCreateDialog();
2160
+ createInput.value = "New Conversation";
2161
+ await el._submitCreateDialog();
2162
+ expect(mockOrchStore.createConversation).toHaveBeenCalledWith(
2163
+ el.db,
2164
+ "New Conversation",
2165
+ );
2166
+ }
2167
+
2168
+ // Delete dialog
2169
+ el._pendingDeleteGroupId = null;
2170
+ await el._submitDeleteDialog();
2171
+ el._pendingDeleteGroupId = "br:to-delete";
2172
+ await el._submitDeleteDialog();
2173
+ expect(mockOrchStore.deleteConversation).toHaveBeenCalledWith(
2174
+ el.db,
2175
+ "br:to-delete",
2176
+ );
2177
+ expect(el._pendingDeleteGroupId).toBeNull();
2178
+
2179
+ // Clone dialog
2180
+ el._pendingCloneGroupId = null;
2181
+ await el._submitCloneDialog();
2182
+ el._pendingCloneGroupId = "br:to-clone";
2183
+ await el._submitCloneDialog();
2184
+ expect(mockOrchStore.cloneConversation).toHaveBeenCalledWith(
2185
+ el.db,
2186
+ "br:to-clone",
2187
+ );
2188
+ expect(el._pendingCloneGroupId).toBeNull();
2189
+
2190
+ // Details dialog empty / missing pending
2191
+ el._pendingRenameGroupId = null;
2192
+ await el._submitDetailsDialog();
2193
+
2194
+ // Dialog cancel buttons
2195
+ const createCancel = el.shadowRoot?.querySelector(
2196
+ ".conversations__create-dialog .conversations__cancel",
2197
+ ) as HTMLButtonElement;
2198
+ createCancel?.click();
2199
+
2200
+ const detailsCancel = el.shadowRoot?.querySelector(
2201
+ ".conversations__details-dialog .conversations__cancel",
2202
+ ) as HTMLButtonElement;
2203
+ detailsCancel?.click();
2204
+
2205
+ const deleteCancel = el.shadowRoot?.querySelector(
2206
+ ".conversations__delete-dialog .conversations__cancel",
2207
+ ) as HTMLButtonElement;
2208
+ deleteCancel?.click();
2209
+
2210
+ const cloneCancel = el.shadowRoot?.querySelector(
2211
+ ".conversations__clone-dialog .conversations__cancel",
2212
+ ) as HTMLButtonElement;
2213
+ cloneCancel?.click();
2214
+
2215
+ // 6. Peer participant details & copy button
2216
+ mockOrchStore.orchestrator = {
2217
+ ...mockOrchStore.orchestrator,
2218
+ peerjs: {
2219
+ connectedPeersSignal: {
2220
+ get: () => ["peer-12345678"],
2221
+ },
2222
+ },
2223
+ peerjsPeerAliases: {
2224
+ Alice: "peer-12345678",
2225
+ },
2226
+ getApiKeyForSpecificProvider: jest.fn().mockResolvedValue("test-key"),
2227
+ };
2228
+
2229
+ // Open details for peer group
2230
+ el.openDetailsDialog("Peer Chat", "peer:peer-12345678");
2231
+ const copyBtn = el.shadowRoot?.querySelector(
2232
+ ".conversations__group-id-copy-btn",
2233
+ ) as HTMLButtonElement;
2234
+ if (copyBtn) {
2235
+ await copyBtn.click();
2236
+ }
2237
+
2238
+ // Also test peer not connected
2239
+ el.openDetailsDialog("Disconnected Peer", "peer:unknown-peer");
2240
+
2241
+ // 7. _loadProviderModels edge cases
2242
+ // array models
2243
+ const pStatic = { id: "p1", name: "P1", models: [{ id: "m1" }] };
2244
+ const resStatic = await el._loadProviderModels(pStatic as any);
2245
+ expect(resStatic).toEqual([{ id: "m1" }]);
2246
+
2247
+ // no modelsUrl
2248
+ const pNoUrl = { id: "p2", name: "P2" };
2249
+ const resNoUrl = await el._loadProviderModels(pNoUrl as any);
2250
+ expect(resNoUrl).toEqual([]);
2251
+
2252
+ // fetch error
2253
+ (global.fetch as any) = jest.fn<any>().mockResolvedValueOnce({
2254
+ ok: false,
2255
+ status: 500,
2256
+ });
2257
+ await expect(
2258
+ el._loadProviderModels({
2259
+ id: "p3",
2260
+ modelsUrl: "http://example.com",
2261
+ } as any),
2262
+ ).rejects.toThrow("HTTP 500");
2263
+
2264
+ // fetch returns array
2265
+ (global.fetch as any) = jest.fn<any>().mockResolvedValueOnce({
2266
+ ok: true,
2267
+ json: async () => [{ id: "arr-model" }],
2268
+ });
2269
+ const resArr = await el._loadProviderModels({
2270
+ id: "p4",
2271
+ modelsUrl: "http://example.com",
2272
+ } as any);
2273
+ expect(resArr[0].id).toBe("arr-model");
2274
+
2275
+ // fetch with apiKeyHeader
2276
+ (global.fetch as any) = jest.fn<any>().mockResolvedValueOnce({
2277
+ ok: true,
2278
+ json: async () => ({ models: [{ id: "obj-model" }] }),
2279
+ });
2280
+ const resObj = await el._loadProviderModels({
2281
+ id: "p5",
2282
+ modelsUrl: "http://example.com",
2283
+ apiKeyHeader: "Authorization",
2284
+ apiKeyHeaderFormat: "Bearer {key}",
2285
+ } as any);
2286
+ expect(resObj[0].id).toBe("obj-model");
2287
+
2288
+ // fetch single object
2289
+ (global.fetch as any) = jest.fn<any>().mockResolvedValueOnce({
2290
+ ok: true,
2291
+ json: async () => ({ id: "single-model", name: "Single" }),
2292
+ });
2293
+ const resSingle = await el._loadProviderModels({
2294
+ id: "p6",
2295
+ modelsUrl: "http://example.com",
2296
+ } as any);
2297
+ expect(resSingle).toHaveLength(1);
2298
+
2299
+ // persist height without argument & when db is null
2300
+ el.getBoundingClientRect = () => ({ height: 320 }) as any;
2301
+ await el._persistHeight();
2302
+ el.db = null;
2303
+ await el._persistHeight();
2304
+
2305
+ document.body.removeChild(el);
2306
+ });
1946
2307
  });
@@ -1,3 +1,4 @@
1
+ import { jest } from "@jest/globals";
1
2
  import { ShadowClawDialog } from "./shadow-claw-dialog.js";
2
3
 
3
4
  describe("shadow-claw-dialog", () => {
@@ -33,4 +34,79 @@ describe("shadow-claw-dialog", () => {
33
34
 
34
35
  document.body.removeChild(el);
35
36
  });
37
+
38
+ it("exposes observedAttributes and updates attributes dynamically", () => {
39
+ expect(ShadowClawDialog.observedAttributes).toEqual([
40
+ "dialog-class",
41
+ "dialog-aria-label",
42
+ "dialog-aria-labelledby",
43
+ ]);
44
+
45
+ const el = new ShadowClawDialog();
46
+ document.body.appendChild(el);
47
+
48
+ el.setAttribute("dialog-class", "custom-class");
49
+ el.setAttribute("dialog-aria-label", "Dynamic Label");
50
+ el.setAttribute("dialog-aria-labelledby", "heading-id");
51
+
52
+ const dialog = el.dialog!;
53
+ expect(dialog.className).toBe("custom-class");
54
+ expect(dialog.getAttribute("aria-label")).toBe("Dynamic Label");
55
+ expect(dialog.getAttribute("aria-labelledby")).toBe("heading-id");
56
+
57
+ // Remove aria attributes
58
+ el.removeAttribute("dialog-aria-label");
59
+ el.removeAttribute("dialog-aria-labelledby");
60
+ expect(dialog.hasAttribute("aria-label")).toBe(false);
61
+ expect(dialog.hasAttribute("aria-labelledby")).toBe(false);
62
+
63
+ document.body.removeChild(el);
64
+ });
65
+
66
+ it("supports showModal, open, close, and returnValue", () => {
67
+ const el = new ShadowClawDialog();
68
+ document.body.appendChild(el);
69
+
70
+ const dialog = el.dialog!;
71
+ dialog.showModal = jest.fn(() => {
72
+ dialog.setAttribute("open", "");
73
+ });
74
+ dialog.close = jest.fn((ret?: string) => {
75
+ dialog.removeAttribute("open");
76
+ if (ret !== undefined) dialog.returnValue = ret;
77
+ });
78
+
79
+ expect(el.open).toBe(false);
80
+ el.showModal();
81
+ expect(dialog.showModal).toHaveBeenCalled();
82
+ expect(el.open).toBe(true);
83
+
84
+ el.returnValue = "confirmed";
85
+ expect(el.returnValue).toBe("confirmed");
86
+
87
+ el.close("cancel");
88
+ expect(dialog.close).toHaveBeenCalledWith("cancel");
89
+ expect(el.returnValue).toBe("cancel");
90
+ expect(el.open).toBe(false);
91
+
92
+ document.body.removeChild(el);
93
+ });
94
+
95
+ it("handles fallback showModal and close when native methods are absent", () => {
96
+ const el = new ShadowClawDialog();
97
+ document.body.appendChild(el);
98
+
99
+ const dialog = el.dialog!;
100
+ (dialog as any).showModal = undefined;
101
+ (dialog as any).close = undefined;
102
+
103
+ el.showModal();
104
+ expect(dialog.hasAttribute("open")).toBe(true);
105
+
106
+ el.close("closed-val");
107
+ expect(dialog.hasAttribute("open")).toBe(false);
108
+ expect(dialog.returnValue).toBe("closed-val");
109
+
110
+ document.body.removeChild(el);
111
+ });
36
112
  });