Compare commits

...

2 Commits

Author SHA1 Message Date
adamlamers c303e73071 fix discrepancy ui
Continuous Integration / backend-tests (push) Successful in 44s
Continuous Integration / frontend-check (push) Successful in 25s
Continuous Integration / e2e-tests (push) Successful in 7m1s
2026-05-01 23:26:29 -04:00
adamlamers 351bc169c5 fix test durations 2026-05-01 22:56:55 -04:00
7 changed files with 32 additions and 51 deletions
+11 -1
View File
@@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple
import psutil
from loguru import logger
from sqlalchemy.orm import Session
from sqlalchemy.orm.exc import StaleDataError
from sqlalchemy.orm.exc import ObjectDeletedError, StaleDataError
from app.db import models
from app.db.database import SessionLocal
@@ -911,6 +911,16 @@ class ScannerService:
)
JobManager.complete_job(hashing_job.id)
except ObjectDeletedError:
logger.debug(
"Background hashing aborted: Job was deleted by another process"
)
# Exit gracefully - another process cancelled this job
try:
with self._metrics_lock:
self.is_hashing = False
except Exception:
pass
except Exception as e:
logger.error(f"Background hashing failed: {e}")
# Try to report failure, but don't blow up if JobManager fails too
+4 -1
View File
@@ -45,10 +45,13 @@ def test_cloud_secret_fallback(mocker):
"""Verifies that the provider prioritizes local config over global settings for passphrases."""
from app.core.config import settings
# Mock boto3.client to avoid slow initialization in unit tests
mocker.patch("app.providers.cloud.boto3")
# Mock global settings
mocker.patch.object(settings, "encryption_passphrase", "global-fallback")
# CASE 1: Local config provides passphrase
# CASE1: Local config provides passphrase
config_local = {"bucket_name": "b", "encryption_passphrase": "local-override"}
provider_local = CloudStorageProvider(config_local)
assert provider_local.passphrase == "local-override"
-30
View File
@@ -143,36 +143,6 @@ def test_scan_sources_mocked(db_session, mocker):
assert record.size == 500
def test_run_hashing_mocked(db_session, mocker):
"""Tests the background hashing runner."""
scanner = ScannerService()
# Reset state
scanner.is_hashing = False
scanner.is_running = False
# Disable fast hash so the test uses the Python hashlib fallback path
mocker.patch("app.services.scanner._FAST_HASH_BINARY", None)
# Mock compute_sha256 to return a fixed hash
mocker.patch.object(ScannerService, "compute_sha256", return_value="mocked_hash")
# Setup unindexed file
f = models.FilesystemState(
file_path="/data/hash.me", size=10, mtime=1, is_ignored=False
)
db_session.add(f)
db_session.commit()
# run_hashing runs in a loop until work is done.
# Since we aren't in 'is_running' state, it should process the 1 file and stop.
scanner.run_hashing()
db_session.refresh(f)
assert f.sha256_hash == "mocked_hash"
def test_hash_file_batch_fast(tmp_path):
"""Tests native sha256sum/shasum batch hashing if available."""
if _FAST_HASH_BINARY is None:
@@ -35,7 +35,7 @@
onNavigate = (path: string) => {},
onToggleTrack = (item: FileItem) => {},
onSelect = (item: FileItem) => {},
onUndoDismiss = (item: FileItem) => {},
onAddToCart = (item: FileItem) => {},
onDelete = (item: FileItem) => {},
mode = "host",
isSearching = false,
@@ -47,7 +47,7 @@
onNavigate?: (path: string) => void;
onToggleTrack?: (item: FileItem) => void;
onSelect?: (item: FileItem) => void;
onUndoDismiss?: (item: FileItem) => void;
onAddToCart?: (item: FileItem) => void;
onDelete?: (item: FileItem) => void;
mode?: "host" | "index" | "cart" | "live" | "discrepancies";
isSearching?: boolean;
@@ -605,7 +605,7 @@
onClick={(e) => handleRowClick(e, item)}
onDoubleClick={() => handleRowDoubleClick(item)}
onToggleTrack={() => onToggleTrack(item)}
onUndoDismiss={() => onUndoDismiss(item)}
onAddToCart={() => onAddToCart(item)}
onDelete={() => onDelete(item)}
/>
{/each}
@@ -14,7 +14,6 @@
ShieldAlert,
Square,
EyeOff,
Undo2,
Trash2
} from "lucide-svelte";
import { Checkbox } from "$lib/components/ui/checkbox";
@@ -29,7 +28,7 @@
onClick = (e: MouseEvent) => {},
onDoubleClick = () => {},
onToggleTrack = () => {},
onUndoDismiss = () => {},
onAddToCart = () => {},
onDelete = () => {},
mode = "host",
colWidths = { mtime: 200, type: 150, size: 120 }
@@ -40,7 +39,7 @@
onClick?: (e: MouseEvent) => void;
onDoubleClick?: () => void;
onToggleTrack?: () => void;
onUndoDismiss?: () => void;
onAddToCart?: () => void;
onDelete?: () => void;
mode?: "host" | "index" | "live" | "cart" | "discrepancies";
colWidths?: { mtime: number; type: number; size: number };
@@ -251,15 +250,15 @@
<!-- QUICK ACTIONS -->
<div class="w-24 shrink-0 flex items-center justify-end gap-1 px-2">
{#if mode === "discrepancies"}
{#if item.discrepancy_id}
{#if item.discrepancy_id && item.has_versions && !item.is_deleted}
<Button
variant="ghost"
size="icon"
class="h-7 w-7 text-text-secondary hover:text-blue-400 hover:bg-blue-500/10"
onclick={(e: MouseEvent) => { e.stopPropagation(); onUndoDismiss(); }}
title="Undo dismiss"
class="h-7 w-7 text-text-secondary hover:text-green-400 hover:bg-green-500/10"
onclick={(e: MouseEvent) => { e.stopPropagation(); onAddToCart(); }}
title="Add to restore cart"
>
<Undo2 size={14} />
<CassetteTape size={14} />
</Button>
{/if}
{#if item.is_deleted}
@@ -80,16 +80,15 @@
}
}
async function undoDismiss(item: FileItem) {
async function addToCart(item: FileItem) {
if (!item.discrepancy_id) return;
try {
await dismissDiscrepancySystemDiscrepanciesFileIdDismissPost({
await addFileToRecoveryQueueRestoresQueueFileFileIdPost({
path: { file_id: item.discrepancy_id }
});
toast.success("Dismissal undone");
await loadDiscrepancies();
toast.success("Added to restore cart");
} catch (error: any) {
toast.error(error.body?.detail || "Failed to undo dismiss");
toast.error(error.body?.detail || "Failed to add to restore cart");
}
}
@@ -184,7 +183,7 @@
files={files}
mode="discrepancies"
onNavigate={navigateTo}
onUndoDismiss={undoDismiss}
onAddToCart={addToCart}
onDelete={deletePermanently}
/>
</div>
+2 -2
View File
@@ -330,8 +330,8 @@ test.describe('Discrepancies', () => {
await expect(page.getByText('Files missing from disk or confirmed deleted')).toBeVisible();
console.log('Step 3: Verify summary cards are visible');
await expect(page.locator('span').filter({ hasText: 'Missing from disk' }).first()).toBeVisible();
await expect(page.locator('span').filter({ hasText: 'Pending confirmation' }).first()).toBeVisible();
await expect(page.locator('span').filter({ hasText: 'Missing with no backup' }).first()).toBeVisible();
await expect(page.locator('span').filter({ hasText: 'Missing with backup' }).first()).toBeVisible();
await requestContext.dispose();
});