Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
712d34e
feat: Sacred Remedy Engine + GET /api/instant-light
sraphaz Jan 31, 2026
6f39a32
feat(Engine 2.1): Ayurvedic qualities, action selector, 50 estados, c…
sraphaz Jan 31, 2026
63b0f31
feat(Engine 2.1): P0 unificar engines, P1 cooldown server-side, P2 nu…
sraphaz Jan 31, 2026
c4b83ab
docs: validação Engine 2.1 + depreciação lib/diagnosis e lib/sacred
sraphaz Jan 31, 2026
3d8b658
feat(sacred-remedy): Engine 2.1 - numerologia, ayurveda high-end, coo…
sraphaz Jan 31, 2026
e47f07e
docs(PR3): resposta à avaliação Engine 2.1 + deprecations + ayurveda …
sraphaz Jan 31, 2026
edd9edb
feat(sacred-remedy): testes Engine 2.1 + themes + Nakshatra->klesha
sraphaz Jan 31, 2026
6138ad5
feat(engine-2.1): P0 unificar engines, P1 numerologia completa, P2 Yo…
sraphaz Jan 31, 2026
cfeb300
feat(sacred-remedy): Engine 2.1 Premium - Ayurveda season/hour, sleep…
sraphaz Jan 31, 2026
9ec836a
fix: ESLint react-hooks/exhaustive-deps e prebuild ENOENT
sraphaz Jan 31, 2026
47f3a7d
feat(corpus): ampliar Puranas (400) e Upanishads (200) com tags compl…
sraphaz Jan 31, 2026
dd344c7
feat(instant-light): cooldown server-side autônomo (7 dias)
sraphaz Jan 31, 2026
e26f006
feat(engine-2.1): pipeline integrado, Truth Package, input parser, co…
sraphaz Jan 31, 2026
4fc08b0
feat(input-parser): JSONs definitivos e stateScorer fear+love
sraphaz Jan 31, 2026
ba415fe
feat(engine-2.1): TruthPackage padrão, fallback confusion, doc estado…
sraphaz Jan 31, 2026
649de2c
docs: corrige resumo ESTADO_REAL - wrappers removidos, 5 criticos
sraphaz Jan 31, 2026
9a3199f
feat(sacredRemedy): Sutra Context Engine - verso anterior e bloco con…
sraphaz Jan 31, 2026
b8ac06e
fix(build): prebuild ENOENT, tipos TS e doc PR3 atualizado
sraphaz Jan 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions __tests__/api/instant-light.route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
* Testes de integração para GET /api/instant-light.
* Cobre: resposta universal (anon), resposta personal (perfil), cooldown server-side (sessão mock).
*/

import { sessionCookieHeader } from "@/lib/auth";

const mockCookies = jest.fn();
const mockGetRecentSacredIds = jest.fn();
const mockGetRecentStateKeys = jest.fn();
const mockRecordInstantLight = jest.fn();

jest.mock("next/headers", () => ({
cookies: () => mockCookies(),
}));

jest.mock("@/lib/history/historyAdapter", () => ({
getRecentSacredIds: (...args: unknown[]) => mockGetRecentSacredIds(...args),
getRecentStateKeys: (...args: unknown[]) => mockGetRecentStateKeys(...args),
recordInstantLight: (...args: unknown[]) => mockRecordInstantLight(...args),
}));

describe("GET /api/instant-light", () => {
beforeEach(() => {
jest.clearAllMocks();
mockCookies.mockResolvedValue({ toString: () => "" });
mockGetRecentSacredIds.mockResolvedValue([]);
mockGetRecentStateKeys.mockResolvedValue([]);
mockRecordInstantLight.mockResolvedValue(undefined);
});

async function getHandler() {
const mod = await import("@/app/api/instant-light/route");
return mod.GET;
}

describe("modo anônimo (sem sessão)", () => {
it("retorna 200 com DarshanTruthPackage: sacred, practice, question, sacredId, stateKey", async () => {
const GET = await getHandler();
const req = new Request("http://localhost/api/instant-light");
const res = await GET(req);
expect(res.status).toBe(200);

const body = await res.json();
expect(body).toHaveProperty("sacredText");
expect(body).toHaveProperty("sacred");
expect(body.sacred).toHaveProperty("id");
expect(body.sacred).toHaveProperty("corpus");
expect(body).toHaveProperty("practice");
expect(body.practice).toHaveProperty("title");
expect(body.practice).toHaveProperty("steps");
expect(body).toHaveProperty("question");
expect(body.question).toHaveProperty("text");
expect(body).toHaveProperty("contemplativeQuestion");
expect(body.contemplativeQuestion).toHaveProperty("text");
expect(body).toHaveProperty("meta");
expect(typeof body.meta?.generatedAt).toBe("string");
expect(body).toHaveProperty("sacredId");
expect(body).toHaveProperty("stateKey");
expect(typeof body.sacredText).toBe("string");
expect(typeof body.question.text).toBe("string");
});

it("não inclui insight quando não há perfil", async () => {
const GET = await getHandler();
const req = new Request("http://localhost/api/instant-light");
const res = await GET(req);
const body = await res.json();
expect(body.insight).toBeUndefined();
});

it("não chama getRecentSacredIds nem recordInstantLight quando não há sessão", async () => {
const GET = await getHandler();
const req = new Request("http://localhost/api/instant-light");
await GET(req);
expect(mockGetRecentSacredIds).not.toHaveBeenCalled();
expect(mockRecordInstantLight).not.toHaveBeenCalled();
});
});

describe("modo personal (query fullName + birthDate)", () => {
it("retorna 200 e pode incluir insight", async () => {
const GET = await getHandler();
const req = new Request(
"http://localhost/api/instant-light?fullName=Ana+Costa&birthDate=1988-03-20"
);
const res = await GET(req);
expect(res.status).toBe(200);
const body = await res.json();
expect(body).toHaveProperty("sacredText");
expect(body).toHaveProperty("practice");
expect(body).toHaveProperty("question");
if (body.insight) expect(typeof body.insight).toBe("string");
});
});

describe("cooldown server-side (com sessão)", () => {
it("com sessão válida, chama getRecentSacredIds e getRecentStateKeys e usa resultado no composer", async () => {
const cookieStr = sessionCookieHeader({ email: "user@example.com" });
mockCookies.mockResolvedValue({ toString: () => cookieStr });
mockGetRecentSacredIds.mockResolvedValue(["yoga_sutras.YS.1.1"]);
mockGetRecentStateKeys.mockResolvedValue(["anxiety"]);

const GET = await getHandler();
const req = new Request("http://localhost/api/instant-light");
const res = await GET(req);
expect(res.status).toBe(200);
expect(mockGetRecentSacredIds).toHaveBeenCalledWith("user@example.com", 7);
expect(mockGetRecentStateKeys).toHaveBeenCalledWith("user@example.com", 7);
const body = await res.json();
expect(body).toHaveProperty("sacredId");
expect(body).toHaveProperty("stateKey");
});

it("com sessão e sacred na resposta, chama recordInstantLight", async () => {
const cookieStr = sessionCookieHeader({ email: "user@example.com" });
mockCookies.mockResolvedValue({ toString: () => cookieStr });
mockGetRecentSacredIds.mockResolvedValue([]);
mockGetRecentStateKeys.mockResolvedValue([]);

const GET = await getHandler();
const req = new Request("http://localhost/api/instant-light");
const res = await GET(req);
expect(res.status).toBe(200);
const body = await res.json();
if (body.sacred?.id) {
expect(mockRecordInstantLight).toHaveBeenCalledWith("user@example.com", body);
}
});
});

describe("formato da resposta (food, sleep, routine opcionais)", () => {
it("resposta pode conter food (do/avoid), sleep, routine quando aplicável", async () => {
const GET = await getHandler();
const req = new Request(
"http://localhost/api/instant-light?fullName=Maria&birthDate=1990-05-15"
);
const res = await GET(req);
const body = await res.json();
expect(body).toHaveProperty("practice");
expect(body.practice).toHaveProperty("steps");
expect(body).toHaveProperty("question");
expect(body.question).toHaveProperty("text");
if (body.food !== undefined) expect(Array.isArray(body.food.do) || Array.isArray(body.food)).toBe(true);
if (body.sleep !== undefined) expect(typeof body.sleep).toBe("string");
if (body.routine !== undefined) expect(typeof body.routine).toBe("string");
});
});
});
2 changes: 1 addition & 1 deletion __tests__/lib/finance/creditsManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ describe("lib/finance/creditsManager (com Supabase)", () => {
it("retorna null quando insert payment falha", async () => {
mockGetSupabase.mockReturnValue(
createMockSupabaseClient({
selectUser: { data: { id: "u1" } },
selectUser: { data: { id: "u1", credits_balance: 0 } },
insertPayment: { data: null },
})
);
Expand Down
66 changes: 66 additions & 0 deletions __tests__/lib/input/intentParser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Testes para lib/input — Intent Parser e State Scorer.
* Multi-eixo: verbo, sujeito, tema, emoção → stateCandidates → bestStateKey.
*/

import { parseIntent } from "@/lib/input/intentParser";
import { scoreState } from "@/lib/input/stateScorer";

describe("lib/input intentParser", () => {
describe("parseIntent", () => {
it("retorna null para texto vazio ou null", () => {
expect(parseIntent(null)).toBeNull();
expect(parseIntent(undefined)).toBeNull();
expect(parseIntent("")).toBeNull();
expect(parseIntent(" ")).toBeNull();
});

it("detecta verbClass fear em 'Tenho medo de perder meu relacionamento'", () => {
const intent = parseIntent("Tenho medo de perder meu relacionamento");
expect(intent).not.toBeNull();
expect(intent!.verbClass).toBe("fear");
expect(intent!.subject).toBe("self");
expect(intent!.theme).toBe("love");
expect(intent!.stateCandidates.length).toBeGreaterThan(0);
});

it("detecta tema love em texto com 'amor' e 'relacionamento'", () => {
const intent = parseIntent("Estou com medo no meu relacionamento");
expect(intent).not.toBeNull();
expect(intent!.theme).toBe("love");
});

it("detecta emoção e stateCandidates para 'ansioso'", () => {
const intent = parseIntent("Estou muito ansioso");
expect(intent).not.toBeNull();
expect(intent!.emotionLabels.length).toBeGreaterThan(0);
expect(intent!.stateCandidates.some((c) => c.stateKey === "anxiety")).toBe(true);
});

it("retorna subject self quando tem 'eu' ou 'meu'", () => {
expect(parseIntent("Eu não aguento mais")!.subject).toBe("self");
expect(parseIntent("Meu trabalho está me matando")!.subject).toBe("self");
});
});
});

describe("lib/input stateScorer", () => {
it("retorna undefined para intent null; retorna fallback confusion quando sem stateCandidates", () => {
expect(scoreState(null)).toBeUndefined();
expect(scoreState({ subject: "self", verbClass: null, theme: "general", emotionLabels: [], stateCandidates: [] })).toBe("confusion");
});

it("retorna stateKey válido quando há candidatos", () => {
const intent = parseIntent("Tenho medo de perder meu relacionamento");
const best = scoreState(intent);
expect(best).toBeDefined();
expect(["anxiety", "relational_insecurity", "avoidance"]).toContain(best);
});

it("prioriza anxiety para fear + love (ex.: medo de perder relacionamento)", () => {
const intent = parseIntent("Tenho medo de perder meu relacionamento");
const best = scoreState(intent);
expect(best).toBeDefined();
expect(["anxiety", "relational_insecurity"]).toContain(best);
});
});
150 changes: 150 additions & 0 deletions __tests__/lib/sacredRemedy/ayurvedaActionSelector.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* Testes para lib/sacredRemedy/ayurvedaActionSelector.
* Antídotos por qualidade, múltiplas qualities, prioridade por dosha.
*/

import {
getPracticeForQuality,
getFoodForQuality,
getActionsForQualities,
getActionsForQualitiesWithDosha,
getSeasonFromDate,
getHourPeriodFromDate,
getFullActionsForQualitiesWithDosha,
QUALITY_TO_PRACTICE,
QUALITY_TO_FOOD,
} from "@/lib/sacredRemedy/ayurvedaActionSelector";

describe("sacredRemedy/ayurvedaActionSelector", () => {
describe("getPracticeForQuality / getFoodForQuality", () => {
it("retorna prática e alimento para qualidade conhecida", () => {
const practice = getPracticeForQuality("ruksha");
const food = getFoodForQuality("ruksha");
expect(typeof practice).toBe("string");
expect(practice.length).toBeGreaterThan(0);
expect(practice).not.toBe("—");
expect(typeof food).toBe("string");
expect(food.length).toBeGreaterThan(0);
});

it("ruksha tem antídoto oleação/calor", () => {
expect(getPracticeForQuality("ruksha")).toMatch(/oleação|óleo/i);
expect(getFoodForQuality("ruksha")).toMatch(/ghee|oleação|sopa/i);
});

it("chala tem antídoto grounding", () => {
expect(getPracticeForQuality("chala")).toMatch(/grounding|pés|chão|caminhada/i);
});
});

describe("getActionsForQualities", () => {
it("retorna practice e food para lista de qualities", () => {
const { practice, food } = getActionsForQualities(["ruksha", "chala"]);
expect(typeof practice).toBe("string");
expect(typeof food).toBe("string");
});

it("usa primeira qualidade com mapeamento", () => {
const { practice } = getActionsForQualities(["ruksha"]);
expect(practice).toBe(QUALITY_TO_PRACTICE["ruksha"]);
});

it("lista vazia retorna strings vazias", () => {
const { practice, food } = getActionsForQualities([]);
expect(practice).toBe("");
expect(food).toBe("");
});
});

describe("getActionsForQualitiesWithDosha", () => {
it("retorna practice e food com múltiplas sugestões quando há várias qualities", () => {
const { practice, food } = getActionsForQualitiesWithDosha(
["ruksha", "chala", "ushna"],
"vata",
{ maxSuggestions: 3 }
);
expect(typeof practice).toBe("string");
expect(typeof food).toBe("string");
if (practice) expect(practice.length).toBeGreaterThan(0);
if (food) expect(food.length).toBeGreaterThan(0);
});

it("com dosha pitta prioriza qualities do pitta", () => {
const { practice } = getActionsForQualitiesWithDosha(
["ushna", "tikshna", "ruksha"],
"pitta",
{ maxSuggestions: 2 }
);
expect(typeof practice).toBe("string");
});

it("maxSuggestions limita quantidade de sugestões combinadas", () => {
const { practice } = getActionsForQualitiesWithDosha(
["ruksha", "chala", "guru", "manda", "sthira"],
"kapha",
{ maxSuggestions: 1 }
);
const parts = practice.split(". ").filter(Boolean);
expect(parts.length).toBeLessThanOrEqual(2);
});

it("com season summer prioriza qualities do pitta na ordenação", () => {
const { practice } = getActionsForQualitiesWithDosha(
["ushna", "ruksha", "tikshna"],
"vata",
{ maxSuggestions: 3, season: "summer" }
);
expect(typeof practice).toBe("string");
expect(practice.length).toBeGreaterThan(0);
});

it("com hour midday retorna resultado consistente", () => {
const { practice, food } = getFullActionsForQualitiesWithDosha(
["ushna", "tikshna"],
"pitta",
{ maxSuggestions: 2, hour: "midday" }
);
expect(typeof practice).toBe("string");
expect(typeof food).toBe("string");
});
});

describe("getSeasonFromDate / getHourPeriodFromDate", () => {
it("getSeasonFromDate retorna winter para jan", () => {
expect(getSeasonFromDate(new Date(2025, 0, 15))).toBe("winter");
});
it("getSeasonFromDate retorna summer para jul", () => {
expect(getSeasonFromDate(new Date(2025, 6, 15))).toBe("summer");
});
it("getSeasonFromDate retorna autumn para out", () => {
expect(getSeasonFromDate(new Date(2025, 9, 15))).toBe("autumn");
});
it("getHourPeriodFromDate retorna morning para 8h", () => {
expect(getHourPeriodFromDate(new Date(2025, 0, 1, 8, 0))).toBe("morning");
});
it("getHourPeriodFromDate retorna midday para 12h", () => {
expect(getHourPeriodFromDate(new Date(2025, 0, 1, 12, 0))).toBe("midday");
});
it("getHourPeriodFromDate retorna evening para 20h", () => {
expect(getHourPeriodFromDate(new Date(2025, 0, 1, 20, 0))).toBe("evening");
});
});

describe("20 gunas cobertas", () => {
const qualities = [
"ruksha", "chala", "tikshna", "ushna", "guru", "manda", "sthira",
"picchila", "kathina", "khara", "sukshma", "laghu", "snigdha", "sita",
"mridu", "vishada", "sandra", "drava", "sara", "shlakshna", "sthula",
];
it("QUALITY_TO_PRACTICE e QUALITY_TO_FOOD têm entrada para todas as qualities principais", () => {
for (const q of qualities) {
const p = QUALITY_TO_PRACTICE[q];
const f = QUALITY_TO_FOOD[q];
expect(p !== undefined).toBe(true);
expect(f !== undefined).toBe(true);
expect(p === "—" || p.length > 0).toBe(true);
expect(f === "—" || f.length > 0).toBe(true);
}
});
});
});
Loading