// Mocks the admin API via cy.intercept — no live backend. const adminBody = { id: 1, email: "ops@example.com", name: "Ops", createdAt: "2026-08-01T00:00:00.000Z", lastLoginAt: "2026-08-28T09:00:00.000Z", }; function suggestionGroups() { return [ { techStepKey: "simmer", suggestions: [ { id: 11, techStepKey: "simmer", locale: "fr", suggestedSynonyms: ["frémir"], suggestedUtterances: ["laisser cuire tout doucement"], sourceType: "correction", status: "pending", createdAt: "2026-08-20T00:00:00.000Z", sourceCorrection: { id: 5, recipeId: 2, stepId: 7, clauseText: "faire mijoter la sauce", previousTechStepKey: "cook", correctedTechStepKey: "simmer", }, }, ], }, ]; } function corrections() { return [ { id: 5, recipeId: 2, stepId: 7, stepDescription: "Faire mijoter la sauce 20 min.", clauseText: "faire mijoter la sauce", start: 0, end: 21, previousTechStepKey: "cook", correctedTechStepKey: "simmer", createdAt: "2026-08-20T00:00:00.000Z", consumedAt: null, }, { id: 6, recipeId: 3, stepId: 9, stepDescription: "Réserver au frais.", clauseText: "Réserver au frais", start: 0, end: 17, previousTechStepKey: "setAside", correctedTechStepKey: null, createdAt: "2026-08-19T00:00:00.000Z", consumedAt: null, }, ]; } describe("Admin corrections triage", () => { beforeEach(() => { cy.viewport(1400, 1000); cy.intercept("GET", "**/admin/auth/me", { statusCode: 200, body: adminBody }); cy.intercept("GET", "**/admin/tech-steps/suggestions*", { statusCode: 200, body: suggestionGroups(), }).as("getSuggestions"); cy.intercept("GET", "**/admin/tech-steps/corrections*", { statusCode: 200, body: corrections(), }).as("getCorrections"); }); it("shows the caveat, groups suggestions by technique, and applies one", () => { cy.intercept("PATCH", "**/admin/tech-steps/suggestions/11", { statusCode: 200, body: { ...suggestionGroups()[0].suggestions[0], status: "applied" }, }).as("patch"); cy.visit("/corrections"); cy.wait("@getSuggestions"); cy.contains(".corrections-caveat", "training_data.py").should("be.visible"); cy.contains(".suggestion-group h2", "simmer").should("be.visible"); cy.contains(".suggestion-card", "faire mijoter la sauce").should( "contain.text", "cook → simmer", ); cy.contains(".suggestion-card button", "Appliquer").click(); cy.wait("@patch").its("request.body").should("deep.equal", { status: "applied" }); }); it("generates a training_data.py snippet", () => { cy.intercept("GET", "**/admin/tech-steps/training-data-snippet*", { statusCode: 200, body: { techStepKey: "simmer", locale: "fr", status: "applied", suggestionCount: 2, synonyms: ["frémir", "réduire"], utterances: [], snippet: '# simmer (fr) — 2 suggestion(s) "applied"\n"synonyms": [\n "frémir",\n "réduire",\n],', }, }).as("getSnippet"); cy.visit("/corrections"); cy.get(".corrections-panel input").type("simmer"); cy.contains(".corrections-panel button", "Générer").click(); cy.wait("@getSnippet"); cy.get(".corrections-snippet").should("contain.value", '"synonyms": ['); }); it("runs the F1 gate and shows the result", () => { cy.intercept("POST", "**/admin/tech-steps/retrain", { statusCode: 200, body: { f1: 0.83, precision: 0.8, recall: 0.86, minF1: 0.8, gatePassed: true, backfilled: { total: 120, changed: 4 }, marked: { applied: 0, rejected: 0 }, }, }).as("retrain"); cy.visit("/corrections"); cy.contains(".corrections-panel--retrain button", "Lancer").click(); cy.wait("@retrain"); cy.contains(".retrain-result", "F1 0.830") .should("have.class", "retrain-result--ok") .and("contain.text", "4/120"); }); it("lists raw corrections including the removals, on the second tab", () => { cy.visit("/corrections"); cy.contains(".corrections-tabs button", "Corrections brutes").click(); cy.wait("@getCorrections"); cy.get(".corrections-table tbody tr").should("have.length", 2); cy.contains(".corrections-table tr", "Réserver au frais").should( "contain.text", "setAside → ∅", ); }); });