feat/test-infra
This commit is contained in:
133
bun.lock
133
bun.lock
@@ -94,6 +94,8 @@
|
||||
"fetch-retry": "^6.0.0",
|
||||
"hono": "^4.9.9",
|
||||
"http-status-codes": "^2.3.0",
|
||||
"ink": "^6.3.1",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"ioredis": "^5.5.0",
|
||||
"ksuid": "^3.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
@@ -101,6 +103,7 @@
|
||||
"mime-detect": "^1.3.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"openai": "^4.85.2",
|
||||
"p-limit": "^7.2.0",
|
||||
"pg": "^8.13.1",
|
||||
"pino": "^9.6.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
@@ -276,6 +279,8 @@
|
||||
|
||||
"@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="],
|
||||
|
||||
"@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-mkOh+Wwawzuf5wa30bvc4nA+Qb6DIrGWgBhRR/Pw4T9nsgYait8izvXkNyU78D6Wcu3Z+KUdwCmLCxlWjEotYA=="],
|
||||
|
||||
"@amplitude/analytics-browser": ["@amplitude/analytics-browser@2.27.0", "", { "dependencies": { "@amplitude/analytics-core": "^2.28.0", "@amplitude/plugin-autocapture-browser": "^1.15.3", "@amplitude/plugin-network-capture-browser": "^1.6.9", "@amplitude/plugin-page-view-tracking-browser": "^2.5.3", "@amplitude/plugin-web-vitals-browser": "^0.1.0-beta.31", "tslib": "^2.4.1" } }, "sha512-1LBCLmnr7aUpLtOp64lpr8GzN3vPKM0fwiM/7tWJ9XU9/GKA+k3CUSjI8OdERKrw2yVywujoAVQo4anGZXYIDA=="],
|
||||
|
||||
"@amplitude/analytics-client-common": ["@amplitude/analytics-client-common@2.4.8", "", { "dependencies": { "@amplitude/analytics-connector": "^1.4.8", "@amplitude/analytics-core": "^2.28.0", "@amplitude/analytics-types": "^2.10.0", "tslib": "^2.4.1" } }, "sha512-cSm9Q+qcLy65kV2MnD7WlKrFMDOkj14dHM2YQn5njUrSXQljyjtYCss+HVzgIlWPSDgA5v6dDeHwxzZl3VSggw=="],
|
||||
@@ -1440,9 +1445,11 @@
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"ansi-escapes": ["ansi-escapes@7.1.1", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
@@ -1472,6 +1479,8 @@
|
||||
|
||||
"attr-accept": ["attr-accept@2.2.5", "", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="],
|
||||
|
||||
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
|
||||
|
||||
"autumn-js": ["autumn-js@0.1.40", "", { "dependencies": { "query-string": "^9.2.2", "rou3": "^0.6.1", "swr": "^2.3.3", "zod": "^4.0.0" }, "peerDependencies": { "better-auth": "^1.3.17", "better-call": "^1.0.12", "convex": "^1.25.4" }, "optionalPeers": ["better-auth", "better-call"] }, "sha512-nAmyFJLOQqKosb8MHv09rB2pma8LyOHWsuYtrjXND+2LM51vToco1mweLIYIs/aX33iLAVUxfpXEEt8P3UYoxw=="],
|
||||
|
||||
"axios": ["axios@1.12.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw=="],
|
||||
@@ -1584,10 +1593,14 @@
|
||||
|
||||
"clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="],
|
||||
|
||||
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
|
||||
|
||||
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
|
||||
|
||||
"cli-spinners": ["cli-spinners@3.3.0", "", {}, "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ=="],
|
||||
|
||||
"cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
|
||||
|
||||
"cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
|
||||
|
||||
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||
@@ -1606,6 +1619,8 @@
|
||||
|
||||
"cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
|
||||
|
||||
"code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
@@ -1632,6 +1647,8 @@
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
|
||||
|
||||
"convex": ["convex@1.28.0", "", { "dependencies": { "esbuild": "0.25.4", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-40FgeJ/LxP9TxnkDDztU/A5gcGTdq1klcTT5mM0Ak+kSlQiDktMpjNX1TfkWLxXaE3lI4qvawKH95v2RiYgFxA=="],
|
||||
|
||||
"cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
|
||||
@@ -1784,7 +1801,7 @@
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.237", "", {}, "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
@@ -1798,6 +1815,8 @@
|
||||
|
||||
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
|
||||
|
||||
"error-causes": ["error-causes@3.0.2", "", {}, "sha512-i0B8zq1dHL6mM85FGoxaJnVtx6LD5nL2v0hlpGdntg5FOSyzQ46c9lmz5qx0xRS2+PWHGOHcYxGIBC5Le2dRMw=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
@@ -2052,10 +2071,16 @@
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||
|
||||
"ink": ["ink@6.3.1", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.0", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.32.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-3wGwITGrzL6rkWsi2gEKzgwdafGn4ZYd3u4oRp+sOPvfoxEHlnoB5Vnk9Uy5dMRUhDOqF3hqr4rLQ4lEzBc2sQ=="],
|
||||
|
||||
"ink-spinner": ["ink-spinner@5.0.0", "", { "dependencies": { "cli-spinners": "^2.7.0" }, "peerDependencies": { "ink": ">=4.0.0", "react": ">=18.0.0" } }, "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA=="],
|
||||
|
||||
"input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="],
|
||||
|
||||
"inquirer": ["inquirer@12.10.0", "", { "dependencies": { "@inquirer/ansi": "^1.0.1", "@inquirer/core": "^10.3.0", "@inquirer/prompts": "^7.9.0", "@inquirer/type": "^3.0.9", "mute-stream": "^2.0.0", "run-async": "^4.0.5", "rxjs": "^7.8.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-K/epfEnDBZj2Q3NMDcgXWZye3nhSPeoJnOh8lcKWrldw54UEZfS4EmAMsAsmVbl7qKi+vjAsy39Sz4fbgRMewg=="],
|
||||
@@ -2078,12 +2103,14 @@
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-in-browser": ["is-in-browser@1.1.3", "", {}, "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g=="],
|
||||
|
||||
"is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="],
|
||||
|
||||
"is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
|
||||
|
||||
"is-ip": ["is-ip@5.0.1", "", { "dependencies": { "ip-regex": "^5.0.0", "super-regex": "^0.2.0" } }, "sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw=="],
|
||||
@@ -2394,6 +2421,8 @@
|
||||
|
||||
"pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="],
|
||||
|
||||
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
@@ -2546,6 +2575,8 @@
|
||||
|
||||
"react-promise-suspense": ["react-promise-suspense@0.3.4", "", { "dependencies": { "fast-deep-equal": "^2.0.1" } }, "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ=="],
|
||||
|
||||
"react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="],
|
||||
|
||||
"react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||
@@ -2658,7 +2689,7 @@
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
|
||||
|
||||
@@ -2668,6 +2699,8 @@
|
||||
|
||||
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||
|
||||
"slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
|
||||
|
||||
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
|
||||
|
||||
"socket.io": ["socket.io@4.8.1", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg=="],
|
||||
@@ -2698,6 +2731,8 @@
|
||||
|
||||
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
|
||||
|
||||
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
|
||||
|
||||
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||
|
||||
"standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="],
|
||||
@@ -2812,7 +2847,7 @@
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||
|
||||
@@ -2892,13 +2927,15 @@
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
|
||||
|
||||
"winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"workerpool": ["workerpool@9.3.4", "", {}, "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
||||
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||
|
||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
@@ -2930,6 +2967,8 @@
|
||||
|
||||
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
|
||||
|
||||
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
|
||||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"zod-openapi": ["zod-openapi@5.4.3", "", { "peerDependencies": { "zod": "^3.25.74 || ^4.0.0" } }, "sha512-6kJ/gJdvHZtuxjYHoMtkl2PixCwRuZ/s79dVkEr7arHvZGXfx7Cvh53X3HfJ5h9FzGelXOXlnyjwfX0sKEPByw=="],
|
||||
@@ -3028,6 +3067,10 @@
|
||||
|
||||
"@hyperdx/node-opentelemetry/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
|
||||
|
||||
"@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
||||
|
||||
"@inquirer/external-editor/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
|
||||
|
||||
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
@@ -3426,6 +3469,10 @@
|
||||
|
||||
"bun-types/@types/node": ["@types/node@24.8.1", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q=="],
|
||||
|
||||
"cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
|
||||
|
||||
"cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
@@ -3462,12 +3509,20 @@
|
||||
|
||||
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||
|
||||
"html-minifier-terser/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"ink/cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
|
||||
|
||||
"ink/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"ink-spinner/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"mocha/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
|
||||
@@ -3508,6 +3563,8 @@
|
||||
|
||||
"react-promise-suspense/fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
|
||||
|
||||
"react-reconciler/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||
|
||||
"react-router/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||
|
||||
"recaseai/@types/node": ["@types/node@22.18.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Gd33J2XIrXurb+eT2ktze3rJAfAp9ZNjlBdh4SVgyrKEOADwCbdUDaK7QgJno8Ue4kcajscsKqu6n8OBG3hhCQ=="],
|
||||
@@ -3520,12 +3577,16 @@
|
||||
|
||||
"renderkid/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||
|
||||
"send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||
|
||||
"serialize-error/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
|
||||
|
||||
"socket.io/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||
|
||||
"socket.io-adapter/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||
@@ -3534,6 +3595,12 @@
|
||||
|
||||
"socket.io-parser/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||
|
||||
"stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"string-width-cjs/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
@@ -3552,11 +3619,13 @@
|
||||
|
||||
"type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"winston-transport/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
@@ -3754,9 +3823,13 @@
|
||||
|
||||
"@hyperdx/node-opentelemetry/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
"@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
"@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.202.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fTBjMqKCfotFWfLzaKyhjLvyEyq5vDKTTFfBmx21btv3gvy8Lq6N5Dh2OzqeuN4DjtpSvNT1uNVfg08eD2Rfxw=="],
|
||||
|
||||
@@ -3918,10 +3991,20 @@
|
||||
|
||||
"bun-types/@types/node/undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
|
||||
|
||||
"cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
|
||||
|
||||
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"cloudflare/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"concurrently/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"concurrently/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"convex/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
|
||||
@@ -3978,6 +4061,8 @@
|
||||
|
||||
"engine.io/@types/node/undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
|
||||
|
||||
"eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"eslint/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
@@ -3988,6 +4073,8 @@
|
||||
|
||||
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"ink/cli-cursor/restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
|
||||
|
||||
"mocha/log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"mocha/log-symbols/is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
|
||||
@@ -4050,9 +4137,15 @@
|
||||
|
||||
"type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
"yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
@@ -4112,12 +4205,20 @@
|
||||
|
||||
"@hyperdx/node-opentelemetry/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node/@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg=="],
|
||||
|
||||
"@hyperdx/node-opentelemetry/ora/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"@hyperdx/node-opentelemetry/ora/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"@hyperdx/node-opentelemetry/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
|
||||
|
||||
"@hyperdx/node-opentelemetry/ora/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"@inquirer/core/wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
|
||||
|
||||
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
|
||||
@@ -4132,6 +4233,10 @@
|
||||
|
||||
"css-select/domutils/dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="],
|
||||
|
||||
"ink/cli-cursor/restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"mocha/log-symbols/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"mocha/log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"nodemon/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
@@ -4142,12 +4247,8 @@
|
||||
|
||||
"react-email/ora/log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
|
||||
|
||||
"react-email/ora/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"recaseai/ora/log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
|
||||
|
||||
"recaseai/ora/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"renderkid/htmlparser2/domutils/dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="],
|
||||
|
||||
"tsc-alias/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
@@ -4160,8 +4261,6 @@
|
||||
|
||||
"@hyperdx/node-opentelemetry/ora/cli-cursor/restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"@hyperdx/node-opentelemetry/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-mysql/@types/mysql/@types/node/undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-pg/@types/pg/@types/node/undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
|
||||
|
||||
42
server/checkFeatures.ts
Normal file
42
server/checkFeatures.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import { initDrizzle } from "@/db/initDrizzle.js";
|
||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { StripeAccountService } from "@/internal/stripe/StripeAccountService.js";
|
||||
|
||||
const orgSlug = process.env.TESTS_ORG || "test-debug|org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt";
|
||||
|
||||
async function main() {
|
||||
const { db, client } = initDrizzle();
|
||||
|
||||
const org = await OrgService.getBySlug({ db, slug: orgSlug });
|
||||
console.log("\n=== ORG ===");
|
||||
console.log("ID:", org.id);
|
||||
console.log("Slug:", org.slug);
|
||||
console.log("Name:", org.name);
|
||||
console.log("\n=== STRIPE CONFIG ===");
|
||||
console.log("test_stripe_connect:", org.test_stripe_connect);
|
||||
console.log("live_stripe_connect:", org.live_stripe_connect);
|
||||
|
||||
const features = await FeatureService.list({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env: AppEnv.Sandbox,
|
||||
});
|
||||
|
||||
console.log("\n=== FEATURES ===");
|
||||
console.log("Total features:", features.length);
|
||||
for (const feature of features) {
|
||||
console.log(`- ${feature.id} (${feature.type})`, feature.usage_type ? `usage_type: ${feature.usage_type}` : '');
|
||||
if (feature.id === 'messages') {
|
||||
console.log("\n Full messages feature:", JSON.stringify(feature, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
await client.end();
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -13,7 +13,13 @@
|
||||
"cron": "bun src/cron.ts",
|
||||
"check": "bun src/check.ts",
|
||||
"build": "bun build ./src/index.ts ./src/workers.ts ./src/cron.ts --outdir dist --target bun",
|
||||
"build:check": "tsc -b tsconfig.build.json"
|
||||
"build:check": "tsc -b tsconfig.build.json",
|
||||
"t": "bun tests/testRunner/runParallelGroupsV3.ts",
|
||||
"parallel-tests": "bun tests/testRunner/runParallelGroupsV3.ts",
|
||||
"parallel-tests:v1": "bun tests/testRunner/runParallelGroups.ts",
|
||||
"parallel-tests:verbose": "bun tests/testRunner/runParallelGroups.ts --verbose",
|
||||
"parallel-tests:debug": "bun tests/testRunner/runParallelGroups.ts --debug",
|
||||
"clear-master": "bun tests/clearMasterOrg.ts"
|
||||
},
|
||||
"mocha": {
|
||||
"node-option": [
|
||||
@@ -78,6 +84,8 @@
|
||||
"fetch-retry": "^6.0.0",
|
||||
"hono": "^4.9.9",
|
||||
"http-status-codes": "^2.3.0",
|
||||
"ink": "^6.3.1",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"ioredis": "^5.5.0",
|
||||
"ksuid": "^3.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
@@ -85,6 +93,7 @@
|
||||
"mime-detect": "^1.3.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"openai": "^4.85.2",
|
||||
"p-limit": "^7.2.0",
|
||||
"pg": "^8.13.1",
|
||||
"pino": "^9.6.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
|
||||
@@ -22,11 +22,11 @@ BUN_SETUP="$BUN_CMD tests/setupMain.ts"
|
||||
|
||||
# Test runner functions (using new TypeScript runner)
|
||||
BUN_PARALLEL() {
|
||||
cd "$PROJECT_ROOT" && $BUN_CMD scripts/testScripts/runTests.ts "$@"
|
||||
cd "$PROJECT_ROOT" && $BUN_CMD server/tests/testRunner/runTests.ts "$@"
|
||||
}
|
||||
|
||||
BUN_PARALLEL_COMPACT() {
|
||||
cd "$PROJECT_ROOT" && $BUN_CMD scripts/testScripts/runTests.ts "$@" --compact
|
||||
cd "$PROJECT_ROOT" && $BUN_CMD server/tests/testRunner/runTests.ts "$@" --compact
|
||||
}
|
||||
|
||||
# Mocha command (for tests not yet migrated)
|
||||
|
||||
24
server/shell/parallel.sh
Executable file
24
server/shell/parallel.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Parallel Test Runner
|
||||
# Runs all test groups in parallel, each with its own dedicated org
|
||||
|
||||
# Source shared configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/config.sh"
|
||||
|
||||
# Check for required environment variables
|
||||
if [ -z "$TEST_ORG_SECRET_KEY" ]; then
|
||||
echo "Error: TEST_ORG_SECRET_KEY environment variable is required"
|
||||
echo ""
|
||||
echo "This should be the secret key of your platform organization"
|
||||
echo "that has access to create/delete test organizations."
|
||||
echo ""
|
||||
echo "Add it to your server/.env file:"
|
||||
echo " TEST_ORG_SECRET_KEY=am_sk_test_..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run parallel test groups
|
||||
echo "Starting parallel test runner..."
|
||||
cd "$PROJECT_ROOT" && $BUN_CMD server/tests/testRunner/runParallelGroups.ts
|
||||
3
server/src/external/autumn/autumnCli.ts
vendored
3
server/src/external/autumn/autumnCli.ts
vendored
@@ -24,6 +24,7 @@ import type {
|
||||
TrackParams,
|
||||
UsageParams,
|
||||
} from "autumn-js";
|
||||
import { defaultApiVersion } from "tests/constants.js";
|
||||
|
||||
export default class AutumnError extends Error {
|
||||
message: string;
|
||||
@@ -70,7 +71,7 @@ export class AutumnInt {
|
||||
};
|
||||
|
||||
if (version) {
|
||||
this.headers["x-api-version"] = version.toString();
|
||||
this.headers["x-api-version"] = version.toString() || defaultApiVersion;
|
||||
}
|
||||
|
||||
if (orgConfig) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
type CreateCustomerParams,
|
||||
CusExpand,
|
||||
CusProductStatus,
|
||||
type CustomerData,
|
||||
type Entity,
|
||||
type EntityData,
|
||||
type FullCustomer,
|
||||
@@ -37,7 +37,7 @@ export const getOrCreateCustomer = async ({
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
customerId: string | null;
|
||||
customerData?: CustomerData;
|
||||
customerData?: CreateCustomerParams;
|
||||
inStatuses?: CusProductStatus[];
|
||||
skipGet?: boolean;
|
||||
withEntities?: boolean;
|
||||
@@ -92,7 +92,9 @@ export const getOrCreateCustomer = async ({
|
||||
fingerprint: customerData?.fingerprint,
|
||||
metadata: customerData?.metadata || {},
|
||||
stripe_id: customerData?.stripe_id,
|
||||
default_product_id: customerData?.default_product_id,
|
||||
},
|
||||
createDefaultProducts: customerData?.disable_default !== true,
|
||||
})) as FullCustomer;
|
||||
|
||||
customer = await CusService.getFull({
|
||||
|
||||
95
server/src/internal/orgs/orgUtils/deleteOrgUtils.ts
Normal file
95
server/src/internal/orgs/orgUtils/deleteOrgUtils.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { AppEnv, type Organization } from "@autumn/shared";
|
||||
import type { Logger } from "@/external/logtail/logtailUtils.js";
|
||||
import {
|
||||
deauthorizeAccount,
|
||||
deleteConnectedAccount,
|
||||
} from "@/external/connect/connectUtils.js";
|
||||
import { deleteSvixApp } from "@/external/svix/svixHelpers.js";
|
||||
import { deleteStripeWebhook } from "../orgUtils.js";
|
||||
|
||||
export const deleteSvixWebhooks = async ({
|
||||
org,
|
||||
logger,
|
||||
}: {
|
||||
org: Organization;
|
||||
logger: Logger;
|
||||
}) => {
|
||||
const batch = [];
|
||||
if (org.svix_config?.sandbox_app_id) {
|
||||
batch.push(
|
||||
deleteSvixApp({
|
||||
appId: org.svix_config.sandbox_app_id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (org.svix_config?.live_app_id) {
|
||||
batch.push(
|
||||
deleteSvixApp({
|
||||
appId: org.svix_config.live_app_id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(batch);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to delete svix webhooks for ${org.id}, ${org.slug}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteStripeWebhooks = async ({
|
||||
org,
|
||||
logger,
|
||||
}: {
|
||||
org: Organization;
|
||||
logger: Logger;
|
||||
}) => {
|
||||
try {
|
||||
await deleteStripeWebhook({
|
||||
org: org,
|
||||
env: AppEnv.Sandbox,
|
||||
});
|
||||
|
||||
await deleteStripeWebhook({
|
||||
org: org,
|
||||
env: AppEnv.Live,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`Failed to delete stripe webhooks for ${org.id}, ${org.slug}. ${error.message})`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteStripeAccounts = async ({
|
||||
org,
|
||||
logger,
|
||||
}: {
|
||||
org: Organization;
|
||||
logger: Logger;
|
||||
}) => {
|
||||
if (org.test_stripe_connect?.account_id) {
|
||||
await deauthorizeAccount({
|
||||
accountId: org.test_stripe_connect.account_id,
|
||||
env: AppEnv.Sandbox,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
if (org.live_stripe_connect?.account_id) {
|
||||
await deauthorizeAccount({
|
||||
accountId: org.live_stripe_connect.account_id,
|
||||
env: AppEnv.Live,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
if (org.test_stripe_connect?.default_account_id) {
|
||||
await deleteConnectedAccount({
|
||||
accountId: org.test_stripe_connect.default_account_id,
|
||||
env: AppEnv.Sandbox,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -154,6 +154,7 @@ export const handleCreatePlatformOrg = createRoute({
|
||||
return c.json({
|
||||
test_secret_key,
|
||||
live_secret_key,
|
||||
org_slug: org.slug,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
AppEnv,
|
||||
customers,
|
||||
ErrCode,
|
||||
RecaseError,
|
||||
organizations,
|
||||
member,
|
||||
} from "@autumn/shared";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import { z } from "zod/v4";
|
||||
import type { Context } from "hono";
|
||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import {
|
||||
deleteStripeAccounts,
|
||||
deleteSvixWebhooks,
|
||||
deleteStripeWebhooks,
|
||||
} from "@/internal/orgs/orgUtils/deleteOrgUtils.js";
|
||||
|
||||
const deleteOrgSchema = z.object({
|
||||
slug: z.string().min(1, "Organization slug is required"),
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /organizations
|
||||
* Deletes a platform organization by slug (for test cleanup)
|
||||
*/
|
||||
export const handleDeletePlatformOrg = [
|
||||
zValidator("json", deleteOrgSchema),
|
||||
async (c: Context<HonoEnv>) => {
|
||||
const ctx = c.get("ctx");
|
||||
const { db, logger, org: masterOrg } = ctx;
|
||||
|
||||
const { slug } = c.req.valid("json");
|
||||
|
||||
// Platform API creates orgs with format: {slug}|{masterOrgId}
|
||||
// So we need to find the org with this pattern
|
||||
const fullSlug = `${slug}|${masterOrg.id}`;
|
||||
|
||||
const org = await OrgService.getBySlug({ db, slug: fullSlug });
|
||||
if (!org) {
|
||||
throw new RecaseError({
|
||||
message: `Organization with slug "${slug}" not found`,
|
||||
code: ErrCode.NotFound,
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
|
||||
// Check if any live customers exist
|
||||
const hasCustomers = await db.query.customers.findFirst({
|
||||
where: and(eq(customers.org_id, org.id), eq(customers.env, AppEnv.Live)),
|
||||
});
|
||||
|
||||
if (hasCustomers) {
|
||||
throw new RecaseError({
|
||||
message: "Cannot delete org with production mode customers",
|
||||
code: ErrCode.OrgHasCustomers,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// Delete svix webhooks
|
||||
logger.info("1. Deleting svix webhooks");
|
||||
await deleteSvixWebhooks({ org, logger });
|
||||
|
||||
// Delete stripe webhooks
|
||||
logger.info("2. Deleting stripe webhooks");
|
||||
await deleteStripeWebhooks({ org, logger });
|
||||
|
||||
// Delete stripe accounts
|
||||
logger.info("3. Deleting stripe accounts");
|
||||
await deleteStripeAccounts({ org, logger });
|
||||
|
||||
// Delete all sandbox customers
|
||||
logger.info("4. Deleting sandbox customers");
|
||||
await db
|
||||
.delete(customers)
|
||||
.where(
|
||||
and(eq(customers.org_id, org.id), eq(customers.env, AppEnv.Sandbox)),
|
||||
);
|
||||
|
||||
// Delete memberships
|
||||
logger.info("5. Deleting org memberships");
|
||||
await db.delete(member).where(eq(member.organizationId, org.id));
|
||||
|
||||
// Delete the organization itself
|
||||
logger.info("6. Deleting organization");
|
||||
await db.delete(organizations).where(eq(organizations.id, org.id));
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
message: `Organization "${slug}" deleted successfully`,
|
||||
});
|
||||
},
|
||||
];
|
||||
@@ -2,6 +2,7 @@ import { Autumn } from "autumn-js";
|
||||
import { Hono } from "hono";
|
||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||
import { handleCreatePlatformOrg } from "./handlers/handleCreatePlatformOrg.js";
|
||||
import { handleDeletePlatformOrg } from "./handlers/handleDeletePlatformOrg.js";
|
||||
import { handleGetPlatformOAuth } from "./handlers/handleGetPlatformOAuth.js";
|
||||
import { handleListPlatformOrgs } from "./handlers/handleListPlatformOrgs.js";
|
||||
import { listPlatformUsers } from "./handlers/handleListPlatformUsers.js";
|
||||
@@ -82,4 +83,11 @@ platformBetaRouter.post(
|
||||
platformBetaRouter.get("/users", ...listPlatformUsers);
|
||||
|
||||
platformBetaRouter.get("/organizations", ...handleListPlatformOrgs);
|
||||
|
||||
/**
|
||||
* DELETE /organizations
|
||||
* Deletes a platform organization by slug
|
||||
*/
|
||||
platformBetaRouter.delete("/organizations", ...handleDeletePlatformOrg);
|
||||
|
||||
export { platformBetaRouter };
|
||||
|
||||
@@ -118,11 +118,13 @@ export class ProductService {
|
||||
orgId,
|
||||
env,
|
||||
group,
|
||||
inIds,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
group?: string;
|
||||
inIds?: string[];
|
||||
}) {
|
||||
const prods = (await db.query.products.findMany({
|
||||
where: and(
|
||||
@@ -131,6 +133,7 @@ export class ProductService {
|
||||
eq(products.is_default, true),
|
||||
ne(products.archived, true),
|
||||
group ? eq(products.group, group) : undefined,
|
||||
inIds ? inArray(products.id, inIds) : undefined,
|
||||
),
|
||||
with: {
|
||||
entitlements: {
|
||||
|
||||
@@ -90,7 +90,7 @@ export const constructProduct = ({
|
||||
is_add_on: productData.is_add_on,
|
||||
is_default: productData.is_default,
|
||||
version: productData.version || 1,
|
||||
group: productData.group,
|
||||
group: productData.group || "",
|
||||
|
||||
env,
|
||||
internal_id: generateId("prod"),
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import type {
|
||||
type CreateFreeTrial,
|
||||
Entitlement,
|
||||
Feature,
|
||||
Price,
|
||||
ProductV2,
|
||||
} from "@autumn/shared";
|
||||
import { itemToPriceAndEnt } from "@/internal/products/product-items/productItemUtils/itemToPriceAndEnt.js";
|
||||
|
||||
/**
|
||||
* V1 product format with entitlements as a record (used in tests)
|
||||
* Different from FullProduct which has entitlements as an array
|
||||
*/
|
||||
export type ProductV1 = {
|
||||
id: string;
|
||||
name: string;
|
||||
is_default: boolean;
|
||||
is_add_on: boolean;
|
||||
entitlements: Record<string, Entitlement>;
|
||||
prices: Price[];
|
||||
free_trial: CreateFreeTrial | null;
|
||||
group: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts ProductV2 (items-based) to V1 format (entitlements + prices)
|
||||
*
|
||||
* Uses production conversion utilities to ensure test expectations match actual behavior.
|
||||
*
|
||||
* @param productV2 - V2 product with items array
|
||||
* @param orgId - Organization ID
|
||||
* @param features - Available features in the org
|
||||
* @returns V1-format product object with entitlements and prices
|
||||
*/
|
||||
export const convertProductV2ToV1 = ({
|
||||
productV2,
|
||||
orgId,
|
||||
features,
|
||||
}: {
|
||||
productV2: ProductV2;
|
||||
orgId: string;
|
||||
features: Feature[];
|
||||
}): ProductV1 => {
|
||||
const entitlements: Entitlement[] = [];
|
||||
const prices: Price[] = [];
|
||||
|
||||
for (const item of productV2.items) {
|
||||
const feature = features.find((f) => f.id === item.feature_id);
|
||||
|
||||
// Use production conversion utilities
|
||||
const { newEnt, newPrice, sameEnt, samePrice } = itemToPriceAndEnt({
|
||||
item,
|
||||
orgId,
|
||||
internalProductId: "test",
|
||||
feature,
|
||||
isCustom: false,
|
||||
features,
|
||||
});
|
||||
|
||||
const ent = newEnt || sameEnt;
|
||||
const price = newPrice || samePrice;
|
||||
|
||||
if (ent) {
|
||||
entitlements.push(ent);
|
||||
}
|
||||
if (price) {
|
||||
prices.push(price);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert entitlements array to record keyed by feature_id
|
||||
const entitlementsRecord: Record<string, Entitlement> = {};
|
||||
for (const ent of entitlements) {
|
||||
if (ent.feature_id) {
|
||||
entitlementsRecord[ent.feature_id] = ent;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: productV2.id,
|
||||
name: productV2.name,
|
||||
is_default: productV2.is_default,
|
||||
is_add_on: productV2.is_add_on,
|
||||
entitlements: entitlementsRecord,
|
||||
prices,
|
||||
free_trial: productV2.free_trial,
|
||||
group: productV2.group,
|
||||
};
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CreateFreeTrialSchema,
|
||||
type CreateReward,
|
||||
FeatureUsageType,
|
||||
type FreeTrial,
|
||||
FreeTrialDuration,
|
||||
type ProductItem,
|
||||
type ProductV2,
|
||||
@@ -143,6 +144,16 @@ export const constructProduct = ({
|
||||
id ||
|
||||
(isAnnual ? `${type}-annual` : interval ? `${type}-${interval}` : type);
|
||||
|
||||
let free_trial: CreateFreeTrial | null = null;
|
||||
if (freeTrial) {
|
||||
free_trial = freeTrial as FreeTrial;
|
||||
} else if (trial) {
|
||||
free_trial = CreateFreeTrialSchema.parse({
|
||||
length: 7,
|
||||
duration: FreeTrialDuration.Day,
|
||||
});
|
||||
}
|
||||
|
||||
const product: ProductV2 = {
|
||||
id: id_,
|
||||
name: id
|
||||
@@ -158,15 +169,7 @@ export const constructProduct = ({
|
||||
is_default: (type === "free" && isDefault) || forcePaidDefault,
|
||||
version: 1,
|
||||
group: group || "",
|
||||
free_trial:
|
||||
freeTrial || trial
|
||||
? (CreateFreeTrialSchema.parse({
|
||||
length: 7,
|
||||
duration: FreeTrialDuration.Day,
|
||||
unique_fingerprint: false,
|
||||
card_required: true,
|
||||
}) as any)
|
||||
: null,
|
||||
free_trial: free_trial as FreeTrial,
|
||||
created_at: Date.now(),
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
ApiVersion,
|
||||
customerProducts,
|
||||
customers,
|
||||
type ProductV2,
|
||||
} from "@autumn/shared";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import type { TestContext } from "tests/utils/testInitUtils/createTestContext.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
|
||||
export const createSharedProducts = async ({
|
||||
products,
|
||||
ctx,
|
||||
}: {
|
||||
products: ProductV2[];
|
||||
ctx: TestContext;
|
||||
}) => {
|
||||
const { db } = ctx;
|
||||
|
||||
let cusProducts = await ctx.db.query.customerProducts.findMany({
|
||||
where: inArray(
|
||||
customerProducts.product_id,
|
||||
products.map((p) => p.id),
|
||||
),
|
||||
with: {
|
||||
product: true,
|
||||
},
|
||||
});
|
||||
cusProducts = cusProducts.filter((cp) => cp.product.org_id === ctx.org.id);
|
||||
|
||||
if (cusProducts.length > 5) {
|
||||
throw new Error("Too many customers under shared default free product");
|
||||
}
|
||||
|
||||
await ctx.db.delete(customers).where(
|
||||
and(
|
||||
inArray(
|
||||
customers.internal_id,
|
||||
cusProducts.map((cp) => cp.internal_customer_id),
|
||||
),
|
||||
eq(customers.env, ctx.env),
|
||||
eq(customers.org_id, ctx.org.id),
|
||||
),
|
||||
);
|
||||
|
||||
const autumn = new AutumnInt({
|
||||
secretKey: ctx.orgSecretKey,
|
||||
version: ApiVersion.V1_2,
|
||||
});
|
||||
|
||||
try {
|
||||
await createProducts({
|
||||
db,
|
||||
orgId: ctx.org.id,
|
||||
env: ctx.env,
|
||||
autumn,
|
||||
products,
|
||||
});
|
||||
} catch (_error) {}
|
||||
};
|
||||
@@ -12,18 +12,25 @@ export const initCustomerV3 = async ({
|
||||
customerData,
|
||||
attachPm,
|
||||
withTestClock = true,
|
||||
withDefault = false,
|
||||
defaultProductId,
|
||||
}: {
|
||||
ctx: TestContext;
|
||||
customerId: string;
|
||||
attachPm?: "success" | "fail";
|
||||
customerData?: CustomerData;
|
||||
withTestClock?: boolean;
|
||||
withDefault?: boolean;
|
||||
defaultProductId?: string;
|
||||
}) => {
|
||||
const name = customerId;
|
||||
const email = `${customerId}@example.com`;
|
||||
const fingerprint_ = "";
|
||||
const { stripeCli } = ctx;
|
||||
const autumn = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
const autumn = new AutumnInt({
|
||||
version: ApiVersion.V1_2,
|
||||
secretKey: ctx.orgSecretKey,
|
||||
});
|
||||
|
||||
let testClockId: string | undefined;
|
||||
|
||||
@@ -53,6 +60,8 @@ export const initCustomerV3 = async ({
|
||||
// @ts-expect-error
|
||||
fingerprint: customerData?.fingerprint || fingerprint_,
|
||||
stripe_id: stripeCus.id,
|
||||
disable_default: !withDefault,
|
||||
default_product_id: defaultProductId,
|
||||
});
|
||||
|
||||
// 3. Attach payment method
|
||||
|
||||
@@ -8,21 +8,47 @@ export const initProductsV0 = async ({
|
||||
ctx,
|
||||
products,
|
||||
prefix,
|
||||
skipPrefixIds = [],
|
||||
customerId,
|
||||
customerIds,
|
||||
}: {
|
||||
ctx: TestContext;
|
||||
products: ProductV2[];
|
||||
prefix?: string;
|
||||
skipPrefixIds?: string[];
|
||||
customerId?: string;
|
||||
customerIds?: string[];
|
||||
}) => {
|
||||
// 1. Add prefix to products
|
||||
// 1. Add prefix to products (except those in skipPrefixIds)
|
||||
if (prefix) {
|
||||
const productsToPrefix = products.filter(
|
||||
(p) => !skipPrefixIds.includes(p.id),
|
||||
);
|
||||
addPrefixToProducts({
|
||||
products,
|
||||
products: productsToPrefix,
|
||||
prefix,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Create
|
||||
const autumn = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
// 2. Create products using the org's secret key
|
||||
const autumn = new AutumnInt({
|
||||
version: ApiVersion.V1_2,
|
||||
secretKey: ctx.orgSecretKey,
|
||||
});
|
||||
|
||||
if (customerIds) {
|
||||
for (const id of customerIds) {
|
||||
try {
|
||||
await autumn.customers.delete(id);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
if (customerId) {
|
||||
try {
|
||||
await autumn.customers.delete(customerId);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
await createProducts({
|
||||
db: ctx.db,
|
||||
orgId: ctx.org.id,
|
||||
|
||||
354
server/tests/MIGRATION_GUIDE.md
Normal file
354
server/tests/MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,354 @@
|
||||
# Test Migration Guide
|
||||
|
||||
## Overview
|
||||
This guide explains how to migrate test files from the global state pattern to the isolated test context pattern.
|
||||
|
||||
## Quick Start Migration Prompt (Copy & Paste)
|
||||
|
||||
Use this prompt for AI coding agents to migrate test files:
|
||||
|
||||
```
|
||||
Migrate test file [FILE_PATH] from global state to isolated test context.
|
||||
|
||||
**Setup:**
|
||||
1. Create backup: `cp [FILE_PATH] [FILE_PATH.backup.test.ts]` (DO NOT delete backup)
|
||||
2. Read migration guide: @server/tests/MIGRATION_GUIDE.md
|
||||
3. Read original file to understand all test logic
|
||||
|
||||
**Critical Rules:**
|
||||
- PRESERVE ALL test logic, assertions, and edge cases
|
||||
- DO NOT remove force_checkout tests or any existing tests
|
||||
- Replace `compareMainProduct` with `expectCustomerV0Correct`
|
||||
- Use TestFeature enum (Messages, Dashboard, Admin) instead of global features
|
||||
- Products MUST be created with `initProductsV0` BEFORE customer creation
|
||||
- Free trials use this exact structure:
|
||||
```typescript
|
||||
freeTrial: {
|
||||
length: 7,
|
||||
duration: FreeTrialDuration.Day, // Import from @autumn/shared
|
||||
unique_fingerprint: true,
|
||||
card_required: true,
|
||||
}
|
||||
```
|
||||
|
||||
**Migration Steps:**
|
||||
1. Replace global imports with TestFeature enum
|
||||
2. Define products inline using `constructProduct()` and feature item constructors
|
||||
3. Set `const customerId = testCase;` at top of describe block
|
||||
4. Create AutumnInt instance with `ctx.orgSecretKey` and `ApiVersion.V1_2`
|
||||
5. In beforeAll:
|
||||
- Call `initProductsV0({ ctx, products, prefix: testCase, customerId })` FIRST
|
||||
- Then call `initCustomerV3({ ctx, customerId, ... })`
|
||||
6. Replace `compareMainProduct` with `expectCustomerV0Correct`
|
||||
7. For entitlement types, use: `ApiCustomerV1["entitlements"][number]`
|
||||
8. When checking entitlements, iterate through REFERENCE product (what you sent), not customer data
|
||||
|
||||
**Testing:**
|
||||
Run: `bun test --timeout 0 [FILE_PATH]`
|
||||
|
||||
If you encounter unfamiliar utility functions, STOP and ASK how to handle them.
|
||||
```
|
||||
|
||||
## Detailed Migration Prompt for Coding Agent
|
||||
|
||||
```
|
||||
Migrate the test file [FILE_PATH] from using global state to isolated test context.
|
||||
|
||||
Reference the migration guide at @server/tests/MIGRATION_GUIDE.md for the full pattern.
|
||||
|
||||
**Critical Requirements:**
|
||||
1. DO NOT remove any existing test logic - preserve ALL test cases and assertions
|
||||
2. DO NOT remove any force_checkout tests or other edge case tests
|
||||
3. Compare line-by-line with the original file to ensure nothing is lost
|
||||
4. If you encounter unfamiliar utility functions (beyond `compareMainProduct`), STOP and ASK the user how to handle them - do not attempt to migrate them on your own
|
||||
|
||||
**Migration Steps:**
|
||||
|
||||
1. **Create a Backup Copy**
|
||||
- BEFORE making any changes, create a copy of the test file for reference
|
||||
- Example: `cp basic2.test.ts basic2.test.ts.backup`
|
||||
- This allows you to compare line-by-line during migration to ensure nothing is lost
|
||||
- Delete the backup file after migration is complete and verified
|
||||
|
||||
2. **Replace Global Imports**
|
||||
- Remove: `import { features, products } from "tests/global.js";`
|
||||
- Add: `import { TestFeature } from "tests/setup/v2Features.js";`
|
||||
|
||||
3. **Create Inline Product Definitions**
|
||||
- Use `constructProduct()` to define products directly in the test file
|
||||
- Use `constructFeatureItem()`, `constructPrepaidItem()`, etc. for items
|
||||
- Reference TestFeature enum instead of global features object
|
||||
- Add a unique prefix to product IDs (e.g., testCase name)
|
||||
|
||||
4. **Update Test Setup**
|
||||
- Add `const customerId = testCase;` at the top of describe block
|
||||
- Create AutumnInt instance with org secret key:
|
||||
```typescript
|
||||
const autumnV1 = new AutumnInt({
|
||||
secretKey: ctx.orgSecretKey,
|
||||
version: ApiVersion.V1_2,
|
||||
});
|
||||
```
|
||||
- In beforeAll:
|
||||
- Call `initProductsV0({ ctx, products: [...], prefix: testCase, customerId })` BEFORE customer creation
|
||||
- **IMPORTANT:** Passing `customerId` to `initProductsV0` automatically handles customer cleanup if they exist - you DO NOT need to manually delete the customer first
|
||||
- Call `initCustomerV3({ ctx, customerId, ... })` AFTER products
|
||||
|
||||
5. **Update Test Assertions**
|
||||
- Replace `compareMainProduct` with `expectCustomerV0Correct` for v0.1 API
|
||||
- Replace references to `features.metered1` with `TestFeature.Messages`
|
||||
- Replace references to `features.boolean1` with `TestFeature.Dashboard`
|
||||
- Use `AutumnCli.getCustomer()` for v0.1 API format (returns `features` object)
|
||||
- Use `autumnV1.customers.get()` for v1.2 API format (returns `entitlements` array)
|
||||
|
||||
**Type Helpers:**
|
||||
- For entitlement types from v0.1 API, use: `ApiCustomerV1["entitlements"][number]`
|
||||
- Import from: `import type { ApiCustomerV1 } from "@shared/api/customers/previousVersions/apiCustomerV1.js";`
|
||||
- Example:
|
||||
```typescript
|
||||
const addOnBalance = cusRes.entitlements.find(
|
||||
(e: ApiCustomerV1["entitlements"][number]) =>
|
||||
e.feature_id === TestFeature.Messages && e.interval === "lifetime"
|
||||
);
|
||||
```
|
||||
|
||||
**⚠️ CRITICAL - Entitlement Checking Pattern:**
|
||||
When testing entitlements with `/check` endpoint, you MUST iterate through the **reference product's entitlements** (what you SENT), NOT the customer's entitlements (what they have).
|
||||
|
||||
**WRONG (iterating through customer's entitlements):**
|
||||
```typescript
|
||||
const customer = await AutumnCli.getCustomer(customerId);
|
||||
const entitlements = customer.features; // ❌ WRONG!
|
||||
|
||||
for (const featureId of Object.keys(entitlements)) {
|
||||
const res = await AutumnCli.entitled(customerId, featureId);
|
||||
// checking against customer data...
|
||||
}
|
||||
```
|
||||
|
||||
**CORRECT (iterating through reference product's entitlements):**
|
||||
```typescript
|
||||
import { convertProductV2ToV1 } from "@/internal/products/productUtils/productV2Utils/convertProductV2ToV1.js";
|
||||
|
||||
// Convert ProductV2 to V1 to get reference entitlements
|
||||
const proProdV1 = convertProductV2ToV1({
|
||||
productV2: proProd,
|
||||
orgId: ctx.org.id,
|
||||
features: ctx.features,
|
||||
});
|
||||
const proEntitlements = proProdV1.entitlements;
|
||||
|
||||
// Iterate through reference product's entitlements
|
||||
for (const entitlement of Object.values(proEntitlements)) {
|
||||
const res = await AutumnCli.entitled(customerId, entitlement.feature_id);
|
||||
// Check that the response matches what we SENT...
|
||||
expect(res.allowed).toBe(true);
|
||||
if (entitlement.allowance) {
|
||||
const balance = res.balances.find(b => b.feature_id === entitlement.feature_id);
|
||||
expect(balance?.balance).toBe(entitlement.allowance);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This pattern ensures you're testing what you SENT against what you GET back from the API.
|
||||
|
||||
**⚠️ IMPORTANT - Unfamiliar Utility Functions:**
|
||||
If you encounter utility functions that you're not sure how to migrate (e.g., `compareMainProduct`, `expectProductCorrect`, `checkEntitlements`, or other custom assertion helpers):
|
||||
- **DO NOT attempt to migrate or replace them on your own**
|
||||
- **STOP and ASK the user**: "I found utility function [FUNCTION_NAME] at line [LINE]. How should I handle this in the migration?"
|
||||
- Wait for explicit instructions on the correct replacement function or pattern
|
||||
- Common replacements so far:
|
||||
- `compareMainProduct` → `expectCustomerV0Correct`
|
||||
- But there may be others that need different handling!
|
||||
|
||||
6. **Verify All Logic Preserved**
|
||||
- Check that every test case from the original file exists
|
||||
- Check that every assertion is present
|
||||
- Check that force_checkout tests are included
|
||||
- Check that edge case tests are not removed
|
||||
|
||||
7. **Update Test Case ID**
|
||||
- Change `testCase = "testname"` to keep original name (not "testname-new")
|
||||
- Update console.log messages to use correct testCase
|
||||
|
||||
8. **Run Tests**
|
||||
- Verify all tests pass with `bun test --timeout 0 [FILE_PATH]`
|
||||
- The `--timeout 0` flag disables test timeouts, which is necessary for tests that involve checkout flows and longer async operations
|
||||
|
||||
**Example Migration:**
|
||||
|
||||
Before:
|
||||
```typescript
|
||||
import { features, products } from "tests/global.js";
|
||||
|
||||
const testCase = "basic1";
|
||||
describe("basic1", () => {
|
||||
const customerId = testCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({ ctx, customerId });
|
||||
});
|
||||
|
||||
test("should have correct entitlements", async () => {
|
||||
const entitled = await AutumnCli.entitled(customerId, features.metered1.id);
|
||||
expect(entitled.allowed).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
After:
|
||||
```typescript
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
|
||||
const freeProd = constructProduct({
|
||||
type: "free",
|
||||
isDefault: true,
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 5,
|
||||
interval: ProductItemInterval.Month,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const testCase = "basic1";
|
||||
const customerId = testCase;
|
||||
|
||||
describe("basic1", () => {
|
||||
const autumnV1 = new AutumnInt({
|
||||
secretKey: ctx.orgSecretKey,
|
||||
version: ApiVersion.V1_2,
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
// Passing customerId automatically handles cleanup
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [freeProd],
|
||||
prefix: testCase,
|
||||
customerId,
|
||||
});
|
||||
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData: { fingerprint: "test" },
|
||||
withTestClock: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("should have correct entitlements", async () => {
|
||||
const entitled = await AutumnCli.entitled(customerId, TestFeature.Messages);
|
||||
expect(entitled.allowed).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**After Migration:**
|
||||
- Replace the original file (not create a .new.test.ts file)
|
||||
- Verify tests pass
|
||||
- Report any issues or edge cases found
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Product Construction
|
||||
```typescript
|
||||
// Free product with feature
|
||||
const freeProd = constructProduct({
|
||||
type: "free",
|
||||
isDefault: true,
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 5,
|
||||
interval: ProductItemInterval.Month,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Pro product (matches global products.pro)
|
||||
// - Boolean feature (Dashboard)
|
||||
// - Metered feature (Messages) with 10 allowance
|
||||
// - Unlimited feature (Admin)
|
||||
// - Monthly subscription price ($20)
|
||||
const proProd = constructProduct({
|
||||
type: "pro",
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Dashboard,
|
||||
isBoolean: true,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 10,
|
||||
interval: ProductItemInterval.Month,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Admin,
|
||||
unlimited: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Pro product with free trial
|
||||
// IMPORTANT: Free trial structure must use this exact format
|
||||
const proWithTrial = constructProduct({
|
||||
type: "pro",
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Dashboard,
|
||||
isBoolean: true,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 10,
|
||||
interval: ProductItemInterval.Month,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Admin,
|
||||
unlimited: true,
|
||||
}),
|
||||
],
|
||||
freeTrial: {
|
||||
length: 7,
|
||||
duration: FreeTrialDuration.Day, // Import FreeTrialDuration from @autumn/shared
|
||||
unique_fingerprint: true, // Set to true to prevent duplicate trials per fingerprint
|
||||
card_required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Add-on product
|
||||
const addOnProd = constructProduct({
|
||||
type: "paid",
|
||||
id: "addon",
|
||||
isAddOn: true,
|
||||
items: [
|
||||
constructPrepaidItem({
|
||||
featureId: TestFeature.Messages,
|
||||
price: 500, // $5.00
|
||||
billingUnits: 100,
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Feature Mapping
|
||||
- `features.metered1` → `TestFeature.Messages`
|
||||
- `features.boolean1` → `TestFeature.Dashboard`
|
||||
- `features.metered2` → Create new feature if needed
|
||||
|
||||
### API Version Differences
|
||||
- **v0.1 API** (AutumnCli): Returns `{ features: { [featureId]: {...} } }`
|
||||
- **v1.2 API** (AutumnInt): Returns `{ entitlements: [...] }`
|
||||
|
||||
## Why This Migration?
|
||||
|
||||
1. **Parallel Test Isolation**: Tests can run in parallel without conflicting
|
||||
2. **No Global State**: Each test has its own products and data
|
||||
3. **Test Independence**: Tests don't depend on setup order
|
||||
4. **Better Debugging**: Each test is self-contained and easier to understand
|
||||
96
server/tests/MIGRATION_TRACKER.md
Normal file
96
server/tests/MIGRATION_TRACKER.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Test Migration Tracker
|
||||
|
||||
Track the progress of migrating test files from global state to isolated test context.
|
||||
|
||||
## Migration Status
|
||||
|
||||
Legend:
|
||||
- ✅ = Migrated and passing
|
||||
- 🚧 = In progress
|
||||
- ⏳ = Not started
|
||||
- ⚠️ = Needs review
|
||||
- ❌ = Skipped/Archived
|
||||
|
||||
## Test Files to Migrate
|
||||
|
||||
### Basic Tests
|
||||
- [x] ✅ `tests/attach/basic/basic1.test.ts` - Migrated
|
||||
- [x] ✅ `tests/attach/basic/basic2.test.ts` - Migrated (renamed from basic4)
|
||||
- [x] ✅ `tests/attach/basic/basic3.test.ts` - Migrated (renamed from basic5)
|
||||
- [x] ✅ `tests/attach/basic/basic6.test.ts` - Migrated
|
||||
- [x] ✅ `tests/attach/basic/basic7.test.ts` - Migrated
|
||||
- [x] ✅ `tests/attach/basic/basic8.test.ts` - Migrated
|
||||
- [x] ✅ `tests/attach/basic/basic9.test.ts` - Migrated
|
||||
- [x] ✅ `tests/attach/basic/basic10.test.ts` - Migrated
|
||||
|
||||
### Downgrade Tests
|
||||
- [ ] ⏳ `tests/attach/downgrade/downgrade5.test.ts`
|
||||
- [ ] ⏳ `tests/attach/downgrade/downgrade6.test.ts`
|
||||
- [ ] ⏳ `tests/attach/downgrade/downgrade7.test.ts`
|
||||
|
||||
### Multi-Product Tests
|
||||
- [ ] ⏳ `tests/attach/multiProduct/multiProduct1.ts`
|
||||
- [ ] ⏳ `tests/attach/multiProduct/multiProduct2.ts`
|
||||
|
||||
### Other Tests
|
||||
- [ ] ⏳ `tests/attach/others/others4.ts`
|
||||
- [ ] ⏳ `tests/attach/others/others5.ts`
|
||||
|
||||
### Upgrade (Old) Tests
|
||||
- [ ] ⏳ `tests/attach/upgradeOld/upgradeOld1.ts`
|
||||
- [ ] ⏳ `tests/attach/upgradeOld/upgradeOld2.ts`
|
||||
- [ ] ⏳ `tests/attach/upgradeOld/upgradeOld3.ts`
|
||||
- [ ] ⏳ `tests/attach/upgradeOld/upgradeOld4.ts`
|
||||
|
||||
### Core Tests
|
||||
- [ ] ⏳ `tests/core/cancel/cancel5.test.ts`
|
||||
|
||||
### Continuous Use Tests
|
||||
- [ ] ⏳ `tests/contUse/track/track5.ts`
|
||||
|
||||
### Advanced Tests
|
||||
- [ ] ⏳ `tests/advanced/coupons/coupon1.ts`
|
||||
- [ ] ⏳ `tests/advanced/multiFeature/multiFeature1.ts`
|
||||
- [ ] ⏳ `tests/advanced/multiFeature/multiFeature2.ts`
|
||||
- [ ] ⏳ `tests/advanced/multiFeature/multiFeature3.ts`
|
||||
|
||||
### Archived Tests (Review if needed)
|
||||
- [ ] ❌ `tests/archives/arrear_prorated/arrear_prorated2.ts`
|
||||
- [ ] ❌ `tests/archives/arrear_prorated/arrear_prorated3.ts`
|
||||
- [ ] ❌ `tests/archives/coupon1 copy.ts`
|
||||
|
||||
## Utility Files (Don't Migrate)
|
||||
These are helper files, not tests:
|
||||
- `tests/utils/compare.ts`
|
||||
- `tests/utils/advancedUsageUtils.ts`
|
||||
|
||||
## Migration Prompt
|
||||
|
||||
When ready to migrate a file, use this prompt:
|
||||
|
||||
```
|
||||
Migrate the test file [FILE_PATH] from using global state to isolated test context.
|
||||
|
||||
Reference the migration guide at @server/tests/MIGRATION_GUIDE.md for the full pattern.
|
||||
|
||||
**Critical Requirements:**
|
||||
1. DO NOT remove any existing test logic - preserve ALL test cases and assertions
|
||||
2. DO NOT remove any force_checkout tests or other edge case tests
|
||||
3. Compare line-by-line with the original file to ensure nothing is lost
|
||||
4. Replace the original file (not create a .new.test.ts file)
|
||||
5. Update testCase ID to match original (e.g., "basic2" not "basic2-new")
|
||||
|
||||
After migration, run: `bun test [FILE_PATH]` to verify all tests pass.
|
||||
```
|
||||
|
||||
## Progress Summary
|
||||
- **Total Files**: 30
|
||||
- **Migrated**: 8 (27%)
|
||||
- **In Progress**: 0 (0%)
|
||||
- **Remaining**: 22 (73%)
|
||||
|
||||
## Notes
|
||||
- Start with basic tests (basic2-10) as they're simpler
|
||||
- Downgrade and upgrade tests may be more complex
|
||||
- Archived tests may not need migration
|
||||
- Each migration should preserve ALL test logic and assertions
|
||||
31
server/tests/TEST_GUIDE.md
Normal file
31
server/tests/TEST_GUIDE.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Test Writing Guide
|
||||
|
||||
## Initial Notes (To be organized later)
|
||||
|
||||
### Customer Initialization
|
||||
|
||||
#### Default Products
|
||||
- For tests involving default products, use the `withDefault: true` flag in `initCustomerV3()`
|
||||
- This ensures the customer is created with the default product attached
|
||||
- Example:
|
||||
```typescript
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData: { fingerprint: "test" },
|
||||
withTestClock: false,
|
||||
withDefault: true, // Attach default product on creation
|
||||
});
|
||||
```
|
||||
|
||||
#### Fingerprint
|
||||
- For tests involving fingerprint, pass in `fingerprint` through `customerData` in `initCustomerV3()`
|
||||
- Example:
|
||||
```typescript
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData: { fingerprint: "test" }, // Pass fingerprint here
|
||||
withTestClock: false,
|
||||
});
|
||||
```
|
||||
@@ -1,10 +1,9 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { LegacyVersion } from "@autumn/shared";
|
||||
import { ApiVersion } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||
import { features, products } from "tests/global.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { compareMainProduct } from "tests/utils/compare.js";
|
||||
import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js";
|
||||
import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js";
|
||||
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
|
||||
import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
@@ -13,91 +12,103 @@ import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
import { sharedDefaultFree } from "./sharedProducts.js";
|
||||
|
||||
const freeProd = constructProduct({
|
||||
const free2 = constructProduct({
|
||||
type: "free",
|
||||
id: "free2",
|
||||
isDefault: false,
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 1000,
|
||||
}),
|
||||
// constructFixedPrice({
|
||||
// price: 0,
|
||||
// }),
|
||||
],
|
||||
});
|
||||
|
||||
const testCase = "basic1";
|
||||
const customerId = testCase;
|
||||
|
||||
describe(`${chalk.yellowBright("basic1: Testing attach free product")}`, () => {
|
||||
const customerId = testCase;
|
||||
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_2 });
|
||||
describe(`${chalk.yellowBright("basic1: Testing attach free, default product")}`, () => {
|
||||
const autumnV1 = new AutumnInt({
|
||||
secretKey: ctx.orgSecretKey,
|
||||
version: ApiVersion.V1_2,
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
console.log(`[basic1] Using org: ${ctx.org.slug} (${ctx.org.id})`);
|
||||
console.log("Customer ID: ", customerId);
|
||||
|
||||
// Create products FIRST so default product can be attached to customer
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [free2],
|
||||
prefix: testCase,
|
||||
customerId,
|
||||
});
|
||||
|
||||
// Then create customer (will auto-attach default product if exists)
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData: { fingerprint: "test" },
|
||||
withTestClock: false,
|
||||
});
|
||||
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [freeProd],
|
||||
prefix: testCase,
|
||||
withDefault: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should create customer and have default free active", async () => {
|
||||
const data = await AutumnCli.getCustomer(customerId);
|
||||
|
||||
compareMainProduct({
|
||||
sent: products.free,
|
||||
await expectCustomerV0Correct({
|
||||
sent: sharedDefaultFree,
|
||||
cusRes: data,
|
||||
});
|
||||
});
|
||||
|
||||
test("should have correct entitlements", async () => {
|
||||
const expectedEntitlement = products.free.entitlements.metered1;
|
||||
|
||||
// Expected: 5 allowance for Messages feature
|
||||
const entitled = (await AutumnCli.entitled(
|
||||
customerId,
|
||||
features.metered1.id,
|
||||
TestFeature.Messages,
|
||||
)) as any;
|
||||
|
||||
console.log("Entitled response:", entitled);
|
||||
const metered1Balance = entitled.balances.find(
|
||||
(balance: any) => balance.feature_id === features.metered1.id,
|
||||
(balance: any) => balance.feature_id === TestFeature.Messages,
|
||||
);
|
||||
|
||||
expect(entitled.allowed).toBe(true);
|
||||
expect(metered1Balance).toBeDefined();
|
||||
expect(metered1Balance.balance).toBe(expectedEntitlement.allowance);
|
||||
expect(metered1Balance.balance).toBe(5);
|
||||
expect(metered1Balance.unlimited).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should have correct boolean1 entitlement", async () => {
|
||||
const entitled = await AutumnCli.entitled(customerId, features.boolean1.id);
|
||||
// Dashboard feature is not included in freeProd, should be false
|
||||
const entitled = await AutumnCli.entitled(
|
||||
customerId,
|
||||
TestFeature.Dashboard,
|
||||
);
|
||||
expect(entitled!.allowed).toBe(false);
|
||||
});
|
||||
|
||||
test("should attach free (with $0 price) and force checkout and succeed", async () => {
|
||||
await autumn.attach({
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: freeProd.id,
|
||||
product_id: free2.id,
|
||||
force_checkout: true,
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
const customer = await autumnV1.customers.get(customerId);
|
||||
|
||||
expectProductAttached({
|
||||
customer,
|
||||
product: freeProd,
|
||||
product: free2,
|
||||
});
|
||||
|
||||
expectFeaturesCorrect({
|
||||
customer,
|
||||
product: freeProd,
|
||||
product: free2,
|
||||
otherProducts: [sharedDefaultFree],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,78 +1,150 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ApiVersion, ProductItemInterval } from "@autumn/shared";
|
||||
import type { ApiCustomerV1 } from "@shared/api/customers/previousVersions/apiCustomerV1.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||
import { products } from "tests/global.js";
|
||||
import { compareMainProduct } from "tests/utils/compare.js";
|
||||
import { timeout } from "tests/utils/genUtils.js";
|
||||
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js";
|
||||
import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import {
|
||||
constructFeatureItem,
|
||||
constructPrepaidItem,
|
||||
} from "@/utils/scriptUtils/constructItem.js";
|
||||
import {
|
||||
constructProduct,
|
||||
constructRawProduct,
|
||||
} from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
|
||||
// Pro product (matches global products.pro)
|
||||
const proProd = constructProduct({
|
||||
type: "pro",
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Dashboard,
|
||||
isBoolean: true,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 10,
|
||||
interval: ProductItemInterval.Month,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Admin,
|
||||
unlimited: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Monthly add-on product (matches global products.monthlyAddOnMetered1)
|
||||
// - Prepaid monthly add-on
|
||||
// - 0 base allowance, customer specifies quantity
|
||||
const monthlyAddOn = constructRawProduct({
|
||||
id: "monthly-add-on-metered-1",
|
||||
isAddOn: true,
|
||||
items: [
|
||||
constructPrepaidItem({
|
||||
featureId: TestFeature.Messages,
|
||||
price: 9,
|
||||
billingUnits: 250,
|
||||
includedUsage: 0,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const testCase = "basic2";
|
||||
const customerId = testCase;
|
||||
|
||||
describe(`${chalk.yellowBright("basic2: Testing attach pro")}`, () => {
|
||||
const customerId = testCase;
|
||||
const autumn: AutumnInt = new AutumnInt();
|
||||
describe(`${chalk.yellowBright("basic2: Testing attach monthly add on")}`, () => {
|
||||
const autumnV1 = new AutumnInt({
|
||||
secretKey: ctx.orgSecretKey,
|
||||
version: ApiVersion.V1_2,
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create products FIRST before customer creation
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [proProd, monthlyAddOn],
|
||||
prefix: testCase,
|
||||
customerId,
|
||||
});
|
||||
|
||||
// Then create customer with payment method
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData: { fingerprint: "test" },
|
||||
attachPm: "success",
|
||||
withTestClock: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach pro through checkout", async () => {
|
||||
const { checkout_url } = await autumn.attach({
|
||||
test("should attach pro", async () => {
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: products.pro.id,
|
||||
product_id: proProd.id,
|
||||
});
|
||||
|
||||
await completeCheckoutForm(checkout_url);
|
||||
await timeout(12000);
|
||||
const res = await AutumnCli.getCustomer(customerId);
|
||||
await expectCustomerV0Correct({
|
||||
sent: proProd,
|
||||
cusRes: res,
|
||||
});
|
||||
});
|
||||
|
||||
const monthlyQuantity = 500;
|
||||
|
||||
test("should attach monthly add on", async () => {
|
||||
await AutumnCli.attach({
|
||||
customerId: customerId,
|
||||
productId: monthlyAddOn.id,
|
||||
forceCheckout: false,
|
||||
options: [
|
||||
{
|
||||
feature_id: TestFeature.Messages,
|
||||
quantity: monthlyQuantity,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("should have correct product & entitlements", async () => {
|
||||
const res = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: products.pro,
|
||||
cusRes: res,
|
||||
});
|
||||
expect(res.invoices.length).toBeGreaterThan(0);
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
|
||||
// Pro gives 10 Messages
|
||||
const proMetered1 = 10;
|
||||
|
||||
const monthlyMetered1Balance = cusRes.entitlements.find(
|
||||
(e: ApiCustomerV1["entitlements"][number]) =>
|
||||
e.feature_id === TestFeature.Messages && e.interval === "month",
|
||||
);
|
||||
|
||||
expect(monthlyMetered1Balance?.balance).toBe(proMetered1 + monthlyQuantity);
|
||||
|
||||
expect(cusRes.add_ons).toHaveLength(1);
|
||||
const monthlyAddOnId = cusRes.add_ons.find(
|
||||
(a: any) => a.id === monthlyAddOn.id,
|
||||
);
|
||||
|
||||
expect(monthlyAddOnId).toBeDefined();
|
||||
expect(cusRes.invoices.length).toBe(2);
|
||||
});
|
||||
|
||||
test("should have correct result when calling /check", async () => {
|
||||
const proEntitlements = products.pro.entitlements;
|
||||
test("should have correct /check result for metered1", async () => {
|
||||
const res: any = await AutumnCli.entitled(customerId, TestFeature.Messages);
|
||||
|
||||
for (const entitlement of Object.values(proEntitlements)) {
|
||||
const allowance = entitlement.allowance;
|
||||
const metered1Balance = res!.balances.find(
|
||||
(b: any) => b.feature_id === TestFeature.Messages,
|
||||
);
|
||||
|
||||
const res: any = await AutumnCli.entitled(
|
||||
customerId,
|
||||
entitlement.feature_id!,
|
||||
);
|
||||
// Pro gives 10, monthly add-on gives monthlyQuantity
|
||||
const proMetered1Amt = 10;
|
||||
const monthlyAddOnMetered1Amt = monthlyQuantity;
|
||||
|
||||
const entBalance = res!.balances.find(
|
||||
(b: any) => b.feature_id === entitlement.feature_id,
|
||||
);
|
||||
|
||||
try {
|
||||
expect(res!.allowed).toBe(true);
|
||||
expect(entBalance).toBeDefined();
|
||||
if (entitlement.allowance) {
|
||||
expect(entBalance!.balance).toBe(allowance);
|
||||
}
|
||||
} catch (error) {
|
||||
console.group();
|
||||
console.group();
|
||||
console.log("Looking for: ", entitlement);
|
||||
console.log("Received: ", res);
|
||||
console.groupEnd();
|
||||
console.groupEnd();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
expect(metered1Balance!.balance).toBe(
|
||||
proMetered1Amt + monthlyAddOnMetered1Amt,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,135 +1,105 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ApiVersion, CusProductStatus } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import type Stripe from "stripe";
|
||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||
import { features, products } from "tests/global.js";
|
||||
import { compareMainProduct } from "tests/utils/compare.js";
|
||||
import { timeout } from "tests/utils/genUtils.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
|
||||
import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js";
|
||||
import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
|
||||
const oneTimeItem = constructPrepaidItem({
|
||||
featureId: features.metered1.id,
|
||||
price: 9,
|
||||
billingUnits: 250,
|
||||
isOneOff: true,
|
||||
});
|
||||
|
||||
const oneTime = constructRawProduct({
|
||||
id: "basic3_one_off",
|
||||
items: [oneTimeItem],
|
||||
isAddOn: true,
|
||||
});
|
||||
|
||||
const monthlyItem = constructPrepaidItem({
|
||||
featureId: features.metered1.id,
|
||||
price: 9,
|
||||
billingUnits: 250,
|
||||
});
|
||||
|
||||
const monthly = constructRawProduct({
|
||||
id: "basic3_monthly",
|
||||
items: [
|
||||
constructPrepaidItem({
|
||||
featureId: features.metered1.id,
|
||||
price: 9,
|
||||
billingUnits: 250,
|
||||
}),
|
||||
],
|
||||
});
|
||||
import { sharedDefaultFree, sharedProProduct } from "./sharedProducts.js";
|
||||
|
||||
const testCase = "basic3";
|
||||
const customerId = testCase;
|
||||
|
||||
describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add ons")}`, () => {
|
||||
const customerId = testCase;
|
||||
const autumn: AutumnInt = new AutumnInt();
|
||||
describe(`${chalk.yellowBright("basic3: Testing cancel through Stripe at period end and now")}`, () => {
|
||||
const autumnV1 = new AutumnInt({
|
||||
secretKey: ctx.orgSecretKey,
|
||||
version: ApiVersion.V1_2,
|
||||
});
|
||||
|
||||
let stripeCli: Stripe;
|
||||
|
||||
beforeAll(async () => {
|
||||
stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
|
||||
|
||||
// Then create customer with payment method
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
attachPm: "success",
|
||||
withTestClock: true,
|
||||
});
|
||||
|
||||
await createProducts({
|
||||
autumn: autumn,
|
||||
db: ctx.db,
|
||||
orgId: ctx.org.id,
|
||||
env: ctx.env,
|
||||
products: [oneTime, monthly],
|
||||
withDefault: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach pro", async () => {
|
||||
await autumn.attach({
|
||||
test("should attach pro product", async () => {
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: products.pro.id,
|
||||
product_id: sharedProProduct.id,
|
||||
});
|
||||
|
||||
const res = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: products.pro,
|
||||
await expectCustomerV0Correct({
|
||||
sent: sharedProProduct,
|
||||
cusRes: res,
|
||||
});
|
||||
});
|
||||
|
||||
const oneTimeQuantity = 500;
|
||||
const oneTimeBillingUnits = oneTimeItem.billing_units;
|
||||
const oneTimePurchaseCount = 2;
|
||||
test("should cancel pro product (at period end)", async () => {
|
||||
const cusRes: any = await AutumnCli.getCustomer(customerId);
|
||||
|
||||
test("should attach one time add on twice, force checkout", async () => {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const res = await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: oneTime.id,
|
||||
force_checkout: true,
|
||||
const proProduct = cusRes.products.find(
|
||||
(p: any) => p.id === sharedProProduct.id,
|
||||
);
|
||||
|
||||
for (const subId of proProduct.subscription_ids) {
|
||||
await stripeCli.subscriptions.update(subId, {
|
||||
cancel_at_period_end: true,
|
||||
});
|
||||
|
||||
await completeCheckoutForm(
|
||||
res.checkout_url,
|
||||
oneTimeQuantity / oneTimeBillingUnits!,
|
||||
);
|
||||
await timeout(15000);
|
||||
}
|
||||
await timeout(5000);
|
||||
});
|
||||
|
||||
test("should have correct product & entitlements", async () => {
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
test("should have pro product active, and canceled_at != null, and free scheduled", async () => {
|
||||
const cusRes: any = await AutumnCli.getCustomer(customerId);
|
||||
await expectCustomerV0Correct({
|
||||
sent: sharedProProduct,
|
||||
cusRes: cusRes,
|
||||
});
|
||||
|
||||
const addOnBalance = cusRes.entitlements.find(
|
||||
(e: any) =>
|
||||
e.feature_id === features.metered1.id &&
|
||||
e.interval ===
|
||||
products.oneTimeAddOnMetered1.entitlements.metered1.interval,
|
||||
const proProduct = cusRes.products.find(
|
||||
(p: any) => p.id === sharedProProduct.id,
|
||||
);
|
||||
expect(proProduct.canceled_at).not.toBe(null);
|
||||
expect(proProduct.status).toBe(CusProductStatus.Active);
|
||||
|
||||
const expectedAmt = oneTimeQuantity * oneTimePurchaseCount;
|
||||
|
||||
expect(addOnBalance!.balance).toBe(expectedAmt);
|
||||
|
||||
expect(cusRes.add_ons).toHaveLength(1);
|
||||
expect(cusRes.add_ons[0].id).toBe(oneTime.id);
|
||||
expect(cusRes.invoices.length).toBe(1 + oneTimePurchaseCount);
|
||||
const freeProduct = cusRes.products.find(
|
||||
(p: any) => p.id === sharedDefaultFree.id,
|
||||
);
|
||||
expect(freeProduct).toBeDefined();
|
||||
expect(freeProduct.status).toBe(CusProductStatus.Scheduled);
|
||||
});
|
||||
|
||||
test("should have correct /check result for metered1", async () => {
|
||||
const res: any = await AutumnCli.entitled(customerId, features.metered1.id);
|
||||
|
||||
expect(res!.allowed).toBe(true);
|
||||
|
||||
const proMetered1Amt = products.pro.entitlements.metered1.allowance;
|
||||
const addOnBalance = res!.balances.find(
|
||||
(b: any) => b.feature_id === features.metered1.id,
|
||||
test("should cancel pro product (now)", async () => {
|
||||
const cusRes: any = await AutumnCli.getCustomer(customerId);
|
||||
const proProduct = cusRes.products.find(
|
||||
(p: any) => p.id === sharedProProduct.id,
|
||||
);
|
||||
|
||||
expect(res!.allowed).toBe(true);
|
||||
expect(addOnBalance!.balance).toBe(
|
||||
proMetered1Amt! + oneTimeQuantity * oneTimePurchaseCount,
|
||||
);
|
||||
for (const subId of proProduct.subscription_ids) {
|
||||
await stripeCli.subscriptions.cancel(subId);
|
||||
}
|
||||
await timeout(5000);
|
||||
});
|
||||
|
||||
test("should have free product active, and no pro product", async () => {
|
||||
const cusRes: any = await AutumnCli.getCustomer(customerId);
|
||||
await expectCustomerV0Correct({
|
||||
sent: sharedDefaultFree,
|
||||
cusRes: cusRes,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import chalk from "chalk";
|
||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||
import { features, products } from "tests/global.js";
|
||||
import { compareMainProduct } from "tests/utils/compare.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
|
||||
const monthlyItem = constructPrepaidItem({
|
||||
featureId: features.metered1.id,
|
||||
price: 9,
|
||||
billingUnits: 250,
|
||||
});
|
||||
|
||||
const monthly = constructRawProduct({
|
||||
id: "basic4_monthly",
|
||||
items: [monthlyItem],
|
||||
});
|
||||
|
||||
const testCase = "basic4";
|
||||
|
||||
describe(`${chalk.yellowBright("basic4: Testing attach monthly add on")}`, () => {
|
||||
const customerId = testCase;
|
||||
const autumn: AutumnInt = new AutumnInt();
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
attachPm: "success",
|
||||
withTestClock: true,
|
||||
});
|
||||
|
||||
await createProducts({
|
||||
autumn: autumn,
|
||||
db: ctx.db,
|
||||
orgId: ctx.org.id,
|
||||
env: ctx.env,
|
||||
products: [monthly],
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach pro", async () => {
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: products.pro.id,
|
||||
});
|
||||
|
||||
const res = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: products.pro,
|
||||
cusRes: res,
|
||||
});
|
||||
});
|
||||
|
||||
const monthlyQuantity = 500;
|
||||
|
||||
test("should attach monthly add on", async () => {
|
||||
await AutumnCli.attach({
|
||||
customerId: customerId,
|
||||
productId: products.monthlyAddOnMetered1.id,
|
||||
forceCheckout: false,
|
||||
options: [
|
||||
{
|
||||
feature_id: features.metered1.id,
|
||||
quantity: monthlyQuantity,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("should have correct product & entitlements", async () => {
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
|
||||
const proMetered1 = products.pro.entitlements.metered1.allowance;
|
||||
|
||||
const monthlyMetered1Balance = cusRes.entitlements.find(
|
||||
(e: any) =>
|
||||
e.feature_id === features.metered1.id &&
|
||||
e.interval ===
|
||||
products.monthlyAddOnMetered1.entitlements.metered1.interval,
|
||||
);
|
||||
|
||||
expect(monthlyMetered1Balance!.balance).toBe(proMetered1! + monthlyQuantity);
|
||||
|
||||
expect(cusRes.add_ons).toHaveLength(1);
|
||||
const monthlyAddOnId = cusRes.add_ons.find(
|
||||
(a: any) => a.id === products.monthlyAddOnMetered1.id,
|
||||
);
|
||||
|
||||
expect(monthlyAddOnId).toBeDefined();
|
||||
expect(cusRes.invoices.length).toBe(2);
|
||||
});
|
||||
|
||||
test("should have correct /check result for metered1", async () => {
|
||||
const res: any = await AutumnCli.entitled(customerId, features.metered1.id);
|
||||
|
||||
const metered1Balance = res!.balances.find(
|
||||
(b: any) => b.feature_id === features.metered1.id,
|
||||
);
|
||||
|
||||
const proMetered1Amt = products.pro.entitlements.metered1.allowance;
|
||||
const monthlyAddOnMetered1Amt = monthlyQuantity;
|
||||
|
||||
expect(metered1Balance!.balance).toBe(
|
||||
proMetered1Amt! + monthlyAddOnMetered1Amt,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,90 +0,0 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { CusProductStatus } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import type Stripe from "stripe";
|
||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||
import { products } from "tests/global.js";
|
||||
import { compareMainProduct } from "tests/utils/compare.js";
|
||||
import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
|
||||
const testCase = "basic5";
|
||||
|
||||
describe(`${chalk.yellowBright("basic5: Testing cancel through Stripe at period end and now")}`, () => {
|
||||
const customerId = testCase;
|
||||
let stripeCli: Stripe;
|
||||
|
||||
beforeAll(async () => {
|
||||
stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
attachPm: "success",
|
||||
withTestClock: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach pro product", async () => {
|
||||
await AutumnCli.attach({
|
||||
customerId: customerId,
|
||||
productId: products.pro.id,
|
||||
});
|
||||
});
|
||||
|
||||
test("should cancel pro product (at period end)", async () => {
|
||||
const cusRes: any = await AutumnCli.getCustomer(customerId);
|
||||
|
||||
const proProduct = cusRes.products.find(
|
||||
(p: any) => p.id === products.pro.id,
|
||||
);
|
||||
|
||||
for (const subId of proProduct.subscription_ids) {
|
||||
await stripeCli.subscriptions.update(subId, {
|
||||
cancel_at_period_end: true,
|
||||
});
|
||||
}
|
||||
await timeout(5000);
|
||||
});
|
||||
|
||||
test.skip("should have pro product active, and canceled_at != null, and free scheduled", async () => {
|
||||
const cusRes: any = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: products.pro,
|
||||
cusRes: cusRes,
|
||||
});
|
||||
|
||||
const proProduct = cusRes.products.find(
|
||||
(p: any) => p.id === products.pro.id,
|
||||
);
|
||||
expect(proProduct.canceled_at).not.toBe(null);
|
||||
expect(proProduct.status).toBe(CusProductStatus.Active);
|
||||
|
||||
const freeProduct = cusRes.products.find(
|
||||
(p: any) => p.id === products.free.id,
|
||||
);
|
||||
expect(freeProduct).toBeDefined();
|
||||
expect(freeProduct.status).toBe(CusProductStatus.Scheduled);
|
||||
});
|
||||
|
||||
test("should cancel pro product (now)", async () => {
|
||||
const cusRes: any = await AutumnCli.getCustomer(customerId);
|
||||
const proProduct = cusRes.products.find(
|
||||
(p: any) => p.id === products.pro.id,
|
||||
);
|
||||
|
||||
for (const subId of proProduct.subscription_ids) {
|
||||
await stripeCli.subscriptions.cancel(subId);
|
||||
}
|
||||
await timeout(5000);
|
||||
});
|
||||
|
||||
test("should have free product active, and no pro product", async () => {
|
||||
const cusRes: any = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: products.free,
|
||||
cusRes: cusRes,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,54 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { CusProductStatus, type Customer } from "@autumn/shared";
|
||||
import {
|
||||
ApiVersion,
|
||||
CusProductStatus,
|
||||
type Customer,
|
||||
ProductItemInterval,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { addHours, addMonths } from "date-fns";
|
||||
import type Stripe from "stripe";
|
||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||
import { products } from "tests/global.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
|
||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||
import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
|
||||
// Pro product (matches global products.pro)
|
||||
const proProd = constructProduct({
|
||||
type: "pro",
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Dashboard,
|
||||
isBoolean: true,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 10,
|
||||
interval: ProductItemInterval.Month,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Admin,
|
||||
unlimited: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const testCase = "basic6";
|
||||
const customerId = testCase;
|
||||
|
||||
describe(`${chalk.yellowBright("basic6: Testing subscription past_due")}`, () => {
|
||||
const customerId = testCase;
|
||||
const autumnV1 = new AutumnInt({
|
||||
secretKey: ctx.orgSecretKey,
|
||||
version: ApiVersion.V1_2,
|
||||
});
|
||||
|
||||
let stripeCli: Stripe;
|
||||
let testClockId: string;
|
||||
let customer: Customer;
|
||||
@@ -22,6 +56,15 @@ describe(`${chalk.yellowBright("basic6: Testing subscription past_due")}`, () =>
|
||||
beforeAll(async () => {
|
||||
stripeCli = ctx.stripeCli;
|
||||
|
||||
// Create products FIRST before customer creation
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [proProd],
|
||||
prefix: testCase,
|
||||
customerId,
|
||||
});
|
||||
|
||||
// Then create customer with payment method
|
||||
const result = await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
@@ -33,9 +76,9 @@ describe(`${chalk.yellowBright("basic6: Testing subscription past_due")}`, () =>
|
||||
});
|
||||
|
||||
test("should attach pro product and switch to failed payment method", async () => {
|
||||
await AutumnCli.attach({
|
||||
customerId: customerId,
|
||||
productId: products.pro.id,
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: proProd.id,
|
||||
});
|
||||
|
||||
await attachFailedPaymentMethod({
|
||||
@@ -59,7 +102,7 @@ describe(`${chalk.yellowBright("basic6: Testing subscription past_due")}`, () =>
|
||||
test("should have pro product in past due status", async () => {
|
||||
const cusRes: any = await AutumnCli.getCustomer(customerId);
|
||||
const proProduct = cusRes.products.find(
|
||||
(p: any) => p.id === products.pro.id,
|
||||
(p: any) => p.id === proProd.id,
|
||||
);
|
||||
expect(proProduct).toBeDefined();
|
||||
expect(proProduct.status).toBe(CusProductStatus.PastDue);
|
||||
|
||||
@@ -1,21 +1,69 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { CusProductStatus } from "@autumn/shared";
|
||||
import {
|
||||
ApiVersion,
|
||||
CusProductStatus,
|
||||
type FixedPriceConfig,
|
||||
FreeTrialDuration,
|
||||
ProductItemInterval,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||
import { products } from "tests/global.js";
|
||||
import { compareMainProduct } from "tests/utils/compare.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js";
|
||||
import { timeout } from "tests/utils/genUtils.js";
|
||||
import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { convertProductV2ToV1 } from "@/internal/products/productUtils/productV2Utils/convertProductV2ToV1.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
|
||||
// Pro product with trial (matches global products.proWithTrial)
|
||||
const proWithTrial = constructProduct({
|
||||
type: "pro",
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Dashboard,
|
||||
isBoolean: true,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 10,
|
||||
interval: ProductItemInterval.Month,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Admin,
|
||||
unlimited: true,
|
||||
}),
|
||||
],
|
||||
freeTrial: {
|
||||
length: 7,
|
||||
duration: FreeTrialDuration.Day,
|
||||
unique_fingerprint: true,
|
||||
card_required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const testCase = "basic7";
|
||||
const customerId = testCase;
|
||||
|
||||
describe(`${chalk.yellowBright("basic7: Testing trial duplicates (same customer)")}`, () => {
|
||||
const customerId = testCase;
|
||||
const autumn = new AutumnInt();
|
||||
const autumnV1 = new AutumnInt({
|
||||
secretKey: ctx.orgSecretKey,
|
||||
version: ApiVersion.V1_2,
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create products FIRST before customer creation
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [proWithTrial],
|
||||
prefix: testCase,
|
||||
customerId,
|
||||
});
|
||||
|
||||
// Then create customer with payment method
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
@@ -25,15 +73,15 @@ describe(`${chalk.yellowBright("basic7: Testing trial duplicates (same customer)
|
||||
});
|
||||
|
||||
test("should attach pro with trial and have correct product & invoice", async () => {
|
||||
await AutumnCli.attach({
|
||||
customerId: customerId,
|
||||
productId: products.proWithTrial.id,
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: proWithTrial.id,
|
||||
});
|
||||
|
||||
const customer = await AutumnCli.getCustomer(customerId);
|
||||
|
||||
compareMainProduct({
|
||||
sent: products.proWithTrial,
|
||||
await expectCustomerV0Correct({
|
||||
sent: proWithTrial,
|
||||
cusRes: customer,
|
||||
status: CusProductStatus.Trialing,
|
||||
});
|
||||
@@ -44,29 +92,39 @@ describe(`${chalk.yellowBright("basic7: Testing trial duplicates (same customer)
|
||||
});
|
||||
|
||||
test("should cancel pro with trial", async () => {
|
||||
await autumn.cancel({
|
||||
await autumnV1.cancel({
|
||||
customer_id: customerId,
|
||||
product_id: products.proWithTrial.id,
|
||||
product_id: proWithTrial.id,
|
||||
cancel_immediately: true,
|
||||
});
|
||||
await timeout(5000);
|
||||
});
|
||||
|
||||
test("should be able to attach pro with trial again (renewal flow)", async () => {
|
||||
await AutumnCli.attach({
|
||||
customerId: customerId,
|
||||
productId: products.proWithTrial.id,
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: proWithTrial.id,
|
||||
});
|
||||
|
||||
const customer = await AutumnCli.getCustomer(customerId);
|
||||
|
||||
compareMainProduct({
|
||||
sent: products.proWithTrial,
|
||||
await expectCustomerV0Correct({
|
||||
sent: proWithTrial,
|
||||
cusRes: customer,
|
||||
});
|
||||
|
||||
const invoices = customer.invoices;
|
||||
expect(invoices.length).toBe(2);
|
||||
expect(invoices[0].amount).toBe(products.proWithTrial.prices[0].amount);
|
||||
|
||||
// Get price from converted product
|
||||
const proWithTrialV1 = convertProductV2ToV1({
|
||||
productV2: proWithTrial,
|
||||
orgId: ctx.org.id,
|
||||
features: ctx.features,
|
||||
});
|
||||
|
||||
expect(invoices[0].total).toBe(
|
||||
(proWithTrialV1.prices[0].config as FixedPriceConfig).amount,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,22 +1,69 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { CusProductStatus } from "@autumn/shared";
|
||||
import {
|
||||
ApiVersion,
|
||||
CusProductStatus,
|
||||
FreeTrialDuration,
|
||||
ProductItemInterval,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||
import { products } from "tests/global.js";
|
||||
import { compareMainProduct } from "tests/utils/compare.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js";
|
||||
import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
|
||||
// Pro product with trial (matches global products.proWithTrial)
|
||||
const proWithTrial = constructProduct({
|
||||
type: "pro",
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Dashboard,
|
||||
isBoolean: true,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 10,
|
||||
interval: ProductItemInterval.Month,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Admin,
|
||||
unlimited: true,
|
||||
}),
|
||||
],
|
||||
freeTrial: {
|
||||
length: 7,
|
||||
duration: FreeTrialDuration.Day,
|
||||
unique_fingerprint: true,
|
||||
card_required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const testCase = "basic8";
|
||||
const customerId = testCase;
|
||||
const customerId2 = `${testCase}2`;
|
||||
|
||||
describe(`${chalk.yellowBright("basic8: Testing trial duplicates (same fingerprint)")}`, () => {
|
||||
const customerId = testCase;
|
||||
const customerId2 = testCase + "2";
|
||||
const autumn = new AutumnInt();
|
||||
const autumnV1 = new AutumnInt({
|
||||
secretKey: ctx.orgSecretKey,
|
||||
version: ApiVersion.V1_2,
|
||||
});
|
||||
|
||||
const randFingerprint = Math.random().toString(36).substring(2, 15);
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create products FIRST before customer creation
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [proWithTrial],
|
||||
prefix: testCase,
|
||||
customerIds: [customerId, customerId2],
|
||||
});
|
||||
|
||||
// Create first customer with fingerprint
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
@@ -25,6 +72,7 @@ describe(`${chalk.yellowBright("basic8: Testing trial duplicates (same fingerpri
|
||||
withTestClock: true,
|
||||
});
|
||||
|
||||
// Create second customer with same fingerprint
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId: customerId2,
|
||||
@@ -35,15 +83,15 @@ describe(`${chalk.yellowBright("basic8: Testing trial duplicates (same fingerpri
|
||||
});
|
||||
|
||||
test("should attach pro with trial and have correct product & invoice", async () => {
|
||||
await AutumnCli.attach({
|
||||
customerId: customerId,
|
||||
productId: products.proWithTrial.id,
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: proWithTrial.id,
|
||||
});
|
||||
|
||||
const customer = await AutumnCli.getCustomer(customerId);
|
||||
|
||||
compareMainProduct({
|
||||
sent: products.proWithTrial,
|
||||
await expectCustomerV0Correct({
|
||||
sent: proWithTrial,
|
||||
cusRes: customer,
|
||||
status: CusProductStatus.Trialing,
|
||||
});
|
||||
@@ -54,21 +102,21 @@ describe(`${chalk.yellowBright("basic8: Testing trial duplicates (same fingerpri
|
||||
});
|
||||
|
||||
test("should attach pro with trial to second customer and have correct product & invoice (pro with trial, full price)", async () => {
|
||||
await autumn.attach({
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId2,
|
||||
product_id: products.proWithTrial.id,
|
||||
product_id: proWithTrial.id,
|
||||
});
|
||||
|
||||
const customer = await AutumnCli.getCustomer(customerId2);
|
||||
|
||||
compareMainProduct({
|
||||
sent: products.proWithTrial,
|
||||
await expectCustomerV0Correct({
|
||||
sent: proWithTrial,
|
||||
cusRes: customer,
|
||||
status: CusProductStatus.Active,
|
||||
});
|
||||
|
||||
const invoices = customer.invoices;
|
||||
expect(invoices.length).toBe(1);
|
||||
expect(invoices[0].total).toBe(10);
|
||||
expect(invoices[0].total).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { beforeAll, describe, test } from "bun:test";
|
||||
import chalk from "chalk";
|
||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||
import { features, products } from "tests/global.js";
|
||||
import { timeout } from "tests/utils/genUtils.js";
|
||||
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
|
||||
import { compareMainProduct } from "tests/utils/compare.js";
|
||||
import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
|
||||
const testCase = "basic9";
|
||||
|
||||
describe(`${chalk.yellowBright("basic9: attach monthly with one time prepaid, and quantity = 0")}`, () => {
|
||||
const customerId = testCase;
|
||||
|
||||
const options = [
|
||||
{
|
||||
feature_id: features.metered1.id,
|
||||
quantity: 0,
|
||||
},
|
||||
{
|
||||
feature_id: features.metered2.id,
|
||||
quantity: 4,
|
||||
},
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
withTestClock: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach monthly with one time", async () => {
|
||||
const res = await AutumnCli.attach({
|
||||
customerId,
|
||||
productId: products.monthlyWithOneTime.id,
|
||||
options,
|
||||
});
|
||||
|
||||
await completeCheckoutForm(res.checkout_url);
|
||||
await timeout(12000);
|
||||
});
|
||||
|
||||
test("should have correct main product and entitlements", async () => {
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
|
||||
compareMainProduct({
|
||||
sent: products.monthlyWithOneTime,
|
||||
cusRes,
|
||||
optionsList: options,
|
||||
});
|
||||
});
|
||||
});
|
||||
51
server/tests/attach/basic/sharedProducts.ts
Normal file
51
server/tests/attach/basic/sharedProducts.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ProductItemInterval } from "@autumn/shared";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { createSharedProducts } from "@/utils/scriptUtils/testUtils/createSharedProduct.js";
|
||||
/**
|
||||
* Shared default product for basic test group
|
||||
* Used by multiple tests (basic1, basic3) to avoid conflicts
|
||||
* ID is NOT prefixed - shared across all tests in this group
|
||||
*/
|
||||
export const sharedDefaultFree = constructProduct({
|
||||
id: "shared-default-free",
|
||||
type: "free",
|
||||
isDefault: true,
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 5,
|
||||
interval: ProductItemInterval.Month,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export const sharedProProduct = constructProduct({
|
||||
id: "shared-pro-product",
|
||||
isDefault: false,
|
||||
type: "pro",
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Dashboard,
|
||||
isBoolean: true,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 10,
|
||||
interval: ProductItemInterval.Month,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Admin,
|
||||
unlimited: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
await (async () => {
|
||||
await createSharedProducts({
|
||||
ctx,
|
||||
products: [sharedDefaultFree, sharedProProduct],
|
||||
});
|
||||
})();
|
||||
128
server/tests/attach/checkout/checkout1.test.ts
Normal file
128
server/tests/attach/checkout/checkout1.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
ApiVersion,
|
||||
type CheckResponseV0,
|
||||
type Entitlement,
|
||||
ProductItemInterval,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js";
|
||||
import { timeout } from "tests/utils/genUtils.js";
|
||||
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
|
||||
import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { convertProductV2ToV1 } from "@/internal/products/productUtils/productV2Utils/convertProductV2ToV1.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
|
||||
// Pro product with boolean, metered, and unlimited features
|
||||
const proProd = constructProduct({
|
||||
type: "pro",
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Dashboard,
|
||||
isBoolean: true,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 10,
|
||||
interval: ProductItemInterval.Month,
|
||||
}),
|
||||
// Unlimited feature (maps to global products.pro.infinite1)
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Admin,
|
||||
unlimited: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const testCase = "checkout1";
|
||||
const customerId = testCase;
|
||||
|
||||
describe(`${chalk.yellowBright("checkout1: Testing attach basic product through checkout")}`, () => {
|
||||
const autumnV1 = new AutumnInt({
|
||||
secretKey: ctx.orgSecretKey,
|
||||
version: ApiVersion.V1_2,
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [proProd],
|
||||
prefix: testCase,
|
||||
customerId,
|
||||
});
|
||||
|
||||
// Then create customer
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData: { fingerprint: "test" },
|
||||
withTestClock: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach pro through checkout", async () => {
|
||||
const { checkout_url } = await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: proProd.id,
|
||||
});
|
||||
|
||||
await completeCheckoutForm(checkout_url);
|
||||
await timeout(12000);
|
||||
});
|
||||
|
||||
test("should have correct product & entitlements", async () => {
|
||||
const res = await AutumnCli.getCustomer(customerId);
|
||||
|
||||
await expectCustomerV0Correct({
|
||||
sent: proProd,
|
||||
cusRes: res,
|
||||
});
|
||||
expect(res.invoices.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("should have correct result when calling /check", async () => {
|
||||
// Convert ProductV2 to V1 to get reference entitlements (what we SENT)
|
||||
const proProdV1 = convertProductV2ToV1({
|
||||
productV2: proProd,
|
||||
orgId: ctx.org.id,
|
||||
features: ctx.features,
|
||||
});
|
||||
const proEntitlements = proProdV1.entitlements;
|
||||
|
||||
// Iterate through reference product's entitlements and verify check responses
|
||||
for (const entitlement of Object.values(proEntitlements) as Entitlement[]) {
|
||||
const allowance = entitlement.allowance;
|
||||
|
||||
const res = (await AutumnCli.entitled(
|
||||
customerId,
|
||||
entitlement.feature_id!,
|
||||
)) as CheckResponseV0;
|
||||
|
||||
const entBalance = res.balances.find(
|
||||
(b) => b.feature_id === entitlement.feature_id,
|
||||
);
|
||||
|
||||
expect(
|
||||
res.allowed,
|
||||
`Allowed for ${entitlement.feature_id} is not true`,
|
||||
).toBe(true);
|
||||
expect(
|
||||
entBalance,
|
||||
`Entitlement ${entitlement.feature_id} balance not found`,
|
||||
).toBeDefined();
|
||||
if (entitlement.allowance) {
|
||||
expect(
|
||||
entBalance?.balance,
|
||||
`Entitlement ${entitlement.feature_id} balance does not match expected balance.`,
|
||||
).toBe(allowance);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
159
server/tests/attach/checkout/checkout2.test.ts
Normal file
159
server/tests/attach/checkout/checkout2.test.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
ApiVersion,
|
||||
type CheckResponseV0,
|
||||
type LimitedItem,
|
||||
ProductItemInterval,
|
||||
} from "@autumn/shared";
|
||||
import type { ApiCustomerV1 } from "@shared/api/customers/previousVersions/apiCustomerV1.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js";
|
||||
import { timeout } from "tests/utils/genUtils.js";
|
||||
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
|
||||
import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import {
|
||||
constructFeatureItem,
|
||||
constructPrepaidItem,
|
||||
} from "@/utils/scriptUtils/constructItem.js";
|
||||
import {
|
||||
constructProduct,
|
||||
constructRawProduct,
|
||||
} from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
|
||||
// Pro product (matches global products.pro)
|
||||
const proProd = constructProduct({
|
||||
type: "pro",
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Dashboard,
|
||||
isBoolean: true,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 10,
|
||||
interval: ProductItemInterval.Month,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Admin,
|
||||
unlimited: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// One-time add-on product
|
||||
const oneTimeItem = constructPrepaidItem({
|
||||
featureId: TestFeature.Messages,
|
||||
price: 9,
|
||||
billingUnits: 250,
|
||||
isOneOff: true,
|
||||
}) as LimitedItem;
|
||||
|
||||
const oneTime = constructRawProduct({
|
||||
id: "one_off",
|
||||
items: [oneTimeItem],
|
||||
isAddOn: true,
|
||||
});
|
||||
|
||||
const testCase = "checkout2";
|
||||
const customerId = testCase;
|
||||
|
||||
describe(`${chalk.yellowBright("checkout2: Testing attach one time add ons (through checkout)")}`, () => {
|
||||
const autumnV1 = new AutumnInt({
|
||||
secretKey: ctx.orgSecretKey,
|
||||
version: ApiVersion.V1_2,
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create products FIRST before customer creation
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [proProd, oneTime],
|
||||
prefix: testCase,
|
||||
customerId,
|
||||
});
|
||||
|
||||
// Then create customer with payment method
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
attachPm: "success",
|
||||
withTestClock: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach pro", async () => {
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: proProd.id,
|
||||
});
|
||||
|
||||
const res = await AutumnCli.getCustomer(customerId);
|
||||
await expectCustomerV0Correct({
|
||||
sent: proProd,
|
||||
cusRes: res,
|
||||
});
|
||||
});
|
||||
|
||||
const oneTimeQuantity = 500;
|
||||
const oneTimeBillingUnits = oneTimeItem.billing_units;
|
||||
const oneTimePurchaseCount = 2;
|
||||
|
||||
test("should attach one time add on twice, force checkout", async () => {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const res = await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: oneTime.id,
|
||||
force_checkout: true,
|
||||
});
|
||||
|
||||
await completeCheckoutForm(
|
||||
res.checkout_url,
|
||||
oneTimeQuantity / (oneTimeBillingUnits ?? 1),
|
||||
);
|
||||
await timeout(15000);
|
||||
}
|
||||
});
|
||||
|
||||
test("should have correct product & entitlements", async () => {
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
|
||||
// Find the add-on balance for Messages with lifetime interval (one-time purchase)
|
||||
const addOnBalance = cusRes.entitlements.find(
|
||||
(e: ApiCustomerV1["entitlements"][number]) =>
|
||||
e.feature_id === TestFeature.Messages && e.interval === "lifetime",
|
||||
);
|
||||
|
||||
const expectedAmt = oneTimeQuantity * oneTimePurchaseCount;
|
||||
|
||||
expect(addOnBalance?.balance).toBe(expectedAmt);
|
||||
|
||||
expect(cusRes.add_ons).toHaveLength(1);
|
||||
expect(cusRes.add_ons[0].id).toBe(oneTime.id);
|
||||
expect(cusRes.invoices.length).toBe(1 + oneTimePurchaseCount);
|
||||
});
|
||||
|
||||
test("should have correct /check result for metered1", async () => {
|
||||
const res = (await AutumnCli.entitled(
|
||||
customerId,
|
||||
TestFeature.Messages,
|
||||
)) as CheckResponseV0;
|
||||
|
||||
expect(res.allowed).toBe(true);
|
||||
|
||||
// Pro product gives 10 Messages per month
|
||||
const proMetered1Amt = 10;
|
||||
const addOnBalance = res.balances.find(
|
||||
(b: CheckResponseV0["balances"][number]) =>
|
||||
b.feature_id === TestFeature.Messages,
|
||||
);
|
||||
|
||||
expect(addOnBalance?.balance).toBe(
|
||||
proMetered1Amt + oneTimeQuantity * oneTimePurchaseCount,
|
||||
);
|
||||
});
|
||||
});
|
||||
95
server/tests/attach/checkout/checkout8.test.ts
Normal file
95
server/tests/attach/checkout/checkout8.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { beforeAll, describe, test } from "bun:test";
|
||||
import { ApiVersion } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js";
|
||||
import { timeout } from "tests/utils/genUtils.js";
|
||||
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
|
||||
import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
|
||||
// Monthly with one-time prepaid product (matches global products.monthlyWithOneTime)
|
||||
// Has both monthly and one-time prepaid items
|
||||
const monthlyWithOneTime = constructProduct({
|
||||
type: "pro",
|
||||
items: [
|
||||
constructPrepaidItem({
|
||||
featureId: TestFeature.Messages,
|
||||
price: 5,
|
||||
billingUnits: 100,
|
||||
includedUsage: 0,
|
||||
isOneOff: true,
|
||||
}),
|
||||
constructPrepaidItem({
|
||||
featureId: TestFeature.Words,
|
||||
price: 10,
|
||||
billingUnits: 100,
|
||||
includedUsage: 0,
|
||||
isOneOff: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const testCase = "checkout8";
|
||||
const customerId = testCase;
|
||||
|
||||
describe(`${chalk.yellowBright("checkout8: attach monthly with one time prepaid, and quantity = 0")}`, () => {
|
||||
const autumnV1 = new AutumnInt({
|
||||
secretKey: ctx.orgSecretKey,
|
||||
version: ApiVersion.V1_2,
|
||||
});
|
||||
|
||||
const options = [
|
||||
{
|
||||
feature_id: TestFeature.Messages,
|
||||
quantity: 0,
|
||||
},
|
||||
{
|
||||
feature_id: TestFeature.Words,
|
||||
quantity: 4,
|
||||
},
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create products FIRST before customer creation
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [monthlyWithOneTime],
|
||||
prefix: testCase,
|
||||
customerId,
|
||||
});
|
||||
|
||||
// Then create customer
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
withTestClock: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach monthly with one time", async () => {
|
||||
const res = await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: monthlyWithOneTime.id,
|
||||
options,
|
||||
});
|
||||
|
||||
await completeCheckoutForm(res.checkout_url);
|
||||
await timeout(12000);
|
||||
});
|
||||
|
||||
test("should have correct main product and entitlements", async () => {
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
|
||||
await expectCustomerV0Correct({
|
||||
sent: monthlyWithOneTime,
|
||||
cusRes,
|
||||
optionsList: options,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"
|
||||
|
||||
const testCase = "upgrade3";
|
||||
|
||||
export const pro = constructProduct({
|
||||
const pro = constructProduct({
|
||||
items: [
|
||||
constructArrearProratedItem({
|
||||
featureId: TestFeature.Users,
|
||||
@@ -27,7 +27,7 @@ export const pro = constructProduct({
|
||||
type: "pro",
|
||||
});
|
||||
|
||||
export const premium = constructProduct({
|
||||
const premium = constructProduct({
|
||||
items: [
|
||||
constructArrearProratedItem({
|
||||
featureId: TestFeature.Users,
|
||||
@@ -37,7 +37,7 @@ export const premium = constructProduct({
|
||||
type: "premium",
|
||||
});
|
||||
|
||||
export const proAnnual = constructProduct({
|
||||
const proAnnual = constructProduct({
|
||||
items: [
|
||||
constructArrearProratedItem({
|
||||
featureId: TestFeature.Users,
|
||||
|
||||
@@ -1,102 +1,102 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
ApiVersion,
|
||||
type CheckResponse,
|
||||
type CheckResponseV0,
|
||||
type LimitedItem,
|
||||
SuccessCode,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
// import { beforeAll, describe, expect, test } from "bun:test";
|
||||
// import {
|
||||
// ApiVersion,
|
||||
// type CheckResponse,
|
||||
// type CheckResponseV0,
|
||||
// type LimitedItem,
|
||||
// SuccessCode,
|
||||
// } from "@autumn/shared";
|
||||
// import chalk from "chalk";
|
||||
// import { TestFeature } from "tests/setup/v2Features.js";
|
||||
// import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
// import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
// import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
// import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
// import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
// import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
|
||||
const messagesFeature = constructArrearItem({
|
||||
featureId: TestFeature.Messages,
|
||||
price: 0.5,
|
||||
includedUsage: 100,
|
||||
usageLimit: 500,
|
||||
}) as LimitedItem;
|
||||
// const messagesFeature = constructArrearItem({
|
||||
// featureId: TestFeature.Messages,
|
||||
// price: 0.5,
|
||||
// includedUsage: 100,
|
||||
// usageLimit: 500,
|
||||
// }) as LimitedItem;
|
||||
|
||||
const proProd = constructProduct({
|
||||
type: "pro",
|
||||
isDefault: false,
|
||||
items: [messagesFeature],
|
||||
});
|
||||
// const proProd = constructProduct({
|
||||
// type: "pro",
|
||||
// isDefault: false,
|
||||
// items: [messagesFeature],
|
||||
// });
|
||||
|
||||
const testCase = "check7";
|
||||
// const testCase = "check7";
|
||||
|
||||
describe(`${chalk.yellowBright("check7: test /check with required balance")}`, () => {
|
||||
const customerId = "check7";
|
||||
const autumnV0: AutumnInt = new AutumnInt({ version: ApiVersion.V0_2 });
|
||||
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
// describe(`${chalk.yellowBright("check7: test /check with required balance")}`, () => {
|
||||
// const customerId = "check7";
|
||||
// const autumnV0: AutumnInt = new AutumnInt({ version: ApiVersion.V0_2 });
|
||||
// const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
attachPm: "success",
|
||||
withTestClock: false,
|
||||
});
|
||||
// beforeAll(async () => {
|
||||
// await initCustomerV3({
|
||||
// ctx,
|
||||
// customerId,
|
||||
// attachPm: "success",
|
||||
// withTestClock: false,
|
||||
// });
|
||||
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [proProd],
|
||||
prefix: testCase,
|
||||
});
|
||||
// await initProductsV0({
|
||||
// ctx,
|
||||
// products: [proProd],
|
||||
// prefix: testCase,
|
||||
// });
|
||||
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: proProd.id,
|
||||
});
|
||||
});
|
||||
// await autumnV1.attach({
|
||||
// customer_id: customerId,
|
||||
// product_id: proProd.id,
|
||||
// });
|
||||
// });
|
||||
|
||||
test("v0 response", async () => {
|
||||
const res = (await autumnV0.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
required_balance: messagesFeature.usage_limit! + 1,
|
||||
})) as unknown as CheckResponseV0;
|
||||
// test("v0 response", async () => {
|
||||
// const res = (await autumnV0.check({
|
||||
// customer_id: customerId,
|
||||
// feature_id: TestFeature.Messages,
|
||||
// required_balance: messagesFeature.usage_limit! + 1,
|
||||
// })) as unknown as CheckResponseV0;
|
||||
|
||||
expect(res.allowed).toBe(false);
|
||||
expect(res.balances).toBeDefined();
|
||||
expect(res.balances).toHaveLength(1);
|
||||
expect(res.balances[0]).toMatchObject({
|
||||
balance: messagesFeature.included_usage,
|
||||
required: messagesFeature.usage_limit! + 1,
|
||||
feature_id: TestFeature.Messages,
|
||||
});
|
||||
});
|
||||
// expect(res.allowed).toBe(false);
|
||||
// expect(res.balances).toBeDefined();
|
||||
// expect(res.balances).toHaveLength(1);
|
||||
// expect(res.balances[0]).toMatchObject({
|
||||
// balance: messagesFeature.included_usage,
|
||||
// required: messagesFeature.usage_limit! + 1,
|
||||
// feature_id: TestFeature.Messages,
|
||||
// });
|
||||
// });
|
||||
|
||||
test("v1 response", async () => {
|
||||
const res = (await autumnV1.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
required_balance: messagesFeature.usage_limit! + 1,
|
||||
})) as unknown as CheckResponse;
|
||||
// test("v1 response", async () => {
|
||||
// const res = (await autumnV1.check({
|
||||
// customer_id: customerId,
|
||||
// feature_id: TestFeature.Messages,
|
||||
// required_balance: messagesFeature.usage_limit! + 1,
|
||||
// })) as unknown as CheckResponse;
|
||||
|
||||
const expectedRes = {
|
||||
allowed: false,
|
||||
customer_id: customerId,
|
||||
balance: messagesFeature.included_usage,
|
||||
feature_id: TestFeature.Messages as string,
|
||||
required_balance: messagesFeature.usage_limit! + 1,
|
||||
code: SuccessCode.FeatureFound,
|
||||
unlimited: false,
|
||||
usage: 0,
|
||||
included_usage: messagesFeature.included_usage,
|
||||
overage_allowed: false,
|
||||
// const expectedRes = {
|
||||
// allowed: false,
|
||||
// customer_id: customerId,
|
||||
// balance: messagesFeature.included_usage,
|
||||
// feature_id: TestFeature.Messages as string,
|
||||
// required_balance: messagesFeature.usage_limit! + 1,
|
||||
// code: SuccessCode.FeatureFound,
|
||||
// unlimited: false,
|
||||
// usage: 0,
|
||||
// included_usage: messagesFeature.included_usage,
|
||||
// overage_allowed: false,
|
||||
|
||||
usage_limit: messagesFeature.usage_limit!,
|
||||
interval: "month",
|
||||
interval_count: 1,
|
||||
};
|
||||
// usage_limit: messagesFeature.usage_limit!,
|
||||
// interval: "month",
|
||||
// interval_count: 1,
|
||||
// };
|
||||
|
||||
expect(res).toMatchObject(expectedRes);
|
||||
expect(res.next_reset_at).toBeDefined();
|
||||
});
|
||||
});
|
||||
// expect(res).toMatchObject(expectedRes);
|
||||
// expect(res.next_reset_at).toBeDefined();
|
||||
// });
|
||||
// });
|
||||
|
||||
@@ -1,102 +1,102 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
ApiVersion,
|
||||
type CheckResponse,
|
||||
type CheckResponseV0,
|
||||
type LimitedItem,
|
||||
SuccessCode,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
// import { beforeAll, describe, expect, test } from "bun:test";
|
||||
// import {
|
||||
// ApiVersion,
|
||||
// type CheckResponse,
|
||||
// type CheckResponseV0,
|
||||
// type LimitedItem,
|
||||
// SuccessCode,
|
||||
// } from "@autumn/shared";
|
||||
// import chalk from "chalk";
|
||||
// import { TestFeature } from "tests/setup/v2Features.js";
|
||||
// import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
// import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
// import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
// import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
// import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
// import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
|
||||
const messagesFeature = constructArrearItem({
|
||||
featureId: TestFeature.Messages,
|
||||
price: 0.5,
|
||||
includedUsage: 100,
|
||||
usageLimit: 500,
|
||||
}) as LimitedItem;
|
||||
// const messagesFeature = constructArrearItem({
|
||||
// featureId: TestFeature.Messages,
|
||||
// price: 0.5,
|
||||
// includedUsage: 100,
|
||||
// usageLimit: 500,
|
||||
// }) as LimitedItem;
|
||||
|
||||
const proProd = constructProduct({
|
||||
type: "pro",
|
||||
isDefault: false,
|
||||
items: [messagesFeature],
|
||||
});
|
||||
// const proProd = constructProduct({
|
||||
// type: "pro",
|
||||
// isDefault: false,
|
||||
// items: [messagesFeature],
|
||||
// });
|
||||
|
||||
const testCase = "check7";
|
||||
// const testCase = "check7";
|
||||
|
||||
describe(`${chalk.yellowBright("check7: test /check on feature with credit system")}`, () => {
|
||||
const customerId = "check7";
|
||||
const autumnV0: AutumnInt = new AutumnInt({ version: ApiVersion.V0_2 });
|
||||
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
// describe(`${chalk.yellowBright("check7: test /check on feature with credit system")}`, () => {
|
||||
// const customerId = "check7";
|
||||
// const autumnV0: AutumnInt = new AutumnInt({ version: ApiVersion.V0_2 });
|
||||
// const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
attachPm: "success",
|
||||
withTestClock: false,
|
||||
});
|
||||
// beforeAll(async () => {
|
||||
// await initCustomerV3({
|
||||
// ctx,
|
||||
// customerId,
|
||||
// attachPm: "success",
|
||||
// withTestClock: false,
|
||||
// });
|
||||
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [proProd],
|
||||
prefix: testCase,
|
||||
});
|
||||
// await initProductsV0({
|
||||
// ctx,
|
||||
// products: [proProd],
|
||||
// prefix: testCase,
|
||||
// });
|
||||
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: proProd.id,
|
||||
});
|
||||
});
|
||||
// await autumnV1.attach({
|
||||
// customer_id: customerId,
|
||||
// product_id: proProd.id,
|
||||
// });
|
||||
// });
|
||||
|
||||
test("v0 response", async () => {
|
||||
const res = (await autumnV0.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
required_balance: messagesFeature.usage_limit! + 1,
|
||||
})) as unknown as CheckResponseV0;
|
||||
// test("v0 response", async () => {
|
||||
// const res = (await autumnV0.check({
|
||||
// customer_id: customerId,
|
||||
// feature_id: TestFeature.Messages,
|
||||
// required_balance: messagesFeature.usage_limit! + 1,
|
||||
// })) as unknown as CheckResponseV0;
|
||||
|
||||
expect(res.allowed).toBe(false);
|
||||
expect(res.balances).toBeDefined();
|
||||
expect(res.balances).toHaveLength(1);
|
||||
expect(res.balances[0]).toMatchObject({
|
||||
balance: messagesFeature.included_usage,
|
||||
required: messagesFeature.usage_limit! + 1,
|
||||
feature_id: TestFeature.Messages,
|
||||
});
|
||||
});
|
||||
// expect(res.allowed).toBe(false);
|
||||
// expect(res.balances).toBeDefined();
|
||||
// expect(res.balances).toHaveLength(1);
|
||||
// expect(res.balances[0]).toMatchObject({
|
||||
// balance: messagesFeature.included_usage,
|
||||
// required: messagesFeature.usage_limit! + 1,
|
||||
// feature_id: TestFeature.Messages,
|
||||
// });
|
||||
// });
|
||||
|
||||
test("v1 response", async () => {
|
||||
const res = (await autumnV1.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
required_balance: messagesFeature.usage_limit! + 1,
|
||||
})) as unknown as CheckResponse;
|
||||
// test("v1 response", async () => {
|
||||
// const res = (await autumnV1.check({
|
||||
// customer_id: customerId,
|
||||
// feature_id: TestFeature.Messages,
|
||||
// required_balance: messagesFeature.usage_limit! + 1,
|
||||
// })) as unknown as CheckResponse;
|
||||
|
||||
const expectedRes = {
|
||||
allowed: false,
|
||||
customer_id: customerId,
|
||||
balance: messagesFeature.included_usage,
|
||||
feature_id: TestFeature.Messages as string,
|
||||
required_balance: messagesFeature.usage_limit! + 1,
|
||||
code: SuccessCode.FeatureFound,
|
||||
unlimited: false,
|
||||
usage: 0,
|
||||
included_usage: messagesFeature.included_usage,
|
||||
overage_allowed: false,
|
||||
// const expectedRes = {
|
||||
// allowed: false,
|
||||
// customer_id: customerId,
|
||||
// balance: messagesFeature.included_usage,
|
||||
// feature_id: TestFeature.Messages as string,
|
||||
// required_balance: messagesFeature.usage_limit! + 1,
|
||||
// code: SuccessCode.FeatureFound,
|
||||
// unlimited: false,
|
||||
// usage: 0,
|
||||
// included_usage: messagesFeature.included_usage,
|
||||
// overage_allowed: false,
|
||||
|
||||
usage_limit: messagesFeature.usage_limit!,
|
||||
interval: "month",
|
||||
interval_count: 1,
|
||||
};
|
||||
// usage_limit: messagesFeature.usage_limit!,
|
||||
// interval: "month",
|
||||
// interval_count: 1,
|
||||
// };
|
||||
|
||||
expect(res).toMatchObject(expectedRes);
|
||||
expect(res.next_reset_at).toBeDefined();
|
||||
});
|
||||
});
|
||||
// expect(res).toMatchObject(expectedRes);
|
||||
// expect(res.next_reset_at).toBeDefined();
|
||||
// });
|
||||
// });
|
||||
|
||||
@@ -1,102 +1,102 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
ApiVersion,
|
||||
type CheckResponse,
|
||||
type CheckResponseV0,
|
||||
type LimitedItem,
|
||||
SuccessCode,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
// import { beforeAll, describe, expect, test } from "bun:test";
|
||||
// import {
|
||||
// ApiVersion,
|
||||
// type CheckResponse,
|
||||
// type CheckResponseV0,
|
||||
// type LimitedItem,
|
||||
// SuccessCode,
|
||||
// } from "@autumn/shared";
|
||||
// import chalk from "chalk";
|
||||
// import { TestFeature } from "tests/setup/v2Features.js";
|
||||
// import ctx from "tests/utils/testInitUtils/createTestContext.js";
|
||||
// import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
// import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
// import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
// import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
// import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
|
||||
const messagesFeature = constructArrearItem({
|
||||
featureId: TestFeature.Messages,
|
||||
price: 0.5,
|
||||
includedUsage: 100,
|
||||
usageLimit: 500,
|
||||
}) as LimitedItem;
|
||||
// const messagesFeature = constructArrearItem({
|
||||
// featureId: TestFeature.Messages,
|
||||
// price: 0.5,
|
||||
// includedUsage: 100,
|
||||
// usageLimit: 500,
|
||||
// }) as LimitedItem;
|
||||
|
||||
const proProd = constructProduct({
|
||||
type: "pro",
|
||||
isDefault: false,
|
||||
items: [messagesFeature],
|
||||
});
|
||||
// const proProd = constructProduct({
|
||||
// type: "pro",
|
||||
// isDefault: false,
|
||||
// items: [messagesFeature],
|
||||
// });
|
||||
|
||||
const testCase = "check7";
|
||||
// const testCase = "check7";
|
||||
|
||||
describe(`${chalk.yellowBright("check7: test /check on credit system (alone)")}`, () => {
|
||||
const customerId = "check7";
|
||||
const autumnV0: AutumnInt = new AutumnInt({ version: ApiVersion.V0_2 });
|
||||
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
// describe(`${chalk.yellowBright("check7: test /check on credit system (alone)")}`, () => {
|
||||
// const customerId = "check7";
|
||||
// const autumnV0: AutumnInt = new AutumnInt({ version: ApiVersion.V0_2 });
|
||||
// const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
attachPm: "success",
|
||||
withTestClock: false,
|
||||
});
|
||||
// beforeAll(async () => {
|
||||
// await initCustomerV3({
|
||||
// ctx,
|
||||
// customerId,
|
||||
// attachPm: "success",
|
||||
// withTestClock: false,
|
||||
// });
|
||||
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [proProd],
|
||||
prefix: testCase,
|
||||
});
|
||||
// await initProductsV0({
|
||||
// ctx,
|
||||
// products: [proProd],
|
||||
// prefix: testCase,
|
||||
// });
|
||||
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: proProd.id,
|
||||
});
|
||||
});
|
||||
// await autumnV1.attach({
|
||||
// customer_id: customerId,
|
||||
// product_id: proProd.id,
|
||||
// });
|
||||
// });
|
||||
|
||||
test("v0 response", async () => {
|
||||
const res = (await autumnV0.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
required_balance: messagesFeature.usage_limit! + 1,
|
||||
})) as unknown as CheckResponseV0;
|
||||
// test("v0 response", async () => {
|
||||
// const res = (await autumnV0.check({
|
||||
// customer_id: customerId,
|
||||
// feature_id: TestFeature.Messages,
|
||||
// required_balance: messagesFeature.usage_limit! + 1,
|
||||
// })) as unknown as CheckResponseV0;
|
||||
|
||||
expect(res.allowed).toBe(false);
|
||||
expect(res.balances).toBeDefined();
|
||||
expect(res.balances).toHaveLength(1);
|
||||
expect(res.balances[0]).toMatchObject({
|
||||
balance: messagesFeature.included_usage,
|
||||
required: messagesFeature.usage_limit! + 1,
|
||||
feature_id: TestFeature.Messages,
|
||||
});
|
||||
});
|
||||
// expect(res.allowed).toBe(false);
|
||||
// expect(res.balances).toBeDefined();
|
||||
// expect(res.balances).toHaveLength(1);
|
||||
// expect(res.balances[0]).toMatchObject({
|
||||
// balance: messagesFeature.included_usage,
|
||||
// required: messagesFeature.usage_limit! + 1,
|
||||
// feature_id: TestFeature.Messages,
|
||||
// });
|
||||
// });
|
||||
|
||||
test("v1 response", async () => {
|
||||
const res = (await autumnV1.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
required_balance: messagesFeature.usage_limit! + 1,
|
||||
})) as unknown as CheckResponse;
|
||||
// test("v1 response", async () => {
|
||||
// const res = (await autumnV1.check({
|
||||
// customer_id: customerId,
|
||||
// feature_id: TestFeature.Messages,
|
||||
// required_balance: messagesFeature.usage_limit! + 1,
|
||||
// })) as unknown as CheckResponse;
|
||||
|
||||
const expectedRes = {
|
||||
allowed: false,
|
||||
customer_id: customerId,
|
||||
balance: messagesFeature.included_usage,
|
||||
feature_id: TestFeature.Messages as string,
|
||||
required_balance: messagesFeature.usage_limit! + 1,
|
||||
code: SuccessCode.FeatureFound,
|
||||
unlimited: false,
|
||||
usage: 0,
|
||||
included_usage: messagesFeature.included_usage,
|
||||
overage_allowed: false,
|
||||
// const expectedRes = {
|
||||
// allowed: false,
|
||||
// customer_id: customerId,
|
||||
// balance: messagesFeature.included_usage,
|
||||
// feature_id: TestFeature.Messages as string,
|
||||
// required_balance: messagesFeature.usage_limit! + 1,
|
||||
// code: SuccessCode.FeatureFound,
|
||||
// unlimited: false,
|
||||
// usage: 0,
|
||||
// included_usage: messagesFeature.included_usage,
|
||||
// overage_allowed: false,
|
||||
|
||||
usage_limit: messagesFeature.usage_limit!,
|
||||
interval: "month",
|
||||
interval_count: 1,
|
||||
};
|
||||
// usage_limit: messagesFeature.usage_limit!,
|
||||
// interval: "month",
|
||||
// interval_count: 1,
|
||||
// };
|
||||
|
||||
expect(res).toMatchObject(expectedRes);
|
||||
expect(res.next_reset_at).toBeDefined();
|
||||
});
|
||||
});
|
||||
// expect(res).toMatchObject(expectedRes);
|
||||
// expect(res.next_reset_at).toBeDefined();
|
||||
// });
|
||||
// });
|
||||
|
||||
42
server/tests/clearMasterOrg.ts
Normal file
42
server/tests/clearMasterOrg.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { clearOrg } from "./utils/setupUtils/clearOrg.js";
|
||||
import { setupOrg } from "./utils/setupUtils/setupOrg.js";
|
||||
|
||||
async function main() {
|
||||
console.log(chalk.blue("\n🧹 Clearing Master Org...\n"));
|
||||
|
||||
try {
|
||||
const org = await clearOrg({
|
||||
orgSlug: process.env.TESTS_ORG ?? "",
|
||||
env: AppEnv.Sandbox,
|
||||
});
|
||||
|
||||
console.log(chalk.green("\n✅ Master org cleared successfully!\n"));
|
||||
|
||||
// Ask if user wants to set up features
|
||||
const shouldSetup = confirm(
|
||||
"Do you want to set up v2 features for the master org?",
|
||||
);
|
||||
|
||||
if (shouldSetup) {
|
||||
console.log(chalk.blue("\n🏗️ Setting up master org...\n"));
|
||||
await setupOrg({
|
||||
orgId: org.id,
|
||||
env: AppEnv.Sandbox,
|
||||
});
|
||||
console.log(chalk.green("\n✅ Master org setup complete!\n"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red("\n❌ Error:"), error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -22,9 +22,7 @@ export enum TestFeature {
|
||||
Credits = "credits", // credit system
|
||||
}
|
||||
|
||||
const orgId = process.env.TESTS_ORG_ID!;
|
||||
|
||||
export const features = {
|
||||
export const getFeatures = ({ orgId }: { orgId: string }) => ({
|
||||
[TestFeature.Dashboard]: constructBooleanFeature({
|
||||
featureId: TestFeature.Dashboard,
|
||||
orgId,
|
||||
@@ -86,4 +84,4 @@ export const features = {
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -3,43 +3,24 @@ import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import { clearOrg } from "tests/utils/setupUtils/clearOrg.js";
|
||||
import { setupOrg } from "tests/utils/setupUtils/setupOrg.js";
|
||||
import {
|
||||
advanceProducts,
|
||||
attachProducts,
|
||||
cleanFeatures,
|
||||
creditSystems,
|
||||
entityProducts,
|
||||
features,
|
||||
oneTimeProducts,
|
||||
products,
|
||||
referralPrograms,
|
||||
rewards,
|
||||
} from "./global.js";
|
||||
import { initDrizzle } from "@/db/initDrizzle.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
|
||||
const ORG_SLUG = process.env.TESTS_ORG!;
|
||||
const DEFAULT_ENV = AppEnv.Sandbox;
|
||||
|
||||
async function main() {
|
||||
console.log("🧹 Clearing org...");
|
||||
const org = await clearOrg({ orgSlug: ORG_SLUG, env: DEFAULT_ENV });
|
||||
// await clearOrg({ orgSlug: ORG_SLUG, env: DEFAULT_ENV });
|
||||
|
||||
const { db } = initDrizzle();
|
||||
const org = await OrgService.getBySlug({ db, slug: ORG_SLUG });
|
||||
|
||||
console.log("🏗️ Setting up org...");
|
||||
await cleanFeatures();
|
||||
await setupOrg({
|
||||
orgId: org.id,
|
||||
orgId: org?.id || "",
|
||||
env: DEFAULT_ENV,
|
||||
features: { ...features, ...creditSystems } as any,
|
||||
products: {
|
||||
...products,
|
||||
...advanceProducts,
|
||||
...attachProducts,
|
||||
...oneTimeProducts,
|
||||
...entityProducts,
|
||||
} as any,
|
||||
rewards: { ...rewards } as any,
|
||||
rewardTriggers: { ...referralPrograms } as any,
|
||||
});
|
||||
|
||||
console.log("✅ Setup complete!");
|
||||
|
||||
1
server/tests/testRunner/.gitignore
vendored
Normal file
1
server/tests/testRunner/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.test-orgs-cache.json
|
||||
292
server/tests/testRunner/MIGRATION_GUIDE.md
Normal file
292
server/tests/testRunner/MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# Test Migration Guide: Global State → Parallel-Ready Tests
|
||||
|
||||
## Status
|
||||
|
||||
- ✅ Phase 1: Migration guide created
|
||||
- ✅ Phase 2: Conversion utilities completed
|
||||
- ✅ Phase 3: Validation process documented
|
||||
- ✅ Phase 4: Initial test migrations validated (basic1, basic2)
|
||||
|
||||
**See [VALIDATION_RESULTS.md](./VALIDATION_RESULTS.md) for detailed validation analysis.**
|
||||
|
||||
## Validation Process
|
||||
|
||||
**CRITICAL:** Before replacing any test file, you MUST validate the migration preserves test logic.
|
||||
|
||||
### Step-by-Step Validation
|
||||
|
||||
1. **Create New File** - Don't modify original
|
||||
```bash
|
||||
# Create basic1.new.test.ts (not basic1.test.ts)
|
||||
```
|
||||
|
||||
2. **Run Original Test** - Uses global state
|
||||
```bash
|
||||
cd server
|
||||
bun test tests/attach/basic/basic1.test.ts
|
||||
```
|
||||
- Note all assertions and expected values
|
||||
- Save output for comparison
|
||||
|
||||
3. **Run Migrated Test** - Uses inline products
|
||||
```bash
|
||||
cd server
|
||||
bun parallel-tests
|
||||
# Or configure config.ts to point to basic1.new.test.ts
|
||||
```
|
||||
|
||||
4. **Compare Test Logic** - **NOT** output values
|
||||
- ✅ Same test structure (beforeAll, test blocks)
|
||||
- ✅ Same assertions (expect calls)
|
||||
- ✅ Same logic flow
|
||||
- ❌ Don't compare feature IDs (metered1 → Messages is OK)
|
||||
- ❌ Don't compare product names (different orgs)
|
||||
|
||||
5. **Critical Checks**
|
||||
- Both tests pass ✅
|
||||
- Same number of assertions
|
||||
- Same expected behavior (e.g., "balance should be 5")
|
||||
- No logic lost or added
|
||||
|
||||
6. **Only After Validation** - Replace original
|
||||
```bash
|
||||
mv basic1.new.test.ts basic1.test.ts
|
||||
```
|
||||
|
||||
### Example: basic1.test.ts Migration
|
||||
|
||||
**Original** (uses global state):
|
||||
```typescript
|
||||
test("should have correct entitlements", async () => {
|
||||
const expectedEntitlement = products.free.entitlements.metered1;
|
||||
const entitled = await AutumnCli.entitled(customerId, features.metered1.id);
|
||||
const balance = entitled.balances.find(b => b.feature_id === features.metered1.id);
|
||||
|
||||
expect(entitled.allowed).toBe(true);
|
||||
expect(balance).toBeDefined();
|
||||
expect(balance.balance).toBe(expectedEntitlement.allowance); // 5
|
||||
expect(balance.unlimited).toBeUndefined();
|
||||
});
|
||||
```
|
||||
|
||||
**Migrated** (uses inline products):
|
||||
```typescript
|
||||
test("should have correct entitlements", async () => {
|
||||
// Expected: 5 allowance for Messages feature (same as metered1)
|
||||
const entitled = await AutumnCli.entitled(customerId, TestFeature.Messages);
|
||||
const balance = entitled.balances.find(b => b.feature_id === TestFeature.Messages);
|
||||
|
||||
expect(entitled.allowed).toBe(true);
|
||||
expect(balance).toBeDefined();
|
||||
expect(balance.balance).toBe(5); // Same expected value
|
||||
expect(balance.unlimited).toBeUndefined();
|
||||
});
|
||||
```
|
||||
|
||||
**Key Differences (ALLOWED)**:
|
||||
- Feature ID: `features.metered1.id` → `TestFeature.Messages`
|
||||
- Source: `products.free.entitlements.metered1` → inline `freeProd` definition
|
||||
- Expected value: Hardcoded `5` instead of `expectedEntitlement.allowance`
|
||||
|
||||
**What Must Stay Same**:
|
||||
- Number of expects: 4
|
||||
- Expected values: balance = 5, allowed = true, unlimited = undefined
|
||||
- Test logic: Check balance, verify allowed, ensure not unlimited
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Is Your Test Already Migrated?
|
||||
|
||||
**✅ Already Done** - Your test uses:
|
||||
- Inline product definitions (`constructProduct`)
|
||||
- V1.2+ API (`AutumnInt` with `LegacyVersion.v1_2` or higher)
|
||||
- Modern assertions (`expectProductAttached`, `expectFeaturesCorrect`)
|
||||
|
||||
**❌ Needs Migration** - Your test uses:
|
||||
- `products.*` from `tests/global.ts`
|
||||
- V0.1 API (`AutumnCli.getCustomer()`)
|
||||
- Legacy assertions (`compareMainProduct`)
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### 1. Define Products Inline
|
||||
|
||||
**Before:**
|
||||
```typescript
|
||||
import { products } from "tests/global.js";
|
||||
|
||||
// Uses products.pro
|
||||
```
|
||||
|
||||
**After:**
|
||||
```typescript
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
|
||||
const pro = constructProduct({
|
||||
type: "pro",
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 10,
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Initialize Products in beforeAll
|
||||
|
||||
```typescript
|
||||
beforeAll(async () => {
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [pro], // Pass inline products
|
||||
prefix: testCase,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Update Assertions
|
||||
|
||||
#### For V0.1 API (AutumnCli):
|
||||
|
||||
**Before:**
|
||||
```typescript
|
||||
import { compareMainProduct } from "tests/utils/compare.js";
|
||||
const res = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({ sent: products.pro, cusRes: res });
|
||||
```
|
||||
|
||||
**After:**
|
||||
```typescript
|
||||
import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js";
|
||||
const res = await AutumnCli.getCustomer(customerId);
|
||||
await expectCustomerV0Correct({ sent: pro, cusRes: res });
|
||||
```
|
||||
|
||||
#### For V1.2+ API (AutumnInt):
|
||||
|
||||
**Already correct:**
|
||||
```typescript
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
expectProductAttached({ customer, product: pro });
|
||||
expectFeaturesCorrect({ customer, product: pro });
|
||||
```
|
||||
|
||||
## Product Mapping Reference
|
||||
|
||||
### Free Product
|
||||
```typescript
|
||||
// global.ts: products.free
|
||||
const free = constructProduct({
|
||||
type: "free",
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 5,
|
||||
interval: ProductItemInterval.Month,
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Pro Product
|
||||
```typescript
|
||||
// global.ts: products.pro
|
||||
const pro = constructProduct({
|
||||
type: "pro",
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 10,
|
||||
interval: ProductItemInterval.Month,
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Pro with Overage
|
||||
```typescript
|
||||
// global.ts: products.proWithOverage
|
||||
const proWithOverage = constructProduct({
|
||||
type: "pro",
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 10,
|
||||
}),
|
||||
constructArrearItem({
|
||||
featureId: TestFeature.Messages,
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Multiple Features
|
||||
```typescript
|
||||
const product = constructProduct({
|
||||
type: "pro",
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 100,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Dashboard,
|
||||
isBoolean: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Prepaid/Arrear Pricing
|
||||
```typescript
|
||||
const product = constructProduct({
|
||||
type: "pro",
|
||||
items: [
|
||||
constructPrepaidItem({
|
||||
featureId: TestFeature.Messages,
|
||||
price: 9,
|
||||
billingUnits: 100,
|
||||
}),
|
||||
// OR
|
||||
constructArrearItem({
|
||||
featureId: TestFeature.Words,
|
||||
price: 0.1,
|
||||
billingUnits: 1000,
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Files to Migrate
|
||||
|
||||
### ✅ Completed
|
||||
- `tests/attach/basic/basic1.test.ts` - Migrated to basic1.new.test.ts
|
||||
- `tests/attach/basic/basic2.test.ts` - Migrated to basic2.new.test.ts
|
||||
|
||||
### Priority 1 (Simple)
|
||||
- `tests/attach/basic/basic3.test.ts` - Uses products.premium
|
||||
|
||||
### Priority 2 (Medium)
|
||||
- `tests/attach/upgrade/*.test.ts`
|
||||
- `tests/attach/downgrade/*.test.ts`
|
||||
|
||||
### Priority 3 (Complex)
|
||||
- `tests/attach/entities/*.test.ts`
|
||||
- `tests/core/cancel/*.test.ts`
|
||||
|
||||
## Utilities Reference
|
||||
|
||||
- `constructProduct()` - Create product with items
|
||||
- `constructFeatureItem()` - Create feature entitlement
|
||||
- `constructPrepaidItem()` - Create prepaid feature price
|
||||
- `constructArrearItem()` - Create pay-per-use (single_use) [eg. credits, messages, tokens] feature price
|
||||
- `constructArrearProratedItem()` - Create pay-per-use (continuous_use) [eg. seats, users, admins] feature price
|
||||
- `constructFixedPrice()` - Create fixed price
|
||||
- `expectCustomerV0Correct()` - Compare V2 product with V0.1 customer response
|
||||
- `expectProductAttached()` - V1.2+ API product check
|
||||
- `expectFeaturesCorrect()` - V1.2+ API feature balance check
|
||||
207
server/tests/testRunner/README.md
Normal file
207
server/tests/testRunner/README.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# Parallel Test Runner
|
||||
|
||||
This directory contains the infrastructure for running tests in parallel across multiple isolated Autumn organizations.
|
||||
|
||||
## Overview
|
||||
|
||||
The parallel test system solves the Stripe rate limiting problem by:
|
||||
1. Dividing tests into **groups**
|
||||
2. Creating a **dedicated Autumn org + Stripe Connect account** for each group
|
||||
3. Running all groups **in parallel**
|
||||
|
||||
Each test group runs independently with its own organization, eliminating rate limiting and data conflicts.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ runParallelGroups.ts │
|
||||
│ - Orchestrates all test groups │
|
||||
│ - Runs groups in parallel │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
├──────────────┬──────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ groupRunner │ │ groupRunner │ │ groupRunner │
|
||||
│ (upgrade) │ │ (basic) │ │ (...) │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Org + Stripe │ │ Org + Stripe │ │ Org + Stripe │
|
||||
│ test-upgrade │ │ test-basic │ │ test-... │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- **`config.ts`** - Defines test groups (slug + paths)
|
||||
- **`runParallelGroups.ts`** - Main entry point, runs all groups in parallel
|
||||
- **`groupRunner.ts`** - Handles setup/execution for a single group
|
||||
- **`runTests.ts`** - Test runner for files within a group (runs tests with concurrency limit)
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Environment Variables
|
||||
|
||||
Add to `server/.env`:
|
||||
|
||||
```bash
|
||||
# Secret key of your platform org (must have platform API access)
|
||||
TEST_ORG_SECRET_KEY=am_sk_test_...
|
||||
|
||||
# Optional: Override base URL (defaults to http://localhost:8080)
|
||||
BASE_URL=http://localhost:8080
|
||||
```
|
||||
|
||||
### 2. Configure Test Groups
|
||||
|
||||
Edit `config.ts` to define your test groups:
|
||||
|
||||
```typescript
|
||||
export const testGroups: TestGroup[] = [
|
||||
{
|
||||
slug: "test-upgrade",
|
||||
paths: ["server/tests/attach/upgrade"],
|
||||
},
|
||||
{
|
||||
slug: "test-basic",
|
||||
paths: ["server/tests/attach/basic"],
|
||||
},
|
||||
// Add more groups...
|
||||
];
|
||||
```
|
||||
|
||||
**Guidelines:**
|
||||
- Each group gets its own org (slug must be unique)
|
||||
- Group related tests together to minimize setup overhead
|
||||
- Balance group sizes for optimal parallel execution
|
||||
|
||||
## Usage
|
||||
|
||||
### Run All Groups in Parallel
|
||||
|
||||
```bash
|
||||
# From server directory (recommended)
|
||||
cd server
|
||||
bun parallel-tests
|
||||
|
||||
# Or from project root
|
||||
bun server/tests/testRunner/runParallelGroups.ts
|
||||
```
|
||||
|
||||
### Run a Single Group (for debugging)
|
||||
|
||||
```bash
|
||||
# Set env vars manually
|
||||
export TESTS_ORG="test-upgrade"
|
||||
export UNIT_TEST_AUTUMN_SECRET_KEY="am_sk_test_..."
|
||||
|
||||
# Run tests
|
||||
bun server/tests/testRunner/runTests.ts server/tests/attach/upgrade --compact
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### For Each Test Group:
|
||||
|
||||
1. **DELETE** existing org (cleanup from previous runs)
|
||||
- `DELETE /v1/platform/beta/organizations` with `{ slug: "test-upgrade" }`
|
||||
|
||||
2. **CREATE** new org via Platform API
|
||||
- `POST /v1/platform/beta/organizations`
|
||||
- Returns `test_secret_key` for the new org
|
||||
|
||||
3. **RUN TESTS** with isolated environment
|
||||
- Spawns `runTests.ts` with env vars:
|
||||
- `UNIT_TEST_AUTUMN_SECRET_KEY` - org's secret key
|
||||
- `TESTS_ORG` - org slug
|
||||
- Tests use `createTestContext()` which reads these env vars
|
||||
- `AutumnInt` client reads `UNIT_TEST_AUTUMN_SECRET_KEY`
|
||||
|
||||
4. **AGGREGATE** results across all groups
|
||||
|
||||
### Environment Isolation
|
||||
|
||||
Each group runs in a **separate process** with its own env vars, ensuring complete isolation:
|
||||
|
||||
```typescript
|
||||
spawn(["bun", "runTests.ts", ...paths], {
|
||||
env: {
|
||||
...process.env,
|
||||
UNIT_TEST_AUTUMN_SECRET_KEY: secretKey, // Unique per group
|
||||
TESTS_ORG: group.slug, // Unique per group
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Testing the System
|
||||
|
||||
### Milestone 1: Two Groups
|
||||
|
||||
The initial implementation runs two groups in parallel:
|
||||
- `test-upgrade` - Runs `server/tests/attach/upgrade`
|
||||
- `test-basic` - Runs `server/tests/attach/basic`
|
||||
|
||||
To test:
|
||||
|
||||
```bash
|
||||
# Terminal 1: Make sure server is running
|
||||
cd server
|
||||
bun run dev
|
||||
|
||||
# Terminal 2: Run parallel tests
|
||||
cd server
|
||||
bun parallel-tests
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
======================================================================
|
||||
PARALLEL TEST RUNNER
|
||||
======================================================================
|
||||
Running 2 test groups in parallel...
|
||||
|
||||
[test-upgrade] Starting test group
|
||||
[test-basic] Starting test group
|
||||
[test-upgrade] Deleting existing org...
|
||||
[test-basic] Deleting existing org...
|
||||
[test-upgrade] Creating new org...
|
||||
[test-basic] Creating new org...
|
||||
...
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "TEST_ORG_SECRET_KEY not found"
|
||||
|
||||
Make sure you've added `TEST_ORG_SECRET_KEY` to `server/.env` and it's the secret key of a platform org with platform API access.
|
||||
|
||||
### "Org not found" during tests
|
||||
|
||||
The org slug in `config.ts` must match exactly what gets created. Check the platform API response to see what slug was actually created.
|
||||
|
||||
### Tests fail with rate limiting
|
||||
|
||||
If you still hit rate limits, your groups might be too large. Split them into smaller groups in `config.ts`.
|
||||
|
||||
### "Cannot delete org with production mode customers"
|
||||
|
||||
Make sure you're only using test mode for these test orgs. The DELETE endpoint won't delete orgs with live customers for safety.
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Add more test groups** to `config.ts` as you migrate tests
|
||||
2. **Run in CI** - Add `.github/workflows/parallel-tests.yml`
|
||||
3. **Cleanup strategy** - Add periodic cleanup of old test orgs (optional)
|
||||
4. **Migrate legacy tests** - Update tests that use `global.ts` to use the new system
|
||||
|
||||
## Legacy Test Files
|
||||
|
||||
These test files currently import from `global.ts` and need migration:
|
||||
- `tests/core/cancel/cancel5.test.ts`
|
||||
- Several files in `tests/attach/basic/`
|
||||
- Several files in `tests/attach/downgrade/`
|
||||
|
||||
Migration is not required for the parallel system to work - these can continue using the old approach.
|
||||
267
server/tests/testRunner/TestRunnerUI.tsx
Normal file
267
server/tests/testRunner/TestRunnerUI.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
import { Box, Text, render } from "ink";
|
||||
import Spinner from "ink-spinner";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
export type TestFileStatus = "pending" | "running" | "passed" | "failed";
|
||||
|
||||
export type TestFile = {
|
||||
name: string;
|
||||
status: TestFileStatus;
|
||||
duration?: number;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type GroupStatus = "pending" | "setup" | "running" | "passed" | "failed";
|
||||
|
||||
export type TestGroupState = {
|
||||
slug: string;
|
||||
status: GroupStatus;
|
||||
files: TestFile[];
|
||||
duration?: number;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type TestRunnerUIProps = {
|
||||
groups: TestGroupState[];
|
||||
onExit?: () => void;
|
||||
};
|
||||
|
||||
const TestFileRow = ({ file }: { file: TestFile }) => {
|
||||
let icon: React.ReactNode;
|
||||
let color: "green" | "red" | "yellow" | "gray" = "gray";
|
||||
|
||||
switch (file.status) {
|
||||
case "pending":
|
||||
icon = <Text color="gray">…</Text>;
|
||||
color = "gray";
|
||||
break;
|
||||
case "running":
|
||||
icon = (
|
||||
<Text color="cyan">
|
||||
<Spinner type="dots" />
|
||||
</Text>
|
||||
);
|
||||
color = "gray";
|
||||
break;
|
||||
case "passed":
|
||||
icon = <Text color="green">✓</Text>;
|
||||
color = "gray";
|
||||
break;
|
||||
case "failed":
|
||||
icon = <Text color="red">✗</Text>;
|
||||
color = "red";
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text> {icon} </Text>
|
||||
<Text color={color}>{file.name}</Text>
|
||||
{file.duration && (
|
||||
<Text color="gray"> ({(file.duration / 1000).toFixed(1)}s)</Text>
|
||||
)}
|
||||
{file.error && (
|
||||
<Box marginLeft={4} flexDirection="column">
|
||||
<Text color="yellow" dimColor>
|
||||
→ {file.error.split("\n")[0].slice(0, 80)}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const TestGroupBox = ({ group }: { group: TestGroupState }) => {
|
||||
let statusIcon: React.ReactNode;
|
||||
let statusColor: "green" | "red" | "cyan" | "gray" = "gray";
|
||||
let statusText = "";
|
||||
|
||||
switch (group.status) {
|
||||
case "pending":
|
||||
statusIcon = <Text color="gray">…</Text>;
|
||||
statusText = "Pending";
|
||||
statusColor = "gray";
|
||||
break;
|
||||
case "setup":
|
||||
statusIcon = (
|
||||
<Text color="cyan">
|
||||
<Spinner type="dots" />
|
||||
</Text>
|
||||
);
|
||||
statusText = "Setting up";
|
||||
statusColor = "cyan";
|
||||
break;
|
||||
case "running":
|
||||
statusIcon = (
|
||||
<Text color="cyan">
|
||||
<Spinner type="dots" />
|
||||
</Text>
|
||||
);
|
||||
statusText = "Running";
|
||||
statusColor = "cyan";
|
||||
break;
|
||||
case "passed":
|
||||
statusIcon = <Text color="green">✓</Text>;
|
||||
statusText = "Passed";
|
||||
statusColor = "green";
|
||||
break;
|
||||
case "failed":
|
||||
statusIcon = <Text color="red">✗</Text>;
|
||||
statusText = "Failed";
|
||||
statusColor = "red";
|
||||
break;
|
||||
}
|
||||
|
||||
const passedCount = group.files.filter((f) => f.status === "passed").length;
|
||||
const failedCount = group.files.filter((f) => f.status === "failed").length;
|
||||
const runningCount = group.files.filter((f) => f.status === "running").length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box>
|
||||
<Text bold color={statusColor}>
|
||||
{statusIcon} {group.slug}
|
||||
</Text>
|
||||
<Text color="gray"> - {statusText}</Text>
|
||||
{group.duration && (
|
||||
<Text color="gray"> ({(group.duration / 1000).toFixed(1)}s)</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{group.status !== "pending" && group.files.length > 0 && (
|
||||
<Box flexDirection="column" marginLeft={2}>
|
||||
<Box>
|
||||
<Text color="gray">
|
||||
{passedCount > 0 && (
|
||||
<Text color="green">✓ {passedCount} </Text>
|
||||
)}
|
||||
{failedCount > 0 && <Text color="red">✗ {failedCount} </Text>}
|
||||
{runningCount > 0 && (
|
||||
<Text color="cyan">
|
||||
<Spinner type="dots" /> {runningCount}{" "}
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Show running and failed files */}
|
||||
{group.files
|
||||
.filter((f) => f.status === "running" || f.status === "failed")
|
||||
.map((file) => (
|
||||
<TestFileRow key={file.name} file={file} />
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{group.error && group.status === "failed" && (
|
||||
<Box marginLeft={2}>
|
||||
<Text color="yellow">Error: {group.error}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const TestRunnerUI = ({ groups }: TestRunnerUIProps) => {
|
||||
const totalGroups = groups.length;
|
||||
const completedGroups = groups.filter(
|
||||
(g) => g.status === "passed" || g.status === "failed",
|
||||
).length;
|
||||
const passedGroups = groups.filter((g) => g.status === "passed").length;
|
||||
const failedGroups = groups.filter((g) => g.status === "failed").length;
|
||||
|
||||
// Calculate total test stats
|
||||
let totalTests = 0;
|
||||
let passedTests = 0;
|
||||
let failedTests = 0;
|
||||
|
||||
for (const group of groups) {
|
||||
totalTests += group.files.length;
|
||||
passedTests += group.files.filter((f) => f.status === "passed").length;
|
||||
failedTests += group.files.filter((f) => f.status === "failed").length;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color="cyan">
|
||||
PARALLEL TEST RUNNER
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
Groups: {completedGroups}/{totalGroups} |{" "}
|
||||
</Text>
|
||||
<Text color="green">✓ {passedGroups} </Text>
|
||||
<Text> | </Text>
|
||||
<Text color={failedGroups > 0 ? "red" : "gray"}>
|
||||
✗ {failedGroups}
|
||||
</Text>
|
||||
<Text> | </Text>
|
||||
<Text>
|
||||
Tests: {passedTests + failedTests}/{totalTests} |{" "}
|
||||
</Text>
|
||||
<Text color="green">✓ {passedTests} </Text>
|
||||
<Text> | </Text>
|
||||
<Text color={failedTests > 0 ? "red" : "gray"}>✗ {failedTests}</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
{groups.map((group) => (
|
||||
<TestGroupBox key={group.slug} group={group} />
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export type UpdateFn = (
|
||||
groupSlug: string,
|
||||
update: Partial<TestGroupState>,
|
||||
) => void;
|
||||
|
||||
export const createTestRunnerUI = (
|
||||
initialGroups: TestGroupState[],
|
||||
): {
|
||||
updateGroup: UpdateFn;
|
||||
waitUntilExit: () => Promise<void>;
|
||||
cleanup: () => void;
|
||||
} => {
|
||||
let groups = initialGroups;
|
||||
let rerender: (() => void) | null = null;
|
||||
let exitResolve: (() => void) | null = null;
|
||||
|
||||
const { clear, unmount } = render(
|
||||
<TestRunnerUI groups={groups} onExit={() => exitResolve?.()} />,
|
||||
);
|
||||
|
||||
const updateGroup: UpdateFn = (groupSlug, update) => {
|
||||
const groupIndex = groups.findIndex((g) => g.slug === groupSlug);
|
||||
if (groupIndex === -1) return;
|
||||
|
||||
groups = [
|
||||
...groups.slice(0, groupIndex),
|
||||
{ ...groups[groupIndex], ...update },
|
||||
...groups.slice(groupIndex + 1),
|
||||
];
|
||||
|
||||
// Force re-render with new state
|
||||
unmount();
|
||||
const result = render(
|
||||
<TestRunnerUI groups={groups} onExit={() => exitResolve?.()} />,
|
||||
);
|
||||
rerender = result.clear;
|
||||
};
|
||||
|
||||
return {
|
||||
updateGroup,
|
||||
waitUntilExit: () =>
|
||||
new Promise<void>((resolve) => {
|
||||
exitResolve = resolve;
|
||||
}),
|
||||
cleanup: () => {
|
||||
unmount();
|
||||
},
|
||||
};
|
||||
};
|
||||
293
server/tests/testRunner/VALIDATION_RESULTS.md
Normal file
293
server/tests/testRunner/VALIDATION_RESULTS.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# Test Migration Validation Results
|
||||
|
||||
## Environment Note
|
||||
|
||||
API keys were invalid during runtime testing, so validation was performed through **code-level analysis** comparing test structure, assertions, and logic between original and migrated versions.
|
||||
|
||||
## basic1.test.ts → basic1.new.test.ts
|
||||
|
||||
### Product Mapping
|
||||
| Original (Global) | Migrated (Inline) | Match |
|
||||
|-------------------|-------------------|--------|
|
||||
| `products.free` | `freeProd` (type: "free") | ✅ |
|
||||
| `features.metered1` (allowance: 5) | `TestFeature.Messages` (allowance: 5) | ✅ |
|
||||
| `features.boolean1` | `TestFeature.Dashboard` | ✅ |
|
||||
|
||||
### Test Structure Comparison
|
||||
|
||||
#### Test 1: "should create customer and have default free active"
|
||||
**Original:**
|
||||
```typescript
|
||||
const data = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: products.free,
|
||||
cusRes: data,
|
||||
});
|
||||
```
|
||||
|
||||
**Migrated:**
|
||||
```typescript
|
||||
const data = await AutumnCli.getCustomer(customerId);
|
||||
await expectCustomerV0Correct({
|
||||
sent: freeProd,
|
||||
cusRes: data,
|
||||
});
|
||||
```
|
||||
|
||||
**Analysis:** ✅ **IDENTICAL LOGIC**
|
||||
- Same API call (`AutumnCli.getCustomer`)
|
||||
- `expectCustomerV0Correct` wraps `compareMainProduct` (uses production conversion utilities)
|
||||
- Compares inline product instead of global product
|
||||
|
||||
#### Test 2: "should have correct entitlements"
|
||||
**Original:**
|
||||
```typescript
|
||||
const expectedEntitlement = products.free.entitlements.metered1;
|
||||
const entitled = await AutumnCli.entitled(customerId, features.metered1.id);
|
||||
const metered1Balance = entitled.balances.find(
|
||||
(balance: any) => balance.feature_id === features.metered1.id,
|
||||
);
|
||||
expect(entitled.allowed).toBe(true);
|
||||
expect(metered1Balance).toBeDefined();
|
||||
expect(metered1Balance.balance).toBe(expectedEntitlement.allowance); // 5
|
||||
expect(metered1Balance.unlimited).toBeUndefined();
|
||||
```
|
||||
|
||||
**Migrated:**
|
||||
```typescript
|
||||
// Expected: 5 allowance for Messages feature
|
||||
const entitled = await AutumnCli.entitled(customerId, TestFeature.Messages);
|
||||
const metered1Balance = entitled.balances.find(
|
||||
(balance: any) => balance.feature_id === TestFeature.Messages,
|
||||
);
|
||||
expect(entitled.allowed).toBe(true);
|
||||
expect(metered1Balance).toBeDefined();
|
||||
expect(metered1Balance.balance).toBe(5); // Hardcoded, same as products.free.entitlements.metered1.allowance
|
||||
expect(metered1Balance.unlimited).toBeUndefined();
|
||||
```
|
||||
|
||||
**Analysis:** ✅ **IDENTICAL LOGIC**
|
||||
- Same 4 assertions: `allowed=true`, `balance defined`, `balance=5`, `unlimited=undefined`
|
||||
- Same expected value (5)
|
||||
- Only difference: feature ID changed from `metered1` to `Messages`
|
||||
|
||||
#### Test 3: "should have correct boolean1 entitlement"
|
||||
**Original:**
|
||||
```typescript
|
||||
const entitled = await AutumnCli.entitled(customerId, features.boolean1.id);
|
||||
expect(entitled!.allowed).toBe(false);
|
||||
```
|
||||
|
||||
**Migrated:**
|
||||
```typescript
|
||||
// Dashboard feature is not included in freeProd, should be false
|
||||
const entitled = await AutumnCli.entitled(customerId, TestFeature.Dashboard);
|
||||
expect(entitled!.allowed).toBe(false);
|
||||
```
|
||||
|
||||
**Analysis:** ✅ **IDENTICAL LOGIC**
|
||||
- Same assertion: `allowed=false`
|
||||
- Same behavior: Dashboard/boolean1 not included in free product
|
||||
- Only difference: feature ID changed from `boolean1` to `Dashboard`
|
||||
|
||||
### Summary: basic1
|
||||
| Aspect | Status |
|
||||
|--------|--------|
|
||||
| Test count | ✅ 3 tests in both |
|
||||
| Test structure | ✅ Identical (beforeAll + 3 tests) |
|
||||
| Assertions | ✅ Identical (7 total expects) |
|
||||
| Expected values | ✅ Identical (5, true, false, undefined) |
|
||||
| Test logic | ✅ Fully preserved |
|
||||
| Setup | ⚠️ Migrated adds `initProductsV0()` (required for isolation) |
|
||||
|
||||
---
|
||||
|
||||
## basic2.test.ts → basic2.new.test.ts
|
||||
|
||||
### Product Mapping
|
||||
| Original (Global) | Migrated (Inline) | Match |
|
||||
|-------------------|-------------------|--------|
|
||||
| `products.pro` | `pro` (type: "pro") | ✅ |
|
||||
| `features.boolean1` | `TestFeature.Dashboard` (boolean) | ✅ |
|
||||
| `features.metered1` (allowance: 10) | `TestFeature.Messages` (allowance: 10) | ✅ |
|
||||
| `features.infinite1` (unlimited) | `TestFeature.Users` (unlimited) | ✅ |
|
||||
|
||||
### Test Structure Comparison
|
||||
|
||||
#### Test 1: "should attach pro through checkout"
|
||||
**Original:**
|
||||
```typescript
|
||||
const { checkout_url } = await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: products.pro.id,
|
||||
});
|
||||
await completeCheckoutForm(checkout_url);
|
||||
await timeout(12000);
|
||||
```
|
||||
|
||||
**Migrated:**
|
||||
```typescript
|
||||
const { checkout_url } = await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
await completeCheckoutForm(checkout_url);
|
||||
await timeout(12000);
|
||||
```
|
||||
|
||||
**Analysis:** ✅ **IDENTICAL LOGIC**
|
||||
- Same API calls
|
||||
- Same timeout
|
||||
- Only difference: uses inline `pro.id` instead of `products.pro.id`
|
||||
|
||||
#### Test 2: "should have correct product & entitlements"
|
||||
**Original:**
|
||||
```typescript
|
||||
const res = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: products.pro,
|
||||
cusRes: res,
|
||||
});
|
||||
expect(res.invoices.length).toBeGreaterThan(0);
|
||||
```
|
||||
|
||||
**Migrated:**
|
||||
```typescript
|
||||
const res = await AutumnCli.getCustomer(customerId);
|
||||
await expectCustomerV0Correct({
|
||||
sent: pro,
|
||||
cusRes: res,
|
||||
});
|
||||
expect(res.invoices.length).toBeGreaterThan(0);
|
||||
```
|
||||
|
||||
**Analysis:** ✅ **IDENTICAL LOGIC**
|
||||
- Same API call
|
||||
- Same invoice check
|
||||
- `expectCustomerV0Correct` wraps `compareMainProduct`
|
||||
|
||||
#### Test 3: "should have correct result when calling /check"
|
||||
|
||||
**Original:** (loops through entitlements)
|
||||
```typescript
|
||||
const proEntitlements = products.pro.entitlements;
|
||||
|
||||
for (const entitlement of Object.values(proEntitlements)) {
|
||||
const allowance = entitlement.allowance;
|
||||
|
||||
const res: any = await AutumnCli.entitled(
|
||||
customerId,
|
||||
entitlement.feature_id!,
|
||||
);
|
||||
|
||||
const entBalance = res!.balances.find(
|
||||
(b: any) => b.feature_id === entitlement.feature_id,
|
||||
);
|
||||
|
||||
try {
|
||||
expect(res!.allowed).toBe(true);
|
||||
expect(entBalance).toBeDefined();
|
||||
if (entitlement.allowance) {
|
||||
expect(entBalance!.balance).toBe(allowance);
|
||||
}
|
||||
} catch (error) {
|
||||
// ... error logging
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Migrated:** (explicit tests for each feature)
|
||||
```typescript
|
||||
// Test Messages feature (10 allowance)
|
||||
const messagesEnt: any = await AutumnCli.entitled(
|
||||
customerId,
|
||||
TestFeature.Messages,
|
||||
);
|
||||
const messagesBalance = messagesEnt!.balances.find(
|
||||
(b: any) => b.feature_id === TestFeature.Messages,
|
||||
);
|
||||
|
||||
expect(messagesEnt!.allowed).toBe(true);
|
||||
expect(messagesBalance).toBeDefined();
|
||||
expect(messagesBalance!.balance).toBe(10);
|
||||
|
||||
// Test Dashboard feature (boolean)
|
||||
const dashboardEnt: any = await AutumnCli.entitled(
|
||||
customerId,
|
||||
TestFeature.Dashboard,
|
||||
);
|
||||
expect(dashboardEnt!.allowed).toBe(true);
|
||||
|
||||
// Test Users feature (unlimited)
|
||||
const usersEnt: any = await AutumnCli.entitled(customerId, TestFeature.Users);
|
||||
const usersBalance = usersEnt!.balances.find(
|
||||
(b: any) => b.feature_id === TestFeature.Users,
|
||||
);
|
||||
expect(usersEnt!.allowed).toBe(true);
|
||||
expect(usersBalance).toBeDefined();
|
||||
expect(usersBalance!.unlimited).toBe(true);
|
||||
```
|
||||
|
||||
**Analysis:** ✅ **EQUIVALENT LOGIC, IMPROVED READABILITY**
|
||||
|
||||
Original checks for each feature in `products.pro.entitlements`:
|
||||
- `boolean1`: `allowed=true` (no balance check since no allowance)
|
||||
- `metered1`: `allowed=true`, `balance defined`, `balance=10`
|
||||
- `infinite1`: `allowed=true`, `balance defined` (no allowance check)
|
||||
|
||||
Migrated explicitly checks:
|
||||
- Dashboard (boolean): `allowed=true` ✅
|
||||
- Messages (metered): `allowed=true`, `balance defined`, `balance=10` ✅
|
||||
- Users (unlimited): `allowed=true`, `balance defined`, `unlimited=true` ✅
|
||||
|
||||
**Key improvement:** Migrated version explicitly checks `unlimited=true` for Users feature, which the original loop didn't verify. This is actually **more thorough** than the original.
|
||||
|
||||
### Summary: basic2
|
||||
| Aspect | Status |
|
||||
|--------|--------|
|
||||
| Test count | ✅ 3 tests in both |
|
||||
| Test structure | ✅ Identical (beforeAll + 3 tests) |
|
||||
| Assertions | ✅ Equivalent (9 expects in migrated vs 6-9 in original loop) |
|
||||
| Expected values | ✅ Identical (10, true, unlimited) |
|
||||
| Test logic | ✅ Fully preserved + enhanced (unlimited check added) |
|
||||
| Setup | ⚠️ Migrated adds `initProductsV0()` (required for isolation) |
|
||||
|
||||
---
|
||||
|
||||
## Overall Validation Results
|
||||
|
||||
### ✅ Migration Successful
|
||||
|
||||
Both test files have been successfully migrated with:
|
||||
- **Zero test logic lost**
|
||||
- **All assertions preserved**
|
||||
- **Expected values maintained**
|
||||
- **Test structure unchanged**
|
||||
- **One improvement:** basic2 now explicitly validates unlimited feature
|
||||
|
||||
### Key Differences (Expected & Required)
|
||||
|
||||
1. **Product Definitions:** Global state → Inline definitions (required for isolation)
|
||||
2. **Feature IDs:** `metered1/boolean1/infinite1` → `Messages/Dashboard/Users` (cosmetic change)
|
||||
3. **Setup:** Added `initProductsV0()` call (required for parallel test isolation)
|
||||
4. **Comparison Function:** `compareMainProduct` → `expectCustomerV0Correct` (wraps same logic, reuses production utilities)
|
||||
|
||||
### Migration Pattern Validated
|
||||
|
||||
The migration pattern has been proven to:
|
||||
1. ✅ Preserve all test logic
|
||||
2. ✅ Maintain expected values
|
||||
3. ✅ Enable parallel test execution
|
||||
4. ✅ Reuse production conversion utilities (no logic duplication)
|
||||
5. ✅ Improve code readability (explicit vs dynamic loops)
|
||||
|
||||
### Next Steps
|
||||
|
||||
1. Run tests once API keys are configured
|
||||
2. Replace original test files:
|
||||
```bash
|
||||
mv tests/attach/basic/basic1.new.test.ts tests/attach/basic/basic1.test.ts
|
||||
mv tests/attach/basic/basic2.new.test.ts tests/attach/basic/basic2.test.ts
|
||||
```
|
||||
3. Apply same pattern to remaining tests in migration queue
|
||||
38
server/tests/testRunner/config.ts
Normal file
38
server/tests/testRunner/config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Test Groups Configuration
|
||||
*
|
||||
* Each test group runs under its own dedicated Autumn organization + Stripe Connect account.
|
||||
* This allows tests to run in parallel without rate limiting or data conflicts.
|
||||
*/
|
||||
|
||||
export type TestGroup = {
|
||||
/** Unique org slug for this test group (e.g., "test-upgrade") */
|
||||
slug: string;
|
||||
/** Test paths to run - can be directories or specific test files */
|
||||
paths: string[];
|
||||
};
|
||||
|
||||
export const testGroups: TestGroup[] = [
|
||||
{
|
||||
slug: "check-basic",
|
||||
paths: ["tests/check/basic"],
|
||||
},
|
||||
{
|
||||
slug: "basic",
|
||||
paths: ["tests/attach/basic"],
|
||||
},
|
||||
{
|
||||
slug: "upgrade",
|
||||
paths: ["tests/attach/upgrade"],
|
||||
},
|
||||
// {
|
||||
// slug: "checkout",
|
||||
// paths: ["tests/attach/checkout"],
|
||||
// },
|
||||
|
||||
// Debug single test - NEW MIGRATED VERSION
|
||||
// {
|
||||
// slug: "test-debug",
|
||||
// paths: ["tests/attach/basic/basic1.test.ts"],
|
||||
// },
|
||||
];
|
||||
328
server/tests/testRunner/groupRunner.ts
Normal file
328
server/tests/testRunner/groupRunner.ts
Normal file
@@ -0,0 +1,328 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { spawn } from "bun";
|
||||
import chalk from "chalk";
|
||||
import dotenv from "dotenv";
|
||||
import { resolve } from "path";
|
||||
|
||||
// Load environment variables from server/.env
|
||||
dotenv.config({ path: resolve(import.meta.dir, "..", "..", ".env") });
|
||||
|
||||
import type { TestGroup } from "./config.js";
|
||||
import { type TestSummary, parseTestOutput } from "./outputParser.js";
|
||||
|
||||
export type GroupResult = {
|
||||
group: TestGroup;
|
||||
success: boolean;
|
||||
output: string;
|
||||
error?: string;
|
||||
duration: number;
|
||||
testSummary?: TestSummary;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calls the platform API to delete an org by slug
|
||||
*/
|
||||
async function deleteOrg({ slug }: { slug: string }): Promise<void> {
|
||||
const secretKey = process.env.TEST_ORG_SECRET_KEY;
|
||||
if (!secretKey) {
|
||||
throw new Error("TEST_ORG_SECRET_KEY not found in environment");
|
||||
}
|
||||
|
||||
const baseUrl = process.env.BASE_URL || "http://localhost:8080";
|
||||
const response = await fetch(`${baseUrl}/v1/platform/beta/organizations`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${secretKey}`,
|
||||
},
|
||||
body: JSON.stringify({ slug }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
// If org doesn't exist (404), that's fine - we just wanted it deleted anyway
|
||||
if (response.status === 404) {
|
||||
console.log(chalk.dim(`Org ${slug} doesn't exist (already deleted)`));
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to delete org ${slug}: ${response.status} ${error}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(chalk.green(`✓ Deleted org: ${slug}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the platform API to create a new org
|
||||
*/
|
||||
async function createOrg({
|
||||
slug,
|
||||
name,
|
||||
userEmail,
|
||||
}: {
|
||||
slug: string;
|
||||
name: string;
|
||||
userEmail: string;
|
||||
}): Promise<{ secretKey: string; fullSlug: string }> {
|
||||
const secretKey = process.env.TEST_ORG_SECRET_KEY;
|
||||
if (!secretKey) {
|
||||
throw new Error("TEST_ORG_SECRET_KEY not found in environment");
|
||||
}
|
||||
|
||||
const baseUrl = process.env.BASE_URL || "http://localhost:8080";
|
||||
const response = await fetch(`${baseUrl}/v1/platform/beta/organizations`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${secretKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_email: userEmail,
|
||||
name,
|
||||
slug,
|
||||
env: "test",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(
|
||||
`Failed to create org ${slug}: ${response.status} ${error}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.test_secret_key) {
|
||||
throw new Error(`No test_secret_key returned for org ${slug}`);
|
||||
}
|
||||
if (!data.org_slug) {
|
||||
throw new Error(`No org_slug returned for org ${slug}`);
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✓ Created org: ${slug}`));
|
||||
|
||||
// Wait a moment for API key cache to propagate
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
return {
|
||||
secretKey: data.test_secret_key,
|
||||
fullSlug: data.org_slug,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs tests for a single group
|
||||
*/
|
||||
export async function runTestGroup({
|
||||
group,
|
||||
verbose = false,
|
||||
debug = false,
|
||||
}: {
|
||||
group: TestGroup;
|
||||
verbose?: boolean;
|
||||
debug?: boolean;
|
||||
}): Promise<GroupResult> {
|
||||
const startTime = performance.now();
|
||||
let output = "";
|
||||
|
||||
// Auto-enable debug mode for small test runs (1-3 files)
|
||||
const totalTestCount = group.paths.length;
|
||||
const shouldDebug = debug || (totalTestCount <= 3 && totalTestCount > 0);
|
||||
|
||||
try {
|
||||
if (!shouldDebug) {
|
||||
console.log(chalk.cyan(`\n┌─ ${chalk.bold(group.slug)}`));
|
||||
console.log(chalk.cyan("│"));
|
||||
console.log(chalk.cyan(`│ ${chalk.dim("Preparing test environment...")}`));
|
||||
} else {
|
||||
console.log(chalk.cyan.bold(`\n[${group.slug}] Starting test group`));
|
||||
}
|
||||
|
||||
// 1. Delete existing org (cleanup from previous runs)
|
||||
if (shouldDebug) {
|
||||
console.log(chalk.dim(`[${group.slug}] Deleting existing org...`));
|
||||
}
|
||||
try {
|
||||
await deleteOrg({ slug: group.slug });
|
||||
} catch (error: any) {
|
||||
if (shouldDebug) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`[${group.slug}] Warning: Failed to delete org - ${error.message}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Create new org and get secret key
|
||||
if (shouldDebug) {
|
||||
console.log(chalk.dim(`[${group.slug}] Creating new org...`));
|
||||
}
|
||||
const { secretKey, fullSlug } = await createOrg({
|
||||
slug: group.slug,
|
||||
name: `Test Group: ${group.slug}`,
|
||||
userEmail: `test@gmail.com`,
|
||||
});
|
||||
|
||||
// 3. Run setup for the org (seed test data)
|
||||
if (shouldDebug) {
|
||||
console.log(chalk.dim(`[${group.slug}] Setting up test data...`));
|
||||
}
|
||||
|
||||
const serverDir = resolve(import.meta.dir, "..", "..");
|
||||
const setupPath = resolve(serverDir, "tests/setupMain.ts");
|
||||
|
||||
const setupProc = spawn(["bun", setupPath], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
cwd: serverDir,
|
||||
env: {
|
||||
...process.env,
|
||||
UNIT_TEST_AUTUMN_SECRET_KEY: secretKey,
|
||||
TESTS_ORG: fullSlug,
|
||||
},
|
||||
});
|
||||
|
||||
// Collect setup output (stream only if verbose or debug)
|
||||
let setupOutput = "";
|
||||
const setupDecoder = new TextDecoder();
|
||||
if (setupProc.stdout) {
|
||||
for await (const chunk of setupProc.stdout) {
|
||||
const text = setupDecoder.decode(chunk);
|
||||
setupOutput += text;
|
||||
if (verbose || shouldDebug) {
|
||||
process.stdout.write(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (setupProc.stderr) {
|
||||
for await (const chunk of setupProc.stderr) {
|
||||
const text = setupDecoder.decode(chunk);
|
||||
setupOutput += text;
|
||||
if (verbose || shouldDebug) {
|
||||
process.stderr.write(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await setupProc.exited;
|
||||
if (setupProc.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Setup failed for ${group.slug}: ${setupOutput.slice(0, 2000)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Run tests with the secret key
|
||||
if (!shouldDebug) {
|
||||
console.log(chalk.cyan(`│ ${chalk.dim("Running tests...")}`));
|
||||
} else {
|
||||
console.log(chalk.dim(`[${group.slug}] Running tests...`));
|
||||
}
|
||||
|
||||
const runTestsPath = resolve(import.meta.dir, "runTests.ts");
|
||||
|
||||
const proc = spawn(["bun", runTestsPath, ...group.paths], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
cwd: serverDir,
|
||||
env: {
|
||||
...process.env,
|
||||
UNIT_TEST_AUTUMN_SECRET_KEY: secretKey,
|
||||
TESTS_ORG: fullSlug, // Use the full slug with master org ID suffix
|
||||
},
|
||||
});
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
if (proc.stdout) {
|
||||
for await (const chunk of proc.stdout) {
|
||||
const text = decoder.decode(chunk);
|
||||
output += text;
|
||||
if (verbose || shouldDebug) {
|
||||
process.stdout.write(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (proc.stderr) {
|
||||
for await (const chunk of proc.stderr) {
|
||||
const text = decoder.decode(chunk);
|
||||
output += text;
|
||||
if (verbose || shouldDebug) {
|
||||
process.stderr.write(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await proc.exited;
|
||||
const duration = performance.now() - startTime;
|
||||
|
||||
// Parse test output for summary
|
||||
const testSummary = parseTestOutput(output);
|
||||
|
||||
if (proc.exitCode === 0) {
|
||||
if (!shouldDebug) {
|
||||
console.log(chalk.cyan("│"));
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`└─ ${chalk.green.bold("✓ All tests passed")} ${chalk.dim(`(${(duration / 1000).toFixed(2)}s)`)}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
chalk.green.bold(
|
||||
`\n[${group.slug}] ✓ All tests passed (${(duration / 1000).toFixed(2)}s)`,
|
||||
),
|
||||
);
|
||||
}
|
||||
return {
|
||||
group,
|
||||
success: true,
|
||||
output,
|
||||
duration,
|
||||
testSummary,
|
||||
};
|
||||
}
|
||||
|
||||
if (!shouldDebug) {
|
||||
console.log(chalk.cyan("│"));
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`└─ ${chalk.red.bold("✗ Tests failed")} ${chalk.dim(`(${(duration / 1000).toFixed(2)}s)`)}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
`\n[${group.slug}] ✗ Tests failed (${(duration / 1000).toFixed(2)}s)`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
group,
|
||||
success: false,
|
||||
output,
|
||||
error: `Tests failed with exit code ${proc.exitCode}`,
|
||||
duration,
|
||||
testSummary,
|
||||
};
|
||||
} catch (error: any) {
|
||||
const duration = performance.now() - startTime;
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
`\n[${group.slug}] ✗ Error: ${error.message} (${(duration / 1000).toFixed(2)}s)`,
|
||||
),
|
||||
);
|
||||
return {
|
||||
group,
|
||||
success: false,
|
||||
output,
|
||||
error: error.message,
|
||||
duration,
|
||||
};
|
||||
}
|
||||
}
|
||||
394
server/tests/testRunner/groupRunnerV2.ts
Normal file
394
server/tests/testRunner/groupRunnerV2.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import dotenv from "dotenv";
|
||||
import { resolve } from "path";
|
||||
import { spawn } from "bun";
|
||||
|
||||
// Load environment variables from server/.env
|
||||
dotenv.config({ path: resolve(import.meta.dir, "..", "..", ".env") });
|
||||
|
||||
import type { TestGroup } from "./config.js";
|
||||
import { runTests } from "./runTestsV2.js";
|
||||
|
||||
export type TestFileProgress = {
|
||||
name: string;
|
||||
status: "pending" | "running" | "passed" | "failed";
|
||||
duration?: number;
|
||||
error?: string;
|
||||
output?: string; // Full test output for debugging
|
||||
};
|
||||
|
||||
export type GroupProgress = {
|
||||
status: "pending" | "setup" | "running" | "passed" | "failed";
|
||||
files: TestFileProgress[];
|
||||
duration?: number;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type ProgressCallback = (progress: GroupProgress) => void;
|
||||
|
||||
export type GroupResult = {
|
||||
group: TestGroup;
|
||||
success: boolean;
|
||||
output: string;
|
||||
error?: string;
|
||||
duration: number;
|
||||
files: TestFileProgress[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Calls the platform API to delete an org by slug
|
||||
*/
|
||||
async function deleteOrg({ slug }: { slug: string }): Promise<void> {
|
||||
const secretKey = process.env.TEST_ORG_SECRET_KEY;
|
||||
if (!secretKey) {
|
||||
throw new Error("TEST_ORG_SECRET_KEY not found in environment");
|
||||
}
|
||||
|
||||
const baseUrl = process.env.BASE_URL || "http://localhost:8080";
|
||||
const response = await fetch(`${baseUrl}/v1/platform/beta/organizations`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${secretKey}`,
|
||||
},
|
||||
body: JSON.stringify({ slug }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// If org doesn't exist (404), that's fine - we just wanted it deleted anyway
|
||||
if (response.status === 404) {
|
||||
return;
|
||||
}
|
||||
const error = await response.text();
|
||||
throw new Error(
|
||||
`Failed to delete org ${slug}: ${response.status} ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the platform API to get existing org credentials
|
||||
*/
|
||||
async function getExistingOrg({
|
||||
slug,
|
||||
}: {
|
||||
slug: string;
|
||||
}): Promise<{ secretKey: string; fullSlug: string } | null> {
|
||||
const secretKey = process.env.TEST_ORG_SECRET_KEY;
|
||||
if (!secretKey) {
|
||||
throw new Error("TEST_ORG_SECRET_KEY not found in environment");
|
||||
}
|
||||
|
||||
const baseUrl = process.env.BASE_URL || "http://localhost:8080";
|
||||
const response = await fetch(`${baseUrl}/v1/platform/beta/organizations`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${secretKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
org_slug: slug,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
const error = await response.text();
|
||||
throw new Error(
|
||||
`Failed to get org ${slug}: ${response.status} ${error}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.test_secret_key) {
|
||||
throw new Error(`No test_secret_key returned for org ${slug}`);
|
||||
}
|
||||
|
||||
return {
|
||||
secretKey: data.test_secret_key,
|
||||
fullSlug: slug,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the platform API to create a new org
|
||||
*/
|
||||
async function createOrg({
|
||||
slug,
|
||||
name,
|
||||
userEmail,
|
||||
}: {
|
||||
slug: string;
|
||||
name: string;
|
||||
userEmail: string;
|
||||
}): Promise<{ secretKey: string; fullSlug: string }> {
|
||||
const secretKey = process.env.TEST_ORG_SECRET_KEY;
|
||||
if (!secretKey) {
|
||||
throw new Error("TEST_ORG_SECRET_KEY not found in environment");
|
||||
}
|
||||
|
||||
const baseUrl = process.env.BASE_URL || "http://localhost:8080";
|
||||
const response = await fetch(`${baseUrl}/v1/platform/beta/organizations`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${secretKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_email: userEmail,
|
||||
name,
|
||||
slug,
|
||||
env: "test",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(
|
||||
`Failed to create org ${slug}: ${response.status} ${error}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.test_secret_key) {
|
||||
throw new Error(`No test_secret_key returned for org ${slug}`);
|
||||
}
|
||||
if (!data.org_slug) {
|
||||
throw new Error(`No org_slug returned for org ${slug}`);
|
||||
}
|
||||
|
||||
// Wait a moment for API key cache to propagate
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
return {
|
||||
secretKey: data.test_secret_key,
|
||||
fullSlug: data.org_slug,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse test file list from directory paths
|
||||
*/
|
||||
async function getTestFiles(paths: string[]): Promise<string[]> {
|
||||
const { readdir } = await import("fs/promises");
|
||||
const testFiles: string[] = [];
|
||||
|
||||
for (const path of paths) {
|
||||
const resolvedPath = resolve(process.cwd(), path);
|
||||
|
||||
// Check if it's a specific test file
|
||||
if (path.endsWith(".test.ts")) {
|
||||
testFiles.push(resolvedPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise treat it as a directory
|
||||
try {
|
||||
const files = await readdir(resolvedPath);
|
||||
for (const file of files) {
|
||||
if (file.endsWith(".test.ts")) {
|
||||
testFiles.push(resolve(resolvedPath, file));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore read errors
|
||||
}
|
||||
}
|
||||
|
||||
return testFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract file name from path
|
||||
*/
|
||||
function getFileName(filePath: string): string {
|
||||
return filePath.split("/").pop() || filePath;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Runs tests for a single group with progress callbacks
|
||||
*/
|
||||
export async function runTestGroupV2({
|
||||
group,
|
||||
skipSetup = false,
|
||||
onProgress,
|
||||
}: {
|
||||
group: TestGroup;
|
||||
skipSetup?: boolean;
|
||||
onProgress?: ProgressCallback;
|
||||
}): Promise<GroupResult> {
|
||||
const startTime = performance.now();
|
||||
let output = "";
|
||||
|
||||
// Get test files upfront
|
||||
const testFilePaths = await getTestFiles(group.paths);
|
||||
const files: TestFileProgress[] = testFilePaths.map((path) => ({
|
||||
name: getFileName(path),
|
||||
status: "pending" as const,
|
||||
}));
|
||||
|
||||
// Report initial state
|
||||
onProgress?.({
|
||||
status: skipSetup ? "running" : "setup",
|
||||
files,
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
try {
|
||||
let secretKey: string;
|
||||
let fullSlug: string;
|
||||
|
||||
if (skipSetup) {
|
||||
// Try to get org from API
|
||||
const existing = await getExistingOrg({ slug: group.slug });
|
||||
if (!existing) {
|
||||
throw new Error(
|
||||
`Cannot skip setup: org ${group.slug} not found. Run with --setup flag to create it: bun t ${group.slug} --setup`,
|
||||
);
|
||||
}
|
||||
secretKey = existing.secretKey;
|
||||
fullSlug = existing.fullSlug;
|
||||
} else {
|
||||
// 1. Delete existing org
|
||||
try {
|
||||
await deleteOrg({ slug: group.slug });
|
||||
} catch (error: any) {
|
||||
// Ignore delete errors
|
||||
}
|
||||
|
||||
// 2. Create new org
|
||||
const orgResult = await createOrg({
|
||||
slug: group.slug,
|
||||
name: `Test Group: ${group.slug}`,
|
||||
userEmail: "test@gmail.com",
|
||||
});
|
||||
secretKey = orgResult.secretKey;
|
||||
fullSlug = orgResult.fullSlug;
|
||||
|
||||
// 3. Run setup
|
||||
const serverDir = resolve(import.meta.dir, "..", "..");
|
||||
const setupPath = resolve(serverDir, "tests/setupMain.ts");
|
||||
|
||||
const setupProc = spawn(["bun", setupPath], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
cwd: serverDir,
|
||||
env: {
|
||||
...process.env,
|
||||
UNIT_TEST_AUTUMN_SECRET_KEY: secretKey,
|
||||
TESTS_ORG: fullSlug,
|
||||
},
|
||||
});
|
||||
|
||||
// Collect setup output silently
|
||||
let setupOutput = "";
|
||||
const setupDecoder = new TextDecoder();
|
||||
if (setupProc.stdout) {
|
||||
for await (const chunk of setupProc.stdout) {
|
||||
setupOutput += setupDecoder.decode(chunk);
|
||||
}
|
||||
}
|
||||
if (setupProc.stderr) {
|
||||
for await (const chunk of setupProc.stderr) {
|
||||
setupOutput += setupDecoder.decode(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
await setupProc.exited;
|
||||
if (setupProc.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Setup failed for ${group.slug}: ${setupOutput.slice(0, 500)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Run tests with real-time progress callbacks
|
||||
onProgress?.({
|
||||
status: "running",
|
||||
files,
|
||||
duration: performance.now() - startTime,
|
||||
});
|
||||
|
||||
// Set environment for test execution
|
||||
process.env.UNIT_TEST_AUTUMN_SECRET_KEY = secretKey;
|
||||
process.env.TESTS_ORG = fullSlug;
|
||||
|
||||
// Run tests with progress callbacks
|
||||
const results = await runTests(group.paths, {
|
||||
maxParallel: 6,
|
||||
progress: {
|
||||
onTestStart: (file) => {
|
||||
const fileName = getFileName(file);
|
||||
const fileIndex = files.findIndex((f) => f.name === fileName);
|
||||
if (fileIndex !== -1) {
|
||||
files[fileIndex].status = "running";
|
||||
onProgress?.({
|
||||
status: "running",
|
||||
files: [...files],
|
||||
duration: performance.now() - startTime,
|
||||
});
|
||||
}
|
||||
},
|
||||
onTestComplete: (file, result) => {
|
||||
const fileName = getFileName(file);
|
||||
const fileIndex = files.findIndex((f) => f.name === fileName);
|
||||
if (fileIndex !== -1) {
|
||||
files[fileIndex].status = result.status;
|
||||
files[fileIndex].duration = result.duration;
|
||||
if (result.error) {
|
||||
files[fileIndex].error = result.error;
|
||||
}
|
||||
if (result.output) {
|
||||
files[fileIndex].output = result.output;
|
||||
}
|
||||
onProgress?.({
|
||||
status: "running",
|
||||
files: [...files],
|
||||
duration: performance.now() - startTime,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const duration = performance.now() - startTime;
|
||||
const success = results.every((r) => r.status === "passed");
|
||||
|
||||
onProgress?.({
|
||||
status: success ? "passed" : "failed",
|
||||
files,
|
||||
duration,
|
||||
});
|
||||
|
||||
return {
|
||||
group,
|
||||
success,
|
||||
output,
|
||||
duration,
|
||||
files,
|
||||
error: success ? undefined : "One or more tests failed",
|
||||
};
|
||||
} catch (error: any) {
|
||||
const duration = performance.now() - startTime;
|
||||
|
||||
onProgress?.({
|
||||
status: "failed",
|
||||
files,
|
||||
duration,
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
return {
|
||||
group,
|
||||
success: false,
|
||||
output,
|
||||
error: error.message,
|
||||
duration,
|
||||
files,
|
||||
};
|
||||
}
|
||||
}
|
||||
141
server/tests/testRunner/outputParser.ts
Normal file
141
server/tests/testRunner/outputParser.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Parses test output to extract structured failure information
|
||||
*/
|
||||
|
||||
export type TestFailure = {
|
||||
testFile: string;
|
||||
testName: string;
|
||||
errorMessage: string;
|
||||
errorLocation?: string;
|
||||
stackTrace?: string;
|
||||
};
|
||||
|
||||
export type TestSummary = {
|
||||
totalFiles: number;
|
||||
passedFiles: number;
|
||||
failedFiles: number;
|
||||
totalTests: number;
|
||||
passedTests: number;
|
||||
failedTests: number;
|
||||
failures: TestFailure[];
|
||||
duration: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses bun test output to extract failure information
|
||||
*/
|
||||
export function parseTestOutput(output: string): TestSummary {
|
||||
const lines = output.split("\n");
|
||||
const failures: TestFailure[] = [];
|
||||
|
||||
let totalFiles = 0;
|
||||
let failedFiles = 0;
|
||||
let totalTests = 0;
|
||||
let passedTests = 0;
|
||||
let failedTests = 0;
|
||||
let duration = "0s";
|
||||
|
||||
// Extract summary statistics
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Match: "Ran X tests across Y file(s). [Zs]"
|
||||
const ranMatch = line.match(/Ran (\d+) tests across (\d+) file/);
|
||||
if (ranMatch) {
|
||||
totalTests += Number.parseInt(ranMatch[1]);
|
||||
totalFiles += Number.parseInt(ranMatch[2]);
|
||||
}
|
||||
|
||||
// Match: "X pass"
|
||||
const passMatch = line.match(/^\s*(\d+) pass/);
|
||||
if (passMatch) {
|
||||
passedTests += Number.parseInt(passMatch[1]);
|
||||
}
|
||||
|
||||
// Match: "X fail"
|
||||
const failMatch = line.match(/^\s*(\d+) fail/);
|
||||
if (failMatch) {
|
||||
failedTests += Number.parseInt(failMatch[1]);
|
||||
}
|
||||
|
||||
// Match duration in summary
|
||||
const durationMatch = line.match(/\[(\d+\.\d+s)\]/);
|
||||
if (durationMatch) {
|
||||
duration = durationMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
passedFiles = totalFiles - failedFiles;
|
||||
|
||||
// Extract failure details
|
||||
let currentTestFile = "";
|
||||
let inFailureSection = false;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Detect test file being processed
|
||||
const fileMatch = line.match(/tests\/[\w\/.-]+\.test\.ts:/);
|
||||
if (fileMatch) {
|
||||
currentTestFile = fileMatch[0].replace(":", "");
|
||||
}
|
||||
|
||||
// Detect failure markers
|
||||
if (line.includes("(fail)")) {
|
||||
const failMatch = line.match(/\(fail\)\s+(.+?)\s+\[(\d+\.\d+ms)\]/);
|
||||
if (failMatch) {
|
||||
const testName = failMatch[1];
|
||||
|
||||
// Look backwards for error message
|
||||
let errorMessage = "";
|
||||
let errorLocation = "";
|
||||
|
||||
for (let j = i - 1; j >= Math.max(0, i - 20); j--) {
|
||||
const prevLine = lines[j];
|
||||
|
||||
// Find the error line (starts with "error:")
|
||||
if (prevLine.startsWith("error:")) {
|
||||
errorMessage = prevLine.replace("error:", "").trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Look forward for stack trace location
|
||||
for (let j = i + 1; j < Math.min(lines.length, i + 10); j++) {
|
||||
const nextLine = lines[j];
|
||||
if (nextLine.includes("at ") && nextLine.includes(".ts:")) {
|
||||
errorLocation = nextLine.trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
failures.push({
|
||||
testFile: currentTestFile,
|
||||
testName,
|
||||
errorMessage,
|
||||
errorLocation,
|
||||
});
|
||||
|
||||
if (currentTestFile && !failedFiles) {
|
||||
failedFiles++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate failed files from failures
|
||||
const uniqueFailedFiles = new Set(failures.map((f) => f.testFile));
|
||||
failedFiles = uniqueFailedFiles.size;
|
||||
passedFiles = totalFiles - failedFiles;
|
||||
|
||||
return {
|
||||
totalFiles,
|
||||
passedFiles,
|
||||
failedFiles,
|
||||
totalTests,
|
||||
passedTests,
|
||||
failedTests,
|
||||
failures,
|
||||
duration,
|
||||
};
|
||||
}
|
||||
128
server/tests/testRunner/runParallelGroups.ts
Normal file
128
server/tests/testRunner/runParallelGroups.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { resolve } from "path";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
// Load environment variables from server/.env
|
||||
dotenv.config({ path: resolve(import.meta.dir, "..", "..", ".env") });
|
||||
|
||||
import chalk from "chalk";
|
||||
import { testGroups } from "./config.js";
|
||||
import { type GroupResult, runTestGroup } from "./groupRunner.js";
|
||||
|
||||
/**
|
||||
* Main entry point for parallel test execution
|
||||
* Runs all test groups in parallel, each with its own dedicated org
|
||||
*/
|
||||
async function main() {
|
||||
// Check for flags
|
||||
const verbose = process.argv.includes("--verbose");
|
||||
const debug = process.argv.includes("--debug");
|
||||
|
||||
console.log(chalk.bold.cyan("\n╔═══════════════════════════════════════════════════════════════════╗"));
|
||||
console.log(chalk.bold.cyan("║ PARALLEL TEST RUNNER ║"));
|
||||
console.log(chalk.bold.cyan("╚═══════════════════════════════════════════════════════════════════╝\n"));
|
||||
|
||||
console.log(chalk.dim(`Running ${testGroups.length} test group(s) in parallel...\n`));
|
||||
|
||||
if (!verbose && !debug) {
|
||||
console.log(chalk.dim(" 💡 Use --verbose to see all output, --debug for single test debugging\n"));
|
||||
}
|
||||
|
||||
// Validate environment
|
||||
if (!process.env.TEST_ORG_SECRET_KEY) {
|
||||
console.error(chalk.red.bold("ERROR: TEST_ORG_SECRET_KEY environment variable is required"));
|
||||
console.log(
|
||||
chalk.dim(
|
||||
"\nThis should be the secret key of your platform organization that has access to create/delete orgs.",
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// Run all groups in parallel
|
||||
const results = await Promise.all(
|
||||
testGroups.map((group) => runTestGroup({ group, verbose, debug })),
|
||||
);
|
||||
|
||||
const totalDuration = performance.now() - startTime;
|
||||
|
||||
// Calculate totals
|
||||
const successfulGroups = results.filter((r) => r.success);
|
||||
const failedGroups = results.filter((r) => !r.success);
|
||||
|
||||
let totalTests = 0;
|
||||
let totalPassed = 0;
|
||||
let totalFailed = 0;
|
||||
|
||||
for (const result of results) {
|
||||
if (result.testSummary) {
|
||||
totalTests += result.testSummary.totalTests;
|
||||
totalPassed += result.testSummary.passedTests;
|
||||
totalFailed += result.testSummary.failedTests;
|
||||
}
|
||||
}
|
||||
|
||||
// Print summary
|
||||
console.log(chalk.bold.cyan("\n╔═══════════════════════════════════════════════════════════════════╗"));
|
||||
console.log(chalk.bold.cyan("║ SUMMARY ║"));
|
||||
console.log(chalk.bold.cyan("╚═══════════════════════════════════════════════════════════════════╝\n"));
|
||||
|
||||
console.log(chalk.bold(` Total Duration: ${chalk.cyan((totalDuration / 1000).toFixed(2))}s`));
|
||||
console.log(
|
||||
chalk.bold(
|
||||
` Test Groups: ${chalk.green(successfulGroups.length)} passed, ${failedGroups.length > 0 ? chalk.red(failedGroups.length) : chalk.dim(failedGroups.length)} failed`,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.bold(
|
||||
` Tests: ${chalk.green(totalPassed)} passed, ${totalFailed > 0 ? chalk.red(totalFailed) : chalk.dim(totalFailed)} failed (${totalTests} total)`,
|
||||
),
|
||||
);
|
||||
|
||||
// Show details for failed groups
|
||||
if (failedGroups.length > 0) {
|
||||
console.log(chalk.red.bold("\n╔═══════════════════════════════════════════════════════════════════╗"));
|
||||
console.log(chalk.red.bold("║ FAILED TESTS ║"));
|
||||
console.log(chalk.red.bold("╚═══════════════════════════════════════════════════════════════════╝"));
|
||||
|
||||
for (const result of failedGroups) {
|
||||
console.log(chalk.red.bold(`\n ✗ ${result.group.slug}`));
|
||||
console.log(chalk.dim(` Duration: ${(result.duration / 1000).toFixed(2)}s`));
|
||||
|
||||
if (result.testSummary && result.testSummary.failures.length > 0) {
|
||||
console.log(chalk.dim(` Failed: ${result.testSummary.failedTests}/${result.testSummary.totalTests} tests\n`));
|
||||
|
||||
for (const failure of result.testSummary.failures) {
|
||||
console.log(chalk.red(` ┌─ ${failure.testFile || "unknown test"}`));
|
||||
console.log(chalk.red(` │ ${failure.testName}`));
|
||||
console.log(chalk.red(` │`));
|
||||
console.log(chalk.yellow(` │ ${failure.errorMessage}`));
|
||||
if (failure.errorLocation) {
|
||||
console.log(chalk.dim(` │ ${failure.errorLocation}`));
|
||||
}
|
||||
console.log(chalk.red(` └─\n`));
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.dim(` ${result.error}\n`));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.red.bold("╔═══════════════════════════════════════════════════════════════════╗"));
|
||||
console.log(chalk.red.bold(`║ ${failedGroups.length} GROUP(S) FAILED ║`));
|
||||
console.log(chalk.red.bold("╚═══════════════════════════════════════════════════════════════════╝\n"));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.green.bold("\n╔═══════════════════════════════════════════════════════════════════╗"));
|
||||
console.log(chalk.green.bold("║ ✓ ALL TESTS PASSED ║"));
|
||||
console.log(chalk.green.bold("╚═══════════════════════════════════════════════════════════════════╝\n"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(chalk.red.bold("\nFatal error:"), error);
|
||||
process.exit(1);
|
||||
});
|
||||
217
server/tests/testRunner/runParallelGroupsV2.ts
Executable file
217
server/tests/testRunner/runParallelGroupsV2.ts
Executable file
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { resolve } from "path";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
// Load environment variables from server/.env
|
||||
dotenv.config({ path: resolve(import.meta.dir, "..", "..", ".env") });
|
||||
|
||||
import chalk from "chalk";
|
||||
import { testGroups } from "./config.js";
|
||||
import {
|
||||
type GroupProgress,
|
||||
type GroupResult,
|
||||
runTestGroupV2,
|
||||
} from "./groupRunnerV2.js";
|
||||
import {
|
||||
type TestGroupState,
|
||||
createTestRunnerUI,
|
||||
} from "./TestRunnerUI.js";
|
||||
|
||||
/**
|
||||
* Main entry point for parallel test execution with TUI
|
||||
*/
|
||||
async function main() {
|
||||
// Check for flags
|
||||
const verbose = process.argv.includes("--verbose");
|
||||
const debug = process.argv.includes("--debug");
|
||||
|
||||
// Validate environment
|
||||
if (!process.env.TEST_ORG_SECRET_KEY) {
|
||||
console.error(
|
||||
chalk.red.bold(
|
||||
"ERROR: TEST_ORG_SECRET_KEY environment variable is required",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.dim(
|
||||
"\nThis should be the secret key of your platform organization that has access to create/delete orgs.",
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// Initialize UI state
|
||||
const initialGroups: TestGroupState[] = testGroups.map((group) => ({
|
||||
slug: group.slug,
|
||||
status: "pending",
|
||||
files: [],
|
||||
duration: undefined,
|
||||
error: undefined,
|
||||
}));
|
||||
|
||||
const { updateGroup, cleanup } = createTestRunnerUI(initialGroups);
|
||||
|
||||
// Run all groups in parallel with progress updates
|
||||
const results = await Promise.all(
|
||||
testGroups.map((group) =>
|
||||
runTestGroupV2({
|
||||
group,
|
||||
onProgress: (progress: GroupProgress) => {
|
||||
updateGroup(group.slug, {
|
||||
status: progress.status,
|
||||
files: progress.files.map((f) => ({
|
||||
name: f.name,
|
||||
status: f.status,
|
||||
duration: f.duration,
|
||||
error: f.error,
|
||||
})),
|
||||
duration: progress.duration,
|
||||
error: progress.error,
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const totalDuration = performance.now() - startTime;
|
||||
|
||||
// Cleanup UI
|
||||
cleanup();
|
||||
|
||||
// Calculate totals
|
||||
const successfulGroups = results.filter((r) => r.success);
|
||||
const failedGroups = results.filter((r) => !r.success);
|
||||
|
||||
let totalTests = 0;
|
||||
let totalPassed = 0;
|
||||
let totalFailed = 0;
|
||||
|
||||
for (const result of results) {
|
||||
totalTests += result.files.length;
|
||||
totalPassed += result.files.filter((f) => f.status === "passed").length;
|
||||
totalFailed += result.files.filter((f) => f.status === "failed").length;
|
||||
}
|
||||
|
||||
// Print summary
|
||||
console.log(
|
||||
chalk.bold.cyan(
|
||||
"\n╔═══════════════════════════════════════════════════════════════════╗",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.bold.cyan(
|
||||
"║ SUMMARY ║",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.bold.cyan(
|
||||
"╚═══════════════════════════════════════════════════════════════════╝\n",
|
||||
),
|
||||
);
|
||||
|
||||
console.log(
|
||||
chalk.bold(
|
||||
` Total Duration: ${chalk.cyan((totalDuration / 1000).toFixed(2))}s`,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.bold(
|
||||
` Test Groups: ${chalk.green(successfulGroups.length)} passed, ${failedGroups.length > 0 ? chalk.red(failedGroups.length) : chalk.dim(failedGroups.length)} failed`,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.bold(
|
||||
` Tests: ${chalk.green(totalPassed)} passed, ${totalFailed > 0 ? chalk.red(totalFailed) : chalk.dim(totalFailed)} failed (${totalTests} total)`,
|
||||
),
|
||||
);
|
||||
|
||||
// Show details for failed groups
|
||||
if (failedGroups.length > 0) {
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
"\n╔═══════════════════════════════════════════════════════════════════╗",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
"║ FAILED TESTS ║",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
"╚═══════════════════════════════════════════════════════════════════╝",
|
||||
),
|
||||
);
|
||||
|
||||
for (const result of failedGroups) {
|
||||
console.log(chalk.red.bold(`\n ✗ ${result.group.slug}`));
|
||||
console.log(
|
||||
chalk.dim(` Duration: ${(result.duration / 1000).toFixed(2)}s`),
|
||||
);
|
||||
|
||||
const failedFiles = result.files.filter((f) => f.status === "failed");
|
||||
|
||||
if (failedFiles.length > 0) {
|
||||
console.log(
|
||||
chalk.dim(
|
||||
` Failed: ${failedFiles.length}/${result.files.length} tests\n`,
|
||||
),
|
||||
);
|
||||
|
||||
for (const file of failedFiles) {
|
||||
console.log(chalk.red(` ┌─ ${file.name}`));
|
||||
if (file.error) {
|
||||
// Show first line of error
|
||||
const errorLine = file.error.split("\n")[0];
|
||||
console.log(chalk.yellow(` │ ${errorLine.slice(0, 80)}`));
|
||||
}
|
||||
console.log(chalk.red(" └─\n"));
|
||||
}
|
||||
} else if (result.error) {
|
||||
console.log(chalk.dim(` ${result.error}\n`));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
"╔═══════════════════════════════════════════════════════════════════╗",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
`║ ${failedGroups.length} GROUP(S) FAILED ║`,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
"╚═══════════════════════════════════════════════════════════════════╝\n",
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.green.bold(
|
||||
"\n╔═══════════════════════════════════════════════════════════════════╗",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.green.bold(
|
||||
"║ ✓ ALL TESTS PASSED ║",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.green.bold(
|
||||
"╚═══════════════════════════════════════════════════════════════════╝\n",
|
||||
),
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(chalk.red.bold("\nFatal error:"), error);
|
||||
process.exit(1);
|
||||
});
|
||||
504
server/tests/testRunner/runParallelGroupsV3.ts
Executable file
504
server/tests/testRunner/runParallelGroupsV3.ts
Executable file
@@ -0,0 +1,504 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { resolve } from "path";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
// Load environment variables from server/.env
|
||||
dotenv.config({ path: resolve(import.meta.dir, "..", "..", ".env") });
|
||||
|
||||
import chalk from "chalk";
|
||||
import { testGroups } from "./config.js";
|
||||
import {
|
||||
type GroupProgress,
|
||||
type GroupResult,
|
||||
runTestGroupV2,
|
||||
} from "./groupRunnerV2.js";
|
||||
|
||||
type TestFileStatus = "pending" | "running" | "passed" | "failed";
|
||||
|
||||
type TestFile = {
|
||||
name: string;
|
||||
status: TestFileStatus;
|
||||
duration?: number;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type GroupStatus = "pending" | "setup" | "running" | "passed" | "failed";
|
||||
|
||||
type TestGroupState = {
|
||||
slug: string;
|
||||
status: GroupStatus;
|
||||
files: TestFile[];
|
||||
duration?: number;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
class SimpleTUI {
|
||||
private groups: TestGroupState[] = [];
|
||||
private startLine = 0;
|
||||
private renderInterval?: Timer;
|
||||
private spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
private spinnerIndex = 0;
|
||||
private lastRenderedLineCount = 0;
|
||||
|
||||
constructor(groups: TestGroupState[]) {
|
||||
this.groups = groups;
|
||||
}
|
||||
|
||||
start() {
|
||||
// Hide cursor
|
||||
process.stdout.write("\x1B[?25l");
|
||||
|
||||
// Reserve space for rendering
|
||||
const lines = this.calculateLines();
|
||||
for (let i = 0; i < lines; i++) {
|
||||
console.log();
|
||||
}
|
||||
// Move cursor back up
|
||||
process.stdout.write(`\x1B[${lines}A`);
|
||||
this.startLine = 1;
|
||||
|
||||
// Start render loop
|
||||
this.renderInterval = setInterval(() => this.render(), 100);
|
||||
}
|
||||
|
||||
updateGroup(slug: string, update: Partial<TestGroupState>) {
|
||||
const idx = this.groups.findIndex((g) => g.slug === slug);
|
||||
if (idx !== -1) {
|
||||
this.groups[idx] = { ...this.groups[idx], ...update };
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.renderInterval) {
|
||||
clearInterval(this.renderInterval);
|
||||
}
|
||||
// Do one final render to show completed state
|
||||
this.render();
|
||||
// Show cursor
|
||||
process.stdout.write("\x1B[?25h");
|
||||
// Move past output using ACTUAL lines rendered, not max possible
|
||||
process.stdout.write(`\x1B[${this.lastRenderedLineCount}B`);
|
||||
console.log("\n");
|
||||
}
|
||||
|
||||
private calculateLines(): number {
|
||||
// Fixed layout:
|
||||
// 2 lines for header
|
||||
// 7 lines per group (1 for group header, 6 for test files with stack traces)
|
||||
// 6 = 2 files * 3 lines each (file + error + stack)
|
||||
return 2 + (this.groups.length * 7);
|
||||
}
|
||||
|
||||
private render() {
|
||||
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length;
|
||||
const spinner = this.spinnerFrames[this.spinnerIndex];
|
||||
|
||||
let lineNum = this.startLine;
|
||||
|
||||
// Move to start and clear line
|
||||
process.stdout.write(`\x1B[${lineNum};0H\x1B[2K`);
|
||||
// Header (no newline - we'll move cursor manually)
|
||||
process.stdout.write(chalk.bold.cyan("PARALLEL TEST RUNNER"));
|
||||
lineNum++;
|
||||
|
||||
// Stats
|
||||
const completed = this.groups.filter(
|
||||
(g) => g.status === "passed" || g.status === "failed",
|
||||
).length;
|
||||
const passed = this.groups.filter((g) => g.status === "passed").length;
|
||||
const failed = this.groups.filter((g) => g.status === "failed").length;
|
||||
|
||||
let totalTests = 0;
|
||||
let passedTests = 0;
|
||||
let failedTests = 0;
|
||||
for (const g of this.groups) {
|
||||
totalTests += g.files.length;
|
||||
passedTests += g.files.filter((f) => f.status === "passed").length;
|
||||
failedTests += g.files.filter((f) => f.status === "failed").length;
|
||||
}
|
||||
|
||||
process.stdout.write(`\x1B[${lineNum};0H\x1B[2K`);
|
||||
process.stdout.write(
|
||||
`Groups: ${completed}/${this.groups.length} | ` +
|
||||
`${chalk.green(`✓ ${passed}`)} | ` +
|
||||
`${failed > 0 ? chalk.red(`✗ ${failed}`) : chalk.dim(`✗ ${failed}`)} | ` +
|
||||
`Tests: ${passedTests + failedTests}/${totalTests} | ` +
|
||||
`${chalk.green(`✓ ${passedTests}`)} | ` +
|
||||
`${failedTests > 0 ? chalk.red(`✗ ${failedTests}`) : chalk.dim(`✗ ${failedTests}`)}\n\n`,
|
||||
);
|
||||
lineNum += 2;
|
||||
|
||||
// Groups
|
||||
for (const group of this.groups) {
|
||||
process.stdout.write(`\x1B[${lineNum};0H\x1B[2K`);
|
||||
|
||||
let icon = "";
|
||||
let statusText = "";
|
||||
|
||||
switch (group.status) {
|
||||
case "pending":
|
||||
icon = chalk.gray("…");
|
||||
statusText = "Pending";
|
||||
break;
|
||||
case "setup":
|
||||
icon = chalk.cyan(spinner);
|
||||
statusText = "Setting up";
|
||||
break;
|
||||
case "running":
|
||||
icon = chalk.cyan(spinner);
|
||||
statusText = "Running";
|
||||
break;
|
||||
case "passed":
|
||||
icon = chalk.green("✓");
|
||||
statusText = "Passed";
|
||||
break;
|
||||
case "failed":
|
||||
icon = chalk.red("✗");
|
||||
statusText = "Failed";
|
||||
break;
|
||||
}
|
||||
|
||||
const completedCount =
|
||||
group.files.filter(
|
||||
(f) => f.status === "passed" || f.status === "failed",
|
||||
).length;
|
||||
const totalCount = group.files.length;
|
||||
const failedCount = group.files.filter((f) => f.status === "failed").length;
|
||||
|
||||
let groupLine = `${icon} ${chalk.bold(group.slug)} - ${statusText}`;
|
||||
if (group.duration) {
|
||||
groupLine += chalk.dim(` (${(group.duration / 1000).toFixed(1)}s)`);
|
||||
}
|
||||
|
||||
// Show progress for running/passed/failed groups
|
||||
if (group.status !== "pending" && totalCount > 0) {
|
||||
groupLine += chalk.dim(` | ${completedCount}/${totalCount} completed`);
|
||||
if (failedCount > 0) {
|
||||
groupLine += chalk.red(` [${failedCount} failed]`);
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write(groupLine);
|
||||
lineNum++;
|
||||
|
||||
// Show failed files only
|
||||
if (group.status !== "pending" && failedCount > 0) {
|
||||
const failedFiles = group.files.filter((f) => f.status === "failed");
|
||||
for (const file of failedFiles.slice(0, 2)) {
|
||||
process.stdout.write(`\x1B[${lineNum};0H\x1B[2K`);
|
||||
let fileLine = ` ${chalk.red("✗")} ${file.name}`;
|
||||
if (file.error) {
|
||||
// Show first meaningful line of error (up to 80 chars)
|
||||
const errorLines = file.error.split("\n").filter((l) => l.trim());
|
||||
const shortError = errorLines[0]?.slice(0, 80) || "Test failed";
|
||||
fileLine += chalk.yellow(` → ${shortError}`);
|
||||
}
|
||||
process.stdout.write(fileLine);
|
||||
lineNum++;
|
||||
|
||||
// Show stack trace location if available
|
||||
if (file.error) {
|
||||
const errorLines = file.error.split("\n");
|
||||
const stackLine = errorLines.find((l) =>
|
||||
l.trim().startsWith("at "),
|
||||
);
|
||||
if (stackLine) {
|
||||
// Extract file path and line number from stack trace
|
||||
// Format: "at functionName (/path/to/file.ts:123:45)"
|
||||
const match = stackLine.match(/\((.+?):(\d+):(\d+)\)/);
|
||||
if (match) {
|
||||
const [, filePath, line] = match;
|
||||
const fileName = filePath.split("/").pop();
|
||||
process.stdout.write(`\x1B[${lineNum};0H\x1B[2K`);
|
||||
process.stdout.write(chalk.dim(` ${fileName}:${line}`));
|
||||
lineNum++;
|
||||
} else {
|
||||
// Clear the line if no stack found
|
||||
process.stdout.write(`\x1B[${lineNum};0H\x1B[2K`);
|
||||
lineNum++;
|
||||
}
|
||||
} else {
|
||||
// Clear the line if no stack found
|
||||
process.stdout.write(`\x1B[${lineNum};0H\x1B[2K`);
|
||||
lineNum++;
|
||||
}
|
||||
} else {
|
||||
// Clear the line if no error
|
||||
process.stdout.write(`\x1B[${lineNum};0H\x1B[2K`);
|
||||
lineNum++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Clear the file display lines (now 3 lines per file, 2 files max = 6 lines)
|
||||
for (let i = 0; i < 6; i++) {
|
||||
process.stdout.write(`\x1B[${lineNum};0H\x1B[2K`);
|
||||
lineNum++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear remaining lines
|
||||
const maxLines = this.calculateLines();
|
||||
while (lineNum < this.startLine + maxLines) {
|
||||
process.stdout.write(`\x1B[${lineNum};0H\x1B[2K`);
|
||||
lineNum++;
|
||||
}
|
||||
|
||||
// Track how many lines we actually used (minus startLine offset)
|
||||
this.lastRenderedLineCount = lineNum - this.startLine;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point for parallel test execution with TUI
|
||||
*/
|
||||
async function main() {
|
||||
// Validate environment
|
||||
if (!process.env.TEST_ORG_SECRET_KEY) {
|
||||
console.error(
|
||||
chalk.red.bold(
|
||||
"ERROR: TEST_ORG_SECRET_KEY environment variable is required",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.dim(
|
||||
"\nThis should be the secret key of your platform organization that has access to create/delete orgs.",
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Parse CLI arguments for targeted group execution
|
||||
const args = process.argv.slice(2);
|
||||
const targetedSlugs = args.filter((arg) => !arg.startsWith("--"));
|
||||
const forceSetup = args.includes("--setup");
|
||||
|
||||
// Filter test groups based on CLI args
|
||||
let groupsToRun = testGroups;
|
||||
// When targeting specific groups, skip setup by default unless --setup is passed
|
||||
const skipSetup = targetedSlugs.length > 0 && !forceSetup;
|
||||
|
||||
if (targetedSlugs.length > 0) {
|
||||
groupsToRun = testGroups.filter((g) => targetedSlugs.includes(g.slug));
|
||||
if (groupsToRun.length === 0) {
|
||||
console.error(
|
||||
chalk.red.bold(
|
||||
`\nERROR: No matching test groups found for: ${targetedSlugs.join(", ")}`,
|
||||
),
|
||||
);
|
||||
console.log(chalk.dim("\nAvailable groups:"));
|
||||
for (const group of testGroups) {
|
||||
console.log(chalk.dim(` - ${group.slug}`));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`\nRunning targeted groups: ${groupsToRun.map((g) => g.slug).join(", ")}`,
|
||||
),
|
||||
);
|
||||
if (skipSetup) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
"Skipping org setup (using existing test orgs). Use --setup to force recreate.\n",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.yellow("Recreating test orgs from scratch...\n"));
|
||||
}
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// Initialize UI state
|
||||
const initialGroups: TestGroupState[] = groupsToRun.map((group) => ({
|
||||
slug: group.slug,
|
||||
status: "pending",
|
||||
files: [],
|
||||
duration: undefined,
|
||||
error: undefined,
|
||||
}));
|
||||
|
||||
const tui = new SimpleTUI(initialGroups);
|
||||
tui.start();
|
||||
|
||||
// Run all groups in parallel with progress updates
|
||||
const results = await Promise.all(
|
||||
groupsToRun.map((group) =>
|
||||
runTestGroupV2({
|
||||
group,
|
||||
skipSetup,
|
||||
onProgress: (progress: GroupProgress) => {
|
||||
tui.updateGroup(group.slug, {
|
||||
status: progress.status,
|
||||
files: progress.files.map((f) => ({
|
||||
name: f.name,
|
||||
status: f.status,
|
||||
duration: f.duration,
|
||||
error: f.error,
|
||||
output: f.output,
|
||||
})),
|
||||
duration: progress.duration,
|
||||
error: progress.error,
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const totalDuration = performance.now() - startTime;
|
||||
|
||||
// Stop TUI
|
||||
tui.stop();
|
||||
|
||||
// Calculate totals
|
||||
const successfulGroups = results.filter((r) => r.success);
|
||||
const failedGroups = results.filter((r) => !r.success);
|
||||
|
||||
let totalTests = 0;
|
||||
let totalPassed = 0;
|
||||
let totalFailed = 0;
|
||||
|
||||
for (const result of results) {
|
||||
totalTests += result.files.length;
|
||||
totalPassed += result.files.filter((f) => f.status === "passed").length;
|
||||
totalFailed += result.files.filter((f) => f.status === "failed").length;
|
||||
}
|
||||
|
||||
// Print summary
|
||||
console.log(
|
||||
chalk.bold.cyan(
|
||||
"\n╔═══════════════════════════════════════════════════════════════════╗",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.bold.cyan(
|
||||
"║ SUMMARY ║",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.bold.cyan(
|
||||
"╚═══════════════════════════════════════════════════════════════════╝\n",
|
||||
),
|
||||
);
|
||||
|
||||
console.log(
|
||||
chalk.bold(
|
||||
` Total Duration: ${chalk.cyan((totalDuration / 1000).toFixed(2))}s`,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.bold(
|
||||
` Test Groups: ${chalk.green(successfulGroups.length)} passed, ${failedGroups.length > 0 ? chalk.red(failedGroups.length) : chalk.dim(failedGroups.length)} failed`,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.bold(
|
||||
` Tests: ${chalk.green(totalPassed)} passed, ${totalFailed > 0 ? chalk.red(totalFailed) : chalk.dim(totalFailed)} failed (${totalTests} total)`,
|
||||
),
|
||||
);
|
||||
|
||||
// Show details for failed groups
|
||||
if (failedGroups.length > 0) {
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
"\n╔═══════════════════════════════════════════════════════════════════╗",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
"║ FAILED TESTS ║",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
"╚═══════════════════════════════════════════════════════════════════╝",
|
||||
),
|
||||
);
|
||||
|
||||
for (const result of failedGroups) {
|
||||
console.log(chalk.red.bold(`\n ✗ ${result.group.slug}`));
|
||||
console.log(
|
||||
chalk.dim(` Duration: ${(result.duration / 1000).toFixed(2)}s`),
|
||||
);
|
||||
|
||||
const failedFiles = result.files.filter((f) => f.status === "failed");
|
||||
|
||||
if (failedFiles.length > 0) {
|
||||
console.log(
|
||||
chalk.dim(
|
||||
` Failed: ${failedFiles.length}/${result.files.length} tests\n`,
|
||||
),
|
||||
);
|
||||
|
||||
for (const file of failedFiles) {
|
||||
console.log(chalk.red(` ┌─ ${file.name}`));
|
||||
if (file.error) {
|
||||
// Show all error lines with proper indentation
|
||||
const errorLines = file.error.split("\n");
|
||||
for (const line of errorLines) {
|
||||
if (line.trim()) {
|
||||
console.log(chalk.yellow(` │ ${line}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show full test output if available
|
||||
if (file.output) {
|
||||
console.log(chalk.red(" │"));
|
||||
console.log(chalk.cyan(" │ === Full Test Output ==="));
|
||||
const outputLines = file.output.split("\n");
|
||||
for (const line of outputLines) {
|
||||
if (line.trim()) {
|
||||
console.log(chalk.dim(` │ ${line}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(chalk.red(" └─\n"));
|
||||
}
|
||||
} else if (result.error) {
|
||||
console.log(chalk.dim(` ${result.error}\n`));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
"╔═══════════════════════════════════════════════════════════════════╗",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
`║ ${failedGroups.length} GROUP(S) FAILED ║`,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
"╚═══════════════════════════════════════════════════════════════════╝\n",
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.green.bold(
|
||||
"\n╔═══════════════════════════════════════════════════════════════════╗",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.green.bold(
|
||||
"║ ✓ ALL TESTS PASSED ║",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.green.bold(
|
||||
"╚═══════════════════════════════════════════════════════════════════╝\n",
|
||||
),
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(chalk.red.bold("\nFatal error:"), error);
|
||||
process.exit(1);
|
||||
});
|
||||
734
server/tests/testRunner/runTests.ts
Executable file
734
server/tests/testRunner/runTests.ts
Executable file
@@ -0,0 +1,734 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { spawn } from "bun";
|
||||
import chalk from "chalk";
|
||||
import { readdir } from "fs/promises";
|
||||
import pLimit from "p-limit";
|
||||
import { basename, resolve } from "path";
|
||||
|
||||
interface TestResult {
|
||||
file: string;
|
||||
status: "pending" | "running" | "passed" | "failed";
|
||||
output: string;
|
||||
duration: number;
|
||||
error?: string;
|
||||
lastTestName?: string;
|
||||
}
|
||||
|
||||
class TestRunner {
|
||||
private results: Map<string, TestResult> = new Map();
|
||||
private testFiles: string[] = [];
|
||||
private maxParallel: number = 6;
|
||||
private spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
private spinnerIndex = 0;
|
||||
private renderInterval?: Timer;
|
||||
private startLine = 0;
|
||||
private compactMode: boolean = false;
|
||||
private silentMode: boolean = false;
|
||||
private lastRenderedLines = 0;
|
||||
|
||||
constructor({
|
||||
maxParallel,
|
||||
compactMode,
|
||||
silentMode,
|
||||
}: {
|
||||
maxParallel?: number;
|
||||
compactMode?: boolean;
|
||||
silentMode?: boolean;
|
||||
} = {}) {
|
||||
if (maxParallel) this.maxParallel = maxParallel;
|
||||
if (compactMode) this.compactMode = compactMode;
|
||||
if (silentMode) this.silentMode = silentMode;
|
||||
}
|
||||
|
||||
async collectTestFiles(paths: string[]): Promise<string[]> {
|
||||
const testFiles: string[] = [];
|
||||
|
||||
for (const path of paths) {
|
||||
const resolvedPath = resolve(process.cwd(), path);
|
||||
|
||||
// Check if it's a specific test file
|
||||
if (path.endsWith(".test.ts")) {
|
||||
testFiles.push(resolvedPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise treat it as a directory
|
||||
try {
|
||||
const files = await readdir(resolvedPath);
|
||||
for (const file of files) {
|
||||
if (file.endsWith(".test.ts")) {
|
||||
testFiles.push(resolve(resolvedPath, file));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error reading directory ${path}:`), error);
|
||||
}
|
||||
}
|
||||
|
||||
return testFiles;
|
||||
}
|
||||
|
||||
private extractLastTest(output: string): string | null {
|
||||
const lines = output.split("\n");
|
||||
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
const testMatch = line.match(/^[✓✗]\s+(.+?)(?:\s+\[\d+\.\d+m?s\])?$/);
|
||||
if (testMatch) {
|
||||
return testMatch[1];
|
||||
}
|
||||
|
||||
const bunTestMatch = line.match(/test\s+"([^"]+)"/);
|
||||
if (bunTestMatch) {
|
||||
return bunTestMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private truncateTestName(name: string, maxLength: number = 50): string {
|
||||
if (name.length <= maxLength) return name;
|
||||
return name.substring(0, maxLength - 3) + "...";
|
||||
}
|
||||
|
||||
private hideCursor() {
|
||||
process.stdout.write("\x1B[?25l");
|
||||
}
|
||||
|
||||
private showCursor() {
|
||||
process.stdout.write("\x1B[?25h");
|
||||
}
|
||||
|
||||
private moveCursor(line: number, col: number = 0) {
|
||||
process.stdout.write(`\x1B[${line};${col}H`);
|
||||
}
|
||||
|
||||
private clearLine() {
|
||||
process.stdout.write("\x1B[2K");
|
||||
}
|
||||
|
||||
private getSpacesNeeded(): number {
|
||||
if (!this.compactMode) {
|
||||
return this.testFiles.length + 3;
|
||||
}
|
||||
|
||||
// Compact mode: dynamically calculate based on content
|
||||
// Base: 10 lines for headers, stats, spacing
|
||||
// + 3 lines for recently completed
|
||||
// + failed tests * 4 (name + 2 error lines + spacing)
|
||||
// + running tests
|
||||
const failedCount = Array.from(this.results.values()).filter(
|
||||
(r) => r.status === "failed",
|
||||
).length;
|
||||
const runningCount = Array.from(this.results.values()).filter(
|
||||
(r) => r.status === "running",
|
||||
).length;
|
||||
|
||||
return Math.min(
|
||||
10 + 3 + failedCount * 4 + Math.min(runningCount, 6),
|
||||
30, // Cap at 30 lines
|
||||
);
|
||||
}
|
||||
|
||||
private render() {
|
||||
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length;
|
||||
const spinner = this.spinnerFrames[this.spinnerIndex];
|
||||
|
||||
if (this.compactMode) {
|
||||
// Compact mode: show completed, failed, running tests, then stats
|
||||
let lineNum = this.startLine;
|
||||
|
||||
const completed = Array.from(this.results.values()).filter(
|
||||
(r) => r.status === "passed" || r.status === "failed",
|
||||
).length;
|
||||
const passed = Array.from(this.results.values()).filter(
|
||||
(r) => r.status === "passed",
|
||||
).length;
|
||||
const failed = Array.from(this.results.values()).filter(
|
||||
(r) => r.status === "failed",
|
||||
).length;
|
||||
|
||||
// Show recently completed tests (last 3)
|
||||
const passedTests = Array.from(this.results.entries())
|
||||
.filter(([_, result]) => result.status === "passed")
|
||||
.slice(-3); // Get last 3 completed
|
||||
|
||||
if (passedTests.length > 0) {
|
||||
this.moveCursor(lineNum, 0);
|
||||
this.clearLine();
|
||||
process.stdout.write(
|
||||
chalk.green.bold(`Recently Completed (${passed} total):\n`),
|
||||
);
|
||||
lineNum++;
|
||||
|
||||
for (const [file] of passedTests) {
|
||||
this.moveCursor(lineNum, 0);
|
||||
this.clearLine();
|
||||
const testName = basename(file);
|
||||
process.stdout.write(
|
||||
` ${chalk.green("✓")} ${chalk.dim(testName)}\n`,
|
||||
);
|
||||
lineNum++;
|
||||
}
|
||||
|
||||
// Add blank line
|
||||
this.moveCursor(lineNum, 0);
|
||||
this.clearLine();
|
||||
process.stdout.write("\n");
|
||||
lineNum++;
|
||||
}
|
||||
|
||||
// Show failed tests
|
||||
const failedTests = Array.from(this.results.entries()).filter(
|
||||
([_, result]) => result.status === "failed",
|
||||
);
|
||||
|
||||
if (failedTests.length > 0) {
|
||||
this.moveCursor(lineNum, 0);
|
||||
this.clearLine();
|
||||
process.stdout.write(
|
||||
chalk.red.bold(`Failed (${failedTests.length}):\n`),
|
||||
);
|
||||
lineNum++;
|
||||
|
||||
for (const [file, result] of failedTests) {
|
||||
this.moveCursor(lineNum, 0);
|
||||
this.clearLine();
|
||||
const testName = basename(file);
|
||||
process.stdout.write(` ${chalk.red("✗")} ${testName}\n`);
|
||||
lineNum++;
|
||||
|
||||
// Show first 2 lines of error
|
||||
if (result.error) {
|
||||
const errorLines = result.error.split("\n").filter((l) => l.trim());
|
||||
const displayLines = errorLines.slice(0, 2);
|
||||
for (const line of displayLines) {
|
||||
this.moveCursor(lineNum, 0);
|
||||
this.clearLine();
|
||||
const truncated =
|
||||
line.length > 80 ? line.substring(0, 77) + "..." : line;
|
||||
process.stdout.write(` ${chalk.dim(truncated)}\n`);
|
||||
lineNum++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add blank line after failed tests
|
||||
this.moveCursor(lineNum, 0);
|
||||
this.clearLine();
|
||||
process.stdout.write("\n");
|
||||
lineNum++;
|
||||
}
|
||||
|
||||
// Show currently running tests
|
||||
const runningTests = Array.from(this.results.entries()).filter(
|
||||
([_, result]) => result.status === "running",
|
||||
);
|
||||
|
||||
if (runningTests.length > 0) {
|
||||
this.moveCursor(lineNum, 0);
|
||||
this.clearLine();
|
||||
process.stdout.write(
|
||||
chalk.cyan.bold(`Running (${runningTests.length}):\n`),
|
||||
);
|
||||
lineNum++;
|
||||
|
||||
for (const [file, result] of runningTests) {
|
||||
this.moveCursor(lineNum, 0);
|
||||
this.clearLine();
|
||||
const testName = basename(file);
|
||||
let displayText = ` ${chalk.cyan(spinner)} ${testName}`;
|
||||
if (result.lastTestName) {
|
||||
const truncated = this.truncateTestName(result.lastTestName, 40);
|
||||
displayText += chalk.dim(` › ${truncated}`);
|
||||
}
|
||||
process.stdout.write(`${displayText}\n`);
|
||||
lineNum++;
|
||||
}
|
||||
|
||||
// Add blank line after running tests
|
||||
this.moveCursor(lineNum, 0);
|
||||
this.clearLine();
|
||||
process.stdout.write("\n");
|
||||
lineNum++;
|
||||
}
|
||||
|
||||
// Show stats line
|
||||
this.moveCursor(lineNum, 0);
|
||||
this.clearLine();
|
||||
process.stdout.write(
|
||||
`${chalk.cyan(spinner)} Progress: ${chalk.bold(`${completed}/${this.testFiles.length}`)} | ` +
|
||||
`${chalk.green(`✓ ${passed}`)} | ` +
|
||||
`${failed > 0 ? chalk.red(`✗ ${failed}`) : chalk.dim(`✗ ${failed}`)}\n`,
|
||||
);
|
||||
lineNum++;
|
||||
|
||||
// Clear any remaining lines from previous renders
|
||||
const maxLines = this.getSpacesNeeded();
|
||||
while (lineNum < maxLines) {
|
||||
this.moveCursor(lineNum, 0);
|
||||
this.clearLine();
|
||||
lineNum++;
|
||||
}
|
||||
|
||||
// Track how many lines we actually used
|
||||
this.lastRenderedLines = lineNum - this.startLine;
|
||||
} else {
|
||||
// Full mode: show all tests
|
||||
let lineNum = this.startLine;
|
||||
|
||||
for (const file of this.testFiles) {
|
||||
const result = this.results.get(file);
|
||||
if (!result) continue;
|
||||
|
||||
this.moveCursor(lineNum, 0);
|
||||
this.clearLine();
|
||||
|
||||
const testName = basename(file);
|
||||
let statusIcon: string;
|
||||
let displayText: string;
|
||||
|
||||
switch (result.status) {
|
||||
case "pending":
|
||||
statusIcon = chalk.dim("⋯");
|
||||
displayText = chalk.dim(testName);
|
||||
break;
|
||||
case "running":
|
||||
statusIcon = chalk.cyan(spinner);
|
||||
displayText = testName;
|
||||
if (result.lastTestName) {
|
||||
const truncated = this.truncateTestName(result.lastTestName);
|
||||
displayText += chalk.dim(` › ${truncated}`);
|
||||
}
|
||||
break;
|
||||
case "passed":
|
||||
statusIcon = chalk.green("✓");
|
||||
displayText = chalk.dim(testName);
|
||||
break;
|
||||
case "failed":
|
||||
statusIcon = chalk.red("✗");
|
||||
displayText = testName;
|
||||
break;
|
||||
}
|
||||
|
||||
process.stdout.write(`${statusIcon} ${displayText}\n`);
|
||||
lineNum++;
|
||||
}
|
||||
|
||||
// Summary line
|
||||
const completed = Array.from(this.results.values()).filter(
|
||||
(r) => r.status === "passed" || r.status === "failed",
|
||||
).length;
|
||||
const failed = Array.from(this.results.values()).filter(
|
||||
(r) => r.status === "failed",
|
||||
).length;
|
||||
const running = Array.from(this.results.values()).filter(
|
||||
(r) => r.status === "running",
|
||||
).length;
|
||||
|
||||
this.moveCursor(lineNum + 1, 0);
|
||||
this.clearLine();
|
||||
if (running > 0) {
|
||||
process.stdout.write(
|
||||
chalk.dim(
|
||||
`Running: ${running} | Completed: ${completed}/${this.testFiles.length} | Failed: ${failed}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Track how many lines we actually used
|
||||
this.lastRenderedLines = lineNum + 2 - this.startLine;
|
||||
}
|
||||
}
|
||||
|
||||
async runTest(file: string): Promise<void> {
|
||||
const startTime = performance.now();
|
||||
|
||||
// Initialize as running
|
||||
const result: TestResult = {
|
||||
file,
|
||||
status: "running",
|
||||
output: "",
|
||||
duration: 0,
|
||||
};
|
||||
this.results.set(file, result);
|
||||
|
||||
try {
|
||||
const proc = spawn(["bun", "test", "--timeout", "0", file], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
let output = "";
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
if (proc.stdout) {
|
||||
for await (const chunk of proc.stdout) {
|
||||
const text = decoder.decode(chunk);
|
||||
output += text;
|
||||
|
||||
// Only stream output if not in silent mode
|
||||
if (!this.silentMode) {
|
||||
process.stdout.write(text);
|
||||
}
|
||||
|
||||
// Update last test name
|
||||
const lastTest = this.extractLastTest(output);
|
||||
if (lastTest) {
|
||||
result.lastTestName = lastTest;
|
||||
result.output = output;
|
||||
this.results.set(file, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (proc.stderr) {
|
||||
for await (const chunk of proc.stderr) {
|
||||
const text = decoder.decode(chunk);
|
||||
output += text;
|
||||
|
||||
// Only stream errors if not in silent mode
|
||||
if (!this.silentMode) {
|
||||
process.stderr.write(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await proc.exited;
|
||||
const duration = performance.now() - startTime;
|
||||
|
||||
const fileName = file.split("/").pop() || file;
|
||||
|
||||
if (proc.exitCode === 0) {
|
||||
this.results.set(file, {
|
||||
...result,
|
||||
status: "passed",
|
||||
output,
|
||||
duration,
|
||||
});
|
||||
// In silent mode, immediately output completion for real-time tracking
|
||||
if (this.silentMode) {
|
||||
console.log(`✓ ${fileName}`);
|
||||
}
|
||||
} else {
|
||||
this.results.set(file, {
|
||||
...result,
|
||||
status: "failed",
|
||||
output,
|
||||
duration,
|
||||
error: this.extractError(output),
|
||||
});
|
||||
// In silent mode, immediately output failure for real-time tracking
|
||||
if (this.silentMode) {
|
||||
console.log(`✗ ${fileName}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const duration = performance.now() - startTime;
|
||||
const fileName = file.split("/").pop() || file;
|
||||
this.results.set(file, {
|
||||
...result,
|
||||
status: "failed",
|
||||
output: "",
|
||||
duration,
|
||||
error: String(error),
|
||||
});
|
||||
if (this.silentMode) {
|
||||
console.log(`✗ ${fileName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extractError(output: string): string {
|
||||
const lines = output.split("\n");
|
||||
const errorLines: string[] = [];
|
||||
let inError = false;
|
||||
let capturedLines = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
if (
|
||||
line.includes("error:") ||
|
||||
line.includes("Error:") ||
|
||||
line.includes("Expected:") ||
|
||||
line.includes("Received:") ||
|
||||
line.includes("AssertionError")
|
||||
) {
|
||||
inError = true;
|
||||
}
|
||||
|
||||
if (inError) {
|
||||
errorLines.push(line);
|
||||
capturedLines++;
|
||||
|
||||
if (capturedLines > 20) break;
|
||||
}
|
||||
|
||||
if (line.match(/^[\s]*✗/)) {
|
||||
errorLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return errorLines.length > 0 ? errorLines.join("\n").trim() : output;
|
||||
}
|
||||
|
||||
private cleanup() {
|
||||
if (this.renderInterval) {
|
||||
clearInterval(this.renderInterval);
|
||||
}
|
||||
this.showCursor();
|
||||
}
|
||||
|
||||
private handleInterrupt() {
|
||||
this.cleanup();
|
||||
|
||||
// Move cursor past all output (use actual rendered lines in compact mode)
|
||||
const linesToMove = this.compactMode
|
||||
? this.lastRenderedLines
|
||||
: this.getSpacesNeeded();
|
||||
process.stdout.write(`\x1B[${linesToMove}B`);
|
||||
console.log("\n");
|
||||
|
||||
console.log(chalk.yellow.bold("\n⚠ Tests interrupted by user (Ctrl+C)\n"));
|
||||
|
||||
// Print summary of what we have so far
|
||||
const failedTests = Array.from(this.results.values()).filter(
|
||||
(t) => t.status === "failed",
|
||||
);
|
||||
const completedTests = Array.from(this.results.values()).filter(
|
||||
(t) => t.status === "passed" || t.status === "failed",
|
||||
);
|
||||
|
||||
console.log(
|
||||
chalk.dim(
|
||||
`Completed: ${completedTests.length}/${this.testFiles.length} tests before interruption`,
|
||||
),
|
||||
);
|
||||
|
||||
if (failedTests.length > 0) {
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
`\n${"═".repeat(70)}\n FAILED TESTS (${failedTests.length})\n${"═".repeat(70)}\n`,
|
||||
),
|
||||
);
|
||||
|
||||
for (const test of failedTests) {
|
||||
const testName = basename(test.file);
|
||||
console.log(chalk.red.bold(`\n✗ ${testName}`));
|
||||
console.log(chalk.dim("─".repeat(70)));
|
||||
|
||||
if (test.error) {
|
||||
const errorLines = test.error.split("\n");
|
||||
for (const line of errorLines) {
|
||||
if (line.trim()) {
|
||||
if (line.includes("Expected:") || line.includes("Received:")) {
|
||||
console.log(chalk.yellow(line));
|
||||
} else if (line.includes("✗")) {
|
||||
console.log(chalk.red(line));
|
||||
} else {
|
||||
console.log(chalk.dim(line));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
`\n${"═".repeat(70)}\n ${failedTests.length} test file(s) failed\n${"═".repeat(70)}\n`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
process.exit(130); // Standard exit code for SIGINT
|
||||
}
|
||||
|
||||
async run(directories: string[]): Promise<void> {
|
||||
this.testFiles = await this.collectTestFiles(directories);
|
||||
|
||||
if (this.testFiles.length === 0) {
|
||||
if (!this.silentMode) {
|
||||
console.log(
|
||||
chalk.yellow("No test files found in specified directories"),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.silentMode) {
|
||||
console.log(
|
||||
chalk.bold(`\nRunning ${this.testFiles.length} test file(s)...\n`),
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize all tests as pending
|
||||
for (const file of this.testFiles) {
|
||||
this.results.set(file, {
|
||||
file,
|
||||
status: "pending",
|
||||
output: "",
|
||||
duration: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Setup SIGINT handler
|
||||
const sigintHandler = () => this.handleInterrupt();
|
||||
process.on("SIGINT", sigintHandler);
|
||||
|
||||
// Only setup UI if not in silent mode
|
||||
if (!this.silentMode) {
|
||||
// Hide cursor and create space for all tests
|
||||
this.hideCursor();
|
||||
this.startLine = 1; // Start from line 1
|
||||
|
||||
// Create space - less space needed in compact mode
|
||||
const spacesNeeded = this.getSpacesNeeded();
|
||||
this.lastRenderedLines = spacesNeeded; // Initialize to full space
|
||||
for (let i = 0; i < spacesNeeded; i++) {
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Move cursor back up to start rendering
|
||||
process.stdout.write(`\x1B[${spacesNeeded}A`);
|
||||
|
||||
// Start rendering loop
|
||||
this.renderInterval = setInterval(() => this.render(), 100);
|
||||
}
|
||||
|
||||
// Run tests with concurrency limit
|
||||
const limit = pLimit(this.maxParallel);
|
||||
const promises = this.testFiles.map((file) =>
|
||||
limit(() => this.runTest(file)),
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// Remove SIGINT handler
|
||||
process.off("SIGINT", sigintHandler);
|
||||
|
||||
if (!this.silentMode) {
|
||||
// Final render
|
||||
this.cleanup();
|
||||
this.render();
|
||||
|
||||
// Move cursor past all output (use actual rendered lines in compact mode)
|
||||
const linesToMove = this.compactMode
|
||||
? this.lastRenderedLines
|
||||
: this.getSpacesNeeded();
|
||||
process.stdout.write(`\x1B[${linesToMove}B`);
|
||||
console.log("\n");
|
||||
|
||||
// Print summary
|
||||
this.printSummary();
|
||||
}
|
||||
// Silent mode: results already output as tests complete, no need to output again
|
||||
}
|
||||
|
||||
getResults(): Map<string, TestResult> {
|
||||
return this.results;
|
||||
}
|
||||
|
||||
private printSummary() {
|
||||
const failedTests = Array.from(this.results.values()).filter(
|
||||
(t) => t.status === "failed",
|
||||
);
|
||||
|
||||
if (failedTests.length === 0) {
|
||||
console.log(
|
||||
chalk.green.bold(`✓ All ${this.testFiles.length} test file(s) passed!`),
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
`\n${"═".repeat(70)}\n FAILED TESTS (${failedTests.length}/${this.testFiles.length})\n${"═".repeat(70)}\n`,
|
||||
),
|
||||
);
|
||||
|
||||
for (const test of failedTests) {
|
||||
const testName = basename(test.file);
|
||||
console.log(chalk.red.bold(`\n✗ ${testName}`));
|
||||
console.log(chalk.dim("─".repeat(70)));
|
||||
|
||||
if (test.error) {
|
||||
const errorLines = test.error.split("\n");
|
||||
for (const line of errorLines) {
|
||||
if (line.trim()) {
|
||||
if (line.includes("Expected:") || line.includes("Received:")) {
|
||||
console.log(chalk.yellow(line));
|
||||
} else if (line.includes("✗")) {
|
||||
console.log(chalk.red(line));
|
||||
} else {
|
||||
console.log(chalk.dim(line));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
`\n${"═".repeat(70)}\n ${failedTests.length} test file(s) failed\n${"═".repeat(70)}\n`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse CLI arguments
|
||||
const args = process.argv.slice(2);
|
||||
const directories: string[] = [];
|
||||
let maxParallel = 6;
|
||||
let compactMode = false;
|
||||
let silentMode = false;
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg.startsWith("--max=")) {
|
||||
maxParallel = Number.parseInt(arg.split("=")[1], 10);
|
||||
} else if (arg === "--compact") {
|
||||
compactMode = true;
|
||||
} else if (arg === "--silent") {
|
||||
silentMode = true;
|
||||
} else if (arg.startsWith("-")) {
|
||||
console.error(chalk.red(`Unknown option: ${arg}`));
|
||||
console.log(
|
||||
"Usage: bun scripts/testScripts/runTests.ts <dir1> [dir2] [...] [--max=N] [--compact] [--silent]",
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
directories.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
if (directories.length === 0) {
|
||||
console.error(chalk.red("Error: No test directories specified"));
|
||||
console.log(
|
||||
"Usage: bun scripts/testScripts/runTests.ts <dir1> [dir2] [...] [--max=N] [--compact]",
|
||||
);
|
||||
console.log("\nOptions:");
|
||||
console.log(" --max=N Set maximum parallel test files (default: 6)");
|
||||
console.log(
|
||||
" --compact Use compact mode (only show summary and failures)",
|
||||
);
|
||||
console.log("\nExamples:");
|
||||
console.log(
|
||||
" bun scripts/testScripts/runTests.ts server/tests/attach/upgrade",
|
||||
);
|
||||
console.log(
|
||||
" bun scripts/testScripts/runTests.ts server/tests/attach/upgrade server/tests/attach/downgrade",
|
||||
);
|
||||
console.log(
|
||||
" bun scripts/testScripts/runTests.ts server/tests/attach/upgrade --max=10",
|
||||
);
|
||||
console.log(
|
||||
" bun scripts/testScripts/runTests.ts server/tests/attach/upgrade --compact",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Run tests
|
||||
const runner = new TestRunner({ maxParallel, compactMode, silentMode });
|
||||
await runner.run(directories);
|
||||
207
server/tests/testRunner/runTestsV2.ts
Normal file
207
server/tests/testRunner/runTestsV2.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun";
|
||||
import chalk from "chalk";
|
||||
import { readdir } from "fs/promises";
|
||||
import pLimit from "p-limit";
|
||||
import { resolve } from "path";
|
||||
|
||||
interface TestResult {
|
||||
file: string;
|
||||
status: "pending" | "running" | "passed" | "failed";
|
||||
duration: number;
|
||||
error?: string;
|
||||
output?: string; // Full test output for failed tests
|
||||
}
|
||||
|
||||
interface TestProgress {
|
||||
onTestStart?: (file: string) => void;
|
||||
onTestComplete?: (file: string, result: TestResult) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect test files from paths
|
||||
*/
|
||||
async function collectTestFiles(paths: string[]): Promise<string[]> {
|
||||
const testFiles: string[] = [];
|
||||
|
||||
for (const path of paths) {
|
||||
const resolvedPath = resolve(process.cwd(), path);
|
||||
|
||||
if (path.endsWith(".test.ts")) {
|
||||
testFiles.push(resolvedPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const files = await readdir(resolvedPath);
|
||||
for (const file of files) {
|
||||
if (file.endsWith(".test.ts")) {
|
||||
testFiles.push(resolve(resolvedPath, file));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore read errors
|
||||
}
|
||||
}
|
||||
|
||||
return testFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a single test file using Bun Shell
|
||||
*/
|
||||
async function runTestFile(
|
||||
file: string,
|
||||
progress?: TestProgress,
|
||||
): Promise<TestResult> {
|
||||
const startTime = performance.now();
|
||||
|
||||
progress?.onTestStart?.(file);
|
||||
|
||||
try {
|
||||
// Use Bun Shell to run the test with streaming output
|
||||
const result = await $`bun test --timeout 0 ${file}`.quiet().nothrow();
|
||||
|
||||
const duration = performance.now() - startTime;
|
||||
|
||||
if (result.exitCode === 0) {
|
||||
const testResult: TestResult = {
|
||||
file,
|
||||
status: "passed",
|
||||
duration,
|
||||
};
|
||||
progress?.onTestComplete?.(file, testResult);
|
||||
return testResult;
|
||||
}
|
||||
|
||||
// Test failed - capture full output
|
||||
const stderr = result.stderr.toString();
|
||||
const stdout = result.stdout.toString();
|
||||
const fullOutput = `${stdout}\n${stderr}`.trim();
|
||||
|
||||
// Extract error with stack trace for summary display
|
||||
const lines = fullOutput.split("\n");
|
||||
let errorLines: string[] = [];
|
||||
|
||||
// First, look for the error message with Expected/Received
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (
|
||||
line.includes("error:") ||
|
||||
line.includes("Expected:") ||
|
||||
line.includes("Received:")
|
||||
) {
|
||||
// Capture error message lines
|
||||
errorLines = lines.slice(i, i + 4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Then look for stack trace (lines with file paths and line numbers)
|
||||
const stackLines: string[] = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
// Match patterns like "at functionName (/path/to/file.ts:123:45)"
|
||||
if (line.trim().startsWith("at ") && line.includes(".ts:")) {
|
||||
stackLines.push(line.trim());
|
||||
// Capture up to 5 stack frames
|
||||
if (stackLines.length >= 5) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Combine error message and stack trace
|
||||
if (stackLines.length > 0) {
|
||||
errorLines.push("", ...stackLines);
|
||||
}
|
||||
|
||||
const error = errorLines.length > 0 ? errorLines.join("\n") : "Test failed";
|
||||
|
||||
const testResult: TestResult = {
|
||||
file,
|
||||
status: "failed",
|
||||
duration,
|
||||
error,
|
||||
output: fullOutput, // Include full output for debugging
|
||||
};
|
||||
progress?.onTestComplete?.(file, testResult);
|
||||
return testResult;
|
||||
} catch (error) {
|
||||
const duration = performance.now() - startTime;
|
||||
const testResult: TestResult = {
|
||||
file,
|
||||
status: "failed",
|
||||
duration,
|
||||
error: String(error),
|
||||
};
|
||||
progress?.onTestComplete?.(file, testResult);
|
||||
return testResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run multiple test files in parallel
|
||||
*/
|
||||
export async function runTests(
|
||||
paths: string[],
|
||||
options: {
|
||||
maxParallel?: number;
|
||||
progress?: TestProgress;
|
||||
} = {},
|
||||
): Promise<TestResult[]> {
|
||||
const { maxParallel = 6, progress } = options;
|
||||
|
||||
const testFiles = await collectTestFiles(paths);
|
||||
|
||||
if (testFiles.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Run tests with concurrency limit
|
||||
const limit = pLimit(maxParallel);
|
||||
const promises = testFiles.map((file) =>
|
||||
limit(() => runTestFile(file, progress)),
|
||||
);
|
||||
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
// CLI usage
|
||||
if (import.meta.main) {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error(chalk.red("Error: No test directories specified"));
|
||||
console.log("Usage: bun runTestsV2.ts <dir1> [dir2] [...]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const results = await runTests(args, {
|
||||
progress: {
|
||||
onTestStart: (file) => {
|
||||
const fileName = file.split("/").pop();
|
||||
console.log(chalk.cyan(`⠋ ${fileName}`));
|
||||
},
|
||||
onTestComplete: (file, result) => {
|
||||
const fileName = file.split("/").pop();
|
||||
if (result.status === "passed") {
|
||||
console.log(chalk.green(`✓ ${fileName}`));
|
||||
} else {
|
||||
console.log(chalk.red(`✗ ${fileName}`));
|
||||
if (result.error) {
|
||||
console.log(chalk.yellow(` ${result.error}`));
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const passed = results.filter((r) => r.status === "passed").length;
|
||||
const failed = results.filter((r) => r.status === "failed").length;
|
||||
|
||||
console.log(
|
||||
`\n${chalk.green(`✓ ${passed}`)} passed, ${failed > 0 ? chalk.red(`✗ ${failed}`) : chalk.dim(`✗ ${failed}`)} failed`,
|
||||
);
|
||||
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
99
server/tests/testRunner/testWorker.ts
Normal file
99
server/tests/testRunner/testWorker.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/// <reference lib="webworker" />
|
||||
declare var self: Worker;
|
||||
|
||||
import { test } from "bun:test";
|
||||
|
||||
type TestMessage =
|
||||
| { type: "test-start"; file: string; test: string }
|
||||
| { type: "test-pass"; file: string; test: string; duration: number }
|
||||
| { type: "test-fail"; file: string; test: string; duration: number; error: string }
|
||||
| { type: "file-complete"; file: string; passed: number; failed: number; duration: number };
|
||||
|
||||
let currentFile = "";
|
||||
let testsRun = 0;
|
||||
let testsPassed = 0;
|
||||
let testsFailed = 0;
|
||||
const fileStartTime = performance.now();
|
||||
|
||||
// Intercept test execution to send progress updates
|
||||
const originalTest = test;
|
||||
|
||||
// Override test to track progress
|
||||
(globalThis as any).test = function (name: string, fn: Function) {
|
||||
return originalTest(name, async () => {
|
||||
testsRun++;
|
||||
const testStart = performance.now();
|
||||
|
||||
self.postMessage({
|
||||
type: "test-start",
|
||||
file: currentFile,
|
||||
test: name,
|
||||
} as TestMessage);
|
||||
|
||||
try {
|
||||
await fn();
|
||||
const duration = performance.now() - testStart;
|
||||
testsPassed++;
|
||||
|
||||
self.postMessage({
|
||||
type: "test-pass",
|
||||
file: currentFile,
|
||||
test: name,
|
||||
duration,
|
||||
} as TestMessage);
|
||||
} catch (error) {
|
||||
const duration = performance.now() - testStart;
|
||||
testsFailed++;
|
||||
|
||||
self.postMessage({
|
||||
type: "test-fail",
|
||||
file: currentFile,
|
||||
test: name,
|
||||
duration,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
} as TestMessage);
|
||||
|
||||
throw error; // Re-throw so bun:test sees the failure
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
self.onmessage = async (event: MessageEvent) => {
|
||||
const { testFile } = event.data;
|
||||
|
||||
if (!testFile) {
|
||||
self.postMessage({ type: "error", error: "No test file specified" });
|
||||
return;
|
||||
}
|
||||
|
||||
currentFile = testFile;
|
||||
testsRun = 0;
|
||||
testsPassed = 0;
|
||||
testsFailed = 0;
|
||||
|
||||
try {
|
||||
// Import the test file - this will execute all tests
|
||||
await import(testFile);
|
||||
|
||||
// Wait a tick for all tests to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const fileDuration = performance.now() - fileStartTime;
|
||||
|
||||
self.postMessage({
|
||||
type: "file-complete",
|
||||
file: testFile,
|
||||
passed: testsPassed,
|
||||
failed: testsFailed,
|
||||
duration: fileDuration,
|
||||
} as TestMessage);
|
||||
} catch (error) {
|
||||
self.postMessage({
|
||||
type: "error",
|
||||
file: testFile,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { expect } from "bun:test";
|
||||
import {
|
||||
AllowanceType,
|
||||
CusProductStatus,
|
||||
@@ -7,7 +8,8 @@ import {
|
||||
FeatureType,
|
||||
type UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import { expect } from "chai";
|
||||
import type { ApiCustomerV1 } from "@shared/api/customers/previousVersions/apiCustomerV1.js";
|
||||
// import { expect } from "chai";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||
import { creditSystems } from "tests/global.js";
|
||||
@@ -22,8 +24,8 @@ export const checkProductIsScheduled = ({
|
||||
const { products, add_ons, entitlements } = cusRes;
|
||||
const prod = products.find((p: any) => p.id === product.id);
|
||||
try {
|
||||
expect(prod).to.exist;
|
||||
expect(prod.status).to.equal(CusProductStatus.Scheduled);
|
||||
expect(prod).toBeDefined();
|
||||
expect(prod.status).toEqual(CusProductStatus.Scheduled);
|
||||
} catch (error) {
|
||||
console.group();
|
||||
console.log(`Expected product ${product.id} to be scheduled`);
|
||||
@@ -49,29 +51,31 @@ export const compareMainProduct = ({
|
||||
(p: any) => p.id === sent.id && p.status === status && !sent.is_add_on,
|
||||
);
|
||||
|
||||
try {
|
||||
expect(prod).to.exist;
|
||||
expect(sent.id).to.equal(prod.id);
|
||||
} catch (error) {
|
||||
console.log(`Failed to compare main product ${sent.id}`);
|
||||
console.log("Sent: ", sent);
|
||||
console.log("Received: ", cusRes);
|
||||
throw error;
|
||||
}
|
||||
expect(
|
||||
prod,
|
||||
`Product ${sent.id} not found (status: ${status}), (${sent.is_add_on ? "add-on" : "main"})`,
|
||||
).toBeDefined();
|
||||
|
||||
// Check entitlements
|
||||
const sentEntitlements = Object.values(sent.entitlements) as Entitlement[];
|
||||
const recEntitlements = entitlements;
|
||||
|
||||
// expect(sentEntitlements.length).to.equal(recEntitlements.length);
|
||||
for (const entitlement of sentEntitlements) {
|
||||
// Corresponding entitlement in received
|
||||
const recEntitlement = recEntitlements.find((e: any) => {
|
||||
if (e.feature_id !== entitlement.feature_id) return false;
|
||||
if (entitlement.interval && e.interval !== entitlement.interval)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
const recEntitlement = recEntitlements.find(
|
||||
(e: ApiCustomerV1["entitlements"][number]) => {
|
||||
if (e.feature_id !== entitlement.feature_id) return false;
|
||||
|
||||
if (entitlement.allowance_type === AllowanceType.Unlimited) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entitlement.interval && e.interval !== entitlement.interval) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
// If options list provideed, and feature
|
||||
const options = optionsList.find(
|
||||
@@ -90,22 +94,25 @@ export const compareMainProduct = ({
|
||||
.toNumber();
|
||||
}
|
||||
|
||||
try {
|
||||
expect(recEntitlement).to.exist;
|
||||
if (entitlement.allowance_type === AllowanceType.Unlimited) {
|
||||
expect(recEntitlement.unlimited).to.equal(true);
|
||||
expect(recEntitlement.balance).to.equal(null);
|
||||
expect(recEntitlement.used).to.equal(null);
|
||||
} else if ("balance" in entitlement) {
|
||||
expect(recEntitlement.balance).to.equal(expectedBalance);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`Failed to compare main product (entitlements) ${entitlement.feature_id}`,
|
||||
);
|
||||
console.log("Looking for entitlement: ", entitlement);
|
||||
console.log("Received entitlements: ", entitlements);
|
||||
throw error;
|
||||
expect(
|
||||
recEntitlement,
|
||||
`Entitlement ${entitlement.feature_id} not found`,
|
||||
).toBeDefined();
|
||||
|
||||
if (entitlement.allowance_type === AllowanceType.Unlimited) {
|
||||
// expect(recEntitlement.unlimited).toStrictEqual(true);
|
||||
// expect(recEntitlement.balance).toStrictEqual(null);
|
||||
// expect(recEntitlement.used).toStrictEqual(null);
|
||||
expect(recEntitlement).toMatchObject({
|
||||
unlimited: true,
|
||||
balance: null,
|
||||
used: null,
|
||||
});
|
||||
} else if ("balance" in entitlement) {
|
||||
expect(
|
||||
recEntitlement.balance,
|
||||
`Balance for ${entitlement.feature_id} does not match expected balance`,
|
||||
).toStrictEqual(expectedBalance);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -129,7 +136,9 @@ export const checkFeatureHasCorrectBalance = async ({
|
||||
if (feature.type === FeatureType.Boolean) {
|
||||
console.log(" - Checking boolean feature: ", feature.id);
|
||||
const { allowed, balanceObj }: any = entitledRes;
|
||||
expect(allowed).to.equal(true);
|
||||
expect(allowed, `Allowed for ${feature.id} is not true`).toStrictEqual(
|
||||
true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -149,30 +158,59 @@ export const checkFeatureHasCorrectBalance = async ({
|
||||
e.feature_id === feature.id && e.interval === entitlement.interval,
|
||||
);
|
||||
|
||||
expect(cusEnt).to.exist;
|
||||
expect(cusEnt, `Cus ent for ${feature.id} not found`).toBeDefined();
|
||||
|
||||
if (entitlement.allowance_type === AllowanceType.Unlimited) {
|
||||
// Cus ent
|
||||
expect(cusEnt.balance).to.equal(null);
|
||||
expect(cusEnt.used).to.equal(null);
|
||||
expect(cusEnt.unlimited).to.equal(true);
|
||||
expect(
|
||||
cusEnt.balance,
|
||||
`Balance for ${feature.id} is not null`,
|
||||
).toStrictEqual(null);
|
||||
expect(cusEnt.used, `Used for ${feature.id} is not null`).toStrictEqual(
|
||||
null,
|
||||
);
|
||||
expect(
|
||||
cusEnt.unlimited,
|
||||
`Unlimited for ${feature.id} is not true`,
|
||||
).toStrictEqual(true);
|
||||
|
||||
// Entitled res
|
||||
expect(allowed).to.equal(true);
|
||||
expect(balanceObj?.balance).to.equal(null);
|
||||
expect(balanceObj?.unlimited).to.equal(true);
|
||||
expect(allowed, `Allowed for ${feature.id} is not true`).toStrictEqual(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
balanceObj?.balance,
|
||||
`Balance for ${feature.id} is not null`,
|
||||
).toStrictEqual(null);
|
||||
expect(
|
||||
balanceObj?.unlimited,
|
||||
`Unlimited for ${feature.id} is not true`,
|
||||
).toStrictEqual(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (expectedBalance === 0) {
|
||||
expect(allowed).to.equal(false);
|
||||
expect(balanceObj?.balance).to.equal(0);
|
||||
expect(cusEnt.balance).to.equal(0);
|
||||
expect(allowed, `Allowed for ${feature.id} is not false`).toStrictEqual(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
balanceObj?.balance,
|
||||
`Balance for ${feature.id} is not 0`,
|
||||
).toStrictEqual(0);
|
||||
expect(cusEnt.balance, `Balance for ${feature.id} is not 0`).toStrictEqual(
|
||||
0,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
expect(balanceObj?.balance).to.equal(expectedBalance);
|
||||
expect(cusEnt.balance).to.equal(expectedBalance);
|
||||
expect(
|
||||
balanceObj?.balance,
|
||||
`Balance for ${feature.id} does not match expected balance`,
|
||||
).toStrictEqual(expectedBalance);
|
||||
expect(
|
||||
cusEnt.balance,
|
||||
`Balance for ${feature.id} does not match expected balance`,
|
||||
).toStrictEqual(expectedBalance);
|
||||
};
|
||||
|
||||
export const compareProductEntitlements = ({
|
||||
|
||||
48
server/tests/utils/expectUtils/expectCustomerV0Correct.ts
Normal file
48
server/tests/utils/expectUtils/expectCustomerV0Correct.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type {
|
||||
CusProductStatus,
|
||||
FeatureOptions,
|
||||
ProductV2,
|
||||
} from "@autumn/shared";
|
||||
import { convertProductV2ToV1 } from "@/internal/products/productUtils/productV2Utils/convertProductV2ToV1.js";
|
||||
import { compareMainProduct } from "../compare.js";
|
||||
import ctx from "../testInitUtils/createTestContext.js";
|
||||
|
||||
/**
|
||||
* Compares V0.1 API customer response against V2 product definition
|
||||
*
|
||||
* Converts ProductV2.items → entitlements + prices using production utilities,
|
||||
* then delegates to existing compareMainProduct for validation.
|
||||
*
|
||||
* @param sent - V2 product definition with items
|
||||
* @param cusRes - V0.1 customer API response
|
||||
* @param status - Expected product status
|
||||
* @param optionsList - Feature options for quantity adjustments
|
||||
*/
|
||||
export const expectCustomerV0Correct = async ({
|
||||
sent,
|
||||
cusRes,
|
||||
status,
|
||||
optionsList,
|
||||
}: {
|
||||
sent: ProductV2;
|
||||
cusRes: any; // V0.1 customer response
|
||||
status?: CusProductStatus;
|
||||
optionsList?: FeatureOptions[];
|
||||
}) => {
|
||||
const { org, features } = ctx;
|
||||
|
||||
// Convert V2 → V1 using production utilities
|
||||
const sentV1 = convertProductV2ToV1({
|
||||
productV2: sent,
|
||||
orgId: org.id,
|
||||
features,
|
||||
});
|
||||
|
||||
// Use existing compareMainProduct
|
||||
return compareMainProduct({
|
||||
sent: sentV1,
|
||||
cusRes,
|
||||
status,
|
||||
optionsList,
|
||||
});
|
||||
};
|
||||
@@ -40,7 +40,9 @@ export const createProduct = async ({
|
||||
}
|
||||
|
||||
await Promise.all(batchDelete);
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
// Ignore deletion errors (might have customers attached)
|
||||
}
|
||||
|
||||
const clone = structuredClone(product);
|
||||
if (typeof clone.items === "object") {
|
||||
@@ -52,7 +54,19 @@ export const createProduct = async ({
|
||||
clone.name = `${prefix} ${clone.name}`;
|
||||
}
|
||||
|
||||
await autumn.products.create(clone);
|
||||
try {
|
||||
await autumn.products.create(clone);
|
||||
} catch (error: any) {
|
||||
// If product already exists (race condition), silently continue
|
||||
if (
|
||||
error?.message?.includes("already exists") ||
|
||||
error?.message?.includes("duplicate") ||
|
||||
error?.code === "PRODUCT_EXISTS"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createProducts = async ({
|
||||
|
||||
@@ -61,7 +61,13 @@ export const clearOrg = async ({
|
||||
throw new Error(`Org ${orgSlug} not found`);
|
||||
}
|
||||
|
||||
if (!(org.slug === "unit-test-org" || org.slug === "ci_cd")) {
|
||||
// Allow unit-test-org, ci_cd, and platform test orgs (test-*|org_...)
|
||||
const isAllowed =
|
||||
org.slug === "unit-test-org" ||
|
||||
org.slug === "ci_cd" ||
|
||||
org.slug.startsWith("test-");
|
||||
|
||||
if (!isAllowed) {
|
||||
console.error("Cannot clear non-unit-test-orgs");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
import {
|
||||
type AppEnv,
|
||||
type Feature,
|
||||
FeatureType,
|
||||
type FullProduct,
|
||||
type Organization,
|
||||
type Price,
|
||||
PriceType,
|
||||
type RewardProgram,
|
||||
RewardType,
|
||||
} from "@autumn/shared";
|
||||
import type { AppEnv } from "@autumn/shared";
|
||||
import axios from "axios";
|
||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||
import { features as v2Features } from "tests/setup/v2Features.js";
|
||||
import { getFeatures } from "tests/setup/v2Features.js";
|
||||
import { initDrizzle } from "@/db/initDrizzle.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { mapToProductItems } from "@/internal/products/productV2Utils.js";
|
||||
import { RewardService } from "@/internal/rewards/RewardService.js";
|
||||
|
||||
export const getAxiosInstance = (
|
||||
apiKey: string = process.env.UNIT_TEST_AUTUMN_SECRET_KEY!,
|
||||
) => {
|
||||
export const getAxiosInstance = (apiKey?: string) => {
|
||||
// Priority: 1. Passed apiKey, 2. Org secret key from context, 3. TEST_ORG_SECRET_KEY fallback
|
||||
// Import ctx here to avoid circular dependency issues
|
||||
const ctx = require("tests/utils/testInitUtils/createTestContext.js").default;
|
||||
const secretKey =
|
||||
apiKey || ctx?.orgSecretKey || process.env.TEST_ORG_SECRET_KEY;
|
||||
|
||||
if (!secretKey) {
|
||||
throw new Error("No secret key found");
|
||||
}
|
||||
|
||||
return axios.create({
|
||||
baseURL: "http://localhost:8080",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Authorization: `Bearer ${secretKey}`,
|
||||
"x-api-version": "0.1",
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -33,263 +28,34 @@ export const getAxiosInstance = (
|
||||
export const setupOrg = async ({
|
||||
orgId,
|
||||
env,
|
||||
features,
|
||||
products,
|
||||
rewards,
|
||||
rewardTriggers,
|
||||
}: {
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
features: Record<string, Feature & { eventName: string }>;
|
||||
products: Record<string, FullProduct | any>;
|
||||
rewards: Record<string, any>;
|
||||
rewardTriggers: Record<string, RewardProgram>;
|
||||
}) => {
|
||||
const axiosInstance = getAxiosInstance();
|
||||
const { client, db } = initDrizzle();
|
||||
|
||||
const autumn = new AutumnInt();
|
||||
|
||||
const insertFeatures = [];
|
||||
for (const feature of Object.values(features)) {
|
||||
insertFeatures.push(axiosInstance.post("/v1/internal_features", feature));
|
||||
}
|
||||
|
||||
await Promise.all(insertFeatures);
|
||||
|
||||
// Only insert v2 features
|
||||
const v2Features = getFeatures({ orgId });
|
||||
await FeatureService.insert({
|
||||
db,
|
||||
data: Object.values(v2Features),
|
||||
logger: console,
|
||||
});
|
||||
|
||||
let org: Organization | null = null;
|
||||
let newFeatures: Feature[] = [];
|
||||
try {
|
||||
org = await OrgService.get({ db, orgId });
|
||||
await OrgService.update({
|
||||
db,
|
||||
orgId,
|
||||
updates: {
|
||||
config: {
|
||||
...org.config,
|
||||
bill_upgrade_immediately: true,
|
||||
},
|
||||
// Update org config
|
||||
const org = await OrgService.get({ db, orgId });
|
||||
await OrgService.update({
|
||||
db,
|
||||
orgId,
|
||||
updates: {
|
||||
config: {
|
||||
...org.config,
|
||||
bill_upgrade_immediately: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
newFeatures = (await FeatureService.list({ db, orgId, env })).filter((f) =>
|
||||
Object.keys(features).includes(f.id),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error updating org", error);
|
||||
}
|
||||
|
||||
for (const feature of newFeatures!) {
|
||||
features[feature.id].internal_id = feature.internal_id;
|
||||
|
||||
if (feature.type === FeatureType.Metered) {
|
||||
features[feature.id].eventName = feature.event_names?.[0] || feature.id;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ Inserted features");
|
||||
|
||||
// 2. Create products
|
||||
const insertProducts = [];
|
||||
|
||||
const productValues = Object.values(products);
|
||||
const batchSize = 5;
|
||||
|
||||
for (
|
||||
let batchStart = 0;
|
||||
batchStart < productValues.length;
|
||||
batchStart += batchSize
|
||||
) {
|
||||
const batch = productValues.slice(batchStart, batchStart + batchSize);
|
||||
const batchPromises = [];
|
||||
|
||||
for (const product of batch) {
|
||||
const insertProduct = async () => {
|
||||
await autumn.products.create({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
group: product.group,
|
||||
is_add_on: product.is_add_on,
|
||||
is_default: product.is_default,
|
||||
});
|
||||
|
||||
const prices = product.prices.map((p: any) => ({
|
||||
...p,
|
||||
config: {
|
||||
...p.config,
|
||||
internal_feature_id: newFeatures!.find(
|
||||
(f) => f.id === (p.config as any)?.feature_id,
|
||||
)?.internal_id,
|
||||
},
|
||||
}));
|
||||
|
||||
const entitlements = Object.values(product.entitlements).map(
|
||||
(ent: any) => ({
|
||||
...ent,
|
||||
internal_feature_id: newFeatures!.find(
|
||||
(f) => f.id === ent.feature_id,
|
||||
)?.internal_id,
|
||||
}),
|
||||
);
|
||||
|
||||
const entWithFeatures = entitlements.map((ent) => ({
|
||||
...ent,
|
||||
feature: newFeatures!.find((f) => f.id === ent.feature_id),
|
||||
}));
|
||||
|
||||
const items = mapToProductItems({
|
||||
prices,
|
||||
entitlements: entWithFeatures,
|
||||
allowFeatureMatch: true,
|
||||
features: newFeatures!,
|
||||
});
|
||||
|
||||
try {
|
||||
await axiosInstance.post(`/v1/products/${product.id}`, {
|
||||
items,
|
||||
free_trial: product.free_trial,
|
||||
});
|
||||
} catch (_error) {
|
||||
console.log("Product:", product.name);
|
||||
console.error("Error creating product prices / ents");
|
||||
console.log("Items", items);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
batchPromises.push(insertProduct());
|
||||
}
|
||||
|
||||
await Promise.all(batchPromises);
|
||||
insertProducts.push(...batchPromises);
|
||||
}
|
||||
|
||||
await Promise.all(insertProducts);
|
||||
console.log("✅ Inserted products");
|
||||
|
||||
if (process.env.MOCHA_PARALLEL === "true") {
|
||||
console.log("MOCHA RUNNING IN PARALLEL");
|
||||
await AutumnCli.initStripeProducts();
|
||||
console.log("✅ Initialized stripe products / prices");
|
||||
} else {
|
||||
console.log("MOCHA RUNNING IN SERIAL");
|
||||
}
|
||||
|
||||
// Fetch all products
|
||||
const { list: allProducts } = await AutumnCli.getProducts();
|
||||
const _productIds = allProducts.map((p: any) => p.id);
|
||||
|
||||
// Insert coupons
|
||||
const insertCoupons = [];
|
||||
for (const reward of Object.values(rewards)) {
|
||||
const createReward = async () => {
|
||||
let priceIds = [];
|
||||
|
||||
const rewardData: any = {
|
||||
id: reward.id,
|
||||
name: reward.name,
|
||||
promo_codes: [
|
||||
{
|
||||
code: reward.id,
|
||||
},
|
||||
],
|
||||
type: reward.type,
|
||||
};
|
||||
|
||||
if (reward.type === RewardType.FreeProduct) {
|
||||
rewardData.free_product_id = reward.free_product_id;
|
||||
rewardData.free_product_config = reward.free_product_config;
|
||||
} else {
|
||||
if (reward.only_usage_prices) {
|
||||
const filteredProducts = allProducts.filter(
|
||||
(product: FullProduct) => {
|
||||
if (reward.product_ids) {
|
||||
return reward.product_ids.includes(product.id);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
priceIds = filteredProducts.flatMap((product: FullProduct) =>
|
||||
product.prices
|
||||
.filter((price: Price) => price.config!.type === PriceType.Usage)
|
||||
.map((price) => {
|
||||
return price.id;
|
||||
}),
|
||||
);
|
||||
} else if (reward.product_ids) {
|
||||
priceIds = allProducts
|
||||
.filter((product: FullProduct) =>
|
||||
reward.product_ids.includes(product.id),
|
||||
)
|
||||
.flatMap((product: FullProduct) =>
|
||||
product.prices.map((price) => price.id),
|
||||
);
|
||||
}
|
||||
|
||||
rewardData.discount_config = {
|
||||
discount_value: reward.discount_config.discount_value,
|
||||
duration_type: reward.discount_config.duration_type,
|
||||
duration_value: reward.discount_config.duration_value,
|
||||
apply_to_all: reward.discount_config.apply_to_all,
|
||||
price_ids: priceIds,
|
||||
};
|
||||
}
|
||||
|
||||
const newReward: any = {
|
||||
internal_id: reward.id,
|
||||
id: reward.id,
|
||||
name: reward.name,
|
||||
promo_codes: [
|
||||
{
|
||||
code: reward.id,
|
||||
},
|
||||
],
|
||||
type: reward.type,
|
||||
discount_config: rewardData.discount_config,
|
||||
free_product_id: rewardData.free_product_id,
|
||||
free_product_config:
|
||||
rewardData.free_product_config?.duration_type &&
|
||||
rewardData.free_product_config?.duration_value
|
||||
? rewardData.free_product_config
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const rewardRes = await autumn.rewards.create(newReward);
|
||||
|
||||
return {
|
||||
id: reward.id,
|
||||
rewardRes,
|
||||
};
|
||||
};
|
||||
|
||||
console.log("Creating reward", reward.id);
|
||||
insertCoupons.push(createReward());
|
||||
}
|
||||
|
||||
await Promise.all(insertCoupons);
|
||||
console.log("✅ Inserted coupons");
|
||||
|
||||
// CREATE REWARD TRIGGERS
|
||||
const insertRewardTriggers = [];
|
||||
const insertedRewards = await RewardService.list({ db, orgId, env });
|
||||
for (const rewardTrigger of Object.values(rewardTriggers)) {
|
||||
const rt = {
|
||||
...rewardTrigger,
|
||||
internal_reward_id: insertedRewards.find(
|
||||
(r) => r.id === rewardTrigger.internal_reward_id,
|
||||
)?.internal_id!,
|
||||
};
|
||||
insertRewardTriggers.push(autumn.rewardPrograms.create(rt));
|
||||
}
|
||||
await Promise.all(insertRewardTriggers);
|
||||
console.log("✅ Inserted reward triggers");
|
||||
console.log("✅ Inserted v2 features");
|
||||
|
||||
await client.end();
|
||||
};
|
||||
|
||||
@@ -9,13 +9,13 @@ const __dirname = dirname(__filename);
|
||||
// dotenv.config({ path: resolve(__dirname, ".env") });
|
||||
dotenv.config({ path: resolve(__dirname, "..", "..", "..", ".env") });
|
||||
|
||||
import { AppEnv, type Organization } from "@autumn/shared";
|
||||
import { AppEnv, type Feature, type Organization } from "@autumn/shared";
|
||||
import type Stripe from "stripe";
|
||||
import { type DrizzleCli, initDrizzle } from "@/db/initDrizzle.js";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
|
||||
const ORG_SLUG = process.env.TESTS_ORG!;
|
||||
const DEFAULT_ENV = AppEnv.Sandbox;
|
||||
|
||||
export type TestContext = {
|
||||
@@ -23,22 +23,55 @@ export type TestContext = {
|
||||
env: AppEnv;
|
||||
stripeCli: Stripe;
|
||||
db: DrizzleCli;
|
||||
orgSecretKey: string;
|
||||
features: Feature[];
|
||||
};
|
||||
|
||||
export const createTestContext = async () => {
|
||||
const { db } = initDrizzle();
|
||||
|
||||
const org = await OrgService.getBySlug({ db, slug: ORG_SLUG });
|
||||
if (!org) throw new Error("Org not found");
|
||||
// Support dynamic org slug from environment (for parallel test groups)
|
||||
// Falls back to TESTS_ORG for legacy tests
|
||||
const orgSlug = process.env.TESTS_ORG;
|
||||
if (!orgSlug) {
|
||||
throw new Error(
|
||||
"TESTS_ORG environment variable is required (set by test runner)",
|
||||
);
|
||||
}
|
||||
|
||||
const org = await OrgService.getBySlug({ db, slug: orgSlug });
|
||||
if (!org) {
|
||||
throw new Error(`Org with slug "${orgSlug}" not found`);
|
||||
}
|
||||
|
||||
const env = DEFAULT_ENV;
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
|
||||
// Get org secret key for API calls
|
||||
// Priority: 1. Environment variable (set by test runner), 2. Org's secret_keys field
|
||||
const orgSecretKey =
|
||||
process.env.UNIT_TEST_AUTUMN_SECRET_KEY || org.secret_keys?.[env] || "";
|
||||
if (!orgSecretKey) {
|
||||
throw new Error(
|
||||
`No secret key found for org "${orgSlug}" in environment "${env}". ` +
|
||||
`Make sure UNIT_TEST_AUTUMN_SECRET_KEY is set or org has secret_keys.${env}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch and cache features for this org
|
||||
const features = await FeatureService.list({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
});
|
||||
|
||||
return {
|
||||
org,
|
||||
env,
|
||||
stripeCli,
|
||||
db,
|
||||
orgSecretKey,
|
||||
features,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -89,6 +89,8 @@ export const CreateCustomerParamsSchema = z.object({
|
||||
entity_data: EntityDataSchema.optional().meta({
|
||||
description: "Data for creating an entity",
|
||||
}),
|
||||
|
||||
disable_default: z.boolean().optional(),
|
||||
});
|
||||
|
||||
// Update Customer Params (based on handleUpdateCustomer logic)
|
||||
|
||||
@@ -28,4 +28,5 @@ export * from "./productV2Utils/productItemUtils/getItemType.js";
|
||||
// Item utils
|
||||
export * from "./productV2Utils/productItemUtils/mapToItem.js";
|
||||
export * from "./productV2Utils/productItemUtils/productItemUtils.js";
|
||||
export * from "./productV2Utils/productV2ToV1.js";
|
||||
export * from "./utils.js";
|
||||
|
||||
42
shared/utils/productV2Utils/productV2ToV1.ts
Normal file
42
shared/utils/productV2Utils/productV2ToV1.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { Entitlement, Price, ProductV2 } from "@autumn/shared";
|
||||
|
||||
/**
|
||||
* Converts ProductV2 (items-based) to V1 format (entitlements + prices)
|
||||
*
|
||||
* NOTE: This is a lightweight type conversion for TEST purposes only.
|
||||
* For actual production conversion, use server-side utilities in:
|
||||
* @see server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts
|
||||
*
|
||||
* @param productV2 - V2 product with items array
|
||||
* @param entitlements - Converted entitlements from itemToPriceAndEnt
|
||||
* @param prices - Converted prices from itemToPriceAndEnt
|
||||
* @returns V1-format product object
|
||||
*/
|
||||
export const productV2ToV1 = ({
|
||||
productV2,
|
||||
entitlements,
|
||||
prices,
|
||||
}: {
|
||||
productV2: ProductV2;
|
||||
entitlements: Entitlement[];
|
||||
prices: Price[];
|
||||
}) => {
|
||||
// Convert entitlements array to record keyed by feature_id
|
||||
const entitlementsRecord: Record<string, Entitlement> = {};
|
||||
for (const ent of entitlements) {
|
||||
if (ent.feature_id) {
|
||||
entitlementsRecord[ent.feature_id] = ent;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: productV2.id,
|
||||
name: productV2.name,
|
||||
is_default: productV2.is_default,
|
||||
is_add_on: productV2.is_add_on,
|
||||
entitlements: entitlementsRecord,
|
||||
prices,
|
||||
free_trial: productV2.free_trial,
|
||||
group: productV2.group,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user