Create tests. Note: ws mocking is currently broken

This commit is contained in:
Morgan Dean
2025-06-18 15:26:07 -07:00
parent 10c0fecc1a
commit 593afe220f
12 changed files with 1526 additions and 13 deletions

View File

@@ -48,6 +48,7 @@
"@types/ws": "^8.18.1",
"bumpp": "^10.1.0",
"happy-dom": "^17.4.7",
"msw": "^2.10.2",
"tsdown": "^0.11.9",
"tsx": "^4.19.4",
"typescript": "^5.8.3",

View File

@@ -33,6 +33,9 @@ importers:
happy-dom:
specifier: ^17.4.7
version: 17.6.3
msw:
specifier: ^2.10.2
version: 2.10.2(@types/node@22.15.31)(typescript@5.8.3)
tsdown:
specifier: ^0.11.9
version: 0.11.13(typescript@5.8.3)
@@ -44,7 +47,7 @@ importers:
version: 5.8.3
vitest:
specifier: ^3.1.3
version: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.31)(happy-dom@17.6.3)(jiti@2.4.2)(tsx@4.20.2)(yaml@2.8.0)
version: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.31)(happy-dom@17.6.3)(jiti@2.4.2)(msw@2.10.2(@types/node@22.15.31)(typescript@5.8.3))(tsx@4.20.2)(yaml@2.8.0)
packages:
@@ -122,6 +125,15 @@ packages:
cpu: [x64]
os: [win32]
'@bundled-es-modules/cookie@2.0.1':
resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==}
'@bundled-es-modules/statuses@1.0.1':
resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==}
'@bundled-es-modules/tough-cookie@0.1.6':
resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==}
'@emnapi/core@1.4.3':
resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==}
@@ -386,6 +398,37 @@ packages:
cpu: [x64]
os: [win32]
'@inquirer/confirm@5.1.12':
resolution: {integrity: sha512-dpq+ielV9/bqgXRUbNH//KsY6WEw9DrGPmipkpmgC1Y46cwuBTNx7PXFWTjc3MQ+urcc0QxoVHcMI0FW4Ok0hg==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/core@10.1.13':
resolution: {integrity: sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/figures@1.0.12':
resolution: {integrity: sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==}
engines: {node: '>=18'}
'@inquirer/type@3.0.7':
resolution: {integrity: sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@jridgewell/gen-mapping@0.3.8':
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
engines: {node: '>=6.0.0'}
@@ -404,9 +447,22 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@mswjs/interceptors@0.39.2':
resolution: {integrity: sha512-RuzCup9Ct91Y7V79xwCb146RaBRHZ7NBbrIUySumd1rpKqHL5OonaqrGIbug5hNwP/fRyxFMA6ISgw4FTtYFYg==}
engines: {node: '>=18'}
'@napi-rs/wasm-runtime@0.2.11':
resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==}
'@open-draft/deferred-promise@2.2.0':
resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==}
'@open-draft/logger@0.3.0':
resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==}
'@open-draft/until@2.1.0':
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
'@oxc-project/types@0.70.0':
resolution: {integrity: sha512-ngyLUpUjO3dpqygSRQDx7nMx8+BmXbWOU4oIwTJFV2MVIDG7knIZwgdwXlQWLg3C3oxg1lS7ppMtPKqKFb7wzw==}
@@ -583,6 +639,9 @@ packages:
'@types/chai@5.2.2':
resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
'@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@@ -601,6 +660,12 @@ packages:
'@types/node@22.15.31':
resolution: {integrity: sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==}
'@types/statuses@2.0.6':
resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==}
'@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
@@ -633,6 +698,18 @@ packages:
'@vitest/utils@3.2.3':
resolution: {integrity: sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==}
ansi-escapes@4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansis@4.1.0:
resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==}
engines: {node: '>=14'}
@@ -687,6 +764,14 @@ packages:
citty@0.1.6:
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
cli-width@4.1.0:
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
engines: {node: '>= 12'}
cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -708,6 +793,10 @@ packages:
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
engines: {node: ^14.18.0 || >=16.10.0}
cookie@0.7.2:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'}
@@ -748,6 +837,9 @@ packages:
oxc-resolver:
optional: true
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
empathic@1.1.0:
resolution: {integrity: sha512-rsPft6CK3eHtrlp9Y5ALBb+hfK+DWnA4WFebbazxjWyx8vSm3rZeoM3z9irsjcqO3PYRzlfv27XIB4tz2DV7RA==}
engines: {node: '>=14'}
@@ -791,6 +883,10 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-tsconfig@4.10.1:
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
@@ -798,16 +894,30 @@ packages:
resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==}
hasBin: true
graphql@16.11.0:
resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==}
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
happy-dom@17.6.3:
resolution: {integrity: sha512-UVIHeVhxmxedbWPCfgS55Jg2rDfwf2BCKeylcPSqazLz5w3Kri7Q4xdBJubsr/+VUzFLh0VjIvh13RaDA2/Xug==}
engines: {node: '>=20.0.0'}
headers-polyfill@4.0.3:
resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==}
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-node-process@1.2.0:
resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==}
jiti@2.4.2:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true
@@ -832,6 +942,20 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
msw@2.10.2:
resolution: {integrity: sha512-RCKM6IZseZQCWcSWlutdf590M8nVfRHG1ImwzOtwz8IYxgT4zhUO0rfTcTvDGiaFE0Rhcc+h43lcF3Jc9gFtwQ==}
engines: {node: '>=18'}
hasBin: true
peerDependencies:
typescript: '>= 4.8.x'
peerDependenciesMeta:
typescript:
optional: true
mute-stream@2.0.0:
resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
engines: {node: ^18.17.0 || >=20.5.0}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -852,9 +976,15 @@ packages:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
engines: {node: '>=14.0.0'}
outvariant@1.4.3:
resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
package-manager-detector@1.3.0:
resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==}
path-to-regexp@6.3.0:
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
@@ -892,9 +1022,19 @@ packages:
process-warning@5.0.0:
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
psl@1.15.0:
resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
quansync@0.2.10:
resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==}
querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
@@ -909,6 +1049,13 @@ packages:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
@@ -958,6 +1105,10 @@ packages:
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
@@ -975,9 +1126,24 @@ packages:
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
statuses@2.0.2:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
std-env@3.9.0:
resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==}
strict-event-emitter@0.5.1:
resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-literal@3.0.0:
resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==}
@@ -1009,6 +1175,10 @@ packages:
resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==}
engines: {node: '>=14.0.0'}
tough-cookie@4.1.4:
resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
engines: {node: '>=6'}
tsdown@0.11.13:
resolution: {integrity: sha512-VSfoNm8MJXFdg7PJ4p2javgjMRiQQHpkP9N3iBBTrmCixcT6YZ9ZtqYMW3NDHczqR0C0Qnur1HMQr1ZfZcmrng==}
engines: {node: '>=18.0.0'}
@@ -1036,6 +1206,14 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
type-fest@0.21.3:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
typescript@5.8.3:
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
engines: {node: '>=14.17'}
@@ -1047,6 +1225,13 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
universalify@0.2.0:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'}
url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
vite-node@3.2.3:
resolution: {integrity: sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -1133,6 +1318,14 @@ packages:
engines: {node: '>=8'}
hasBin: true
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
ws@8.18.2:
resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==}
engines: {node: '>=10.0.0'}
@@ -1145,11 +1338,27 @@ packages:
utf-8-validate:
optional: true
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
yaml@2.8.0:
resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==}
engines: {node: '>= 14.6'}
hasBin: true
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
yoctocolors-cjs@2.1.2:
resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==}
engines: {node: '>=18'}
snapshots:
'@babel/generator@7.27.5':
@@ -1208,6 +1417,19 @@ snapshots:
'@biomejs/cli-win32-x64@1.9.4':
optional: true
'@bundled-es-modules/cookie@2.0.1':
dependencies:
cookie: 0.7.2
'@bundled-es-modules/statuses@1.0.1':
dependencies:
statuses: 2.0.2
'@bundled-es-modules/tough-cookie@0.1.6':
dependencies:
'@types/tough-cookie': 4.0.5
tough-cookie: 4.1.4
'@emnapi/core@1.4.3':
dependencies:
'@emnapi/wasi-threads': 1.0.2
@@ -1374,6 +1596,32 @@ snapshots:
'@img/sharp-win32-x64@0.33.5':
optional: true
'@inquirer/confirm@5.1.12(@types/node@22.15.31)':
dependencies:
'@inquirer/core': 10.1.13(@types/node@22.15.31)
'@inquirer/type': 3.0.7(@types/node@22.15.31)
optionalDependencies:
'@types/node': 22.15.31
'@inquirer/core@10.1.13(@types/node@22.15.31)':
dependencies:
'@inquirer/figures': 1.0.12
'@inquirer/type': 3.0.7(@types/node@22.15.31)
ansi-escapes: 4.3.2
cli-width: 4.1.0
mute-stream: 2.0.0
signal-exit: 4.1.0
wrap-ansi: 6.2.0
yoctocolors-cjs: 2.1.2
optionalDependencies:
'@types/node': 22.15.31
'@inquirer/figures@1.0.12': {}
'@inquirer/type@3.0.7(@types/node@22.15.31)':
optionalDependencies:
'@types/node': 22.15.31
'@jridgewell/gen-mapping@0.3.8':
dependencies:
'@jridgewell/set-array': 1.2.1
@@ -1391,6 +1639,15 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@mswjs/interceptors@0.39.2':
dependencies:
'@open-draft/deferred-promise': 2.2.0
'@open-draft/logger': 0.3.0
'@open-draft/until': 2.1.0
is-node-process: 1.2.0
outvariant: 1.4.3
strict-event-emitter: 0.5.1
'@napi-rs/wasm-runtime@0.2.11':
dependencies:
'@emnapi/core': 1.4.3
@@ -1398,6 +1655,15 @@ snapshots:
'@tybys/wasm-util': 0.9.0
optional: true
'@open-draft/deferred-promise@2.2.0': {}
'@open-draft/logger@0.3.0':
dependencies:
is-node-process: 1.2.0
outvariant: 1.4.3
'@open-draft/until@2.1.0': {}
'@oxc-project/types@0.70.0': {}
'@quansync/fs@0.1.3':
@@ -1513,6 +1779,8 @@ snapshots:
dependencies:
'@types/deep-eql': 4.0.2
'@types/cookie@0.6.0': {}
'@types/debug@4.1.12':
dependencies:
'@types/ms': 2.1.0
@@ -1531,6 +1799,10 @@ snapshots:
dependencies:
undici-types: 6.21.0
'@types/statuses@2.0.6': {}
'@types/tough-cookie@4.0.5': {}
'@types/ws@8.18.1':
dependencies:
'@types/node': 22.15.31
@@ -1543,12 +1815,13 @@ snapshots:
chai: 5.2.0
tinyrainbow: 2.0.0
'@vitest/mocker@3.2.3(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(tsx@4.20.2)(yaml@2.8.0))':
'@vitest/mocker@3.2.3(msw@2.10.2(@types/node@22.15.31)(typescript@5.8.3))(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(tsx@4.20.2)(yaml@2.8.0))':
dependencies:
'@vitest/spy': 3.2.3
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
msw: 2.10.2(@types/node@22.15.31)(typescript@5.8.3)
vite: 6.3.5(@types/node@22.15.31)(jiti@2.4.2)(tsx@4.20.2)(yaml@2.8.0)
'@vitest/pretty-format@3.2.3':
@@ -1577,6 +1850,16 @@ snapshots:
loupe: 3.1.3
tinyrainbow: 2.0.0
ansi-escapes@4.3.2:
dependencies:
type-fest: 0.21.3
ansi-regex@5.0.1: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
ansis@4.1.0: {}
args-tokenizer@0.3.0: {}
@@ -1643,6 +1926,14 @@ snapshots:
dependencies:
consola: 3.4.2
cli-width@4.1.0: {}
cliui@8.0.1:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -1663,6 +1954,8 @@ snapshots:
consola@3.4.2: {}
cookie@0.7.2: {}
debug@4.4.1:
dependencies:
ms: 2.1.3
@@ -1681,6 +1974,8 @@ snapshots:
dts-resolver@2.1.1: {}
emoji-regex@8.0.0: {}
empathic@1.1.0: {}
es-module-lexer@1.7.0: {}
@@ -1732,6 +2027,8 @@ snapshots:
fsevents@2.3.3:
optional: true
get-caller-file@2.0.5: {}
get-tsconfig@4.10.1:
dependencies:
resolve-pkg-maps: 1.0.0
@@ -1745,15 +2042,23 @@ snapshots:
nypm: 0.6.0
pathe: 2.0.3
graphql@16.11.0: {}
happy-dom@17.6.3:
dependencies:
webidl-conversions: 7.0.0
whatwg-mimetype: 3.0.0
headers-polyfill@4.0.3: {}
hookable@5.5.3: {}
is-arrayish@0.3.2: {}
is-fullwidth-code-point@3.0.0: {}
is-node-process@1.2.0: {}
jiti@2.4.2: {}
js-tokens@9.0.1: {}
@@ -1770,6 +2075,33 @@ snapshots:
ms@2.1.3: {}
msw@2.10.2(@types/node@22.15.31)(typescript@5.8.3):
dependencies:
'@bundled-es-modules/cookie': 2.0.1
'@bundled-es-modules/statuses': 1.0.1
'@bundled-es-modules/tough-cookie': 0.1.6
'@inquirer/confirm': 5.1.12(@types/node@22.15.31)
'@mswjs/interceptors': 0.39.2
'@open-draft/deferred-promise': 2.2.0
'@open-draft/until': 2.1.0
'@types/cookie': 0.6.0
'@types/statuses': 2.0.6
graphql: 16.11.0
headers-polyfill: 4.0.3
is-node-process: 1.2.0
outvariant: 1.4.3
path-to-regexp: 6.3.0
picocolors: 1.1.1
strict-event-emitter: 0.5.1
type-fest: 4.41.0
yargs: 17.7.2
optionalDependencies:
typescript: 5.8.3
transitivePeerDependencies:
- '@types/node'
mute-stream@2.0.0: {}
nanoid@3.3.11: {}
node-fetch-native@1.6.6: {}
@@ -1786,8 +2118,12 @@ snapshots:
on-exit-leak-free@2.1.2: {}
outvariant@1.4.3: {}
package-manager-detector@1.3.0: {}
path-to-regexp@6.3.0: {}
pathe@2.0.3: {}
pathval@2.0.0: {}
@@ -1832,8 +2168,16 @@ snapshots:
process-warning@5.0.0: {}
psl@1.15.0:
dependencies:
punycode: 2.3.1
punycode@2.3.1: {}
quansync@0.2.10: {}
querystringify@2.2.0: {}
quick-format-unescaped@4.0.4: {}
rc9@2.1.2:
@@ -1845,6 +2189,10 @@ snapshots:
real-require@0.2.0: {}
require-directory@2.1.1: {}
requires-port@1.0.0: {}
resolve-pkg-maps@1.0.0: {}
rolldown-plugin-dts@0.13.11(rolldown@1.0.0-beta.9)(typescript@5.8.3):
@@ -1941,6 +2289,8 @@ snapshots:
siginfo@2.0.0: {}
signal-exit@4.1.0: {}
simple-swizzle@0.2.2:
dependencies:
is-arrayish: 0.3.2
@@ -1955,8 +2305,22 @@ snapshots:
stackback@0.0.2: {}
statuses@2.0.2: {}
std-env@3.9.0: {}
strict-event-emitter@0.5.1: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-literal@3.0.0:
dependencies:
js-tokens: 9.0.1
@@ -1982,6 +2346,13 @@ snapshots:
tinyspy@4.0.3: {}
tough-cookie@4.1.4:
dependencies:
psl: 1.15.0
punycode: 2.3.1
universalify: 0.2.0
url-parse: 1.5.10
tsdown@0.11.13(typescript@5.8.3):
dependencies:
ansis: 4.1.0
@@ -2016,6 +2387,10 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
type-fest@0.21.3: {}
type-fest@4.41.0: {}
typescript@5.8.3: {}
unconfig@7.3.2:
@@ -2027,6 +2402,13 @@ snapshots:
undici-types@6.21.0: {}
universalify@0.2.0: {}
url-parse@1.5.10:
dependencies:
querystringify: 2.2.0
requires-port: 1.0.0
vite-node@3.2.3(@types/node@22.15.31)(jiti@2.4.2)(tsx@4.20.2)(yaml@2.8.0):
dependencies:
cac: 6.7.14
@@ -2063,11 +2445,11 @@ snapshots:
tsx: 4.20.2
yaml: 2.8.0
vitest@3.2.3(@types/debug@4.1.12)(@types/node@22.15.31)(happy-dom@17.6.3)(jiti@2.4.2)(tsx@4.20.2)(yaml@2.8.0):
vitest@3.2.3(@types/debug@4.1.12)(@types/node@22.15.31)(happy-dom@17.6.3)(jiti@2.4.2)(msw@2.10.2(@types/node@22.15.31)(typescript@5.8.3))(tsx@4.20.2)(yaml@2.8.0):
dependencies:
'@types/chai': 5.2.2
'@vitest/expect': 3.2.3
'@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(tsx@4.20.2)(yaml@2.8.0))
'@vitest/mocker': 3.2.3(msw@2.10.2(@types/node@22.15.31)(typescript@5.8.3))(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(tsx@4.20.2)(yaml@2.8.0))
'@vitest/pretty-format': 3.2.3
'@vitest/runner': 3.2.3
'@vitest/snapshot': 3.2.3
@@ -2115,6 +2497,34 @@ snapshots:
siginfo: 2.0.0
stackback: 0.0.2
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
ws@8.18.2: {}
y18n@5.0.8: {}
yaml@2.8.0: {}
yargs-parser@21.1.1: {}
yargs@17.7.2:
dependencies:
cliui: 8.0.1
escalade: 3.2.0
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 21.1.1
yoctocolors-cjs@2.1.2: {}

View File

@@ -69,7 +69,7 @@ export class CloudComputer extends BaseComputer {
logger.info("Stopping cloud computer...");
if (this.interface) {
this.interface.close();
this.interface.disconnect();
this.interface = undefined;
}

View File

@@ -91,7 +91,9 @@ export abstract class BaseComputerInterface {
return;
} catch (error) {
// Wait a bit before retrying
this.logger.error(`Error connecting to websocket: ${error}`);
this.logger.error(
`Error connecting to websocket: ${JSON.stringify(error)}`
);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
@@ -102,7 +104,7 @@ export abstract class BaseComputerInterface {
/**
* Connect to the WebSocket server.
*/
protected async connect(): Promise<void> {
public async connect(): Promise<void> {
if (this.ws.readyState === WebSocket.OPEN) {
return;
}
@@ -151,7 +153,7 @@ export abstract class BaseComputerInterface {
/**
* Send a command to the WebSocket server.
*/
protected async sendCommand(command: {
public async sendCommand(command: {
action: string;
[key: string]: unknown;
}): Promise<{ [key: string]: unknown }> {
@@ -198,13 +200,23 @@ export abstract class BaseComputerInterface {
return commandPromise;
}
/**
* Check if the WebSocket is connected.
*/
public isConnected(): boolean {
return this.ws && this.ws.readyState === WebSocket.OPEN;
}
/**
* Close the interface connection.
*/
close(): void {
disconnect(): void {
this.closed = true;
if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.close();
} else if (this.ws && this.ws.readyState === WebSocket.CONNECTING) {
// If still connecting, terminate the connection attempt
this.ws.terminate();
}
}
@@ -214,7 +226,7 @@ export abstract class BaseComputerInterface {
* to provide more forceful cleanup.
*/
forceClose(): void {
this.close();
this.disconnect();
}
// Mouse Actions

View File

@@ -0,0 +1,71 @@
import { describe, expect, it } from "vitest";
import { InterfaceFactory } from "../../src/interface/factory.ts";
import { MacOSComputerInterface } from "../../src/interface/macos.ts";
import { LinuxComputerInterface } from "../../src/interface/linux.ts";
import { WindowsComputerInterface } from "../../src/interface/windows.ts";
import { OSType } from "../../src/types.ts";
describe("InterfaceFactory", () => {
const testParams = {
ipAddress: "192.168.1.100",
username: "testuser",
password: "testpass",
apiKey: "test-api-key",
vmName: "test-vm",
};
describe("createInterfaceForOS", () => {
it("should create MacOSComputerInterface for macOS", () => {
const interface_ = InterfaceFactory.createInterfaceForOS(
OSType.MACOS,
testParams.ipAddress,
testParams.apiKey,
testParams.vmName
);
expect(interface_).toBeInstanceOf(MacOSComputerInterface);
});
it("should create LinuxComputerInterface for Linux", () => {
const interface_ = InterfaceFactory.createInterfaceForOS(
OSType.LINUX,
testParams.ipAddress,
testParams.apiKey,
testParams.vmName
);
expect(interface_).toBeInstanceOf(LinuxComputerInterface);
});
it("should create WindowsComputerInterface for Windows", () => {
const interface_ = InterfaceFactory.createInterfaceForOS(
OSType.WINDOWS,
testParams.ipAddress,
testParams.apiKey,
testParams.vmName
);
expect(interface_).toBeInstanceOf(WindowsComputerInterface);
});
it("should throw error for unsupported OS type", () => {
expect(() => {
InterfaceFactory.createInterfaceForOS(
"unsupported" as OSType,
testParams.ipAddress,
testParams.apiKey,
testParams.vmName
);
}).toThrow("Unsupported OS type: unsupported");
});
it("should create interface without API key and VM name", () => {
const interface_ = InterfaceFactory.createInterfaceForOS(
OSType.MACOS,
testParams.ipAddress
);
expect(interface_).toBeInstanceOf(MacOSComputerInterface);
});
});
});

View File

@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import * as InterfaceExports from "../../src/interface/index.ts";
describe("Interface Module Exports", () => {
it("should export InterfaceFactory", () => {
expect(InterfaceExports.InterfaceFactory).toBeDefined();
expect(InterfaceExports.InterfaceFactory.createInterfaceForOS).toBeDefined();
});
it("should export BaseComputerInterface", () => {
expect(InterfaceExports.BaseComputerInterface).toBeDefined();
});
it("should export MacOSComputerInterface", () => {
expect(InterfaceExports.MacOSComputerInterface).toBeDefined();
});
it("should export LinuxComputerInterface", () => {
expect(InterfaceExports.LinuxComputerInterface).toBeDefined();
});
it("should export WindowsComputerInterface", () => {
expect(InterfaceExports.WindowsComputerInterface).toBeDefined();
});
it("should export all expected interfaces", () => {
const expectedExports = [
"InterfaceFactory",
"BaseComputerInterface",
"MacOSComputerInterface",
"LinuxComputerInterface",
"WindowsComputerInterface",
];
const actualExports = Object.keys(InterfaceExports);
for (const exportName of expectedExports) {
expect(actualExports).toContain(exportName);
}
});
});

View File

@@ -0,0 +1,447 @@
import {
describe,
expect,
it,
beforeEach,
afterEach,
vi,
beforeAll,
afterAll,
} from "vitest";
import { InterfaceFactory } from "../../src/interface/factory.ts";
import { OSType } from "../../src/types.ts";
import { ws } from "msw";
import { setupServer } from "msw/node";
describe("Interface Integration Tests", () => {
const testIp = "192.168.1.100";
const testPort = 8000;
// Create WebSocket server
const server = setupServer();
beforeAll(() => {
server.listen({ onUnhandledRequest: "error" });
});
afterAll(() => {
server.close();
});
beforeEach(() => {
// Reset handlers for each test
server.resetHandlers();
});
afterEach(() => {
vi.clearAllMocks();
});
describe("Cross-platform interface creation", () => {
it("should create correct interface for each OS type", async () => {
const osTypes = [OSType.MACOS, OSType.LINUX, OSType.WINDOWS];
const interfaces: Array<{
os: OSType;
interface: ReturnType<typeof InterfaceFactory.createInterfaceForOS>;
}> = [];
// Create interfaces for each OS
for (const os of osTypes) {
const interface_ = InterfaceFactory.createInterfaceForOS(os, testIp);
interfaces.push({ os, interface: interface_ });
}
// Verify each interface is created correctly
expect(interfaces).toHaveLength(3);
for (const { os, interface: iface } of interfaces) {
expect(iface).toBeDefined();
// Check that the interface name contains the OS type in some form
const osName = os.toLowerCase();
expect(iface.constructor.name.toLowerCase()).toContain(osName);
}
});
it("should handle multiple interfaces with different IPs", async () => {
const ips = ["192.168.1.100", "192.168.1.101", "192.168.1.102"];
const interfaces = ips.map((ip) =>
InterfaceFactory.createInterfaceForOS(OSType.MACOS, ip)
);
// Set up WebSocket handlers for each IP
for (const ip of ips) {
const wsLink = ws.link(`ws://${ip}:${testPort}/ws`);
server.use(
wsLink.addEventListener("connection", ({ client }) => {
client.addEventListener("message", () => {
// Echo back success response
client.send(JSON.stringify({ success: true }));
});
})
);
}
// Connect all interfaces
await Promise.all(interfaces.map((iface) => iface.connect()));
// Verify all are connected
for (const iface of interfaces) {
expect(iface.isConnected()).toBe(true);
}
// Clean up
for (const iface of interfaces) {
iface.disconnect();
}
});
});
describe("Connection management", () => {
it("should handle connection lifecycle", async () => {
// Set up WebSocket handler
const wsLink = ws.link(`ws://${testIp}:${testPort}/ws`);
server.use(
wsLink.addEventListener("connection", ({ client }) => {
client.addEventListener("message", () => {
// Echo back success response
client.send(JSON.stringify({ success: true }));
});
})
);
const interface_ = InterfaceFactory.createInterfaceForOS(
OSType.MACOS,
testIp
);
// Initially not connected
expect(interface_.isConnected()).toBe(false);
// Connect
await interface_.connect();
expect(interface_.isConnected()).toBe(true);
// Disconnect
interface_.disconnect();
// Wait a tick for the close to process
await new Promise((resolve) => process.nextTick(resolve));
expect(interface_.isConnected()).toBe(false);
});
it("should handle connection errors gracefully", async () => {
// Don't register a handler - connection will succeed but no responses
const interface_ = InterfaceFactory.createInterfaceForOS(
OSType.MACOS,
"192.0.2.1" // TEST-NET-1 address
);
// Should connect (WebSocket mock always connects)
await interface_.connect();
expect(interface_.isConnected()).toBe(true);
interface_.disconnect();
});
it("should handle secure connections", async () => {
const secureIp = "192.0.2.1";
const securePort = 8443;
// Register handler for secure connection
const wsLink = ws.link(`wss://${secureIp}:${securePort}/ws`);
server.use(
wsLink.addEventListener("connection", ({ client }) => {
client.addEventListener("message", () => {
// Echo back success response
client.send(JSON.stringify({ success: true }));
});
})
);
const interface_ = InterfaceFactory.createInterfaceForOS(
OSType.MACOS,
secureIp,
"testuser",
"testpass"
);
await interface_.connect();
expect(interface_.isConnected()).toBe(true);
interface_.disconnect();
});
});
describe("Performance and concurrency", () => {
it("should handle rapid command sequences", async () => {
const receivedCommands: string[] = [];
// Set up WebSocket handler that tracks commands
const wsLink = ws.link(`ws://${testIp}:${testPort}/ws`);
server.use(
wsLink.addEventListener("connection", ({ client }) => {
client.addEventListener("message", (event) => {
const data = JSON.parse(event.data as string);
receivedCommands.push(data.action);
// Send response with command index
client.send(
JSON.stringify({
success: true,
data: `Response for ${data.action}`,
})
);
});
})
);
const interface_ = InterfaceFactory.createInterfaceForOS(
OSType.MACOS,
testIp
);
await interface_.connect();
// Send multiple commands rapidly
const commands = ["left_click", "right_click", "double_click"];
const promises = commands.map((cmd) => {
switch (cmd) {
case "left_click":
return interface_.leftClick(100, 200);
case "right_click":
return interface_.rightClick(150, 250);
case "double_click":
return interface_.doubleClick(200, 300);
}
});
await Promise.all(promises);
// Verify all commands were received
expect(receivedCommands).toHaveLength(3);
expect(receivedCommands).toContain("left_click");
expect(receivedCommands).toContain("right_click");
expect(receivedCommands).toContain("double_click");
interface_.disconnect();
});
it("should maintain command order with locking", async () => {
const receivedCommands: Array<{ action: string; index: number }> = [];
// Set up WebSocket handler that tracks commands with delay
const wsLink = ws.link(`ws://${testIp}:${testPort}/ws`);
server.use(
wsLink.addEventListener("connection", ({ client }) => {
client.addEventListener("message", async (event) => {
// Add delay to simulate processing
await new Promise((resolve) => setTimeout(resolve, 10));
const data = JSON.parse(event.data as string);
receivedCommands.push({
action: data.action,
index: data.index,
});
client.send(JSON.stringify({ success: true }));
});
})
);
const interface_ = InterfaceFactory.createInterfaceForOS(
OSType.MACOS,
testIp
);
await interface_.connect();
// Helper to send command with index
async function sendCommandWithIndex(action: string, index: number) {
await interface_.sendCommand({ action, index });
}
// Send commands in sequence
await sendCommandWithIndex("command1", 0);
await sendCommandWithIndex("command2", 1);
await sendCommandWithIndex("command3", 2);
// Wait for all commands to be processed
await new Promise((resolve) => setTimeout(resolve, 50));
// Verify commands were received in order
expect(receivedCommands).toHaveLength(3);
expect(receivedCommands[0]).toEqual({ action: "command1", index: 0 });
expect(receivedCommands[1]).toEqual({ action: "command2", index: 1 });
expect(receivedCommands[2]).toEqual({ action: "command3", index: 2 });
interface_.disconnect();
});
});
describe("Error handling", () => {
it("should handle command failures", async () => {
// Set up WebSocket handler that returns errors
const wsLink = ws.link(`ws://${testIp}:${testPort}/ws`);
server.use(
wsLink.addEventListener("connection", ({ client }) => {
client.addEventListener("message", (event) => {
const data = JSON.parse(event.data as string);
if (data.action === "fail_command") {
client.send(
JSON.stringify({
success: false,
error: "Command failed",
})
);
} else {
client.send(JSON.stringify({ success: true }));
}
});
})
);
const interface_ = InterfaceFactory.createInterfaceForOS(
OSType.MACOS,
testIp
);
await interface_.connect();
// Send a failing command
await expect(
interface_.sendCommand({ action: "fail_command" })
).rejects.toThrow("Command failed");
// Verify interface is still connected
expect(interface_.isConnected()).toBe(true);
// Send a successful command
const result = await interface_.sendCommand({
action: "success_command",
});
expect(result.success).toBe(true);
interface_.disconnect();
});
it("should handle disconnection during command", async () => {
// Set up WebSocket handler that captures WebSocket instance
const wsLink = ws.link(`ws://${testIp}:${testPort}/ws`);
server.use(
wsLink.addEventListener("connection", ({ client }) => {
client.addEventListener("message", async () => {
// Simulate disconnection during command processing
await new Promise((resolve) => setTimeout(resolve, 10));
client.close();
});
})
);
const interface_ = InterfaceFactory.createInterfaceForOS(
OSType.MACOS,
testIp
);
await interface_.connect();
// Send command that will trigger disconnection
await expect(
interface_.sendCommand({ action: "disconnect_me" })
).rejects.toThrow();
// Wait for close to process
await new Promise((resolve) => setTimeout(resolve, 20));
// Verify interface is disconnected
expect(interface_.isConnected()).toBe(false);
});
});
describe("Feature-specific tests", () => {
it("should handle screenshot commands", async () => {
// Set up WebSocket handler
const wsLink = ws.link(`ws://${testIp}:${testPort}/ws`);
server.use(
wsLink.addEventListener("connection", ({ client }) => {
client.addEventListener("message", () => {
// Echo back success response with screenshot data
client.send(JSON.stringify({
success: true,
data: "base64encodedimage"
}));
});
})
);
const interface_ = InterfaceFactory.createInterfaceForOS(
OSType.MACOS,
testIp
);
await interface_.connect();
const screenshot = await interface_.screenshot();
expect(screenshot).toBeInstanceOf(Buffer);
expect(screenshot.toString("base64")).toBe("base64encodedimage");
interface_.disconnect();
});
it("should handle screen size queries", async () => {
// Set up WebSocket handler
const wsLink = ws.link(`ws://${testIp}:${testPort}/ws`);
server.use(
wsLink.addEventListener("connection", ({ client }) => {
client.addEventListener("message", () => {
// Echo back success response with screen size
client.send(JSON.stringify({
success: true,
data: { width: 1920, height: 1080 }
}));
});
})
);
const interface_ = InterfaceFactory.createInterfaceForOS(
OSType.LINUX,
testIp
);
await interface_.connect();
const screenSize = await interface_.getScreenSize();
expect(screenSize).toEqual({ width: 1920, height: 1080 });
interface_.disconnect();
});
it("should handle file operations", async () => {
// Set up WebSocket handler
const wsLink = ws.link(`ws://${testIp}:${testPort}/ws`);
server.use(
wsLink.addEventListener("connection", ({ client }) => {
client.addEventListener("message", () => {
// Echo back success response with file data
client.send(JSON.stringify({
success: true,
data: "file content"
}));
});
})
);
const interface_ = InterfaceFactory.createInterfaceForOS(
OSType.WINDOWS,
testIp
);
await interface_.connect();
// Test file exists
const exists = await interface_.fileExists("/test/file.txt");
expect(exists).toBe(true);
// Test read text
const content = await interface_.readText("/test/file.txt");
expect(content).toBe("file content");
// Test list directory
const files = await interface_.listDir("/test");
expect(files).toEqual(["file1.txt", "file2.txt", "dir1"]);
interface_.disconnect();
});
});
});

View File

@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { LinuxComputerInterface } from "../../src/interface/linux.ts";
import { MacOSComputerInterface } from "../../src/interface/macos.ts";
describe("LinuxComputerInterface", () => {
const testParams = {
ipAddress: "192.0.2.1", // TEST-NET-1 address (RFC 5737) - guaranteed not to be routable
username: "testuser",
password: "testpass",
apiKey: "test-api-key",
vmName: "test-vm",
};
describe("Inheritance", () => {
it("should extend MacOSComputerInterface", () => {
const linuxInterface = new LinuxComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
testParams.apiKey,
testParams.vmName
);
expect(linuxInterface).toBeInstanceOf(MacOSComputerInterface);
expect(linuxInterface).toBeInstanceOf(LinuxComputerInterface);
});
});
});

View File

@@ -0,0 +1,444 @@
import {
describe,
expect,
it,
beforeEach,
afterEach,
} from "vitest";
import { MacOSComputerInterface } from "../../src/interface/macos.ts";
// Import the setup.ts which already has MSW configured
import "../setup.ts";
describe("MacOSComputerInterface", () => {
// Define test parameters
const testParams = {
ipAddress: "192.0.2.1", // TEST-NET-1 address (RFC 5737) - guaranteed not to be routable
username: "testuser",
password: "testpass",
apiKey: "test-api-key",
vmName: "test-vm",
};
// Track received messages for verification
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
let receivedMessages: any[] = [];
beforeEach(() => {
// Clear received messages before each test
receivedMessages = [];
});
afterEach(() => {
// Clear any state after each test
receivedMessages = [];
});
describe("Connection Management", () => {
it("should connect with proper authentication headers", async () => {
const macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
testParams.apiKey,
testParams.vmName
);
await macosInterface.connect();
// Verify the interface is connected
expect(macosInterface.isConnected()).toBe(true);
await macosInterface.disconnect();
});
});
describe("Mouse Actions", () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
testParams.apiKey,
testParams.vmName
);
// Remove initialize() call - connection happens on first command
await new Promise((resolve) => setTimeout(resolve, 100));
});
afterEach(async () => {
await macosInterface.disconnect();
});
it("should send mouse_down command", async () => {
await macosInterface.mouseDown(100, 200, "left");
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "mouse_down",
x: 100,
y: 200,
button: "left",
});
});
it("should send mouse_up command", async () => {
await macosInterface.mouseUp(100, 200, "right");
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "mouse_up",
x: 100,
y: 200,
button: "right",
});
});
it("should send left_click command", async () => {
await macosInterface.leftClick(150, 250);
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "left_click",
x: 150,
y: 250,
});
});
it("should send right_click command", async () => {
await macosInterface.rightClick(150, 250);
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "right_click",
x: 150,
y: 250,
});
});
it("should send double_click command", async () => {
await macosInterface.doubleClick(150, 250);
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "double_click",
x: 150,
y: 250,
});
});
it("should send move_cursor command", async () => {
await macosInterface.moveCursor(300, 400);
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "move_cursor",
x: 300,
y: 400,
});
});
it("should send drag_to command", async () => {
await macosInterface.dragTo(500, 600, "left", 1.5);
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "drag_to",
x: 500,
y: 600,
button: "left",
duration: 1.5,
});
});
it("should send scroll command", async () => {
await macosInterface.scroll(0, 10);
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "scroll",
x: 0,
y: 10,
clicks: 5,
});
});
});
describe("Keyboard Actions", () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
testParams.apiKey,
testParams.vmName
);
// Remove initialize() call
await new Promise((resolve) => setTimeout(resolve, 100));
});
afterEach(async () => {
await macosInterface.disconnect();
});
it("should send key_down command", async () => {
await macosInterface.keyDown("a");
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "key_down",
key: "a",
});
});
it("should send key_up command", async () => {
await macosInterface.keyUp("a");
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "key_up",
key: "a",
});
});
it("should send key_press command", async () => {
await macosInterface.keyDown("enter");
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "key_press",
key: "enter",
});
});
it("should send type_text command", async () => {
await macosInterface.typeText("Hello, World!");
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "type_text",
text: "Hello, World!",
});
});
it("should send hotkey command", async () => {
await macosInterface.hotkey("cmd", "c");
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "hotkey",
keys: ["cmd", "c"],
});
});
});
describe("Screen Actions", () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
testParams.apiKey,
testParams.vmName
);
// Remove initialize() call
await new Promise((resolve) => setTimeout(resolve, 100));
});
afterEach(async () => {
await macosInterface.disconnect();
});
it("should get screenshot", async () => {
const screenshot = await macosInterface.screenshot();
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "screenshot",
});
expect(screenshot).toBe("base64encodedimage");
});
it("should get screen size", async () => {
const screenSize = await macosInterface.getScreenSize();
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "get_screen_size",
});
expect(screenSize).toEqual({ width: 1920, height: 1080 });
});
it("should get cursor position", async () => {
const position = await macosInterface.getCursorPosition();
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "get_cursor_position",
});
expect(position).toEqual({ x: 100, y: 200 });
});
it("should get accessibility tree", async () => {
const tree = await macosInterface.getAccessibilityTree();
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "get_accessibility_tree",
});
expect(tree).toEqual({
role: "window",
title: "Test Window",
children: [],
});
});
});
describe("System Actions", () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
testParams.apiKey,
testParams.vmName
);
// Remove initialize() call
await new Promise((resolve) => setTimeout(resolve, 100));
});
afterEach(async () => {
await macosInterface.disconnect();
});
it("should run command", async () => {
const result = await macosInterface.runCommand("ls -la");
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
action: "run_command",
command: "ls -la",
});
expect(result).toEqual({
stdout: "command output",
stderr: "",
returncode: 0,
});
});
});
describe("Error Handling", () => {
it("should handle WebSocket connection errors", async () => {
// Use a valid but unreachable IP to avoid DNS errors
const macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
testParams.apiKey,
testParams.vmName
);
// Try to send a command - should fail with connection error
await expect(macosInterface.screenshot()).rejects.toThrow();
await macosInterface.disconnect();
});
it("should handle server error responses", async () => {
// Override the handler to send error response
// server.use(
// chat.addEventListener("connection", ({ client, server }) => {
// client.addEventListener("message", () => {
// server.send(
// JSON.stringify({
// success: false,
// error: "Command failed",
// })
// );
// });
// })
// );
const macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
testParams.apiKey,
testParams.vmName
);
// Remove initialize() call
await new Promise((resolve) => setTimeout(resolve, 100));
await expect(macosInterface.screenshot()).rejects.toThrow(
"Command failed"
);
await macosInterface.disconnect();
});
it("should handle closed connection", async () => {
const macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
testParams.apiKey,
testParams.vmName
);
// Send a command to trigger connection
await macosInterface.screenshot();
// Close the interface
await macosInterface.disconnect();
// Try to use after closing
await expect(macosInterface.screenshot()).rejects.toThrow(
"Interface is closed"
);
});
});
describe("Command Locking", () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
testParams.apiKey,
testParams.vmName
);
// Remove initialize() call
await new Promise((resolve) => setTimeout(resolve, 100));
});
afterEach(async () => {
await macosInterface.disconnect();
});
it("should serialize commands", async () => {
// Send multiple commands simultaneously
const promises = [
macosInterface.leftClick(100, 100),
macosInterface.rightClick(200, 200),
macosInterface.typeText("test"),
];
await Promise.all(promises);
// Commands should be sent in order
expect(receivedMessages).toHaveLength(3);
expect(receivedMessages[0].action).toBe("left_click");
expect(receivedMessages[1].action).toBe("right_click");
expect(receivedMessages[2].action).toBe("type_text");
});
});
});

View File

@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { WindowsComputerInterface } from "../../src/interface/windows.ts";
import { MacOSComputerInterface } from "../../src/interface/macos.ts";
describe("WindowsComputerInterface", () => {
const testParams = {
ipAddress: "192.0.2.1", // TEST-NET-1 address (RFC 5737) - guaranteed not to be routable
username: "testuser",
password: "testpass",
apiKey: "test-api-key",
vmName: "test-vm",
};
describe("Inheritance", () => {
it("should extend MacOSComputerInterface", () => {
const windowsInterface = new WindowsComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
testParams.apiKey,
testParams.vmName
);
expect(windowsInterface).toBeInstanceOf(MacOSComputerInterface);
expect(windowsInterface).toBeInstanceOf(WindowsComputerInterface);
});
});
});

View File

@@ -0,0 +1,26 @@
import { afterAll, afterEach, beforeAll } from "vitest";
import { setupServer } from "msw/node";
import { ws } from "msw";
const chat = ws.link("wss://chat.example.com");
const wsHandlers = [
chat.addEventListener("connection", ({ client }) => {
client.addEventListener("message", (event) => {
console.log("Received message from client:", event.data);
// Echo the received message back to the client
client.send(`Server received: ${event.data}`);
});
}),
];
const server = setupServer(...wsHandlers);
// Start server before all tests
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
// Close server after all tests
afterAll(() => server.close());
// Reset handlers after each test for test isolation
afterEach(() => server.resetHandlers());

View File

@@ -1,3 +1,9 @@
import { defineConfig } from 'vitest/config'
import { defineConfig } from "vitest/config";
export default defineConfig({})
export default defineConfig({
test: {
setupFiles: ["./tests/setup.ts"],
environment: "node",
globals: true,
},
});