From 593afe220f972e706034fac7dea2ee70f5015509 Mon Sep 17 00:00:00 2001 From: Morgan Dean Date: Wed, 18 Jun 2025 15:26:07 -0700 Subject: [PATCH] Create tests. Note: ws mocking is currently broken --- libs/computer/typescript/package.json | 1 + libs/computer/typescript/pnpm-lock.yaml | 418 +++++++++++++++- .../src/computer/providers/cloud.ts | 2 +- .../computer/typescript/src/interface/base.ts | 24 +- .../tests/interface/factory.test.ts | 71 +++ .../typescript/tests/interface/index.test.ts | 40 ++ .../tests/interface/integration.test.ts | 447 ++++++++++++++++++ .../typescript/tests/interface/linux.test.ts | 28 ++ .../typescript/tests/interface/macos.test.ts | 444 +++++++++++++++++ .../tests/interface/windows.test.ts | 28 ++ libs/computer/typescript/tests/setup.ts | 26 + libs/computer/typescript/vitest.config.ts | 10 +- 12 files changed, 1526 insertions(+), 13 deletions(-) create mode 100644 libs/computer/typescript/tests/interface/factory.test.ts create mode 100644 libs/computer/typescript/tests/interface/index.test.ts create mode 100644 libs/computer/typescript/tests/interface/integration.test.ts create mode 100644 libs/computer/typescript/tests/interface/linux.test.ts create mode 100644 libs/computer/typescript/tests/interface/macos.test.ts create mode 100644 libs/computer/typescript/tests/interface/windows.test.ts create mode 100644 libs/computer/typescript/tests/setup.ts diff --git a/libs/computer/typescript/package.json b/libs/computer/typescript/package.json index 71141f22..8ff5c095 100644 --- a/libs/computer/typescript/package.json +++ b/libs/computer/typescript/package.json @@ -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", diff --git a/libs/computer/typescript/pnpm-lock.yaml b/libs/computer/typescript/pnpm-lock.yaml index 2cd52963..1abeebdc 100644 --- a/libs/computer/typescript/pnpm-lock.yaml +++ b/libs/computer/typescript/pnpm-lock.yaml @@ -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: {} diff --git a/libs/computer/typescript/src/computer/providers/cloud.ts b/libs/computer/typescript/src/computer/providers/cloud.ts index eb1ec1b1..d8f9f1cc 100644 --- a/libs/computer/typescript/src/computer/providers/cloud.ts +++ b/libs/computer/typescript/src/computer/providers/cloud.ts @@ -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; } diff --git a/libs/computer/typescript/src/interface/base.ts b/libs/computer/typescript/src/interface/base.ts index aef1e2be..169cf4b0 100644 --- a/libs/computer/typescript/src/interface/base.ts +++ b/libs/computer/typescript/src/interface/base.ts @@ -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 { + public async connect(): Promise { 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 diff --git a/libs/computer/typescript/tests/interface/factory.test.ts b/libs/computer/typescript/tests/interface/factory.test.ts new file mode 100644 index 00000000..8aa1a1d7 --- /dev/null +++ b/libs/computer/typescript/tests/interface/factory.test.ts @@ -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); + }); + }); +}); diff --git a/libs/computer/typescript/tests/interface/index.test.ts b/libs/computer/typescript/tests/interface/index.test.ts new file mode 100644 index 00000000..0c8264ab --- /dev/null +++ b/libs/computer/typescript/tests/interface/index.test.ts @@ -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); + } + }); +}); diff --git a/libs/computer/typescript/tests/interface/integration.test.ts b/libs/computer/typescript/tests/interface/integration.test.ts new file mode 100644 index 00000000..b0867ca7 --- /dev/null +++ b/libs/computer/typescript/tests/interface/integration.test.ts @@ -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; + }> = []; + + // 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(); + }); + }); +}); diff --git a/libs/computer/typescript/tests/interface/linux.test.ts b/libs/computer/typescript/tests/interface/linux.test.ts new file mode 100644 index 00000000..d5d8532b --- /dev/null +++ b/libs/computer/typescript/tests/interface/linux.test.ts @@ -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); + }); + }); +}); diff --git a/libs/computer/typescript/tests/interface/macos.test.ts b/libs/computer/typescript/tests/interface/macos.test.ts new file mode 100644 index 00000000..fb7e4b08 --- /dev/null +++ b/libs/computer/typescript/tests/interface/macos.test.ts @@ -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: + 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"); + }); + }); +}); diff --git a/libs/computer/typescript/tests/interface/windows.test.ts b/libs/computer/typescript/tests/interface/windows.test.ts new file mode 100644 index 00000000..cf8fe9e9 --- /dev/null +++ b/libs/computer/typescript/tests/interface/windows.test.ts @@ -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); + }); + }); +}); diff --git a/libs/computer/typescript/tests/setup.ts b/libs/computer/typescript/tests/setup.ts new file mode 100644 index 00000000..42b09201 --- /dev/null +++ b/libs/computer/typescript/tests/setup.ts @@ -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()); diff --git a/libs/computer/typescript/vitest.config.ts b/libs/computer/typescript/vitest.config.ts index abed6b21..4f76f6cc 100644 --- a/libs/computer/typescript/vitest.config.ts +++ b/libs/computer/typescript/vitest.config.ts @@ -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, + }, +});