feat(S05/T01): Unified workflow trust signals across the API, table, da…
- JobTrackerApi/Controllers/JobApplicationsController.cs - JobTrackerApi.Tests/JobApplicationsWorkflowSignalsTests.cs - job-tracker-ui/src/jobWorkflowSignals.ts - job-tracker-ui/src/components/JobTable.tsx - job-tracker-ui/src/components/DashboardView.tsx - job-tracker-ui/src/components/RemindersView.tsx - job-tracker-ui/src/workflow-trust-signals.test.tsx
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
using System.Security.Claims;
|
||||
using JobTrackerApi.Controllers;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
using JobTrackerApi.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace JobTrackerApi.Tests;
|
||||
|
||||
public sealed class JobApplicationsWorkflowSignalsTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Readiness_keeps_package_gap_when_notes_do_not_contain_saved_answer_block()
|
||||
{
|
||||
await using var db = CreateDb();
|
||||
var company = new Company { Name = "Acme", OwnerUserId = "user-1" };
|
||||
db.Companies.Add(company);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var job = new JobApplication
|
||||
{
|
||||
JobTitle = "Backend Developer",
|
||||
CompanyId = company.Id,
|
||||
OwnerUserId = "user-1",
|
||||
Status = "Applied",
|
||||
DateApplied = DateTime.UtcNow,
|
||||
TailoredCvText = "Saved tailored CV",
|
||||
Notes = "Interview talking points and recruiter notes without any saved application-answer markers.",
|
||||
HasPortfolio = true,
|
||||
NextAction = "Wait for recruiter reply"
|
||||
};
|
||||
db.JobApplications.Add(job);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var controller = CreateController(db, "user-1");
|
||||
var result = await controller.GetReadiness(job.Id, CancellationToken.None);
|
||||
|
||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||
var payload = Assert.IsType<JobApplicationsController.ReadinessDto>(ok.Value);
|
||||
|
||||
Assert.Equal("package-work", payload.WorkflowSignal.ActionKey);
|
||||
Assert.True(payload.WorkflowSignal.HasPackageGap);
|
||||
Assert.True(payload.WorkflowSignal.HasTailoredCv);
|
||||
Assert.False(payload.WorkflowSignal.HasSavedApplicationAnswerDraft);
|
||||
Assert.Contains(payload.Missing, item => item.Contains("Save application answers", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(payload.Reminders, item => item.Contains("tailored cv and saved application answers", StringComparison.OrdinalIgnoreCase) && item.Contains("notes", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reminders_return_normalized_workflow_routes_for_package_and_follow_up_actions()
|
||||
{
|
||||
await using var db = CreateDb();
|
||||
var company = new Company { Name = "Acme", OwnerUserId = "user-1" };
|
||||
db.Companies.Add(company);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var packageGapJob = new JobApplication
|
||||
{
|
||||
JobTitle = "Platform Engineer",
|
||||
CompanyId = company.Id,
|
||||
OwnerUserId = "user-1",
|
||||
Status = "Waiting",
|
||||
DateApplied = DateTime.UtcNow.AddDays(-20),
|
||||
Notes = "General notes only",
|
||||
TailoredCvText = null,
|
||||
ResponseReceived = false
|
||||
};
|
||||
|
||||
var followUpJob = new JobApplication
|
||||
{
|
||||
JobTitle = "API Engineer",
|
||||
CompanyId = company.Id,
|
||||
OwnerUserId = "user-1",
|
||||
Status = "Waiting",
|
||||
DateApplied = DateTime.UtcNow.AddDays(-20),
|
||||
TailoredCvText = "Saved tailored CV",
|
||||
Notes = "Prep notes\n\n<<<APPLICATION_ANSWER_DRAFT>>>\nSaved answer\n<<<END_APPLICATION_ANSWER_DRAFT>>>",
|
||||
ResponseReceived = false
|
||||
};
|
||||
|
||||
db.JobApplications.AddRange(packageGapJob, followUpJob);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var controller = CreateController(db, "user-1");
|
||||
var result = await controller.GetReminders(14, CancellationToken.None);
|
||||
|
||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||
var payload = Assert.IsType<List<JobApplicationsController.JobApplicationDto>>(ok.Value);
|
||||
|
||||
var packageReminder = Assert.Single(payload, item => item.Id == packageGapJob.Id);
|
||||
Assert.Equal("package-work", packageReminder.WorkflowSignal.ActionKey);
|
||||
Assert.Equal("tailored-cv", packageReminder.WorkflowSignal.WorkspaceTab);
|
||||
Assert.Equal(packageReminder.WorkflowSignal.Reason, packageReminder.FollowUpReason);
|
||||
Assert.True(packageReminder.WorkflowSignal.HasPackageGap);
|
||||
|
||||
var followUpReminder = Assert.Single(payload, item => item.Id == followUpJob.Id);
|
||||
Assert.Equal("follow-up", followUpReminder.WorkflowSignal.ActionKey);
|
||||
Assert.Equal("follow-up", followUpReminder.WorkflowSignal.WorkspaceTab);
|
||||
Assert.Equal("waiting-update", followUpReminder.WorkflowSignal.FollowMode);
|
||||
Assert.Equal(followUpReminder.WorkflowSignal.Reason, followUpReminder.FollowUpReason);
|
||||
Assert.True(followUpReminder.WorkflowSignal.NeedsFollowUpAction);
|
||||
}
|
||||
|
||||
private static JobApplicationsController CreateController(JobTrackerContext db, string userId)
|
||||
{
|
||||
var controller = new JobApplicationsController(db, Mock.Of<ISummarizerService>(), Mock.Of<IAppEmailSender>(), CreateUserManager().Object);
|
||||
controller.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext
|
||||
{
|
||||
User = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, userId)
|
||||
}, "test"))
|
||||
}
|
||||
};
|
||||
return controller;
|
||||
}
|
||||
|
||||
private static JobTrackerContext CreateDb()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<JobTrackerContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
var currentUser = new Mock<ICurrentUserService>();
|
||||
currentUser.SetupGet(service => service.UserId).Returns("user-1");
|
||||
return new JobTrackerContext(options, currentUser.Object);
|
||||
}
|
||||
|
||||
private static Mock<UserManager<ApplicationUser>> CreateUserManager()
|
||||
{
|
||||
var store = new Mock<IUserStore<ApplicationUser>>();
|
||||
return new Mock<UserManager<ApplicationUser>>(
|
||||
store.Object,
|
||||
Options.Create(new IdentityOptions()),
|
||||
new PasswordHasher<ApplicationUser>(),
|
||||
Array.Empty<IUserValidator<ApplicationUser>>(),
|
||||
Array.Empty<IPasswordValidator<ApplicationUser>>(),
|
||||
new UpperInvariantLookupNormalizer(),
|
||||
new IdentityErrorDescriber(),
|
||||
null!,
|
||||
new NullLogger<UserManager<ApplicationUser>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user