* fix(snapshot): prevent drift corruption from duplicate installs + boot-time mismatch (#161, #162) Closes #162 and #161 as the paired fix the issues describe — both touch the same data-loss surface and verify against the same dev-sync rig. installs at import time, log critical, register nothing in the alphabetical loser; client-side `window` sentinel as belt-and-suspenders; align .env.example / dev-iteration-loop / sync_to_dev / README at the Registry-derived `koolook/` path. compare against live /userdata + picks, set a drift flag on mismatch; gate periodic + pre-load autosave routing to `_unsaved_autosave/` while drifted; promote status pill to "drifted" precedence over "saved" with a recovery tooltip; auto-clear on the next markStateSaved (Save / Load). 26 new tests (14 install guard, 4 sidebar sentinel, 8 boot drift) — 316 total passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(snapshot): apply review fixes — canonical fingerprint + early loser exit + drift timestamp Review-driven follow-ups on PR #186: 1. **Symmetrize file-side fingerprint (MEDIUM).** `detectBootDrift` now uses a new `_canonicalStateFingerprint()` that normalizes the workflows tree through `normalizeWorkflowsStore` AND deep-sorts keys via `sortJsonValue`. Without this, hand-edited / cross-machine / pre-v0.3 files would false-positive as drifted on first boot (e.g. `archived: "false"` string vs `false` bool, missing `directories: {}`, key insertion-order asymmetry from the `{...wf, archived, module, tags}` spread). `_computeFingerprint` stays unchanged so the localStorage save baseline contract holds for existing users. New regression test verifies a shape-only diff does NOT trigger drift. 2. **Gate fork imports on `_is_winning_install` (LOW).** Non-winning duplicate installs now short-circuit before the heavy torch / PIL / diffusers / aiohttp imports, exiting in ~5 LoC instead of paying full import cost just to discard the mappings. 3. **Drift timestamp uses `driftDetectedAt`, not stale `lastNamedSaveAt` (LOW).** The localStorage save baseline could be from a prior session whose live state was already corrupt — surfacing it as "Last named save" in the drift tooltip would mislead. Replaced with the session-noticed-at timestamp from `_bootDriftDiagnostics`. 352 tests passing (316 prior + 26 from PR #186 + 1 new normalize- symmetry case). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(install-guard): fail safe on unreadable / non-UTF-8 siblings (#162) Re-review follow-up. The import-time duplicate-install scan could crash the entire plugin load (fail-silent — the alphabetical winner vanished too, not just the loser) on two unguarded paths: - detect_duplicate_koolook_installs: the per-entry is_dir()/is_file() over custom_nodes/ siblings was unguarded, so a non-traversable sibling (restrictive perms, broken mount, a special dir) raised PermissionError up into __init__.py. No duplicate even required. Now caught per-entry and skipped. - read_pyproject_version: read_text(encoding="utf-8") raises UnicodeDecodeError (a ValueError, NOT OSError) on a non-UTF-8 / binary pyproject.toml, escaping the `except OSError`. Now degrades to "?" via `except (OSError, ValueError)`. - __init__.py: wrapped the guard call site as a final backstop — any unforeseen guard failure now degrades to register-normally rather than aborting the import. Both paths reproduced before the fix; the first surfaced while running the suite from a /tmp checkout whose parent held a root-owned dir. 3 regression tests added (non-traversable sibling skipped; non-UTF-8 pyproject -> "?"; report survives a non-UTF-8 sibling). Full suite: 452 passed (449 + 3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: kflame99 <261674183+kflame99@users.noreply.github.com>
44 lines
2.2 KiB
JavaScript
44 lines
2.2 KiB
JavaScript
// SPDX-FileCopyrightText: 2025-2026 Kforge Labs <https://github.com/malkuthro/ComfyUI-Koolook>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
// =============================================================================
|
|
// Client-side duplicate-install guard (#162). Pulled out of
|
|
// `koolook_sidebar.js` so the sentinel + decision can be unit-tested under
|
|
// Node — the sidebar entry imports `../../../scripts/app.js` (ComfyUI's
|
|
// frontend app), which doesn't exist in a headless context.
|
|
//
|
|
// The backend `__init__.py` guard normally prevents a duplicate's `./web/`
|
|
// folder from being published in the first place. This module is the
|
|
// fallback: if a stale build / older Koolook copy on a sibling install
|
|
// ships a `koolook_sidebar.js` to the browser anyway, the sentinel set
|
|
// here makes sure only the first-loaded copy calls `app.registerExtension`.
|
|
//
|
|
// `globalScope` defaults to `window` for production but is parameterised
|
|
// so tests can pass a synthetic object and verify the contract without
|
|
// touching real browser globals.
|
|
// =============================================================================
|
|
|
|
export const SIDEBAR_SENTINEL_KEY = "__koolookCuratedSidebarRegistered";
|
|
|
|
// Returns `true` if no prior Koolook sidebar has registered on this
|
|
// scope (and sets the sentinel as a side-effect), `false` otherwise.
|
|
//
|
|
// The set-on-success contract means the helper is single-shot per
|
|
// scope: subsequent calls return `false` even from the same caller.
|
|
// That's the duplicate-detected signal — `koolook_sidebar.js` reads
|
|
// the return value as "should I register?".
|
|
export function claimSidebarRegistration(globalScope) {
|
|
const scope = globalScope || (typeof window !== "undefined" ? window : globalThis);
|
|
if (scope[SIDEBAR_SENTINEL_KEY]) return false;
|
|
scope[SIDEBAR_SENTINEL_KEY] = true;
|
|
return true;
|
|
}
|
|
|
|
// Reset the sentinel on a scope. Production code never calls this; it
|
|
// exists so tests can exercise the claim flow more than once inside a
|
|
// single Node process without leaking state across cases.
|
|
export function resetSidebarRegistrationForTesting(globalScope) {
|
|
const scope = globalScope || (typeof window !== "undefined" ? window : globalThis);
|
|
delete scope[SIDEBAR_SENTINEL_KEY];
|
|
}
|