From e39bf56248402486d6bb42f985a2b2e7a3ac0c59 Mon Sep 17 00:00:00 2001 From: chiguyong Date: Sun, 21 Jun 2026 01:42:50 +0800 Subject: [PATCH] feat(frontend): U6 tauri-auth adapter + vitest unit tests - src/api/tauri-auth.ts: abstract Keychain (Tauri) / localStorage (Web) behind a single async API (set/get/clear refresh token). Falls back to localStorage with a console.warn when the Keychain is unavailable (KTD-confirmed decision: silent localStorage fallback). - tests/unit/api/tauri-auth.test.ts: 13 vitest cases covering both Tauri and Web code paths plus the failure / fallback behaviour. - vitest.config.ts + tsconfig.test.json: minimal Vitest setup (happy-dom env, @ alias). Adds test:unit, test:unit:watch, and a typecheck alias that includes the test tree. Refs: U6 in docs/plans/2026-06-20-002-feat-centralized-auth-token-persistence-plan.md --- .../server/frontend/package-lock.json | 478 ++++++++++++++++++ src/agentkit/server/frontend/package.json | 7 +- .../server/frontend/src/api/tauri-auth.ts | 119 +++++ .../tests/unit/api/tauri-auth.test.ts | 199 ++++++++ .../server/frontend/tsconfig.test.json | 13 + src/agentkit/server/frontend/vitest.config.ts | 25 + 6 files changed, 840 insertions(+), 1 deletion(-) create mode 100644 src/agentkit/server/frontend/src/api/tauri-auth.ts create mode 100644 src/agentkit/server/frontend/tests/unit/api/tauri-auth.test.ts create mode 100644 src/agentkit/server/frontend/tsconfig.test.json create mode 100644 src/agentkit/server/frontend/vitest.config.ts diff --git a/src/agentkit/server/frontend/package-lock.json b/src/agentkit/server/frontend/package-lock.json index 199d9bd..e740af0 100644 --- a/src/agentkit/server/frontend/package-lock.json +++ b/src/agentkit/server/frontend/package-lock.json @@ -29,10 +29,12 @@ "@types/markdown-it": "^14.1.2", "@vitejs/plugin-vue": "^5.1.0", "echarts": "^6.1.0", + "happy-dom": "^15.11.7", "typescript": "^5.6.0", "unplugin-auto-import": "^21.0.0", "unplugin-vue-components": "^32.1.0", "vite": "^5.4.0", + "vitest": "^2.1.9", "vue-tsc": "^2.1.0" } }, @@ -1261,6 +1263,133 @@ "vue": "^3.2.25" } }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@volar/language-core": { "version": "2.4.15", "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz", @@ -1588,6 +1717,16 @@ "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==", "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async-validator": { "version": "4.2.5", "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", @@ -1611,6 +1750,43 @@ "balanced-match": "^1.0.0" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmmirror.com/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", @@ -1775,6 +1951,34 @@ "dev": true, "license": "MIT" }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/dom-align": { "version": "1.12.4", "resolved": "https://registry.npmmirror.com/dom-align/-/dom-align-1.12.4.tgz", @@ -1819,6 +2023,13 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", @@ -1881,6 +2092,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz", @@ -1921,6 +2142,34 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/happy-dom": { + "version": "15.11.7", + "resolved": "https://registry.npmmirror.com/happy-dom/-/happy-dom-15.11.7.tgz", + "integrity": "sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0", + "webidl-conversions": "^7.0.0", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/happy-dom/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", @@ -2023,6 +2272,13 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", @@ -2125,6 +2381,13 @@ "pathe": "^2.0.1" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/muggle-string": { "version": "0.4.1", "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", @@ -2184,6 +2447,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", @@ -2425,6 +2698,13 @@ "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==", "license": "MIT" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2434,6 +2714,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/strip-literal": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-3.1.0.tgz", @@ -2462,6 +2756,20 @@ "node": ">=12.22" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -2479,6 +2787,36 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tslib": { "version": "2.3.0", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", @@ -2713,6 +3051,109 @@ } } }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", @@ -2823,6 +3264,16 @@ "loose-envify": "^1.0.0" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", @@ -2830,6 +3281,33 @@ "dev": true, "license": "MIT" }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/zrender": { "version": "6.1.0", "resolved": "https://registry.npmmirror.com/zrender/-/zrender-6.1.0.tgz", diff --git a/src/agentkit/server/frontend/package.json b/src/agentkit/server/frontend/package.json index 896a46f..0003f31 100644 --- a/src/agentkit/server/frontend/package.json +++ b/src/agentkit/server/frontend/package.json @@ -11,7 +11,10 @@ "preview": "vite preview", "tauri": "tauri", "test:e2e": "playwright test", - "test:e2e:ui": "playwright test --ui" + "test:e2e:ui": "playwright test --ui", + "test:unit": "vitest run", + "test:unit:watch": "vitest", + "typecheck": "vue-tsc --noEmit -p tsconfig.test.json" }, "dependencies": { "@ant-design/icons-vue": "^7.0.0", @@ -35,10 +38,12 @@ "@types/markdown-it": "^14.1.2", "@vitejs/plugin-vue": "^5.1.0", "echarts": "^6.1.0", + "happy-dom": "^15.11.7", "typescript": "^5.6.0", "unplugin-auto-import": "^21.0.0", "unplugin-vue-components": "^32.1.0", "vite": "^5.4.0", + "vitest": "^2.1.9", "vue-tsc": "^2.1.0" } } diff --git a/src/agentkit/server/frontend/src/api/tauri-auth.ts b/src/agentkit/server/frontend/src/api/tauri-auth.ts new file mode 100644 index 0000000..c41532c --- /dev/null +++ b/src/agentkit/server/frontend/src/api/tauri-auth.ts @@ -0,0 +1,119 @@ +/** + * Tauri-aware refresh token storage adapter. + * + * In Tauri mode the refresh token is written to the OS Keychain via the + * `store_refresh_token` / `load_refresh_token` / `clear_refresh_token` + * Rust commands (see `src-tauri/src/auth.rs`). In plain Web mode + * (no Tauri runtime) it falls back to `localStorage` with the + * documented degraded security model. + * + * The async API surface is identical in both modes, so the auth store + * (and any other caller) does not need to branch on the environment. + * + * Keychain failures are logged and transparently downgraded to + * `localStorage` so a broken OS credential service never blocks login + * (KTD-confirmed decision: silent localStorage fallback). + */ + +const STORAGE_KEY = 'agentkit.refresh_token' + +/** Detect whether the current page is running inside a Tauri WebView. */ +export function isTauri(): boolean { + return typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window +} + +/** + * Lazily import the Tauri `invoke` API. + * + * The dynamic import keeps the `@tauri-apps/api` bundle out of the Web + * build when it is not needed and lets us `catch` import-time errors + * that occur when the WebView is not actually Tauri-backed. + */ +async function tauriInvoke(cmd: string, args?: Record): Promise { + const mod = await import('@tauri-apps/api/core') + return mod.invoke(cmd, args) +} + +function localGet(): string | null { + try { + return localStorage.getItem(STORAGE_KEY) + } catch { + return null + } +} + +function localSet(value: string): void { + try { + localStorage.setItem(STORAGE_KEY, value) + } catch { + /* localStorage may be unavailable in some sandboxed contexts */ + } +} + +function localRemove(): void { + try { + localStorage.removeItem(STORAGE_KEY) + } catch { + /* ignore */ + } +} + +export const tauriAuthStorage = { + /** + * Persist the refresh token. + * + * Tauri → OS Keychain via `store_refresh_token`. Web → localStorage. + * On Keychain failure (or any Tauri error), the value is written to + * localStorage as a fallback and a warning is logged. + */ + async setRefreshToken(token: string): Promise { + if (isTauri()) { + try { + await tauriInvoke('store_refresh_token', { token }) + return + } catch (e) { + console.warn('[auth] Keychain write failed, falling back to localStorage', e) + } + } + localSet(token) + }, + + /** + * Read the persisted refresh token. + * + * Tauri → OS Keychain via `load_refresh_token`. Web → localStorage. + * Returns `null` when no token has been stored. On Keychain failure + * (or any Tauri error), falls back to localStorage and logs a warning. + */ + async getRefreshToken(): Promise { + if (isTauri()) { + try { + const value = await tauriInvoke('load_refresh_token') + return value ?? null + } catch (e) { + console.warn('[auth] Keychain read failed, falling back to localStorage', e) + } + } + return localGet() + }, + + /** + * Remove the persisted refresh token. + * + * Tauri → OS Keychain via `clear_refresh_token`. Web → localStorage. + * On Keychain failure, the localStorage copy is also cleared as a + * best-effort defence-in-depth. + */ + async clearRefreshToken(): Promise { + if (isTauri()) { + try { + await tauriInvoke('clear_refresh_token') + } catch (e) { + console.warn('[auth] Keychain clear failed, falling back to localStorage clear', e) + } + } + localRemove() + }, +} + +export type TauriAuthStorage = typeof tauriAuthStorage diff --git a/src/agentkit/server/frontend/tests/unit/api/tauri-auth.test.ts b/src/agentkit/server/frontend/tests/unit/api/tauri-auth.test.ts new file mode 100644 index 0000000..13eaa03 --- /dev/null +++ b/src/agentkit/server/frontend/tests/unit/api/tauri-auth.test.ts @@ -0,0 +1,199 @@ +/** + * Unit tests for the tauri-auth adapter. + * + * The adapter is a thin wrapper over the Tauri `invoke` API and + * `localStorage`. We mock `@tauri-apps/api/core` so we can assert + * which command is dispatched and simulate the two relevant + * failure modes (Keychain unavailable, invoke throws). + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +// Mock the @tauri-apps/api/core module BEFORE importing the SUT. +const mockInvoke = vi.fn() +vi.mock('@tauri-apps/api/core', () => ({ + invoke: (...args: unknown[]) => mockInvoke(...args), +})) + +// Import after the mock is in place. +const { isTauri, tauriAuthStorage } = await import('@/api/tauri-auth') + +const STORAGE_KEY = 'agentkit.refresh_token' + +/** + * Polyfill a minimal in-memory `localStorage` for the test environment. + * + * Some happy-dom versions ship a stub that lacks `.clear()` (a known + * regression in vitest 2.x with happy-dom). We work around it by + * providing our own implementation that has the full Storage interface. + */ +const _store = new Map() +const _localStoragePolyfill: Storage = { + get length() { + return _store.size + }, + clear() { + _store.clear() + }, + getItem(key: string) { + return _store.has(key) ? (_store.get(key) as string) : null + }, + key(index: number) { + return Array.from(_store.keys())[index] ?? null + }, + removeItem(key: string) { + _store.delete(key) + }, + setItem(key: string, value: string) { + _store.set(key, String(value)) + }, +} + +beforeEach(() => { + ;(globalThis as { localStorage?: Storage }).localStorage = _localStoragePolyfill + mockInvoke.mockReset() + _store.clear() + setTauri(false) +}) + +afterEach(() => { + setTauri(false) + _store.clear() + vi.restoreAllMocks() +}) + +/** Inject / remove the Tauri runtime marker on `window`. */ +function setTauri(present: boolean): void { + if (present) { + ;(window as unknown as { __TAURI_INTERNALS__?: object }).__TAURI_INTERNALS__ = {} + } else { + delete (window as unknown as { __TAURI_INTERNALS__?: object }).__TAURI_INTERNALS__ + } +} + +beforeEach(() => { + mockInvoke.mockReset() + localStorage.clear() + setTauri(false) +}) + +afterEach(() => { + setTauri(false) + localStorage.clear() + vi.restoreAllMocks() +}) + +describe('isTauri', () => { + it('returns true when __TAURI_INTERNALS__ is present on window', () => { + setTauri(true) + expect(isTauri()).toBe(true) + }) + + it('returns false when __TAURI_INTERNALS__ is missing', () => { + setTauri(false) + expect(isTauri()).toBe(false) + }) +}) + +describe('tauriAuthStorage.setRefreshToken', () => { + it('writes to localStorage in Web mode (no Tauri)', async () => { + await tauriAuthStorage.setRefreshToken('web-token') + expect(localStorage.getItem(STORAGE_KEY)).toBe('web-token') + expect(mockInvoke).not.toHaveBeenCalled() + }) + + it('calls invoke("store_refresh_token") in Tauri mode', async () => { + setTauri(true) + mockInvoke.mockResolvedValueOnce(undefined) + await tauriAuthStorage.setRefreshToken('keychain-token') + expect(mockInvoke).toHaveBeenCalledTimes(1) + expect(mockInvoke).toHaveBeenCalledWith('store_refresh_token', { token: 'keychain-token' }) + // Web fallback should NOT have been used. + expect(localStorage.getItem(STORAGE_KEY)).toBeNull() + }) + + it('falls back to localStorage when invoke throws in Tauri mode', async () => { + setTauri(true) + mockInvoke.mockRejectedValueOnce(new Error('keychain unavailable')) + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + + await tauriAuthStorage.setRefreshToken('fallback-token') + + expect(warnSpy).toHaveBeenCalled() + expect(localStorage.getItem(STORAGE_KEY)).toBe('fallback-token') + }) +}) + +describe('tauriAuthStorage.getRefreshToken', () => { + it('returns null from localStorage in Web mode when nothing is stored', async () => { + const result = await tauriAuthStorage.getRefreshToken() + expect(result).toBeNull() + expect(mockInvoke).not.toHaveBeenCalled() + }) + + it('returns the value from localStorage in Web mode', async () => { + localStorage.setItem(STORAGE_KEY, 'stored-web') + const result = await tauriAuthStorage.getRefreshToken() + expect(result).toBe('stored-web') + expect(mockInvoke).not.toHaveBeenCalled() + }) + + it('returns the value from invoke("load_refresh_token") in Tauri mode', async () => { + setTauri(true) + mockInvoke.mockResolvedValueOnce('keychain-value') + const result = await tauriAuthStorage.getRefreshToken() + expect(result).toBe('keychain-value') + expect(mockInvoke).toHaveBeenCalledTimes(1) + // The first argument is the command name; the second may be undefined + // when no args are needed. + expect(mockInvoke.mock.calls[0]?.[0]).toBe('load_refresh_token') + }) + + it('normalizes a null invoke response to null in Tauri mode', async () => { + setTauri(true) + mockInvoke.mockResolvedValueOnce(null) + const result = await tauriAuthStorage.getRefreshToken() + expect(result).toBeNull() + }) + + it('falls back to localStorage when invoke throws in Tauri mode', async () => { + setTauri(true) + localStorage.setItem(STORAGE_KEY, 'web-copy') + mockInvoke.mockRejectedValueOnce(new Error('read failed')) + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + + const result = await tauriAuthStorage.getRefreshToken() + + expect(warnSpy).toHaveBeenCalled() + expect(result).toBe('web-copy') + }) +}) + +describe('tauriAuthStorage.clearRefreshToken', () => { + it('removes the entry from localStorage in Web mode', async () => { + localStorage.setItem(STORAGE_KEY, 'to-remove') + await tauriAuthStorage.clearRefreshToken() + expect(localStorage.getItem(STORAGE_KEY)).toBeNull() + expect(mockInvoke).not.toHaveBeenCalled() + }) + + it('calls invoke("clear_refresh_token") in Tauri mode', async () => { + setTauri(true) + mockInvoke.mockResolvedValueOnce(undefined) + await tauriAuthStorage.clearRefreshToken() + expect(mockInvoke).toHaveBeenCalledTimes(1) + expect(mockInvoke.mock.calls[0]?.[0]).toBe('clear_refresh_token') + }) + + it('still removes the localStorage copy when invoke throws in Tauri mode', async () => { + setTauri(true) + localStorage.setItem(STORAGE_KEY, 'still-here') + mockInvoke.mockRejectedValueOnce(new Error('clear failed')) + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + + await tauriAuthStorage.clearRefreshToken() + + expect(warnSpy).toHaveBeenCalled() + expect(localStorage.getItem(STORAGE_KEY)).toBeNull() + }) +}) diff --git a/src/agentkit/server/frontend/tsconfig.test.json b/src/agentkit/server/frontend/tsconfig.test.json new file mode 100644 index 0000000..7a00cc6 --- /dev/null +++ b/src/agentkit/server/frontend/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.vue", + "tests/**/*.ts", + "env.d.ts" + ] +} diff --git a/src/agentkit/server/frontend/vitest.config.ts b/src/agentkit/server/frontend/vitest.config.ts new file mode 100644 index 0000000..0ff5b7c --- /dev/null +++ b/src/agentkit/server/frontend/vitest.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vitest/config' +import { resolve } from 'path' + +/** + * Vitest config for frontend unit tests. + * + * The unit tests cover pure-TS modules (no Vue components) and run in a + * happy-dom environment so the few modules that touch `window`, + * `localStorage`, or `__TAURI_INTERNALS__` work the same way as in the + * browser. + */ +export default defineConfig({ + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + }, + test: { + environment: 'happy-dom', + globals: false, + include: ['tests/unit/**/*.test.ts'], + // Keep CI noise low: fail fast on the first failure. + reporters: process.env.CI ? ['default'] : ['default'], + }, +})