From 510fe42a0e8734e69ee40192198d985a470a49be Mon Sep 17 00:00:00 2001 From: ColtenOuO Date: Mon, 14 Sep 2026 17:39:34 +0000 Subject: [PATCH 1/3] Ship the grounding snippets checked at start The start handler read which snippets were checked before the sign-in round trip, but looked their text up in the extracted lists after it. A JD picked while a gated start was waiting on /api/login replaced those lists, so the stored indexes pointed into the new file and the packet carried snippets the candidate never checked. The lists are now copied alongside the selection, the way the rest of the form is read. --- tests/browser/lobby.test.js | 26 ++++++++++++++++++++++++++ web/app.js | 7 ++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/browser/lobby.test.js b/tests/browser/lobby.test.js index b479f23..093a1e6 100644 --- a/tests/browser/lobby.test.js +++ b/tests/browser/lobby.test.js @@ -973,6 +973,32 @@ lobbyTest("a start ships the length that was on screen when it was pressed", asy ); }); +lobbyTest("a start ships the grounding snippets that were checked when it was pressed", async (page) => { + session = { signedIn: false, loginRequired: true }; + await lobby(page); + await page.click("details.interview-context summary"); + const txt = (name, text) => ({ name, mimeType: "text/plain", buffer: Buffer.from(text) }); + + await page.setInputFiles("#grounding-jd", txt("first.txt", "Must know Rust")); + await page.locator('#grounding-choices input[data-group="requirements"]').first().check(); + await page.check("#grounding-consent"); + + const finishLogin = (() => { + let done; + holdLogin = new Promise((resolve) => (done = resolve)); + return done; + })(); + await page.fill("#github-login", "candidate"); + await page.click("#start"); + await page.setInputFiles("#grounding-jd", txt("second.txt", "Must know SQL")); + await settles(page, () => document.querySelector("#grounding-choices label")?.textContent.includes("SQL")); + finishLogin(); + + await page.waitForURL(/\/interview/); + const stored = await page.evaluate(() => JSON.parse(sessionStorage.getItem("codetrial.interview-grounding.v1"))); + assert.deepEqual(stored?.requirements, ["Must know Rust"]); +}); + lobbyTest("a start already on its way out is not undone by a later choice", async (page) => { // The sign-in round trip is the one window where the start handler is // suspended with the page still live under it, and everything the candidate diff --git a/web/app.js b/web/app.js index 720df78..c9b8093 100644 --- a/web/app.js +++ b/web/app.js @@ -222,6 +222,11 @@ start.addEventListener("click", async () => { if (profile.targetCompany) destination.searchParams.set("company", profile.targetCompany); const focus = nodes.practiceFocusShareInput.checked ? practiceFocus(reports) : null; const selected = checkedGrounding(); + const extracted = { + requirements: [...grounding.requirements], + skills: [...grounding.skills], + anchors: [...grounding.anchors], + }; const consented = nodes.groundingConsent.checked; starting = true; @@ -244,7 +249,7 @@ start.addEventListener("click", async () => { } start.textContent = "Starting..."; try { - const packet = selectedGroundingPacket(grounding, selected, consented); + const packet = selectedGroundingPacket(extracted, selected, consented); storeGroundingPacket(sessionStorage, packet); storeSharedFocus(sessionStorage, focus?.weakness ?? null); } catch (error) { From bf3ab318e0d0779fab279785e5ed97027ce53e4d Mon Sep 17 00:00:00 2001 From: ColtenOuO Date: Tue, 15 Sep 2026 06:34:31 +0000 Subject: [PATCH 2/3] Build the grounding packet before signing in Copying the extracted lists before the sign-in round trip kept the indices and the text together, but still assembled the packet after it, and a start the packet was always going to refuse spent a login first. The packet is now built with the rest of the form, and only stored once the sign-in has answered. --- tests/browser/lobby.test.js | 17 +++++++++++++++++ web/app.js | 15 +++++++-------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/tests/browser/lobby.test.js b/tests/browser/lobby.test.js index 093a1e6..09eaf72 100644 --- a/tests/browser/lobby.test.js +++ b/tests/browser/lobby.test.js @@ -990,8 +990,11 @@ lobbyTest("a start ships the grounding snippets that were checked when it was pr })(); await page.fill("#github-login", "candidate"); await page.click("#start"); + await page.click("#grounding-clear"); + await page.uncheck("#grounding-consent"); await page.setInputFiles("#grounding-jd", txt("second.txt", "Must know SQL")); await settles(page, () => document.querySelector("#grounding-choices label")?.textContent.includes("SQL")); + await page.locator('#grounding-choices input[data-group="requirements"]').first().check(); finishLogin(); await page.waitForURL(/\/interview/); @@ -999,6 +1002,20 @@ lobbyTest("a start ships the grounding snippets that were checked when it was pr assert.deepEqual(stored?.requirements, ["Must know Rust"]); }); +lobbyTest("a start the grounding packet refuses does not sign in first", async (page) => { + session = { signedIn: false, loginRequired: true }; + await lobby(page); + await page.click("details.interview-context summary"); + await page.setInputFiles("#grounding-jd", { name: "jd.txt", mimeType: "text/plain", buffer: Buffer.from("Must know Rust") }); + await page.locator('#grounding-choices input[data-group="requirements"]').first().check(); + + await page.fill("#github-login", "candidate"); + await page.click("#start"); + await settles(page, () => document.querySelector("#grounding-error").textContent.includes("Agree")); + assert.equal(session.signedIn, false, "no /api/login for a start that was never going to go"); + assert.equal(await page.isDisabled("#start"), false); +}); + lobbyTest("a start already on its way out is not undone by a later choice", async (page) => { // The sign-in round trip is the one window where the start handler is // suspended with the page still live under it, and everything the candidate diff --git a/web/app.js b/web/app.js index c9b8093..0855b6d 100644 --- a/web/app.js +++ b/web/app.js @@ -221,13 +221,13 @@ start.addEventListener("click", async () => { if (profile.seniority) destination.searchParams.set("seniority", profile.seniority); if (profile.targetCompany) destination.searchParams.set("company", profile.targetCompany); const focus = nodes.practiceFocusShareInput.checked ? practiceFocus(reports) : null; - const selected = checkedGrounding(); - const extracted = { - requirements: [...grounding.requirements], - skills: [...grounding.skills], - anchors: [...grounding.anchors], - }; - const consented = nodes.groundingConsent.checked; + let packet; + try { + packet = selectedGroundingPacket(grounding, checkedGrounding(), nodes.groundingConsent.checked); + } catch (error) { + nodes.groundingError.textContent = error.message; + return; + } starting = true; start.disabled = true; @@ -249,7 +249,6 @@ start.addEventListener("click", async () => { } start.textContent = "Starting..."; try { - const packet = selectedGroundingPacket(extracted, selected, consented); storeGroundingPacket(sessionStorage, packet); storeSharedFocus(sessionStorage, focus?.weakness ?? null); } catch (error) { From 813b14680421fefb0ba05bec3f633e8b0da3595c Mon Sep 17 00:00:00 2001 From: ColtenOuO Date: Tue, 15 Sep 2026 14:24:25 +0000 Subject: [PATCH 3/3] Let withdrawn grounding consent win the sign-in Building the packet before the sign-in round trip also snapshotted consent, so a candidate who cleared grounding or unchecked consent while the login was pending still had the pre-click snippets stored. Consent is now read again when the packet is stored, and a withdrawn consent stores nothing, which is what clearing did on main. The existing snapshot test unchecked a box Clear had already unchecked, so it never exercised that path. It now re-checks consent before the login resolves, and a separate test pins that withdrawing consent alone leaves no grounding in session storage. --- tests/browser/lobby.test.js | 29 ++++++++++++++++++++++++++++- web/app.js | 1 + 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/browser/lobby.test.js b/tests/browser/lobby.test.js index 09eaf72..5bbde20 100644 --- a/tests/browser/lobby.test.js +++ b/tests/browser/lobby.test.js @@ -991,10 +991,10 @@ lobbyTest("a start ships the grounding snippets that were checked when it was pr await page.fill("#github-login", "candidate"); await page.click("#start"); await page.click("#grounding-clear"); - await page.uncheck("#grounding-consent"); await page.setInputFiles("#grounding-jd", txt("second.txt", "Must know SQL")); await settles(page, () => document.querySelector("#grounding-choices label")?.textContent.includes("SQL")); await page.locator('#grounding-choices input[data-group="requirements"]').first().check(); + await page.check("#grounding-consent"); finishLogin(); await page.waitForURL(/\/interview/); @@ -1002,6 +1002,33 @@ lobbyTest("a start ships the grounding snippets that were checked when it was pr assert.deepEqual(stored?.requirements, ["Must know Rust"]); }); +lobbyTest("consent withdrawn during the sign-in wait stores no grounding", async (page) => { + session = { signedIn: false, loginRequired: true }; + await lobby(page); + await page.click("details.interview-context summary"); + await page.setInputFiles("#grounding-jd", { + name: "jd.txt", + mimeType: "text/plain", + buffer: Buffer.from("Must know Rust"), + }); + await page.locator('#grounding-choices input[data-group="requirements"]').first().check(); + await page.check("#grounding-consent"); + + const finishLogin = (() => { + let done; + holdLogin = new Promise((resolve) => (done = resolve)); + return done; + })(); + await page.fill("#github-login", "candidate"); + await page.click("#start"); + await page.uncheck("#grounding-consent"); + finishLogin(); + + await page.waitForURL(/\/interview/); + const stored = await page.evaluate(() => sessionStorage.getItem("codetrial.interview-grounding.v1")); + assert.equal(stored, null); +}); + lobbyTest("a start the grounding packet refuses does not sign in first", async (page) => { session = { signedIn: false, loginRequired: true }; await lobby(page); diff --git a/web/app.js b/web/app.js index 0855b6d..eb5e5d1 100644 --- a/web/app.js +++ b/web/app.js @@ -248,6 +248,7 @@ start.addEventListener("click", async () => { signInFirst = false; } start.textContent = "Starting..."; + if (!nodes.groundingConsent.checked) packet = null; try { storeGroundingPacket(sessionStorage, packet); storeSharedFocus(sessionStorage, focus?.weakness ?? null);