42a437afa3
- Corregido el problema donde la función devolvía siempre 400; ahora se lanzan HttpError con status adecuados (405, 401, 415, 422, 404, 409, 502, 500) según el caso. - Refactor: usar sendError/sendSuccess y HttpError desde _shared/utils.ts para respuestas consistentes y logging seguro. - Validación: agrupar errores de parse/validación en un único mensaje numerado (cada error en nueva línea). - OpenAI: manejo explícito de fallos (MissingEnv, JSON inválido, salida estructurada ausente) y mapeo a status apropiados. - Supabase: distinguir errores 404/409/500 en consultas e inserts; incluir detalles en internalDetails para diagnóstico server-side. - Eliminada dependencia de invocación HTTP a ai-structured; usar servicio compartido directo. - Ajustes menores en mensajes y configuración (deno imports/deno.json) y tests marcados como ignorados donde corresponde.
166 lines
4.8 KiB
TypeScript
166 lines
4.8 KiB
TypeScript
// supabase/functions/ai-structured/ai_structured.test.ts
|
|
/// <reference lib="deno.ns" />
|
|
/// <reference lib="dom" />
|
|
/// <reference lib="dom.iterable" />
|
|
|
|
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(
|
|
{ name: "ai-structured (JSON body) [DEPRECATED]", ignore: true },
|
|
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({
|
|
name: "ai-structured (multipart + file) [DEPRECATED]",
|
|
ignore: true,
|
|
}, 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-nano",
|
|
},
|
|
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));
|
|
});
|