# Test Plan: Speaker-Aware Extraction (v0.7.0)

> Para o time Arandu Spector
> Feature: `speaker_name` obrigatório no `write()`
> Data: 2026-03-25

## Como rodar

Todos os cenários usam `MemoryClient.write()` com LLM real (GPT-4o ou Claude).
Verificar o `WriteResult` retornado: entidades, fatos, relações.

```python
result = await memory.write(
    user_id="test-spector",
    message="<MENSAGEM DO CENÁRIO>",
    speaker_name="<SPEAKER DO CENÁRIO>",
)
```

Após cada cenário, inspecionar:
- `result.entities_resolved` — quais entidades foram criadas
- `result.facts_added` — quais fatos, com qual subject
- Banco: verificar que NÃO existe entidade com `entity_key = "user:self"`

---

## Cenário 1: Caso base — apresentação pessoal

**Input:**
```python
speaker_name = "Pedro"
message = "Meu nome é Pedro, moro em Botafogo, trabalho na Stone."
```

**Esperado:**
- Entidades: `Pedro` (person), `Botafogo` (place), `Stone` (organization)
- Fatos: ~3-5 fatos, TODOS com subject "Pedro"
- Zero entidade "user"
- Zero mirror facts (ex: "Stone emprega Pedro" NÃO deve existir)
- Relações: `Pedro → lives_in → Botafogo`, `Pedro → works_at → Stone`

**Critério PASS:** 0 entidades "user", 0 fatos com subject "user", 3 entidades nomeadas.

---

## Cenário 2: Pronomes em inglês

**Input:**
```python
speaker_name = "Rafael"
message = "I'm a backend engineer. I live in São Paulo. My girlfriend's name is Ana."
```

**Esperado:**
- Entidades: `Rafael` (person), `São Paulo` (place), `Ana` (person)
- Fatos com subject "Rafael": "Rafael is a backend engineer", "Rafael lives in São Paulo"
- Fato sobre Ana: "Ana is Rafael's girlfriend" (ou similar)
- Zero entidade "user" ou "I" ou "me"

**Critério PASS:** Pronomes "I", "My" resolvem para "Rafael", não criam entidade separada.

---

## Cenário 3: Pronomes em português

**Input:**
```python
speaker_name = "Maria"
message = "Eu sou médica, moro em Curitiba. Meu marido se chama João e ele é advogado."
```

**Esperado:**
- Entidades: `Maria` (person), `Curitiba` (place), `João` (person)
- Fatos: "Maria é médica", "Maria mora em Curitiba", "João é advogado", "João é marido de Maria"
- Zero entidade "eu" ou "user"

**Critério PASS:** "Eu", "Meu" resolvem para "Maria".

---

## Cenário 4: Múltiplas entidades, sem duplicação

**Input:**
```python
speaker_name = "Carlos"
message = "Trabalho na Acme Corp com a Fernanda. Ela é head de produto. O escritório fica em Campinas."
```

**Esperado:**
- Entidades: `Carlos` (person), `Acme Corp` (organization), `Fernanda` (person), `Campinas` (place)
- Fato "Carlos trabalha na Acme Corp" — MAS NÃO "Acme Corp emprega Carlos"
- Fato "Fernanda é head de produto" — com subject "Fernanda", não "Acme Corp"
- Zero mirror facts

**Critério PASS:** Cada informação aparece em exatamente 1 fato. Sem duplicação bidirecional.

---

## Cenário 5: Edge case — speaker_name ≠ nome na mensagem

**Input:**
```python
speaker_name = "Pedro"
message = "Meu nome é João Pedro, mas todo mundo me chama de JP."
```

**Esperado:**
- Entidade criada: `João Pedro` (person) com alias `JP`
- O speaker_name "Pedro" deve ser associado/resolvido com "João Pedro" (o sistema pode tratar como a mesma pessoa ou como alias)
- Fatos com subject "João Pedro" ou "Pedro" (ambos aceitáveis)
- O importante: NÃO criar 3 entidades separadas (Pedro, João Pedro, JP)

**Critério PASS:** No máximo 1 entidade de pessoa (com aliases), não 3 separadas.

---

## Cenário 6: Edge case — speaker_name é apelido, mensagem usa nome completo

**Input:**
```python
speaker_name = "Rafa"
message = "Meu nome completo é Rafael Mendes. Sou engenheiro de dados na Nubank."
```

**Esperado:**
- Entidade: `Rafael Mendes` (person) — possivelmente com alias "Rafa"
- Entidade: `Nubank` (organization)
- Fatos com subject "Rafael Mendes"
- Entity resolution deve resolver "Rafa" (speaker) para "Rafael Mendes" (fuzzy match)

**Critério PASS:** Speaker e nome completo resolvem para 1 entidade. Sem duplicação.

---

## Cenário 7: Mensagem sobre terceiros (speaker não é o foco)

**Input:**
```python
speaker_name = "Ana"
message = "O Lucas é engenheiro na Google. Ele mora em Mountain View com a esposa dele, a Carla."
```

**Esperado:**
- Entidades: `Ana` (person), `Lucas` (person), `Google` (organization), `Mountain View` (place), `Carla` (person)
- Fatos: sobre Lucas (trabalho, moradia) e sobre Carla (esposa do Lucas)
- Ana pode não ter fatos próprios (ela só narrou) — OK
- Zero mirror facts

**Critério PASS:** Fatos sobre Lucas e Carla corretos. Ana aparece como entidade do speaker mas pode não ter fatos.

---

## Cenário 8: Mensagem vazia / sem conteúdo extraível

**Input:**
```python
speaker_name = "Pedro"
message = "Oi, tudo bem?"
```

**Esperado:**
- Poucos ou zero fatos extraídos
- Pipeline completa sem erro
- `result.success == True`

**Critério PASS:** Não quebra. Retorna resultado vazio graciosamente.

---

## Cenário 9: speaker_name vazio — deve dar erro

**Input:**
```python
speaker_name = ""
message = "Qualquer coisa"
```

**Esperado:**
- `ValueError: speaker_name is required and cannot be empty`

**Também testar:**
- `speaker_name = "   "` (só espaços) → mesmo erro
- Sem speaker_name (omitido) → `TypeError`

**Critério PASS:** Erros claros, não silenciosos.

---

## Cenário 10: Atualização cross-message (write sequencial)

**Input:**
```python
# Mensagem 1
speaker_name = "Rafael"
message = "Moro em São Paulo, trabalho na Acme."

# Mensagem 2
speaker_name = "Rafael"
message = "Me mudei pra Rio de Janeiro. Saí da Acme e fui pra Stone."
```

**Esperado após mensagem 2:**
- Fato "Rafael mora em São Paulo" → superseded (UPDATE)
- Novo fato "Rafael mora no Rio de Janeiro"
- Fato "Rafael trabalha na Acme" → superseded
- Novo fato "Rafael trabalha na Stone"
- Entidade "Rafael" é a MESMA nas duas mensagens (não duplicada)

**Critério PASS:** Entity resolution reconhece "Rafael" entre mensagens. Facts são UPDATE, não ADD duplicado.

---

## Cenário 11: Mensagem longa com muitas entidades

**Input:**
```python
speaker_name = "Marcos"
message = """
Ontem fui jantar com a Fernanda no Outback do Shopping Morumbi.
Encontramos o Bruno lá, que agora tá trabalhando na iFood.
A Fernanda tá grávida de 5 meses, vai ter uma menina.
O Bruno acabou de voltar de uma viagem pro Japão.
Combinamos de jogar tênis no sábado no clube Pinheiros.
"""
```

**Esperado:**
- Entidades: `Marcos`, `Fernanda`, `Outback`, `Shopping Morumbi`, `Bruno`, `iFood`, `Japão`, `Clube Pinheiros`
- ~6-8 fatos distintos (cada informação 1x)
- Todos os pronomes resolvem pra "Marcos"
- Zero duplicação

**Critério PASS:** ≥ 6 entidades, ≥ 5 fatos, zero "user", zero mirror facts.

---

## Cenário 12: Idiomas misturados (code-switching)

**Input:**
```python
speaker_name = "Pedro"
message = "I work at Stone as a backend engineer. Moro em Botafogo, no Rio. My team uses Python and Go."
```

**Esperado:**
- Pronomes em inglês ("I", "My") e contexto em português resolvem para "Pedro"
- Entidades: `Pedro`, `Stone`, `Botafogo`, `Rio` (ou `Rio de Janeiro`), `Python`, `Go`
- Fatos coerentes misturando idiomas

**Critério PASS:** Code-switching não confunde o pipeline.

---

## Cenário 13: Speaker mencionado na terceira pessoa

**Input:**
```python
speaker_name = "Ana"
message = "Todo mundo fala que a Ana é workaholic, mas eu discordo. Eu só gosto do meu trabalho na Globo."
```

**Esperado:**
- "Ana" (3a pessoa) e "eu" (1a pessoa) resolvem para a MESMA entidade
- NÃO criar 2 entidades Ana separadas
- Fatos: "Ana trabalha na Globo", "Ana gosta do trabalho dela" (ou similar)

**Critério PASS:** Auto-referência em 3a pessoa não cria duplicata.

---

## Cenário 14: Retrieve após write (sanity check end-to-end)

**Input:**
```python
# Write
await memory.write(
    user_id="test-e2e",
    message="Meu nome é Pedro, moro em Botafogo, trabalho na Stone como engenheiro de software.",
    speaker_name="Pedro",
)

# Retrieve
result = await memory.retrieve(
    user_id="test-e2e",
    query="Onde o Pedro mora e onde trabalha?",
)
```

**Esperado:**
- `result.facts` contém fatos sobre Pedro (moradia e trabalho)
- `result.context` menciona Botafogo e Stone
- Zero menção a "user" no contexto

**Critério PASS:** Retrieve retorna fatos corretos escritos com speaker_name.

---

## Checklist de Regressão

Além dos cenários acima, verificar que NÃO quebrou:

- [ ] Write com `dry_run=True` funciona normalmente (com speaker_name)
- [ ] Write com `verbose=True` retorna trace com dados de extraction
- [ ] Write com `config_overrides` funciona
- [ ] Write com `recent_messages` funciona
- [ ] Retrieve não foi afetado (não mudou nada no read pipeline)
- [ ] Background jobs rodam normalmente após writes com speaker_name
- [ ] Não existe entidade `user:self` no banco após nenhum dos testes acima

---

## Notas para o time

1. **Breaking change:** Todo `write()` agora exige `speaker_name`. Code antigo sem esse parâmetro vai dar `TypeError`.
2. **`user_id` ≠ `speaker_name`:** O `user_id` é o agente/cérebro (particionamento). O `speaker_name` é quem fala. Num chatbot, o `user_id` é o bot e o `speaker_name` é o humano.
3. **Sem backward compat:** V0, sem usuários em produção. Não tem migration.
4. **LLM-dependent:** Os cenários 1-13 dependem do LLM interpretar os prompts corretamente. Se um cenário falhar por output inesperado do LLM, reportar o output completo pro debug.
