From 0f79ad5588d7b3478e926e93014fdd7695b1b8bb Mon Sep 17 00:00:00 2001 From: ColtenOuO Date: Mon, 14 Sep 2026 12:13:39 +0000 Subject: [PATCH] Drop a grounding read that a newer one replaced A file read that was still in flight committed its result whenever it landed. Picking a second JD before the first finished reading could leave the first one's snippets under the second file's name, and a resume read when Clear was pressed brought the cleared snippets back. Each document kind now counts its reads and Clear bumps both, so a read that is no longer the latest one is dropped without touching the lists or the status line. --- tests/browser/lobby.test.js | 56 +++++++++++++++++++++++++++++++++++++ web/app.js | 19 ++++++++----- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/tests/browser/lobby.test.js b/tests/browser/lobby.test.js index 42f4737..b479f23 100644 --- a/tests/browser/lobby.test.js +++ b/tests/browser/lobby.test.js @@ -394,6 +394,62 @@ lobbyTest("loading a resume keeps the JD requirements already checked", async (p assert.deepEqual(await jd.evaluateAll((boxes) => boxes.map((box) => box.checked)), [false, true]); }); +async function holdSlowRead(page) { + await page.evaluate(() => { + const read = Blob.prototype.arrayBuffer; + let open; + const gate = new Promise((resolve) => { open = resolve; }); + let done = null; + Blob.prototype.arrayBuffer = async function () { + if (this.name === "slow.txt") { + await gate; + done = read.call(this); + return done; + } + return read.call(this); + }; + window.releaseSlowRead = async () => { + open(); + for (let wait = 0; !done && wait < 1000; wait++) await new Promise((resolve) => setTimeout(resolve, 10)); + if (!done) throw new Error("the gated read never started"); + await done; + await new Promise((resolve) => setTimeout(resolve, 0)); + }; + }); + return () => page.evaluate(() => window.releaseSlowRead()); +} + +const groundingTxt = (name, text) => ({ name, mimeType: "text/plain", buffer: Buffer.from(text) }); +const groundingText = (page) => + page.evaluate(() => [...document.querySelectorAll("#grounding-choices label")].map((row) => row.textContent.trim())); + +lobbyTest("a slower read of an earlier JD does not replace the one picked after it", async (page) => { + await lobby(page); + await page.click("details.interview-context summary"); + const release = await holdSlowRead(page); + + await page.setInputFiles("#grounding-jd", groundingTxt("slow.txt", "Must know Rust")); + await page.setInputFiles("#grounding-jd", groundingTxt("jd.txt", "Must know SQL")); + await settles(page, () => document.querySelector("#grounding-choices label") !== null); + await release(); + + assert.deepEqual(await groundingText(page), ["Must know SQL"]); + assert.match(await page.locator("#grounding-jd-status").textContent(), /^Parsed locally/); +}); + +lobbyTest("a resume still reading when grounding is cleared stays cleared", async (page) => { + await lobby(page); + await page.click("details.interview-context summary"); + const release = await holdSlowRead(page); + + await page.setInputFiles("#grounding-resume", groundingTxt("slow.txt", "Skills: Rust, Go\nBuilt a parser")); + await page.click("#grounding-clear"); + await release(); + + assert.deepEqual(await groundingText(page), []); + assert.equal(await page.locator("#grounding-resume-status").textContent(), ""); +}); + lobbyTest("a manually selected problem still receives the arriving practice focus", async (page) => { reports = [focusedAttempt(EASY[0])]; const release = await heldLobby(page); diff --git a/web/app.js b/web/app.js index 2361c0f..720df78 100644 --- a/web/app.js +++ b/web/app.js @@ -153,6 +153,7 @@ for (const input of levels) { } let grounding = { requirements: [], skills: [], anchors: [] }; +const groundingReads = { jd: 0, resume: 0 }; nodes.groundingJd.addEventListener("change", () => loadGroundingFile("jd")); nodes.groundingResume.addEventListener("change", () => loadGroundingFile("resume")); @@ -259,17 +260,19 @@ start.addEventListener("click", async () => { async function loadGroundingFile(kind) { const input = kind === "jd" ? nodes.groundingJd : nodes.groundingResume; const status = kind === "jd" ? nodes.groundingJdStatus : nodes.groundingResumeStatus; + const read = ++groundingReads[kind]; status.textContent = "Reading locally..."; + let parsed = null; + let message = "Parsed locally. Select only snippets you want to send."; try { - const parsed = await parseGroundingFile(input.files[0], kind); - if (kind === "jd") grounding.requirements = parsed.requirements; - else ({ skills: grounding.skills, anchors: grounding.anchors } = parsed); - status.textContent = "Parsed locally. Select only snippets you want to send."; + parsed = await parseGroundingFile(input.files[0], kind); } catch (error) { - if (kind === "jd") grounding.requirements = []; - else { grounding.skills = []; grounding.anchors = []; } - status.textContent = error.message; + message = error.message; } + if (read !== groundingReads[kind]) return; + if (kind === "jd") grounding.requirements = parsed?.requirements ?? []; + else { grounding.skills = parsed?.skills ?? []; grounding.anchors = parsed?.anchors ?? []; } + status.textContent = message; renderGroundingChoices(retainedSelection(checkedGrounding(), kind)); } @@ -302,6 +305,8 @@ function renderGroundingChoices(selected) { } function clearGrounding() { + groundingReads.jd++; + groundingReads.resume++; grounding = { requirements: [], skills: [], anchors: [] }; nodes.groundingJd.value = ""; nodes.groundingResume.value = "";