mirror of
https://github.com/bybrooklyn/alchemist.git
synced 2026-04-18 01:43:34 -04:00
Prepare 0.3.0-rc.2 with Windows support and regression tests
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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
2
Cargo.lock
generated
@@ -13,7 +13,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alchemist"
|
||||
version = "0.3.0-rc.1"
|
||||
version = "0.3.0-rc.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
|
||||
@@ -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"
|
||||
|
||||
12
README.md
12
README.md
@@ -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).
|
||||
|
||||
20
RELEASING.md
20
RELEASING.md
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
16
justfile
16
justfile
@@ -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
12
scripts/check_windows.ps1
Normal 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
9
scripts/dev_windows.ps1
Normal 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"
|
||||
@@ -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`."
|
||||
|
||||
|
||||
62
scripts/windows_common.ps1
Normal file
62
scripts/windows_common.ps1
Normal 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
|
||||
}
|
||||
}
|
||||
141
src/main.rs
141
src/main.rs
@@ -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>> {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -111,6 +111,7 @@ export interface JobFixture {
|
||||
progress: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
attempt_count?: number;
|
||||
vmaf_score?: number;
|
||||
decision_reason?: string;
|
||||
}
|
||||
|
||||
105
web-e2e/tests/jobs-stability.spec.ts
Normal file
105
web-e2e/tests/jobs-stability.spec.ts
Normal 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();
|
||||
});
|
||||
60
web-e2e/tests/library-intake-stability.spec.ts
Normal file
60
web-e2e/tests/library-intake-stability.spec.ts
Normal 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();
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user