Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions tests/browser/lobby.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
19 changes: 12 additions & 7 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down Expand Up @@ -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 ?? [];
Comment thread
ColtenOuO marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guards grounding against a stale read, but the start handler still reads it across an await: selected from checkedGrounding() and consented are captured before await recordGitHubLogin(false), while selectedGroundingPacket(grounding, selected, consented) runs after it. Nothing disables the file inputs or the consent box during that round trip, so a JD picked while login is pending lands here and leaves the old indices in selected pointing at snippets the candidate never checked, and a Clear followed by a new pick ships them under a consented captured before the box was unchecked. A follow-up should build the packet before the await, the rule the handler already applies to duration and problem.

else { grounding.skills = parsed?.skills ?? []; grounding.anchors = parsed?.anchors ?? []; }
status.textContent = message;
renderGroundingChoices(retainedSelection(checkedGrounding(), kind));
}

Expand Down Expand Up @@ -302,6 +305,8 @@ function renderGroundingChoices(selected) {
}

function clearGrounding() {
groundingReads.jd++;
groundingReads.resume++;
grounding = { requirements: [], skills: [], anchors: [] };
nodes.groundingJd.value = "";
nodes.groundingResume.value = "";
Expand Down