feat: implement stability fixes and enhance error handling
This commit is contained in:
1 parent
b0ec96d4a6
commit
5772e2e90e
17 files changed
+1161
-108
No files matched your search
Generated
+364
-66
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "antigravity-manager",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "antigravity-manager",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"version": "0.1.1",
|
||||
"license": "CC-BY-NC-SA-4.0",
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.14.3",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
@@ -52,6 +52,7 @@
|
||||
"i18next": "^25.6.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"keytar": "^7.9.0",
|
||||
"lodash-es": "^4.17.22",
|
||||
"lucide-react": "^0.553.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
@@ -89,6 +90,7 @@
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
"@types/electron-squirrel-startup": "^1.0.2",
|
||||
"@types/eslint-config-prettier": "^6.11.3",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
@@ -945,9 +947,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/cli": {
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-7.10.2.tgz",
|
||||
"integrity": "sha512-X1RtS5IqNgzGDS2rr1q0Y74wU/m3DbU4vSgllNun1ZQv1BfMpDcKLhnKi3aeetoA0huLTpMVU9eWJ7bziI9fxA==",
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-7.11.1.tgz",
|
||||
"integrity": "sha512-pk8AoLsr7t7LBAt0cFD06XFA6uxtPdvtLx06xeal7O9o7GHGCbj29WGwFoJ8Br/ENM0Ho868S3PrAn1PtBXt5g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -961,9 +963,9 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/core": "7.10.2",
|
||||
"@electron-forge/core-utils": "7.10.2",
|
||||
"@electron-forge/shared-types": "7.10.2",
|
||||
"@electron-forge/core": "7.11.1",
|
||||
"@electron-forge/core-utils": "7.11.1",
|
||||
"@electron-forge/shared-types": "7.11.1",
|
||||
"@electron/get": "^3.0.0",
|
||||
"@inquirer/prompts": "^6.0.1",
|
||||
"@listr2/prompt-adapter-inquirer": "^2.0.22",
|
||||
@@ -984,10 +986,39 @@
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/cli/node_modules/@electron-forge/shared-types": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz",
|
||||
"integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/tracer": "7.11.1",
|
||||
"@electron/packager": "^18.3.5",
|
||||
"@electron/rebuild": "^3.7.0",
|
||||
"listr2": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/cli/node_modules/@electron-forge/tracer": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz",
|
||||
"integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chrome-trace-event": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.17.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/core": {
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/core/-/core-7.10.2.tgz",
|
||||
"integrity": "sha512-HAIuOtpOfGjA0cd55tbEV2gAv+A7tSZg9bonmVDYFEe6dBgbLk8a3+/1fJUdWW8fyFkg1wa8zK7pjP751bAXsA==",
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/core/-/core-7.11.1.tgz",
|
||||
"integrity": "sha512-YtuPLzggPKPabFAD2rOZFE0s7f4KaUTpGRduhSMbZUqpqD1TIPyfoDBpYiZvao3Ht8pyZeOJjbzcC0LpFs9gIQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -1001,17 +1032,17 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/core-utils": "7.10.2",
|
||||
"@electron-forge/maker-base": "7.10.2",
|
||||
"@electron-forge/plugin-base": "7.10.2",
|
||||
"@electron-forge/publisher-base": "7.10.2",
|
||||
"@electron-forge/shared-types": "7.10.2",
|
||||
"@electron-forge/template-base": "7.10.2",
|
||||
"@electron-forge/template-vite": "7.10.2",
|
||||
"@electron-forge/template-vite-typescript": "7.10.2",
|
||||
"@electron-forge/template-webpack": "7.10.2",
|
||||
"@electron-forge/template-webpack-typescript": "7.10.2",
|
||||
"@electron-forge/tracer": "7.10.2",
|
||||
"@electron-forge/core-utils": "7.11.1",
|
||||
"@electron-forge/maker-base": "7.11.1",
|
||||
"@electron-forge/plugin-base": "7.11.1",
|
||||
"@electron-forge/publisher-base": "7.11.1",
|
||||
"@electron-forge/shared-types": "7.11.1",
|
||||
"@electron-forge/template-base": "7.11.1",
|
||||
"@electron-forge/template-vite": "7.11.1",
|
||||
"@electron-forge/template-vite-typescript": "7.11.1",
|
||||
"@electron-forge/template-webpack": "7.11.1",
|
||||
"@electron-forge/template-webpack-typescript": "7.11.1",
|
||||
"@electron-forge/tracer": "7.11.1",
|
||||
"@electron/get": "^3.0.0",
|
||||
"@electron/packager": "^18.3.5",
|
||||
"@electron/rebuild": "^3.7.0",
|
||||
@@ -1041,13 +1072,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/core-utils": {
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/core-utils/-/core-utils-7.10.2.tgz",
|
||||
"integrity": "sha512-JXrk2hWR4q8KgZFABpojjuqql3tYeVIH6qmtbkNEkZEQq7YIxajJBCct7J7bWfNQTmHotsQ3k5KLknhyhTaBMw==",
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/core-utils/-/core-utils-7.11.1.tgz",
|
||||
"integrity": "sha512-9UxRWVsfcziBsbAA2MS0Oz4yYovQCO2BhnGIfsbKNTBtMc/RcVSxAS0NMyymce44i43p1ZC/FqWhnt1XqYw3bQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/shared-types": "7.10.2",
|
||||
"@electron-forge/shared-types": "7.11.1",
|
||||
"@electron/rebuild": "^3.7.0",
|
||||
"@malept/cross-spawn-promise": "^2.0.0",
|
||||
"chalk": "^4.0.0",
|
||||
@@ -1062,6 +1093,105 @@
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/core-utils/node_modules/@electron-forge/shared-types": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz",
|
||||
"integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/tracer": "7.11.1",
|
||||
"@electron/packager": "^18.3.5",
|
||||
"@electron/rebuild": "^3.7.0",
|
||||
"listr2": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/core-utils/node_modules/@electron-forge/tracer": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz",
|
||||
"integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chrome-trace-event": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.17.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/core/node_modules/@electron-forge/maker-base": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/maker-base/-/maker-base-7.11.1.tgz",
|
||||
"integrity": "sha512-yhZrCGoN6bDeiB5DHFaueZ1h84AReElEj+f0hl2Ph4UbZnO0cnLpbx+Bs+XfMLAiA+beC8muB5UDK5ysfuT9BQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/shared-types": "7.11.1",
|
||||
"fs-extra": "^10.0.0",
|
||||
"which": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/core/node_modules/@electron-forge/plugin-base": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/plugin-base/-/plugin-base-7.11.1.tgz",
|
||||
"integrity": "sha512-lKpSOV1GA3FoYiD9k05i6v4KaQVmojnRgCr7d6VL1bFp13QOtXSaAWhFI9mtSY7rGElOacX6Zt7P7rPoB8T9eQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/shared-types": "7.11.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/core/node_modules/@electron-forge/publisher-base": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/publisher-base/-/publisher-base-7.11.1.tgz",
|
||||
"integrity": "sha512-rXE9oMFGMtdQrixnumWYH5TTGsp99iPHZb3jI74YWq518ctCh6DlIgWlhf6ok2X0+lhWovcIb45KJucUFAQ13w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/shared-types": "7.11.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/core/node_modules/@electron-forge/shared-types": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz",
|
||||
"integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/tracer": "7.11.1",
|
||||
"@electron/packager": "^18.3.5",
|
||||
"@electron/rebuild": "^3.7.0",
|
||||
"listr2": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/core/node_modules/@electron-forge/tracer": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz",
|
||||
"integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chrome-trace-event": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.17.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/maker-base": {
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/maker-base/-/maker-base-7.10.2.tgz",
|
||||
@@ -1262,14 +1392,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/template-base": {
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/template-base/-/template-base-7.10.2.tgz",
|
||||
"integrity": "sha512-D9DbEx3rtikIhUyn4tcz2pJqHNU/+FXKNnzSvmrJoJ9LusR3C42OU9GtbU8oT3nawpnCGgPFIOGXrzexFPp6DA==",
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/template-base/-/template-base-7.11.1.tgz",
|
||||
"integrity": "sha512-XpTaEf+EfQw+0BlSAtSpZKYIKYvKu4raNzSGHZZoSYHp+HDC7R+MlpFQmSJiGdYQzQ14C+uxO42tVjgM0DMbpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/core-utils": "7.10.2",
|
||||
"@electron-forge/shared-types": "7.10.2",
|
||||
"@electron-forge/core-utils": "7.11.1",
|
||||
"@electron-forge/shared-types": "7.11.1",
|
||||
"@malept/cross-spawn-promise": "^2.0.0",
|
||||
"debug": "^4.3.1",
|
||||
"fs-extra": "^10.0.0",
|
||||
@@ -1280,15 +1410,44 @@
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/template-vite": {
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/template-vite/-/template-vite-7.10.2.tgz",
|
||||
"integrity": "sha512-hR9HBOM902yq7zhFl8bO3w5ufMgitdd5ZwDzAdKITFh2ttZemHy9ha5S0K+R+4GoXHz8t7hUTHk8+iPy09qrpA==",
|
||||
"node_modules/@electron-forge/template-base/node_modules/@electron-forge/shared-types": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz",
|
||||
"integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/shared-types": "7.10.2",
|
||||
"@electron-forge/template-base": "7.10.2",
|
||||
"@electron-forge/tracer": "7.11.1",
|
||||
"@electron/packager": "^18.3.5",
|
||||
"@electron/rebuild": "^3.7.0",
|
||||
"listr2": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/template-base/node_modules/@electron-forge/tracer": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz",
|
||||
"integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chrome-trace-event": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.17.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/template-vite": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/template-vite/-/template-vite-7.11.1.tgz",
|
||||
"integrity": "sha512-Or8Lxf4awoeUZoMTKJEw5KQDIhqOFs24WhVka3yZXxc6VgVWN79KmYKYM6uM/YMQttmafhsBhY2t1Lxo1WR/ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/shared-types": "7.11.1",
|
||||
"@electron-forge/template-base": "7.11.1",
|
||||
"fs-extra": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1296,29 +1455,87 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/template-vite-typescript": {
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/template-vite-typescript/-/template-vite-typescript-7.10.2.tgz",
|
||||
"integrity": "sha512-df7rpxxIOIyZn0RfQ1GIlLW7dXhxkerc9uZ3ozO4C7zfvip3z0Mg+wS1synktPfr4WISaPktIdnj3mVu6Uu7Mw==",
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/template-vite-typescript/-/template-vite-typescript-7.11.1.tgz",
|
||||
"integrity": "sha512-Us4AHXFb+4z+gXgZImSqMBS63oKnsQWLOhqRg321xiDzu2UcQPlwgWNb4rAEKNVC1e7LXrUNDHuBiTrQkvWXbg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/shared-types": "7.10.2",
|
||||
"@electron-forge/template-base": "7.10.2",
|
||||
"@electron-forge/shared-types": "7.11.1",
|
||||
"@electron-forge/template-base": "7.11.1",
|
||||
"fs-extra": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/template-webpack": {
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/template-webpack/-/template-webpack-7.10.2.tgz",
|
||||
"integrity": "sha512-VIUXA+XHM5SLjg7fIpOOmBsgi0LstkjrEz4gUzVL0AaITM7e+BCziIHld1ceXLbQ1FnKtrUGnQ9X/cHYxYvhHg==",
|
||||
"node_modules/@electron-forge/template-vite-typescript/node_modules/@electron-forge/shared-types": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz",
|
||||
"integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/shared-types": "7.10.2",
|
||||
"@electron-forge/template-base": "7.10.2",
|
||||
"@electron-forge/tracer": "7.11.1",
|
||||
"@electron/packager": "^18.3.5",
|
||||
"@electron/rebuild": "^3.7.0",
|
||||
"listr2": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/template-vite-typescript/node_modules/@electron-forge/tracer": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz",
|
||||
"integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chrome-trace-event": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.17.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/template-vite/node_modules/@electron-forge/shared-types": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz",
|
||||
"integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/tracer": "7.11.1",
|
||||
"@electron/packager": "^18.3.5",
|
||||
"@electron/rebuild": "^3.7.0",
|
||||
"listr2": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/template-vite/node_modules/@electron-forge/tracer": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz",
|
||||
"integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chrome-trace-event": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.17.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/template-webpack": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/template-webpack/-/template-webpack-7.11.1.tgz",
|
||||
"integrity": "sha512-15lbXxi+er461MPk6sbwAOyjofAHwmQjTvxNCiNpaU2naEwbj3t0SlLq/BMr5HxnVOaMmA7+lKV9afkIom+d4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/shared-types": "7.11.1",
|
||||
"@electron-forge/template-base": "7.11.1",
|
||||
"fs-extra": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1326,14 +1543,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/template-webpack-typescript": {
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/template-webpack-typescript/-/template-webpack-typescript-7.10.2.tgz",
|
||||
"integrity": "sha512-JtrLUAFbxxWJ1kU7b8MNyL5SO9/rY5UeNz1b9hvMvilW8GxyMWUen58dafgdnx3OpKLNZnhOOhgRagNppEzJOA==",
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/template-webpack-typescript/-/template-webpack-typescript-7.11.1.tgz",
|
||||
"integrity": "sha512-6ExfFnFkHBz8rvRFTFg5HVGTC12uJpbVk4q8DVg0R8rhhxhqiVNh8lF2UPtZ2yT2UtGWjXNVlyP3Y3T6q6E3GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/shared-types": "7.10.2",
|
||||
"@electron-forge/template-base": "7.10.2",
|
||||
"@electron-forge/shared-types": "7.11.1",
|
||||
"@electron-forge/template-base": "7.11.1",
|
||||
"fs-extra": "^10.0.0",
|
||||
"typescript": "~5.4.5",
|
||||
"webpack": "^5.69.1"
|
||||
@@ -1342,6 +1559,35 @@
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/template-webpack-typescript/node_modules/@electron-forge/shared-types": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz",
|
||||
"integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/tracer": "7.11.1",
|
||||
"@electron/packager": "^18.3.5",
|
||||
"@electron/rebuild": "^3.7.0",
|
||||
"listr2": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/template-webpack-typescript/node_modules/@electron-forge/tracer": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz",
|
||||
"integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chrome-trace-event": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.17.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/template-webpack-typescript/node_modules/typescript": {
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||
@@ -1356,6 +1602,35 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/template-webpack/node_modules/@electron-forge/shared-types": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz",
|
||||
"integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/tracer": "7.11.1",
|
||||
"@electron/packager": "^18.3.5",
|
||||
"@electron/rebuild": "^3.7.0",
|
||||
"listr2": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/template-webpack/node_modules/@electron-forge/tracer": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz",
|
||||
"integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chrome-trace-event": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.17.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/tracer": {
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.10.2.tgz",
|
||||
@@ -8110,6 +8385,23 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash-es": {
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mute-stream": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz",
|
||||
@@ -8562,9 +8854,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/sudo-prompt": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.1.tgz",
|
||||
"integrity": "sha512-9ORTwwS74VaTn38tNbQhsA5U44zkJfcb0BdTSyyG6frP4e8KMtHuTXYmwefe5dpL8XB1aGSIVTaLjD3BbWb5iA==",
|
||||
"version": "9.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.2.tgz",
|
||||
"integrity": "sha512-gcXoCN00METUNFeQOFJ+C9xUI0DKB+0EGMVg7wbVYRHBw2Eq3fKisDZOkRdOz3kqXRKOENMfShPOmypw1/8nOw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -10738,9 +11030,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
|
||||
"integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==",
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz",
|
||||
"integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
@@ -11374,9 +11666,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron/node_modules/@types/node": {
|
||||
"version": "20.19.25",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
|
||||
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
|
||||
"version": "20.19.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
|
||||
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -15618,6 +15910,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.22",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
|
||||
"integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
@@ -20523,9 +20821,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
|
||||
"integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz",
|
||||
"integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
|
||||
+2
-6
@@ -47,6 +47,7 @@
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
"@types/electron-squirrel-startup": "^1.0.2",
|
||||
"@types/eslint-config-prettier": "^6.11.3",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
@@ -120,6 +121,7 @@
|
||||
"i18next": "^25.6.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"keytar": "^7.9.0",
|
||||
"lodash-es": "^4.17.22",
|
||||
"lucide-react": "^0.553.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
@@ -132,11 +134,5 @@
|
||||
"update-electron-app": "^3.1.1",
|
||||
"uuid": "^13.0.0",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"ts-node": {
|
||||
"transpileOnly": true,
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Usage,
|
||||
GroundingMetadata,
|
||||
} from './types';
|
||||
import { decodeSignature } from './signature-utils';
|
||||
|
||||
/**
|
||||
* Non-streaming response processor (Gemini -> Claude)
|
||||
@@ -55,7 +56,7 @@ class NonStreamingProcessor {
|
||||
}
|
||||
|
||||
private processPart(part: GeminiPart) {
|
||||
const signature = part.thoughtSignature || null;
|
||||
const signature = decodeSignature(part.thoughtSignature) || null;
|
||||
|
||||
// 1. Handle FunctionCall
|
||||
if (part.functionCall) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { GeminiPart, Usage, UsageMetadata, GroundingMetadata } from './types';
|
||||
import { GeminiPart, Usage, UsageMetadata } from './types';
|
||||
import { SignatureStore } from './SignatureStore';
|
||||
import { decodeSignature } from './signature-utils';
|
||||
|
||||
type BlockType = 'None' | 'Text' | 'Thinking' | 'Function';
|
||||
|
||||
@@ -43,6 +44,8 @@ export class StreamingState {
|
||||
public webSearchQuery: string | null = null;
|
||||
public groundingChunks: any[] | null = null;
|
||||
|
||||
private parseErrorCount: number = 0;
|
||||
|
||||
constructor() {}
|
||||
|
||||
public emit(eventType: string, data: any): string {
|
||||
@@ -242,6 +245,56 @@ export class StreamingState {
|
||||
public storeSignature(signature?: string) {
|
||||
this.signatures.store(signature);
|
||||
}
|
||||
public handleParseError(rawData: string): string[] {
|
||||
const chunks: string[] = [];
|
||||
this.parseErrorCount++;
|
||||
|
||||
console.warn(
|
||||
`[SSE-Parser] Parse error #${this.parseErrorCount}. Raw data length: ${rawData.length}`,
|
||||
);
|
||||
|
||||
// Safely close current block
|
||||
if (this.blockType !== 'None') {
|
||||
chunks.push(...this.endBlock());
|
||||
}
|
||||
|
||||
// Emit error event if too many errors
|
||||
if (this.parseErrorCount > 3) {
|
||||
console.error(
|
||||
`[SSE-Parser] High error rate (${this.parseErrorCount} errors). Stream may be corrupted.`,
|
||||
);
|
||||
chunks.push(
|
||||
this.emit('error', {
|
||||
type: 'error',
|
||||
error: {
|
||||
type: 'network_error',
|
||||
message: 'Unstable network connection. Please check your network or proxy settings.',
|
||||
code: 'stream_decode_error',
|
||||
details: {
|
||||
error_count: this.parseErrorCount,
|
||||
suggestion: 'Check network connection',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset error state (call after recovery)
|
||||
*/
|
||||
public resetErrorState(): void {
|
||||
this.parseErrorCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current error count (for monitoring)
|
||||
*/
|
||||
public getErrorCount(): number {
|
||||
return this.parseErrorCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -252,7 +305,7 @@ export class PartProcessor {
|
||||
|
||||
public process(part: GeminiPart): string[] {
|
||||
const chunks: string[] = [];
|
||||
const signature = part.thoughtSignature || undefined;
|
||||
const signature = decodeSignature(part.thoughtSignature);
|
||||
|
||||
// 1. Handle FunctionCall
|
||||
if (part.functionCall) {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/** Jitter factor: ±20% */
|
||||
const JITTER_FACTOR = 0.2;
|
||||
|
||||
/** Maximum retry delay in milliseconds (30 seconds) */
|
||||
const MAX_DELAY_MS = 30000;
|
||||
|
||||
/**
|
||||
* Apply jitter to a delay value to prevent thundering herd
|
||||
* Returns delay ± JITTER_FACTOR (e.g., 1000ms ± 20% = 800-1200ms)
|
||||
*
|
||||
* @param delayMs - Base delay in milliseconds
|
||||
* @returns Jittered delay (minimum 1ms)
|
||||
*/
|
||||
export function applyJitter(delayMs: number): number {
|
||||
if (delayMs <= 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const jitterRange = delayMs * JITTER_FACTOR;
|
||||
const jitter = (Math.random() * 2 - 1) * jitterRange;
|
||||
return Math.max(1, Math.round(delayMs + jitter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate retry delay with exponential backoff and jitter
|
||||
*
|
||||
* @param attempt - Current attempt number (0-indexed)
|
||||
* @param baseMs - Base delay for first retry (default 1000ms)
|
||||
* @returns Delay in milliseconds with jitter applied
|
||||
*/
|
||||
export function calculateRetryDelay(attempt: number, baseMs: number = 1000): number {
|
||||
// Exponential backoff: 1s, 2s, 4s, 8s, ...
|
||||
const exponentialDelay = baseMs * Math.pow(2, attempt);
|
||||
|
||||
// Cap at maximum delay
|
||||
const cappedDelay = Math.min(exponentialDelay, MAX_DELAY_MS);
|
||||
|
||||
// Apply jitter
|
||||
return applyJitter(cappedDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep for specified milliseconds
|
||||
*
|
||||
* @param ms - Milliseconds to sleep
|
||||
*/
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { isEmpty } from 'lodash-es';
|
||||
/**
|
||||
* Decode Base64-encoded signature if present
|
||||
* Gemini sends Base64, Claude expects raw string
|
||||
*
|
||||
* @param signature - The signature string (may be Base64 or raw)
|
||||
* @returns Decoded signature or undefined if input is empty
|
||||
*/
|
||||
export function decodeSignature(signature: string | undefined | null): string | undefined {
|
||||
if (isEmpty(signature)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = Buffer.from(signature!, 'base64').toString('utf-8');
|
||||
|
||||
// Validate: Check if it looks like valid UTF-8 text
|
||||
// If decoding produces replacement characters, it wasn't valid Base64
|
||||
if (decoded && !decoded.includes('\ufffd') && decoded.length > 0) {
|
||||
// Additional check: re-encode and compare to detect false positives
|
||||
const reEncoded = Buffer.from(decoded).toString('base64');
|
||||
if (reEncoded === signature) {
|
||||
return decoded;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Not valid Base64, return as-is
|
||||
}
|
||||
|
||||
// Return original if not Base64
|
||||
return signature!;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Stream Error Utilities
|
||||
* Classifies network errors and formats user-friendly messages
|
||||
*
|
||||
* Problem: Technical error messages confuse users
|
||||
* Solution: Classify errors and provide localized, actionable messages
|
||||
*/
|
||||
|
||||
export interface ClassifiedError {
|
||||
type: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify a stream error and return user-friendly message
|
||||
*
|
||||
* @param error - The error object
|
||||
* @returns Classified error with type and localized message
|
||||
*/
|
||||
export function classifyStreamError(error: Error): ClassifiedError {
|
||||
const msg = error.message.toLowerCase();
|
||||
|
||||
if (msg.includes('timeout') || msg.includes('timedout')) {
|
||||
return {
|
||||
type: 'timeout_error',
|
||||
message: 'Request timed out. Please check your network connection or try again later.',
|
||||
};
|
||||
}
|
||||
|
||||
if (msg.includes('econnrefused') || msg.includes('connect')) {
|
||||
return {
|
||||
type: 'connection_error',
|
||||
message: 'Unable to connect to server. Please check your network.',
|
||||
};
|
||||
}
|
||||
|
||||
if (msg.includes('enotfound') || msg.includes('network') || msg.includes('dns')) {
|
||||
return {
|
||||
type: 'network_error',
|
||||
message: 'Network connection failed. Please check your network settings.',
|
||||
};
|
||||
}
|
||||
|
||||
if (msg.includes('socket') || msg.includes('aborted') || msg.includes('reset')) {
|
||||
return {
|
||||
type: 'stream_error',
|
||||
message: 'Data transfer interrupted. Please try again later.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'unknown_error',
|
||||
message: `Unknown error occurred: ${error.message.substring(0, 100)}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format error as SSE event for Claude protocol
|
||||
*
|
||||
* @param errorType - Error type string
|
||||
* @param message - User-friendly message
|
||||
* @returns SSE-formatted error event string
|
||||
*/
|
||||
export function formatErrorForSSE(errorType: string, message: string): string {
|
||||
const errorEvent = {
|
||||
type: 'error',
|
||||
error: {
|
||||
type: errorType,
|
||||
message: message,
|
||||
code: 'stream_error',
|
||||
},
|
||||
};
|
||||
|
||||
return `event: error\ndata: ${JSON.stringify(errorEvent)}\n\n`;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Thinking Block Utilities
|
||||
* Handles validation and graceful degradation of thinking blocks
|
||||
*
|
||||
* Problem: Invalid signatures cause thinking blocks to be dropped, losing content
|
||||
* Solution: Validate and convert invalid blocks to text instead of dropping
|
||||
*/
|
||||
|
||||
/** Minimum length for a valid thought signature */
|
||||
const MIN_SIGNATURE_LENGTH = 10;
|
||||
|
||||
export interface ThinkingBlock {
|
||||
type: 'thinking';
|
||||
thinking: string;
|
||||
signature?: string;
|
||||
}
|
||||
|
||||
export interface TextBlock {
|
||||
type: 'text';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export type ContentBlock = ThinkingBlock | TextBlock | { type: string; [key: string]: any };
|
||||
|
||||
/**
|
||||
* Check if a thinking block has a valid signature
|
||||
*
|
||||
* Rules:
|
||||
* - Empty thinking + any signature = valid (trailing signature case)
|
||||
* - Non-empty thinking + signature >= MIN_SIGNATURE_LENGTH = valid
|
||||
* - Non-empty thinking + no/short signature = invalid
|
||||
* - Non-thinking blocks = always valid
|
||||
*
|
||||
* @param block - Content block to validate
|
||||
* @returns true if block is valid or not a thinking block
|
||||
*/
|
||||
export function hasValidSignature(block: ContentBlock): boolean {
|
||||
if (block.type !== 'thinking') {
|
||||
return true; // Non-thinking blocks are always valid
|
||||
}
|
||||
|
||||
const thinkingBlock = block as ThinkingBlock;
|
||||
|
||||
// Empty thinking + any signature = valid (trailing signature)
|
||||
if (!thinkingBlock.thinking || thinkingBlock.thinking.length === 0) {
|
||||
return (
|
||||
thinkingBlock.signature !== undefined &&
|
||||
thinkingBlock.signature !== null &&
|
||||
thinkingBlock.signature.length >= MIN_SIGNATURE_LENGTH
|
||||
);
|
||||
}
|
||||
|
||||
// Non-empty thinking needs a long enough signature
|
||||
return (
|
||||
thinkingBlock.signature !== undefined &&
|
||||
thinkingBlock.signature !== null &&
|
||||
thinkingBlock.signature.length >= MIN_SIGNATURE_LENGTH
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gracefully degrade an invalid thinking block
|
||||
*
|
||||
* - Valid blocks: return as-is
|
||||
* - Invalid with content: convert to text block
|
||||
* - Invalid without content: return null (should be dropped)
|
||||
*
|
||||
* @param block - Content block to process
|
||||
* @returns Processed block or null if should be dropped
|
||||
*/
|
||||
export function gracefulDegradeThinking(block: ContentBlock): ContentBlock | null {
|
||||
if (block.type !== 'thinking') {
|
||||
return block;
|
||||
}
|
||||
|
||||
const thinkingBlock = block as ThinkingBlock;
|
||||
|
||||
if (hasValidSignature(block)) {
|
||||
return block;
|
||||
}
|
||||
|
||||
// Invalid signature - check if we have content to preserve
|
||||
if (thinkingBlock.thinking && thinkingBlock.thinking.length > 0) {
|
||||
// Convert to text block to preserve content
|
||||
console.info(
|
||||
`[Thinking-Utils] Converting invalid thinking block to text. Content length: ${thinkingBlock.thinking.length}`,
|
||||
);
|
||||
return {
|
||||
type: 'text',
|
||||
text: thinkingBlock.thinking,
|
||||
};
|
||||
}
|
||||
|
||||
// Empty content + invalid signature = drop
|
||||
console.debug('[Thinking-Utils] Dropping empty thinking block with invalid signature');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an array of content blocks, applying graceful degradation
|
||||
*
|
||||
* @param blocks - Array of content blocks
|
||||
* @returns Processed array with invalid thinking blocks handled
|
||||
*/
|
||||
export function processThinkingBlocks(blocks: ContentBlock[]): ContentBlock[] {
|
||||
const result: ContentBlock[] = [];
|
||||
|
||||
for (const block of blocks) {
|
||||
const processed = gracefulDegradeThinking(block);
|
||||
if (processed !== null) {
|
||||
result.push(processed);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -7,6 +7,11 @@ import { transformClaudeRequestIn } from '../../../lib/antigravity/ClaudeRequest
|
||||
import { transformResponse } from '../../../lib/antigravity/ClaudeResponseMapper';
|
||||
import { StreamingState, PartProcessor } from '../../../lib/antigravity/ClaudeStreamingMapper';
|
||||
import { ClaudeRequest } from '../../../lib/antigravity/types';
|
||||
import { calculateRetryDelay, sleep } from '../../../lib/antigravity/retry-utils';
|
||||
import {
|
||||
classifyStreamError,
|
||||
formatErrorForSSE,
|
||||
} from '../../../lib/antigravity/stream-error-utils';
|
||||
import {
|
||||
OpenAIChatRequest,
|
||||
AnthropicChatRequest,
|
||||
@@ -45,6 +50,13 @@ export class ProxyService {
|
||||
const maxRetries = 3;
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
// Fix #6: Add jitter delay for retries (skip first attempt)
|
||||
if (i > 0) {
|
||||
const delay = calculateRetryDelay(i - 1);
|
||||
this.logger.log(`Retry attempt ${i + 1}/${maxRetries}, waiting ${delay}ms (jittered)`);
|
||||
await sleep(delay);
|
||||
}
|
||||
|
||||
const token = await this.tokenManager.getNextToken();
|
||||
if (!token) {
|
||||
throw new Error('No available accounts');
|
||||
@@ -172,7 +184,11 @@ export class ProxyService {
|
||||
let lastFinishReason: string | undefined;
|
||||
let lastUsageMetadata: any | undefined;
|
||||
|
||||
// Fix #1: Track if we received any valid data
|
||||
let receivedData = false;
|
||||
|
||||
upstreamStream.on('data', (chunk: Buffer) => {
|
||||
receivedData = true; // Mark that we got data
|
||||
buffer += decoder.decode(chunk, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
@@ -205,21 +221,40 @@ export class ProxyService {
|
||||
const chunks = processor.process(part as any);
|
||||
chunks.forEach((c) => subscriber.next(c));
|
||||
}
|
||||
|
||||
// Reset error state on successful parse
|
||||
state.resetErrorState();
|
||||
} catch (e) {
|
||||
// Fix #3: Use handleParseError for graceful degradation
|
||||
this.logger.error('Stream parse error', e);
|
||||
const errorChunks = state.handleParseError(dataStr);
|
||||
errorChunks.forEach((c) => subscriber.next(c));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
upstreamStream.on('end', () => {
|
||||
// Fix #1: Check for empty stream
|
||||
if (!receivedData) {
|
||||
this.logger.warn('Empty response stream detected');
|
||||
subscriber.error(new Error('Empty response stream'));
|
||||
return;
|
||||
}
|
||||
|
||||
const finishChunks = state.emitFinish(lastFinishReason, lastUsageMetadata);
|
||||
finishChunks.forEach((c) => subscriber.next(c));
|
||||
subscriber.complete();
|
||||
});
|
||||
|
||||
upstreamStream.on('error', (err: any) => {
|
||||
// Convert to clean Error to avoid circular reference issues (socket objects)
|
||||
const cleanError = err instanceof Error ? new Error(err.message) : new Error(String(err));
|
||||
// Fix #2: Classify error and send friendly message
|
||||
const cleanError = err instanceof Error ? err : new Error(String(err));
|
||||
const { type, message } = classifyStreamError(cleanError);
|
||||
|
||||
this.logger.error(`Stream error: ${type} - ${cleanError.message}`);
|
||||
|
||||
// Send SSE error event before closing
|
||||
subscriber.next(formatErrorForSSE(type, message));
|
||||
subscriber.error(cleanError);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { EventEmitter } from 'events';
|
||||
import { ProxyService } from '../../server/modules/proxy/proxy.service';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
// Mock dependencies
|
||||
const mockTokenManager = { getNextToken: vi.fn(), markAsRateLimited: vi.fn() };
|
||||
const mockGeminiClient = { streamGenerateInternal: vi.fn(), generateInternal: vi.fn() };
|
||||
|
||||
// Subclass to access private method
|
||||
class TestableProxyService extends ProxyService {
|
||||
constructor() {
|
||||
super(mockTokenManager as any, mockGeminiClient as any);
|
||||
}
|
||||
|
||||
public testProcessStream(stream: any, model: string = 'model'): Observable<string> {
|
||||
// Access private method using type assertion
|
||||
return (this as any).processAnthropicInternalStream(stream, model);
|
||||
}
|
||||
}
|
||||
|
||||
describe('ProxyService Empty Stream Retry Logic', () => {
|
||||
it('should emit error when stream ends without data', async () => {
|
||||
const service = new TestableProxyService();
|
||||
const stream = new EventEmitter();
|
||||
|
||||
const resultObservable = service.testProcessStream(stream);
|
||||
|
||||
let errorReceived: Error | undefined;
|
||||
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
resultObservable.subscribe({
|
||||
next: () => {},
|
||||
error: (err) => {
|
||||
errorReceived = err;
|
||||
resolve();
|
||||
},
|
||||
complete: () => resolve(),
|
||||
});
|
||||
});
|
||||
|
||||
// Simulate empty stream: straight to end
|
||||
setTimeout(() => stream.emit('end'), 10);
|
||||
|
||||
await promise;
|
||||
|
||||
expect(errorReceived).toBeDefined();
|
||||
expect(errorReceived?.message).toBe('Empty response stream');
|
||||
});
|
||||
|
||||
it('should NOT emit error when stream has data', async () => {
|
||||
const service = new TestableProxyService();
|
||||
const stream = new EventEmitter();
|
||||
|
||||
const resultObservable = service.testProcessStream(stream);
|
||||
|
||||
let errorReceived: Error | undefined;
|
||||
const receivedChunks: string[] = [];
|
||||
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
resultObservable.subscribe({
|
||||
next: (c) => receivedChunks.push(c),
|
||||
error: (err) => {
|
||||
errorReceived = err;
|
||||
resolve();
|
||||
},
|
||||
complete: () => resolve(),
|
||||
});
|
||||
});
|
||||
|
||||
// Simulate valid data stream
|
||||
setTimeout(() => {
|
||||
const validJson = JSON.stringify({
|
||||
candidates: [
|
||||
{
|
||||
content: { parts: [{ text: 'hello' }] },
|
||||
finishReason: 'STOP',
|
||||
},
|
||||
],
|
||||
});
|
||||
stream.emit('data', Buffer.from(`data: ${validJson}\n\n`));
|
||||
stream.emit('end');
|
||||
}, 10);
|
||||
|
||||
await promise;
|
||||
|
||||
expect(errorReceived).toBeUndefined();
|
||||
// It should produce chunks (though exact number depends on mapper logic, at least it shouldn't error)
|
||||
// Actually our mapper might produce "message_start", "content_block_start" etc.
|
||||
// We just care that it didn't error with "Empty response stream"
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { applyJitter, calculateRetryDelay } from '../../lib/antigravity/retry-utils';
|
||||
|
||||
describe('applyJitter', () => {
|
||||
it('should return values within ±20% range', () => {
|
||||
const baseDelay = 1000;
|
||||
const results: number[] = [];
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
results.push(applyJitter(baseDelay));
|
||||
}
|
||||
|
||||
// All values should be within 800-1200 range (±20%)
|
||||
results.forEach((r) => {
|
||||
expect(r).toBeGreaterThanOrEqual(800);
|
||||
expect(r).toBeLessThanOrEqual(1200);
|
||||
});
|
||||
});
|
||||
|
||||
it('should produce different values (randomness)', () => {
|
||||
const results = new Set<number>();
|
||||
for (let i = 0; i < 20; i++) {
|
||||
results.add(applyJitter(1000));
|
||||
}
|
||||
// 20 calls should produce multiple distinct values
|
||||
expect(results.size).toBeGreaterThan(5);
|
||||
});
|
||||
|
||||
it('should guarantee minimum value of 1 for small inputs', () => {
|
||||
expect(applyJitter(1)).toBeGreaterThanOrEqual(1);
|
||||
expect(applyJitter(0)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateRetryDelay', () => {
|
||||
it('should implement exponential backoff', () => {
|
||||
// First attempt (index 0): 1s ± 20% = 800-1200ms
|
||||
const delay0 = calculateRetryDelay(0);
|
||||
expect(delay0).toBeGreaterThanOrEqual(800);
|
||||
expect(delay0).toBeLessThanOrEqual(1200);
|
||||
|
||||
// Second attempt (index 1): 2s ± 20% = 1600-2400ms
|
||||
const delay1 = calculateRetryDelay(1);
|
||||
expect(delay1).toBeGreaterThanOrEqual(1600);
|
||||
expect(delay1).toBeLessThanOrEqual(2400);
|
||||
});
|
||||
|
||||
it('should have maximum delay limit', () => {
|
||||
// Even with high retry count, should not exceed 30s + 20% jitter = 36s
|
||||
const delay = calculateRetryDelay(10);
|
||||
expect(delay).toBeLessThanOrEqual(36000);
|
||||
});
|
||||
|
||||
it('should accept custom base delay', () => {
|
||||
const delay = calculateRetryDelay(0, 500);
|
||||
expect(delay).toBeGreaterThanOrEqual(400); // 500 - 20%
|
||||
expect(delay).toBeLessThanOrEqual(600); // 500 + 20%
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { decodeSignature } from '../../lib/antigravity/signature-utils';
|
||||
|
||||
describe('decodeSignature', () => {
|
||||
describe('Base64 decoding', () => {
|
||||
it('should correctly decode valid Base64 signature', () => {
|
||||
const original = 'valid-thinking-signature-12345';
|
||||
const base64 = Buffer.from(original).toString('base64');
|
||||
expect(decodeSignature(base64)).toBe(original);
|
||||
});
|
||||
|
||||
it('should decode signature with special characters', () => {
|
||||
const original = 'sig+with/special=chars==';
|
||||
const base64 = Buffer.from(original).toString('base64');
|
||||
expect(decodeSignature(base64)).toBe(original);
|
||||
});
|
||||
|
||||
it('should decode UTF-8 Chinese signature', () => {
|
||||
const original = '思考签名-测试';
|
||||
const base64 = Buffer.from(original).toString('base64');
|
||||
expect(decodeSignature(base64)).toBe(original);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Non-Base64 passthrough', () => {
|
||||
it('should return non-Base64 strings as-is', () => {
|
||||
expect(decodeSignature('plain-text-signature')).toBe('plain-text-signature');
|
||||
});
|
||||
|
||||
it('should handle strings with invalid Base64 characters', () => {
|
||||
expect(decodeSignature('not!valid@base64#')).toBe('not!valid@base64#');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Empty value handling', () => {
|
||||
it('should return undefined for null input', () => {
|
||||
expect(decodeSignature(null)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for undefined input', () => {
|
||||
expect(decodeSignature(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for empty string', () => {
|
||||
expect(decodeSignature('')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { classifyStreamError, formatErrorForSSE } from '../../lib/antigravity/stream-error-utils';
|
||||
|
||||
describe('classifyStreamError', () => {
|
||||
it('should identify timeout errors', () => {
|
||||
const error = new Error('timeout of 30000ms exceeded');
|
||||
const { type, message } = classifyStreamError(error);
|
||||
expect(type).toBe('timeout_error');
|
||||
expect(message).toContain('timed out');
|
||||
});
|
||||
|
||||
it('should identify connection errors', () => {
|
||||
const error = new Error('connect ECONNREFUSED 127.0.0.1:8080');
|
||||
const { type, message } = classifyStreamError(error);
|
||||
expect(type).toBe('connection_error');
|
||||
expect(message).toContain('connect');
|
||||
});
|
||||
|
||||
it('should identify network DNS errors', () => {
|
||||
const error = new Error('getaddrinfo ENOTFOUND googleapis.com');
|
||||
const { type, message } = classifyStreamError(error);
|
||||
expect(type).toBe('network_error');
|
||||
expect(message).toContain('Network');
|
||||
});
|
||||
|
||||
it('should identify socket errors', () => {
|
||||
const error = new Error('socket hang up');
|
||||
const { type, message } = classifyStreamError(error);
|
||||
expect(type).toBe('stream_error');
|
||||
expect(message).toContain('interrupted');
|
||||
});
|
||||
|
||||
it('should return generic message for unknown errors', () => {
|
||||
const error = new Error('some random error');
|
||||
const { type, message } = classifyStreamError(error);
|
||||
expect(type).toBe('unknown_error');
|
||||
expect(message).toBeTruthy();
|
||||
expect(message).toContain('some random error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatErrorForSSE', () => {
|
||||
it('should format as Claude SSE error event', () => {
|
||||
const result = formatErrorForSSE('network_error', 'Network connection failed');
|
||||
expect(result).toContain('event: error');
|
||||
expect(result).toContain('"type":"error"');
|
||||
expect(result).toContain('network_error');
|
||||
expect(result).toContain('Network connection failed');
|
||||
});
|
||||
|
||||
it('should end with newlines', () => {
|
||||
const result = formatErrorForSSE('test', 'test');
|
||||
expect(result.endsWith('\n\n')).toBe(true);
|
||||
});
|
||||
|
||||
it('should be valid SSE format', () => {
|
||||
const result = formatErrorForSSE('timeout_error', 'Request timed out');
|
||||
expect(result.startsWith('event: ')).toBe(true);
|
||||
expect(result).toContain('\ndata: ');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { StreamingState } from '../../lib/antigravity/ClaudeStreamingMapper';
|
||||
|
||||
describe('StreamingState', () => {
|
||||
let state: StreamingState;
|
||||
|
||||
beforeEach(() => {
|
||||
state = new StreamingState();
|
||||
});
|
||||
|
||||
describe('handleParseError', () => {
|
||||
it('should return empty array on first error', () => {
|
||||
const chunks = state.handleParseError('invalid json');
|
||||
expect(chunks).toEqual([]);
|
||||
});
|
||||
|
||||
it('should emit error event when error count exceeds 3', () => {
|
||||
// Simulate 4 parse errors
|
||||
state.handleParseError('error 1');
|
||||
state.handleParseError('error 2');
|
||||
state.handleParseError('error 3');
|
||||
const chunks = state.handleParseError('error 4');
|
||||
|
||||
expect(chunks.length).toBeGreaterThan(0);
|
||||
expect(chunks[0]).toContain('network_error');
|
||||
expect(chunks[0]).toContain('Unstable network');
|
||||
});
|
||||
|
||||
it('should safely close active block on error', () => {
|
||||
// Start a text block first
|
||||
state.startBlock('Text', { type: 'text', text: '' });
|
||||
|
||||
const chunks = state.handleParseError('error during block');
|
||||
|
||||
// Should contain content_block_stop event
|
||||
expect(chunks.some((c) => c.includes('content_block_stop'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetErrorState', () => {
|
||||
it('should reset error counter', () => {
|
||||
state.handleParseError('error 1');
|
||||
state.handleParseError('error 2');
|
||||
state.resetErrorState();
|
||||
|
||||
// After reset, should start counting from 0
|
||||
const chunks = state.handleParseError('error after reset');
|
||||
expect(chunks).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getErrorCount', () => {
|
||||
it('should return current error count', () => {
|
||||
expect(state.getErrorCount()).toBe(0);
|
||||
state.handleParseError('error 1');
|
||||
expect(state.getErrorCount()).toBe(1);
|
||||
state.handleParseError('error 2');
|
||||
expect(state.getErrorCount()).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
hasValidSignature,
|
||||
gracefulDegradeThinking,
|
||||
processThinkingBlocks,
|
||||
} from '../../lib/antigravity/thinking-utils';
|
||||
|
||||
describe('hasValidSignature', () => {
|
||||
it('should return true for empty thinking + valid signature', () => {
|
||||
const block = { type: 'thinking' as const, thinking: '', signature: 'valid-signature-long' };
|
||||
expect(hasValidSignature(block)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for content + long signature', () => {
|
||||
const block = {
|
||||
type: 'thinking' as const,
|
||||
thinking: 'some thought',
|
||||
signature: 'long-enough-signature',
|
||||
};
|
||||
expect(hasValidSignature(block)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for content + short signature', () => {
|
||||
const block = { type: 'thinking' as const, thinking: 'some thought', signature: 'short' };
|
||||
expect(hasValidSignature(block)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for content + no signature', () => {
|
||||
const block = { type: 'thinking' as const, thinking: 'some thought', signature: undefined };
|
||||
expect(hasValidSignature(block)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for non-thinking types', () => {
|
||||
const block = { type: 'text' as const, text: 'hello' };
|
||||
expect(hasValidSignature(block)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gracefulDegradeThinking', () => {
|
||||
it('should convert invalid signature thinking to text', () => {
|
||||
const block = { type: 'thinking' as const, thinking: 'important content', signature: 'bad' };
|
||||
const result = gracefulDegradeThinking(block);
|
||||
expect(result?.type).toBe('text');
|
||||
expect((result as any).text).toBe('important content');
|
||||
});
|
||||
|
||||
it('should keep valid thinking block unchanged', () => {
|
||||
const block = {
|
||||
type: 'thinking' as const,
|
||||
thinking: 'content',
|
||||
signature: 'valid-long-signature',
|
||||
};
|
||||
const result = gracefulDegradeThinking(block);
|
||||
expect(result?.type).toBe('thinking');
|
||||
});
|
||||
|
||||
it('should drop empty content + invalid signature block', () => {
|
||||
const block = { type: 'thinking' as const, thinking: '', signature: 'bad' };
|
||||
const result = gracefulDegradeThinking(block);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should keep empty content + valid signature trailing block', () => {
|
||||
const block = { type: 'thinking' as const, thinking: '', signature: 'any-valid-sig' };
|
||||
const result = gracefulDegradeThinking(block);
|
||||
expect(result?.type).toBe('thinking');
|
||||
});
|
||||
});
|
||||
|
||||
describe('processThinkingBlocks', () => {
|
||||
it('should filter out invalid empty thinking blocks', () => {
|
||||
const blocks = [
|
||||
{ type: 'text' as const, text: 'hello' },
|
||||
{ type: 'thinking' as const, thinking: '', signature: 'bad' },
|
||||
{ type: 'text' as const, text: 'world' },
|
||||
];
|
||||
const result = processThinkingBlocks(blocks);
|
||||
expect(result.length).toBe(2);
|
||||
expect(result[0].type).toBe('text');
|
||||
expect(result[1].type).toBe('text');
|
||||
});
|
||||
|
||||
it('should convert content with invalid signature to text', () => {
|
||||
const blocks = [{ type: 'thinking' as const, thinking: 'keep me', signature: 'x' }];
|
||||
const result = processThinkingBlocks(blocks);
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].type).toBe('text');
|
||||
expect((result[0] as any).text).toBe('keep me');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(process.cwd(), './src'),
|
||||
// Mock native modules that fail to load in test environment
|
||||
keytar: path.resolve(process.cwd(), './src/mocks/empty.ts'),
|
||||
'better-sqlite3': path.resolve(process.cwd(), './src/mocks/empty.ts'),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'happy-dom',
|
||||
include: ['src/tests/unit/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
import * as path from 'path';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react() as any],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
dir: './src/tests/unit',
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
setupFiles: './src/tests/unit/setup.ts',
|
||||
css: true,
|
||||
reporters: ['verbose'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
include: ['src/**/*'],
|
||||
exclude: [],
|
||||
},
|
||||
server: {
|
||||
deps: {
|
||||
inline: ['jsdom'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user