diff --git a/deno.lock b/deno.lock index c50e4d8..722a380 100644 --- a/deno.lock +++ b/deno.lock @@ -2,6 +2,9 @@ "version": "5", "specifiers": { "jsr:@openai/openai@*": "6.16.0", + "jsr:@std/assert@1": "1.0.16", + "jsr:@std/dotenv@*": "0.225.6", + "jsr:@std/internal@^1.0.12": "1.0.12", "jsr:@supabase/functions-js@*": "2.90.1", "npm:@supabase/supabase-js@^2.90.1": "2.90.1", "npm:@types/bun@^1.3.5": "1.3.5", @@ -13,6 +16,18 @@ "@openai/openai@6.16.0": { "integrity": "ccee548f61c382d715091fff0c2c3390a4487823644430b8235df543d6d6a78b" }, + "@std/assert@1.0.16": { + "integrity": "6a7272ed1eaa77defe76e5ff63ca705d9c495077e2d5fd0126d2b53fc5bd6532", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/dotenv@0.225.6": { + "integrity": "1d6f9db72f565bd26790fa034c26e45ecb260b5245417be76c2279e5734c421b" + }, + "@std/internal@1.0.12": { + "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" + }, "@supabase/functions-js@2.90.1": { "integrity": "9a077ecf42aa84594ef3aac621d643e15ffaa9506a4c051e35c93f127ffdb035" } diff --git a/supabase/functions/tests/ai-structured-test.ts b/supabase/functions/tests/ai-structured-test.ts new file mode 100644 index 0000000..98da2d5 --- /dev/null +++ b/supabase/functions/tests/ai-structured-test.ts @@ -0,0 +1,146 @@ +// supabase/functions/ai-structured/ai_structured.test.ts +/// +/// +/// + +import { assert, assertEquals } from "jsr:@std/assert@1"; +import "jsr:@std/dotenv/load"; +import { createClient, type SupabaseClient } from "npm:@supabase/supabase-js@2"; + +const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? ""; +const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY") ?? ""; // (o PUBLISHABLE_KEY si así lo nombraste) +const EMAIL = Deno.env.get("TEST_EMAIL") ?? "guillermo.arrieta@lasalle.mx"; +const PASSWORD = Deno.env.get("TEST_PASSWORD") ?? "admin"; + +const options = { + auth: { + autoRefreshToken: false, + persistSession: false, + detectSessionInUrl: false, + }, +}; + +function mustEnv() { + if (!SUPABASE_URL) throw new Error("SUPABASE_URL is required"); + if (!SUPABASE_ANON_KEY) throw new Error("SUPABASE_ANON_KEY is required"); +} + +async function getAuthedClient(): Promise<{ client: SupabaseClient; accessToken: string }> { + mustEnv(); + const client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, options); + + const { data, error } = await client.auth.signInWithPassword({ + email: EMAIL, + password: PASSWORD, + }); + if (error) throw new Error("Sign-in failed: " + error.message); + + const accessToken = data.session?.access_token; + if (!accessToken) throw new Error("No access_token returned from signInWithPassword"); + + return { client, accessToken }; +} + +Deno.test("ai-structured (JSON body)", async () => { + const { client, accessToken } = await getAuthedClient(); + + const { data, error } = await client.functions.invoke("ai-structured", { + headers: { + Authorization: `Bearer ${accessToken}`, // 👈 clave para que tu función pase el authHeader check + }, + body: { + response: { + model: "gpt-5", + input: [ + { role: "system", content: "Responde SIEMPRE en JSON válido." }, + { role: "user", content: "Dame 3 ideas de proyecto de IA para educación." }, + ], + }, + structured: { + type: "json_schema", + name: "ideas", + strict: true, + schema: { + type: "object", + properties: { + ideas: { + type: "array", + items: { + type: "object", + properties: { + titulo: { type: "string" }, + descripcion: { type: "string" }, + }, + required: ["titulo", "descripcion"], + additionalProperties: false, + }, + }, + }, + required: ["ideas"], + additionalProperties: false, + }, + }, + references: {}, + usarMCP: false, + }, + }); + + if (error) throw new Error("Invoke failed: " + error.message); + assert(data, "Expected data from function"); + + // tu función responde { ok, output, outputText, ... } + assertEquals(data.ok, true); + assert(Array.isArray(data.output?.ideas), "output.ideas should be an array"); + assertEquals(data.output.ideas.length, 3); + for (const it of data.output.ideas) { + assert(typeof it.titulo === "string"); + assert(typeof it.descripcion === "string"); + } +}); + +Deno.test("ai-structured (multipart + file)", async () => { + const { client, accessToken } = await getAuthedClient(); + + // Lee un PDF local (ajusta path) + const bytes = await Deno.readFile("files/carta.pdf"); + const file = new File([bytes], "carta.pdf", { type: "application/pdf" }); + + const payload = { + response: { input: "Resume estos documentos en JSON.", model: "gpt-5" }, + structured: { + type: "json_schema", + name: "resumen", + strict: true, + schema: { + type: "object", + properties: { resumen: { type: "string" } }, + required: ["resumen"], + additionalProperties: false, + }, + }, + storage: { prefix: "tmp" }, + }; + + const fd = new FormData(); + fd.append("payload", JSON.stringify(payload)); + fd.append("files", file); + + const { data, error } = await client.functions.invoke("ai-structured", { + headers: { + Authorization: `Bearer ${accessToken}`, // 👈 necesario + // NO pongas Content-Type: fetch lo setea con boundary para FormData ✅ + }, + method: "POST", + body: fd, + }); + + if (error) throw new Error("Invoke failed: " + error.message); + assert(data, "Expected data from function"); + + assertEquals(data.ok, true); + assert(typeof data.output?.resumen === "string"); + assert(data.output.resumen.length > 0); + + // opcional: valida que registró upload(s) a storage + assert(Array.isArray(data.references?.uploadedToStorage)); +}); diff --git a/testers/ai-structured-files.ts b/testers/ai-structured-files.ts deleted file mode 100644 index 5a10360..0000000 --- a/testers/ai-structured-files.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { supabase } from "./new-user"; - -const file = Bun.file("files/carta.pdf"); - -await supabase.auth.signInWithPassword({ - email: "guillermo.arrieta@lasalle.mx", - password: "admin", -}); - -const fd = new FormData(); -fd.append("payload", JSON.stringify({ - response: { input: "Resume estos documentos en JSON.", model: "gpt-5" }, - structured: { type: "json_schema", name: "resumen", strict: true, schema: { - type: "object", properties: { resumen: { type: "string" } }, required: ["resumen"], additionalProperties: false - } }, - storage: { prefix: "tmp" }, -})); -fd.append("files", file); - -const response = await supabase.functions.invoke("ai-structured", { - method: "POST", - body: fd, -}); - -console.log({ d: response.data, e: response.error }); \ No newline at end of file diff --git a/testers/ai-structured.ts b/testers/ai-structured.ts deleted file mode 100644 index 494cafe..0000000 --- a/testers/ai-structured.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { supabase } from "./new-user"; - -const /* { data, error } */_ = await supabase.auth.signInWithPassword({ - email: "guillermo.arrieta@lasalle.mx", - password: "admin", -}); - -/* if (error) { - console.error("Error signing in:", error); - process.exit(1); -} else { - console.log("Successfully signed in:", data); -} */ - -try { - - const response = await supabase.functions.invoke("ai-structured", { - body: { - response: { - model: "gpt-5", - input: [ - { role: "system", content: "Responde SIEMPRE en JSON válido." }, - { role: "user", content: "Dame 3 ideas de proyecto de IA para educación." }, - ], - /* conversation: "conv_...", */ // opcional - }, - structured: { - type: "json_schema", - name: "ideas", - strict: true, - schema: { - type: "object", - properties: { - ideas: { - type: "array", - items: { - type: "object", - properties: { - titulo: { type: "string" }, - descripcion: { type: "string" } - }, - required: ["titulo", "descripcion"], - additionalProperties: false - } - } - }, - required: ["ideas"], - additionalProperties: false - } - }, - references: { - /* vectorStoreIds: ["vs_..."], */ // opcional (file_search) ✅ - /* openaiFileIds: ["file_..."], */ // opcional (input_file) ✅ - }, - usarMCP: false, // opcional ✅ - }, - }); - console.log({ d: response.data, e: response.error }); -} catch (e: any) { - const res = e?.context ?? e?.response; - console.log("status", res?.status); - if (res?.text) console.log("text", await res.text()); - else console.log("raw error keys", Object.keys(e ?? {})); -} diff --git a/testers/new-user.ts b/testers/new-user.ts deleted file mode 100644 index 4d7b575..0000000 --- a/testers/new-user.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createClient } from '@supabase/supabase-js' - -export const supabase = createClient(Bun.env.SUPABASE_URL ?? '', Bun.env.SUPABASE_ANON_KEY ?? '') -/* try { - const { data, error } = await supabase.auth.signUp({ - email: 'guillermo.arrieta@lasalle.mx', - password: 'sadfasdfsadfdsasadfsad', - }) - console.log({ data, error }) -} catch (error) { - console.log({ error }) -} */ \ No newline at end of file