From 66ed1931d66650d7df16553f3ed204a83e05632a Mon Sep 17 00:00:00 2001 From: YangJie Date: Fri, 26 Jun 2026 13:40:22 +0800 Subject: [PATCH 1/5] fix(batch-auth): confirm existing UUID row on skip; drop _slotMap cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a device already has a valid auth code (Skipped result), the newly allocated Excel row is released and the row matching the device's existing UUID is found and confirmed as Used — preventing the same code from being issued to another device. Rust: BatchAuthSlotResult::Skipped now carries existing_uuid; added ExcelRowAllocator::find_and_confirm_by_uuid; lib.rs emits excelError on confirm failure. Frontend: removed the _slotMap port→slot cache (linear scan is correct and simpler); propagates excelError into slot state on skip. Also: add batch-flash-auth-extended.test.ts (extended test coverage). --- crates/tyutool-core/src/authorize.rs | 14 +- src-tauri/src/batch_auth.rs | 34 ++ src-tauri/src/lib.rs | 23 +- src/stores/batch-flash-auth-extended.test.ts | 590 +++++++++++++++++++ src/stores/batch-flash-auth.ts | 23 +- 5 files changed, 661 insertions(+), 23 deletions(-) create mode 100644 src/stores/batch-flash-auth-extended.test.ts diff --git a/crates/tyutool-core/src/authorize.rs b/crates/tyutool-core/src/authorize.rs index 4fe6104..b83fca0 100644 --- a/crates/tyutool-core/src/authorize.rs +++ b/crates/tyutool-core/src/authorize.rs @@ -671,7 +671,9 @@ pub enum BatchAuthSlotResult { /// Device already had the exact credentials — nothing written. AlreadyDone { mac: String }, /// Auth on device didn't match but conflict_policy=Skip — nothing written. - Skipped { mac: String }, + /// `existing_uuid` is the UUID already on the device, so the caller can + /// find and confirm that Excel row. + Skipped { mac: String, existing_uuid: String }, /// Operation was cancelled. Cancelled, } @@ -1079,7 +1081,10 @@ where log::info!( "[batch-auth] skipped port={port} mac={mac} existing_uuid={ex_uuid}" ); - return Ok(BatchAuthSlotResult::Skipped { mac }); + return Ok(BatchAuthSlotResult::Skipped { + mac, + existing_uuid: ex_uuid.clone(), + }); } } } @@ -1175,7 +1180,10 @@ where } if conflict_policy == ConflictPolicy::Skip { log::info!("[batch-auth] skipped (old fw) port={port} mac={mac} existing_uuid={ex_uuid}"); - return Ok(BatchAuthSlotResult::Skipped { mac }); + return Ok(BatchAuthSlotResult::Skipped { + mac, + existing_uuid: ex_uuid.clone(), + }); } } } diff --git a/src-tauri/src/batch_auth.rs b/src-tauri/src/batch_auth.rs index 6d0f7e2..abadcef 100644 --- a/src-tauri/src/batch_auth.rs +++ b/src-tauri/src/batch_auth.rs @@ -222,6 +222,40 @@ impl ExcelRowAllocator { } } + /// Find the row whose UUID matches `uuid` and confirm it as Used (mark MAC + + /// timestamp and persist). If the row is already Used this is a no-op. + /// Returns the row index that was confirmed, or None if uuid was not found. + pub fn find_and_confirm_by_uuid( + &self, + uuid: &str, + mac: String, + ) -> Result, String> { + let row_idx = { + let state = self.state.lock().unwrap(); + state + .rows + .iter() + .enumerate() + .find(|(_, r)| r.uuid == uuid) + .map(|(i, _)| i) + }; + match row_idx { + None => Ok(None), + Some(idx) => { + // Only confirm rows that haven't been marked Used yet. + let already_used = { + let state = self.state.lock().unwrap(); + state.rows[idx].status == RowStatus::Used + }; + if already_used { + return Ok(Some(idx)); + } + self.confirm_row(idx, mac)?; + Ok(Some(idx)) + } + } + } + pub fn confirm_row(&self, row_idx: usize, mac: String) -> Result<(), String> { let mut state = self.state.lock().unwrap(); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c2789f1..a135156 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -592,13 +592,24 @@ fn batch_auth_start( } let _ = app_clone.emit("batch-auth-progress", payload); } - Ok(tyutool_core::BatchAuthSlotResult::Skipped { mac }) => { - log::info!("[batch-auth] slot skipped port={port_clone} mac={mac} uuid={uuid} excel_row={row_idx}"); + Ok(tyutool_core::BatchAuthSlotResult::Skipped { mac, existing_uuid }) => { + log::info!("[batch-auth] slot skipped port={port_clone} mac={mac} existing_uuid={existing_uuid} new_row={row_idx}"); + // Release the newly-allocated row (we won't write a new auth code). alloc_clone.release_row(row_idx); - let _ = app_clone.emit( - "batch-auth-progress", - serde_json::json!({ "port": port_clone, "step": "skipped", "mac": mac }), - ); + // Mark the device's existing auth-code row as Used so the same + // code isn't handed out to another device. + let excel_err = alloc_clone + .find_and_confirm_by_uuid(&existing_uuid, mac.clone()) + .err(); + if let Some(ref e) = excel_err { + log::error!("[batch-auth] excel-confirm-skipped-failed port={port_clone} existing_uuid={existing_uuid} error={e}"); + } + let mut payload = + serde_json::json!({ "port": port_clone, "step": "skipped", "mac": mac }); + if let Some(e) = excel_err { + payload["excelError"] = serde_json::Value::String(e); + } + let _ = app_clone.emit("batch-auth-progress", payload); } Ok(tyutool_core::BatchAuthSlotResult::Cancelled) => { log::info!( diff --git a/src/stores/batch-flash-auth-extended.test.ts b/src/stores/batch-flash-auth-extended.test.ts new file mode 100644 index 0000000..f9efe07 --- /dev/null +++ b/src/stores/batch-flash-auth-extended.test.ts @@ -0,0 +1,590 @@ +// src/stores/batch-flash-auth-extended.test.ts +// Extended unit tests for batch-flash-auth feature (covers test plan TODO items). +// Tests for confirmed bugs are marked [BUG TC-XXX] and are expected to fail until fixed. +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { setActivePinia, createPinia } from "pinia"; +import { useBatchFlashAuthStore } from "./batch-flash-auth"; +import { + normalizePortName, + applyPortFilter, +} from "@/features/batch-flash-auth/port-filter"; +import { filterByChip } from "@/features/batch-flash-auth/auth-firmware"; +import type { AuthFirmwareEntry } from "@/features/batch-flash-auth/types"; + +vi.mock("@/runtime", () => ({ + isTauriRuntime: () => false, +})); + +// ─── S02 Slot state machine ──────────────────────────────────────────────── + +describe("S02 slot state machine — additional", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("TC-015: removeSlot cannot remove a flashing slot", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].status = "flashing"; + store.removeSlot("COM3"); + expect(store.slots).toHaveLength(1); + }); + + it("TC-015b: removeSlot cannot remove a reading_mac slot", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].status = "reading_mac"; + store.removeSlot("COM3"); + expect(store.slots).toHaveLength(1); + }); + + it("TC-015c: removeSlot cannot remove an authorizing slot", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].status = "authorizing"; + store.removeSlot("COM3"); + expect(store.slots).toHaveLength(1); + }); + + it("TC-015d: removeSlot removes a failed slot", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].status = "failed"; + // failed is not in allowed remove-statuses per store code; verify current behavior + store.removeSlot("COM3"); + // per removeSlot implementation: allowed = idle | done | skipped + // failed is NOT in that list, so slot remains + expect(store.slots).toHaveLength(1); + }); + + it("TC-016: updateSlot Object.assign semantics — mac preserved when only updating progress", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].mac = "aabbccddeeff"; + // Call handleAuthProgress done to test updateSlot merging + store.batchStartTime = Date.now(); + store.handleAuthProgress({ + port: "COM3", + step: "done", + mac: "aabbccddeeff", + }); + // Patch only progress via a subsequent reading_mac (to simulate merge) + // Direct: set mac then update only progress via slots ref + store.slots[0].mac = "112233445566"; + store.handleAuthProgress({ port: "COM3", step: "reading_mac" }); + // reading_mac only updates status and currentPhase, mac is preserved + expect(store.slots[0].mac).toBe("112233445566"); + }); + + it("TC-017: excelError: undefined in updateSlot patch clears the field", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].excelError = "row not found"; + // cancelled step calls updateSlot with excelError: undefined + store.handleAuthProgress({ port: "COM3", step: "cancelled" }); + expect(store.slots[0].excelError).toBeUndefined(); + }); + + it("TC-018: initial slot has correct default fields", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + const s = store.slots[0]; + expect(s.status).toBe("idle"); + expect(s.progress).toBe(0); + expect(s.currentPhase).toBe(""); + expect(s.mac).toBeUndefined(); + expect(s.error).toBeUndefined(); + expect(s.excelError).toBeUndefined(); + }); +}); + +// ─── S03 Flash progress — additional ────────────────────────────────────── + +describe("S03 handleFlashProgress — port isolation", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("TC-023: flash progress only updates the matching port", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3", "COM5"]); + store.slots[0].status = "flashing"; + store.slots[1].status = "flashing"; + store.handleFlashProgress({ + port: "COM3", + event: { kind: "percent", value: 75 }, + }); + expect(store.slots[0].progress).toBe(75); + expect(store.slots[1].progress).toBe(0); // COM5 unchanged + }); + + it("TC-023b: auth progress only updates the matching port", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3", "COM5"]); + store.batchStartTime = Date.now(); + store.handleAuthProgress({ port: "COM3", step: "done", mac: "aabb" }); + expect(store.slots[0].status).toBe("done"); + expect(store.slots[1].status).toBe("idle"); // COM5 unchanged + }); +}); + +// ─── S04 Auth progress — BUG tests ──────────────────────────────────────── + +describe("S04 handleAuthProgress — skipped excelError [TC-030]", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("TC-030: skipped step propagates excelError to slot", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.batchStartTime = Date.now(); + store.handleAuthProgress({ + port: "COM3", + step: "skipped", + mac: "aabbccddeeff", + excelError: "row not found in spreadsheet", + }); + expect(store.slots[0].excelError).toBe("row not found in spreadsheet"); + }); + + it("TC-030: skipped step without excelError leaves excelError undefined", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.batchStartTime = Date.now(); + store.handleAuthProgress({ + port: "COM3", + step: "skipped", + mac: "aabbccddeeff", + }); + expect(store.slots[0].excelError).toBeUndefined(); + }); + + it("cancelled step correctly clears excelError (for contrast)", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].excelError = "pre-existing error"; + store.handleAuthProgress({ port: "COM3", step: "cancelled" }); + expect(store.slots[0].excelError).toBeUndefined(); + }); + + it("done step preserves excelError when present in event", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.batchStartTime = Date.now(); + store.handleAuthProgress({ + port: "COM3", + step: "done", + mac: "aabb", + excelError: "confirm failed", + }); + // done step DOES propagate excelError (line 349 in store) + expect(store.slots[0].excelError).toBe("confirm failed"); + }); +}); + +// ─── S05 Batch operations — BUG tests ───────────────────────────────────── + +describe("S05 retryFailed — excelError cleared [TC-037]", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("TC-037: retryFailed clears excelError on failed slot", async () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].status = "failed"; + store.slots[0].error = "some error"; + store.slots[0].excelError = "confirm_row failed"; + await store.retryFailed(); + expect(store.slots[0].excelError).toBeUndefined(); + }); + + it("retryFailed: error is cleared (correct behavior)", async () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].status = "failed"; + store.slots[0].error = "timeout"; + await store.retryFailed(); + expect(store.slots[0].error).toBeUndefined(); + }); + + it("TC-038: retryFailed only affects failed slots, not done/skipped", async () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3", "COM5", "COM7"]); + store.slots[0].status = "failed"; + store.slots[1].status = "done"; + store.slots[1].mac = "aabb"; + store.slots[2].status = "skipped"; + store.slots[2].mac = "ccdd"; + await store.retryFailed(); + expect(store.slots[0].status).toBe("idle"); // failed → reset to idle + expect(store.slots[1].status).toBe("done"); // done unchanged + expect(store.slots[1].mac).toBe("aabb"); + expect(store.slots[2].status).toBe("skipped"); // skipped unchanged + expect(store.slots[2].mac).toBe("ccdd"); + }); + + it("retryFailed: canRetry gate — no-op when no failed slots", async () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.completionBanner = { kind: "all-success", count: 1 }; + await store.retryFailed(); // canRetry is false + expect(store.completionBanner).not.toBeNull(); // banner not cleared + }); +}); + +// ─── S06 Completion banner — additional ─────────────────────────────────── + +describe("S06 completion banner — additional", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("TC-049: checkBatchCompletion no-ops when any slot is still active", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3", "COM5"]); + store.batchStartTime = Date.now(); + store.slots[0].status = "authorizing"; // active + store.slots[1].status = "done"; + store.checkBatchCompletion(); + expect(store.completionBanner).toBeNull(); // no banner while active + }); + + it("TC-049b: banner appears only after all active slots finish", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3", "COM5"]); + store.batchStartTime = Date.now(); + store.currentBatchPorts = ["COM3", "COM5"]; + store.slots[0].status = "authorizing"; + store.slots[1].status = "done"; + store.checkBatchCompletion(); + expect(store.completionBanner).toBeNull(); + // Now finish the last active slot + store.slots[0].status = "done"; + store.checkBatchCompletion(); + expect(store.completionBanner?.kind).toBe("all-success"); + }); + + it("TC-051: resetAuthStats resets only auth cumulative to zero", () => { + const store = useBatchFlashAuthStore(); + store.cumulativeStats.auth = { total: 10, success: 8, fail: 2 }; + store.cumulativeStats.flash = { total: 5, success: 4, fail: 1 }; + store.resetAuthStats(); + expect(store.cumulativeStats.auth).toEqual({ + total: 0, + success: 0, + fail: 0, + }); + // flash stats should remain untouched + expect(store.cumulativeStats.flash).toEqual({ + total: 5, + success: 4, + fail: 1, + }); + }); + + it("TC-051b: after resetAuthStats, subsequent auth events accumulate from 0", () => { + const store = useBatchFlashAuthStore(); + store.cumulativeStats.auth = { total: 10, success: 8, fail: 2 }; + store.resetAuthStats(); + store.addPorts(["COM3"]); + store.batchStartTime = Date.now(); + store.handleAuthProgress({ port: "COM3", step: "done", mac: "aabb" }); + expect(store.cumulativeStats.auth.total).toBe(1); + expect(store.cumulativeStats.auth.success).toBe(1); + }); +}); + +// ─── S07 Port filter — pure functions ───────────────────────────────────── + +describe("S07 port-filter pure functions", () => { + it("TC-052: normalizePortName — on non-Windows platform, COM port NOT uppercased", () => { + // On Linux (test environment), isWindowsPlatform() returns false + // com3 stays as-is since we're not on Windows + expect(normalizePortName("com3")).toBe("com3"); + }); + + it("TC-053: normalizePortName — Unix-style path unchanged", () => { + expect(normalizePortName("/dev/ttyUSB0")).toBe("/dev/ttyUSB0"); + expect(normalizePortName("/dev/cu.usbserial-0001")).toBe( + "/dev/cu.usbserial-0001", + ); + }); + + it("TC-052c: normalizePortName — already uppercase COM port unchanged on any platform", () => { + // On Linux, COM3 is returned unchanged (no transformation for non-Windows) + expect(normalizePortName("COM3")).toBe("COM3"); + }); + + it("TC-054: applyPortFilter removes blocked ports", () => { + const result = applyPortFilter( + ["/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyUSB2"], + ["/dev/ttyUSB1"], + ); + expect(result).toEqual(["/dev/ttyUSB0", "/dev/ttyUSB2"]); + }); + + it("TC-055: applyPortFilter preserves non-blocked ports", () => { + const result = applyPortFilter( + ["/dev/ttyUSB0", "/dev/ttyUSB1"], + ["/dev/ttyUSB9"], + ); + expect(result).toEqual(["/dev/ttyUSB0", "/dev/ttyUSB1"]); + }); + + it("TC-056: applyPortFilter with empty blocklist returns all ports", () => { + const ports = ["/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyUSB2"]; + expect(applyPortFilter(ports, [])).toEqual(ports); + }); + + it("TC-056b: applyPortFilter with all ports blocked returns empty array", () => { + const result = applyPortFilter( + ["/dev/ttyUSB0", "/dev/ttyUSB1"], + ["/dev/ttyUSB0", "/dev/ttyUSB1"], + ); + expect(result).toEqual([]); + }); +}); + +// ─── S07 Port filter — store integration ────────────────────────────────── + +describe("S07 addBlockedPort / removeBlockedPort — store", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("addBlockedPort adds to blockedPorts list", () => { + const store = useBatchFlashAuthStore(); + store.addBlockedPort("/dev/ttyUSB0"); + expect(store.filterConfig.blockedPorts).toContain("/dev/ttyUSB0"); + }); + + it("addBlockedPort removes idle slot that matches the newly blocked port", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["/dev/ttyUSB0", "/dev/ttyUSB1"]); + store.addBlockedPort("/dev/ttyUSB0"); + expect(store.slots.map((s) => s.port)).not.toContain("/dev/ttyUSB0"); + expect(store.slots.map((s) => s.port)).toContain("/dev/ttyUSB1"); + }); + + it("addBlockedPort does NOT remove active slot matching the blocked port", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["/dev/ttyUSB0"]); + store.slots[0].status = "authorizing"; // active + store.addBlockedPort("/dev/ttyUSB0"); + expect(store.slots).toHaveLength(1); // still there because it's active + }); + + it("addBlockedPort is idempotent — no duplicates in blocklist", () => { + const store = useBatchFlashAuthStore(); + store.addBlockedPort("/dev/ttyUSB0"); + store.addBlockedPort("/dev/ttyUSB0"); + expect( + store.filterConfig.blockedPorts.filter((p) => p === "/dev/ttyUSB0"), + ).toHaveLength(1); + }); + + it("removeBlockedPort removes from blockedPorts list", () => { + const store = useBatchFlashAuthStore(); + store.addBlockedPort("/dev/ttyUSB0"); + store.removeBlockedPort("/dev/ttyUSB0"); + expect(store.filterConfig.blockedPorts).not.toContain("/dev/ttyUSB0"); + }); + + it("filterActive computed: true when blockedPorts non-empty", () => { + const store = useBatchFlashAuthStore(); + expect(store.filterActive).toBe(false); + store.addBlockedPort("/dev/ttyUSB0"); + expect(store.filterActive).toBe(true); + store.removeBlockedPort("/dev/ttyUSB0"); + expect(store.filterActive).toBe(false); + }); +}); + +// ─── S08 Auth firmware pure functions ───────────────────────────────────── + +describe("S08 filterByChip", () => { + const entries: AuthFirmwareEntry[] = [ + { version: "1.0.0", chip: "t5ai", url: "u1", sha256: "h1" }, + { version: "1.2.0", chip: "t5ai", url: "u2", sha256: "h2" }, + { version: "1.10.0", chip: "t5ai", url: "u3", sha256: "h3" }, + { version: "2.0.0", chip: "esp32", url: "u4", sha256: "h4" }, + { version: "1.1.0", chip: "t5ai", url: "u5", sha256: "h5" }, + ]; + + it("TC-058: returns entries sorted descending by version (Intl numeric)", () => { + const result = filterByChip(entries, "t5ai"); + const versions = result.map((e) => e.version); + expect(versions).toEqual(["1.10.0", "1.2.0", "1.1.0", "1.0.0"]); + }); + + it("TC-059: returns only entries for the matching chip", () => { + const result = filterByChip(entries, "t5ai"); + expect(result.every((e) => e.chip === "t5ai")).toBe(true); + expect(result).toHaveLength(4); + }); + + it("TC-060: returns empty array when chip has no entries in manifest", () => { + expect(filterByChip(entries, "gd32")).toEqual([]); + expect(filterByChip([], "t5ai")).toEqual([]); + }); + + it("TC-060b: returns empty array when entries list is empty", () => { + expect(filterByChip([], "t5ai")).toHaveLength(0); + }); + + it("filterByChip: esp32 entries returned correctly", () => { + const result = filterByChip(entries, "esp32"); + expect(result).toHaveLength(1); + expect(result[0].version).toBe("2.0.0"); + }); + + it("version comparison: v prefix stripped correctly", () => { + const withV: AuthFirmwareEntry[] = [ + { version: "v1.0.0", chip: "t5ai", url: "u1", sha256: "h1" }, + { version: "v1.2.0", chip: "t5ai", url: "u2", sha256: "h2" }, + { version: "v1.10.0", chip: "t5ai", url: "u3", sha256: "h3" }, + ]; + const result = filterByChip(withV, "t5ai"); + expect(result.map((e) => e.version)).toEqual([ + "v1.10.0", + "v1.2.0", + "v1.0.0", + ]); + }); +}); + +// ─── S08 downloadAuthFirmware ────────────────────────────────────────────── + +describe("S08 downloadAuthFirmware — web mode guard", () => { + it("TC-061: downloadAuthFirmware throws in web mode (isTauriRuntime=false)", async () => { + const { downloadAuthFirmware } = + await import("@/features/batch-flash-auth/auth-firmware"); + await expect( + downloadAuthFirmware({ + version: "1.1.0", + chip: "t5ai", + url: "https://example.com/fw.bin", + sha256: "abc123", + }), + ).rejects.toThrow("download requires desktop runtime"); + }); +}); + +// ─── S05 canStart / isBusy guards ───────────────────────────────────────── + +describe("S05 canStart guards", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("canStart is false when isBusy (active slot exists)", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3", "COM5"]); + store.authConfig.excelPath = "/auth.xlsx"; + store.slots[0].status = "authorizing"; + store.slots[1].status = "idle"; + // isBusy is true, but canStart checks idle slots and inputsValid, not isBusy directly + // canStart = slots.some(idle) && inputsValid — COM5 is idle + expect(store.canStart).toBe(true); // idle slot exists + expect(store.isBusy).toBe(true); // authorizing slot makes isBusy true + }); + + it("isBusy returns false when all slots are idle/done/failed/skipped", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3", "COM5"]); + store.slots[0].status = "done"; + store.slots[1].status = "failed"; + expect(store.isBusy).toBe(false); + }); + + it("isBusy returns true for each active status", () => { + for (const status of ["flashing", "reading_mac", "authorizing"] as const) { + setActivePinia(createPinia()); + const s = useBatchFlashAuthStore(); + s.addPorts(["COM3"]); + s.slots[0].status = status; + expect(s.isBusy).toBe(true); + } + }); +}); + +// ─── S05 startBatch web no-op ────────────────────────────────────────────── + +describe("S05 startBatch — web mode no-op", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("startBatch does not throw and leaves slots unchanged in web mode", async () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.authConfig.excelPath = "/auth.xlsx"; + await store.startBatch(); // isTauriRuntime()=false → early return + expect(store.slots[0].status).toBe("idle"); + }); + + it("cancelAll does not throw in web mode", async () => { + const store = useBatchFlashAuthStore(); + await expect(store.cancelAll()).resolves.toBeUndefined(); + }); + + it("cancelPort does not throw in web mode", async () => { + const store = useBatchFlashAuthStore(); + await expect(store.cancelPort("COM3")).resolves.toBeUndefined(); + }); +}); + +// ─── S09 persistence — web mode no-ops ──────────────────────────────────── + +describe("S09 workspace persistence — web mode", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("TC-066: loadPersistedData is a no-op in web mode — returns without throwing", async () => { + const store = useBatchFlashAuthStore(); + await expect(store.loadPersistedData()).resolves.toBeUndefined(); + }); + + it("loadPersistedData does not alter state in web mode", async () => { + const store = useBatchFlashAuthStore(); + store.chipId = "t5ai"; + store.authConfig.excelPath = "/my/path.xlsx"; + await store.loadPersistedData(); + // State should remain as-set (workspace returns empty in web mode) + expect(store.authConfig.excelPath).toBe("/my/path.xlsx"); + }); +}); + +// ─── Miscellaneous correctness ──────────────────────────────────────────── + +describe("dismissBanner", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("dismissBanner clears the completion banner", () => { + const store = useBatchFlashAuthStore(); + store.completionBanner = { kind: "all-success", count: 1 }; + store.dismissBanner(); + expect(store.completionBanner).toBeNull(); + }); +}); + +describe("currentStats computed", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("counts active/done/failed/skipped correctly", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["a", "b", "c", "d", "e"]); + store.slots[0].status = "authorizing"; + store.slots[1].status = "done"; + store.slots[2].status = "failed"; + store.slots[3].status = "skipped"; + // slots[4] remains idle + expect(store.currentStats).toEqual({ + active: 1, + done: 1, + failed: 1, + skipped: 1, + }); + }); +}); + +describe("addPorts — deduplication with mixed ports", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("deduplicates across multiple addPorts calls", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["/dev/ttyUSB0", "/dev/ttyUSB1"]); + store.addPorts(["/dev/ttyUSB1", "/dev/ttyUSB2"]); + expect(store.slots).toHaveLength(3); + expect(store.slots.map((s) => s.port)).toEqual([ + "/dev/ttyUSB0", + "/dev/ttyUSB1", + "/dev/ttyUSB2", + ]); + }); +}); diff --git a/src/stores/batch-flash-auth.ts b/src/stores/batch-flash-auth.ts index f1b0459..f84866b 100644 --- a/src/stores/batch-flash-auth.ts +++ b/src/stores/batch-flash-auth.ts @@ -51,9 +51,6 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { // ── Session state ───────────────────────────────────────────────────────── const slots = ref([]); - // O(1) port→slot lookup; kept in sync by addPorts/removeSlot/addBlockedPort. - // Falls back to linear scan for test cases that assign store.slots directly. - const _slotMap = new Map(); const chipId = ref("esp32"); const baudRate = ref(115200); const authBaudRate = ref(115200); @@ -112,7 +109,7 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { // ── Slot helpers ────────────────────────────────────────────────────────── function findSlot(port: string): BatchSlotState | undefined { - return _slotMap.get(port) ?? slots.value.find((s) => s.port === port); + return slots.value.find((s) => s.port === port); } function updateSlot(port: string, patch: Partial) { @@ -132,7 +129,6 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { currentPhase: "", }; slots.value.push(entry); - _slotMap.set(port, entry); existing.add(port); } } @@ -147,7 +143,6 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { slot.status === "skipped" ) { slots.value = slots.value.filter((s) => s.port !== port); - _slotMap.delete(port); } } @@ -249,6 +244,7 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { progress: 0, currentPhase: "reading_mac", error: undefined, + excelError: undefined, }); } @@ -288,6 +284,7 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { progress: 0, currentPhase: "", error: undefined, + excelError: undefined, }); } await startBatch(); @@ -367,7 +364,12 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { scheduleSaveStats(); checkBatchCompletion(); } else if (step === "skipped") { - updateSlot(port, { status: "skipped", currentPhase: "", mac: ev.mac }); + updateSlot(port, { + status: "skipped", + currentPhase: "", + mac: ev.mac, + excelError: ev.excelError, + }); checkBatchCompletion(); } else if (step === "cancelled") { // Rust cancelled the slot (e.g. user hit cancel mid-flight). @@ -438,13 +440,6 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { if (!filterConfig.value.blockedPorts.includes(normalized)) { filterConfig.value.blockedPorts.push(normalized); } - // Remove idle slots that are now filtered; sync _slotMap accordingly - const removed = slots.value.filter( - (s) => - s.status === "idle" && - filterConfig.value.blockedPorts.includes(normalizePortName(s.port)), - ); - for (const s of removed) _slotMap.delete(s.port); slots.value = slots.value.filter( (s) => s.status !== "idle" || From 088d8a1c54b0cd2b7a8b0432c45034067a4c6cec Mon Sep 17 00:00:00 2001 From: YangJie Date: Fri, 26 Jun 2026 13:46:11 +0800 Subject: [PATCH 2/5] test(batch-auth): dissolve extended test file into proper locations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New store tests (removeSlot statuses, excelError field, retryFailed, isBusy, addBlockedPort, resetAuthStats, checkBatchCompletion gate, web-mode no-ops, dismissBanner, currentStats) → batch-flash-auth.test.ts - TC-061 downloadAuthFirmware web-mode guard → auth-firmware.test.ts - port-filter and filterByChip tests were already covered by their co-located test files; duplicate sections dropped - Delete batch-flash-auth-extended.test.ts (non-standard naming) --- .../batch-flash-auth/auth-firmware.test.ts | 19 +- src/stores/batch-flash-auth-extended.test.ts | 590 ------------------ src/stores/batch-flash-auth.test.ts | 384 ++++++++++++ 3 files changed, 402 insertions(+), 591 deletions(-) delete mode 100644 src/stores/batch-flash-auth-extended.test.ts diff --git a/src/features/batch-flash-auth/auth-firmware.test.ts b/src/features/batch-flash-auth/auth-firmware.test.ts index 56d5b98..8c9e37d 100644 --- a/src/features/batch-flash-auth/auth-firmware.test.ts +++ b/src/features/batch-flash-auth/auth-firmware.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from "vitest"; -import { filterByChip, AUTH_FIRMWARE_SOURCES } from "./auth-firmware"; +import { + filterByChip, + AUTH_FIRMWARE_SOURCES, + downloadAuthFirmware, +} from "./auth-firmware"; import type { AuthFirmwareEntry } from "./types"; const mk = (version: string, chip: string): AuthFirmwareEntry => ({ @@ -69,3 +73,16 @@ describe("AUTH_FIRMWARE_SOURCES", () => { } }); }); + +describe("downloadAuthFirmware", () => { + it("throws in web mode (isTauriRuntime=false)", async () => { + await expect( + downloadAuthFirmware({ + version: "1.1.0", + chip: "t5ai", + url: "https://example.com/fw.bin", + sha256: "abc123", + }), + ).rejects.toThrow("download requires desktop runtime"); + }); +}); diff --git a/src/stores/batch-flash-auth-extended.test.ts b/src/stores/batch-flash-auth-extended.test.ts deleted file mode 100644 index f9efe07..0000000 --- a/src/stores/batch-flash-auth-extended.test.ts +++ /dev/null @@ -1,590 +0,0 @@ -// src/stores/batch-flash-auth-extended.test.ts -// Extended unit tests for batch-flash-auth feature (covers test plan TODO items). -// Tests for confirmed bugs are marked [BUG TC-XXX] and are expected to fail until fixed. -import { describe, it, expect, beforeEach, vi } from "vitest"; -import { setActivePinia, createPinia } from "pinia"; -import { useBatchFlashAuthStore } from "./batch-flash-auth"; -import { - normalizePortName, - applyPortFilter, -} from "@/features/batch-flash-auth/port-filter"; -import { filterByChip } from "@/features/batch-flash-auth/auth-firmware"; -import type { AuthFirmwareEntry } from "@/features/batch-flash-auth/types"; - -vi.mock("@/runtime", () => ({ - isTauriRuntime: () => false, -})); - -// ─── S02 Slot state machine ──────────────────────────────────────────────── - -describe("S02 slot state machine — additional", () => { - beforeEach(() => setActivePinia(createPinia())); - - it("TC-015: removeSlot cannot remove a flashing slot", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3"]); - store.slots[0].status = "flashing"; - store.removeSlot("COM3"); - expect(store.slots).toHaveLength(1); - }); - - it("TC-015b: removeSlot cannot remove a reading_mac slot", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3"]); - store.slots[0].status = "reading_mac"; - store.removeSlot("COM3"); - expect(store.slots).toHaveLength(1); - }); - - it("TC-015c: removeSlot cannot remove an authorizing slot", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3"]); - store.slots[0].status = "authorizing"; - store.removeSlot("COM3"); - expect(store.slots).toHaveLength(1); - }); - - it("TC-015d: removeSlot removes a failed slot", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3"]); - store.slots[0].status = "failed"; - // failed is not in allowed remove-statuses per store code; verify current behavior - store.removeSlot("COM3"); - // per removeSlot implementation: allowed = idle | done | skipped - // failed is NOT in that list, so slot remains - expect(store.slots).toHaveLength(1); - }); - - it("TC-016: updateSlot Object.assign semantics — mac preserved when only updating progress", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3"]); - store.slots[0].mac = "aabbccddeeff"; - // Call handleAuthProgress done to test updateSlot merging - store.batchStartTime = Date.now(); - store.handleAuthProgress({ - port: "COM3", - step: "done", - mac: "aabbccddeeff", - }); - // Patch only progress via a subsequent reading_mac (to simulate merge) - // Direct: set mac then update only progress via slots ref - store.slots[0].mac = "112233445566"; - store.handleAuthProgress({ port: "COM3", step: "reading_mac" }); - // reading_mac only updates status and currentPhase, mac is preserved - expect(store.slots[0].mac).toBe("112233445566"); - }); - - it("TC-017: excelError: undefined in updateSlot patch clears the field", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3"]); - store.slots[0].excelError = "row not found"; - // cancelled step calls updateSlot with excelError: undefined - store.handleAuthProgress({ port: "COM3", step: "cancelled" }); - expect(store.slots[0].excelError).toBeUndefined(); - }); - - it("TC-018: initial slot has correct default fields", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3"]); - const s = store.slots[0]; - expect(s.status).toBe("idle"); - expect(s.progress).toBe(0); - expect(s.currentPhase).toBe(""); - expect(s.mac).toBeUndefined(); - expect(s.error).toBeUndefined(); - expect(s.excelError).toBeUndefined(); - }); -}); - -// ─── S03 Flash progress — additional ────────────────────────────────────── - -describe("S03 handleFlashProgress — port isolation", () => { - beforeEach(() => setActivePinia(createPinia())); - - it("TC-023: flash progress only updates the matching port", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3", "COM5"]); - store.slots[0].status = "flashing"; - store.slots[1].status = "flashing"; - store.handleFlashProgress({ - port: "COM3", - event: { kind: "percent", value: 75 }, - }); - expect(store.slots[0].progress).toBe(75); - expect(store.slots[1].progress).toBe(0); // COM5 unchanged - }); - - it("TC-023b: auth progress only updates the matching port", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3", "COM5"]); - store.batchStartTime = Date.now(); - store.handleAuthProgress({ port: "COM3", step: "done", mac: "aabb" }); - expect(store.slots[0].status).toBe("done"); - expect(store.slots[1].status).toBe("idle"); // COM5 unchanged - }); -}); - -// ─── S04 Auth progress — BUG tests ──────────────────────────────────────── - -describe("S04 handleAuthProgress — skipped excelError [TC-030]", () => { - beforeEach(() => setActivePinia(createPinia())); - - it("TC-030: skipped step propagates excelError to slot", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3"]); - store.batchStartTime = Date.now(); - store.handleAuthProgress({ - port: "COM3", - step: "skipped", - mac: "aabbccddeeff", - excelError: "row not found in spreadsheet", - }); - expect(store.slots[0].excelError).toBe("row not found in spreadsheet"); - }); - - it("TC-030: skipped step without excelError leaves excelError undefined", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3"]); - store.batchStartTime = Date.now(); - store.handleAuthProgress({ - port: "COM3", - step: "skipped", - mac: "aabbccddeeff", - }); - expect(store.slots[0].excelError).toBeUndefined(); - }); - - it("cancelled step correctly clears excelError (for contrast)", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3"]); - store.slots[0].excelError = "pre-existing error"; - store.handleAuthProgress({ port: "COM3", step: "cancelled" }); - expect(store.slots[0].excelError).toBeUndefined(); - }); - - it("done step preserves excelError when present in event", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3"]); - store.batchStartTime = Date.now(); - store.handleAuthProgress({ - port: "COM3", - step: "done", - mac: "aabb", - excelError: "confirm failed", - }); - // done step DOES propagate excelError (line 349 in store) - expect(store.slots[0].excelError).toBe("confirm failed"); - }); -}); - -// ─── S05 Batch operations — BUG tests ───────────────────────────────────── - -describe("S05 retryFailed — excelError cleared [TC-037]", () => { - beforeEach(() => setActivePinia(createPinia())); - - it("TC-037: retryFailed clears excelError on failed slot", async () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3"]); - store.slots[0].status = "failed"; - store.slots[0].error = "some error"; - store.slots[0].excelError = "confirm_row failed"; - await store.retryFailed(); - expect(store.slots[0].excelError).toBeUndefined(); - }); - - it("retryFailed: error is cleared (correct behavior)", async () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3"]); - store.slots[0].status = "failed"; - store.slots[0].error = "timeout"; - await store.retryFailed(); - expect(store.slots[0].error).toBeUndefined(); - }); - - it("TC-038: retryFailed only affects failed slots, not done/skipped", async () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3", "COM5", "COM7"]); - store.slots[0].status = "failed"; - store.slots[1].status = "done"; - store.slots[1].mac = "aabb"; - store.slots[2].status = "skipped"; - store.slots[2].mac = "ccdd"; - await store.retryFailed(); - expect(store.slots[0].status).toBe("idle"); // failed → reset to idle - expect(store.slots[1].status).toBe("done"); // done unchanged - expect(store.slots[1].mac).toBe("aabb"); - expect(store.slots[2].status).toBe("skipped"); // skipped unchanged - expect(store.slots[2].mac).toBe("ccdd"); - }); - - it("retryFailed: canRetry gate — no-op when no failed slots", async () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3"]); - store.completionBanner = { kind: "all-success", count: 1 }; - await store.retryFailed(); // canRetry is false - expect(store.completionBanner).not.toBeNull(); // banner not cleared - }); -}); - -// ─── S06 Completion banner — additional ─────────────────────────────────── - -describe("S06 completion banner — additional", () => { - beforeEach(() => setActivePinia(createPinia())); - - it("TC-049: checkBatchCompletion no-ops when any slot is still active", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3", "COM5"]); - store.batchStartTime = Date.now(); - store.slots[0].status = "authorizing"; // active - store.slots[1].status = "done"; - store.checkBatchCompletion(); - expect(store.completionBanner).toBeNull(); // no banner while active - }); - - it("TC-049b: banner appears only after all active slots finish", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3", "COM5"]); - store.batchStartTime = Date.now(); - store.currentBatchPorts = ["COM3", "COM5"]; - store.slots[0].status = "authorizing"; - store.slots[1].status = "done"; - store.checkBatchCompletion(); - expect(store.completionBanner).toBeNull(); - // Now finish the last active slot - store.slots[0].status = "done"; - store.checkBatchCompletion(); - expect(store.completionBanner?.kind).toBe("all-success"); - }); - - it("TC-051: resetAuthStats resets only auth cumulative to zero", () => { - const store = useBatchFlashAuthStore(); - store.cumulativeStats.auth = { total: 10, success: 8, fail: 2 }; - store.cumulativeStats.flash = { total: 5, success: 4, fail: 1 }; - store.resetAuthStats(); - expect(store.cumulativeStats.auth).toEqual({ - total: 0, - success: 0, - fail: 0, - }); - // flash stats should remain untouched - expect(store.cumulativeStats.flash).toEqual({ - total: 5, - success: 4, - fail: 1, - }); - }); - - it("TC-051b: after resetAuthStats, subsequent auth events accumulate from 0", () => { - const store = useBatchFlashAuthStore(); - store.cumulativeStats.auth = { total: 10, success: 8, fail: 2 }; - store.resetAuthStats(); - store.addPorts(["COM3"]); - store.batchStartTime = Date.now(); - store.handleAuthProgress({ port: "COM3", step: "done", mac: "aabb" }); - expect(store.cumulativeStats.auth.total).toBe(1); - expect(store.cumulativeStats.auth.success).toBe(1); - }); -}); - -// ─── S07 Port filter — pure functions ───────────────────────────────────── - -describe("S07 port-filter pure functions", () => { - it("TC-052: normalizePortName — on non-Windows platform, COM port NOT uppercased", () => { - // On Linux (test environment), isWindowsPlatform() returns false - // com3 stays as-is since we're not on Windows - expect(normalizePortName("com3")).toBe("com3"); - }); - - it("TC-053: normalizePortName — Unix-style path unchanged", () => { - expect(normalizePortName("/dev/ttyUSB0")).toBe("/dev/ttyUSB0"); - expect(normalizePortName("/dev/cu.usbserial-0001")).toBe( - "/dev/cu.usbserial-0001", - ); - }); - - it("TC-052c: normalizePortName — already uppercase COM port unchanged on any platform", () => { - // On Linux, COM3 is returned unchanged (no transformation for non-Windows) - expect(normalizePortName("COM3")).toBe("COM3"); - }); - - it("TC-054: applyPortFilter removes blocked ports", () => { - const result = applyPortFilter( - ["/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyUSB2"], - ["/dev/ttyUSB1"], - ); - expect(result).toEqual(["/dev/ttyUSB0", "/dev/ttyUSB2"]); - }); - - it("TC-055: applyPortFilter preserves non-blocked ports", () => { - const result = applyPortFilter( - ["/dev/ttyUSB0", "/dev/ttyUSB1"], - ["/dev/ttyUSB9"], - ); - expect(result).toEqual(["/dev/ttyUSB0", "/dev/ttyUSB1"]); - }); - - it("TC-056: applyPortFilter with empty blocklist returns all ports", () => { - const ports = ["/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyUSB2"]; - expect(applyPortFilter(ports, [])).toEqual(ports); - }); - - it("TC-056b: applyPortFilter with all ports blocked returns empty array", () => { - const result = applyPortFilter( - ["/dev/ttyUSB0", "/dev/ttyUSB1"], - ["/dev/ttyUSB0", "/dev/ttyUSB1"], - ); - expect(result).toEqual([]); - }); -}); - -// ─── S07 Port filter — store integration ────────────────────────────────── - -describe("S07 addBlockedPort / removeBlockedPort — store", () => { - beforeEach(() => setActivePinia(createPinia())); - - it("addBlockedPort adds to blockedPorts list", () => { - const store = useBatchFlashAuthStore(); - store.addBlockedPort("/dev/ttyUSB0"); - expect(store.filterConfig.blockedPorts).toContain("/dev/ttyUSB0"); - }); - - it("addBlockedPort removes idle slot that matches the newly blocked port", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["/dev/ttyUSB0", "/dev/ttyUSB1"]); - store.addBlockedPort("/dev/ttyUSB0"); - expect(store.slots.map((s) => s.port)).not.toContain("/dev/ttyUSB0"); - expect(store.slots.map((s) => s.port)).toContain("/dev/ttyUSB1"); - }); - - it("addBlockedPort does NOT remove active slot matching the blocked port", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["/dev/ttyUSB0"]); - store.slots[0].status = "authorizing"; // active - store.addBlockedPort("/dev/ttyUSB0"); - expect(store.slots).toHaveLength(1); // still there because it's active - }); - - it("addBlockedPort is idempotent — no duplicates in blocklist", () => { - const store = useBatchFlashAuthStore(); - store.addBlockedPort("/dev/ttyUSB0"); - store.addBlockedPort("/dev/ttyUSB0"); - expect( - store.filterConfig.blockedPorts.filter((p) => p === "/dev/ttyUSB0"), - ).toHaveLength(1); - }); - - it("removeBlockedPort removes from blockedPorts list", () => { - const store = useBatchFlashAuthStore(); - store.addBlockedPort("/dev/ttyUSB0"); - store.removeBlockedPort("/dev/ttyUSB0"); - expect(store.filterConfig.blockedPorts).not.toContain("/dev/ttyUSB0"); - }); - - it("filterActive computed: true when blockedPorts non-empty", () => { - const store = useBatchFlashAuthStore(); - expect(store.filterActive).toBe(false); - store.addBlockedPort("/dev/ttyUSB0"); - expect(store.filterActive).toBe(true); - store.removeBlockedPort("/dev/ttyUSB0"); - expect(store.filterActive).toBe(false); - }); -}); - -// ─── S08 Auth firmware pure functions ───────────────────────────────────── - -describe("S08 filterByChip", () => { - const entries: AuthFirmwareEntry[] = [ - { version: "1.0.0", chip: "t5ai", url: "u1", sha256: "h1" }, - { version: "1.2.0", chip: "t5ai", url: "u2", sha256: "h2" }, - { version: "1.10.0", chip: "t5ai", url: "u3", sha256: "h3" }, - { version: "2.0.0", chip: "esp32", url: "u4", sha256: "h4" }, - { version: "1.1.0", chip: "t5ai", url: "u5", sha256: "h5" }, - ]; - - it("TC-058: returns entries sorted descending by version (Intl numeric)", () => { - const result = filterByChip(entries, "t5ai"); - const versions = result.map((e) => e.version); - expect(versions).toEqual(["1.10.0", "1.2.0", "1.1.0", "1.0.0"]); - }); - - it("TC-059: returns only entries for the matching chip", () => { - const result = filterByChip(entries, "t5ai"); - expect(result.every((e) => e.chip === "t5ai")).toBe(true); - expect(result).toHaveLength(4); - }); - - it("TC-060: returns empty array when chip has no entries in manifest", () => { - expect(filterByChip(entries, "gd32")).toEqual([]); - expect(filterByChip([], "t5ai")).toEqual([]); - }); - - it("TC-060b: returns empty array when entries list is empty", () => { - expect(filterByChip([], "t5ai")).toHaveLength(0); - }); - - it("filterByChip: esp32 entries returned correctly", () => { - const result = filterByChip(entries, "esp32"); - expect(result).toHaveLength(1); - expect(result[0].version).toBe("2.0.0"); - }); - - it("version comparison: v prefix stripped correctly", () => { - const withV: AuthFirmwareEntry[] = [ - { version: "v1.0.0", chip: "t5ai", url: "u1", sha256: "h1" }, - { version: "v1.2.0", chip: "t5ai", url: "u2", sha256: "h2" }, - { version: "v1.10.0", chip: "t5ai", url: "u3", sha256: "h3" }, - ]; - const result = filterByChip(withV, "t5ai"); - expect(result.map((e) => e.version)).toEqual([ - "v1.10.0", - "v1.2.0", - "v1.0.0", - ]); - }); -}); - -// ─── S08 downloadAuthFirmware ────────────────────────────────────────────── - -describe("S08 downloadAuthFirmware — web mode guard", () => { - it("TC-061: downloadAuthFirmware throws in web mode (isTauriRuntime=false)", async () => { - const { downloadAuthFirmware } = - await import("@/features/batch-flash-auth/auth-firmware"); - await expect( - downloadAuthFirmware({ - version: "1.1.0", - chip: "t5ai", - url: "https://example.com/fw.bin", - sha256: "abc123", - }), - ).rejects.toThrow("download requires desktop runtime"); - }); -}); - -// ─── S05 canStart / isBusy guards ───────────────────────────────────────── - -describe("S05 canStart guards", () => { - beforeEach(() => setActivePinia(createPinia())); - - it("canStart is false when isBusy (active slot exists)", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3", "COM5"]); - store.authConfig.excelPath = "/auth.xlsx"; - store.slots[0].status = "authorizing"; - store.slots[1].status = "idle"; - // isBusy is true, but canStart checks idle slots and inputsValid, not isBusy directly - // canStart = slots.some(idle) && inputsValid — COM5 is idle - expect(store.canStart).toBe(true); // idle slot exists - expect(store.isBusy).toBe(true); // authorizing slot makes isBusy true - }); - - it("isBusy returns false when all slots are idle/done/failed/skipped", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3", "COM5"]); - store.slots[0].status = "done"; - store.slots[1].status = "failed"; - expect(store.isBusy).toBe(false); - }); - - it("isBusy returns true for each active status", () => { - for (const status of ["flashing", "reading_mac", "authorizing"] as const) { - setActivePinia(createPinia()); - const s = useBatchFlashAuthStore(); - s.addPorts(["COM3"]); - s.slots[0].status = status; - expect(s.isBusy).toBe(true); - } - }); -}); - -// ─── S05 startBatch web no-op ────────────────────────────────────────────── - -describe("S05 startBatch — web mode no-op", () => { - beforeEach(() => setActivePinia(createPinia())); - - it("startBatch does not throw and leaves slots unchanged in web mode", async () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["COM3"]); - store.authConfig.excelPath = "/auth.xlsx"; - await store.startBatch(); // isTauriRuntime()=false → early return - expect(store.slots[0].status).toBe("idle"); - }); - - it("cancelAll does not throw in web mode", async () => { - const store = useBatchFlashAuthStore(); - await expect(store.cancelAll()).resolves.toBeUndefined(); - }); - - it("cancelPort does not throw in web mode", async () => { - const store = useBatchFlashAuthStore(); - await expect(store.cancelPort("COM3")).resolves.toBeUndefined(); - }); -}); - -// ─── S09 persistence — web mode no-ops ──────────────────────────────────── - -describe("S09 workspace persistence — web mode", () => { - beforeEach(() => setActivePinia(createPinia())); - - it("TC-066: loadPersistedData is a no-op in web mode — returns without throwing", async () => { - const store = useBatchFlashAuthStore(); - await expect(store.loadPersistedData()).resolves.toBeUndefined(); - }); - - it("loadPersistedData does not alter state in web mode", async () => { - const store = useBatchFlashAuthStore(); - store.chipId = "t5ai"; - store.authConfig.excelPath = "/my/path.xlsx"; - await store.loadPersistedData(); - // State should remain as-set (workspace returns empty in web mode) - expect(store.authConfig.excelPath).toBe("/my/path.xlsx"); - }); -}); - -// ─── Miscellaneous correctness ──────────────────────────────────────────── - -describe("dismissBanner", () => { - beforeEach(() => setActivePinia(createPinia())); - - it("dismissBanner clears the completion banner", () => { - const store = useBatchFlashAuthStore(); - store.completionBanner = { kind: "all-success", count: 1 }; - store.dismissBanner(); - expect(store.completionBanner).toBeNull(); - }); -}); - -describe("currentStats computed", () => { - beforeEach(() => setActivePinia(createPinia())); - - it("counts active/done/failed/skipped correctly", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["a", "b", "c", "d", "e"]); - store.slots[0].status = "authorizing"; - store.slots[1].status = "done"; - store.slots[2].status = "failed"; - store.slots[3].status = "skipped"; - // slots[4] remains idle - expect(store.currentStats).toEqual({ - active: 1, - done: 1, - failed: 1, - skipped: 1, - }); - }); -}); - -describe("addPorts — deduplication with mixed ports", () => { - beforeEach(() => setActivePinia(createPinia())); - - it("deduplicates across multiple addPorts calls", () => { - const store = useBatchFlashAuthStore(); - store.addPorts(["/dev/ttyUSB0", "/dev/ttyUSB1"]); - store.addPorts(["/dev/ttyUSB1", "/dev/ttyUSB2"]); - expect(store.slots).toHaveLength(3); - expect(store.slots.map((s) => s.port)).toEqual([ - "/dev/ttyUSB0", - "/dev/ttyUSB1", - "/dev/ttyUSB2", - ]); - }); -}); diff --git a/src/stores/batch-flash-auth.test.ts b/src/stores/batch-flash-auth.test.ts index f9b2335..1dff58a 100644 --- a/src/stores/batch-flash-auth.test.ts +++ b/src/stores/batch-flash-auth.test.ts @@ -549,3 +549,387 @@ describe("useBatchFlashAuthStore firmware source", () => { expect(store.defaultFirmwareStatus).toBe("idle"); }); }); + +describe("removeSlot — additional statuses", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("cannot remove a reading_mac slot", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].status = "reading_mac"; + store.removeSlot("COM3"); + expect(store.slots).toHaveLength(1); + }); + + it("cannot remove an authorizing slot", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].status = "authorizing"; + store.removeSlot("COM3"); + expect(store.slots).toHaveLength(1); + }); + + it("cannot remove a failed slot", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].status = "failed"; + store.removeSlot("COM3"); + expect(store.slots).toHaveLength(1); + }); +}); + +describe("slot defaults and field merging", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("initial slot has correct default fields including excelError", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + const s = store.slots[0]; + expect(s.status).toBe("idle"); + expect(s.progress).toBe(0); + expect(s.currentPhase).toBe(""); + expect(s.mac).toBeUndefined(); + expect(s.error).toBeUndefined(); + expect(s.excelError).toBeUndefined(); + }); + + it("updateSlot patch only changes named fields — mac preserved when updating phase", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].mac = "112233445566"; + store.handleAuthProgress({ port: "COM3", step: "reading_mac" }); + expect(store.slots[0].mac).toBe("112233445566"); + }); + + it("excelError: undefined in patch clears the field", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].excelError = "row not found"; + store.handleAuthProgress({ port: "COM3", step: "cancelled" }); + expect(store.slots[0].excelError).toBeUndefined(); + }); +}); + +describe("handleFlashProgress — port isolation", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("only updates the matching port", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3", "COM5"]); + store.slots[0].status = "flashing"; + store.slots[1].status = "flashing"; + store.handleFlashProgress({ + port: "COM3", + event: { kind: "percent", value: 75 }, + }); + expect(store.slots[0].progress).toBe(75); + expect(store.slots[1].progress).toBe(0); + }); +}); + +describe("handleAuthProgress — excelError", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("skipped step propagates excelError to slot", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.batchStartTime = Date.now(); + store.handleAuthProgress({ + port: "COM3", + step: "skipped", + mac: "aabbccddeeff", + excelError: "row not found in spreadsheet", + }); + expect(store.slots[0].excelError).toBe("row not found in spreadsheet"); + }); + + it("skipped step without excelError leaves excelError undefined", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.batchStartTime = Date.now(); + store.handleAuthProgress({ + port: "COM3", + step: "skipped", + mac: "aabbccddeeff", + }); + expect(store.slots[0].excelError).toBeUndefined(); + }); + + it("done step propagates excelError when present in event", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.batchStartTime = Date.now(); + store.handleAuthProgress({ + port: "COM3", + step: "done", + mac: "aabb", + excelError: "confirm failed", + }); + expect(store.slots[0].excelError).toBe("confirm failed"); + }); +}); + +describe("retryFailed", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("clears excelError on failed slot", async () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].status = "failed"; + store.slots[0].error = "some error"; + store.slots[0].excelError = "confirm_row failed"; + await store.retryFailed(); + expect(store.slots[0].excelError).toBeUndefined(); + }); + + it("clears error on failed slot", async () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].status = "failed"; + store.slots[0].error = "timeout"; + await store.retryFailed(); + expect(store.slots[0].error).toBeUndefined(); + }); + + it("only resets failed slots — done and skipped are unchanged", async () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3", "COM5", "COM7"]); + store.slots[0].status = "failed"; + store.slots[1].status = "done"; + store.slots[1].mac = "aabb"; + store.slots[2].status = "skipped"; + store.slots[2].mac = "ccdd"; + await store.retryFailed(); + expect(store.slots[0].status).toBe("idle"); + expect(store.slots[1].status).toBe("done"); + expect(store.slots[1].mac).toBe("aabb"); + expect(store.slots[2].status).toBe("skipped"); + expect(store.slots[2].mac).toBe("ccdd"); + }); + + it("is a no-op when canRetry is false (no failed slots)", async () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.completionBanner = { kind: "all-success", count: 1 }; + await store.retryFailed(); + expect(store.completionBanner).not.toBeNull(); + }); +}); + +describe("checkBatchCompletion — active slot gate", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("no-ops when any slot is still active", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3", "COM5"]); + store.batchStartTime = Date.now(); + store.slots[0].status = "authorizing"; + store.slots[1].status = "done"; + store.checkBatchCompletion(); + expect(store.completionBanner).toBeNull(); + }); + + it("banner appears only after all active slots finish", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3", "COM5"]); + store.batchStartTime = Date.now(); + store.currentBatchPorts = ["COM3", "COM5"]; + store.slots[0].status = "authorizing"; + store.slots[1].status = "done"; + store.checkBatchCompletion(); + expect(store.completionBanner).toBeNull(); + store.slots[0].status = "done"; + store.checkBatchCompletion(); + expect(store.completionBanner?.kind).toBe("all-success"); + }); +}); + +describe("resetAuthStats", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("resets only auth cumulative to zero, flash stats unchanged", () => { + const store = useBatchFlashAuthStore(); + store.cumulativeStats.auth = { total: 10, success: 8, fail: 2 }; + store.cumulativeStats.flash = { total: 5, success: 4, fail: 1 }; + store.resetAuthStats(); + expect(store.cumulativeStats.auth).toEqual({ + total: 0, + success: 0, + fail: 0, + }); + expect(store.cumulativeStats.flash).toEqual({ + total: 5, + success: 4, + fail: 1, + }); + }); + + it("subsequent auth events accumulate from zero after reset", () => { + const store = useBatchFlashAuthStore(); + store.cumulativeStats.auth = { total: 10, success: 8, fail: 2 }; + store.resetAuthStats(); + store.addPorts(["COM3"]); + store.batchStartTime = Date.now(); + store.handleAuthProgress({ port: "COM3", step: "done", mac: "aabb" }); + expect(store.cumulativeStats.auth.total).toBe(1); + expect(store.cumulativeStats.auth.success).toBe(1); + }); +}); + +describe("addBlockedPort / removeBlockedPort", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("addBlockedPort adds to blockedPorts list", () => { + const store = useBatchFlashAuthStore(); + store.addBlockedPort("/dev/ttyUSB0"); + expect(store.filterConfig.blockedPorts).toContain("/dev/ttyUSB0"); + }); + + it("addBlockedPort removes idle slot that matches the newly blocked port", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["/dev/ttyUSB0", "/dev/ttyUSB1"]); + store.addBlockedPort("/dev/ttyUSB0"); + expect(store.slots.map((s) => s.port)).not.toContain("/dev/ttyUSB0"); + expect(store.slots.map((s) => s.port)).toContain("/dev/ttyUSB1"); + }); + + it("addBlockedPort does NOT remove an active slot", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["/dev/ttyUSB0"]); + store.slots[0].status = "authorizing"; + store.addBlockedPort("/dev/ttyUSB0"); + expect(store.slots).toHaveLength(1); + }); + + it("addBlockedPort is idempotent — no duplicates in blocklist", () => { + const store = useBatchFlashAuthStore(); + store.addBlockedPort("/dev/ttyUSB0"); + store.addBlockedPort("/dev/ttyUSB0"); + expect( + store.filterConfig.blockedPorts.filter((p) => p === "/dev/ttyUSB0"), + ).toHaveLength(1); + }); + + it("removeBlockedPort removes from blockedPorts list", () => { + const store = useBatchFlashAuthStore(); + store.addBlockedPort("/dev/ttyUSB0"); + store.removeBlockedPort("/dev/ttyUSB0"); + expect(store.filterConfig.blockedPorts).not.toContain("/dev/ttyUSB0"); + }); + + it("filterActive reflects blockedPorts non-empty state", () => { + const store = useBatchFlashAuthStore(); + expect(store.filterActive).toBe(false); + store.addBlockedPort("/dev/ttyUSB0"); + expect(store.filterActive).toBe(true); + store.removeBlockedPort("/dev/ttyUSB0"); + expect(store.filterActive).toBe(false); + }); +}); + +describe("isBusy computed", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("is false when all slots are terminal (done / failed / skipped / idle)", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3", "COM5"]); + store.slots[0].status = "done"; + store.slots[1].status = "failed"; + expect(store.isBusy).toBe(false); + }); + + it("is true for each active status", () => { + for (const status of ["flashing", "reading_mac", "authorizing"] as const) { + setActivePinia(createPinia()); + const s = useBatchFlashAuthStore(); + s.addPorts(["COM3"]); + s.slots[0].status = status; + expect(s.isBusy).toBe(true); + } + }); +}); + +describe("startBatch / cancelAll / cancelPort — web mode no-ops", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("startBatch does not throw and leaves slots unchanged", async () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.authConfig.excelPath = "/auth.xlsx"; + await store.startBatch(); + expect(store.slots[0].status).toBe("idle"); + }); + + it("cancelAll does not throw", async () => { + const store = useBatchFlashAuthStore(); + await expect(store.cancelAll()).resolves.toBeUndefined(); + }); + + it("cancelPort does not throw", async () => { + const store = useBatchFlashAuthStore(); + await expect(store.cancelPort("COM3")).resolves.toBeUndefined(); + }); +}); + +describe("loadPersistedData — web mode", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("is a no-op — returns without throwing", async () => { + const store = useBatchFlashAuthStore(); + await expect(store.loadPersistedData()).resolves.toBeUndefined(); + }); + + it("does not alter state", async () => { + const store = useBatchFlashAuthStore(); + store.chipId = "t5ai"; + store.authConfig.excelPath = "/my/path.xlsx"; + await store.loadPersistedData(); + expect(store.authConfig.excelPath).toBe("/my/path.xlsx"); + }); +}); + +describe("dismissBanner", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("clears the completion banner", () => { + const store = useBatchFlashAuthStore(); + store.completionBanner = { kind: "all-success", count: 1 }; + store.dismissBanner(); + expect(store.completionBanner).toBeNull(); + }); +}); + +describe("currentStats computed", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("counts active / done / failed / skipped correctly", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["a", "b", "c", "d", "e"]); + store.slots[0].status = "authorizing"; + store.slots[1].status = "done"; + store.slots[2].status = "failed"; + store.slots[3].status = "skipped"; + expect(store.currentStats).toEqual({ + active: 1, + done: 1, + failed: 1, + skipped: 1, + }); + }); +}); + +describe("addPorts — cross-call deduplication", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("deduplicates ports across multiple addPorts calls", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["/dev/ttyUSB0", "/dev/ttyUSB1"]); + store.addPorts(["/dev/ttyUSB1", "/dev/ttyUSB2"]); + expect(store.slots).toHaveLength(3); + expect(store.slots.map((s) => s.port)).toEqual([ + "/dev/ttyUSB0", + "/dev/ttyUSB1", + "/dev/ttyUSB2", + ]); + }); +}); From 8fb97095a1fb54430d720aa1f259f10455f9dc66 Mon Sep 17 00:00:00 2001 From: YangJie Date: Fri, 26 Jun 2026 13:48:25 +0800 Subject: [PATCH 3/5] docs(claude): require checking existing test files before creating new ones --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CLAUDE.md b/CLAUDE.md index f56e49e..8fa761b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -248,5 +248,6 @@ PRs that modify `crates/tyutool-cli/src/main.rs` (command definitions) without u ### Testing - Test files live next to their source, same name with `.test.ts` suffix; Rust uses inline `#[cfg(test)] mod tests` +- **Before creating a test file:** run `ls` in the source file's directory to check whether a co-located `.test.ts` already exists. If it does, append to it — never create a parallel file with suffixes like `-extended`, `-v2`, etc. - Pure logic (utility functions, type conversions) must have unit tests; Vue components and stores as needed - Frontend tests run in the `node` environment — no DOM From 9a16b54b02fd512659a6e6d9981fa281fc47439d Mon Sep 17 00:00:00 2001 From: YangJie Date: Fri, 26 Jun 2026 13:59:04 +0800 Subject: [PATCH 4/5] chore: bump version to 3.0.14 --- Cargo.lock | 6 +++--- crates/tyutool-cli/Cargo.toml | 2 +- crates/tyutool-core/Cargo.toml | 2 +- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b7e4929..7c649a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5874,7 +5874,7 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "tyutool-cli" -version = "3.0.11" +version = "3.0.14" dependencies = [ "anyhow", "base64 0.22.1", @@ -5905,7 +5905,7 @@ dependencies = [ [[package]] name = "tyutool-core" -version = "3.0.11" +version = "3.0.14" dependencies = [ "crc32fast", "espflash", @@ -5919,7 +5919,7 @@ dependencies = [ [[package]] name = "tyutool_gui" -version = "3.0.11" +version = "3.0.14" dependencies = [ "calamine", "log", diff --git a/crates/tyutool-cli/Cargo.toml b/crates/tyutool-cli/Cargo.toml index 3b19dec..ba1f529 100644 --- a/crates/tyutool-cli/Cargo.toml +++ b/crates/tyutool-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tyutool-cli" -version = "3.0.11" +version = "3.0.14" edition = "2021" description = "tyutool CLI — shared tyutool-core flash API (no Tauri)" diff --git a/crates/tyutool-core/Cargo.toml b/crates/tyutool-core/Cargo.toml index 522338e..89d9d20 100644 --- a/crates/tyutool-core/Cargo.toml +++ b/crates/tyutool-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tyutool-core" -version = "3.0.11" +version = "3.0.14" edition = "2021" description = "Shared flash/erase plugin registry and serial helpers for tyutool CLI and GUI" diff --git a/package.json b/package.json index 4ecb868..dda2a6c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "tyutool", "private": true, - "version": "3.0.11", + "version": "3.0.14", "type": "module", "scripts": { "clean": "node -e \"try { require('fs').rmSync('dist', { recursive: true, force: true }); } catch (e) { if (e && e.code !== 'ENOENT') throw e; }\"", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 21ce390..d6145b6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tyutool_gui" -version = "3.0.11" +version = "3.0.14" description = "tyutool — Tauri shell for tyutool-core (firmware / serial tooling)" authors = ["tyutool contributors"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index bfd0a54..2ba8bd2 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "tyutool", - "version": "3.0.11", + "version": "3.0.14", "identifier": "com.tyutool.desktop", "build": { "beforeDevCommand": "pnpm run dev", From 296cc44abdb01ceba1273121fbe24279b64b3db9 Mon Sep 17 00:00:00 2001 From: YangJie Date: Fri, 26 Jun 2026 14:09:37 +0800 Subject: [PATCH 5/5] fix(clippy): allow too_many_arguments on run_batch_auth_slot --- crates/tyutool-core/src/authorize.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/tyutool-core/src/authorize.rs b/crates/tyutool-core/src/authorize.rs index b83fca0..8348e7c 100644 --- a/crates/tyutool-core/src/authorize.rs +++ b/crates/tyutool-core/src/authorize.rs @@ -1007,6 +1007,7 @@ where /// The caller pre-allocates `uuid`/`authkey` from an Excel row. On return: /// - `Done`/`AlreadyDone` → caller should confirm the Excel row (mark USED). /// - `Skipped`/`Err`/`Cancelled` → caller should release the Excel row. +#[allow(clippy::too_many_arguments)] pub fn run_batch_auth_slot( port: &str, chip_id: &str,