145 lines
4.5 KiB
TypeScript
145 lines
4.5 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("ai_generate_plan creates plan and returns structured data", async () => {
|
|
const { client, accessToken } = await getAuthedClient();
|
|
|
|
const { data: carrera, error: carreraError } = await client
|
|
.from("carreras")
|
|
.select("id")
|
|
.limit(1)
|
|
.single();
|
|
if (carreraError) throw new Error("Failed to fetch carrera: " + carreraError.message);
|
|
if (!carrera) throw new Error("No carrera found");
|
|
|
|
const datosBasicos = {
|
|
nombrePlan: "Plan de estudios de Ingeniería en Sistemas Computacionales",
|
|
carreraId: carrera.id,
|
|
nivel: "Licenciatura",
|
|
tipoCiclo: "Semestre", // important: normalizes to "Semestre"
|
|
numCiclos: 8,
|
|
};
|
|
|
|
const { data, error } = await client.functions.invoke("ai_generate_plan", {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
body: { datosBasicos },
|
|
});
|
|
|
|
if (error) throw new Error("Invoke failed: " + error.message);
|
|
assert(data, "Expected data from function");
|
|
|
|
// Top-level result
|
|
assertEquals(data.ok, true);
|
|
|
|
// `plan` (DB record)
|
|
assert(typeof data.plan?.id === "string");
|
|
assertEquals(data.plan.nombre, datosBasicos.nombrePlan);
|
|
assertEquals(data.plan.nivel, "Licenciatura");
|
|
assertEquals(data.plan.tipo_ciclo, "Semestre");
|
|
assertEquals(data.plan.numero_ciclos, datosBasicos.numCiclos);
|
|
|
|
// `plan_datos` (normalized structured payload)
|
|
assert(typeof data.plan_datos === "object");
|
|
assertEquals(data.plan_datos.nombre, datosBasicos.nombrePlan);
|
|
assertEquals(data.plan_datos.nivel, "Licenciatura");
|
|
assertEquals(
|
|
data.plan_datos.total_de_ciclos_del_plan_de_estudios,
|
|
String(datosBasicos.numCiclos),
|
|
);
|
|
assertEquals(data.plan_datos.duracion_del_ciclo_escolar, "16"); // Semestre -> 16
|
|
});
|
|
/*
|
|
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));
|
|
});
|
|
*/
|