Thomas Fransolet 118813af58 Les prompts du mode proactif ne sont plus journalisés comme des questions de visiteur
RecordVisitorQuestion écrivait Question = request.Message. Or le prompt d'un
déclenchement automatique est une consigne que le système s'écrit à lui-même
(« Tu es un guide audio de musée. Le visiteur vient d'entrer dans la zone X… ») :
chaque passage devant une œuvre aurait ajouté une fausse question de visiteur
dans l'onglet Guide IA, l'écran fait pour montrer ce que les humains demandent.
Et HasAnswer se déduisant des sources, un prompt d'accueil sans citation aurait
gonflé le bloc ambre des questions sans réponse. Même famille que
WeatherSyncService noyant le journal d'audit.

AiChatRequest.IsAutoTriggered, et Chat qui saute la journalisation.

Tranché : ne rien journaliser, plutôt que marquer d'un drapeau. Une ligne
VisitorQuestion sert au rapport de trous de contenu et aux thèmes du lot J ; un
prompt machine n'alimente ni l'un ni l'autre, et le garder marqué obligerait
chaque futur agrégat à penser à l'exclure — la classe de panne qu'on venait de
fermer sur AuditedTypes.

Mais les jetons restent comptés : ces tours coûtent de l'argent réel au quota du
client, et un audioguide proactif peut en consommer beaucoup. Les 2 tests fixent
la paire — pas de ligne journalisée, compteur incrémenté — parce que c'est la
combinaison qui compte, pas chaque moitié isolément.

dotnet test : 199 passés, 14 sautés, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 13:14:01 +02:00

289 lines
12 KiB
C#

using ManagerService.Controllers;
using ManagerService.Data;
using ManagerService.DTOs;
using ManagerService.Services;
using ManagerService.Tests.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging.Abstractions;
using Hangfire;
using Moq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace ManagerService.Tests.Controllers
{
public class AiControllerTests
{
private static readonly AiChatResponse FakeResponse = new AiChatResponse { Reply = "OK", TokensUsed = 42 };
// Chat vérifie les droits de l'appelant (IsSuperAdmin / instance du token) :
// sans utilisateur, User est null et le contrôleur renvoie un 500.
private AiController BuildController(MyInfoMateDbContext db, Mock<IAssistantService>? mockService = null,
string callerRole = Permissions.SuperAdmin, string callerInstanceId = "i1")
{
mockService ??= new Mock<IAssistantService>();
mockService
.Setup(s => s.ChatAsync(It.IsAny<AiChatRequest>()))
.ReturnsAsync(FakeResponse);
var controller = new AiController(mockService.Object, db, NullLogger<AiController>.Instance,
new Mock<IBackgroundJobClient>().Object);
FakeUser.SetUser(controller, FakeUser.Create(callerRole, callerInstanceId));
return controller;
}
private static AiChatRequest MakeRequest(string instanceId, AppType appType = AppType.Tablet) =>
new AiChatRequest { InstanceId = instanceId, AppType = appType, Message = "Bonjour" };
// ── FORBID CASES ─────────────────────────────────────────────────────
[Fact]
public async Task Chat_InstanceNotFound_ReturnsForbid()
{
using var db = DbContextFactory.Create();
var result = await BuildController(db).Chat(MakeRequest("unknown"));
Assert.IsType<ForbidResult>(result);
}
[Fact]
public async Task Chat_InstanceAssistantDisabled_ReturnsForbid()
{
using var db = DbContextFactory.Create();
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", IsAssistant = false, DateCreation = DateTime.UtcNow
});
db.ApplicationInstances.Add(new ApplicationInstance
{
Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = true,
Languages = new List<string>()
});
db.SaveChanges();
var result = await BuildController(db).Chat(MakeRequest("i1"));
Assert.IsType<ForbidResult>(result);
}
[Fact]
public async Task Chat_AppInstanceAssistantDisabled_ReturnsForbid()
{
using var db = DbContextFactory.Create();
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow
});
db.ApplicationInstances.Add(new ApplicationInstance
{
Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = false,
Languages = new List<string>()
});
db.SaveChanges();
var result = await BuildController(db).Chat(MakeRequest("i1"));
Assert.IsType<ForbidResult>(result);
}
[Fact]
public async Task Chat_NoAppInstance_ReturnsForbid()
{
using var db = DbContextFactory.Create();
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow
});
db.SaveChanges();
var result = await BuildController(db).Chat(MakeRequest("i1"));
Assert.IsType<ForbidResult>(result);
}
// ── JOURNALISATION DES QUESTIONS ─────────────────────────────────────
/// <summary>
/// Un tour déclenché par le mode proactif ne doit pas apparaître dans
/// « Ce que demandent vos visiteurs » : sa « question » est une consigne que le
/// système s'est écrite à lui-même. Mais ses jetons sont bien consommés, donc
/// bien comptés — c'est la paire qui compte, pas chaque moitié isolément.
/// </summary>
[Fact]
public async Task Chat_AutoTriggered_CountsTokensButLogsNoVisitorQuestion()
{
using var db = DbContextFactory.Create();
SeedAssistantInstance(db);
var request = MakeRequest("i1");
request.IsAutoTriggered = true;
await BuildController(db).Chat(request);
Assert.Empty(db.VisitorQuestions);
Assert.Equal(42, db.Instances.First().AiTokensThisMonth);
}
[Fact]
public async Task Chat_AskedByVisitor_LogsVisitorQuestion()
{
using var db = DbContextFactory.Create();
SeedAssistantInstance(db);
await BuildController(db).Chat(MakeRequest("i1"));
var logged = Assert.Single(db.VisitorQuestions);
Assert.Equal("Bonjour", logged.Question);
Assert.Equal(42, db.Instances.First().AiTokensThisMonth);
}
private static void SeedAssistantInstance(MyInfoMateDbContext db)
{
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow,
AiTokensPerMonth = 1_000, AiUsageMonthKey = DateTime.UtcNow.ToString("yyyy-MM")
});
db.ApplicationInstances.Add(new ApplicationInstance
{
Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = true,
Languages = new List<string>()
});
db.SaveChanges();
}
// ── QUOTA COUNTER ────────────────────────────────────────────────────
[Fact]
public async Task Chat_FirstRequestOfMonth_ResetsCounterAndAddsTokensUsed()
{
using var db = DbContextFactory.Create();
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow,
AiTokensThisMonth = 99, AiTokensPerMonth = 1_000, AiUsageMonthKey = "2020-01"
});
db.ApplicationInstances.Add(new ApplicationInstance
{
Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = true,
Languages = new List<string>()
});
db.SaveChanges();
await BuildController(db).Chat(MakeRequest("i1"));
var inst = db.Instances.First();
Assert.Equal(42, inst.AiTokensThisMonth);
Assert.Equal(DateTime.UtcNow.ToString("yyyy-MM"), inst.AiUsageMonthKey);
}
[Fact]
public async Task Chat_SameMonth_AddsTokensUsedToCounter()
{
using var db = DbContextFactory.Create();
var monthKey = DateTime.UtcNow.ToString("yyyy-MM");
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow,
AiTokensThisMonth = 3, AiTokensPerMonth = 1_000, AiUsageMonthKey = monthKey
});
db.ApplicationInstances.Add(new ApplicationInstance
{
Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = true,
Languages = new List<string>()
});
db.SaveChanges();
await BuildController(db).Chat(MakeRequest("i1"));
Assert.Equal(45, db.Instances.First().AiTokensThisMonth);
}
[Fact]
public async Task Chat_QuotaAlreadyReached_ReturnsTooManyRequestsWithoutCallingAssistantService()
{
using var db = DbContextFactory.Create();
var monthKey = DateTime.UtcNow.ToString("yyyy-MM");
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow,
AiTokensThisMonth = 100, AiTokensPerMonth = 100, AiUsageMonthKey = monthKey
});
db.ApplicationInstances.Add(new ApplicationInstance
{
Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = true,
Languages = new List<string>()
});
db.SaveChanges();
var mockService = new Mock<IAssistantService>();
mockService.Setup(s => s.ChatAsync(It.IsAny<AiChatRequest>())).ReturnsAsync(FakeResponse);
var result = await BuildController(db, mockService).Chat(MakeRequest("i1"));
var status = Assert.IsType<ObjectResult>(result);
Assert.Equal(429, status.StatusCode);
mockService.Verify(s => s.ChatAsync(It.IsAny<AiChatRequest>()), Times.Never);
}
[Fact]
public async Task Chat_PlanWithoutAi_ReturnsForbiddenWithoutCallingAssistantService()
{
using var db = DbContextFactory.Create();
db.Instances.Add(new Instance
{
// AiTokensPerMonth à 0 = pas d'IA dans le plan, et surtout pas « illimité » :
// c'est l'état de plan-starter et de toute instance fraîchement migrée.
Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow,
AiTokensPerMonth = 0, AiUsageMonthKey = DateTime.UtcNow.ToString("yyyy-MM")
});
db.ApplicationInstances.Add(new ApplicationInstance
{
Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = true,
Languages = new List<string>()
});
db.SaveChanges();
var mockService = new Mock<IAssistantService>();
mockService.Setup(s => s.ChatAsync(It.IsAny<AiChatRequest>())).ReturnsAsync(FakeResponse);
var result = await BuildController(db, mockService).Chat(MakeRequest("i1"));
var status = Assert.IsType<ObjectResult>(result);
Assert.Equal(403, status.StatusCode);
mockService.Verify(s => s.ChatAsync(It.IsAny<AiChatRequest>()), Times.Never);
}
// ── NOMINAL ──────────────────────────────────────────────────────────
[Fact]
public async Task Chat_Success_CallsAssistantServiceAndReturns200()
{
using var db = DbContextFactory.Create();
var mockService = new Mock<IAssistantService>();
mockService.Setup(s => s.ChatAsync(It.IsAny<AiChatRequest>())).ReturnsAsync(FakeResponse);
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow,
AiTokensPerMonth = 1_000, AiUsageMonthKey = DateTime.UtcNow.ToString("yyyy-MM")
});
db.ApplicationInstances.Add(new ApplicationInstance
{
Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = true,
Languages = new List<string>()
});
db.SaveChanges();
var result = await BuildController(db, mockService).Chat(MakeRequest("i1"));
var ok = Assert.IsType<OkObjectResult>(result);
Assert.Equal(FakeResponse, ok.Value);
mockService.Verify(s => s.ChatAsync(It.IsAny<AiChatRequest>()), Times.Once);
}
}
}