mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-21 13:08:05 -05:00
Compare commits
11 Commits
fix/pgvect
...
abhi/integ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd76330183 | ||
|
|
40ef2d511f | ||
|
|
1b4ced3a13 | ||
|
|
25dbab8cf7 | ||
|
|
d9ca19672d | ||
|
|
6e2c7ffbeb | ||
|
|
da978a7410 | ||
|
|
6960ba8f3e | ||
|
|
b22a4098bb | ||
|
|
1a5010f9e5 | ||
|
|
12f47eda10 |
@@ -154,15 +154,16 @@ async def store_content_embedding(
|
||||
|
||||
# Upsert the embedding
|
||||
# WHERE clause in DO UPDATE prevents PostgreSQL 15 bug with NULLS NOT DISTINCT
|
||||
# Use {pgvector_schema}.vector for explicit pgvector type qualification
|
||||
await execute_raw_with_schema(
|
||||
"""
|
||||
INSERT INTO {schema_prefix}"UnifiedContentEmbedding" (
|
||||
"id", "contentType", "contentId", "userId", "embedding", "searchableText", "metadata", "createdAt", "updatedAt"
|
||||
)
|
||||
VALUES (gen_random_uuid()::text, $1::{schema_prefix}"ContentType", $2, $3, $4::{schema}.vector, $5, $6::jsonb, NOW(), NOW())
|
||||
VALUES (gen_random_uuid()::text, $1::{schema_prefix}"ContentType", $2, $3, $4::{pgvector_schema}.vector, $5, $6::jsonb, NOW(), NOW())
|
||||
ON CONFLICT ("contentType", "contentId", "userId")
|
||||
DO UPDATE SET
|
||||
"embedding" = $4::{schema}.vector,
|
||||
"embedding" = $4::{pgvector_schema}.vector,
|
||||
"searchableText" = $5,
|
||||
"metadata" = $6::jsonb,
|
||||
"updatedAt" = NOW()
|
||||
@@ -878,6 +879,8 @@ async def semantic_search(
|
||||
min_similarity_idx = len(params) + 1
|
||||
params.append(min_similarity)
|
||||
|
||||
# Use regular string (not f-string) for template to preserve {schema_prefix} and {schema} placeholders
|
||||
# Use OPERATOR({pgvector_schema}.<=>) for explicit operator schema qualification
|
||||
sql = (
|
||||
"""
|
||||
SELECT
|
||||
@@ -885,9 +888,9 @@ async def semantic_search(
|
||||
"contentType" as content_type,
|
||||
"searchableText" as searchable_text,
|
||||
metadata,
|
||||
1 - (embedding <=> '"""
|
||||
1 - (embedding OPERATOR({pgvector_schema}.<=>) '"""
|
||||
+ embedding_str
|
||||
+ """'::{schema}.vector) as similarity
|
||||
+ """'::{pgvector_schema}.vector) as similarity
|
||||
FROM {schema_prefix}"UnifiedContentEmbedding"
|
||||
WHERE "contentType" IN ("""
|
||||
+ content_type_placeholders
|
||||
@@ -895,9 +898,9 @@ async def semantic_search(
|
||||
"""
|
||||
+ user_filter
|
||||
+ """
|
||||
AND 1 - (embedding <=> '"""
|
||||
AND 1 - (embedding OPERATOR({pgvector_schema}.<=>) '"""
|
||||
+ embedding_str
|
||||
+ """'::{schema}.vector) >= $"""
|
||||
+ """'::{pgvector_schema}.vector) >= $"""
|
||||
+ str(min_similarity_idx)
|
||||
+ """
|
||||
ORDER BY similarity DESC
|
||||
|
||||
@@ -295,7 +295,7 @@ async def unified_hybrid_search(
|
||||
FROM {{schema_prefix}}"UnifiedContentEmbedding" uce
|
||||
WHERE uce."contentType" = ANY({content_types_param}::{{schema_prefix}}"ContentType"[])
|
||||
{user_filter}
|
||||
ORDER BY uce.embedding <=> {embedding_param}::{{schema}}.vector
|
||||
ORDER BY uce.embedding OPERATOR({{pgvector_schema}}.<=>) {embedding_param}::{{pgvector_schema}}.vector
|
||||
LIMIT 200
|
||||
)
|
||||
),
|
||||
@@ -307,7 +307,7 @@ async def unified_hybrid_search(
|
||||
uce.metadata,
|
||||
uce."updatedAt" as updated_at,
|
||||
-- Semantic score: cosine similarity (1 - distance)
|
||||
COALESCE(1 - (uce.embedding <=> {embedding_param}::{{schema}}.vector), 0) as semantic_score,
|
||||
COALESCE(1 - (uce.embedding OPERATOR({{pgvector_schema}}.<=>) {embedding_param}::{{pgvector_schema}}.vector), 0) as semantic_score,
|
||||
-- Lexical score: ts_rank_cd
|
||||
COALESCE(ts_rank_cd(uce.search, plainto_tsquery('english', {query_param})), 0) as lexical_raw,
|
||||
-- Category match from metadata
|
||||
@@ -583,7 +583,7 @@ async def hybrid_search(
|
||||
WHERE uce."contentType" = 'STORE_AGENT'::{{schema_prefix}}"ContentType"
|
||||
AND uce."userId" IS NULL
|
||||
AND {where_clause}
|
||||
ORDER BY uce.embedding <=> {embedding_param}::{{schema}}.vector
|
||||
ORDER BY uce.embedding OPERATOR({{pgvector_schema}}.<=>) {embedding_param}::{{pgvector_schema}}.vector
|
||||
LIMIT 200
|
||||
) uce
|
||||
),
|
||||
@@ -605,7 +605,7 @@ async def hybrid_search(
|
||||
-- Searchable text for BM25 reranking
|
||||
COALESCE(sa.agent_name, '') || ' ' || COALESCE(sa.sub_heading, '') || ' ' || COALESCE(sa.description, '') as searchable_text,
|
||||
-- Semantic score
|
||||
COALESCE(1 - (uce.embedding <=> {embedding_param}::{{schema}}.vector), 0) as semantic_score,
|
||||
COALESCE(1 - (uce.embedding OPERATOR({{pgvector_schema}}.<=>) {embedding_param}::{{pgvector_schema}}.vector), 0) as semantic_score,
|
||||
-- Lexical score (raw, will normalize)
|
||||
COALESCE(ts_rank_cd(uce.search, plainto_tsquery('english', {query_param})), 0) as lexical_raw,
|
||||
-- Category match
|
||||
|
||||
@@ -120,10 +120,11 @@ async def _raw_with_schema(
|
||||
|
||||
Supports placeholders:
|
||||
- {schema_prefix}: Table/type prefix (e.g., "platform".)
|
||||
- {schema}: Raw schema name (e.g., platform) for pgvector types
|
||||
- {schema}: Raw schema name for application tables (e.g., platform)
|
||||
- {pgvector_schema}: Schema where pgvector is installed (defaults to "public")
|
||||
|
||||
Args:
|
||||
query_template: SQL query with {schema_prefix} and/or {schema} placeholders
|
||||
query_template: SQL query with {schema_prefix}, {schema}, and/or {pgvector_schema} placeholders
|
||||
*args: Query parameters
|
||||
execute: If False, executes SELECT query. If True, executes INSERT/UPDATE/DELETE.
|
||||
client: Optional Prisma client for transactions (only used when execute=True).
|
||||
@@ -132,16 +133,23 @@ async def _raw_with_schema(
|
||||
- list[dict] if execute=False (query results)
|
||||
- int if execute=True (number of affected rows)
|
||||
|
||||
Example:
|
||||
Example with vector type:
|
||||
await execute_raw_with_schema(
|
||||
'INSERT INTO {schema_prefix}"Embedding" (vec) VALUES ($1::{schema}.vector)',
|
||||
'INSERT INTO {schema_prefix}"Embedding" (vec) VALUES ($1::{pgvector_schema}.vector)',
|
||||
embedding_data
|
||||
)
|
||||
"""
|
||||
schema = get_database_schema()
|
||||
schema_prefix = f'"{schema}".' if schema != "public" else ""
|
||||
# pgvector extension is typically installed in "public" schema
|
||||
# On Supabase it may be in "extensions" but "public" is the common default
|
||||
pgvector_schema = "public"
|
||||
|
||||
formatted_query = query_template.format(schema_prefix=schema_prefix, schema=schema)
|
||||
formatted_query = query_template.format(
|
||||
schema_prefix=schema_prefix,
|
||||
schema=schema,
|
||||
pgvector_schema=pgvector_schema,
|
||||
)
|
||||
|
||||
import prisma as prisma_module
|
||||
|
||||
|
||||
@@ -29,4 +29,4 @@ NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY=
|
||||
NEXT_PUBLIC_TURNSTILE=disabled
|
||||
|
||||
# PR previews
|
||||
NEXT_PUBLIC_PREVIEW_STEALING_DEV=
|
||||
NEXT_PUBLIC_PREVIEW_STEALING_DEV=
|
||||
|
||||
@@ -16,6 +16,12 @@ export default defineConfig({
|
||||
client: "react-query",
|
||||
httpClient: "fetch",
|
||||
indexFiles: false,
|
||||
mock: {
|
||||
type: "msw",
|
||||
baseUrl: "http://localhost:3000/api/proxy",
|
||||
generateEachHttpStatus: true,
|
||||
delay: 0,
|
||||
},
|
||||
override: {
|
||||
mutator: {
|
||||
path: "./mutators/custom-mutator.ts",
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
"types": "tsc --noEmit",
|
||||
"test": "NEXT_PUBLIC_PW_TEST=true next build --turbo && playwright test",
|
||||
"test-ui": "NEXT_PUBLIC_PW_TEST=true next build --turbo && playwright test --ui",
|
||||
"test:unit": "vitest run",
|
||||
"test:unit:watch": "vitest",
|
||||
"test:no-build": "playwright test",
|
||||
"gentests": "playwright codegen http://localhost:3000",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
@@ -118,6 +120,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "4.1.2",
|
||||
"happy-dom": "20.3.4",
|
||||
"@opentelemetry/instrumentation": "0.209.0",
|
||||
"@playwright/test": "1.56.1",
|
||||
"@storybook/addon-a11y": "9.1.5",
|
||||
@@ -127,6 +130,8 @@
|
||||
"@storybook/nextjs": "9.1.5",
|
||||
"@tanstack/eslint-plugin-query": "5.91.2",
|
||||
"@tanstack/react-query-devtools": "5.90.2",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/react": "16.3.2",
|
||||
"@types/canvas-confetti": "1.9.0",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/negotiator": "0.6.4",
|
||||
@@ -135,6 +140,7 @@
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react-modal": "3.16.3",
|
||||
"@types/react-window": "1.8.8",
|
||||
"@vitejs/plugin-react": "5.1.2",
|
||||
"axe-playwright": "2.2.2",
|
||||
"chromatic": "13.3.3",
|
||||
"concurrently": "9.2.1",
|
||||
@@ -153,7 +159,9 @@
|
||||
"require-in-the-middle": "8.0.1",
|
||||
"storybook": "9.1.5",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.9.3"
|
||||
"typescript": "5.9.3",
|
||||
"vite-tsconfig-paths": "6.0.4",
|
||||
"vitest": "4.0.17"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": [
|
||||
|
||||
1118
autogpt_platform/frontend/pnpm-lock.yaml
generated
1118
autogpt_platform/frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,15 @@
|
||||
"use client";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import {
|
||||
CredentialsMetaInput,
|
||||
CredentialsType,
|
||||
GraphExecutionID,
|
||||
GraphMeta,
|
||||
LibraryAgentPreset,
|
||||
@@ -29,7 +36,11 @@ import {
|
||||
} from "@/components/__legacy__/ui/icons";
|
||||
import { Input } from "@/components/__legacy__/ui/input";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { CredentialsInput } from "@/components/contextual/CredentialsInput/CredentialsInput";
|
||||
import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView";
|
||||
import {
|
||||
findSavedCredentialByProviderAndType,
|
||||
findSavedUserCredentialByProviderAndType,
|
||||
} from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/helpers";
|
||||
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
|
||||
import {
|
||||
useToast,
|
||||
@@ -37,6 +48,7 @@ import {
|
||||
} from "@/components/molecules/Toast/use-toast";
|
||||
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
|
||||
import { cn, isEmpty } from "@/lib/utils";
|
||||
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
|
||||
import { ClockIcon, CopyIcon, InfoIcon } from "@phosphor-icons/react";
|
||||
import { CalendarClockIcon, Trash2Icon } from "lucide-react";
|
||||
|
||||
@@ -90,6 +102,7 @@ export function AgentRunDraftView({
|
||||
const api = useBackendAPI();
|
||||
const { toast } = useToast();
|
||||
const toastOnFail = useToastOnFail();
|
||||
const allProviders = useContext(CredentialsProvidersContext);
|
||||
|
||||
const [inputValues, setInputValues] = useState<Record<string, any>>({});
|
||||
const [inputCredentials, setInputCredentials] = useState<
|
||||
@@ -128,6 +141,77 @@ export function AgentRunDraftView({
|
||||
() => graph.credentials_input_schema.properties,
|
||||
[graph],
|
||||
);
|
||||
const credentialFields = useMemo(
|
||||
function getCredentialFields() {
|
||||
return Object.entries(agentCredentialsInputFields);
|
||||
},
|
||||
[agentCredentialsInputFields],
|
||||
);
|
||||
const requiredCredentials = useMemo(
|
||||
function getRequiredCredentials() {
|
||||
return new Set(
|
||||
(graph.credentials_input_schema?.required as string[]) || [],
|
||||
);
|
||||
},
|
||||
[graph.credentials_input_schema?.required],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function initializeDefaultCredentials() {
|
||||
if (!allProviders) return;
|
||||
if (!graph.credentials_input_schema?.properties) return;
|
||||
if (requiredCredentials.size === 0) return;
|
||||
|
||||
setInputCredentials(function updateCredentials(currentCreds) {
|
||||
const next = { ...currentCreds };
|
||||
let didAdd = false;
|
||||
|
||||
for (const key of requiredCredentials) {
|
||||
if (next[key]) continue;
|
||||
const schema = graph.credentials_input_schema.properties[key];
|
||||
if (!schema) continue;
|
||||
|
||||
const providerNames = schema.credentials_provider || [];
|
||||
const credentialTypes = schema.credentials_types || [];
|
||||
const requiredScopes = schema.credentials_scopes;
|
||||
|
||||
const userCredential = findSavedUserCredentialByProviderAndType(
|
||||
providerNames,
|
||||
credentialTypes,
|
||||
requiredScopes,
|
||||
allProviders,
|
||||
);
|
||||
|
||||
const savedCredential =
|
||||
userCredential ||
|
||||
findSavedCredentialByProviderAndType(
|
||||
providerNames,
|
||||
credentialTypes,
|
||||
requiredScopes,
|
||||
allProviders,
|
||||
);
|
||||
|
||||
if (!savedCredential) continue;
|
||||
|
||||
next[key] = {
|
||||
id: savedCredential.id,
|
||||
provider: savedCredential.provider,
|
||||
type: savedCredential.type as CredentialsType,
|
||||
title: savedCredential.title,
|
||||
};
|
||||
didAdd = true;
|
||||
}
|
||||
|
||||
if (!didAdd) return currentCreds;
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[
|
||||
allProviders,
|
||||
graph.credentials_input_schema?.properties,
|
||||
requiredCredentials,
|
||||
],
|
||||
);
|
||||
|
||||
const [allRequiredInputsAreSet, missingInputs] = useMemo(() => {
|
||||
const nonEmptyInputs = new Set(
|
||||
@@ -145,18 +229,35 @@ export function AgentRunDraftView({
|
||||
);
|
||||
return [isSuperset, difference];
|
||||
}, [agentInputSchema.required, inputValues]);
|
||||
const [allCredentialsAreSet, missingCredentials] = useMemo(() => {
|
||||
const availableCredentials = new Set(Object.keys(inputCredentials));
|
||||
const allCredentials = new Set(Object.keys(agentCredentialsInputFields));
|
||||
// Backwards-compatible implementation of isSupersetOf and difference
|
||||
const isSuperset = Array.from(allCredentials).every((item) =>
|
||||
availableCredentials.has(item),
|
||||
);
|
||||
const difference = Array.from(allCredentials).filter(
|
||||
(item) => !availableCredentials.has(item),
|
||||
);
|
||||
return [isSuperset, difference];
|
||||
}, [agentCredentialsInputFields, inputCredentials]);
|
||||
const [allCredentialsAreSet, missingCredentials] = useMemo(
|
||||
function getCredentialStatus() {
|
||||
const missing = Array.from(requiredCredentials).filter((key) => {
|
||||
const cred = inputCredentials[key];
|
||||
return !cred || !cred.id;
|
||||
});
|
||||
return [missing.length === 0, missing];
|
||||
},
|
||||
[requiredCredentials, inputCredentials],
|
||||
);
|
||||
function addChangedCredentials(prev: Set<keyof LibraryAgentPresetUpdatable>) {
|
||||
const next = new Set(prev);
|
||||
next.add("credentials");
|
||||
return next;
|
||||
}
|
||||
|
||||
function handleCredentialChange(key: string, value?: CredentialsMetaInput) {
|
||||
setInputCredentials(function updateInputCredentials(currentCreds) {
|
||||
const next = { ...currentCreds };
|
||||
if (value === undefined) {
|
||||
delete next[key];
|
||||
return next;
|
||||
}
|
||||
next[key] = value;
|
||||
return next;
|
||||
});
|
||||
setChangedPresetAttributes(addChangedCredentials);
|
||||
}
|
||||
|
||||
const notifyMissingInputs = useCallback(
|
||||
(needPresetName: boolean = true) => {
|
||||
const allMissingFields = (
|
||||
@@ -649,35 +750,6 @@ export function AgentRunDraftView({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Credentials inputs */}
|
||||
{Object.entries(agentCredentialsInputFields).map(
|
||||
([key, inputSubSchema]) => (
|
||||
<CredentialsInput
|
||||
key={key}
|
||||
schema={{ ...inputSubSchema, discriminator: undefined }}
|
||||
selectedCredentials={
|
||||
inputCredentials[key] ?? inputSubSchema.default
|
||||
}
|
||||
onSelectCredentials={(value) => {
|
||||
setInputCredentials((obj) => {
|
||||
const newObj = { ...obj };
|
||||
if (value === undefined) {
|
||||
delete newObj[key];
|
||||
return newObj;
|
||||
}
|
||||
return {
|
||||
...obj,
|
||||
[key]: value,
|
||||
};
|
||||
});
|
||||
setChangedPresetAttributes((prev) =>
|
||||
prev.add("credentials"),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
{/* Regular inputs */}
|
||||
{Object.entries(agentInputFields).map(([key, inputSubSchema]) => (
|
||||
<RunAgentInputs
|
||||
@@ -695,6 +767,17 @@ export function AgentRunDraftView({
|
||||
data-testid={`agent-input-${key}`}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Credentials inputs */}
|
||||
{credentialFields.length > 0 && (
|
||||
<CredentialsGroupedView
|
||||
credentialFields={credentialFields}
|
||||
requiredCredentials={requiredCredentials}
|
||||
inputCredentials={inputCredentials}
|
||||
inputValues={inputValues}
|
||||
onCredentialChange={handleCredentialChange}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
// import { render, screen } from "@testing-library/react";
|
||||
// import { describe, expect, it } from "vitest";
|
||||
// import { Badge } from "./Badge";
|
||||
|
||||
// describe("Badge Component", () => {
|
||||
// it("renders badge with content", () => {
|
||||
// render(<Badge variant="success">Success</Badge>);
|
||||
|
||||
// expect(screen.getByText("Success")).toBeInTheDocument();
|
||||
// });
|
||||
|
||||
// it("applies correct variant styles", () => {
|
||||
// const { rerender } = render(<Badge variant="success">Success</Badge>);
|
||||
// let badge = screen.getByText("Success");
|
||||
// expect(badge).toHaveClass("bg-green-100", "text-green-800");
|
||||
|
||||
// rerender(<Badge variant="error">Error</Badge>);
|
||||
// badge = screen.getByText("Error");
|
||||
// expect(badge).toHaveClass("bg-red-100", "text-red-800");
|
||||
|
||||
// rerender(<Badge variant="info">Info</Badge>);
|
||||
// badge = screen.getByText("Info");
|
||||
// expect(badge).toHaveClass("bg-slate-100", "text-slate-800");
|
||||
// });
|
||||
|
||||
// it("applies custom className", () => {
|
||||
// render(
|
||||
// <Badge variant="success" className="custom-class">
|
||||
// Success
|
||||
// </Badge>,
|
||||
// );
|
||||
|
||||
// const badge = screen.getByText("Success");
|
||||
// expect(badge).toHaveClass("custom-class");
|
||||
// });
|
||||
|
||||
// it("renders as span element", () => {
|
||||
// render(<Badge variant="success">Success</Badge>);
|
||||
|
||||
// const badge = screen.getByText("Success");
|
||||
// expect(badge.tagName).toBe("SPAN");
|
||||
// });
|
||||
|
||||
// it("renders children correctly", () => {
|
||||
// render(
|
||||
// <Badge variant="success">
|
||||
// <span>Custom</span> Content
|
||||
// </Badge>,
|
||||
// );
|
||||
|
||||
// expect(screen.getByText("Custom")).toBeInTheDocument();
|
||||
// expect(screen.getByText("Content")).toBeInTheDocument();
|
||||
// });
|
||||
|
||||
// it("supports all badge variants", () => {
|
||||
// const variants = ["success", "error", "info"] as const;
|
||||
|
||||
// variants.forEach((variant) => {
|
||||
// const { unmount } = render(
|
||||
// <Badge variant={variant} data-testid={`badge-${variant}`}>
|
||||
// {variant}
|
||||
// </Badge>,
|
||||
// );
|
||||
|
||||
// expect(screen.getByTestId(`badge-${variant}`)).toBeInTheDocument();
|
||||
// unmount();
|
||||
// });
|
||||
// });
|
||||
|
||||
// it("handles long text content", () => {
|
||||
// render(
|
||||
// <Badge variant="info">
|
||||
// Very long text that should be handled properly by the component
|
||||
// </Badge>,
|
||||
// );
|
||||
|
||||
// const badge = screen.getByText(/Very long text/);
|
||||
// expect(badge).toBeInTheDocument();
|
||||
// expect(badge).toHaveClass("overflow-hidden", "text-ellipsis");
|
||||
// });
|
||||
// });
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CredentialsProvidersContextType } from "@/providers/agent-credentials/credentials-provider";
|
||||
import { getSystemCredentials } from "../../helpers";
|
||||
import { filterSystemCredentials, getSystemCredentials } from "../../helpers";
|
||||
|
||||
export type CredentialField = [string, any];
|
||||
|
||||
@@ -208,3 +208,42 @@ export function findSavedCredentialByProviderAndType(
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function findSavedUserCredentialByProviderAndType(
|
||||
providerNames: string[],
|
||||
credentialTypes: string[],
|
||||
requiredScopes: string[] | undefined,
|
||||
allProviders: CredentialsProvidersContextType | null,
|
||||
): SavedCredential | undefined {
|
||||
for (const providerName of providerNames) {
|
||||
const providerData = allProviders?.[providerName];
|
||||
if (!providerData) continue;
|
||||
|
||||
const userCredentials = filterSystemCredentials(
|
||||
providerData.savedCredentials ?? [],
|
||||
);
|
||||
|
||||
const matchingCredentials: SavedCredential[] = [];
|
||||
|
||||
for (const credential of userCredentials) {
|
||||
const typeMatches =
|
||||
credentialTypes.length === 0 ||
|
||||
credentialTypes.includes(credential.type);
|
||||
const scopesMatch = hasRequiredScopes(credential, requiredScopes);
|
||||
|
||||
if (!typeMatches) continue;
|
||||
if (!scopesMatch) continue;
|
||||
|
||||
matchingCredentials.push(credential as SavedCredential);
|
||||
}
|
||||
|
||||
if (matchingCredentials.length === 1) {
|
||||
return matchingCredentials[0];
|
||||
}
|
||||
if (matchingCredentials.length > 1) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -98,24 +98,20 @@ export function useCredentialsInput({
|
||||
|
||||
// Auto-select the first available credential on initial mount
|
||||
// Once a user has made a selection, we don't override it
|
||||
useEffect(() => {
|
||||
if (readOnly) return;
|
||||
if (!credentials || !("savedCredentials" in credentials)) return;
|
||||
useEffect(
|
||||
function autoSelectCredential() {
|
||||
if (readOnly) return;
|
||||
if (!credentials || !("savedCredentials" in credentials)) return;
|
||||
if (selectedCredential?.id) return;
|
||||
|
||||
// If already selected, don't auto-select
|
||||
if (selectedCredential?.id) return;
|
||||
const savedCreds = credentials.savedCredentials;
|
||||
if (savedCreds.length === 0) return;
|
||||
|
||||
// Only attempt auto-selection once
|
||||
if (hasAttemptedAutoSelect.current) return;
|
||||
hasAttemptedAutoSelect.current = true;
|
||||
if (hasAttemptedAutoSelect.current) return;
|
||||
hasAttemptedAutoSelect.current = true;
|
||||
|
||||
// If optional, don't auto-select (user can choose "None")
|
||||
if (isOptional) return;
|
||||
if (isOptional) return;
|
||||
|
||||
const savedCreds = credentials.savedCredentials;
|
||||
|
||||
// Auto-select the first credential if any are available
|
||||
if (savedCreds.length > 0) {
|
||||
const cred = savedCreds[0];
|
||||
onSelectCredential({
|
||||
id: cred.id,
|
||||
@@ -123,14 +119,15 @@ export function useCredentialsInput({
|
||||
provider: credentials.provider,
|
||||
title: (cred as any).title,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
credentials,
|
||||
selectedCredential?.id,
|
||||
readOnly,
|
||||
isOptional,
|
||||
onSelectCredential,
|
||||
]);
|
||||
},
|
||||
[
|
||||
credentials,
|
||||
selectedCredential?.id,
|
||||
readOnly,
|
||||
isOptional,
|
||||
onSelectCredential,
|
||||
],
|
||||
);
|
||||
|
||||
if (
|
||||
!credentials ||
|
||||
|
||||
@@ -106,9 +106,14 @@ export function getTimezoneDisplayName(timezone: string): string {
|
||||
const parts = timezone.split("/");
|
||||
const city = parts[parts.length - 1].replace(/_/g, " ");
|
||||
const abbr = getTimezoneAbbreviation(timezone);
|
||||
return abbr ? `${city} (${abbr})` : city;
|
||||
if (abbr && abbr !== timezone) {
|
||||
return `${city} (${abbr})`;
|
||||
}
|
||||
// If abbreviation is same as timezone or not found, show timezone with underscores replaced
|
||||
const timezoneDisplay = timezone.replace(/_/g, " ");
|
||||
return `${city} (${timezoneDisplay})`;
|
||||
} catch {
|
||||
return timezone;
|
||||
return timezone.replace(/_/g, " ");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
13
autogpt_platform/frontend/src/mocks/index.ts
Normal file
13
autogpt_platform/frontend/src/mocks/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// We are not using this for tests because Vitest runs our tests in a Node.js environment.
|
||||
// However, we can use it for development purposes to test our UI in the browser with fake data.
|
||||
export async function initMocks() {
|
||||
if (typeof window === "undefined") {
|
||||
const { server } = await import("./mock-server");
|
||||
server.listen({ onUnhandledRequest: "bypass" });
|
||||
console.log("[MSW] Server mock initialized");
|
||||
} else {
|
||||
const { worker } = await import("./mock-browser");
|
||||
await worker.start({ onUnhandledRequest: "bypass" });
|
||||
console.log("[MSW] Browser mock initialized");
|
||||
}
|
||||
}
|
||||
4
autogpt_platform/frontend/src/mocks/mock-browser.ts
Normal file
4
autogpt_platform/frontend/src/mocks/mock-browser.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { setupWorker } from "msw/browser";
|
||||
import { mockHandlers } from "./mock-handlers";
|
||||
|
||||
export const worker = setupWorker(...mockHandlers);
|
||||
48
autogpt_platform/frontend/src/mocks/mock-handlers.ts
Normal file
48
autogpt_platform/frontend/src/mocks/mock-handlers.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { getAdminMock } from "@/app/api/__generated__/endpoints/admin/admin.msw";
|
||||
import { getAnalyticsMock } from "@/app/api/__generated__/endpoints/analytics/analytics.msw";
|
||||
import { getApiKeysMock } from "@/app/api/__generated__/endpoints/api-keys/api-keys.msw";
|
||||
import { getAuthMock } from "@/app/api/__generated__/endpoints/auth/auth.msw";
|
||||
import { getBlocksMock } from "@/app/api/__generated__/endpoints/blocks/blocks.msw";
|
||||
import { getChatMock } from "@/app/api/__generated__/endpoints/chat/chat.msw";
|
||||
import { getCreditsMock } from "@/app/api/__generated__/endpoints/credits/credits.msw";
|
||||
import { getDefaultMock } from "@/app/api/__generated__/endpoints/default/default.msw";
|
||||
import { getEmailMock } from "@/app/api/__generated__/endpoints/email/email.msw";
|
||||
import { getExecutionsMock } from "@/app/api/__generated__/endpoints/executions/executions.msw";
|
||||
import { getFilesMock } from "@/app/api/__generated__/endpoints/files/files.msw";
|
||||
import { getGraphsMock } from "@/app/api/__generated__/endpoints/graphs/graphs.msw";
|
||||
import { getHealthMock } from "@/app/api/__generated__/endpoints/health/health.msw";
|
||||
import { getIntegrationsMock } from "@/app/api/__generated__/endpoints/integrations/integrations.msw";
|
||||
import { getLibraryMock } from "@/app/api/__generated__/endpoints/library/library.msw";
|
||||
import { getMetricsMock } from "@/app/api/__generated__/endpoints/metrics/metrics.msw";
|
||||
import { getOauthMock } from "@/app/api/__generated__/endpoints/oauth/oauth.msw";
|
||||
import { getOnboardingMock } from "@/app/api/__generated__/endpoints/onboarding/onboarding.msw";
|
||||
import { getOttoMock } from "@/app/api/__generated__/endpoints/otto/otto.msw";
|
||||
import { getPresetsMock } from "@/app/api/__generated__/endpoints/presets/presets.msw";
|
||||
import { getSchedulesMock } from "@/app/api/__generated__/endpoints/schedules/schedules.msw";
|
||||
import { getStoreMock } from "@/app/api/__generated__/endpoints/store/store.msw";
|
||||
|
||||
// Pass hard-coded data to individual handler functions to override faker-generated data.
|
||||
export const mockHandlers = [
|
||||
...getAdminMock(),
|
||||
...getAnalyticsMock(),
|
||||
...getApiKeysMock(),
|
||||
...getAuthMock(),
|
||||
...getBlocksMock(),
|
||||
...getChatMock(),
|
||||
...getCreditsMock(),
|
||||
...getDefaultMock(),
|
||||
...getEmailMock(),
|
||||
...getExecutionsMock(),
|
||||
...getFilesMock(),
|
||||
...getGraphsMock(),
|
||||
...getHealthMock(),
|
||||
...getIntegrationsMock(),
|
||||
...getLibraryMock(),
|
||||
...getMetricsMock(),
|
||||
...getOauthMock(),
|
||||
...getOnboardingMock(),
|
||||
...getOttoMock(),
|
||||
...getPresetsMock(),
|
||||
...getSchedulesMock(),
|
||||
...getStoreMock(),
|
||||
];
|
||||
4
autogpt_platform/frontend/src/mocks/mock-server.ts
Normal file
4
autogpt_platform/frontend/src/mocks/mock-server.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { setupServer } from "msw/node";
|
||||
import { mockHandlers } from "./mock-handlers";
|
||||
|
||||
export const server = setupServer(...mockHandlers);
|
||||
220
autogpt_platform/frontend/src/tests/AGENTS.md
Normal file
220
autogpt_platform/frontend/src/tests/AGENTS.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# Frontend Testing Rules 🧪
|
||||
|
||||
## Testing Types Overview
|
||||
|
||||
| Type | Tool | Speed | Purpose |
|
||||
| --------------- | --------------------- | --------------- | -------------------------------- |
|
||||
| **E2E** | Playwright | Slow (~5s/test) | Real browser, full user journeys |
|
||||
| **Integration** | Vitest + RTL | Fast (~100ms) | Component + mocked API |
|
||||
| **Unit** | Vitest + RTL | Fastest (~10ms) | Individual functions/components |
|
||||
| **Visual** | Storybook + Chromatic | N/A | UI appearance, design system |
|
||||
|
||||
---
|
||||
|
||||
## When to Use Each
|
||||
|
||||
### ✅ E2E Tests (Playwright)
|
||||
|
||||
**Use for:** Critical user journeys that MUST work in a real browser.
|
||||
|
||||
- Authentication flows (login, signup, logout)
|
||||
- Payment or sensitive transactions
|
||||
- Flows requiring real browser APIs (clipboard, downloads)
|
||||
- Cross-page navigation that must work end-to-end
|
||||
|
||||
**Location:** `src/tests/*.spec.ts` (centralized, as there will be fewer of them)
|
||||
|
||||
### ✅ Integration Tests (Vitest + RTL)
|
||||
|
||||
**Use for:** Testing components with their dependencies (API calls, state).
|
||||
|
||||
- Page-level behavior with mocked API responses
|
||||
- Components that fetch data
|
||||
- User interactions that trigger API calls
|
||||
- Feature flows within a single page
|
||||
|
||||
**Location:** Place tests in a `__tests__` folder next to the component:
|
||||
|
||||
```
|
||||
ComponentName/
|
||||
__tests__/
|
||||
main.test.tsx
|
||||
some-flow.test.tsx
|
||||
ComponentName.tsx
|
||||
useComponentName.ts
|
||||
```
|
||||
|
||||
**Start at page level:** Initially write integration tests at the "page" level. No need to write them for every small component.
|
||||
|
||||
```
|
||||
/library/
|
||||
__tests__/
|
||||
main.test.tsx
|
||||
searching-agents.test.tsx
|
||||
agents-pagination.test.tsx
|
||||
page.tsx
|
||||
useLibraryPage.ts
|
||||
```
|
||||
|
||||
Start with a `main.test.tsx` file and split into smaller files as it grows.
|
||||
|
||||
**What integration tests should do:**
|
||||
|
||||
1. Render a page or complex modal (e.g., `AgentPublishModal`)
|
||||
2. Mock API requests via MSW
|
||||
3. Assert UI scenarios via Testing Library
|
||||
|
||||
```tsx
|
||||
// Example: Test page renders data from API
|
||||
import { server } from "@/mocks/mock-server";
|
||||
import { getDeleteV2DeleteStoreSubmissionMockHandler422 } from "@/app/api/__generated__/endpoints/store/store.msw";
|
||||
|
||||
test("shows error when submission fails", async () => {
|
||||
// Override default handler to return error status
|
||||
server.use(getDeleteV2DeleteStoreSubmissionMockHandler422());
|
||||
|
||||
render(<MarketplacePage />);
|
||||
await screen.findByText("Featured Agents");
|
||||
// ... assert error UI
|
||||
});
|
||||
```
|
||||
|
||||
**Tip:** Use `findBy...` methods most of the time—they wait for elements to appear, so async code won't cause flaky tests. The regular `getBy...` methods don't wait and error immediately.
|
||||
|
||||
### ✅ Unit Tests (Vitest + RTL)
|
||||
|
||||
**Use for:** Testing isolated components and utility functions.
|
||||
|
||||
- Pure utility functions (`lib/utils.ts`)
|
||||
- Component rendering with various props
|
||||
- Component state changes
|
||||
- Custom hooks
|
||||
|
||||
**Location:** Co-located with the file: `Component.test.tsx` next to `Component.tsx`
|
||||
|
||||
```tsx
|
||||
// Example: Test component renders correctly
|
||||
render(<AgentCard title="My Agent" />);
|
||||
expect(screen.getByText("My Agent")).toBeInTheDocument();
|
||||
```
|
||||
|
||||
### ✅ Storybook Tests (Visual)
|
||||
|
||||
**Use for:** Design system, visual appearance, component documentation.
|
||||
|
||||
- Atoms (Button, Input, Badge)
|
||||
- Molecules (Dialog, Card)
|
||||
- Visual states (hover, disabled, loading)
|
||||
- Responsive layouts
|
||||
|
||||
**Location:** Co-located: `Component.stories.tsx` next to `Component.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Decision Flowchart
|
||||
|
||||
```
|
||||
Does it need a REAL browser/backend?
|
||||
├─ YES → E2E (Playwright)
|
||||
└─ NO
|
||||
└─ Does it involve API calls or complex state?
|
||||
├─ YES → Integration (Vitest + RTL)
|
||||
└─ NO
|
||||
└─ Is it about visual appearance?
|
||||
├─ YES → Storybook
|
||||
└─ NO → Unit (Vitest + RTL)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What NOT to Test
|
||||
|
||||
❌ Third-party library internals (Radix UI, React Query)
|
||||
❌ CSS styling details (use Storybook)
|
||||
❌ Simple prop-passing components with no logic
|
||||
❌ TypeScript types
|
||||
|
||||
---
|
||||
|
||||
## File Organization
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ └── atoms/
|
||||
│ └── Button/
|
||||
│ ├── Button.tsx
|
||||
│ ├── Button.test.tsx # Unit test
|
||||
│ └── Button.stories.tsx # Visual test
|
||||
├── app/
|
||||
│ └── (platform)/
|
||||
│ └── marketplace/
|
||||
│ └── components/
|
||||
│ └── MainMarketplacePage/
|
||||
│ ├── __tests__/
|
||||
│ │ ├── main.test.tsx # Integration test
|
||||
│ │ └── search-agents.test.tsx # Integration test
|
||||
│ ├── MainMarketplacePage.tsx
|
||||
│ └── useMainMarketplacePage.ts
|
||||
├── lib/
|
||||
│ ├── utils.ts
|
||||
│ └── utils.test.ts # Unit test
|
||||
├── mocks/
|
||||
│ ├── mock-handlers.ts # MSW handlers (auto-generated via Orval)
|
||||
│ └── mock-server.ts # MSW server setup
|
||||
└── tests/
|
||||
├── integrations/
|
||||
│ ├── test-utils.tsx # Testing utilities
|
||||
│ └── vitest.setup.tsx # Integration test setup
|
||||
└── *.spec.ts # E2E tests (Playwright) - centralized
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Priority Matrix
|
||||
|
||||
| Component Type | Test Priority | Recommended Test |
|
||||
| ------------------- | ------------- | ---------------- |
|
||||
| Pages/Features | **Highest** | Integration |
|
||||
| Custom Hooks | High | Unit |
|
||||
| Utility Functions | High | Unit |
|
||||
| Organisms (complex) | High | Integration |
|
||||
| Molecules | Medium | Unit + Storybook |
|
||||
| Atoms | Medium | Storybook only\* |
|
||||
|
||||
\*Atoms are typically simple enough that Storybook visual tests suffice.
|
||||
|
||||
---
|
||||
|
||||
## MSW Mocking
|
||||
|
||||
API mocking is handled via MSW (Mock Service Worker). Handlers are auto-generated by Orval from the OpenAPI schema.
|
||||
|
||||
**Default behavior:** All client-side requests are intercepted and return 200 status with faker-generated data.
|
||||
|
||||
**Override for specific tests:** Use generated error handlers to test non-OK status scenarios:
|
||||
|
||||
```tsx
|
||||
import { server } from "@/mocks/mock-server";
|
||||
import { getDeleteV2DeleteStoreSubmissionMockHandler422 } from "@/app/api/__generated__/endpoints/store/store.msw";
|
||||
|
||||
test("shows error when deletion fails", async () => {
|
||||
server.use(getDeleteV2DeleteStoreSubmissionMockHandler422());
|
||||
|
||||
render(<MyComponent />);
|
||||
// ... assert error UI
|
||||
});
|
||||
```
|
||||
|
||||
**Generated handlers location:** `src/app/api/__generated__/endpoints/*/` - each endpoint has handlers for different status codes.
|
||||
|
||||
---
|
||||
|
||||
## Golden Rules
|
||||
|
||||
1. **Test behavior, not implementation** - Query by role/text, not class names
|
||||
2. **One assertion per concept** - Tests should be focused
|
||||
3. **Mock at boundaries** - Mock API calls, not internal functions
|
||||
4. **Co-locate integration tests** - Keep `__tests__/` folder next to the component
|
||||
5. **E2E is expensive** - Only for critical happy paths; prefer integration tests
|
||||
6. **AI agents are good at writing integration tests** - Start with these when adding test coverage
|
||||
@@ -0,0 +1,25 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
const mockSupabaseClient = {
|
||||
auth: {
|
||||
getUser: vi.fn().mockResolvedValue({
|
||||
data: { user: null },
|
||||
error: null,
|
||||
}),
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
data: { session: null },
|
||||
error: null,
|
||||
}),
|
||||
signOut: vi.fn().mockResolvedValue({ error: null }),
|
||||
refreshSession: vi.fn().mockResolvedValue({
|
||||
data: { session: null, user: null },
|
||||
error: null,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const mockSupabaseRequest = () => {
|
||||
vi.mock("@/lib/supabase/server/getServerSupabase", () => ({
|
||||
getServerSupabase: vi.fn().mockResolvedValue(mockSupabaseClient),
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
export const mockNextjsModules = () => {
|
||||
vi.mock("next/image", () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
fill: _fill,
|
||||
priority: _priority,
|
||||
quality: _quality,
|
||||
placeholder: _placeholder,
|
||||
blurDataURL: _blurDataURL,
|
||||
loader: _loader,
|
||||
...props
|
||||
}: any) => {
|
||||
// eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element
|
||||
return <img {...props} />;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("next/headers", () => ({
|
||||
cookies: vi.fn(() => ({
|
||||
get: vi.fn(() => undefined),
|
||||
getAll: vi.fn(() => []),
|
||||
set: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
has: vi.fn(() => false),
|
||||
})),
|
||||
headers: vi.fn(() => new Headers()),
|
||||
}));
|
||||
|
||||
vi.mock("next/dist/server/request/cookies", () => ({
|
||||
cookies: vi.fn(() => ({
|
||||
get: vi.fn(() => undefined),
|
||||
getAll: vi.fn(() => []),
|
||||
set: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
has: vi.fn(() => false),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
prefetch: vi.fn(),
|
||||
back: vi.fn(),
|
||||
forward: vi.fn(),
|
||||
refresh: vi.fn(),
|
||||
}),
|
||||
usePathname: () => "/marketplace",
|
||||
useSearchParams: () => new URLSearchParams(),
|
||||
useParams: () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock("next/link", () => ({
|
||||
__esModule: true,
|
||||
default: ({ children, href, ...props }: any) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { BackendAPIProvider } from "@/lib/autogpt-server-api/context";
|
||||
import OnboardingProvider from "@/providers/onboarding/onboarding-provider";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { render, RenderOptions } from "@testing-library/react";
|
||||
import { ReactElement, ReactNode } from "react";
|
||||
|
||||
function createTestQueryClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function TestProviders({ children }: { children: ReactNode }) {
|
||||
const queryClient = createTestQueryClient();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BackendAPIProvider>
|
||||
<OnboardingProvider>{children}</OnboardingProvider>
|
||||
</BackendAPIProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function customRender(
|
||||
ui: ReactElement,
|
||||
options?: Omit<RenderOptions, "wrapper">,
|
||||
) {
|
||||
return render(ui, { wrapper: TestProviders, ...options });
|
||||
}
|
||||
|
||||
export * from "@testing-library/react";
|
||||
export { customRender as render };
|
||||
@@ -0,0 +1,12 @@
|
||||
import { beforeAll, afterAll, afterEach } from "vitest";
|
||||
import { server } from "@/mocks/mock-server";
|
||||
import { mockNextjsModules } from "./setup-nextjs-mocks";
|
||||
import { mockSupabaseRequest } from "./mock-supabase-request";
|
||||
|
||||
beforeAll(() => {
|
||||
mockNextjsModules();
|
||||
mockSupabaseRequest(); // If you need user's data - please mock supabase actions in your specific test - it sends null user [It's only to avoid cookies() call]
|
||||
return server.listen({ onUnhandledRequest: "error" });
|
||||
});
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
12
autogpt_platform/frontend/vitest.config.mts
Normal file
12
autogpt_platform/frontend/vitest.config.mts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tsconfigPaths(), react()],
|
||||
test: {
|
||||
environment: "happy-dom",
|
||||
include: ["src/**/*.test.tsx"],
|
||||
setupFiles: ["./src/tests/integrations/vitest.setup.tsx"],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user