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