diff --git a/console/package-lock.json b/console/package-lock.json index 97fc665e4..fc06c01e1 100644 --- a/console/package-lock.json +++ b/console/package-lock.json @@ -37,12 +37,14 @@ "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", + "fast-check": "^4.6.0", "globals": "^16.0.0", "less": "^4.5.1", "prettier": "3.0.0", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", - "vite": "^6.3.5" + "vite": "^6.3.5", + "vitest": "^4.1.0" } }, "node_modules/@agentscope-ai/chat": { @@ -5798,6 +5800,13 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -6011,6 +6020,17 @@ "node": ">=6.9.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/d3": { "version": "7.4.3", "resolved": "https://registry.npmmirror.com/@types/d3/-/d3-7.4.3.tgz", @@ -6273,6 +6293,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -6781,24 +6808,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.4.tgz", @@ -6815,19 +6824,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", @@ -6841,23 +6837,6 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -6985,6 +6964,119 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.0", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -8751,6 +8843,16 @@ "node": ">=12.22" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -8893,6 +8995,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", @@ -9292,8 +9404,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", @@ -9482,6 +9593,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", @@ -9502,12 +9623,45 @@ "node": ">=0.8.x" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/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/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/fast-check": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.6.0.tgz", + "integrity": "sha512-h7H6Dm0Fy+H4ciQYFxFjXnXkzR2kr9Fb22c0UBpHnm59K2zpr2t13aPTHlltFiNT6zuxp6HMPAVVvgur4BLdpA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -9551,6 +9705,24 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -10200,6 +10372,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-2.1.0.tgz", @@ -11296,12 +11478,6 @@ "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", "license": "MIT" }, - "node_modules/mermaid/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "license": "MIT" - }, "node_modules/mermaid/node_modules/pkg-types": { "version": "1.3.1", "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", @@ -11353,15 +11529,6 @@ "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", "license": "MIT" }, - "node_modules/mermaid/node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/mermaid/node_modules/ts-dedent": { "version": "2.2.0", "resolved": "https://registry.npmmirror.com/ts-dedent/-/ts-dedent-2.2.0.tgz", @@ -12111,6 +12278,17 @@ "node": ">=0.10.0" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", @@ -12334,12 +12512,31 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pify": { "version": "4.0.1", "resolved": "https://registry.npmmirror.com/pify/-/pify-4.0.1.tgz", @@ -12395,6 +12592,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.0.0.tgz", + "integrity": "sha512-7rgWlxG2gAvFPIQfUreo1XYlNvrQ9VnQPFWdncPkdl3icucLK0InOxsaafbvxGTnI6Bk/Rxmslg0lQlRCuzOXw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/query-string": { "version": "7.1.3", "resolved": "https://registry.npmmirror.com/query-string/-/query-string-7.1.3.tgz", @@ -13118,6 +13332,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", @@ -13158,6 +13379,20 @@ "node": ">=6" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/strict-uri-encode": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", @@ -13304,6 +13539,49 @@ "license": "MIT", "peer": true }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -13693,24 +13971,6 @@ "@esbuild/win32-x64": "0.25.12" } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/vite/node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", @@ -13730,19 +13990,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/vite/node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", @@ -13827,21 +14074,86 @@ "node": ">=0.10.0" } }, - "node_modules/vite/node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "node_modules/vitest": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" }, "engines": { - "node": ">=12.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } } }, "node_modules/void-elements": { @@ -13956,6 +14268,23 @@ "node": ">=4.0" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/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/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/console/package.json b/console/package.json index 295a62a4a..f7fd4709b 100644 --- a/console/package.json +++ b/console/package.json @@ -45,12 +45,14 @@ "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", + "fast-check": "^4.6.0", "globals": "^16.0.0", "less": "^4.5.1", "prettier": "3.0.0", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", - "vite": "^6.3.5" + "vite": "^6.3.5", + "vitest": "^4.1.0" }, "repository": "https://github.com/agentscope-ai/agentscope-runtime.git" } diff --git a/console/src/pages/Chat/sessionApi/index.ts b/console/src/pages/Chat/sessionApi/index.ts index f449709e0..19cfbf34d 100644 --- a/console/src/pages/Chat/sessionApi/index.ts +++ b/console/src/pages/Chat/sessionApi/index.ts @@ -441,11 +441,14 @@ class SessionApi implements IAgentScopeRuntimeWebUISessionAPI { } async updateSession(session: Partial) { - session.messages = []; - const index = this.sessionList.findIndex((s) => s.id === session.id); + const { messages, ...metadataUpdate } = session; + const index = this.sessionList.findIndex((s) => s.id === metadataUpdate.id); if (index > -1) { - this.sessionList[index] = { ...this.sessionList[index], ...session }; + this.sessionList[index] = { + ...this.sessionList[index], + ...metadataUpdate, + }; // Timestamp session without realId yet — resolve in the background const existing = this.sessionList[index] as ExtendedSession; @@ -461,7 +464,7 @@ class SessionApi implements IAgentScopeRuntimeWebUISessionAPI { } } else { // Session not found locally — refresh and resolve via session_id - const tempId = session.id!; + const tempId = metadataUpdate.id!; await this.getSessionList().then(() => { const { list, realId } = resolveRealId(this.sessionList, tempId); this.sessionList = list; diff --git a/console/src/pages/Chat/sessionApi/sessionApi.test.ts b/console/src/pages/Chat/sessionApi/sessionApi.test.ts new file mode 100644 index 000000000..468ca6400 --- /dev/null +++ b/console/src/pages/Chat/sessionApi/sessionApi.test.ts @@ -0,0 +1,370 @@ +/** + * Bug Condition Exploration Test + * + * Property 1: Bug Condition - updateSession 清空已有消息 + * + * **Validates: Requirements 1.1, 1.2, 2.1, 2.2** + * + * Bug condition: when sessionList contains a session with id === session.id + * and that session has messages.length > 0, calling updateSession should + * preserve the messages. The bug is that updateSession sets session.messages = [] + * on line 444, which overwrites existing messages via the spread operator. + * + * This test is EXPECTED TO FAIL on unfixed code, confirming the bug exists. + */ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import fc from "fast-check"; + +// Mock the api module to prevent real HTTP calls +vi.mock("../../../api", () => ({ + default: { + listChats: vi.fn().mockResolvedValue([]), + getChat: vi.fn().mockResolvedValue({ messages: [] }), + deleteChat: vi.fn().mockResolvedValue(undefined), + }, + api: { + listChats: vi.fn().mockResolvedValue([]), + getChat: vi.fn().mockResolvedValue({ messages: [] }), + deleteChat: vi.fn().mockResolvedValue(undefined), + }, +})); + +// Mock window globals used by SessionApi +vi.stubGlobal("window", { + currentSessionId: undefined, + currentUserId: undefined, + currentChannel: undefined, +}); + +import sessionApi from "./index"; + +/** + * Helper: access the private sessionList via type assertion. + * This is necessary because SessionApi only exports a singleton instance + * and sessionList is private. + */ +function getSessionList( + api: typeof sessionApi, +): Array<{ + id: string; + name: string; + messages: unknown[]; + [k: string]: unknown; +}> { + return ( + api as unknown as { + sessionList: Array<{ + id: string; + name: string; + messages: unknown[]; + [k: string]: unknown; + }>; + } + ).sessionList; +} + +function setSessionList( + api: typeof sessionApi, + list: Array<{ + id: string; + name: string; + messages: unknown[]; + [k: string]: unknown; + }>, +): void { + (api as unknown as { sessionList: typeof list }).sessionList = list; +} + +/** + * Arbitrary: generate a non-empty array of fake messages. + * Messages have { id, role, cards } matching IAgentScopeRuntimeWebUIMessage. + */ +const arbMessage = fc.record({ + id: fc.string({ minLength: 1, maxLength: 20 }), + role: fc.constantFrom( + "user" as const, + "assistant" as const, + "system" as const, + ), + cards: fc.constant([]), +}); + +const arbNonEmptyMessages = fc.array(arbMessage, { + minLength: 1, + maxLength: 10, +}); + +/** + * Arbitrary: generate a session id that is NOT a local timestamp + * (i.e., contains non-digit characters) to avoid triggering the + * realId resolution path which calls getSessionList. + */ +const arbUUIDSessionId = fc.uuid(); + +describe("Bug Condition Exploration: updateSession clears existing messages", () => { + beforeEach(() => { + // Reset sessionList to empty before each test + setSessionList(sessionApi, []); + }); + + it("Property 1: updateSession should preserve messages when updating metadata only", async () => { + await fc.assert( + fc.asyncProperty( + arbUUIDSessionId, + fc.string({ minLength: 1, maxLength: 50 }), + arbNonEmptyMessages, + fc.string({ minLength: 1, maxLength: 50 }), + async (sessionId, originalName, messages, newName) => { + // Pre-condition: newName differs from originalName + fc.pre(newName !== originalName); + + // Setup: place a session with non-empty messages in sessionList + setSessionList(sessionApi, [ + { + id: sessionId, + name: originalName, + messages: [...messages], + meta: {}, + sessionId: sessionId, + userId: "default", + channel: "console", + }, + ]); + + // Capture messages before the call + const messagesBefore = [...getSessionList(sessionApi)[0].messages]; + + // Act: call updateSession with only metadata update (no messages field) + await sessionApi.updateSession({ id: sessionId, name: newName }); + + // Assert: messages in sessionList should be preserved + const sessionAfter = getSessionList(sessionApi).find( + (s) => s.id === sessionId, + ); + expect(sessionAfter).toBeDefined(); + expect(sessionAfter!.messages).toEqual(messagesBefore); + expect(sessionAfter!.messages.length).toBeGreaterThan(0); + }, + ), + { numRuns: 100 }, + ); + }); +}); + +/** + * Preservation Property Tests + * + * Property 2: Preservation - 元数据更新和 realId 解析行为不变 + * + * **Validates: Requirements 3.1, 3.2, 3.4, 3.5** + * + * These tests verify behaviors that must remain unchanged after the bugfix. + * They run on UNFIXED code and are EXPECTED TO PASS, establishing a baseline. + * + * Preservation scope: for all inputs where the bug condition does NOT hold + * (session not in sessionList, or session has empty messages), the behavior + * of updateSession should be identical before and after the fix. + */ + +import api from "../../../api"; + +describe("Preservation: updateSession metadata update and realId resolution", () => { + beforeEach(() => { + setSessionList(sessionApi, []); + // Reset onSessionIdResolved callback + sessionApi.onSessionIdResolved = null; + // Clear mock call history + vi.clearAllMocks(); + }); + + /** + * Observation 1: On unfixed code, updateSession({ id, name: "新名称" }) + * correctly updates the `name` field in sessionList. + * + * Test: For sessions with empty messages (not bug condition), + * metadata fields are correctly merged into sessionList. + */ + it("Preservation: metadata update correctly merges name for sessions with empty messages", async () => { + await fc.assert( + fc.asyncProperty( + arbUUIDSessionId, + fc.string({ minLength: 1, maxLength: 50 }), + fc.string({ minLength: 1, maxLength: 50 }), + async (sessionId, originalName, newName) => { + // Pre-condition: names differ + fc.pre(newName !== originalName); + + // Setup: session with EMPTY messages (not bug condition) + setSessionList(sessionApi, [ + { + id: sessionId, + name: originalName, + messages: [], + meta: {}, + sessionId: sessionId, + userId: "default", + channel: "console", + }, + ]); + + // Act + await sessionApi.updateSession({ id: sessionId, name: newName }); + + // Assert: name is updated + const sessionAfter = getSessionList(sessionApi).find( + (s) => s.id === sessionId, + ); + expect(sessionAfter).toBeDefined(); + expect(sessionAfter!.name).toBe(newName); + // messages remain empty (were already empty) + expect(sessionAfter!.messages).toEqual([]); + }, + ), + { numRuns: 100 }, + ); + }); + + /** + * Observation 2: On unfixed code, for isLocalTimestamp(id) sessions + * without realId, updateSession triggers getSessionList + resolveRealId. + * + * Test: When session id is a pure-digit timestamp and has no realId, + * updateSession triggers the resolution flow (calls listChats). + */ + it("Preservation: realId resolution is triggered for local timestamp sessions without realId", async () => { + // Arbitrary: generate a pure-digit timestamp id (isLocalTimestamp returns true) + const arbTimestampId = fc.stringMatching(/^\d{5,15}$/); + + await fc.assert( + fc.asyncProperty( + arbTimestampId, + fc.string({ minLength: 1, maxLength: 50 }), + async (timestampId, sessionName) => { + // Setup: session with timestamp id, empty messages, no realId + setSessionList(sessionApi, [ + { + id: timestampId, + name: sessionName, + messages: [], + meta: {}, + sessionId: timestampId, + userId: "default", + channel: "console", + }, + ]); + + // Track onSessionIdResolved calls + const resolvedCalls: Array<{ tempId: string; realId: string }> = []; + sessionApi.onSessionIdResolved = (tempId, realId) => { + resolvedCalls.push({ tempId, realId }); + }; + + // Clear mock to track new calls + vi.mocked(api.listChats).mockClear(); + vi.mocked(api.listChats).mockResolvedValue([]); + + // Act + await sessionApi.updateSession({ + id: timestampId, + name: sessionName, + }); + + // Allow microtasks to flush (the if-branch uses .then() without await) + await new Promise((r) => setTimeout(r, 50)); + + // Assert: listChats was called (getSessionList was triggered) + expect(api.listChats).toHaveBeenCalled(); + }, + ), + { numRuns: 50 }, + ); + }); + + /** + * Observation 3: On unfixed code, updateSession({ id: "不存在的ID" }) + * takes the else branch and refreshes sessionList. + * + * Test: When session id is not found in sessionList, updateSession + * calls getSessionList (listChats) to refresh. + */ + it("Preservation: fallback refresh when session not found in sessionList", async () => { + await fc.assert( + fc.asyncProperty( + arbUUIDSessionId, + arbUUIDSessionId, + fc.string({ minLength: 1, maxLength: 50 }), + async (existingId, updateId, updateName) => { + // Pre-condition: updateId must differ from existingId + fc.pre(updateId !== existingId); + + // Setup: sessionList has one session, but we update a different id + setSessionList(sessionApi, [ + { + id: existingId, + name: "existing", + messages: [], + meta: {}, + sessionId: existingId, + userId: "default", + channel: "console", + }, + ]); + + vi.mocked(api.listChats).mockClear(); + vi.mocked(api.listChats).mockResolvedValue([]); + + // Act: update a session that doesn't exist in sessionList + await sessionApi.updateSession({ id: updateId, name: updateName }); + + // Assert: listChats was called (else branch triggers getSessionList) + expect(api.listChats).toHaveBeenCalled(); + }, + ), + { numRuns: 50 }, + ); + }); + + /** + * Observation 4: On unfixed code, updateSession returns a shallow copy + * of sessionList (not the same reference). + * + * Test: The returned array is a new array (different reference) but + * contains the same session objects. + */ + it("Preservation: updateSession returns a shallow copy of sessionList", async () => { + await fc.assert( + fc.asyncProperty( + arbUUIDSessionId, + fc.string({ minLength: 1, maxLength: 50 }), + async (sessionId, sessionName) => { + // Setup: session with empty messages (not bug condition) + setSessionList(sessionApi, [ + { + id: sessionId, + name: sessionName, + messages: [], + meta: {}, + sessionId: sessionId, + userId: "default", + channel: "console", + }, + ]); + + // Act + const result = await sessionApi.updateSession({ + id: sessionId, + name: sessionName, + }); + + // Assert: result is an array + expect(Array.isArray(result)).toBe(true); + // Assert: result is NOT the same reference as internal sessionList + expect(result).not.toBe(getSessionList(sessionApi)); + // Assert: result has the same length + expect(result.length).toBe(getSessionList(sessionApi).length); + }, + ), + { numRuns: 50 }, + ); + }); +}); diff --git a/console/vitest.config.ts b/console/vitest.config.ts new file mode 100644 index 000000000..1178e18be --- /dev/null +++ b/console/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vitest/config"; +import path from "path"; + +export default defineConfig({ + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +});