Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
cb251f7
test(audio): define canonical resource policy contract
seonghobae Aug 16, 2026
3e96ed6
feat(audio): add canonical resource policy
seonghobae Aug 16, 2026
f83a1ba
test(score): require bounded validated PDF reads
seonghobae Aug 16, 2026
d659d9d
refactor(core): expose bounded score reader module
seonghobae Aug 16, 2026
fc1af87
refactor(core): preserve public API through root module
seonghobae Aug 16, 2026
1df5fe0
fix(score): bound stored PDF reads before allocation
seonghobae Aug 16, 2026
521fe12
fix(score): use bounded native PDF reader
seonghobae Aug 16, 2026
ca20dc5
style(score): keep bounded reader rustfmt-clean
seonghobae Aug 16, 2026
e6d31ee
docs(changelog): record bounded stored-score reads
seonghobae Aug 16, 2026
051e39d
fix(core): preserve desktop-core package contract
seonghobae Aug 16, 2026
c11af77
test(score): cover non-file stored score reads
seonghobae Aug 16, 2026
f86e266
test(score): prove same-descriptor growth detection
seonghobae Aug 16, 2026
6aa0098
test(audio): require policy parity at orchestration and decode bounda…
seonghobae Aug 16, 2026
ee3b48c
fix(audio): add one-sample decode probe to resource policy
seonghobae Aug 16, 2026
a982e7f
fix(audio): bind temporal decode to canonical resource policy
seonghobae Aug 16, 2026
38b3fb6
fix(audio): enforce canonical policy before stem inference
seonghobae Aug 16, 2026
ed6e4f7
fix(audio): apply canonical byte policy at request preflight
seonghobae Aug 16, 2026
e7671de
docs(audio): record resource-boundary evidence
seonghobae Aug 16, 2026
9549647
docs(changelog): record canonical audio resource bounds
seonghobae Aug 16, 2026
b35bc12
docs(security): bind local audio to canonical resource policy
seonghobae Aug 16, 2026
710ed16
test(audio): reject resource-policy arithmetic overflow
seonghobae Aug 16, 2026
6e80527
fix(audio): fail closed on extreme sample-rate arithmetic
seonghobae Aug 16, 2026
572333e
style(audio): keep policy bounds formatter-clean
seonghobae Aug 16, 2026
adbfad0
test(audio): reject unrepresentable policy and decoder values
seonghobae Aug 16, 2026
428db0a
fix(audio): make resource policy arithmetic fully fail closed
seonghobae Aug 16, 2026
84f60b3
docs(audio): record checked policy arithmetic
seonghobae Aug 16, 2026
e42ff5c
test(audio): reject oversized desktop selection at the bridge
seonghobae Aug 16, 2026
05fbbb3
test(audio): enforce encoded-byte parity for imported sources
seonghobae Aug 16, 2026
b23e51a
fix(audio): enforce encoded-byte parity at desktop bridge
seonghobae Aug 16, 2026
509c18b
docs(changelog): record desktop audio-policy parity
seonghobae Aug 16, 2026
9823e0e
fix(changelog): restore full release history after parity note drift
seonghobae Aug 16, 2026
f631049
fix(ci): format audio resource policy regressions
seonghobae Aug 16, 2026
0ba0593
test(audio): reject fractional encoded byte metadata
seonghobae Aug 16, 2026
ceb7f71
fix(audio): require integral encoded byte metadata
seonghobae Aug 16, 2026
84f7691
test(audio): require native encoded-byte admission
seonghobae Aug 16, 2026
da676cb
chore(audio): stack native resource policy on bounded core root
seonghobae Aug 16, 2026
0488c8f
fix(audio): enforce native encoded-byte ceiling
seonghobae Aug 16, 2026
fbc7d7d
fix(audio): export native resource policy
seonghobae Aug 16, 2026
e61e858
fix(audio): enforce native bootstrap byte ceiling
seonghobae Aug 16, 2026
3f976e5
docs(audio): record native intake enforcement
seonghobae Aug 16, 2026
2b9d5e3
test(audio): require YouTube download to use canonical 100 MiB policy
cursoragent Aug 16, 2026
19064f4
fix(audio): abort YouTube downloads at the canonical encoded-byte cei…
cursoragent Aug 16, 2026
7b3b1e7
docs(audio): record YouTube download-time policy evidence
cursoragent Aug 16, 2026
d1a75c9
test(audio): cover fail-closed resource admission branches
seonghobae Aug 16, 2026
1f3fdb8
merge(audio): consolidate YouTube admission repair into canonical #866
seonghobae Aug 16, 2026
5e8fa77
fix(audio): delete owned YouTube partials on in-flight abort
cursoragent Aug 16, 2026
6df13ae
style(audio): wrap zero-byte decoder regression
seonghobae Aug 16, 2026
6d3b9b2
test(audio): reject malformed YouTube duration metadata
seonghobae Aug 16, 2026
86f7272
fix(audio): reject malformed YouTube duration metadata
seonghobae Aug 16, 2026
bc0fc98
docs(changelog): record YouTube duration metadata guard
seonghobae Aug 16, 2026
ec64aa7
test(audio): bound decoded buffer memory
seonghobae Aug 16, 2026
104573a
fix(audio): enforce decoded memory budget
seonghobae Aug 16, 2026
c6d9368
docs(audio): record decoded memory admission
seonghobae Aug 16, 2026
f60eb17
docs(audio): record decoded memory budget
seonghobae Aug 16, 2026
aeb42da
test(youtube): cover fail-closed duration conversion
seonghobae Aug 16, 2026
f35f1d4
test(youtube): avoid exceptional float subclasses
seonghobae Aug 17, 2026
f5f0c3d
test(youtube): reject noncanonical numeric metadata
seonghobae Aug 17, 2026
f4cee9e
fix(youtube): reject non-canonical duration numerics
seonghobae Aug 17, 2026
69cdf8b
docs(changelog): record strict duration metadata type gate
seonghobae Aug 17, 2026
3ea49d3
fix(youtube): remove unreachable duration conversion branch
seonghobae Aug 17, 2026
97490c7
test(youtube): require post-download duration revalidation
seonghobae Aug 17, 2026
d58c24e
test(youtube): model owned cleanup on duration drift
seonghobae Aug 17, 2026
d3e2792
fix(youtube): revalidate duration after download
seonghobae Aug 17, 2026
1c85058
test(privacy): fail on temporal path disclosure
seonghobae Aug 17, 2026
4a6e269
fix(privacy): redact temporal analysis failure diagnostics
seonghobae Aug 17, 2026
6d3c388
test(privacy): align temporal diagnostics with redaction contract
seonghobae Aug 17, 2026
aa0191c
test(security): reject foreign YouTube download paths
seonghobae Aug 18, 2026
48d06cf
fix(security): bind completed YouTube path to import cache
seonghobae Aug 18, 2026
85424c9
docs(security): record YouTube completed-path authority
seonghobae Aug 18, 2026
7eca596
docs(changelog): record YouTube completed-path containment
seonghobae Aug 18, 2026
6a728fd
test(audio): exercise module entrypoint with owned path
seonghobae Aug 18, 2026
2b83681
style(tests): restore Ruff formatting
seonghobae Aug 18, 2026
2909bc8
test(audio): reject malformed model stem output
seonghobae Aug 19, 2026
e5293a9
fix(audio): reject malformed model stem output
seonghobae Aug 19, 2026
2464ff9
test(audio): use explicit raw error pattern
seonghobae Aug 19, 2026
c35d55d
test(audio): cover model conversion failures
seonghobae Aug 19, 2026
15a9edb
test(audio): reproduce GPU tensor NumPy boundary
seonghobae Aug 19, 2026
c5519a4
fix(audio): move accelerator stems to CPU before NumPy
seonghobae Aug 19, 2026
389f057
fix(audio): preserve CPU tests while bridging accelerator stems
seonghobae Aug 19, 2026
2ea9e07
test(security): reproduce stem worker log disclosure
seonghobae Aug 20, 2026
b7dd015
test(security): cover parent stem failure logs
seonghobae Aug 20, 2026
cf8b62e
fix(security): redact analysis API tracebacks
seonghobae Aug 20, 2026
2f281be
fix(security): keep API privacy init lint-safe
seonghobae Aug 20, 2026
a8bbb77
test(security): scope analysis log redaction
seonghobae Aug 20, 2026
223dd78
fix(security): scope stem diagnostic redaction
seonghobae Aug 20, 2026
c2cc5bb
fix(audio): preflight source metadata before decode
seonghobae Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
- Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace.
- 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.

### Fixed

- Enforce one canonical local-audio resource policy across native local-file/YouTube bootstrap intake, the desktop bridge, Python request preflight, temporal decoding, and stem separation so oversized, overlong, malformed, wrong-rate, or non-finite input fails before bootstrap storage or expensive analysis/model work.
Comment thread
cursor[bot] marked this conversation as resolved.
- Preflight source-container duration, sample rate, and channel count from the already-open audio handle before temporal, stem, or bass-transcription decoders resample, downmix, or truncate it; successful metadata probes rewind the handle and malformed probes fail closed.
- Bound the admitted canonical decoded mono buffer to 317,520,000 bytes as well as the existing 39,690,000-sample ceiling, so decoder dtype expansion cannot stay within the sample count while exceeding the explicit in-memory audio budget.
- Fail closed on malformed known YouTube duration metadata before `download=True`; Boolean, non-numeric, non-finite, zero, negative, and non-canonical numeric-subtype duration evidence can no longer authorize a media download through Python numeric coercion or subclass semantics.
- Align YouTube download admission with that same 100 MiB encoded-byte ceiling: abort in-flight with yt-dlp `max_filesize` and a progress hook, reject announced oversize before `download=True`, delete owned `.part` / `.ytdl` / `-Frag*` siblings from that import directory on abort, reject a completed path that resolves outside the current import cache before post-download validation, cleanup, or success, and delete owned post-download artifacts that still exceed the policy. A 60 MiB import that the old 50 MB check rejected is now accepted; a file one byte over 100 MiB is not.
- Bound native stored-score PDF reads to the 25 MiB product limit before heap allocation and revalidate PDF magic on the same opened descriptor, preventing an attached score that later grows from bypassing the local resource boundary.

## [0.1.3] - 2026-04-29

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ publish = false

[lib]
name = "bandscope_desktop_core"
path = "src/lib.rs"
path = "src/root.rs"

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage)'] }
Expand Down
22 changes: 22 additions & 0 deletions apps/desktop/core/src/audio_resource.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// Maximum encoded local-audio file size accepted by the desktop bootstrap boundary.
pub const MAX_LOCAL_AUDIO_FILE_BYTES: u64 = 100 * 1024 * 1024;

const LOCAL_AUDIO_READ_ERROR: &str = "Could not read the selected audio file.";
const LOCAL_AUDIO_TOO_LARGE_ERROR: &str =
"Selected audio file exceeds the 100 MiB analysis limit.";

/// Validate a native local-audio file length before storing bootstrap metadata.
///
/// The caller must obtain this length from the native filesystem descriptor or
/// metadata boundary rather than from renderer-controlled JSON. The function
/// intentionally returns only bounded product messages and never includes a
/// local path or payload content.
pub fn validate_local_audio_file_size(file_size_bytes: u64) -> Result<u64, String> {
if file_size_bytes == 0 {
return Err(LOCAL_AUDIO_READ_ERROR.to_string());
}
if file_size_bytes > MAX_LOCAL_AUDIO_FILE_BYTES {
return Err(LOCAL_AUDIO_TOO_LARGE_ERROR.to_string());
}
Ok(file_size_bytes)
}
15 changes: 15 additions & 0 deletions apps/desktop/core/src/root.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! Pure, GUI-independent logic for the BandScope desktop application.
//!
//! The historical desktop-core implementation remains in `lib.rs` as the
//! compatibility module while bounded resource boundaries are isolated in
//! auditable modules. Public symbols are re-exported so downstream callers keep
//! the same crate-root API.

#[path = "lib.rs"]
mod runtime_core;
mod audio_resource;
mod score_pdf;

pub use audio_resource::{validate_local_audio_file_size, MAX_LOCAL_AUDIO_FILE_BYTES};
pub use runtime_core::*;
pub use score_pdf::read_validated_score_pdf;
82 changes: 82 additions & 0 deletions apps/desktop/core/src/score_pdf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use crate::{MAX_SCORE_PDF_BYTES, PDF_MAGIC};
use std::{fs::File, io::Read, path::Path};

const SCORE_READ_ERROR: &str = "Could not read the score PDF.";
const SCORE_TOO_LARGE_ERROR: &str = "Score PDF is too large (exceeds 25MB limit).";
const SCORE_INVALID_PDF_ERROR: &str = "Stored score is not a valid PDF.";

fn read_validated_pdf_stream(
reader: &mut impl Read,
expected_len: u64,
) -> Result<Vec<u8>, String> {
if expected_len > MAX_SCORE_PDF_BYTES {
return Err(SCORE_TOO_LARGE_ERROR.to_string());
}

// MAX_SCORE_PDF_BYTES is 25 MiB, which fits every supported Rust `usize`.
let mut bytes = vec![0_u8; expected_len as usize];
reader
.read_exact(&mut bytes)
.map_err(|_| SCORE_READ_ERROR.to_string())?;

let mut growth_probe = [0_u8; 1];
if reader
.read(&mut growth_probe)
.map_err(|_| SCORE_READ_ERROR.to_string())?
!= 0
{
return Err(SCORE_TOO_LARGE_ERROR.to_string());
}

if !bytes.starts_with(PDF_MAGIC) {
return Err(SCORE_INVALID_PDF_ERROR.to_string());
}

Ok(bytes)
}

/// Read one already-authorized stored score without allocating beyond the PDF limit.
///
/// The caller remains responsible for path authority and containment. This helper
/// opens that resolved path once, snapshots the descriptor length, allocates only
/// that bounded size, reads exactly that many bytes, and then probes one additional
/// byte on the same descriptor. A file that was already oversized is rejected
/// before heap allocation; a file that grows after metadata inspection is rejected
/// by the one-byte probe without extending the heap buffer beyond the product cap.
/// Errors intentionally omit the local path and file content.
pub fn read_validated_score_pdf(path: &Path) -> Result<Vec<u8>, String> {
let mut file = File::open(path).map_err(|_| SCORE_READ_ERROR.to_string())?;
let metadata = file
.metadata()
.map_err(|_| SCORE_READ_ERROR.to_string())?;
if !metadata.is_file() {
return Err(SCORE_READ_ERROR.to_string());
}
read_validated_pdf_stream(&mut file, metadata.len())
}

#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;

#[test]
fn stream_rejects_growth_after_the_metadata_length_snapshot() {
let mut reader = Cursor::new(b"%PDF-extra".to_vec());

let error = read_validated_pdf_stream(&mut reader, PDF_MAGIC.len() as u64)
.expect_err("bytes beyond the metadata snapshot must fail closed");

assert_eq!(error, SCORE_TOO_LARGE_ERROR);
}

#[test]
fn stream_rejects_truncation_after_the_metadata_length_snapshot() {
let mut reader = Cursor::new(PDF_MAGIC.to_vec());

let error = read_validated_pdf_stream(&mut reader, (PDF_MAGIC.len() + 1) as u64)
.expect_err("truncation after the metadata snapshot must fail closed");

assert_eq!(error, SCORE_READ_ERROR);
}
}
25 changes: 25 additions & 0 deletions apps/desktop/core/tests/audio_resource_policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use bandscope_desktop_core::{validate_local_audio_file_size, MAX_LOCAL_AUDIO_FILE_BYTES};

#[test]
fn local_audio_size_policy_accepts_the_exact_native_bootstrap_ceiling() {
assert_eq!(
validate_local_audio_file_size(MAX_LOCAL_AUDIO_FILE_BYTES),
Ok(MAX_LOCAL_AUDIO_FILE_BYTES)
);
}

#[test]
fn local_audio_size_policy_rejects_an_empty_native_bootstrap_source() {
assert_eq!(
validate_local_audio_file_size(0),
Err("Could not read the selected audio file.".to_string())
);
}

#[test]
fn local_audio_size_policy_rejects_a_native_source_above_the_canonical_ceiling() {
assert_eq!(
validate_local_audio_file_size(MAX_LOCAL_AUDIO_FILE_BYTES + 1),
Err("Selected audio file exceeds the 100 MiB analysis limit.".to_string())
);
}
91 changes: 91 additions & 0 deletions apps/desktop/core/tests/score_pdf_read.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use bandscope_desktop_core::{read_validated_score_pdf, MAX_SCORE_PDF_BYTES};
use std::io::Write;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

fn unique_test_dir(name: &str) -> PathBuf {
let suffix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock should be after epoch")
.as_nanos();
std::env::temp_dir().join(format!("bandscope-{name}-{suffix}"))
}

#[test]
fn score_pdf_read_returns_only_valid_bounded_pdf_bytes() {
let root = unique_test_dir("score-read-valid");
std::fs::create_dir_all(&root).expect("test directory should be created");
let path = root.join("score.pdf");
let expected = b"%PDF-1.7\nvalidated body";
std::fs::write(&path, expected).expect("valid PDF fixture should be written");

let actual = read_validated_score_pdf(&path).expect("valid stored PDF should be readable");

assert_eq!(actual, expected);
let _ = std::fs::remove_dir_all(root);
}

#[test]
fn score_pdf_read_rejects_empty_short_and_wrong_magic_content() {
let root = unique_test_dir("score-read-invalid");
std::fs::create_dir_all(&root).expect("test directory should be created");

for (name, content) in [
("empty.pdf", b"".as_slice()),
("short.pdf", b"%PD".as_slice()),
("wrong.pdf", b"PK\x03\x04 not a PDF".as_slice()),
] {
let path = root.join(name);
std::fs::write(&path, content).expect("invalid PDF fixture should be written");
let error = read_validated_score_pdf(&path).expect_err("invalid PDF must fail closed");
assert!(
error == "Could not read the score PDF." || error == "Stored score is not a valid PDF.",
"unexpected payload-safe error: {error}"
);
assert!(!error.contains(root.to_string_lossy().as_ref()));
}

let _ = std::fs::remove_dir_all(root);
}

#[test]
fn score_pdf_read_rejects_oversized_sparse_file_before_heap_allocation() {
let root = unique_test_dir("score-read-oversized");
std::fs::create_dir_all(&root).expect("test directory should be created");
let path = root.join("oversized.pdf");
let mut file = std::fs::File::create(&path).expect("oversized PDF fixture should be created");
file.write_all(b"%PDF-")
.expect("PDF magic should be written before extending sparse file");
file.set_len(MAX_SCORE_PDF_BYTES + 1)
.expect("sparse PDF fixture should exceed the product limit");
drop(file);

let error = read_validated_score_pdf(&path).expect_err("oversized PDF must fail closed");

assert_eq!(error, "Score PDF is too large (exceeds 25MB limit).");
let _ = std::fs::remove_dir_all(root);
}

#[cfg(unix)]
#[test]
fn score_pdf_read_rejects_non_file_descriptor() {
let root = unique_test_dir("score-read-directory");
std::fs::create_dir_all(&root).expect("test directory should be created");

let error = read_validated_score_pdf(&root).expect_err("directory must fail closed");

assert_eq!(error, "Could not read the score PDF.");
assert!(!error.contains(root.to_string_lossy().as_ref()));
let _ = std::fs::remove_dir_all(root);
}

#[test]
fn score_pdf_read_rejects_missing_file_without_exposing_path() {
let root = unique_test_dir("score-read-missing");
let path = root.join("private-score.pdf");

let error = read_validated_score_pdf(&path).expect_err("missing PDF must fail closed");

assert_eq!(error, "Could not read the score PDF.");
assert!(!error.contains("private-score.pdf"));
}
12 changes: 8 additions & 4 deletions apps/desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,10 @@ fn normalize_local_audio_source(path: &Path) -> Result<LocalAudioSourcePayload,
}
let metadata = std::fs::metadata(&canonical)
.map_err(|_| "Could not read the selected audio file.".to_string())?;
if !metadata.is_file() || metadata.len() == 0 {
if !metadata.is_file() {
return Err("Could not read the selected audio file.".into());
}
let file_size_bytes = validate_local_audio_file_size(metadata.len())?;
let file_name = canonical
.file_name()
.and_then(|value| value.to_str())
Expand All @@ -167,7 +168,7 @@ fn normalize_local_audio_source(path: &Path) -> Result<LocalAudioSourcePayload,
source_path: canonical.to_string_lossy().into_owned(),
file_name: file_name.to_string(),
extension,
file_size_bytes: metadata.len(),
file_size_bytes,
})
}

Expand Down Expand Up @@ -712,6 +713,7 @@ async fn import_youtube_url(
if parsed.get("ok").and_then(|v| v.as_bool()) == Some(true) {
if let Some(metadata) = parsed.get("metadata") {
let source = youtube_source_from_metadata(metadata, &cache_root)?;
validate_local_audio_file_size(source.file_size_bytes)?;
Comment thread
cursor[bot] marked this conversation as resolved.

let summary = ProjectBootstrapSummaryPayload {
project_id,
Expand Down Expand Up @@ -826,7 +828,9 @@ fn attach_score_pdf(
/// Security Notes: no path crosses the IPC boundary. Both ids are validated
/// against strict allowlist shapes, the path is rebuilt locally, and the
/// canonicalize-plus-prefix guard in `resolve_existing_score_pdf` rejects any
/// escape from the app-owned scores root.
/// escape from the app-owned scores root. The resolved file is then read
/// through the bounded core helper so growth after attachment cannot trigger
/// an allocation beyond the 25 MiB product limit.
#[tauri::command]
fn read_score_pdf(
project_id: String,
Expand All @@ -838,7 +842,7 @@ fn read_score_pdf(
}
let scores_root = scores_root_for_project(&app, &project_id)?;
let path = resolve_existing_score_pdf(&scores_root, &score_id)?;
std::fs::read(path).map_err(|_| "Could not read the score PDF.".to_string())
read_validated_score_pdf(&path)
}

/// Security Notes: same id validation and traversal guard as `read_score_pdf`;
Expand Down
58 changes: 58 additions & 0 deletions apps/desktop/src/lib/analysis.resource-policy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { importYoutubeUrl, selectLocalAudioSource } from "./analysis";

type TauriWindow = Window & {
__TAURI_INTERNALS__?: unknown;
__TAURI_INVOKE__?: unknown;
};

const tauriWindow = window as TauriWindow;
const INVALID_RESOURCE_POLICY_MESSAGE =
"Selected audio file metadata violates the analysis resource policy.";

function fractionalBootstrap(projectId: string) {
return {
projectId,
sourceMode: "reference",
projectRoot: `/tmp/bandscope/projects/${projectId}`,
cacheRoot: `/tmp/bandscope/cache/${projectId}`,
tempRoot: `/tmp/bandscope/temp/${projectId}`,
source: {
sourcePath: `/tmp/bandscope/${projectId}/input.wav`,
fileName: "input.wav",
extension: "wav",
fileSizeBytes: 1.5
}
};
}

describe("analysis encoded-byte policy parity", () => {
beforeEach(() => {
delete tauriWindow.__TAURI_INTERNALS__;
delete tauriWindow.__TAURI_INVOKE__;
});

it("rejects fractional local-file metadata before project state", async () => {
tauriWindow.__TAURI_INVOKE__ = vi.fn().mockResolvedValue(fractionalBootstrap("local-project"));

await expect(selectLocalAudioSource()).resolves.toEqual({
ok: false,
error: {
code: "invalid_request",
message: INVALID_RESOURCE_POLICY_MESSAGE
}
});
});

it("rejects fractional imported-file metadata before project state", async () => {
tauriWindow.__TAURI_INVOKE__ = vi.fn().mockResolvedValue(fractionalBootstrap("youtube-project"));

await expect(importYoutubeUrl("https://youtu.be/4ozX4yFUC34")).resolves.toEqual({
ok: false,
error: {
code: "invalid_request",
message: INVALID_RESOURCE_POLICY_MESSAGE
}
});
});
});
Loading
Loading