Pular para o conteúdo

Bot de atendimento

Um bot de Telegram com menu, conversa em etapas e fallback — testado sem token e sem rede.

Um bot que só se testa conversando com ele no celular não tem teste nenhum. Tg.testar injeta mensagens e lê o que o bot respondeu, e é isso que permite ter uma suíte para um bot de atendimento com a mesma disciplina de uma API.

PeçaO que ela exercita
Tg.app / mark @app.comandoos comandos, como decoradores
mark @app.textocasar por expressão regular
V.estado por conversa (ctx.estado)a conversa em etapas
Tg.testara sonda que finge ser o Telegram

Estrutura#

text
bot-suporte/
  src/
    bot.df         montar(token) — sem subir nada
    fluxos.df      a conversa de abrir chamado
  main.df          le o token do ambiente e sobe
  tests/
    bot_test.df
forge.toml
[project]
name = "bot-suporte"
version = "0.1.0"
description = "Bot de atendimento"
entry = "src/main.df"
dataforge = ">=1.1"

[dependencies]

[scripts]
start = "run src/main.df"
test = "test tests/"

O núcleo#

Este bloco roda sozinho — copie para um arquivo e rode dataforge run. Ele termina com assert, e é assim que esta página é conferida a cada build.

src/bot.df
adopt Arcane.Telegram as Tg

chamados := []

action montar(token):
    app := Tg.app(token)

    mark @app.comando("start")
    action comecar(ctx):
        ctx.responder("Oi! Mande /chamado para abrir um chamado, ou /status.")

    mark @app.comando("chamado")
    action abrir(ctx):
        ctx.estado["etapa"] := "assunto"
        ctx.responder("Qual e o assunto?")

    mark @app.comando("status")
    action status(ctx):
        ctx.responder($"{len(chamados)} chamado(s) aberto(s).")

    mark @app.qualquer()
    action conversa(ctx):
        match ctx.estado["etapa"] ?? "":
            point "assunto":
                ctx.estado["assunto"] := ctx.texto
                ctx.estado["etapa"] := "detalhe"
                ctx.responder("Descreva o problema em uma frase.")
            point "detalhe":
                chamados.append({"assunto": ctx.estado["assunto"], "detalhe": ctx.texto})
                ctx.estado["etapa"] := ""
                ctx.responder($"Chamado #{len(chamados)} aberto.")
            default:
                ctx.responder("Nao entendi. Mande /start.")

    yield app

t := Tg.testar(montar("123456:AAHexemplo"))
t.comando("start")
assert "/chamado" in t.ultima()

t.comando("chamado")
t.mandar("Impressora")
t.mandar("nao imprime frente e verso")
assert t.ultima() is "Chamado #1 aberto."
assert chamados[0]["assunto"] is "Impressora"

t.mandar("ola?")
assert "Nao entendi" in t.ultima()
t.comando("status")
assert t.ultima() is "1 chamado(s) aberto(s)."
out "bot verde"

O teste#

No projeto, a regra mora em src/ e o teste a importa pelo caminho relativo — dataforge test tests/ descobre o arquivo sozinho.

tests/nucleo_test.df
adopt Arcane.Telegram as Tg
adopt ../src/bot as B

crucible "bot":
    trial "a conversa volta ao comeco depois de abrir":
        t := Tg.testar(B.montar("123456:AAHexemplo"))
        t.comando("chamado")
        t.mandar("a")
        t.mandar("b")
        t.mandar("c")
        expect "Nao entendi" in t.ultima()

As decisões#

DecisãoSem ela
montar(token) devolve o app, não sobeo teste sobe um bot que nunca termina
o token vem do ambiente em main.dfo token vai parar no repositório
a etapa mora no estado da conversaduas pessoas conversando misturam os chamados
qualquer() por últimoo fallback engole os comandos

Para ir além#

Volte para todos os tipos de projeto.