- ASP.NET Core 9 Razor Pages + Minimal API hybrid - 14 validators (CPF, CEP, CNPJ, email, phone, name, yes-no, birthdate, handoff, cancel-intent, company-name, plate-br, postal-code, validate_reply) - OAuth login (Google, Microsoft, GitHub) + cookie auth - MongoDB usage tracking + CEP cache collection - Stripe checkout with inline PriceData (no Price IDs) - MCP server for Claude Code / Cursor integration - Playground (10 calls/IP/day, no auth) - Docs: Quickstart, API Reference, N8N, MCP, Créditos, Erros, Fluxos - Credit system: 3 cr standard validators, 5 cr validate_reply - SmartSuggestion: contextual re-ask via IA when obtained=false - Per-IP rate limiting + daily cap + shared-IP abuse detection - Lightbox for comic images - Validadores page split: Brasileiros / Universais + Em breve Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
199 lines
5.9 KiB
C#
199 lines
5.9 KiB
C#
using FluentAssertions;
|
|
using Nalu.Api.Services;
|
|
using Xunit;
|
|
|
|
namespace Nalu.Tests;
|
|
|
|
public class ValidatorLoaderTests
|
|
{
|
|
private const string FullNameMd = """
|
|
# validate_full_name
|
|
|
|
Extrai o nome completo do usuário.
|
|
|
|
## config
|
|
|
|
- type: extraction
|
|
- version: 1.0
|
|
- languages: pt-BR, es-ES, en-US
|
|
- endpoint: /v1/extract/name
|
|
- mcp_tool: nalu_extract_name
|
|
- mcp_description: Extrai o nome completo.
|
|
|
|
## deterministic_rules
|
|
|
|
### stop_words
|
|
bom dia, boa tarde, boa noite, olá, oi
|
|
|
|
### reject_patterns
|
|
- ^(não|nao|sei la)$
|
|
- ^\d+$
|
|
|
|
### accept_patterns
|
|
- ^meu nome é\s+(.+)$
|
|
- ^me chamo\s+(.+)$
|
|
|
|
### constraints
|
|
- min_length: 2
|
|
- must_have_alpha: true
|
|
- max_length: 120
|
|
|
|
## prompt
|
|
|
|
Você é um extrator de nomes.
|
|
|
|
Diálogo:
|
|
Agente: {{agent_input}}
|
|
Usuário: {{user_input}}
|
|
|
|
Contexto do agente: {{agent_context}}
|
|
|
|
Responda SOMENTE com JSON válido.
|
|
|
|
## few_shot_examples
|
|
|
|
### example 1
|
|
- agent_input: Qual seu nome?
|
|
- user_input: Bom dia!
|
|
- output: {"extracted_value": null, "certain": false, "reasoning": "Saudação"}
|
|
|
|
### example 2
|
|
- agent_input: Qual seu nome?
|
|
- user_input: Maria Silva
|
|
- output: {"extracted_value": "Maria Silva", "certain": true, "reasoning": "Nome plausível"}
|
|
|
|
## post_processors
|
|
- capitalize_proper_name
|
|
- remove_titles
|
|
|
|
## enrichment
|
|
(nenhum)
|
|
|
|
## suggestions
|
|
|
|
### when_null_greeting
|
|
{{greeting_response}} Mas preciso do seu nome. Pode me dizer?
|
|
|
|
### when_null_evasive
|
|
Preciso do seu nome para continuar. Qual é?
|
|
|
|
### when_uncertain
|
|
Só confirmando: seu nome é {{extracted_value}}?
|
|
|
|
### when_certain
|
|
(sem sugestão — agente segue o fluxo)
|
|
""";
|
|
|
|
[Fact]
|
|
public void Parse_Config_ReadsAllFields()
|
|
{
|
|
var def = ValidatorLoader.ParseFromContent("validate_full_name", FullNameMd);
|
|
|
|
def.Id.Should().Be("validate_full_name");
|
|
def.Type.Should().Be("extraction");
|
|
def.Version.Should().Be("1.0");
|
|
def.Languages.Should().BeEquivalentTo(["pt-BR", "es-ES", "en-US"]);
|
|
def.Endpoint.Should().Be("/v1/extract/name");
|
|
def.McpTool.Should().Be("nalu_extract_name");
|
|
def.McpDescription.Should().Contain("Extrai o nome completo");
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_StopWords_ParsesCommaSeparated()
|
|
{
|
|
var def = ValidatorLoader.ParseFromContent("validate_full_name", FullNameMd);
|
|
|
|
def.StopWords.Should().Contain("bom dia");
|
|
def.StopWords.Should().Contain("boa tarde");
|
|
def.StopWords.Should().Contain("oi");
|
|
def.StopWords.Should().HaveCount(5);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_RejectPatterns_ParsesBulletList()
|
|
{
|
|
var def = ValidatorLoader.ParseFromContent("validate_full_name", FullNameMd);
|
|
|
|
def.RejectPatterns.Should().HaveCount(2);
|
|
def.RejectPatterns[0].Should().Be(@"^(não|nao|sei la)$");
|
|
def.RejectPatterns[1].Should().Be(@"^\d+$");
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_AcceptPatterns_ParsesBulletList()
|
|
{
|
|
var def = ValidatorLoader.ParseFromContent("validate_full_name", FullNameMd);
|
|
|
|
def.AcceptPatterns.Should().HaveCount(2);
|
|
def.AcceptPatterns[0].Should().Be(@"^meu nome é\s+(.+)$");
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_Constraints_ParsesKeyValues()
|
|
{
|
|
var def = ValidatorLoader.ParseFromContent("validate_full_name", FullNameMd);
|
|
|
|
def.Constraints.Should().ContainKey("min_length").WhoseValue.Should().Be("2");
|
|
def.Constraints.Should().ContainKey("max_length").WhoseValue.Should().Be("120");
|
|
def.Constraints.Should().ContainKey("must_have_alpha").WhoseValue.Should().Be("true");
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_Prompt_CapturesFullText()
|
|
{
|
|
var def = ValidatorLoader.ParseFromContent("validate_full_name", FullNameMd);
|
|
|
|
def.Prompt.Should().Contain("Você é um extrator de nomes");
|
|
def.Prompt.Should().Contain("{{agent_input}}");
|
|
def.Prompt.Should().Contain("{{user_input}}");
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_FewShots_ParsesAllExamples()
|
|
{
|
|
var def = ValidatorLoader.ParseFromContent("validate_full_name", FullNameMd);
|
|
|
|
def.FewShotExamples.Should().HaveCount(2);
|
|
def.FewShotExamples[0].AgentInput.Should().Be("Qual seu nome?");
|
|
def.FewShotExamples[0].UserInput.Should().Be("Bom dia!");
|
|
def.FewShotExamples[1].UserInput.Should().Be("Maria Silva");
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_PostProcessors_ParsesBulletList()
|
|
{
|
|
var def = ValidatorLoader.ParseFromContent("validate_full_name", FullNameMd);
|
|
|
|
def.PostProcessors.Should().BeEquivalentTo(["capitalize_proper_name", "remove_titles"]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_Enrichment_NenhumYieldsEmptyList()
|
|
{
|
|
var def = ValidatorLoader.ParseFromContent("validate_full_name", FullNameMd);
|
|
|
|
def.Enrichers.Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_Suggestions_ParsesTemplates()
|
|
{
|
|
var def = ValidatorLoader.ParseFromContent("validate_full_name", FullNameMd);
|
|
|
|
def.Suggestions.Should().ContainKey("when_null_greeting");
|
|
def.Suggestions.Should().ContainKey("when_null_evasive");
|
|
def.Suggestions.Should().ContainKey("when_uncertain");
|
|
// when_certain has "(sem sugestão..." so it should NOT be stored
|
|
def.Suggestions.Should().NotContainKey("when_certain");
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_Suggestions_ContainsPlaceholders()
|
|
{
|
|
var def = ValidatorLoader.ParseFromContent("validate_full_name", FullNameMd);
|
|
|
|
def.Suggestions["when_null_greeting"].Should().Contain("{{greeting_response}}");
|
|
def.Suggestions["when_uncertain"].Should().Contain("{{extracted_value}}");
|
|
}
|
|
}
|