From 8f88ce0c76a76ee606e4ac49fcd236c97fd32f2e Mon Sep 17 00:00:00 2001 From: partyplatter08-lab Date: Tue, 19 May 2026 06:15:31 -0400 Subject: [PATCH 1/3] test: add UI package Vitest coverage --- packages/ui-solid/package.json | 8 ++++- packages/ui-solid/src/ProgressCircle.test.tsx | 32 +++++++++++++++++++ packages/ui-solid/src/jest-dom.setup.ts | 1 + packages/ui-solid/vitest.config.ts | 11 +++++++ packages/ui/package.json | 4 ++- packages/ui/src/components/Button.test.tsx | 31 ++++++++++++++++++ packages/ui/vitest.config.ts | 10 ++++++ pnpm-lock.yaml | 12 +++++++ 8 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 packages/ui-solid/src/ProgressCircle.test.tsx create mode 100644 packages/ui-solid/src/jest-dom.setup.ts create mode 100644 packages/ui-solid/vitest.config.ts create mode 100644 packages/ui/src/components/Button.test.tsx create mode 100644 packages/ui/vitest.config.ts diff --git a/packages/ui-solid/package.json b/packages/ui-solid/package.json index c8dbd63d5a1..5998a98e961 100644 --- a/packages/ui-solid/package.json +++ b/packages/ui-solid/package.json @@ -10,6 +10,9 @@ "./vite": "./vite.js", "./types": "./src/types.d.ts" }, + "scripts": { + "test": "vitest run" + }, "dependencies": { "@kobalte/core": "^0.13.7", "cva": "npm:class-variance-authority@^0.7.0", @@ -30,7 +33,10 @@ "tailwindcss-animate": "^1.0.6", "unplugin-auto-import": "^0.18.2", "unplugin-fonts": "^1.1.1", - "unplugin-icons": "^0.19.2" + "unplugin-icons": "^0.19.2", + "vite": "^6.3.5", + "vite-plugin-solid": "^2.10.2", + "vitest": "~2.1.9" }, "peerDependencies": { "@fontsource/geist-sans": "^5.0.3" diff --git a/packages/ui-solid/src/ProgressCircle.test.tsx b/packages/ui-solid/src/ProgressCircle.test.tsx new file mode 100644 index 00000000000..e7ecb2a1d9e --- /dev/null +++ b/packages/ui-solid/src/ProgressCircle.test.tsx @@ -0,0 +1,32 @@ +import { renderToString } from "solid-js/web"; +import { describe, expect, it } from "vitest"; +import { ProgressCircle } from "./ProgressCircle"; + +describe("ProgressCircle", () => { + it("renders accessible progress values", () => { + const markup = renderToString(() => ( + + )); + + expect(markup).toContain('role="progressbar"'); + expect(markup).toContain('aria-valuenow="42"'); + expect(markup).toContain('aria-valuemin="0"'); + expect(markup).toContain('aria-valuemax="100"'); + expect(markup).toContain("stroke-blue-10"); + expect(markup).toContain("stroke-blue-5"); + }); + + it("clamps progress before rendering aria state and stroke offset", () => { + const overMaxMarkup = renderToString(() => ( + + )); + const belowMinMarkup = renderToString(() => ( + + )); + + expect(overMaxMarkup).toContain('aria-valuenow="100"'); + expect(overMaxMarkup).toContain('stroke-dashoffset="0"'); + expect(belowMinMarkup).toContain('aria-valuenow="0"'); + expect(belowMinMarkup).toContain('stroke-dashoffset="37.69911184307752"'); + }); +}); diff --git a/packages/ui-solid/src/jest-dom.setup.ts b/packages/ui-solid/src/jest-dom.setup.ts new file mode 100644 index 00000000000..cb0ff5c3b54 --- /dev/null +++ b/packages/ui-solid/src/jest-dom.setup.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/ui-solid/vitest.config.ts b/packages/ui-solid/vitest.config.ts new file mode 100644 index 00000000000..e1d45e76998 --- /dev/null +++ b/packages/ui-solid/vitest.config.ts @@ -0,0 +1,11 @@ +import solid from "vite-plugin-solid"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [solid({ ssr: true })], + test: { + environment: "node", + include: ["src/**/*.test.{ts,tsx}"], + setupFiles: ["./src/jest-dom.setup.ts"], + }, +}); diff --git a/packages/ui/package.json b/packages/ui/package.json index b76a0f4b1e8..4bcf18973ce 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -10,6 +10,7 @@ "./style": "./style/styles.css" }, "scripts": { + "test": "vitest run", "typecheck": "tsc -b" }, "devDependencies": { @@ -32,7 +33,8 @@ "tsconfig": "workspace:*", "typescript": "^5.8.3", "vite": "^6.3.5", - "vite-tsconfig-paths": "^4.2.1" + "vite-tsconfig-paths": "^4.2.1", + "vitest": "~2.1.9" }, "dependencies": { "@cap/utils": "workspace:*", diff --git a/packages/ui/src/components/Button.test.tsx b/packages/ui/src/components/Button.test.tsx new file mode 100644 index 00000000000..572a1c63fc9 --- /dev/null +++ b/packages/ui/src/components/Button.test.tsx @@ -0,0 +1,31 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; +import { Button } from "./Button"; + +describe("Button", () => { + it("renders default button attributes and content", () => { + const markup = renderToStaticMarkup(); + + expect(markup).toContain(" { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain("D"); + expect(markup).toContain("animation:spin"); + expect(markup).toContain("bg-blue-600"); + }); +}); diff --git a/packages/ui/vitest.config.ts b/packages/ui/vitest.config.ts new file mode 100644 index 00000000000..f9c9982b0f0 --- /dev/null +++ b/packages/ui/vitest.config.ts @@ -0,0 +1,10 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [react()], + test: { + environment: "node", + include: ["src/**/*.test.{ts,tsx}"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1003cb70eb..574c7d903f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1237,6 +1237,9 @@ importers: vite-tsconfig-paths: specifier: ^4.2.1 version: 4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)) + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) packages/ui-solid: dependencies: @@ -1292,6 +1295,15 @@ importers: unplugin-icons: specifier: ^0.19.2 version: 0.19.3 + vite: + specifier: ^6.3.5 + version: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1) + vite-plugin-solid: + specifier: ^2.10.2 + version: 2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)) + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) packages/utils: dependencies: From 6016fe8ca0844a00ee0bd665bec30f1451bbd6c2 Mon Sep 17 00:00:00 2001 From: partyplatter08-lab Date: Tue, 19 May 2026 06:30:57 -0400 Subject: [PATCH 2/3] test: expand monorepo Vitest examples --- apps/desktop/package.json | 1 + apps/storybook/package.json | 6 +- apps/storybook/storybook-vite.test.ts | 9 ++ apps/storybook/vitest.config.ts | 8 ++ apps/web-cluster/package.json | 6 +- .../src/cluster/container-metadata.test.ts | 47 ++++++++++ apps/web-cluster/vitest.config.ts | 8 ++ packages/config/package.json | 6 +- .../config/vite/relativeAliasResolver.test.ts | 32 +++++++ packages/config/vitest.config.ts | 8 ++ packages/database/crypto.test.ts | 21 +++++ packages/database/package.json | 6 +- packages/database/vitest.config.ts | 8 ++ packages/env/build.test.ts | 32 +++++++ packages/env/package.json | 6 +- packages/env/vitest.config.ts | 8 ++ packages/sdk-embed/package.json | 4 +- .../sdk-embed/src/vanilla/cap-embed.test.ts | 30 +++++++ packages/sdk-embed/vitest.config.ts | 8 ++ packages/sdk-recorder/package.json | 4 +- .../sdk-recorder/src/core/mime-types.test.ts | 24 ++++++ packages/sdk-recorder/vitest.config.ts | 8 ++ packages/tsconfig/base.test.ts | 18 ++++ packages/tsconfig/package.json | 6 ++ packages/tsconfig/vitest.config.ts | 8 ++ packages/utils/package.json | 4 +- packages/utils/src/helpers.test.ts | 51 +++++++++++ packages/utils/vitest.config.ts | 8 ++ packages/web-api-contract-effect/package.json | 6 ++ .../web-api-contract-effect/src/index.test.ts | 28 ++++++ .../web-api-contract-effect/vitest.config.ts | 8 ++ packages/web-api-contract/package.json | 6 ++ packages/web-api-contract/src/desktop.test.ts | 45 ++++++++++ packages/web-api-contract/vitest.config.ts | 8 ++ packages/web-backend/package.json | 6 +- .../src/Videos/EffectiveVideoRules.test.ts | 85 +++++++++++++++++++ packages/web-backend/vitest.config.ts | 8 ++ packages/web-domain/package.json | 6 +- packages/web-domain/src/utils.test.ts | 23 +++++ packages/web-domain/vitest.config.ts | 8 ++ pnpm-lock.yaml | 45 +++++++++- 41 files changed, 651 insertions(+), 16 deletions(-) create mode 100644 apps/storybook/storybook-vite.test.ts create mode 100644 apps/storybook/vitest.config.ts create mode 100644 apps/web-cluster/src/cluster/container-metadata.test.ts create mode 100644 apps/web-cluster/vitest.config.ts create mode 100644 packages/config/vite/relativeAliasResolver.test.ts create mode 100644 packages/config/vitest.config.ts create mode 100644 packages/database/crypto.test.ts create mode 100644 packages/database/vitest.config.ts create mode 100644 packages/env/build.test.ts create mode 100644 packages/env/vitest.config.ts create mode 100644 packages/sdk-embed/src/vanilla/cap-embed.test.ts create mode 100644 packages/sdk-embed/vitest.config.ts create mode 100644 packages/sdk-recorder/src/core/mime-types.test.ts create mode 100644 packages/sdk-recorder/vitest.config.ts create mode 100644 packages/tsconfig/base.test.ts create mode 100644 packages/tsconfig/vitest.config.ts create mode 100644 packages/utils/src/helpers.test.ts create mode 100644 packages/utils/vitest.config.ts create mode 100644 packages/web-api-contract-effect/src/index.test.ts create mode 100644 packages/web-api-contract-effect/vitest.config.ts create mode 100644 packages/web-api-contract/src/desktop.test.ts create mode 100644 packages/web-api-contract/vitest.config.ts create mode 100644 packages/web-backend/src/Videos/EffectiveVideoRules.test.ts create mode 100644 packages/web-backend/vitest.config.ts create mode 100644 packages/web-domain/src/utils.test.ts create mode 100644 packages/web-domain/vitest.config.ts diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 3e32c06fb03..645d35a54c2 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -9,6 +9,7 @@ "localdev": "dotenv -e ../../.env -- vinxi dev --port 3002", "build": "vinxi build", "tauri": "tauri", + "test": "vitest run", "test:memory": "node scripts/desktop-memory-soak.js", "test:memory:unit": "vitest run scripts/desktop-memory-soak.test.js" }, diff --git a/apps/storybook/package.json b/apps/storybook/package.json index 4ea3d148eef..89dada1d62e 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -4,7 +4,8 @@ "type": "module", "scripts": { "dev:storybook": "storybook dev -p 6006", - "build:storybook": "storybook build" + "build:storybook": "storybook build", + "test": "vitest run" }, "dependencies": { "@cap/ui-solid": "workspace:*", @@ -25,6 +26,7 @@ "storybook-solidjs-vite": "^1.0.0-beta.2", "typescript": "^5.8.3", "vite": "^6.3.5", - "vite-plugin-solid": "^2.10.2" + "vite-plugin-solid": "^2.10.2", + "vitest": "~2.1.9" } } diff --git a/apps/storybook/storybook-vite.test.ts b/apps/storybook/storybook-vite.test.ts new file mode 100644 index 00000000000..8c50f7ee338 --- /dev/null +++ b/apps/storybook/storybook-vite.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from "vitest"; +import config from "./vite.config"; + +describe("storybook Vite config", () => { + it("loads Solid and Cap UI plugins", () => { + expect(Array.isArray(config.plugins)).toBe(true); + expect(config.plugins).toHaveLength(2); + }); +}); diff --git a/apps/storybook/vitest.config.ts b/apps/storybook/vitest.config.ts new file mode 100644 index 00000000000..ce63ad84542 --- /dev/null +++ b/apps/storybook/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["**/*.test.ts"], + }, +}); diff --git a/apps/web-cluster/package.json b/apps/web-cluster/package.json index 5c30d80a6ce..2e586a6d162 100644 --- a/apps/web-cluster/package.json +++ b/apps/web-cluster/package.json @@ -4,7 +4,8 @@ "scripts": { "dev_": "pnpm dotenv -e ../../.env -- concurrently \"deno run --allow-all --watch ./src/runner/index.ts\" \"deno run --allow-all --watch ./src/shard-manager.ts\"", "build": "pnpm run --filter @cap/web-cluster^... build", - "build:docker": "cd ../.. && docker build -f apps/web-cluster/Dockerfile -t ghcr.io/brendonovich/cap-web-cluster:latest ." + "build:docker": "cd ../.. && docker build -f apps/web-cluster/Dockerfile -t ghcr.io/brendonovich/cap-web-cluster:latest .", + "test": "vitest run" }, "dependencies": { "@cap/web-backend": "workspace:*", @@ -26,6 +27,7 @@ }, "devDependencies": { "concurrently": "^9.2.1", - "dotenv-cli": "^10.0.0" + "dotenv-cli": "^10.0.0", + "vitest": "~2.1.9" } } diff --git a/apps/web-cluster/src/cluster/container-metadata.test.ts b/apps/web-cluster/src/cluster/container-metadata.test.ts new file mode 100644 index 00000000000..e7181d8dd0d --- /dev/null +++ b/apps/web-cluster/src/cluster/container-metadata.test.ts @@ -0,0 +1,47 @@ +import { Effect } from "effect"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { ContainerMetadata } from "./container-metadata"; + +const originalEnv = { ...process.env }; + +afterEach(() => { + process.env = { ...originalEnv }; + vi.unstubAllGlobals(); +}); + +function getContainerMetadata() { + return Effect.gen(function* () { + return yield* ContainerMetadata; + }).pipe(Effect.provide(ContainerMetadata.Default)); +} + +describe("ContainerMetadata", () => { + it("uses local defaults when ECS metadata is unavailable", async () => { + delete process.env.ECS_CONTAINER_METADATA_URI_V4; + delete process.env.PORT; + + const metadata = await Effect.runPromise(getContainerMetadata()); + + expect(metadata).toEqual({ ipAddress: "0.0.0.0", port: 42069 }); + }); + + it("reads container IP and port from the runtime environment", async () => { + process.env.ECS_CONTAINER_METADATA_URI_V4 = "http://metadata.local"; + process.env.PORT = "5173"; + const fetchMock = vi.fn(async () => ({ + json: async () => ({ + Containers: [ + { + Networks: [{ IPv4Addresses: ["10.0.0.42"] }], + }, + ], + }), + })); + vi.stubGlobal("fetch", fetchMock); + + const metadata = await Effect.runPromise(getContainerMetadata()); + + expect(fetchMock).toHaveBeenCalledWith("http://metadata.local/task"); + expect(metadata).toEqual({ ipAddress: "10.0.0.42", port: 5173 }); + }); +}); diff --git a/apps/web-cluster/vitest.config.ts b/apps/web-cluster/vitest.config.ts new file mode 100644 index 00000000000..cfbd4c3fcac --- /dev/null +++ b/apps/web-cluster/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +}); diff --git a/packages/config/package.json b/packages/config/package.json index e05ced6a042..d0e7721690b 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -5,6 +5,9 @@ "./*": "./*", "./vite": "./vite" }, + "scripts": { + "test": "vitest run" + }, "devDependencies": { "@types/node": "^20.4.5", "@typescript-eslint/eslint-plugin": "^5.59.6", @@ -17,7 +20,8 @@ "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-tailwindcss": "^3.12.0", - "eslint-utils": "^3.0.0" + "eslint-utils": "^3.0.0", + "vitest": "~2.1.9" }, "dependencies": { "@vitejs/plugin-react": "^4.0.3", diff --git a/packages/config/vite/relativeAliasResolver.test.ts b/packages/config/vite/relativeAliasResolver.test.ts new file mode 100644 index 00000000000..fe68dd0adff --- /dev/null +++ b/packages/config/vite/relativeAliasResolver.test.ts @@ -0,0 +1,32 @@ +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import resolver from "./relativeAliasResolver"; + +let testRoot: string | undefined; + +afterEach(async () => { + if (testRoot) await rm(testRoot, { recursive: true, force: true }); + testRoot = undefined; +}); + +describe("relativeAliasResolver", () => { + it("resolves ~/ imports relative to a package src directory", async () => { + testRoot = await mkdtemp(join(tmpdir(), "cap-alias-")); + await mkdir(join(testRoot, "src", "nested"), { recursive: true }); + await writeFile(join(testRoot, "package.json"), "{}"); + await writeFile(join(testRoot, "src", "nested", "target.ts"), ""); + + const customResolver = resolver.customResolver; + if (!customResolver) throw new Error("customResolver is not configured"); + + const resolved = await customResolver( + "~/nested/target", + join(testRoot, "src", "importer.ts"), + {}, + ); + + expect(resolved).toBe(join(testRoot, "src", "nested", "target.ts")); + }); +}); diff --git a/packages/config/vitest.config.ts b/packages/config/vitest.config.ts new file mode 100644 index 00000000000..ce63ad84542 --- /dev/null +++ b/packages/config/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["**/*.test.ts"], + }, +}); diff --git a/packages/database/crypto.test.ts b/packages/database/crypto.test.ts new file mode 100644 index 00000000000..b9785e5bbe7 --- /dev/null +++ b/packages/database/crypto.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { hashPassword, verifyPassword } from "./crypto"; + +describe("password hashing", () => { + it("verifies matching passwords and rejects mismatches", async () => { + const hash = await hashPassword("correct horse battery staple"); + + await expect( + verifyPassword(hash, "correct horse battery staple"), + ).resolves.toBe(true); + await expect(verifyPassword(hash, "wrong password")).resolves.toBe(false); + }); + + it("rejects empty stored hashes or password input", async () => { + await expect(verifyPassword("", "password")).resolves.toBe(false); + await expect(verifyPassword("stored", "")).resolves.toBe(false); + await expect(hashPassword("")).rejects.toThrow( + "Cannot hash empty or null password", + ); + }); +}); diff --git a/packages/database/package.json b/packages/database/package.json index a221a469a38..9f8f2ef9880 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -13,7 +13,8 @@ "db:studio": "drizzle-kit studio --config=drizzle.config.ts", "db:check-integrity": "node scripts/check-migration-integrity.js", "drizzle-kit": "pnpm dotenv -e ../../.env drizzle-kit --config=drizzle.config.ts", - "build": "tsdown" + "build": "tsdown", + "test": "vitest run" }, "dependencies": { "@cap/env": "workspace:*", @@ -50,7 +51,8 @@ "react-dom": "^19.1.1", "react-router-dom": "^6.18.0", "tsconfig": "workspace:*", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "~2.1.9" }, "engines": { "node": ">=20" diff --git a/packages/database/vitest.config.ts b/packages/database/vitest.config.ts new file mode 100644 index 00000000000..ce63ad84542 --- /dev/null +++ b/packages/database/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["**/*.test.ts"], + }, +}); diff --git a/packages/env/build.test.ts b/packages/env/build.test.ts new file mode 100644 index 00000000000..13433eb6cd0 --- /dev/null +++ b/packages/env/build.test.ts @@ -0,0 +1,32 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const originalEnv = { ...process.env }; + +beforeEach(() => { + process.env = { ...originalEnv }; + vi.resetModules(); +}); + +afterEach(() => { + process.env = { ...originalEnv }; +}); + +describe("buildEnv", () => { + it("uses WEB_URL as the public web URL when present", async () => { + process.env.WEB_URL = "https://web-url.example"; + process.env.NEXT_PUBLIC_WEB_URL = "https://next-public.example"; + + const { buildEnv } = await import("./build"); + + expect(buildEnv.NEXT_PUBLIC_WEB_URL).toBe("https://web-url.example"); + }); + + it("falls back to NEXT_PUBLIC_WEB_URL when WEB_URL is absent", async () => { + delete process.env.WEB_URL; + process.env.NEXT_PUBLIC_WEB_URL = "https://next-public.example"; + + const { buildEnv } = await import("./build"); + + expect(buildEnv.NEXT_PUBLIC_WEB_URL).toBe("https://next-public.example"); + }); +}); diff --git a/packages/env/package.json b/packages/env/package.json index 926c67f70b7..635593920e8 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -5,7 +5,8 @@ "main": "./index.ts", "types": "./index.ts", "scripts": { - "build": "tsdown" + "build": "tsdown", + "test": "vitest run" }, "publishConfig": { "main": "./dist/index.js" @@ -15,6 +16,7 @@ "zod": "^3.25.76" }, "devDependencies": { - "@types/node": "^22.15.14" + "@types/node": "^22.15.14", + "vitest": "~2.1.9" } } diff --git a/packages/env/vitest.config.ts b/packages/env/vitest.config.ts new file mode 100644 index 00000000000..ce63ad84542 --- /dev/null +++ b/packages/env/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["**/*.test.ts"], + }, +}); diff --git a/packages/sdk-embed/package.json b/packages/sdk-embed/package.json index 0c732bce52e..e694855cbe7 100644 --- a/packages/sdk-embed/package.json +++ b/packages/sdk-embed/package.json @@ -24,6 +24,7 @@ ], "scripts": { "build": "tsup", + "test": "vitest run", "typecheck": "tsc --noEmit" }, "dependencies": {}, @@ -39,6 +40,7 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "tsup": "^8.0.0", - "typescript": "^5.7.3" + "typescript": "^5.7.3", + "vitest": "~2.1.9" } } diff --git a/packages/sdk-embed/src/vanilla/cap-embed.test.ts b/packages/sdk-embed/src/vanilla/cap-embed.test.ts new file mode 100644 index 00000000000..b4fc33bee52 --- /dev/null +++ b/packages/sdk-embed/src/vanilla/cap-embed.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { createEmbedUrl } from "./cap-embed"; + +describe("createEmbedUrl", () => { + it("builds the default Cap embed URL", () => { + const url = createEmbedUrl({ + videoId: "video_123", + publicKey: "pk_test", + }); + + expect(url).toBe("https://cap.so/embed/video_123?sdk=1&pk=pk_test"); + }); + + it("includes optional base, playback, and branding parameters", () => { + const url = createEmbedUrl({ + videoId: "video_123", + publicKey: "pk_test", + apiBase: "https://app.example", + autoplay: true, + branding: { + logoUrl: "https://cdn.example/logo.png", + accentColor: "#abcdef", + }, + }); + + expect(url).toBe( + "https://app.example/embed/video_123?sdk=1&pk=pk_test&autoplay=1&logo=https%3A%2F%2Fcdn.example%2Flogo.png&accent=%23abcdef", + ); + }); +}); diff --git a/packages/sdk-embed/vitest.config.ts b/packages/sdk-embed/vitest.config.ts new file mode 100644 index 00000000000..cfbd4c3fcac --- /dev/null +++ b/packages/sdk-embed/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +}); diff --git a/packages/sdk-recorder/package.json b/packages/sdk-recorder/package.json index 7fef2785793..dcd16b9acad 100644 --- a/packages/sdk-recorder/package.json +++ b/packages/sdk-recorder/package.json @@ -20,6 +20,7 @@ ], "scripts": { "build": "tsup", + "test": "vitest run", "typecheck": "tsc --noEmit" }, "dependencies": {}, @@ -35,6 +36,7 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "tsup": "^8.0.0", - "typescript": "^5.7.3" + "typescript": "^5.7.3", + "vitest": "~2.1.9" } } diff --git a/packages/sdk-recorder/src/core/mime-types.test.ts b/packages/sdk-recorder/src/core/mime-types.test.ts new file mode 100644 index 00000000000..b92649d84fe --- /dev/null +++ b/packages/sdk-recorder/src/core/mime-types.test.ts @@ -0,0 +1,24 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { getSupportedMimeType } from "./mime-types"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("getSupportedMimeType", () => { + it("selects the first supported MIME type by priority", () => { + vi.stubGlobal("MediaRecorder", { + isTypeSupported: vi.fn((mimeType: string) => mimeType.includes("vp8")), + }); + + expect(getSupportedMimeType()).toBe("video/webm;codecs=vp8,opus"); + }); + + it("returns an empty string when no preferred type is supported", () => { + vi.stubGlobal("MediaRecorder", { + isTypeSupported: vi.fn(() => false), + }); + + expect(getSupportedMimeType()).toBe(""); + }); +}); diff --git a/packages/sdk-recorder/vitest.config.ts b/packages/sdk-recorder/vitest.config.ts new file mode 100644 index 00000000000..cfbd4c3fcac --- /dev/null +++ b/packages/sdk-recorder/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +}); diff --git a/packages/tsconfig/base.test.ts b/packages/tsconfig/base.test.ts new file mode 100644 index 00000000000..80509e2d1e3 --- /dev/null +++ b/packages/tsconfig/base.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import base from "./base.json"; +import nextjs from "./nextjs.json"; +import reactLibrary from "./react-library.json"; + +describe("shared TypeScript configs", () => { + it("keeps strictness enabled in the base config", () => { + expect(base.compilerOptions.strict).toBe(true); + expect(base.compilerOptions.forceConsistentCasingInFileNames).toBe(true); + }); + + it("extends the base config for React and Next.js presets", () => { + expect(reactLibrary.extends).toBe("./base.json"); + expect(reactLibrary.compilerOptions.jsx).toBe("react-jsx"); + expect(nextjs.extends).toBe("./base.json"); + expect(nextjs.compilerOptions.jsx).toBe("preserve"); + }); +}); diff --git a/packages/tsconfig/package.json b/packages/tsconfig/package.json index 92ef9e4c59b..fc048323746 100644 --- a/packages/tsconfig/package.json +++ b/packages/tsconfig/package.json @@ -3,6 +3,12 @@ "version": "0.0.0", "private": true, "license": "MIT", + "scripts": { + "test": "vitest run" + }, + "devDependencies": { + "vitest": "~2.1.9" + }, "publishConfig": { "access": "public" } diff --git a/packages/tsconfig/vitest.config.ts b/packages/tsconfig/vitest.config.ts new file mode 100644 index 00000000000..5aee5c5b672 --- /dev/null +++ b/packages/tsconfig/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["*.test.ts"], + }, +}); diff --git a/packages/utils/package.json b/packages/utils/package.json index 82f842d4c57..ee1e804d5b3 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -5,6 +5,7 @@ ".": "./src/index.ts" }, "scripts": { + "test": "vitest run", "typecheck": "tsc -b", "build": "tsdown" }, @@ -16,7 +17,8 @@ "react-dom": "^19.1.1", "react-router-dom": "^6.18.0", "tsconfig": "workspace:*", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "~2.1.9" }, "dependencies": { "@aws-sdk/client-s3": "^3.485.0", diff --git a/packages/utils/src/helpers.test.ts b/packages/utils/src/helpers.test.ts new file mode 100644 index 00000000000..e7cb72b6710 --- /dev/null +++ b/packages/utils/src/helpers.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; +import { + calculateStrokeDashoffset, + classNames, + getDisplayProgress, + getProgressCircleConfig, + isEmailAllowedByRestriction, + uuidFormat, + uuidParse, +} from "./helpers"; + +describe("helpers", () => { + it("merges Tailwind classes with later conflicting classes winning", () => { + expect(classNames("px-2 text-sm", false, "px-4")).toBe("text-sm px-4"); + }); + + it("round-trips UUID formatting", () => { + const uuid = "123e4567-e89b-12d3-a456-426614174000"; + + expect(uuidFormat(uuidParse(uuid))).toBe(uuid); + }); + + it("calculates progress circle geometry", () => { + const { radius, circumference } = getProgressCircleConfig(); + + expect(radius).toBe(8); + expect(circumference).toBe(2 * Math.PI * 8); + expect(calculateStrokeDashoffset(25, circumference)).toBe( + circumference * 0.75, + ); + }); + + it("prefers defined upload progress over processing progress", () => { + expect(getDisplayProgress(42, 10)).toBe(42); + expect(getDisplayProgress(0, 64)).toBe(0); + expect(getDisplayProgress(undefined, 64)).toBe(64); + }); + + it("matches exact emails and domain restrictions", () => { + expect(isEmailAllowedByRestriction("person@example.com", "")).toBe(true); + expect( + isEmailAllowedByRestriction( + "person@example.com", + "admin@example.com, example.com", + ), + ).toBe(true); + expect(isEmailAllowedByRestriction("person@other.com", "example.com")).toBe( + false, + ); + }); +}); diff --git a/packages/utils/vitest.config.ts b/packages/utils/vitest.config.ts new file mode 100644 index 00000000000..cfbd4c3fcac --- /dev/null +++ b/packages/utils/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +}); diff --git a/packages/web-api-contract-effect/package.json b/packages/web-api-contract-effect/package.json index d2e58173bbd..b9ae97fd89a 100644 --- a/packages/web-api-contract-effect/package.json +++ b/packages/web-api-contract-effect/package.json @@ -4,8 +4,14 @@ "main": "./src/index.ts", "types": "./src/index.ts", "type": "module", + "scripts": { + "test": "vitest run" + }, "dependencies": { "@effect/platform": "^0.92.1", "effect": "^3.18.4" + }, + "devDependencies": { + "vitest": "~2.1.9" } } diff --git a/packages/web-api-contract-effect/src/index.test.ts b/packages/web-api-contract-effect/src/index.test.ts new file mode 100644 index 00000000000..2addabb8913 --- /dev/null +++ b/packages/web-api-contract-effect/src/index.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { Authentication, User } from "./index"; + +describe("web API contract effect exports", () => { + it("constructs immutable user context data", () => { + const user = new User({ + id: "user_1", + email: "user@example.com", + stripeSubscriptionStatus: "active", + thirdPartyStripeSubscriptionId: null, + stripeSubscriptionId: "sub_1", + stripeCustomerId: "cus_1", + }); + + expect(user).toMatchObject({ + id: "user_1", + email: "user@example.com", + stripeSubscriptionStatus: "active", + thirdPartyStripeSubscriptionId: null, + stripeSubscriptionId: "sub_1", + stripeCustomerId: "cus_1", + }); + }); + + it("exposes the authentication context tag", () => { + expect(Authentication.key).toBe("Authentication"); + }); +}); diff --git a/packages/web-api-contract-effect/vitest.config.ts b/packages/web-api-contract-effect/vitest.config.ts new file mode 100644 index 00000000000..cfbd4c3fcac --- /dev/null +++ b/packages/web-api-contract-effect/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +}); diff --git a/packages/web-api-contract/package.json b/packages/web-api-contract/package.json index 9b22385f99e..49d6a4b7d67 100644 --- a/packages/web-api-contract/package.json +++ b/packages/web-api-contract/package.json @@ -4,8 +4,14 @@ "main": "./src/index.ts", "types": "./src/index.ts", "type": "module", + "scripts": { + "test": "vitest run" + }, "dependencies": { "@ts-rest/core": "^3.52.1", "zod": "^3.25.76" + }, + "devDependencies": { + "vitest": "~2.1.9" } } diff --git a/packages/web-api-contract/src/desktop.test.ts b/packages/web-api-contract/src/desktop.test.ts new file mode 100644 index 00000000000..8a5a5e0f128 --- /dev/null +++ b/packages/web-api-contract/src/desktop.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import { + OrganizationBrandingPatchBody, + OrganizationHexColor, + OrganizationLogoUpdate, +} from "./desktop"; + +describe("desktop contract schemas", () => { + it("accepts valid organization brand colors and rejects malformed hex", () => { + expect(OrganizationHexColor.safeParse("#ABC123").success).toBe(true); + expect(OrganizationHexColor.safeParse("ABC123").success).toBe(false); + expect(OrganizationHexColor.safeParse("#12345").success).toBe(false); + }); + + it("validates logo upload payloads", () => { + expect( + OrganizationLogoUpdate.safeParse({ + action: "upload", + contentType: "image/png", + data: "base64-data", + }).success, + ).toBe(true); + expect( + OrganizationLogoUpdate.safeParse({ + action: "upload", + contentType: "image/svg+xml", + data: "base64-data", + }).success, + ).toBe(false); + }); + + it("parses a full branding patch body", () => { + const result = OrganizationBrandingPatchBody.parse({ + brandColors: { + primary: "#000000", + secondary: null, + accent: "#ffffff", + background: null, + }, + logo: { action: "keep" }, + }); + + expect(result.logo).toEqual({ action: "keep" }); + }); +}); diff --git a/packages/web-api-contract/vitest.config.ts b/packages/web-api-contract/vitest.config.ts new file mode 100644 index 00000000000..cfbd4c3fcac --- /dev/null +++ b/packages/web-api-contract/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +}); diff --git a/packages/web-backend/package.json b/packages/web-backend/package.json index 6d644700853..97a1137ade3 100644 --- a/packages/web-backend/package.json +++ b/packages/web-backend/package.json @@ -8,7 +8,8 @@ "main": "./dist/index.js" }, "scripts": { - "build": "tsdown" + "build": "tsdown", + "test": "vitest run" }, "dependencies": { "@cap/env": "workspace:*", @@ -30,5 +31,8 @@ "effect": "^3.18.4", "next": "15.5.9", "server-only": "^0.0.1" + }, + "devDependencies": { + "vitest": "~2.1.9" } } diff --git a/packages/web-backend/src/Videos/EffectiveVideoRules.test.ts b/packages/web-backend/src/Videos/EffectiveVideoRules.test.ts new file mode 100644 index 00000000000..efb7a46e245 --- /dev/null +++ b/packages/web-backend/src/Videos/EffectiveVideoRules.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from "vitest"; +import { + collectPasswordHashes, + resolveEffectiveVideoRules, +} from "./EffectiveVideoRules"; + +describe("resolveEffectiveVideoRules", () => { + it("lets space settings override video and organization defaults", () => { + const rules = resolveEffectiveVideoRules({ + videoSettings: { disableCaptions: false, disableComments: true }, + organizationSettings: { + disableCaptions: true, + disableComments: false, + disableTranscript: true, + }, + spaces: [ + { + id: "space_1", + name: "Engineering", + settings: { disableCaptions: true }, + }, + ], + }); + + expect(rules.settings).toMatchObject({ + disableCaptions: true, + disableComments: true, + disableTranscript: true, + }); + expect(rules.inheritedSettings.disableCaptions).toEqual([ + { id: "space_1", name: "Engineering" }, + ]); + }); + + it("preserves explicit false video settings over organization true defaults", () => { + const rules = resolveEffectiveVideoRules({ + videoSettings: { disableSummary: false }, + organizationSettings: { disableSummary: true }, + spaces: [], + }); + + expect(rules.settings.disableSummary).toBe(false); + }); + + it("collects inherited password sources", () => { + const rules = resolveEffectiveVideoRules({ + spaces: [ + { id: "space_1", name: "Engineering", hasPassword: true }, + { id: "space_2", name: "Design", password: "hash" }, + { id: "space_3", name: "Open" }, + ], + }); + + expect(rules.hasInheritedPassword).toBe(true); + expect(rules.inheritedPasswordSources).toEqual([ + { id: "space_1", name: "Engineering" }, + { id: "space_2", name: "Design" }, + ]); + }); +}); + +describe("collectPasswordHashes", () => { + it("collects video and space password hashes while skipping empty values", () => { + expect( + collectPasswordHashes({ + videoPassword: "video_hash", + spacePasswords: [ + { password: "space_hash" }, + { password: null }, + { password: "" }, + {}, + ], + }), + ).toEqual(["video_hash", "space_hash"]); + }); + + it("handles absent video passwords", () => { + expect( + collectPasswordHashes({ + videoPassword: null, + spacePasswords: [{ password: "space_hash" }], + }), + ).toEqual(["space_hash"]); + }); +}); diff --git a/packages/web-backend/vitest.config.ts b/packages/web-backend/vitest.config.ts new file mode 100644 index 00000000000..cfbd4c3fcac --- /dev/null +++ b/packages/web-backend/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +}); diff --git a/packages/web-domain/package.json b/packages/web-domain/package.json index e96e70bc883..d6e6136d59b 100644 --- a/packages/web-domain/package.json +++ b/packages/web-domain/package.json @@ -9,7 +9,8 @@ }, "scripts": { "build": "tsdown", - "generate-openapi": "node scripts/generate-openapi.ts" + "generate-openapi": "node scripts/generate-openapi.ts", + "test": "vitest run" }, "dependencies": { "@effect/platform": "^0.92.1", @@ -18,6 +19,7 @@ "effect": "^3.18.4" }, "devDependencies": { - "@effect/platform-node": "^0.98.3" + "@effect/platform-node": "^0.98.3", + "vitest": "~2.1.9" } } diff --git a/packages/web-domain/src/utils.test.ts b/packages/web-domain/src/utils.test.ts new file mode 100644 index 00000000000..c9f989b9274 --- /dev/null +++ b/packages/web-domain/src/utils.test.ts @@ -0,0 +1,23 @@ +import { Option, Schema } from "effect"; +import { describe, expect, it } from "vitest"; +import { optional } from "./utils"; + +describe("optional", () => { + it("decodes present values as Some", () => { + const decoded = Schema.decodeUnknownSync( + Schema.Struct({ value: optional(Schema.String) }), + )({ value: "cap" }); + + expect(Option.getOrUndefined(decoded.value)).toBe("cap"); + }); + + it("decodes null values as None and leaves absent keys omitted", () => { + const schema = Schema.Struct({ value: optional(Schema.String) }); + + const nullDecoded = Schema.decodeUnknownSync(schema)({ value: null }); + const missingDecoded = Schema.decodeUnknownSync(schema)({}); + + expect(Option.isNone(nullDecoded.value)).toBe(true); + expect("value" in missingDecoded).toBe(false); + }); +}); diff --git a/packages/web-domain/vitest.config.ts b/packages/web-domain/vitest.config.ts new file mode 100644 index 00000000000..cfbd4c3fcac --- /dev/null +++ b/packages/web-domain/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 574c7d903f2..a4591beb8b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -375,6 +375,9 @@ importers: vite-plugin-solid: specifier: ^2.10.2 version: 2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)) + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) apps/web: dependencies: @@ -890,6 +893,9 @@ importers: dotenv-cli: specifier: ^10.0.0 version: 10.0.0 + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) infra: dependencies: @@ -961,6 +967,9 @@ importers: eslint-utils: specifier: ^3.0.0 version: 3.0.0(eslint@8.57.1) + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) packages/database: dependencies: @@ -1064,6 +1073,9 @@ importers: typescript: specifier: ^5.8.3 version: 5.8.3 + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) packages/env: dependencies: @@ -1077,6 +1089,9 @@ importers: '@types/node': specifier: ^22.15.14 version: 22.15.14 + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) packages/local-docker: {} @@ -1098,6 +1113,9 @@ importers: typescript: specifier: ^5.7.3 version: 5.8.3 + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) packages/sdk-recorder: dependencies: @@ -1117,8 +1135,15 @@ importers: typescript: specifier: ^5.7.3 version: 5.8.3 + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) - packages/tsconfig: {} + packages/tsconfig: + devDependencies: + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) packages/ui: dependencies: @@ -1344,6 +1369,9 @@ importers: typescript: specifier: ^5.8.3 version: 5.8.3 + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) packages/web-api-contract: dependencies: @@ -1353,6 +1381,10 @@ importers: zod: specifier: ^3.25.76 version: 3.25.76 + devDependencies: + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) packages/web-api-contract-effect: dependencies: @@ -1362,6 +1394,10 @@ importers: effect: specifier: ^3.18.4 version: 3.18.4 + devDependencies: + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) packages/web-backend: dependencies: @@ -1422,6 +1458,10 @@ importers: server-only: specifier: ^0.0.1 version: 0.0.1 + devDependencies: + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) packages/web-domain: dependencies: @@ -1441,6 +1481,9 @@ importers: '@effect/platform-node': specifier: ^0.98.3 version: 0.98.3(@effect/cluster@0.50.4(@effect/platform@0.92.1(effect@3.18.4))(@effect/rpc@0.71.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(@effect/sql@0.44.2(@effect/experimental@0.56.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4)(ioredis@5.6.1))(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(@effect/workflow@0.11.3(@effect/platform@0.92.1(effect@3.18.4))(@effect/rpc@0.71.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(effect@3.18.4))(effect@3.18.4))(@effect/platform@0.92.1(effect@3.18.4))(@effect/rpc@0.71.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(@effect/sql@0.44.2(@effect/experimental@0.56.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4)(ioredis@5.6.1))(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(effect@3.18.4) + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) packages: From 50debbd9cfa54d07be594a984813ca1f5cb63fb8 Mon Sep 17 00:00:00 2001 From: partyplatter08-lab Date: Tue, 19 May 2026 06:36:57 -0400 Subject: [PATCH 3/3] chore: revise bounty claim package