diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 814518b..9b3b4a8 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -14,7 +14,7 @@ on: types: [opened, synchronize, reopened] env: - NODE_VERSION: "22.9.0" + NODE_VERSION: "22.12.0" PNPM_VERSION: "10.20.0" jobs: diff --git a/README.md b/README.md index f6abd2e..e2b89fd 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ For technical details, see [Architecture Documentation](./docs/architecture.md). ### Prerequisites -- Node.js 22.9.0 or higher +- Node.js 22.12.0 or higher - PostgreSQL database - Kubernetes cluster with KubeBlocks installed - GitHub OAuth application credentials diff --git a/docs/development.md b/docs/development.md index e60afc6..b4275ae 100644 --- a/docs/development.md +++ b/docs/development.md @@ -14,7 +14,7 @@ This document provides guidance for local development and code patterns. ## Prerequisites -- **Node.js**: 22.9.0 or higher +- **Node.js**: 22.12.0 or higher - **pnpm**: 9.x or higher - **PostgreSQL**: 14.x or higher - **Kubernetes cluster**: For full integration testing (optional) diff --git a/lib/platform/control/commands/database/create-database.test.ts b/lib/platform/control/commands/database/create-database.test.ts new file mode 100644 index 0000000..8174134 --- /dev/null +++ b/lib/platform/control/commands/database/create-database.test.ts @@ -0,0 +1,183 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + prisma, + getK8sServiceForUser, + toK8sProjectName, + generateK8sRandomString, +} = vi.hoisted(() => ({ + prisma: { + project: { + findUnique: vi.fn(), + }, + database: { + create: vi.fn(), + }, + }, + getK8sServiceForUser: vi.fn(), + toK8sProjectName: vi.fn(), + generateK8sRandomString: vi.fn(), +})) + +vi.mock('@/lib/db', () => ({ + prisma, +})) + +vi.mock('@/lib/k8s/k8s-service-helper', () => ({ + getK8sServiceForUser, +})) + +vi.mock('@/lib/k8s/kubernetes-utils', () => ({ + KubernetesUtils: { + toK8sProjectName, + generateRandomString: generateK8sRandomString, + }, +})) + +vi.mock('@/lib/k8s/versions', () => ({ + VERSIONS: { + STORAGE: { + DATABASE_SIZE: '3Gi', + }, + RESOURCES: { + DATABASE: { + requests: { + cpu: '100m', + memory: '102Mi', + }, + limits: { + cpu: '1000m', + memory: '1024Mi', + }, + }, + }, + }, +})) + +import { createDatabaseCommand } from '@/lib/platform/control/commands/database/create-database' + +describe('createDatabaseCommand', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('returns an error when the project does not exist', async () => { + prisma.project.findUnique.mockResolvedValue(null) + + const result = await createDatabaseCommand({ + userId: 'user-1', + projectId: 'project-1', + }) + + expect(result).toEqual({ + success: false, + error: 'Project not found', + }) + expect(prisma.database.create).not.toHaveBeenCalled() + }) + + it('returns an error when the project belongs to another user', async () => { + prisma.project.findUnique.mockResolvedValue({ + id: 'project-1', + userId: 'other-user', + databases: [], + }) + + const result = await createDatabaseCommand({ + userId: 'user-1', + projectId: 'project-1', + }) + + expect(result).toEqual({ + success: false, + error: 'Unauthorized', + }) + }) + + it('returns an error when a database already exists', async () => { + prisma.project.findUnique.mockResolvedValue({ + id: 'project-1', + userId: 'user-1', + databases: [{ id: 'database-1' }], + }) + + const result = await createDatabaseCommand({ + userId: 'user-1', + projectId: 'project-1', + }) + + expect(result).toEqual({ + success: false, + error: 'Database already exists for this project', + }) + }) + + it('returns a friendly error when kubeconfig is missing', async () => { + prisma.project.findUnique.mockResolvedValue({ + id: 'project-1', + userId: 'user-1', + name: 'Project Alpha', + databases: [], + }) + getK8sServiceForUser.mockRejectedValue( + new Error('User [user-1] does not have KUBECONFIG configured') + ) + + const result = await createDatabaseCommand({ + userId: 'user-1', + projectId: 'project-1', + }) + + expect(result).toEqual({ + success: false, + error: 'Please configure your kubeconfig before creating a database', + }) + expect(prisma.database.create).not.toHaveBeenCalled() + }) + + it('creates the database with the generated default name and expected resource config', async () => { + prisma.project.findUnique.mockResolvedValue({ + id: 'project-1', + userId: 'user-1', + name: 'Project Alpha', + databases: [], + }) + prisma.database.create.mockResolvedValue({ + id: 'database-1', + name: 'projectalpha-db-suffix', + }) + getK8sServiceForUser.mockResolvedValue({ + getDefaultNamespace: vi.fn().mockReturnValue('ns-user-1'), + }) + toK8sProjectName.mockReturnValue('projectalpha') + generateK8sRandomString.mockReturnValue('suffix') + + const result = await createDatabaseCommand({ + userId: 'user-1', + projectId: 'project-1', + }) + + expect(result).toEqual({ + success: true, + data: { + id: 'database-1', + name: 'projectalpha-db-suffix', + }, + }) + expect(prisma.database.create).toHaveBeenCalledWith({ + data: { + projectId: 'project-1', + name: 'projectalpha-db-suffix', + k8sNamespace: 'ns-user-1', + databaseName: 'projectalpha-db-suffix', + status: 'CREATING', + lockedUntil: null, + storageSize: '3Gi', + cpuRequest: '100m', + cpuLimit: '1000m', + memoryRequest: '102Mi', + memoryLimit: '1024Mi', + }, + }) + }) +}) diff --git a/lib/platform/control/commands/database/delete-database.test.ts b/lib/platform/control/commands/database/delete-database.test.ts new file mode 100644 index 0000000..fc096aa --- /dev/null +++ b/lib/platform/control/commands/database/delete-database.test.ts @@ -0,0 +1,86 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { prisma } = vi.hoisted(() => ({ + prisma: { + database: { + findUnique: vi.fn(), + update: vi.fn(), + }, + }, +})) + +vi.mock('@/lib/db', () => ({ + prisma, +})) + +import { deleteDatabaseCommand } from '@/lib/platform/control/commands/database/delete-database' + +describe('deleteDatabaseCommand', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('returns an error when the database does not exist', async () => { + prisma.database.findUnique.mockResolvedValue(null) + + const result = await deleteDatabaseCommand({ + userId: 'user-1', + databaseId: 'database-1', + }) + + expect(result).toEqual({ + success: false, + error: 'Database not found', + }) + expect(prisma.database.update).not.toHaveBeenCalled() + }) + + it('returns an error when the database belongs to another user', async () => { + prisma.database.findUnique.mockResolvedValue({ + id: 'database-1', + project: { + userId: 'other-user', + }, + }) + + const result = await deleteDatabaseCommand({ + userId: 'user-1', + databaseId: 'database-1', + }) + + expect(result).toEqual({ + success: false, + error: 'Unauthorized', + }) + expect(prisma.database.update).not.toHaveBeenCalled() + }) + + it('marks the database as terminating and clears the lock', async () => { + prisma.database.findUnique.mockResolvedValue({ + id: 'database-1', + project: { + userId: 'user-1', + }, + }) + prisma.database.update.mockResolvedValue({ + id: 'database-1', + }) + + const result = await deleteDatabaseCommand({ + userId: 'user-1', + databaseId: 'database-1', + }) + + expect(result).toEqual({ + success: true, + data: undefined, + }) + expect(prisma.database.update).toHaveBeenCalledWith({ + where: { id: 'database-1' }, + data: { + status: 'TERMINATING', + lockedUntil: null, + }, + }) + }) +}) diff --git a/lib/platform/control/commands/project/create-project-from-github.test.ts b/lib/platform/control/commands/project/create-project-from-github.test.ts new file mode 100644 index 0000000..83d858a --- /dev/null +++ b/lib/platform/control/commands/project/create-project-from-github.test.ts @@ -0,0 +1,249 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + findInstallationRepository, + getUserDefaultNamespace, + findGitHubInstallationById, + createProjectWithSandbox, + createCloneRepositoryTask, + listUserSkills, +} = vi.hoisted(() => ({ + findInstallationRepository: vi.fn(), + getUserDefaultNamespace: vi.fn(), + findGitHubInstallationById: vi.fn(), + createProjectWithSandbox: vi.fn(), + createCloneRepositoryTask: vi.fn(), + listUserSkills: vi.fn(), +})) + +vi.mock('@/lib/platform/integrations/github/find-installation-repository', () => ({ + findInstallationRepository, +})) + +vi.mock('@/lib/platform/integrations/k8s/get-user-default-namespace', () => ({ + getUserDefaultNamespace, +})) + +vi.mock('@/lib/platform/persistence/github/find-github-installation-by-id', () => ({ + findGitHubInstallationById, +})) + +vi.mock('@/lib/platform/persistence/project/create-project-with-sandbox', () => ({ + createProjectWithSandbox, +})) + +vi.mock('@/lib/platform/persistence/project-task/create-clone-repository-task', () => ({ + createCloneRepositoryTask, +})) + +vi.mock('@/lib/repo/user-skill', () => ({ + listUserSkills, +})) + +import { createProjectFromGitHubCommand } from '@/lib/platform/control/commands/project/create-project-from-github' + +const baseInput = { + userId: 'user-1', + installationId: 101, + repoId: 202, + repoName: 'Project Alpha', + repoFullName: 'acme/project-alpha', + defaultBranch: 'main', + description: 'Imported project', +} + +describe('createProjectFromGitHubCommand', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('returns an error when repository metadata is incomplete', async () => { + const result = await createProjectFromGitHubCommand({ + ...baseInput, + repoFullName: '', + }) + + expect(result).toEqual({ + success: false, + error: 'Repository metadata is required', + }) + expect(findGitHubInstallationById).not.toHaveBeenCalled() + }) + + it('returns an error when the repo name is invalid', async () => { + const result = await createProjectFromGitHubCommand({ + ...baseInput, + repoName: '***', + }) + + expect(result).toEqual({ + success: false, + error: 'Project name can only contain letters, numbers, spaces, and hyphens', + }) + expect(findGitHubInstallationById).not.toHaveBeenCalled() + }) + + it('returns an error when the installation is missing or belongs to another user', async () => { + findGitHubInstallationById.mockResolvedValue({ + id: 'gha-1', + installationId: 101, + userId: 'other-user', + }) + + const result = await createProjectFromGitHubCommand(baseInput) + + expect(result).toEqual({ + success: false, + error: 'Installation not found', + }) + expect(findInstallationRepository).not.toHaveBeenCalled() + }) + + it('returns an error when repository verification fails', async () => { + findGitHubInstallationById.mockResolvedValue({ + id: 'gha-1', + installationId: 101, + userId: 'user-1', + }) + findInstallationRepository.mockRejectedValue(new Error('GitHub unavailable')) + + const result = await createProjectFromGitHubCommand(baseInput) + + expect(result).toEqual({ + success: false, + error: 'Failed to verify repository access', + }) + }) + + it('returns an error when the repository is not accessible in the installation', async () => { + findGitHubInstallationById.mockResolvedValue({ + id: 'gha-1', + installationId: 101, + userId: 'user-1', + }) + findInstallationRepository.mockResolvedValue(null) + + const result = await createProjectFromGitHubCommand(baseInput) + + expect(result).toEqual({ + success: false, + error: 'Repository not found in selected installation', + }) + }) + + it('returns a friendly error when kubeconfig is missing', async () => { + findGitHubInstallationById.mockResolvedValue({ + id: 'gha-1', + installationId: 101, + userId: 'user-1', + }) + findInstallationRepository.mockResolvedValue({ + id: 202, + full_name: 'acme/project-alpha', + }) + getUserDefaultNamespace.mockRejectedValue( + new Error('User [user-1] does not have KUBECONFIG configured') + ) + + const result = await createProjectFromGitHubCommand(baseInput) + + expect(result).toEqual({ + success: false, + error: 'Please configure your kubeconfig before creating a project', + }) + expect(createProjectWithSandbox).not.toHaveBeenCalled() + expect(createCloneRepositoryTask).not.toHaveBeenCalled() + }) + + it('does not create a clone task when project creation fails', async () => { + findGitHubInstallationById.mockResolvedValue({ + id: 'gha-1', + installationId: 101, + userId: 'user-1', + }) + findInstallationRepository.mockResolvedValue({ + id: 202, + full_name: 'acme/project-alpha', + }) + getUserDefaultNamespace.mockResolvedValue('ns-user-1') + listUserSkills.mockResolvedValue([]) + createProjectWithSandbox.mockResolvedValue({ + success: false, + error: 'Project already exists', + }) + + const result = await createProjectFromGitHubCommand(baseInput) + + expect(result).toEqual({ + success: false, + error: 'Project already exists', + }) + expect(createCloneRepositoryTask).not.toHaveBeenCalled() + }) + + it('creates the project and clone task when verification succeeds', async () => { + findGitHubInstallationById.mockResolvedValue({ + id: 'gha-1', + installationId: 101, + userId: 'user-1', + }) + findInstallationRepository.mockResolvedValue({ + id: 202, + full_name: 'acme/project-alpha', + }) + getUserDefaultNamespace.mockResolvedValue('ns-user-1') + listUserSkills.mockResolvedValue([ + { + id: 'user-skill-1', + skillId: 'frontend-design', + installCommand: 'install frontend-design', + }, + ]) + createProjectWithSandbox.mockResolvedValue({ + success: true, + data: { + project: { id: 'project-1', name: 'Project Alpha' }, + sandbox: { id: 'sandbox-1' }, + }, + }) + + const result = await createProjectFromGitHubCommand(baseInput) + + expect(result).toEqual({ + success: true, + data: { id: 'project-1', name: 'Project Alpha' }, + }) + expect(findInstallationRepository).toHaveBeenCalledWith({ + installationId: 101, + repoId: 202, + repoFullName: 'acme/project-alpha', + }) + expect(createProjectWithSandbox).toHaveBeenCalledWith({ + userId: 'user-1', + namespace: 'ns-user-1', + name: 'Project Alpha', + description: 'Imported project', + githubSource: { + githubAppInstallationId: 'gha-1', + githubRepoId: 202, + githubRepoFullName: 'acme/project-alpha', + githubRepoDefaultBranch: 'main', + }, + initialInstallSkills: [ + { + userSkillId: 'user-skill-1', + skillId: 'frontend-design', + installCommand: 'install frontend-design', + }, + ], + }) + expect(createCloneRepositoryTask).toHaveBeenCalledWith({ + projectId: 'project-1', + sandboxId: 'sandbox-1', + installationId: 101, + repoId: 202, + repoFullName: 'acme/project-alpha', + defaultBranch: 'main', + }) + }) +}) diff --git a/lib/platform/control/commands/project/create-project.test.ts b/lib/platform/control/commands/project/create-project.test.ts new file mode 100644 index 0000000..19390f2 --- /dev/null +++ b/lib/platform/control/commands/project/create-project.test.ts @@ -0,0 +1,178 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + getUserDefaultNamespace, + createProjectWithSandbox, + listUserSkills, +} = vi.hoisted(() => ({ + getUserDefaultNamespace: vi.fn(), + createProjectWithSandbox: vi.fn(), + listUserSkills: vi.fn(), +})) + +vi.mock('@/lib/platform/integrations/k8s/get-user-default-namespace', () => ({ + getUserDefaultNamespace, +})) + +vi.mock('@/lib/platform/persistence/project/create-project-with-sandbox', () => ({ + createProjectWithSandbox, +})) + +vi.mock('@/lib/repo/user-skill', () => ({ + listUserSkills, +})) + +import { createProjectCommand } from '@/lib/platform/control/commands/project/create-project' + +describe('createProjectCommand', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('returns an error when the project name is empty', async () => { + const result = await createProjectCommand({ + userId: 'user-1', + name: ' ', + }) + + expect(result).toEqual({ + success: false, + error: 'Project name cannot be empty', + }) + expect(getUserDefaultNamespace).not.toHaveBeenCalled() + }) + + it('returns an error immediately when the project name is invalid', async () => { + const result = await createProjectCommand({ + userId: 'user-1', + name: '***', + }) + + expect(result).toEqual({ + success: false, + error: 'Project name can only contain letters, numbers, spaces, and hyphens', + }) + expect(getUserDefaultNamespace).not.toHaveBeenCalled() + expect(listUserSkills).not.toHaveBeenCalled() + expect(createProjectWithSandbox).not.toHaveBeenCalled() + }) + + it('returns an error when the project name does not start with a letter', async () => { + const result = await createProjectCommand({ + userId: 'user-1', + name: '1 project', + }) + + expect(result).toEqual({ + success: false, + error: 'Project name must start with a letter', + }) + }) + + it('returns an error when the project name does not end with a letter', async () => { + const result = await createProjectCommand({ + userId: 'user-1', + name: 'Project 1', + }) + + expect(result).toEqual({ + success: false, + error: 'Project name must end with a letter', + }) + }) + + it('returns a friendly error when kubeconfig is missing', async () => { + getUserDefaultNamespace.mockRejectedValue( + new Error('User [user-1] does not have KUBECONFIG configured') + ) + + const result = await createProjectCommand({ + userId: 'user-1', + name: 'Project Alpha', + }) + + expect(result).toEqual({ + success: false, + error: 'Please configure your kubeconfig before creating a project', + }) + expect(listUserSkills).not.toHaveBeenCalled() + expect(createProjectWithSandbox).not.toHaveBeenCalled() + }) + + it('returns the persistence error when project creation fails', async () => { + getUserDefaultNamespace.mockResolvedValue('ns-user-1') + listUserSkills.mockResolvedValue([]) + createProjectWithSandbox.mockResolvedValue({ + success: false, + error: 'Project already exists', + }) + + const result = await createProjectCommand({ + userId: 'user-1', + name: 'Project Alpha', + description: 'Demo project', + }) + + expect(result).toEqual({ + success: false, + error: 'Project already exists', + }) + expect(createProjectWithSandbox).toHaveBeenCalledWith({ + userId: 'user-1', + namespace: 'ns-user-1', + name: 'Project Alpha', + description: 'Demo project', + initialInstallSkills: [], + }) + }) + + it('creates the project with the user namespace and initial skill installs', async () => { + getUserDefaultNamespace.mockResolvedValue('ns-user-1') + listUserSkills.mockResolvedValue([ + { + id: 'user-skill-1', + skillId: 'frontend-design', + installCommand: 'install frontend-design', + }, + ]) + createProjectWithSandbox.mockResolvedValue({ + success: true, + data: { + project: { + id: 'project-1', + name: 'Project Alpha', + }, + sandbox: { + id: 'sandbox-1', + }, + }, + }) + + const result = await createProjectCommand({ + userId: 'user-1', + name: 'Project Alpha', + description: 'Demo project', + }) + + expect(result).toEqual({ + success: true, + data: { + id: 'project-1', + name: 'Project Alpha', + }, + }) + expect(createProjectWithSandbox).toHaveBeenCalledWith({ + userId: 'user-1', + namespace: 'ns-user-1', + name: 'Project Alpha', + description: 'Demo project', + initialInstallSkills: [ + { + userSkillId: 'user-skill-1', + skillId: 'frontend-design', + installCommand: 'install frontend-design', + }, + ], + }) + }) +}) diff --git a/lib/platform/control/commands/skill/enable-global-skill.test.ts b/lib/platform/control/commands/skill/enable-global-skill.test.ts new file mode 100644 index 0000000..ef8d4b0 --- /dev/null +++ b/lib/platform/control/commands/skill/enable-global-skill.test.ts @@ -0,0 +1,169 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + prisma, + triggerRunnableTasksForProject, + createInstallSkillTask, + findSkillCatalogEntry, +} = vi.hoisted(() => ({ + prisma: { + $transaction: vi.fn(), + }, + triggerRunnableTasksForProject: vi.fn(), + createInstallSkillTask: vi.fn(), + findSkillCatalogEntry: vi.fn(), +})) + +vi.mock('@/lib/db', () => ({ + prisma, +})) + +vi.mock('@/lib/jobs/project-task', () => ({ + triggerRunnableTasksForProject, +})) + +vi.mock('@/lib/platform/persistence/project-task/create-install-skill-task', () => ({ + createInstallSkillTask, +})) + +vi.mock('@/lib/skills/catalog', () => ({ + findSkillCatalogEntry, +})) + +import { enableGlobalSkillCommand } from '@/lib/platform/control/commands/skill/enable-global-skill' + +function createEnableTx() { + return { + userSkill: { + findUnique: vi.fn(), + create: vi.fn(), + }, + project: { + findMany: vi.fn(), + }, + projectTask: { + findFirst: vi.fn(), + }, + } +} + +type EnableTx = ReturnType + +describe('enableGlobalSkillCommand', () => { + beforeEach(() => { + vi.clearAllMocks() + findSkillCatalogEntry.mockReturnValue({ + skillId: 'frontend-design', + installCommand: 'install frontend-design', + }) + }) + + it('returns an error when the skill is not in the catalog', async () => { + findSkillCatalogEntry.mockReturnValue(undefined) + + const result = await enableGlobalSkillCommand({ + userId: 'user-1', + skillId: 'missing-skill', + }) + + expect(result).toEqual({ + success: false, + error: 'Skill not found', + }) + expect(prisma.$transaction).not.toHaveBeenCalled() + }) + + it('returns the existing user skill without creating tasks when already enabled', async () => { + const existingUserSkill = { + id: 'user-skill-1', + skillId: 'frontend-design', + } + const tx = createEnableTx() + tx.userSkill.findUnique.mockResolvedValue(existingUserSkill) + prisma.$transaction.mockImplementation(async (callback: (transaction: EnableTx) => Promise) => + callback(tx) + ) + + const result = await enableGlobalSkillCommand({ + userId: 'user-1', + skillId: 'frontend-design', + }) + + expect(result).toEqual({ + success: true, + data: existingUserSkill, + }) + expect(tx.userSkill.create).not.toHaveBeenCalled() + expect(tx.project.findMany).not.toHaveBeenCalled() + expect(createInstallSkillTask).not.toHaveBeenCalled() + expect(triggerRunnableTasksForProject).not.toHaveBeenCalled() + }) + + it('creates install tasks only for projects that need them and triggers runnable ones', async () => { + const createdUserSkill = { + id: 'user-skill-1', + skillId: 'frontend-design', + installCommand: 'install frontend-design', + } + const tx = createEnableTx() + tx.userSkill.findUnique.mockResolvedValue(null) + tx.userSkill.create.mockResolvedValue(createdUserSkill) + tx.project.findMany.mockResolvedValue([ + { id: 'project-no-sandbox', sandboxes: [] }, + { id: 'project-running', sandboxes: [{ id: 'sandbox-running', status: 'RUNNING' }] }, + { id: 'project-stopped', sandboxes: [{ id: 'sandbox-stopped', status: 'STOPPED' }] }, + { id: 'project-existing-task', sandboxes: [{ id: 'sandbox-existing', status: 'RUNNING' }] }, + ]) + tx.projectTask.findFirst.mockImplementation(async ({ where }: { where: { projectId: string } }) => { + if (where.projectId === 'project-existing-task') { + return { id: 'task-1' } + } + + return null + }) + prisma.$transaction.mockImplementation(async (callback: (transaction: EnableTx) => Promise) => + callback(tx) + ) + + const result = await enableGlobalSkillCommand({ + userId: 'user-1', + skillId: 'frontend-design', + }) + + expect(result).toEqual({ + success: true, + data: createdUserSkill, + }) + expect(createInstallSkillTask).toHaveBeenCalledTimes(2) + expect(createInstallSkillTask).toHaveBeenNthCalledWith(1, tx, { + projectId: 'project-running', + sandboxId: 'sandbox-running', + userSkillId: 'user-skill-1', + skillId: 'frontend-design', + installCommand: 'install frontend-design', + }) + expect(createInstallSkillTask).toHaveBeenNthCalledWith(2, tx, { + projectId: 'project-stopped', + sandboxId: 'sandbox-stopped', + userSkillId: 'user-skill-1', + skillId: 'frontend-design', + installCommand: 'install frontend-design', + }) + expect(triggerRunnableTasksForProject).toHaveBeenCalledTimes(1) + expect(triggerRunnableTasksForProject).toHaveBeenCalledWith('project-running') + }) + + it('returns a uniform failure when the transaction throws', async () => { + prisma.$transaction.mockRejectedValue(new Error('db unavailable')) + + const result = await enableGlobalSkillCommand({ + userId: 'user-1', + skillId: 'frontend-design', + }) + + expect(result).toEqual({ + success: false, + error: 'Failed to enable skill', + }) + }) +}) diff --git a/lib/platform/control/commands/skill/uninstall-global-skill.test.ts b/lib/platform/control/commands/skill/uninstall-global-skill.test.ts new file mode 100644 index 0000000..e408a1a --- /dev/null +++ b/lib/platform/control/commands/skill/uninstall-global-skill.test.ts @@ -0,0 +1,213 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + prisma, + triggerRunnableTasksForProject, + createUninstallSkillTask, + findSkillCatalogEntry, +} = vi.hoisted(() => ({ + prisma: { + $transaction: vi.fn(), + }, + triggerRunnableTasksForProject: vi.fn(), + createUninstallSkillTask: vi.fn(), + findSkillCatalogEntry: vi.fn(), +})) + +vi.mock('@/lib/db', () => ({ + prisma, +})) + +vi.mock('@/lib/jobs/project-task', () => ({ + triggerRunnableTasksForProject, +})) + +vi.mock('@/lib/platform/persistence/project-task/create-uninstall-skill-task', () => ({ + createUninstallSkillTask, +})) + +vi.mock('@/lib/skills/catalog', () => ({ + findSkillCatalogEntry, +})) + +import { uninstallGlobalSkillCommand } from '@/lib/platform/control/commands/skill/uninstall-global-skill' + +function createUninstallTx() { + return { + userSkill: { + findUnique: vi.fn(), + delete: vi.fn(), + }, + project: { + findMany: vi.fn(), + }, + projectTask: { + findFirst: vi.fn(), + updateMany: vi.fn(), + }, + } +} + +type UninstallTx = ReturnType + +describe('uninstallGlobalSkillCommand', () => { + beforeEach(() => { + vi.clearAllMocks() + findSkillCatalogEntry.mockReturnValue({ + skillId: 'frontend-design', + uninstallCommand: 'remove frontend-design', + }) + }) + + it('returns an error when the skill is not in the catalog', async () => { + findSkillCatalogEntry.mockReturnValue(undefined) + + const result = await uninstallGlobalSkillCommand({ + userId: 'user-1', + skillId: 'missing-skill', + }) + + expect(result).toEqual({ + success: false, + error: 'Skill not found', + }) + }) + + it('returns success when the user skill is already absent', async () => { + const tx = createUninstallTx() + tx.userSkill.findUnique.mockResolvedValue(null) + prisma.$transaction.mockImplementation(async (callback: (transaction: UninstallTx) => Promise) => + callback(tx) + ) + + const result = await uninstallGlobalSkillCommand({ + userId: 'user-1', + skillId: 'frontend-design', + }) + + expect(result).toEqual({ + success: true, + data: { + skillId: 'frontend-design', + }, + }) + expect(createUninstallSkillTask).not.toHaveBeenCalled() + expect(triggerRunnableTasksForProject).not.toHaveBeenCalled() + }) + + it('cancels stale install tasks and creates uninstall work only where needed', async () => { + const existingUserSkill = { + id: 'user-skill-1', + installCommand: 'install frontend-design', + } + const installedAt = new Date('2026-03-17T10:00:00Z') + const coveredAt = new Date('2026-03-18T10:00:00Z') + const tx = createUninstallTx() + tx.userSkill.findUnique.mockResolvedValue(existingUserSkill) + tx.project.findMany.mockResolvedValue([ + { id: 'project-no-sandbox', sandboxes: [] }, + { id: 'project-running', sandboxes: [{ id: 'sandbox-running', status: 'RUNNING' }] }, + { id: 'project-stopped', sandboxes: [{ id: 'sandbox-stopped', status: 'STOPPED' }] }, + { id: 'project-no-install', sandboxes: [{ id: 'sandbox-no-install', status: 'RUNNING' }] }, + { id: 'project-covered', sandboxes: [{ id: 'sandbox-covered', status: 'RUNNING' }] }, + ]) + tx.projectTask.findFirst.mockImplementation( + async ({ + where, + }: { + where: { + projectId: string + type: 'INSTALL_SKILL' | 'UNINSTALL_SKILL' + } + }) => { + if (where.projectId === 'project-running' && where.type === 'INSTALL_SKILL') { + return { createdAt: installedAt } + } + if (where.projectId === 'project-stopped' && where.type === 'INSTALL_SKILL') { + return { createdAt: installedAt } + } + if (where.projectId === 'project-covered' && where.type === 'INSTALL_SKILL') { + return { createdAt: installedAt } + } + if (where.projectId === 'project-covered' && where.type === 'UNINSTALL_SKILL') { + return { createdAt: coveredAt } + } + + return null + } + ) + tx.projectTask.updateMany.mockResolvedValue({ count: 1 }) + tx.userSkill.delete.mockResolvedValue(existingUserSkill) + prisma.$transaction.mockImplementation(async (callback: (transaction: UninstallTx) => Promise) => + callback(tx) + ) + + const result = await uninstallGlobalSkillCommand({ + userId: 'user-1', + skillId: 'frontend-design', + }) + + expect(result).toEqual({ + success: true, + data: { + skillId: 'frontend-design', + }, + }) + expect(tx.projectTask.updateMany).toHaveBeenCalledTimes(4) + expect(tx.projectTask.updateMany).toHaveBeenNthCalledWith(1, { + where: { + projectId: 'project-running', + skillId: 'frontend-design', + type: 'INSTALL_SKILL', + status: { + in: ['PENDING', 'WAITING_FOR_PREREQUISITES'], + }, + }, + data: { + status: 'CANCELLED', + error: 'Superseded by global uninstall', + lockedUntil: null, + startedAt: null, + finishedAt: expect.any(Date), + }, + }) + expect(createUninstallSkillTask).toHaveBeenCalledTimes(2) + expect(createUninstallSkillTask).toHaveBeenNthCalledWith(1, tx, { + projectId: 'project-running', + sandboxId: 'sandbox-running', + userSkillId: 'user-skill-1', + skillId: 'frontend-design', + installCommand: 'install frontend-design', + uninstallCommand: 'remove frontend-design', + }) + expect(createUninstallSkillTask).toHaveBeenNthCalledWith(2, tx, { + projectId: 'project-stopped', + sandboxId: 'sandbox-stopped', + userSkillId: 'user-skill-1', + skillId: 'frontend-design', + installCommand: 'install frontend-design', + uninstallCommand: 'remove frontend-design', + }) + expect(tx.userSkill.delete).toHaveBeenCalledWith({ + where: { + id: 'user-skill-1', + }, + }) + expect(triggerRunnableTasksForProject).toHaveBeenCalledTimes(1) + expect(triggerRunnableTasksForProject).toHaveBeenCalledWith('project-running') + }) + + it('returns a uniform failure when the transaction throws', async () => { + prisma.$transaction.mockRejectedValue(new Error('db unavailable')) + + const result = await uninstallGlobalSkillCommand({ + userId: 'user-1', + skillId: 'frontend-design', + }) + + expect(result).toEqual({ + success: false, + error: 'Failed to uninstall skill', + }) + }) +}) diff --git a/lib/platform/integrations/github/find-installation-repository.test.ts b/lib/platform/integrations/github/find-installation-repository.test.ts new file mode 100644 index 0000000..90c41ed --- /dev/null +++ b/lib/platform/integrations/github/find-installation-repository.test.ts @@ -0,0 +1,71 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { listInstallationRepos } = vi.hoisted(() => ({ + listInstallationRepos: vi.fn(), +})) + +vi.mock('@/lib/services/github-app', () => ({ + listInstallationRepos, +})) + +import { findInstallationRepository } from '@/lib/platform/integrations/github/find-installation-repository' + +describe('findInstallationRepository', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('returns the matching repository when id and full name both match', async () => { + listInstallationRepos.mockResolvedValue([ + { id: 201, full_name: 'acme/other' }, + { id: 202, full_name: 'acme/project-alpha' }, + ]) + + const result = await findInstallationRepository({ + installationId: 101, + repoId: 202, + repoFullName: 'acme/project-alpha', + }) + + expect(result).toEqual({ + id: 202, + full_name: 'acme/project-alpha', + }) + }) + + it('returns null when the repo id does not match', async () => { + listInstallationRepos.mockResolvedValue([{ id: 203, full_name: 'acme/project-alpha' }]) + + const result = await findInstallationRepository({ + installationId: 101, + repoId: 202, + repoFullName: 'acme/project-alpha', + }) + + expect(result).toBeNull() + }) + + it('returns null when the repo full name does not match', async () => { + listInstallationRepos.mockResolvedValue([{ id: 202, full_name: 'acme/other' }]) + + const result = await findInstallationRepository({ + installationId: 101, + repoId: 202, + repoFullName: 'acme/project-alpha', + }) + + expect(result).toBeNull() + }) + + it('returns null when the installation has no repositories', async () => { + listInstallationRepos.mockResolvedValue([]) + + const result = await findInstallationRepository({ + installationId: 101, + repoId: 202, + repoFullName: 'acme/project-alpha', + }) + + expect(result).toBeNull() + }) +}) diff --git a/lib/platform/integrations/k8s/get-user-default-namespace.test.ts b/lib/platform/integrations/k8s/get-user-default-namespace.test.ts new file mode 100644 index 0000000..68b1693 --- /dev/null +++ b/lib/platform/integrations/k8s/get-user-default-namespace.test.ts @@ -0,0 +1,35 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { getK8sServiceForUser } = vi.hoisted(() => ({ + getK8sServiceForUser: vi.fn(), +})) + +vi.mock('@/lib/k8s/k8s-service-helper', () => ({ + getK8sServiceForUser, +})) + +import { getUserDefaultNamespace } from '@/lib/platform/integrations/k8s/get-user-default-namespace' + +describe('getUserDefaultNamespace', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('returns the default namespace from the user k8s service', async () => { + getK8sServiceForUser.mockResolvedValue({ + getDefaultNamespace: vi.fn().mockReturnValue('ns-user-1'), + }) + + const result = await getUserDefaultNamespace('user-1') + + expect(result).toBe('ns-user-1') + expect(getK8sServiceForUser).toHaveBeenCalledWith('user-1') + }) + + it('rethrows errors from the underlying k8s service lookup', async () => { + const error = new Error('missing kubeconfig') + getK8sServiceForUser.mockRejectedValue(error) + + await expect(getUserDefaultNamespace('user-1')).rejects.toThrow(error) + }) +}) diff --git a/lib/platform/persistence/github/find-github-installation-by-id.test.ts b/lib/platform/persistence/github/find-github-installation-by-id.test.ts new file mode 100644 index 0000000..f886ecc --- /dev/null +++ b/lib/platform/persistence/github/find-github-installation-by-id.test.ts @@ -0,0 +1,34 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { prisma } = vi.hoisted(() => ({ + prisma: { + gitHubAppInstallation: { + findUnique: vi.fn(), + }, + }, +})) + +vi.mock('@/lib/db', () => ({ + prisma, +})) + +import { findGitHubInstallationById } from '@/lib/platform/persistence/github/find-github-installation-by-id' + +describe('findGitHubInstallationById', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('queries by installation id and always includes the owning user', async () => { + prisma.gitHubAppInstallation.findUnique.mockResolvedValue({ + id: 'gha-1', + }) + + await findGitHubInstallationById(101) + + expect(prisma.gitHubAppInstallation.findUnique).toHaveBeenCalledWith({ + where: { installationId: 101 }, + include: { user: true }, + }) + }) +}) diff --git a/lib/platform/persistence/project-task/create-clone-repository-task.test.ts b/lib/platform/persistence/project-task/create-clone-repository-task.test.ts new file mode 100644 index 0000000..f385c91 --- /dev/null +++ b/lib/platform/persistence/project-task/create-clone-repository-task.test.ts @@ -0,0 +1,51 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { prisma } = vi.hoisted(() => ({ + prisma: { + projectTask: { + create: vi.fn(), + }, + }, +})) + +vi.mock('@/lib/db', () => ({ + prisma, +})) + +import { createCloneRepositoryTask } from '@/lib/platform/persistence/project-task/create-clone-repository-task' + +describe('createCloneRepositoryTask', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('creates a waiting clone task with the expected payload', async () => { + prisma.projectTask.create.mockResolvedValue({ id: 'task-1' }) + + await createCloneRepositoryTask({ + projectId: 'project-1', + sandboxId: 'sandbox-1', + installationId: 101, + repoId: 202, + repoFullName: 'acme/project-alpha', + defaultBranch: 'main', + }) + + expect(prisma.projectTask.create).toHaveBeenCalledWith({ + data: { + projectId: 'project-1', + sandboxId: 'sandbox-1', + type: 'CLONE_REPOSITORY', + status: 'WAITING_FOR_PREREQUISITES', + triggerSource: 'USER_ACTION', + payload: { + installationId: 101, + repoId: 202, + repoFullName: 'acme/project-alpha', + defaultBranch: 'main', + }, + maxAttempts: 3, + }, + }) + }) +}) diff --git a/lib/platform/persistence/project-task/create-install-skill-task.test.ts b/lib/platform/persistence/project-task/create-install-skill-task.test.ts new file mode 100644 index 0000000..af5e149 --- /dev/null +++ b/lib/platform/persistence/project-task/create-install-skill-task.test.ts @@ -0,0 +1,45 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { createProjectTask } = vi.hoisted(() => ({ + createProjectTask: vi.fn(), +})) + +vi.mock('@/lib/repo/project-task', () => ({ + createProjectTask, +})) + +import { createInstallSkillTask } from '@/lib/platform/persistence/project-task/create-install-skill-task' + +describe('createInstallSkillTask', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('creates an install skill task with the expected routing fields', async () => { + const tx = { projectTask: {} } as never + + await createInstallSkillTask(tx, { + projectId: 'project-1', + sandboxId: 'sandbox-1', + userSkillId: 'user-skill-1', + skillId: 'frontend-design', + installCommand: 'install frontend-design', + }) + + expect(createProjectTask).toHaveBeenCalledWith(tx, { + projectId: 'project-1', + sandboxId: 'sandbox-1', + userSkillId: 'user-skill-1', + skillId: 'frontend-design', + type: 'INSTALL_SKILL', + status: 'WAITING_FOR_PREREQUISITES', + triggerSource: 'POLICY_ROLLOUT', + payload: { + userSkillId: 'user-skill-1', + skillId: 'frontend-design', + installCommand: 'install frontend-design', + }, + maxAttempts: 3, + }) + }) +}) diff --git a/lib/platform/persistence/project-task/create-uninstall-skill-task.test.ts b/lib/platform/persistence/project-task/create-uninstall-skill-task.test.ts new file mode 100644 index 0000000..97cf0df --- /dev/null +++ b/lib/platform/persistence/project-task/create-uninstall-skill-task.test.ts @@ -0,0 +1,46 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { createProjectTask } = vi.hoisted(() => ({ + createProjectTask: vi.fn(), +})) + +vi.mock('@/lib/repo/project-task', () => ({ + createProjectTask, +})) + +import { createUninstallSkillTask } from '@/lib/platform/persistence/project-task/create-uninstall-skill-task' + +describe('createUninstallSkillTask', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('creates an uninstall skill task with install and uninstall commands in payload', async () => { + const tx = { projectTask: {} } as never + + await createUninstallSkillTask(tx, { + projectId: 'project-1', + sandboxId: 'sandbox-1', + userSkillId: 'user-skill-1', + skillId: 'frontend-design', + installCommand: 'install frontend-design', + uninstallCommand: 'remove frontend-design', + }) + + expect(createProjectTask).toHaveBeenCalledWith(tx, { + projectId: 'project-1', + sandboxId: 'sandbox-1', + skillId: 'frontend-design', + type: 'UNINSTALL_SKILL', + status: 'WAITING_FOR_PREREQUISITES', + triggerSource: 'POLICY_ROLLOUT', + payload: { + userSkillId: 'user-skill-1', + skillId: 'frontend-design', + installCommand: 'install frontend-design', + uninstallCommand: 'remove frontend-design', + }, + maxAttempts: 3, + }) + }) +}) diff --git a/lib/platform/persistence/project/create-project-with-sandbox.test.ts b/lib/platform/persistence/project/create-project-with-sandbox.test.ts new file mode 100644 index 0000000..bcfbf68 --- /dev/null +++ b/lib/platform/persistence/project/create-project-with-sandbox.test.ts @@ -0,0 +1,232 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + prisma, + toK8sProjectName, + generateK8sRandomString, + createInstallSkillTask, + generateRandomString, +} = vi.hoisted(() => ({ + prisma: { + $transaction: vi.fn(), + }, + toK8sProjectName: vi.fn(), + generateK8sRandomString: vi.fn(), + createInstallSkillTask: vi.fn(), + generateRandomString: vi.fn(), +})) + +vi.mock('@/lib/db', () => ({ + prisma, +})) + +vi.mock('@/lib/k8s/kubernetes-utils', () => ({ + KubernetesUtils: { + toK8sProjectName, + generateRandomString: generateK8sRandomString, + }, +})) + +vi.mock('@/lib/k8s/versions', () => ({ + VERSIONS: { + RUNTIME_IMAGE: 'runtime:test', + RESOURCES: { + SANDBOX: { + requests: { + cpu: '20m', + memory: '25Mi', + }, + limits: { + cpu: '2000m', + memory: '4096Mi', + }, + }, + }, + }, +})) + +vi.mock('@/lib/platform/persistence/project-task/create-install-skill-task', () => ({ + createInstallSkillTask, +})) + +vi.mock('@/lib/util/common', () => ({ + generateRandomString, +})) + +import { createProjectWithSandbox } from '@/lib/platform/persistence/project/create-project-with-sandbox' + +function createProjectTx() { + return { + project: { + create: vi.fn(), + }, + sandbox: { + create: vi.fn(), + }, + environment: { + create: vi.fn(), + }, + } +} + +type CreateProjectTx = ReturnType + +describe('createProjectWithSandbox', () => { + beforeEach(() => { + vi.clearAllMocks() + toK8sProjectName.mockReturnValue('projectalpha') + generateK8sRandomString.mockReturnValue('suffix') + generateRandomString + .mockReturnValueOnce('ttyd-token') + .mockReturnValueOnce('file-browser-password') + }) + + it('creates project, sandbox, and three environments without initial skill tasks', async () => { + const tx = createProjectTx() + tx.project.create.mockResolvedValue({ id: 'project-1', name: 'Project Alpha' }) + tx.sandbox.create.mockResolvedValue({ id: 'sandbox-1' }) + tx.environment.create.mockResolvedValue({}) + prisma.$transaction.mockImplementation( + async ( + callback: (transaction: CreateProjectTx) => Promise, + options?: { timeout: number } + ) => { + expect(options).toEqual({ timeout: 20000 }) + return callback(tx) + } + ) + + const result = await createProjectWithSandbox({ + userId: 'user-1', + namespace: 'ns-user-1', + name: 'Project Alpha', + description: 'Demo project', + }) + + expect(result).toEqual({ + success: true, + data: { + project: { id: 'project-1', name: 'Project Alpha' }, + sandbox: { id: 'sandbox-1' }, + }, + }) + expect(tx.project.create).toHaveBeenCalledWith({ + data: { + name: 'Project Alpha', + description: 'Demo project', + userId: 'user-1', + status: 'CREATING', + githubAppInstallationId: undefined, + githubRepoId: undefined, + githubRepoFullName: undefined, + githubRepoDefaultBranch: undefined, + }, + }) + expect(tx.sandbox.create).toHaveBeenCalledWith({ + data: { + projectId: 'project-1', + name: 'projectalpha-suffix', + k8sNamespace: 'ns-user-1', + sandboxName: 'projectalpha-suffix', + status: 'CREATING', + lockedUntil: null, + runtimeImage: 'runtime:test', + cpuRequest: '20m', + cpuLimit: '2000m', + memoryRequest: '25Mi', + memoryLimit: '4096Mi', + }, + }) + expect(tx.environment.create).toHaveBeenCalledTimes(3) + expect(tx.environment.create).toHaveBeenNthCalledWith(1, { + data: { + projectId: 'project-1', + key: 'TTYD_ACCESS_TOKEN', + value: 'ttyd-token', + category: 'ttyd', + isSecret: true, + }, + }) + expect(tx.environment.create).toHaveBeenNthCalledWith(2, { + data: { + projectId: 'project-1', + key: 'FILE_BROWSER_USERNAME', + value: 'fb-suffix', + category: 'file_browser', + isSecret: false, + }, + }) + expect(tx.environment.create).toHaveBeenNthCalledWith(3, { + data: { + projectId: 'project-1', + key: 'FILE_BROWSER_PASSWORD', + value: 'file-browser-password', + category: 'file_browser', + isSecret: true, + }, + }) + expect(createInstallSkillTask).not.toHaveBeenCalled() + }) + + it('persists github source fields and creates install tasks for initial skills', async () => { + const tx = createProjectTx() + tx.project.create.mockResolvedValue({ id: 'project-1', name: 'Project Alpha' }) + tx.sandbox.create.mockResolvedValue({ id: 'sandbox-1' }) + tx.environment.create.mockResolvedValue({}) + prisma.$transaction.mockImplementation(async (callback: (transaction: CreateProjectTx) => Promise) => + callback(tx) + ) + + await createProjectWithSandbox({ + userId: 'user-1', + namespace: 'ns-user-1', + name: 'Project Alpha', + githubSource: { + githubAppInstallationId: 'gha-1', + githubRepoId: 202, + githubRepoFullName: 'acme/project-alpha', + githubRepoDefaultBranch: 'main', + }, + initialInstallSkills: [ + { + userSkillId: 'user-skill-1', + skillId: 'frontend-design', + installCommand: 'install frontend-design', + }, + { + userSkillId: 'user-skill-2', + skillId: 'backend-tooling', + installCommand: 'install backend-tooling', + }, + ], + }) + + expect(tx.project.create).toHaveBeenCalledWith({ + data: { + name: 'Project Alpha', + description: undefined, + userId: 'user-1', + status: 'CREATING', + githubAppInstallationId: 'gha-1', + githubRepoId: 202, + githubRepoFullName: 'acme/project-alpha', + githubRepoDefaultBranch: 'main', + }, + }) + expect(createInstallSkillTask).toHaveBeenCalledTimes(2) + expect(createInstallSkillTask).toHaveBeenNthCalledWith(1, tx, { + projectId: 'project-1', + sandboxId: 'sandbox-1', + userSkillId: 'user-skill-1', + skillId: 'frontend-design', + installCommand: 'install frontend-design', + }) + expect(createInstallSkillTask).toHaveBeenNthCalledWith(2, tx, { + projectId: 'project-1', + sandboxId: 'sandbox-1', + userSkillId: 'user-skill-2', + skillId: 'backend-tooling', + installCommand: 'install backend-tooling', + }) + }) +}) diff --git a/package.json b/package.json index 91706a3..bbc0f5b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "dev": "next dev -H 0.0.0.0 -p 3000", "build": "prisma generate && next build", "start": "next start -H 0.0.0.0 -p 3000", + "test": "vitest run", + "test:watch": "vitest", "lint": "eslint .", "lint:fix": "eslint . --fix", "prisma:format": "prisma format" @@ -82,7 +84,9 @@ "prettier": "^3.7.4", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", - "typescript": "^5" + "typescript": "^5", + "vite-tsconfig-paths": "^6.1.1", + "vitest": "^4.1.2" }, "pnpm": { "overrides": { @@ -94,7 +98,7 @@ }, "packageManager": "pnpm@10.20.0", "engines": { - "node": ">=22.9.0", + "node": ">=22.12.0", "pnpm": "10.20.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8996f09..c2564c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -201,6 +201,12 @@ importers: typescript: specifier: ^5 version: 5.9.3 + vite-tsconfig-paths: + specifier: ^6.1.1 + version: 6.1.1(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@20.19.27)(jiti@2.6.1)) + vitest: + specifier: ^4.1.2 + version: 4.1.2(@types/node@20.19.27)(vite@8.0.3(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@20.19.27)(jiti@2.6.1)) packages: @@ -538,6 +544,12 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.1.2': + resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@next/env@16.0.10': resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==} @@ -707,6 +719,9 @@ packages: resolution: {integrity: sha512-da6KbdNCV5sr1/txD896V+6W0iamFWrvVl8cHkBSPT+YlvmT3DwXa4jxZnQc+gnuTEqSWbBeoSZYTayXH9wXcw==} engines: {node: '>= 20'} + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + '@panva/hkdf@1.2.1': resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} @@ -1503,6 +1518,98 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1657,6 +1764,12 @@ packages: '@types/aws-lambda@8.10.161': resolution: {integrity: sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1852,6 +1965,35 @@ packages: cpu: [x64] os: [win32] + '@vitest/expect@4.1.2': + resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} + + '@vitest/mocker@4.1.2': + resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.2': + resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} + + '@vitest/runner@4.1.2': + resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} + + '@vitest/snapshot@4.1.2': + resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} + + '@vitest/spy@4.1.2': + resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} + + '@vitest/utils@4.1.2': + resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} + '@xterm/addon-canvas@0.7.0': resolution: {integrity: sha512-LF5LYcfvefJuJ7QotNRdRSPc9YASAVDeoT5uyXS/nZshZXjYplGXRECBGiznwvhNL2I8bq1Lf5MzRwstsYQ2Iw==} peerDependencies: @@ -1944,6 +2086,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -2082,6 +2228,10 @@ packages: caniuse-lite@1.0.30001760: resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2262,6 +2412,9 @@ packages: resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} engines: {node: '>= 0.4'} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -2409,6 +2562,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2416,6 +2572,10 @@ packages: events-universal@1.0.1: resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -2490,6 +2650,11 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -2551,6 +2716,9 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -2831,70 +2999,140 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.30.2: resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.30.2: resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.30.2: resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.30.2: resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.30.2: resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.30.2: resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss@1.30.2: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -3111,6 +3349,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} @@ -3172,6 +3413,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} @@ -3197,6 +3442,10 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + preact-render-to-string@6.5.11: resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} peerDependencies: @@ -3364,6 +3613,11 @@ packages: rfc4648@1.5.4: resolution: {integrity: sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg==} + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3438,6 +3692,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -3473,6 +3730,12 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -3558,6 +3821,9 @@ packages: thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -3566,6 +3832,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3584,6 +3854,16 @@ packages: peerDependencies: typescript: '>=4.8.4' + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -3689,6 +3969,89 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + vite-tsconfig-paths@6.1.1: + resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==} + peerDependencies: + vite: '*' + + vite@8.0.3: + resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.2: + resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.2 + '@vitest/browser-preview': 4.1.2 + '@vitest/browser-webdriverio': 4.1.2 + '@vitest/ui': 4.1.2 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -3718,6 +4081,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -4113,6 +4481,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)': + dependencies: + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 + '@tybys/wasm-util': 0.10.1 + optional: true + '@next/env@16.0.10': {} '@next/eslint-plugin-next@16.0.7': @@ -4299,6 +4674,8 @@ snapshots: '@octokit/request-error': 7.1.0 '@octokit/webhooks-methods': 6.0.0 + '@oxc-project/types@0.122.0': {} + '@panva/hkdf@1.2.1': {} '@pinojs/redact@0.4.0': {} @@ -5138,6 +5515,58 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.12': {} + '@rtsao/scc@1.1.0': {} '@standard-schema/spec@1.1.0': {} @@ -5249,6 +5678,13 @@ snapshots: '@types/aws-lambda@8.10.161': {} + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.8': {} '@types/js-yaml@4.0.9': {} @@ -5443,6 +5879,47 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@vitest/expect@4.1.2': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.2(vite@8.0.3(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@20.19.27)(jiti@2.6.1))': + dependencies: + '@vitest/spy': 4.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.3(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@20.19.27)(jiti@2.6.1) + + '@vitest/pretty-format@4.1.2': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.2': + dependencies: + '@vitest/utils': 4.1.2 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + '@vitest/utils': 4.1.2 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.2': {} + + '@vitest/utils@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@xterm/addon-canvas@0.7.0(@xterm/xterm@5.5.0)': dependencies: '@xterm/xterm': 5.5.0 @@ -5566,6 +6043,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + ast-types-flow@0.0.8: {} async-function@1.0.0: {} @@ -5698,6 +6177,8 @@ snapshots: caniuse-lite@1.0.30001760: {} + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -5926,6 +6407,8 @@ snapshots: iterator.prototype: 1.1.5 safe-array-concat: 1.1.3 + es-module-lexer@2.0.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -6160,6 +6643,10 @@ snapshots: estraverse@5.3.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} events-universal@1.0.1: @@ -6168,6 +6655,8 @@ snapshots: transitivePeerDependencies: - bare-abort-controller + expect-type@1.3.0: {} + exsolve@1.0.8: {} fast-check@3.23.2: @@ -6234,6 +6723,9 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -6307,6 +6799,8 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 + globrex@0.1.2: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -6578,36 +7072,69 @@ snapshots: lightningcss-android-arm64@1.30.2: optional: true + lightningcss-android-arm64@1.32.0: + optional: true + lightningcss-darwin-arm64@1.30.2: optional: true + lightningcss-darwin-arm64@1.32.0: + optional: true + lightningcss-darwin-x64@1.30.2: optional: true + lightningcss-darwin-x64@1.32.0: + optional: true + lightningcss-freebsd-x64@1.30.2: optional: true + lightningcss-freebsd-x64@1.32.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.30.2: optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + lightningcss-linux-arm64-gnu@1.30.2: optional: true + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + lightningcss-linux-arm64-musl@1.30.2: optional: true + lightningcss-linux-arm64-musl@1.32.0: + optional: true + lightningcss-linux-x64-gnu@1.30.2: optional: true + lightningcss-linux-x64-gnu@1.32.0: + optional: true + lightningcss-linux-x64-musl@1.30.2: optional: true + lightningcss-linux-x64-musl@1.32.0: + optional: true + lightningcss-win32-arm64-msvc@1.30.2: optional: true + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + lightningcss-win32-x64-msvc@1.30.2: optional: true + lightningcss-win32-x64-msvc@1.32.0: + optional: true + lightningcss@1.30.2: dependencies: detect-libc: 2.1.2 @@ -6624,6 +7151,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -6815,6 +7358,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obug@2.1.1: {} + ohash@2.0.11: {} on-exit-leak-free@2.1.2: {} @@ -6871,6 +7416,8 @@ snapshots: picomatch@4.0.3: {} + picomatch@4.0.4: {} + pino-abstract-transport@2.0.0: dependencies: split2: 4.2.0 @@ -6911,6 +7458,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + preact-render-to-string@6.5.11(preact@10.24.3): dependencies: preact: 10.24.3 @@ -7117,6 +7670,30 @@ snapshots: rfc4648@1.5.4: {} + rolldown@1.0.0-rc.12(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1): + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1) + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -7238,6 +7815,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} smart-buffer@4.2.0: {} @@ -7270,6 +7849,10 @@ snapshots: stable-hash@0.0.5: {} + stackback@0.0.2: {} + + std-env@4.0.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -7390,6 +7973,8 @@ snapshots: dependencies: real-require: 0.2.0 + tinybench@2.9.0: {} + tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -7397,6 +7982,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyrainbow@3.1.0: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -7411,6 +7998,10 @@ snapshots: dependencies: typescript: 5.9.3 + tsconfck@3.1.6(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -7557,6 +8148,58 @@ snapshots: uuid@9.0.1: {} + vite-tsconfig-paths@6.1.1(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@20.19.27)(jiti@2.6.1)): + dependencies: + debug: 4.4.3 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.9.3) + vite: 8.0.3(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@20.19.27)(jiti@2.6.1) + transitivePeerDependencies: + - supports-color + - typescript + + vite@8.0.3(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@20.19.27)(jiti@2.6.1): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.8 + rolldown: 1.0.0-rc.12(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1) + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.27 + fsevents: 2.3.3 + jiti: 2.6.1 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + vitest@4.1.2(@types/node@20.19.27)(vite@8.0.3(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@20.19.27)(jiti@2.6.1)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(vite@8.0.3(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@20.19.27)(jiti@2.6.1)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 8.0.3(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@20.19.27)(jiti@2.6.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.27 + transitivePeerDependencies: + - msw + webidl-conversions@7.0.0: {} whatwg-url@13.0.0: @@ -7609,6 +8252,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrappy@1.0.2: {} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..08300a7 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { + tsconfigPaths: true, + }, + test: { + environment: 'node', + include: ['**/*.test.ts'], + env: { + SKIP_ENV_VALIDATION: '1', + DATABASE_URL: 'https://example.com/fulling-test', + }, + }, +})