Prepare 0.3.0-rc.2 with Windows support and regression tests

This commit is contained in:
2026-04-04 12:35:36 -04:00
parent a00d68b725
commit 220296c3f7
23 changed files with 448 additions and 123 deletions

View File

@@ -2,6 +2,16 @@
All notable changes to this project will be documented in this file.
## [0.3.0-rc.2] - 2026-04-04
### Release Engineering
- Added Windows-specific contributor scripts for the core `just install-w`, `just dev`, and `just check` path, while keeping the broader Unix-oriented utility and release recipes unchanged for now.
- Updated the release checklist and contributor docs to call out the supported Windows RC.2 workflow and the manual Windows verification follow-up required before promotion.
### Regression Coverage
- Added a startup regression test for the RC.1 security fix: an invalid config on a configured instance with existing users no longer re-enables unauthenticated setup-only access.
- Expanded Playwright stabilization coverage for retry countdown rendering, Library & Intake manual scan success/failure surfacing, and completed job detail rendering from persisted encode stats.
## [0.3.0-rc.1] - 2026-04-04
### Security

View File

@@ -42,6 +42,9 @@ just install-w # Windows
just dev
```
Windows contributor support in RC.2 is limited to the core `install/dev/check`
path. Broader utility and release recipes remain Unix-first for now.
Additional setup and workflow docs live in:
- `docs/docs/contributing/development.md`

2
Cargo.lock generated
View File

@@ -13,7 +13,7 @@ dependencies = [
[[package]]
name = "alchemist"
version = "0.3.0-rc.1"
version = "0.3.0-rc.2"
dependencies = [
"anyhow",
"argon2",

View File

@@ -1,6 +1,6 @@
[package]
name = "alchemist"
version = "0.3.0-rc.1"
version = "0.3.0-rc.2"
edition = "2024"
rust-version = "1.85"
license = "GPL-3.0"

View File

@@ -102,25 +102,27 @@ On Windows, run `alchemist.exe` instead.
### From Source
For macOS and Linux:
```bash
git clone https://github.com/bybrooklyn/alchemist.git
cd alchemist
just install # macOS / Linux
just install-w # Windows
just install
just build
./target/release/alchemist
```
Alchemist requires Rust 1.85 or later (MSRV). Use `rustup update stable` to ensure you are on a recent toolchain, and make sure FFmpeg is installed separately.
For local development instead of a release build:
For Windows local development in RC.2:
```bash
just install # macOS / Linux
just install-w # Windows
just install-w
just dev
```
The broader `just` release and utility recipes are still Unix-first in RC.2.
## First Run
1. Open [http://localhost:3000](http://localhost:3000).

View File

@@ -5,21 +5,26 @@
Use the repo bump script for version changes:
```bash
just bump 0.3.0-rc.1
just bump 0.3.0-rc.2
```
Then complete the release-candidate preflight:
1. Update `CHANGELOG.md` and `docs/docs/changelog.md`.
2. Run `just release-check`.
3. Verify the repo version surfaces all read `0.3.0-rc.1`.
3. Verify the repo version surfaces all read `0.3.0-rc.2`.
4. Complete the manual smoke checklist:
- Docker fresh install over plain HTTP, including login and first dashboard load
- One packaged binary install and first-run setup
- Upgrade from an existing `0.2.x` instance with data preserved
- One successful encode, one skip, one intentional failure, and one notification test send
5. Commit the release-prep changes and merge them to `main`.
6. Create the annotated tag `v0.3.0-rc.1` on the exact merged commit.
5. Complete the Windows contributor follow-up on a real Windows machine:
- `just install-w`
- `just dev`
- `just check`
- Note that broader utility and release recipes remain Unix-first for RC.2.
6. Commit the release-prep changes and merge them to `main`.
7. Create the annotated tag `v0.3.0-rc.2` on the exact merged commit.
## Stable promotion
@@ -33,6 +38,7 @@ Promote to stable only after the RC burn-in is complete and the same automated p
- Packaged binary first-run
- Upgrade from the most recent `0.2.x` or `0.3.0-rc.x`
- Encode, skip, failure, and notification verification
5. Confirm release notes, docs, and hardware-support wording match the tested release state.
6. Merge the stable release commit to `main`.
7. Create the annotated tag `v0.3.0` on the exact merged commit.
5. Re-run the Windows contributor verification checklist if Windows parity changed after RC.2.
6. Confirm release notes, docs, and hardware-support wording match the tested release state.
7. Merge the stable release commit to `main`.
8. Create the annotated tag `v0.3.0` on the exact merged commit.

View File

@@ -1 +1 @@
0.3.0-rc.1
0.3.0-rc.2

View File

@@ -3,6 +3,16 @@ title: Changelog
description: Release history for Alchemist.
---
## [0.3.0-rc.2] - 2026-04-04
### Release Engineering
- Added Windows-specific contributor scripts for the core `just install-w`, `just dev`, and `just check` path, while leaving broader utility and release recipes Unix-first for now.
- Updated the release checklist and contributor docs so RC.2 clearly documents the supported Windows workflow and the manual Windows verification follow-up required before stable.
### Regression Coverage
- Added a startup regression test for the RC.1 security fix so a config parse failure on a configured instance with existing users does not reopen setup-only access.
- Expanded Playwright stabilization coverage for retry countdown rendering, Library & Intake manual scan success/failure surfacing, and completed job detail rendering from persisted encode stats.
## [0.3.0-rc.1] - 2026-04-04
### Rust & Backend

View File

@@ -18,9 +18,9 @@ frontend tooling.
```bash
git clone https://github.com/bybrooklyn/alchemist.git
cd alchemist
just install # macOS / Linux
just install-w # Windows
just dev # build frontend assets, then start the backend
just install # macOS / Linux bootstrap
just install-w # Windows bootstrap
just dev # supported on both paths in RC.2
```
## Common tasks
@@ -28,15 +28,29 @@ just dev # build frontend assets, then start the backend
```bash
just install # macOS / Linux bootstrap
just install-w # Windows bootstrap
just check # fmt + clippy + typecheck + build (mirrors CI)
just check # supported on both paths in RC.2
just test # cargo test
just test-e2e # Playwright reliability suite
just db-reset # wipe dev DB, keep config
just db-reset-all # wipe DB and config (re-triggers wizard)
just bump 0.3.0 # bump version in all files
just update 0.3.0 # full guarded release flow
just bump 0.3.0-rc.2 # bump version in all files
just update 0.3.0-rc.2 # full guarded release flow
```
## Windows support in RC.2
Windows contributor support in RC.2 covers the core path:
- `just install-w`
- `just dev`
- `just check`
The following remain Unix-first for now and are deferred to RC.3 or later:
- broader `just` utility recipes such as database and Docker helpers
- release-oriented guarded flows such as `just update`
- full Playwright contributor parity outside the documented manual verification path
## Frontend only
```bash

View File

@@ -11,6 +11,9 @@ Alchemist is GPLv3 open source. Contributions are welcome.
- [`CONTRIBUTING.md`](https://github.com/bybrooklyn/alchemist/blob/main/CONTRIBUTING.md) for licensing and contribution terms
- [`RELEASING.md`](https://github.com/bybrooklyn/alchemist/blob/main/RELEASING.md) for release and RC checklists
RC.2 adds a supported Windows contributor path for `just install-w`,
`just dev`, and `just check`. Other `just` recipes are still Unix-first.
## Reporting bugs
1. Go to [GitHub Issues](https://github.com/bybrooklyn/alchemist/issues)

View File

@@ -75,6 +75,8 @@ alchemist.exe # Windows
## From source
For macOS and Linux:
```bash
git clone https://github.com/bybrooklyn/alchemist.git
cd alchemist
@@ -85,13 +87,16 @@ just build
Requires Rust 1.85+. Run `rustup update stable` first.
For day-to-day local development instead of a release build:
For Windows local development in RC.2:
```bash
just install
just install-w
just dev
```
Windows contributor support in RC.2 is limited to the core `install/dev/check`
path. Broader `just` release and utility recipes remain Unix-first.
## Nightly builds
```bash

View File

@@ -1,6 +1,6 @@
{
"name": "alchemist-docs",
"version": "0.3.0-rc.1",
"version": "0.3.0-rc.2",
"private": true,
"packageManager": "bun@1.3.5",
"scripts": {

View File

@@ -54,9 +54,16 @@ install-w:
@powershell.exe -NoLogo -ExecutionPolicy Bypass -File .\\scripts\\install_dev_windows.ps1
# Build frontend assets, then start the backend server
dev: web-build
dev:
@just {{ if os_family() == "windows" { "dev-w" } else { "dev-u" } }}
[private]
dev-u: web-build
@just run
dev-w:
@powershell.exe -NoLogo -ExecutionPolicy Bypass -File .\\scripts\\dev_windows.ps1
# Start the backend only
run:
cargo run
@@ -95,6 +102,10 @@ rust-build:
# Run all checks (fmt + clippy + typecheck + frontend build)
check:
@just {{ if os_family() == "windows" { "check-w" } else { "check-u" } }}
[private]
check-u:
@echo "── Rust format ──"
cargo fmt --all -- --check
@echo "── Rust clippy ──"
@@ -105,6 +116,9 @@ check:
cd web && bun install --frozen-lockfile && bun run typecheck && echo "── Frontend build ──" && bun run build
@echo "All checks passed ✓"
check-w:
@powershell.exe -NoLogo -ExecutionPolicy Bypass -File .\\scripts\\check_windows.ps1
# Rust checks only (faster)
check-rust:
cargo fmt --all -- --check

12
scripts/check_windows.ps1 Normal file
View File

@@ -0,0 +1,12 @@
. (Join-Path $PSScriptRoot "windows_common.ps1")
Require-Tool cargo "Install Rust via rustup or `winget install --id Rustlang.Rustup -e`."
Require-Tool bun "Install Bun via `winget install --id Oven-sh.Bun -e` or from https://bun.sh/docs/installation."
Invoke-Native -Command @("cargo", "fmt", "--all", "--", "--check") -Label "Rust format"
Invoke-Native -Command @("cargo", "clippy", "--all-targets", "--all-features", "--", "-D", "warnings") -Label "Rust clippy"
Invoke-Native -Command @("cargo", "check", "--all-targets") -Label "Rust check"
Invoke-Native -Command @("bun", "install", "--frozen-lockfile") -WorkingDirectory (Join-Path $RepoRoot "web") -Label "Web dependencies"
Invoke-Native -Command @("bun", "run", "verify") -WorkingDirectory (Join-Path $RepoRoot "web") -Label "Frontend verify"
Write-Host "All checks passed ✓"

9
scripts/dev_windows.ps1 Normal file
View File

@@ -0,0 +1,9 @@
. (Join-Path $PSScriptRoot "windows_common.ps1")
Require-Tool cargo "Install Rust via rustup or `winget install --id Rustlang.Rustup -e`."
Require-Tool bun "Install Bun via `winget install --id Oven-sh.Bun -e` or from https://bun.sh/docs/installation."
Warn-If-Missing ffmpeg "Install FFmpeg with `winget install Gyan.FFmpeg`."
Invoke-Native -Command @("bun", "install", "--frozen-lockfile") -WorkingDirectory (Join-Path $RepoRoot "web") -Label "Web dependencies"
Invoke-Native -Command @("bun", "run", "build") -WorkingDirectory (Join-Path $RepoRoot "web") -Label "Frontend build"
Invoke-Native -Command @("cargo", "run") -Label "Backend run"

View File

@@ -1,59 +1,13 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Require-Tool {
param(
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[string]$Hint
)
if (Get-Command $Name -ErrorAction SilentlyContinue) {
return
}
Write-Error "Required tool '$Name' was not found on PATH. $Hint"
}
function Warn-If-Missing {
param(
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[string]$Hint
)
if (Get-Command $Name -ErrorAction SilentlyContinue) {
return
}
Write-Warning "Optional tool '$Name' was not found on PATH. $Hint"
}
$RepoRoot = Split-Path -Parent $PSScriptRoot
. (Join-Path $PSScriptRoot "windows_common.ps1")
Require-Tool cargo "Install Rust via rustup or `winget install --id Rustlang.Rustup -e`."
Require-Tool bun "Install Bun via `winget install --id Oven-sh.Bun -e` or from https://bun.sh/docs/installation."
Write-Host "── Rust dependencies ──"
& cargo fetch --locked
Write-Host "── Web dependencies ──"
Push-Location (Join-Path $RepoRoot "web")
& bun install --frozen-lockfile
Pop-Location
Write-Host "── Docs dependencies ──"
Push-Location (Join-Path $RepoRoot "docs")
& bun install --frozen-lockfile
Pop-Location
Write-Host "── E2E dependencies ──"
Push-Location (Join-Path $RepoRoot "web-e2e")
& bun install --frozen-lockfile
& bunx playwright install chromium
Pop-Location
Invoke-Native -Command @("cargo", "fetch", "--locked") -Label "Rust dependencies"
Invoke-Native -Command @("bun", "install", "--frozen-lockfile") -WorkingDirectory (Join-Path $RepoRoot "web") -Label "Web dependencies"
Invoke-Native -Command @("bun", "install", "--frozen-lockfile") -WorkingDirectory (Join-Path $RepoRoot "docs") -Label "Docs dependencies"
Invoke-Native -Command @("bun", "install", "--frozen-lockfile") -WorkingDirectory (Join-Path $RepoRoot "web-e2e") -Label "E2E dependencies"
Invoke-Native -Command @("bunx", "playwright", "install", "chromium") -WorkingDirectory (Join-Path $RepoRoot "web-e2e")
Warn-If-Missing ffmpeg "Install FFmpeg with `winget install Gyan.FFmpeg`."

View File

@@ -0,0 +1,62 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$script:RepoRoot = Split-Path -Parent $PSScriptRoot
function Require-Tool {
param(
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[string]$Hint
)
if (Get-Command $Name -ErrorAction SilentlyContinue) {
return
}
throw "Required tool '$Name' was not found on PATH. $Hint"
}
function Warn-If-Missing {
param(
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[string]$Hint
)
if (Get-Command $Name -ErrorAction SilentlyContinue) {
return
}
Write-Warning "Optional tool '$Name' was not found on PATH. $Hint"
}
function Invoke-Native {
param(
[Parameter(Mandatory = $true)]
[string[]]$Command,
[string]$WorkingDirectory = $script:RepoRoot,
[string]$Label = ""
)
if ($Label) {
Write-Host "── $Label ──"
}
Push-Location $WorkingDirectory
try {
if ($Command.Count -gt 1) {
& $Command[0] @($Command[1..($Command.Count - 1)])
} else {
& $Command[0]
}
if ($LASTEXITCODE -ne 0) {
throw "Command failed ($LASTEXITCODE): $($Command -join ' ')"
}
} finally {
Pop-Location
}
}

View File

@@ -111,6 +111,52 @@ fn orphaned_temp_output_path(output_path: &str) -> PathBuf {
PathBuf::from(format!("{output_path}.alchemist.tmp"))
}
fn load_startup_config(config_path: &Path, is_server_mode: bool) -> (config::Config, bool, bool) {
let config_exists = config_path.exists();
let (config, setup_mode) = if !config_exists {
let cwd = std::env::current_dir().ok();
info!(
target: "startup",
"Config file not found at {:?} (cwd={:?})",
config_path,
cwd
);
if is_server_mode {
info!("No configuration file found. Entering Setup Mode (Web UI).");
(config::Config::default(), true)
} else {
warn!("No configuration file found. Using defaults.");
(config::Config::default(), false)
}
} else {
match config::Config::load(config_path) {
Ok(c) => (c, false),
Err(e) => {
warn!(
"Failed to load config file at {:?}: {}. Using defaults.",
config_path, e
);
if is_server_mode {
warn!(
"Config load failed in server mode. \
Will check for existing users before \
entering Setup Mode."
);
(config::Config::default(), false)
} else {
(config::Config::default(), false)
}
}
}
};
(config, setup_mode, config_exists)
}
fn should_enter_setup_mode_for_missing_users(is_server_mode: bool, has_users: bool) -> bool {
is_server_mode && !has_users
}
async fn run() -> Result<()> {
// Initialize logging
tracing_subscriber::fmt()
@@ -180,48 +226,8 @@ async fn run() -> Result<()> {
let config_path = runtime::config_path();
let db_path = runtime::db_path();
let config_mutable = runtime::config_mutable();
let config_exists = config_path.exists();
let (config, mut setup_mode) = if !config_exists {
let cwd = std::env::current_dir().ok();
info!(
target: "startup",
"Config file not found at {:?} (cwd={:?})",
config_path,
cwd
);
if is_server_mode {
info!("No configuration file found. Entering Setup Mode (Web UI).");
(config::Config::default(), true)
} else {
// CLI mode requires config or explicit args
warn!("No configuration file found. Using defaults.");
(config::Config::default(), false)
}
} else {
match config::Config::load(config_path.as_path()) {
Ok(c) => (c, false),
Err(e) => {
warn!(
"Failed to load config file at {:?}: {}. Using defaults.",
config_path, e
);
if is_server_mode {
warn!(
"Config load failed in server mode. \
Will check for existing users before \
entering Setup Mode."
);
// Do not force setup_mode=true here.
// The user-check below will set it if needed.
// If users exist, we start with defaults but
// do NOT re-enable unauthenticated setup endpoints.
(config::Config::default(), false)
} else {
(config::Config::default(), false)
}
}
}
};
let (config, mut setup_mode, config_exists) =
load_startup_config(config_path.as_path(), is_server_mode);
info!(
target: "startup",
"Config loaded (path={:?}, exists={}, mutable={}, setup_mode={}) in {} ms",
@@ -372,7 +378,7 @@ async fn run() -> Result<()> {
has_users,
users_start.elapsed().as_millis()
);
if !has_users {
if should_enter_setup_mode_for_missing_users(is_server_mode, has_users) {
if !setup_mode {
info!("No users found. Entering Setup Mode (Web UI).");
}
@@ -862,6 +868,55 @@ mod tests {
);
}
#[tokio::test]
async fn invalid_config_with_existing_users_does_not_reenter_setup_mode()
-> std::result::Result<(), Box<dyn std::error::Error>> {
let db_path = temp_db_path("alchemist_invalid_config_users");
let config_path = temp_config_path("alchemist_invalid_config_users");
std::fs::write(&config_path, "not-valid = [")?;
let db = db::Db::new(db_path.to_string_lossy().as_ref()).await?;
db.create_user("admin", "hash").await?;
let (_config, setup_mode, config_exists) = load_startup_config(config_path.as_path(), true);
let has_users = db.has_users().await?;
let final_setup_mode =
setup_mode || should_enter_setup_mode_for_missing_users(true, has_users);
assert!(config_exists);
assert!(has_users);
assert!(!setup_mode);
assert!(!final_setup_mode);
let _ = std::fs::remove_file(config_path);
let _ = std::fs::remove_file(db_path);
Ok(())
}
#[tokio::test]
async fn invalid_config_without_users_still_enters_setup_mode()
-> std::result::Result<(), Box<dyn std::error::Error>> {
let db_path = temp_db_path("alchemist_invalid_config_setup");
let config_path = temp_config_path("alchemist_invalid_config_setup");
std::fs::write(&config_path, "not-valid = [")?;
let db = db::Db::new(db_path.to_string_lossy().as_ref()).await?;
let (_config, setup_mode, config_exists) = load_startup_config(config_path.as_path(), true);
let has_users = db.has_users().await?;
let final_setup_mode =
setup_mode || should_enter_setup_mode_for_missing_users(true, has_users);
assert!(config_exists);
assert!(!has_users);
assert!(!setup_mode);
assert!(final_setup_mode);
let _ = std::fs::remove_file(config_path);
let _ = std::fs::remove_file(db_path);
Ok(())
}
#[tokio::test]
async fn config_reload_refreshes_runtime_hardware_state()
-> std::result::Result<(), Box<dyn std::error::Error>> {

View File

@@ -1,6 +1,6 @@
{
"name": "alchemist-web-e2e",
"version": "0.3.0-rc.1",
"version": "0.3.0-rc.2",
"private": true,
"packageManager": "bun@1",
"type": "module",
@@ -8,7 +8,7 @@
"test": "playwright test",
"test:headed": "playwright test --headed",
"test:ui": "playwright test --ui",
"test:reliability": "playwright test tests/settings-nonok.spec.ts tests/setup-recovery.spec.ts tests/setup-happy-path.spec.ts tests/new-user-redirect.spec.ts tests/stats-poller.spec.ts tests/jobs-actions-nonok.spec.ts"
"test:reliability": "playwright test tests/settings-nonok.spec.ts tests/setup-recovery.spec.ts tests/setup-happy-path.spec.ts tests/new-user-redirect.spec.ts tests/stats-poller.spec.ts tests/jobs-actions-nonok.spec.ts tests/jobs-stability.spec.ts tests/library-intake-stability.spec.ts"
},
"devDependencies": {
"@playwright/test": "^1.54.2"

View File

@@ -111,6 +111,7 @@ export interface JobFixture {
progress: number;
created_at: string;
updated_at: string;
attempt_count?: number;
vmaf_score?: number;
decision_reason?: string;
}

View File

@@ -0,0 +1,105 @@
import { expect, test } from "@playwright/test";
import {
type JobDetailFixture,
type JobFixture,
fulfillJson,
mockEngineStatus,
mockJobDetails,
} from "./helpers";
const completedJob: JobFixture = {
id: 41,
input_path: "/media/completed-stability.mkv",
output_path: "/output/completed-stability-av1.mkv",
status: "completed",
priority: 0,
progress: 100,
created_at: "2025-01-01T00:00:00Z",
updated_at: "2025-01-04T00:00:00Z",
vmaf_score: 95.4,
};
const completedDetail: JobDetailFixture = {
job: completedJob,
metadata: {
duration_secs: 120,
codec_name: "hevc",
width: 3840,
height: 2160,
bit_depth: 10,
size_bytes: 4_000_000_000,
video_bitrate_bps: 15_000_000,
container_bitrate_bps: 15_500_000,
fps: 24,
container: "mkv",
audio_codec: "aac",
audio_channels: 6,
dynamic_range: "hdr10",
},
encode_stats: {
input_size_bytes: 4_000_000_000,
output_size_bytes: 1_800_000_000,
compression_ratio: 0.45,
encode_time_seconds: 3600,
encode_speed: 1.25,
avg_bitrate_kbps: 7000,
vmaf_score: 95.4,
},
job_logs: [
{
id: 10,
level: "info",
message: "Transcode completed successfully",
created_at: "2025-01-04T00:00:02Z",
},
],
};
test.use({ storageState: undefined });
test.beforeEach(async ({ page }) => {
await mockEngineStatus(page);
});
test("failed jobs waiting to retry show a retry countdown", async ({ page }) => {
const retryingJob: JobFixture = {
id: 40,
input_path: "/media/retrying.mkv",
output_path: "/output/retrying-av1.mkv",
status: "failed",
priority: 1,
progress: 100,
attempt_count: 4,
created_at: "2025-01-01T00:00:00Z",
updated_at: new Date().toISOString(),
decision_reason: "transcode_failed|ffmpeg exited 1",
};
await page.route("**/api/jobs/table**", async (route) => {
await fulfillJson(route, 200, [retryingJob]);
});
await page.goto("/jobs");
await expect(page.getByText("Retrying in 6h")).toBeVisible();
});
test("completed job detail renders persisted encode stats", async ({ page }) => {
await page.route("**/api/jobs/table**", async (route) => {
await fulfillJson(route, 200, [completedJob]);
});
await mockJobDetails(page, { 41: completedDetail });
await page.goto("/jobs");
await page.getByTitle("/media/completed-stability.mkv").click();
await expect(page.getByRole("dialog")).toBeVisible();
await expect(page.getByText("Encode Results")).toBeVisible();
await expect(page.getByText("Input size")).toBeVisible();
await expect(page.getByText("Output size")).toBeVisible();
await expect(page.locator("span").filter({ hasText: /^55\.0% saved$/ })).toBeVisible();
await expect(page.getByText("01:00:00")).toBeVisible();
await expect(page.getByText("1.25× realtime")).toBeVisible();
await expect(page.getByText("7000 kbps")).toBeVisible();
await expect(page.getByText("95.4").first()).toBeVisible();
});

View File

@@ -0,0 +1,60 @@
import { expect, test } from "@playwright/test";
import {
fulfillEmpty,
fulfillJson,
mockEngineStatus,
mockSettingsBundle,
} from "./helpers";
test.use({ storageState: undefined });
test.beforeEach(async ({ page }) => {
await mockEngineStatus(page);
await mockSettingsBundle(page);
await page.route("**/api/library/profiles", async (route) => {
await fulfillJson(route, 200, []);
});
await page.route("**/api/profiles/presets", async (route) => {
await fulfillJson(route, 200, []);
});
await page.route("**/api/profiles", async (route) => {
await fulfillJson(route, 200, []);
});
await page.route("**/api/settings/watch-dirs**", async (route) => {
await fulfillJson(route, 200, []);
});
await page.route("**/api/scan/status", async (route) => {
await fulfillJson(route, 200, {
is_running: false,
files_found: 0,
current_folder: null,
});
});
});
test("manual scan success is surfaced from Library & Intake", async ({ page }) => {
let scanStartCalls = 0;
await page.route("**/api/scan/start", async (route) => {
scanStartCalls += 1;
await fulfillEmpty(route, 202);
});
await page.goto("/settings?tab=watch");
await page.getByRole("button", { name: /scan now/i }).click();
await expect.poll(() => scanStartCalls).toBe(1);
await expect(page.getByRole("button", { name: "Scanning..." })).toBeVisible();
await expect(page.getByText("Library scan started.", { exact: true })).toBeVisible();
});
test("manual scan failures are surfaced from Library & Intake", async ({ page }) => {
await page.route("**/api/scan/start", async (route) => {
await fulfillJson(route, 503, { message: "Scanner unavailable" });
});
await page.goto("/settings?tab=watch");
await page.getByRole("button", { name: /scan now/i }).click();
await expect(page.getByText("Scanner unavailable").first()).toBeVisible();
});

View File

@@ -1,6 +1,6 @@
{
"name": "alchemist-web",
"version": "0.3.0-rc.1",
"version": "0.3.0-rc.2",
"private": true,
"packageManager": "bun@1",
"type": "module",