From 62d3f5be8cda326e58c2753615e6be29ac40688e Mon Sep 17 00:00:00 2001 From: voidReq <100312344+voidReq@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:47:23 -0400 Subject: [PATCH 1/2] Apply: both sheet tabs laid out alike The internships tab gets a head card like the week's (no limit, ready / applied, Run on, How Apply works), Sort: on both, and every row the same Apply / Open / More: an assistant's row has one Apply that runs where Run on says, instead of Start here / Start on the homelab. Co-Authored-By: Claude Opus 5.5 --- docs/GUI.md | 11 ++++--- src/nuauto/gui_static/app.js | 64 +++++++++++++++++++++++------------- tests/test_gui.py | 8 ++--- tests/test_link.py | 6 +++- 4 files changed, 58 insertions(+), 31 deletions(-) diff --git a/docs/GUI.md b/docs/GUI.md index a4f95fb..0e27fad 100644 --- a/docs/GUI.md +++ b/docs/GUI.md @@ -88,7 +88,8 @@ SCREENS NUworks co-ops (the sheet's NUworks tab: the weekly limit), " internships" ("Other jobs" when internships are off: the sheet's Other jobs tab, no limits), Assistants. The live run above them. A sheet tab: (NUworks) the week's count, At most and Start (Run on: this computer / the homelab, linked), - How Start works folded; then one toolbar: Search this tab (words, all must appear, any case, in company, + How Start works folded; (the other tab, laid out the same, 2026-10-18, my request: "same buttons, same + subtext") no limit here, how many are ready and applied, Run on, How Apply works folded; then one toolbar: Search this tab (words, all must appear, any case, in company, title, notes, site, kind of work, status, pay; Ctrl+F; Esc clears), Show (kind of work, with counts), the order, Add a job. Then the tab's rows in sections (
: what is left to do open, the rest folded, each with its count; your opening and folding remembered for the session; a search opens every section @@ -105,10 +106,12 @@ SCREENS failed, Other rows; recent runs folded at the end. The other tab: Ready to apply (Approved: the assistant), Applied, Proposed, Dropped or failed, Other rows (sort: score, match %, pay, sheet order; on the homelab's page the queue's order first). - Every row: who and what, its score / match % and pay ("score 105 = 75% match + 10 Boston + 20 security" + Both tabs' rows look alike: Sort: in the toolbar, the same Apply, Open and More on each row, a date chip + (NUworks: when it closes; the other tab: when it was posted). Every row: who and what, its score / match % and pay ("score 105 = 75% match + 10 Boston + 20 security" when bonuses add to it: Start goes by score), a line about it (its site, its notes), its chips (when it closes: amber within a week, red once past; "may also want the company's site"; the queue's place), what to - do next (Apply: after a confirm just that row, `nuauto apply --row N`; Start assistant; Mark done; Submit + do next (Apply: after a confirm just that row, `nuauto apply --row N`; on an assistant's row Apply starts the + assistant, where Run on says; Mark done; Submit on NUworks too), Open, and More (⋯; on a phone in the row's top right corner): on a sent row Mark accepted / Mark rejected (or Back to Applied: what you heard back, sheet.set_outcome), I applied myself, Drop (not applying: closed, a duplicate, a bad location; sheet.drop: Failed, "Dropped : " in Notes; @@ -124,7 +127,7 @@ SCREENS (remote_assist): "assistant running on the homelab: open" / "...: waiting for you: open" (its screen), from `nuauto assist sessions` over ssh every minute (never in view or demo mode); on the homelab's own page and the laptop linked to it, each row's place in the queue as a chip (review.queue_view: starts next by itself / - ready / what it waits for), Start now / Start on the homelab (refused with the reason)). + ready / what it waits for), Apply there (on its page, or Run on: the homelab; refused with the reason)). Assistants: the one line (assistants-bar: "Assistants: N of 3 running here · M on the homelab · K wait for you (Today lists them)"; on the homelab's page also the queue's: what starts by itself, its hours, free slots, NUworks applications left), then every session running now, here and on the homelab: the job, its diff --git a/src/nuauto/gui_static/app.js b/src/nuauto/gui_static/app.js index 800a6ba..5186d8c 100644 --- a/src/nuauto/gui_static/app.js +++ b/src/nuauto/gui_static/app.js @@ -993,7 +993,7 @@ const NU_SECTIONS = [ ["rest", "Other rows", null], ]; const OTHER_SECTIONS = [ - ["ready", "Ready to apply", "The assistant (Claude) applies on the company's site with you, and asks you before anything is submitted. No weekly or total limit here."], + ["ready", "Ready to apply", "Apply starts the assistant (Claude) on that job's own site; nothing is submitted without your review."], ["applied", "Applied", null], ["accepted", "Accepted", null], ["rejected", "Rejected", null], @@ -1086,7 +1086,7 @@ screens.apply = async (view) => { const sel = el("select", { testid: "apply-sort", "aria-label": tab === "nuworks" ? "Apply in this order" : "Sort the list by", title: tab === "nuworks" ? "Start (and At most) goes down Ready to apply in this order." : null, onchange: (e) => { sessionStorage.setItem("applySort:" + tab, e.target.value); if (tab === "nuworks") reload(); else drawPane(); } }, - ...SORT_OPTS[tab].map((k) => el("option", { value: k, text: (tab === "nuworks" ? "Order: " : "Sort: ") + SORTS[k][0], selected: k === how }))); + ...SORT_OPTS[tab].map((k) => el("option", { value: k, text: "Sort: " + SORTS[k][0], selected: k === how }))); fill(bar, el("div", { class: "row toolbar", testid: "apply-toolbar" }, find, cat, sel, el("button", { class: "btn", testid: "add-job", text: "Add a job", onclick: () => addJob(tab === "other" ? "other" : "nuworks", (t) => { tab = t; sessionStorage.setItem("applyTab", t); reload(); }) }))); } @@ -1095,7 +1095,7 @@ screens.apply = async (view) => { drawnRunning = running(); drawBar(); const d = data[tab]; - fill(headBox, tab === "nuworks" && ok(d) ? weekCard(d) : null); + fill(headBox, !ok(d) ? null : tab === "nuworks" ? weekCard(d) : tab === "other" ? otherCard(d) : null); if (tab === "assistants") return fill(listBox, assistantsPane(sessions())); if (!d) return fill(listBox, el("p", {}, el("span", { class: "spinner" }))); if (d.error) return fill(listBox, el("div", { class: "note fail", text: d.error })); @@ -1149,7 +1149,7 @@ screens.apply = async (view) => { chips = [dueChip(r)]; about = r.host; next = assistantStart(d, r, "jobs", `assist-${r.row}`, { body: { row: r.row } }); - open = linkOut(r.target, "Open site"); + open = linkOut(r.target, "Open"); } else if (key === "manual") { // the assistant works it on NUworks itself (your cover letter, the transcript...) chips = [dueChip(r)]; about = r.notes || r.why; @@ -1164,10 +1164,11 @@ screens.apply = async (view) => { next = el("button", { class: "btn small primary", text: "Submit on NUworks too", testid: `retry-btn-${r.row}`, onclick: () => confirmBox("Submit on NUworks too?", `${r.company} · ${r.title} is Applied on the company site. This submits the same job on NUworks.`, "Submit on NUworks", () => action("nuworks_side", runArgs({ row: r.row }))) }); - } else if (key === "ready") { - about = [r.posted_text, r.host || r.why].filter(Boolean).join(" · "); + } else if (key === "ready") { // (when it was posted where NUworks rows say when they close) + chips = [r.posted_text ? el("span", { class: "chip", testid: "posted", text: r.posted_text }) : null]; + about = r.host || r.why; next = assistantStart(d, r, "other", `other-assist-${r.row}`, { disabled: !r.target, body: { row: r.row, tab: "other" } }); - open = r.target ? linkOut(r.target, "Open site") : null; + open = r.target ? linkOut(r.target, "Open") : null; } else { about = [SENT.includes(r.status) && r.date ? `applied ${r.date}` : null, r.notes ? r.notes.slice(0, 140) : null].filter(Boolean).join(" · "); const nu = r.nuworks; // what NUworks said at the last check (the twice-daily update) @@ -1207,9 +1208,7 @@ screens.apply = async (view) => { el("h2", { text: w.room ? `You can apply to ${plural(w.room, "more job")} this week` : "Weekly limit reached" }), el("p", { class: "small muted" }, `${w.applied} of ${w.max} ${w.label} · ${w.total} of ${w.max_total} in total` + (w.next ? ` · new week ${w.next}` : "") + " · ", el("a", { href: "#/settings", text: "change the limit" }))), drawnRunning ? null : el("div", { class: "row" }, - canRunThere() ? el("div", { class: "seg", role: "group", "aria-label": "Run on", testid: "run-on" }, - ...[["here", "This computer"], ["homelab", "The homelab"]].map(([k, label]) => el("button", { "aria-pressed": String(runOn() === k), - testid: `run-on-${k}`, text: label, onclick: () => { sessionStorage.setItem("runOn", k); drawPane(); } }))) : null, + runOnSwitch(), el("label", { class: "small muted" }, "At most ", atMost), el("button", { class: "btn primary big", testid: "btn-start-apply", disabled: !!d.why_not || !!(S.task && S.task.state === "running"), text: runOn() === "homelab" ? "Start on the homelab" : "Start applying", onclick: startApply }))), @@ -1219,6 +1218,26 @@ screens.apply = async (view) => { el("p", { class: "small muted", text: (onHomelab() ? "Approved in the sheet is your go-ahead. Each job is filled and submitted in a browser on the homelab, which you watch and use from here (its link shows when it starts), one at a time, 30–60 s apart. Only one machine applies at a time." : "Approved in the sheet is your go-ahead. Each job is filled and submitted in a visible Firefox window, one at a time, 30–60 s apart.") + " Every form is screenshotted before Submit. Questions without a saved answer come up here. A job that sends you to the company's own site, or needs something only you can give (a cover letter, a transcript), stops (nothing submitted) and moves to Company site needed or By hand on NUworks below. Stop works like Ctrl+C." }))); } + // Run on: this computer / the homelab (the laptop linked to it): where Start, and each row's Apply, run. Both tabs. + function runOnSwitch() { + return canRunThere() ? el("div", { class: "seg", role: "group", "aria-label": "Run on", testid: "run-on" }, + ...[["here", "This computer"], ["homelab", "The homelab"]].map(([k, label]) => el("button", { "aria-pressed": String(runOn() === k), + testid: `run-on-${k}`, text: label, onclick: () => { sessionStorage.setItem("runOn", k); drawPane(); } }))) : null; + } + + // The other tab's head, laid out like the week's: no limit here, how many are ready and sent, Run on, how Apply works + function otherCard(d) { + const s = d.sections, sent = s.applied.length + s.accepted.length + s.rejected.length; + const where = onHomelab() ? "in a browser on the homelab, whose screen you watch and answer from here" : + "in its own terminal window and browser here" + (canRunThere() ? " (Run on: the homelab: on the homelab, you answer it on its screen)" : ""); + return el("div", { class: "card", testid: "apply-other-head" }, + el("h2", { text: "You can apply to as many as you like" }), + el("p", { class: "small muted", text: `${s.ready.length} ready · ${sent} applied · no weekly or total limit here` }), + canRunThere() ? el("div", { class: "row" }, runOnSwitch()) : null, + el("details", { class: "how" }, el("summary", { text: "How Apply works" }), + el("p", { class: "small muted", text: `Approved in the sheet is your go-ahead. Apply on a row starts the assistant (Claude) on that job's own site, ${where}; up to ${d.slots || 1} run side by side. It fills what your saved answers cover, guesses the rest and records each guess, then asks you everything at once before Submit. Nothing is submitted without your review.` }))); + } + function pastCard(d) { if (!d.history.length) return null; const box = el("details", { class: "card section", testid: "section-nuworks-runs" }, @@ -1436,7 +1455,7 @@ function assistantsPane({ d, here, there }) { // ---- The assistant's Start on a row (Apply's Company site needed, and the Other jobs tab's rows): // up to d.slots run side by side, each in its own terminal window and browser; a row with one running says so instead // of offering another; with every slot in use, Start waits. On the homelab's page (and the laptop linked to it): the -// queue's place for the row, and Start on the homelab. +// queue's place for the row; its Apply runs where Run on says (the homelab's page: there). function assistantStart(d, r, tab, testid, args) { // the homelab's browser was refused there (on the homelab's own page the queue's chip says so) if (r.on_laptop && !onHomelab() && !(d.homelab || []).find((s) => s.tab === tab && s.row === r.row)) { @@ -1471,25 +1490,26 @@ function assistantStartButton(d, r, tab, testid, args) { : wait ? `assistant waiting for you (slot ${running.slot})` : `assistant running (slot ${running.slot})` }); } // the homelab's queue (its own page, or the laptop's app through the link): this row's place in it - const q = S.queue && S.queue.rows ? S.queue.rows[`${tab}-${r.row}`] : null; + const q = S.queue && S.queue.rows && (onHomelab() || runOn() === "homelab") ? S.queue.rows[`${tab}-${r.row}`] : null; // (Run on: here: no queue) const said = q && q.group === "laptop" && r.on_laptop && !onHomelab(); // (the "do it on the laptop" chip says it already) const chip = q && (q.group === "next" || !q.start) && !said ? el("span", { class: "chip" + (q.group === "next" ? " accent" : q.group === "check" || q.group === "laptop" ? " warn" : ""), testid: `${testid}-queue`, text: q.text }) : null; - // Start on the homelab: there (its own Start now) or from the laptop's app (its link); the queue says when it can't + // Apply, the same button as a NUworks row's: on the homelab when this is its page, or Run on says the homelab (the + // laptop's app, through its link; the queue says when it can't); otherwise here const there = onHomelab() || d.linked && S.queue; - const startThere = there ? el("button", { class: "btn small" + (onHomelab() ? " primary" : ""), testid: onHomelab() ? testid : `${testid}-homelab-start`, - text: onHomelab() ? "Start now" : "Start on the homelab", disabled: args.disabled || !!(q && !q.start), - title: q && !q.start ? q.text : "Starts its session on the homelab now (you answer it on its screen)", onclick: async (e) => { + if (there && (onHomelab() || runOn() === "homelab")) return el("span", { class: "row" }, chip, el("button", { class: "btn small", + testid: onHomelab() ? testid : `${testid}-homelab-start`, text: "Apply", disabled: args.disabled || !!(q && !q.start), + title: q && !q.start ? q.text : "Starts the assistant on the homelab now, for just this job (you answer it on its screen)", onclick: async (e) => { const btn = e.currentTarget; btn.disabled = true; btn.textContent = "Starting…"; const ok = await action("assist", onHomelab() ? args.body : { ...args.body, on: "homelab" }); setTimeout(() => { if (S.screen && S.screen.reload) S.screen.reload(); }, ok ? 9000 : 0); // its session shows up in a few seconds - } }) : null; - if (onHomelab()) return el("span", { class: "row" }, chip, startThere); + } })); const full = (d.sessions || []).length >= (d.slots || 1); - const startHere = el("button", { class: "btn small primary", text: there ? "Start here" : "Start assistant", testid, disabled: full || (args.disabled || false), - title: full ? `${d.slots} assistants are running (the most at once): finish one first` : "", onclick: (e) => { + const startHere = el("button", { class: "btn small", text: "Apply", testid, disabled: full || (args.disabled || false), + title: full ? `${d.slots} assistants are running (the most at once): finish one first` : + "Starts the assistant (Claude) here, for just this job; it asks you before anything is submitted", onclick: (e) => { // a few seconds greyed out: a second click would start the same job twice (and each start reads the sheet) const btn = e.currentTarget; const label = btn.textContent; @@ -1498,7 +1518,7 @@ function assistantStartButton(d, r, tab, testid, args) { setTimeout(() => { if (btn.isConnected) { btn.disabled = false; btn.textContent = label; } }, 5000); action("assist", args.body); } }); - return there ? el("span", { class: "row" }, chip, startHere, startThere) : startHere; + return chip ? el("span", { class: "row" }, chip, startHere) : startHere; } // One line at the top of Apply's Assistants tab, the same on both machines: how many assistants run (here, and on the homelab), @@ -2105,7 +2125,7 @@ function linkRow(done) { el("button", { class: "btn small" + (l.on ? "" : " primary"), testid: "ha-link-toggle", text: l.on ? "Unlink" : "Link this app", onclick: async () => { if (await action(l.on ? "homelab_unlink" : "homelab_link")) { await pollState(); done(); } } })), el("p", { class: "small muted", text: l.on ? - "The homelab's assistant sessions open here (approve its Submit, reply), and Start on the homelab and Run on: the homelab work from this window." : + "The homelab's assistant sessions open here (approve its Submit, reply), and Run on: the homelab (Start, and each row's Apply) works from this window." : "Link it to start sessions and Apply runs on the homelab from this window, and open its sessions here instead of in your browser. It pairs this app as one of the page's devices (over ssh, once)." }), l.on && l.error ? el("div", { class: "note warn small", testid: "ha-link-error", text: l.error }) : null); } diff --git a/tests/test_gui.py b/tests/test_gui.py index a537792..5d36428 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -689,7 +689,7 @@ def eventually(check, what, seconds=20): applied.locator("summary").click() page.click("[data-testid=tab-other]") page.wait_for_selector("[data-testid=section-other-ready]") # the Other jobs tab is the second tab of Apply - # a row marked "do it on the laptop" shows a chip next to its Start assistant button (the others do not) + # a row marked "do it on the laptop" shows a chip next to its Apply button (the others do not) ready_now = get("/api/other")["ready"] assert ready_now, "the demo's Other jobs tab has an Approved row here" target = ready_now[0]["row"] @@ -737,7 +737,7 @@ def eventually(check, what, seconds=20): # the order menu IS the order Start goes: the server sorts (apply.apply_order), the numbers follow it start_order = page.locator("[data-testid=approved-list] li").evaluate_all("ls => ls.map(l => l.dataset.testid)") assert start_order == [f"job-nuworks-{r['row']}" for r in jobs["sections"]["approved"]], start_order - assert page.locator("[data-testid=apply-sort] option[value=default]").inner_text().startswith("Order: closing within a week") + assert page.locator("[data-testid=apply-sort] option[value=default]").inner_text().startswith("Sort: closing within a week") assert page.get_attribute("[data-testid=apply-sort]", "aria-label") == "Apply in this order" assert page.get_attribute("[data-testid=apply-search]", "placeholder") == "Search this tab (Ctrl+F)" page.select_option("[data-testid=apply-sort]", "match") @@ -793,7 +793,7 @@ def eventually(check, what, seconds=20): assert rows["Quarry Hardware"] == "Needs Human", rows # the external job stopped (nothing submitted) and now waits on the same page, ready for the assistant page.click("[data-testid=tab-nuworks]") - page.wait_for_selector("[data-testid=section-nuworks-agent] li:has-text('Quarry Hardware') button:has-text('Start assistant')") + page.wait_for_selector("[data-testid=section-nuworks-agent] li:has-text('Quarry Hardware') button:text-is('Apply')") assert page.locator("[data-testid=approved-list]").count() == 0 # nothing approved is left quarry = page.inner_text("[data-testid=section-nuworks-agent] li:has-text('Quarry Hardware') [data-testid=job-facts]") assert "68% match" in quarry and "pay " in quarry, quarry # match / pay on the company-site rows too @@ -991,7 +991,7 @@ def log_message(self, *a): page.click("[data-testid=tab-other]") dune = page.locator("[data-testid=section-other-ready] li:has-text('Dune Optics')") dune.wait_for() - dune.locator("button:has-text('Start assistant')").click() + dune.locator("button:text-is('Apply')").click() page.wait_for_selector("[data-testid=terminal-command]") assert page.inner_text("[data-testid=terminal-command]").endswith("assist other 3") page.keyboard.press("Escape") diff --git a/tests/test_link.py b/tests/test_link.py index da3405d..761989c 100644 --- a/tests/test_link.py +++ b/tests/test_link.py @@ -271,7 +271,11 @@ def ended(): assert "on the homelab" in page.inner_text("#view") page.click("[data-testid=as-approve]") # approved from the laptop's window page.wait_for_selector("[data-testid=as-approval]:has-text('Approved: it is submitting now')", timeout=30000) - # Company site needed: the homelab's queue on each row, Start on the homelab next to Start here (the other tab's ready rows too) + # Company site needed: the homelab's queue on each row; with Run on: the homelab, each row's Apply starts it there + # (the other tab's ready rows too) + page.goto(base + "/#/apply") + page.click("[data-testid=tab-other]") + page.click("[data-testid=apply-other-head] [data-testid=run-on-homelab]") # (the same switch as Start's) page.goto(base + "/#/company") page.wait_for_selector("[data-testid=section-nuworks-agent]") page.wait_for_selector("[data-testid$=-homelab-start]") From 49daa12424308c7a3c0a76629d0af5cd9690a2de Mon Sep 17 00:00:00 2001 From: voidReq <100312344+voidReq@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:56:11 -0400 Subject: [PATCH 2/2] Tests never read the real local/; Apply draws only its newest reload test_deploy, test_health, test_jobs, test_mail and test_sheet read the laptop's own local_config.json (its weekly limit, its accounts' email) and failed there: each now uses a temp NUAUTO_STATE_DIR like the others. Apply's lists: two reloads at once (a change's own and the sheet-changed one) could answer out of order, the older one drawn last, so a row just added or moved stayed missing until the next change (test_gui's flaky Ember Labs waits on CI). Only the newest is drawn. Co-Authored-By: Claude Opus 5.5 --- src/nuauto/gui_static/app.js | 3 +++ tests/test_deploy.py | 4 +++- tests/test_health.py | 8 +++++--- tests/test_jobs.py | 10 +++++++--- tests/test_mail.py | 4 +++- tests/test_sheet.py | 8 ++++++-- 6 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/nuauto/gui_static/app.js b/src/nuauto/gui_static/app.js index 5186d8c..12e93b0 100644 --- a/src/nuauto/gui_static/app.js +++ b/src/nuauto/gui_static/app.js @@ -1036,8 +1036,11 @@ screens.apply = async (view) => { pane = el("div", { role: "tabpanel", id: "apply-pane", testid: "apply-pane" }, headBox, bar, listBox); fill(view, runBox, tabs, pane); + let reloads = 0; // the newest reload wins: an older one answered last (read before a change) would undo it async function reload() { + const mine = ++reloads; const [n, o] = await Promise.allSettled([api.get(`/api/jobs?tab=nuworks&order=${sortOf("nuworks")}`), api.get("/api/jobs?tab=other")]); + if (mine !== reloads) return; data.nuworks = n.status === "fulfilled" ? n.value : { error: n.reason.message }; data.other = o.status === "fulfilled" ? o.value : { error: o.reason.message }; for (const [t, d] of Object.entries(data)) { // each row's key in the homelab's queue (Sort: the queue's order) diff --git a/tests/test_deploy.py b/tests/test_deploy.py index 1ac2099..a102bfa 100644 --- a/tests/test_deploy.py +++ b/tests/test_deploy.py @@ -7,7 +7,9 @@ import subprocess import tempfile -from nuauto import config, deploy, doctor, sync +os.environ["NUAUTO_STATE_DIR"] = tempfile.mkdtemp(prefix="nuauto-test-deploy-") # never the real local/ (its limits, its accounts) + +from nuauto import config, deploy, doctor, sync # noqa: E402 TMP = tempfile.mkdtemp() REMOTE_DIR = os.path.join(TMP, "github") diff --git a/tests/test_health.py b/tests/test_health.py index f9b3977..21fac98 100644 --- a/tests/test_health.py +++ b/tests/test_health.py @@ -10,9 +10,11 @@ import time from datetime import date, datetime, timedelta -from nuauto import config -from nuauto import health -from nuauto import sheet +os.environ["NUAUTO_STATE_DIR"] = tempfile.mkdtemp(prefix="nuauto-test-health-") # never the real local/ (its limits, its accounts) + +from nuauto import config # noqa: E402 +from nuauto import health # noqa: E402 +from nuauto import sheet # noqa: E402 tmp = tempfile.mkdtemp() local = os.path.join(tmp, "local") diff --git a/tests/test_jobs.py b/tests/test_jobs.py index 5c6597c..4117dc4 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -1,9 +1,13 @@ """Offline checks for jobs.py (hard rules, pool, model) and sheet.add_proposed. Run: python test_jobs.py""" from datetime import date +import os +import tempfile -from nuauto import jobs -from nuauto import sheet -from nuauto.sheet import HEADERS +os.environ["NUAUTO_STATE_DIR"] = tempfile.mkdtemp(prefix="nuauto-test-jobs-") # never the real local/ (its limits, its accounts) + +from nuauto import jobs # noqa: E402 +from nuauto import sheet # noqa: E402 +from nuauto.sheet import HEADERS # noqa: E402 TODAY = date(2026, 10, 2) diff --git a/tests/test_mail.py b/tests/test_mail.py index a000868..2e645a0 100644 --- a/tests/test_mail.py +++ b/tests/test_mail.py @@ -11,7 +11,9 @@ import tempfile import time -from nuauto import config +os.environ["NUAUTO_STATE_DIR"] = tempfile.mkdtemp(prefix="nuauto-test-mail-") # never the real local/ (its limits, its accounts) + +from nuauto import config # noqa: E402 tmp = tempfile.mkdtemp() config.LOCAL_DIR = os.path.join(tmp, "local") # never the real local/ (the Gmail login lives there) diff --git a/tests/test_sheet.py b/tests/test_sheet.py index 79be500..cb58f90 100644 --- a/tests/test_sheet.py +++ b/tests/test_sheet.py @@ -1,8 +1,12 @@ """Offline checks for sheet.py. Run: python test_sheet.py""" from datetime import date +import os +import tempfile -from nuauto import sheet -from nuauto.sheet import HEADERS, LimitReached, SheetError, parse_rows, check_limits +os.environ["NUAUTO_STATE_DIR"] = tempfile.mkdtemp(prefix="nuauto-test-sheet-") # never the real local/ (its limits, its accounts) + +from nuauto import sheet # noqa: E402 +from nuauto.sheet import HEADERS, LimitReached, SheetError, parse_rows, check_limits # noqa: E402 TODAY = date(2026, 10, 1)