惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Register - Security
The Register - Security
云风的 BLOG
云风的 BLOG
U
Unit 42
F
Fortinet All Blogs
The GitHub Blog
The GitHub Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
D
Docker
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
S
Secure Thoughts
Hacker News: Ask HN
Hacker News: Ask HN
Vercel News
Vercel News
S
Security @ Cisco Blogs
GbyAI
GbyAI
Stack Overflow Blog
Stack Overflow Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
I
Intezer
MongoDB | Blog
MongoDB | Blog
AI
AI
MyScale Blog
MyScale Blog
Engineering at Meta
Engineering at Meta
Y
Y Combinator Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Proofpoint News Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
W
WeLiveSecurity
博客园 - 叶小钗
S
SegmentFault 最新的问题
N
News | PayPal Newsroom
WordPress大学
WordPress大学
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
D
DataBreaches.Net
小众软件
小众软件
Microsoft Azure Blog
Microsoft Azure Blog
Spread Privacy
Spread Privacy
H
Help Net Security
美团技术团队
博客园 - 司徒正美
T
Threat Research - Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
K
Kaspersky official blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
V
Vulnerabilities – Threatpost
TaoSecurity Blog
TaoSecurity Blog
N
Netflix TechBlog - Medium
L
Lohrmann on Cybersecurity
J
Java Code Geeks
量子位
Martin Fowler
Martin Fowler
博客园_首页

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4 test: trim doctor command hotspots · openclaw/openclaw@c66f16a test: isolate agent auth and spawn hotspots · openclaw/openclaw@9285935 test: stabilize MCP startup disposal race · openclaw/openclaw@dd9d2eb test: merge browser contract server suites · openclaw/openclaw@5817a76 test: narrow ollama provider discovery setup · openclaw/openclaw@a0d9598 build: declare qa-lab aimock runtime dependency · openclaw/openclaw@24431e5 test: speed up safe-bins exec harness · openclaw/openclaw@ee856ab test: preserve tool helpers in embedded runner mocks · openclaw/openclaw@acd86a0 refactor: move memory embeddings into provider plugins · openclaw/openclaw@77e6e4c test: reuse system-run temp fixtures · openclaw/openclaw@7e9ff0f test: trim hotspot wait overhead · openclaw/openclaw@12a59b0 Check: avoid duplicate boundary prep · openclaw/openclaw@baf11b8 test: reduce hotspot fixture overhead · openclaw/openclaw@3a59edd feat(ui): overhaul settings and slash command UX (#67819) thanks @Bun… · openclaw/openclaw@2cfb660 QA Matrix: exit cleanly on failure · openclaw/openclaw@42805d2 QA Matrix: isolate scenario coverage · openclaw/openclaw@7e659e1 Matrix: refresh crypto bootstrap state · openclaw/openclaw@94081d8 QA Lab: add provider registry · openclaw/openclaw@bb7e982 Matrix: add plugin changelog · openclaw/openclaw@4acab55 test: trim more hotspot overhead · openclaw/openclaw@f485311 test: trim remaining hotspot tests · openclaw/openclaw@6ba8626 test: narrow hotspot mocks · openclaw/openclaw@dbc8179 test: isolate gemini embedding request helpers · openclaw/openclaw@cd330f5 test: trim memory and mcp hotspots · openclaw/openclaw@fd48dfa test: slim provider registry mocks · openclaw/openclaw@2e08c77 test: harden Parallels update smoke · openclaw/openclaw@1a98090 feat: default Anthropic to Opus 4.7 · openclaw/openclaw@628b454 fix: harden node-host shell payload mutability checks · openclaw/openclaw@75c551e fix: land node-host approval binding for native binaries (#66731) (th… · openclaw/openclaw@29919bb CI: add daily schedule to CodeQL workflow (#67645) · openclaw/openclaw@69d25f5 fix(gateway): capture config hash after plugin auto-enable to prevent… · openclaw/openclaw@8c11210 fix: repair sanitized replay tool results before send (#67620) (thank… · openclaw/openclaw@c3c7a99 fix: restrict HTML timeout short-circuit to transient statuses · openclaw/openclaw@de129a6 fix: keep TUI watchdog bound to active run (#67401) (thanks @xantorres) · openclaw/openclaw@3525273 Gateway/skills: dedupe skills prefix-match + drop dead fallback on log · openclaw/openclaw@d7f489f Extensions/lmstudio: back off inference preload after consecutive fai… · openclaw/openclaw@b555214 TUI/streaming: add watchdog that resets the activity indicator after … · openclaw/openclaw@f44ab20 Agents/tool-loop: enable unknown-tool stream guard by default · openclaw/openclaw@36ed367 Gateway/skills: invalidate session skills snapshot on config write · openclaw/openclaw@b23d59a fix: classify HTML provider error pages correctly (#67642) (thanks @s… · openclaw/openclaw@e588e90 fix(skills): remove unused model-usage import (#67641) · openclaw/openclaw@55f05df docs(changelog): credit codex fix superseded PRs · openclaw/openclaw@e485f24 fix(openai-codex): normalize stale transport metadata in resolution a… · openclaw/openclaw@90801ba CI: pin Docker-related GitHub Actions (#67632) · openclaw/openclaw@f697b01 Android: modernize WebView and discovery API usage (#67627) · openclaw/openclaw@44a6e50 fix(deps): bump hono to 4.12.14 and @hono/node-server to 1.19.14 (GHS… · openclaw/openclaw@fbccc18 fix(deps): bump dompurify to 3.4.0 (#67614) · openclaw/openclaw@2c2dc00 CI: add explicit permissions to all workflow jobs (fixes code-scannin… · openclaw/openclaw@01b7516 fix: register bundled TTS providers and route overrides correctly (#6… · openclaw/openclaw@6ea3cdd fix: align host tilde paths with OS home (#62804) (thanks @stainlu) · openclaw/openclaw@ecfaf64 fix: flush creds queue before reconnect socket open (#67464) (thanks … · openclaw/openclaw@405c63f fix: strip standalone <function> tool call tags from visible text (#6… · openclaw/openclaw@78df859 fix(agents): preserve cli session metadata before transcript persist … · openclaw/openclaw@898fd04 docs(changelog): move cli transcript entry · openclaw/openclaw@c1817c6 fix(agents): normalize cli transcript api field · openclaw/openclaw@3a3fae0 docs(changelog): note cli transcript persistence · openclaw/openclaw@6c343f1 fix(agents): persist cli transcript turns · openclaw/openclaw@b8ef507 fix(msteams): harden security-sensitive flows (#65841) · openclaw/openclaw@c56b56e [Dashboard] Fix exec approval modal overflow for long command content… · openclaw/openclaw@053c5b0 Docs: remove QA changelog entry · openclaw/openclaw@7fd5771 QA: fix private runtime source loading (#67428) · openclaw/openclaw@d5933af docs(gateway): correct protocol.md schema path, hello-ok example, aut… · openclaw/openclaw@489404d CI: pin Node 22 runners to 22.18.0 · openclaw/openclaw@4ffa621 models.authStatus: normalize provider ids + tighten env-backed escape… · openclaw/openclaw@f2fdb9d Update CHANGELOG.md · openclaw/openclaw@7694a92 test(parallels): clean up npm update guard jobs · openclaw/openclaw@045ea7b Plugins: prefer scanDir override paths · openclaw/openclaw@b2974da fix(dreaming): default storage.mode to "separate" so phase blocks sto… · openclaw/openclaw@8c392f0 fix(memory-core): skip dreaming transcript ingestion via session stor… · openclaw/openclaw@a1b01f0 fix: dedupe replayed exec.finished node events (#67281) · openclaw/openclaw@5dcf526
test: merge cron view UI cases · openclaw/openclaw@81d6cf9
steipete · 2026-04-18 · via Recent Commits to openclaw:main

@@ -77,6 +77,12 @@ function createProps(overrides: Partial<CronProps> = {}): CronProps {

7777

};

7878

}

797980+

function getButtonByText(container: Element, text: string) {

81+

return Array.from(container.querySelectorAll("button")).find(

82+

(btn) => btn.textContent?.trim() === text,

83+

);

84+

}

85+8086

describe("cron view", () => {

8187

it("shows all-job history mode by default", () => {

8288

const container = document.createElement("div");

@@ -250,41 +256,36 @@ describe("cron view", () => {

250256

expect(container.textContent).not.toContain("Next 13");

251257

});

252258253-

it("calls onJobsFiltersChange when schedule filter changes", () => {

259+

it("wires jobs filter changes and reset", () => {

254260

const container = document.createElement("div");

255261

const onJobsFiltersChange = vi.fn();

262+

const onJobsFiltersReset = vi.fn();

256263

render(renderCron(createProps({ onJobsFiltersChange })), container);

257264258-

const select = container.querySelector('select[data-test-id="cron-jobs-schedule-filter"]');

259-

expect(select).not.toBeNull();

260-

if (!(select instanceof HTMLSelectElement)) {

265+

const scheduleSelect = container.querySelector(

266+

'select[data-test-id="cron-jobs-schedule-filter"]',

267+

);

268+

expect(scheduleSelect).not.toBeNull();

269+

if (!(scheduleSelect instanceof HTMLSelectElement)) {

261270

return;

262271

}

263-

select.value = "cron";

264-

select.dispatchEvent(new Event("change", { bubbles: true }));

272+

scheduleSelect.value = "cron";

273+

scheduleSelect.dispatchEvent(new Event("change", { bubbles: true }));

265274266275

expect(onJobsFiltersChange).toHaveBeenCalledWith({ cronJobsScheduleKindFilter: "cron" });

267-

});

268-269-

it("calls onJobsFiltersChange when last-run filter changes", () => {

270-

const container = document.createElement("div");

271-

const onJobsFiltersChange = vi.fn();

272-

render(renderCron(createProps({ onJobsFiltersChange })), container);

273276274-

const select = container.querySelector('select[data-test-id="cron-jobs-last-status-filter"]');

275-

expect(select).not.toBeNull();

276-

if (!(select instanceof HTMLSelectElement)) {

277+

const lastRunSelect = container.querySelector(

278+

'select[data-test-id="cron-jobs-last-status-filter"]',

279+

);

280+

expect(lastRunSelect).not.toBeNull();

281+

if (!(lastRunSelect instanceof HTMLSelectElement)) {

277282

return;

278283

}

279-

select.value = "error";

280-

select.dispatchEvent(new Event("change", { bubbles: true }));

284+

lastRunSelect.value = "error";

285+

lastRunSelect.dispatchEvent(new Event("change", { bubbles: true }));

281286282287

expect(onJobsFiltersChange).toHaveBeenCalledWith({ cronJobsLastStatusFilter: "error" });

283-

});

284288285-

it("calls onJobsFiltersReset when reset button is clicked", () => {

286-

const container = document.createElement("div");

287-

const onJobsFiltersReset = vi.fn();

288289

render(

289290

renderCron(

290291

createProps({

@@ -302,7 +303,7 @@ describe("cron view", () => {

302303

expect(onJobsFiltersReset).toHaveBeenCalledTimes(1);

303304

});

304305305-

it("shows webhook delivery option in the form", () => {

306+

it("renders supported delivery options and normalizes stale announce selection", () => {

306307

const container = document.createElement("div");

307308

render(

308309

renderCron(

@@ -317,10 +318,7 @@ describe("cron view", () => {

317318

(opt.textContent ?? "").trim(),

318319

);

319320

expect(options).toContain("Webhook POST");

320-

});

321321322-

it("normalizes stale announce selection in the form when unsupported", () => {

323-

const container = document.createElement("div");

324322

render(

325323

renderCron(

326324

createProps({

@@ -335,12 +333,12 @@ describe("cron view", () => {

335333

container,

336334

);

337335338-

const options = Array.from(container.querySelectorAll("option")).map((opt) =>

336+

const normalizedOptions = Array.from(container.querySelectorAll("option")).map((opt) =>

339337

(opt.textContent ?? "").trim(),

340338

);

341-

expect(options).not.toContain("Announce summary (default)");

342-

expect(options).toContain("Webhook POST");

343-

expect(options).toContain("None (internal)");

339+

expect(normalizedOptions).not.toContain("Announce summary (default)");

340+

expect(normalizedOptions).toContain("Webhook POST");

341+

expect(normalizedOptions).toContain("None (internal)");

344342

expect(container.querySelector('input[placeholder="https://example.com/cron"]')).toBeNull();

345343

});

346344

@@ -386,9 +384,7 @@ describe("cron view", () => {

386384

container,

387385

);

388386389-

const editButton = Array.from(container.querySelectorAll("button")).find(

390-

(btn) => btn.textContent?.trim() === "Edit",

391-

);

387+

const editButton = getButtonByText(container, "Edit");

392388

expect(editButton).not.toBeUndefined();

393389

editButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));

394390

expect(onEdit).toHaveBeenCalledWith(job);

@@ -397,15 +393,13 @@ describe("cron view", () => {

397393

expect(container.textContent).toContain("Edit Job");

398394

expect(container.textContent).toContain("Save changes");

399395400-

const cancelButton = Array.from(container.querySelectorAll("button")).find(

401-

(btn) => btn.textContent?.trim() === "Cancel",

402-

);

396+

const cancelButton = getButtonByText(container, "Cancel");

403397

expect(cancelButton).not.toBeUndefined();

404398

cancelButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));

405399

expect(onCancelEdit).toHaveBeenCalledTimes(1);

406400

});

407401408-

it("renders advanced controls for cron + agent payload + delivery", () => {

402+

it("renders cron form sections and toggles advanced controls by schedule", () => {

409403

const container = document.createElement("div");

410404

render(

411405

renderCron(

@@ -428,52 +422,29 @@ describe("cron view", () => {

428422

expect(container.textContent).toContain("Model");

429423

expect(container.textContent).toContain("Thinking");

430424

expect(container.textContent).toContain("Best effort delivery");

431-

});

432-433-

it("groups stagger window and unit inside the same stagger row", () => {

434-

const container = document.createElement("div");

435-

render(

436-

renderCron(

437-

createProps({

438-

form: {

439-

...DEFAULT_CRON_FORM,

440-

scheduleKind: "cron",

441-

payloadKind: "agentTurn",

442-

},

443-

}),

444-

),

445-

container,

446-

);

447425448426

const staggerGroup = container.querySelector(".cron-stagger-group");

449427

expect(staggerGroup).not.toBeNull();

450428

expect(staggerGroup?.textContent).toContain("Stagger window");

451429

expect(staggerGroup?.textContent).toContain("Stagger unit");

452-

});

453-454-

it("explains timeout blank behavior and shows cron jitter hint", () => {

455-

const container = document.createElement("div");

456-

render(

457-

renderCron(

458-

createProps({

459-

form: {

460-

...DEFAULT_CRON_FORM,

461-

scheduleKind: "cron",

462-

payloadKind: "agentTurn",

463-

},

464-

}),

465-

),

466-

container,

467-

);

468-469430

expect(container.textContent).toContain(

470431

"Optional. Leave blank to use the gateway default timeout behavior for this run.",

471432

);

472433

expect(container.textContent).toContain("Need jitter? Use Advanced");

473-

});

474434475-

it("disables Agent ID when clear-agent is enabled", () => {

476-

const container = document.createElement("div");

435+

expect(container.textContent).toContain("Enabled");

436+

expect(container.textContent).toContain("Jobs");

437+

expect(container.textContent).toContain("Next wake");

438+

expect(container.textContent).toContain("Basics");

439+

expect(container.textContent).toContain("Schedule");

440+

expect(container.textContent).toContain("Execution");

441+

expect(container.textContent).toContain("Delivery");

442+443+

const checkboxLabel = container.querySelector(".cron-checkbox");

444+

expect(checkboxLabel).not.toBeNull();

445+

const firstElement = checkboxLabel?.firstElementChild;

446+

expect(firstElement?.tagName.toLowerCase()).toBe("input");

447+477448

render(

478449

renderCron(

479450

createProps({

@@ -490,32 +461,7 @@ describe("cron view", () => {

490461

expect(agentInput).not.toBeNull();

491462

expect(agentInput instanceof HTMLInputElement).toBe(true);

492463

expect(agentInput instanceof HTMLInputElement ? agentInput.disabled : false).toBe(true);

493-

});

494464495-

it("renders sectioned cron form layout", () => {

496-

const container = document.createElement("div");

497-

render(renderCron(createProps()), container);

498-

expect(container.textContent).toContain("Enabled");

499-

expect(container.textContent).toContain("Jobs");

500-

expect(container.textContent).toContain("Next wake");

501-

expect(container.textContent).toContain("Basics");

502-

expect(container.textContent).toContain("Schedule");

503-

expect(container.textContent).toContain("Execution");

504-

expect(container.textContent).toContain("Delivery");

505-

expect(container.textContent).toContain("Advanced");

506-

});

507-508-

it("renders checkbox fields with input first for alignment", () => {

509-

const container = document.createElement("div");

510-

render(renderCron(createProps()), container);

511-

const checkboxLabel = container.querySelector(".cron-checkbox");

512-

expect(checkboxLabel).not.toBeNull();

513-

const firstElement = checkboxLabel?.firstElementChild;

514-

expect(firstElement?.tagName.toLowerCase()).toBe("input");

515-

});

516-517-

it("hides cron-only advanced controls for non-cron schedules", () => {

518-

const container = document.createElement("div");

519465

render(

520466

renderCron(

521467

createProps({

@@ -535,7 +481,7 @@ describe("cron view", () => {

535481

expect(container.textContent).not.toContain("Best effort delivery");

536482

});

537483538-

it("renders inline validation errors and disables submit when invalid", () => {

484+

it("renders inline validation errors, disabled submit, and required aria bindings", () => {

539485

const container = document.createElement("div");

540486

render(

541487

renderCron(

@@ -569,10 +515,7 @@ describe("cron view", () => {

569515

);

570516

expect(saveButton).not.toBeUndefined();

571517

expect(saveButton?.disabled).toBe(true);

572-

});

573518574-

it("shows required legend and aria bindings for invalid required fields", () => {

575-

const container = document.createElement("div");

576519

render(

577520

renderCron(

578521

createProps({

@@ -609,7 +552,7 @@ describe("cron view", () => {

609552

);

610553

});

611554612-

it("wires the Clone action from job rows", () => {

555+

it("wires job row actions and selects the row before acting", () => {

613556

const container = document.createElement("div");

614557

const onClone = vi.fn();

615558

const onLoadRuns = vi.fn();

@@ -625,85 +568,67 @@ describe("cron view", () => {

625568

container,

626569

);

627570628-

const cloneButton = Array.from(container.querySelectorAll("button")).find(

629-

(btn) => btn.textContent?.trim() === "Clone",

630-

);

571+

const cloneButton = getButtonByText(container, "Clone");

631572

expect(cloneButton).not.toBeUndefined();

632573

cloneButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));

633574

expect(onClone).toHaveBeenCalledWith(job);

634575

expect(onLoadRuns).toHaveBeenCalledWith("job-clone");

635-

});

636576637-

it("selects row when clicking Enable/Disable, Run, and Remove actions", () => {

638-

const container = document.createElement("div");

639577

const onToggle = vi.fn();

640578

const onRun = vi.fn();

641579

const onRemove = vi.fn();

642-

const onLoadRuns = vi.fn();

643-

const job = createJob("job-actions");

580+

const actionLoadRuns = vi.fn();

581+

const actionJob = createJob("job-actions");

644582

render(

645583

renderCron(

646584

createProps({

647-

jobs: [job],

585+

jobs: [actionJob],

648586

onToggle,

649587

onRun,

650588

onRemove,

651-

onLoadRuns,

589+

onLoadRuns: actionLoadRuns,

652590

}),

653591

),

654592

container,

655593

);

656594657-

const enableButton = Array.from(container.querySelectorAll("button")).find(

658-

(btn) => btn.textContent?.trim() === "Disable",

659-

);

595+

const enableButton = getButtonByText(container, "Disable");

660596

expect(enableButton).not.toBeUndefined();

661597

enableButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));

662598663-

const runButton = Array.from(container.querySelectorAll("button")).find(

664-

(btn) => btn.textContent?.trim() === "Run",

665-

);

599+

const runButton = getButtonByText(container, "Run");

666600

expect(runButton).not.toBeUndefined();

667601

runButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));

668602669-

const removeButton = Array.from(container.querySelectorAll("button")).find(

670-

(btn) => btn.textContent?.trim() === "Remove",

671-

);

603+

const removeButton = getButtonByText(container, "Remove");

672604

expect(removeButton).not.toBeUndefined();

673605

removeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));

674606675-

expect(onToggle).toHaveBeenCalledWith(job, false);

676-

expect(onRun).toHaveBeenCalledWith(job, "force");

677-

expect(onRemove).toHaveBeenCalledWith(job);

678-

expect(onLoadRuns).toHaveBeenCalledTimes(3);

679-

expect(onLoadRuns).toHaveBeenNthCalledWith(1, "job-actions");

680-

expect(onLoadRuns).toHaveBeenNthCalledWith(2, "job-actions");

681-

expect(onLoadRuns).toHaveBeenNthCalledWith(3, "job-actions");

682-

});

607+

expect(onToggle).toHaveBeenCalledWith(actionJob, false);

608+

expect(onRun).toHaveBeenCalledWith(actionJob, "force");

609+

expect(onRemove).toHaveBeenCalledWith(actionJob);

610+

expect(actionLoadRuns).toHaveBeenCalledTimes(3);

611+

expect(actionLoadRuns).toHaveBeenNthCalledWith(1, "job-actions");

612+

expect(actionLoadRuns).toHaveBeenNthCalledWith(2, "job-actions");

613+

expect(actionLoadRuns).toHaveBeenNthCalledWith(3, "job-actions");

683614684-

it("wires Run if due action with due mode", () => {

685-

const container = document.createElement("div");

686-

const onRun = vi.fn();

687-

const onLoadRuns = vi.fn();

688-

const job = createJob("job-due");

615+

const onRunDue = vi.fn();

616+

const dueJob = createJob("job-due");

689617

render(

690618

renderCron(

691619

createProps({

692-

jobs: [job],

693-

onRun,

694-

onLoadRuns,

620+

jobs: [dueJob],

621+

onRun: onRunDue,

695622

}),

696623

),

697624

container,

698625

);

699626700-

const runDueButton = Array.from(container.querySelectorAll("button")).find(

701-

(btn) => btn.textContent?.trim() === "Run if due",

702-

);

627+

const runDueButton = getButtonByText(container, "Run if due");

703628

expect(runDueButton).not.toBeUndefined();

704629

runDueButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));

705630706-

expect(onRun).toHaveBeenCalledWith(job, "due");

631+

expect(onRunDue).toHaveBeenCalledWith(dueJob, "due");

707632

});

708633709634

it("renders suggestion datalists for agent/model/thinking/timezone", () => {