Files
photocraft/docs/integration-testing/per-pr-regressions.md
T
FredApps 255ecc81d0 Remove 21 requested PR contributions from the 108-PR integration
Retain 87 PRs. Reconcile overlapping integration fixes and update testing scope.
Removed: #7153 #7742 #5493 #9282 #3455 #9172 #3139 #3967 #4783 #5453 #8347 #1991 #3345 #4042 #4305 #4883 #4890 #5090 #6621 #5870 #5939.

Combined CI pending for this source; historical portable artifact is not applicable.
2026-10-29 18:49:17 +01:00

377 KiB
Raw Blame History

CURRENT SCOPE: 87 retained PRs. All 21 requested exclusions have been removed from this appendix. Rerun relevant cases against the new frozen SHA; older results are historical.

Per-PR regression execution appendix

Each automated case uses the synthetic fixture/setup and assertions in the named checked-in regression, linked at the reviewed head. The action is the command below. Expected: at least one matching test executes, every assertion passes, and the behavior in the case label is retained. A zero-match run is BLOCKED, never PASSED. After a moved-module port, locate the same test in the frozen integration tree and retain its fixture and assertions. Manual combined cases are in the main test plan.

All cases initially NOT RUN. Record Actual, Evidence and Tester in the empty fields; the status vocabulary is PASSED / FAILED / BLOCKED / NOT RUN / SKIPPED. Missing fixtures, unavailable adapters and unsupported platforms are BLOCKED or SKIPPED with a reason, rather than silently passing.

Reviewed-case execution rules

Run every listed test separately on the final frozen integration SHA, recording its final-tree source path, full discovered name, actual count and outcome. Historical reviewed-head links establish provenance only. Zero-match, ignored-only or early-return-without-required-capability runs are BLOCKED, not PASSED. Locate preserved equivalent assertions when a test moved; never substitute an unrelated guard. Automated and native/manual subcases have separate results. Use owned temporary settings/outputs; never migrate actual user settings, publish a release, bypass security or force a driver crash.

Final-tree path guide

Reviewed-head links below retain historical provenance. Use these current paths after module ports; the named test must still be discovered and executed on the frozen build.

Case / area Current source path
#6492 occupied control port apps/photocraft/tests/it/control_exit_code.rs
#8347 type-tool checkbox/canvas coverage crates/ui-egui/src/tools/type_tool/canvas_tests.rs
Engine command regressions crates/engine/src/cmd/ and the command's live test submodule; do not use deleted root monoliths
#7153 Liquify smart-filter editing crates/ui-egui/src/dialogs/liquify_ui/tests.rs
#8896 Select and Mask crates/ui-egui/src/dialogs/select_mask_ui/tests.rs

PR-3748: control: suggest near matches for unknown command ids and methods (#3674)

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

unknown_names_suggest_the_right_method_or_command setup/action excerpt:

let mut h = Headless::new();
        let err = |h: &mut Headless, m: &str, p: Value| h.handle(m, p).unwrap_err().to_string();
        let e = err(&mut h, "engine.execute", json!({"command": "engine.commands"}));
        assert!(e.contains("did you mean `command.list`") && e.contains("protocol method"), "{e}");
        let e = err(&mut h, "engine.execute", json!({"command": "doc.inspect"}));
        assert!(e.contains("`doc.inspect` is a protocol method"), "{e}");
        let e = err(&mut h, "engine.execute", json!({"command": "edit.undoo"}));
        assert!(e.contains("did you mean `edit.undo`"), "{e}");
        let e = err(&mut h, "doc.inspct", Value::Null);
        assert!(e.contains("did you mean `doc.inspect`") && e.contains("(try `methods`)"), "{e}");
        let e = err(&mut h, "edit.undo", Value::Null);
        assert!(e.contains("engine.execute {\"command\": \"edit.undo\"}"), "{e}");

Expected assertion context (overlapping windows merged):

assert!(e.contains("did you mean `command.list`") && e.contains("protocol method"), "{e}");
        let e = err(&mut h, "engine.execute", json!({"command": "doc.inspect"}));
        assert!(e.contains("`doc.inspect` is a protocol method"), "{e}");
        let e = err(&mut h, "engine.execute", json!({"command": "edit.undoo"}));
        assert!(e.contains("did you mean `edit.undo`"), "{e}");
        let e = err(&mut h, "doc.inspct", Value::Null);
        assert!(e.contains("did you mean `doc.inspect`") && e.contains("(try `methods`)"), "{e}");
        let e = err(&mut h, "edit.undo", Value::Null);
        assert!(e.contains("engine.execute {\"command\": \"edit.undo\"}"), "{e}");
    }

errors_are_tool_errors_not_crashes setup/action excerpt:

let client = connect(PhotocraftMcp::headless()).await;
    let r = call(&client, "command_run", json!({"id": "no.such.command"})).await;
    assert_eq!(r.is_error, Some(true));
    assert!(text(&r).contains("unknown command"));
    // #3674: the error names the nearest registered id.
    let r = call(&client, "command_run", json!({"id": "engine.commands"})).await;
    assert_eq!(r.is_error, Some(true));
    assert!(text(&r).contains("did you mean `command.list`"), "{}", text(&r));
    let r = call(&client, "doc_inspect", json!({"index": 0})).await;
    assert_eq!(r.is_error, Some(true));
    let r = call(&client, "doc_open", json!({"path": "/definitely/missing.png"})).await;
    assert_eq!(r.is_error, Some(true));

Expected assertion context (overlapping windows merged):

assert_eq!(r.is_error, Some(true));
    assert!(text(&r).contains("unknown command"));
    // #3674: the error names the nearest registered id.
    let r = call(&client, "command_run", json!({"id": "engine.commands"})).await;
    assert_eq!(r.is_error, Some(true));
    assert!(text(&r).contains("did you mean `command.list`"), "{}", text(&r));
    let r = call(&client, "doc_inspect", json!({"index": 0})).await;
    assert_eq!(r.is_error, Some(true));
    let r = call(&client, "doc_open", json!({"path": "/definitely/missing.png"})).await;
    assert_eq!(r.is_error, Some(true));
    let r = call(&client, "ui_inspect", json!({})).await;
    assert_eq!(r.is_error, Some(true));
    assert!(text(&r).contains("bridge"));
    let r = call(&client, "doc_export", json!({})).await;
    assert_eq!(r.is_error, Some(true));
    // Server still alive.
    json_of(&call(&client, "session_list", json!({})).await);
    client.cancel().await.unwrap();
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4993: Prevent control-channel engine commands from editing behind modal dialogs

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: engine execute cannot change document behind adjustment dialog. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

engine_execute_cannot_change_document_behind_adjustment_dialog setup/action excerpt:

let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        let ctx = egui::Context::default();
        app.run("file.new", json!({"width": 60, "height": 40, "background": "#3366cc"})).unwrap();
        let opened = call(&mut app, &ctx, "ui.menu.invoke", json!({"id": "image.adjustments.hueSaturation"}));
        assert_eq!(opened["ok"], true, "{opened}");
        let dialog = opened["result"]["dialog"].as_u64().unwrap();
        let revision = app.session.active().unwrap().revision;
        let fields = call(&mut app, &ctx, "ui.inspect", json!({}))["result"]["dialogs"][0]["fields"].clone();

        for method in ["engine.execute", "ui.menu.invoke"] {
            let blocked = call(&mut app, &ctx, method, json!({"command": "image.adjustments.invert"}));
            assert_eq!(blocked["ok"], false, "{method}: {blocked}");

Expected assertion context (overlapping windows merged):

assert_eq!(opened["ok"], true, "{opened}");
        let dialog = opened["result"]["dialog"].as_u64().unwrap();
        let revision = app.session.active().unwrap().revision;
        let fields = call(&mut app, &ctx, "ui.inspect", json!({}))["result"]["dialogs"][0]["fields"].clone();
            assert_eq!(blocked["ok"], false, "{method}: {blocked}");
            assert_eq!(app.session.active().unwrap().revision, revision, "{method} changed the document");
            let snapshot = call(&mut app, &ctx, "ui.inspect", json!({}));
            assert_eq!(snapshot["result"]["dialogs"][0]["id"], dialog);
            assert_eq!(snapshot["result"]["dialogs"][0]["fields"], fields);
        }

        assert_eq!(call(&mut app, &ctx, "ui.dialog.cancel", json!({"dialog": dialog}))["ok"], true);
        assert_eq!(call(&mut app, &ctx, "engine.execute", json!({"command": "image.adjustments.invert"}))["ok"], true);
        assert!(app.session.active().unwrap().revision > revision);
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-5176: Play CopyCat, AJ and Line and Color action sets; keep patterns across restarts; Shift Edge scale

  • Fixture/setup: Synthetic ATN mappings, smart-object Edit Contents action, temporary pattern stores and matting hard/soft edges.

  • Action: run each command separately:

    • cargo test -p photocraft-engine --locked every_mapped_command_exports_and_imports_back -- --nocapture
    • cargo test -p photocraft-engine --locked save_in_edit_contents_updates_the_smart_object -- --nocapture
    • cargo test -p photocraft-engine --locked patterns_survive_a_restart -- --nocapture
    • cargo test -p photocraft-engine --locked corrupt_pattern_files_are_skipped_and_kept -- --nocapture
    • cargo test -p photocraft-engine --locked large_patterns_load_on_use -- --nocapture
    • cargo test -p photocraft-algo --locked matting::tests -- --nocapture
  • Expected: Mappings round-trip parameters; Save in Edit Contents updates parent smart content. Pattern names/pixels and rename/delete survive restart; corrupt assets are warned about and preserved; large patterns load full pixels lazily. Hard edge ±100% shifts one pixel, ±50% gives half coverage within 0.02; feather 4/+100% reaches four pixels.

  • Separate coverage / prerequisites: Named CopyCat/AJ/Line and Color playback needs licensed fixture identities and required document/pattern setup; missing action sets leave that subcase BLOCKED. Mapping tests alone do not prove full set playback. Also run all new legacy/new-store migration, interruption, corruption, duplicate-ID and deletion-authority regressions.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-6492: Fail control startup promptly when its port is occupied

  • Fixture/setup: Process-isolated occupied loopback listener and successful prebound control-server fixtures in apps/photocraft tests.

  • Action: run each command separately:

    • cargo test -p photocraft --locked an_occupied_control_port_exits_with_status_2 -- --nocapture
    • cargo test -p photocraft --locked a_prebound_listener_authenticates_and_dispatches_requests -- --nocapture
  • Expected: Occupied port exits with status 2 before window/GPU initialization; successful prebound control authenticates and dispatches requests. Preserve timing, cleanup and child-exit assertions.

  • Separate coverage / prerequisites: Use only the test-owned listener and process.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-8905: fix(plugins): bound a run's total plug-in memory, not just each instance

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: parallel bands fit the total memory budget; one band at a time matches parallel bands. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

parallel_bands_fit_the_total_memory_budget setup/action excerpt:

let d = Limits::default();
    assert_eq!(d.parallel_bands(16), 4);
    assert!(d.parallel_bands(64) * d.max_memory_bytes <= d.max_total_memory_bytes);
    assert_eq!(d.parallel_bands(2), 2, "fewer workers than the budget allows");
    let small = Limits { max_memory_bytes: 64 << 20, ..Limits::default() };
    assert_eq!(small.parallel_bands(16), 16, "small instances keep every worker busy");
    let tight = Limits { max_total_memory_bytes: 1 << 20, ..Limits::default() };
    assert_eq!(tight.parallel_bands(16), 1, "at least one band runs");
    assert_eq!(d.parallel_bands(0), 1);
}

Expected assertion context (overlapping windows merged):

assert_eq!(d.parallel_bands(16), 4);
    assert!(d.parallel_bands(64) * d.max_memory_bytes <= d.max_total_memory_bytes);
    assert_eq!(d.parallel_bands(2), 2, "fewer workers than the budget allows");
    let small = Limits { max_memory_bytes: 64 << 20, ..Limits::default() };
    assert_eq!(small.parallel_bands(16), 16, "small instances keep every worker busy");
    let tight = Limits { max_total_memory_bytes: 1 << 20, ..Limits::default() };
    assert_eq!(tight.parallel_bands(16), 1, "at least one band runs");
    assert_eq!(d.parallel_bands(0), 1);
}

one_band_at_a_time_matches_parallel_bands setup/action excerpt:

let pool = rayon::ThreadPoolBuilder::new().num_threads(8).build().unwrap();
    let s = surface(PixelFormat::RGBA8, 600, 300);
    let canvas = Rect::new(0, 0, 600, 300);
    let run = |limits: Limits| {
        let p = Plugin::load(INVERT, limits).unwrap();
        pool.install(|| p.apply(&s, canvas, None, &json!({}))).unwrap()
    };
    let parallel = run(Limits { band_bytes: 1, ..Limits::default() });
    let serial = run(Limits { band_bytes: 1, max_total_memory_bytes: 512 << 20, ..Limits::default() });
    assert_eq!(serial, parallel);
    assert_ne!(serial, s, "the filter ran");
}

Expected assertion context (overlapping windows merged):

assert_eq!(serial, parallel);
    assert_ne!(serial, s, "the filter ran");
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3874: fix(layer-style): support Blend If Alt-split handle feathering and throttle live preview

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: blend if alt split and drag behavior. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

blend_if_alt_split_and_drag_behavior setup/action excerpt:

// Case 1: Alt-drag on unsplit black handle [0, 0, 255, 255] splits the right half
        let mut mode = BlendDrag::Pair { pair: 0, origin_v: 0 };
        let q = [0, 0, 255, 255];
        let q1 = step_blend_drag(&mut mode, &q, 45, true);
        assert_eq!(q1, [0, 45, 255, 255], "Alt-drag right must split black triangle into [0, 45]");

        // Case 2: Moving the split right half further to 70
        let q2 = step_blend_drag(&mut mode, &q1, 70, false);
        assert_eq!(q2, [0, 70, 255, 255]);

        // Case 3: Dragging the split half back to 0 merges the handle
        let q3 = step_blend_drag(&mut mode, &q2, 0, false);

Expected assertion context (overlapping windows merged):

assert_eq!(q1, [0, 45, 255, 255], "Alt-drag right must split black triangle into [0, 45]");

        // Case 2: Moving the split right half further to 70
        let q2 = step_blend_drag(&mut mode, &q1, 70, false);
        assert_eq!(q2, [0, 70, 255, 255]);

        // Case 3: Dragging the split half back to 0 merges the handle
        let q3 = step_blend_drag(&mut mode, &q2, 0, false);
        assert_eq!(q3, [0, 0, 255, 255], "Dragging back to origin reunites the triangle");

        // Case 4: Alt-drag on unsplit white handle [0, 0, 255, 255] splits the left half
        let mut mode_w = BlendDrag::Pair { pair: 2, origin_v: 255 };
        assert_eq!(qw1, [0, 0, 200, 255], "Alt-drag left must split white triangle into [200, 255]");

        // Case 5: Dragging without Alt moves both halves together
        let mut mode_solid = BlendDrag::Pair { pair: 0, origin_v: 0 };
        assert_eq!(qs1, [35, 35, 255, 255], "Drag without Alt moves solid handle together");

        // Case 6: Clamping prevents black handle from crossing white handle
        let q_close = [0, 100, 150, 255];
        assert_eq!(q_clamped, [0, 150, 150, 255], "Black max cannot exceed white min");
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4023: i18n(es): add 548 missing rows, fix five filter and video terms

  • Fixture/setup: Select Spanish UI, synthetic document, and menus/dialogs for video footage, Blur Gallery, Pixelate and Sharpen. Record Spanish language selection and screenshots.
  • Action: Check Interpret Footage = Interpretar material de vídeo; Replace Footage = Reemplazar material de vídeo; Tilt-Shift = Desenfoque de inclinación y desplazamiento; Mezzotint = Grabado a media tinta; Smart Sharpen = Enfoque inteligente. Check their ellipsis variants in menus. Exercise a Count label with value 3 and confirm Recuento: 3, with no literal {n}.
  • Expected: Specified Spanish strings match the final catalog, ellipses remain where defined, and values replace placeholders without English fallback or raw {n}/{file} tokens.
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester:

PR-4711: Treat Dissolve as a per-pixel choice

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

clipped_dissolve_is_binary_and_preserves_the_base_shape setup/action excerpt:

let mut d = doc_white(40, 12);
    d.layers.push(solid_layer("base", Rect::new(0, 0, 20, 12), [0.0, 0.0, 1.0, 1.0]));
    let mut clip = solid_layer("clip", d.bounds(), [1.0, 0.0, 0.0, 1.0]);
    clip.clipped = true;
    clip.blend = BlendMode::Dissolve;
    clip.opacity = 0.6;
    clip.fill_opacity = 0.5;
    d.layers.push(clip);
    let mut red = 0;
    let mut blue = 0;
    for y in 0..12 {
        for x in 0..20 {

Expected assertion context (overlapping windows merged):

assert!(close4(p, [0.0, 0.0, 1.0, 1.0]), "Dissolve must not interpolate: {p:?} at ({x},{y})");
                blue += 1;
            }
        }
        assert!(close4(px(&d, 30, y), [1.0; 4]), "outside the base the clip is invisible");
    }
    assert!(red > 0 && blue > 0, "Dissolve must expose both source and base: {red}/{blue}");
    assert!((red as f32 / (red + blue) as f32 - 0.3).abs() < 0.1, "coverage should follow 60% opacity × 50% fill: {red}/{blue}");
}

clipped_dissolve_matches_whole_render_at_offset_regions_and_depths setup/action excerpt:

for fmt in [PixelFormat::RGBA8, PixelFormat::RGBA16, PixelFormat::RGBA32F] {
        let mut d = Document::new("coverage", Size::new(32, 18), ColorMode::Rgb, fmt.sample);
        let bounds = Rect::new(-3, -2, 32, 18);
        let mut base = Layer::raster("base", fmt);
        base.surface_mut().unwrap().fill_rect(bounds, &[0.0, 0.0, 1.0, 0.5]);
        d.layers.push(base);
        let mut clip = Layer::raster("clip", fmt);
        clip.surface_mut().unwrap().fill_rect(bounds, &[1.0, 0.0, 0.0, 0.75]);
        clip.clipped = true;
        clip.blend = BlendMode::Dissolve;
        clip.opacity = 0.6;
        clip.fill_opacity = 0.5;

Expected assertion context (overlapping windows merged):

assert!(close4(part.get(x, y), whole.get(x, y)), "{fmt:?}: region mismatch at ({x},{y})");
                }
            }
        }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4992: Preserve Dissolve coverage when compositing clipped layers

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

clipped_dissolve_is_binary_and_preserves_the_base_shape setup/action excerpt:

let mut d = doc_white(40, 12);
    d.layers.push(solid_layer("base", Rect::new(0, 0, 20, 12), [0.0, 0.0, 1.0, 1.0]));
    let mut clip = solid_layer("clip", d.bounds(), [1.0, 0.0, 0.0, 1.0]);
    clip.clipped = true;
    clip.blend = BlendMode::Dissolve;
    clip.opacity = 0.6;
    clip.fill_opacity = 0.5;
    d.layers.push(clip);
    let mut red = 0;
    let mut blue = 0;
    for y in 0..12 {
        for x in 0..20 {

Expected assertion context (overlapping windows merged):

assert!(close4(p, [0.0, 0.0, 1.0, 1.0]), "Dissolve must not interpolate: {p:?} at ({x},{y})");
                blue += 1;
            }
        }
        assert!(close4(px(&d, 30, y), [1.0; 4]), "outside the base the clip is invisible");
    }
    assert!(red > 0 && blue > 0, "Dissolve must expose both source and base: {red}/{blue}");
    assert!((red as f32 / (red + blue) as f32 - 0.3).abs() < 0.1, "coverage should follow 60% opacity × 50% fill: {red}/{blue}");
}

clipped_dissolve_matches_whole_render_at_offset_regions_and_depths setup/action excerpt:

for fmt in [PixelFormat::RGBA8, PixelFormat::RGBA16, PixelFormat::RGBA32F] {
        let mut d = Document::new("coverage", Size::new(32, 18), ColorMode::Rgb, fmt.sample);
        let bounds = Rect::new(-3, -2, 32, 18);
        let mut base = Layer::raster("base", fmt);
        base.surface_mut().unwrap().fill_rect(bounds, &[0.0, 0.0, 1.0, 0.5]);
        d.layers.push(base);
        let mut clip = Layer::raster("clip", fmt);
        clip.surface_mut().unwrap().fill_rect(bounds, &[1.0, 0.0, 0.0, 0.75]);
        clip.clipped = true;
        clip.blend = BlendMode::Dissolve;
        clip.opacity = 0.6;
        clip.fill_opacity = 0.5;

Expected assertion context (overlapping windows merged):

assert!(close4(part.get(x, y), whole.get(x, y)), "{fmt:?}: region mismatch at ({x},{y})");
                }
            }
        }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:
Historical source context (deduplicated excerpts; not additional cases or execution evidence)

rough_pastels_and_angled_strokes_respond_to_every_slider setup/action excerpt:

// #879: these smeared or drowned the image in texture, and some sliders did nothing.
        use crate::GalleryFilter as G;
        let b = Rect::new(0, 0, 56, 40);
        let s = pattern(SampleType::F32);
        let run = |e: &GalleryEffect, tile: i32| {
            let fp = FilterParams::FilterGallery { effects: vec![e.clone()] };
            apply_tiled(&s, &fp, b, b, None, tile, Some(b)).read_region(b)
        };
        for f in [G::RoughPastels, G::AngledStrokes, G::Cutout] {
            let base = run(&GalleryEffect::new(f), 256);
            for prm in f.params() {
                let crate::GalleryParamKind::Range { min, max, .. } = prm.kind else { continue };

Expected assertion context (overlapping windows merged):

assert_ne!(a, base, "{} {} = {v} changed nothing", f.key(), prm.key);
                    let t = run(&e, 16);
                    let worst = a.iter().zip(&t).map(|(x, y)| (x - y).abs()).fold(0.0, f32::max);
                    assert!(worst < 1e-3, "{} {} = {v} differs across tilings by {worst}", f.key(), prm.key);
                }
            }
        }

cutout_makes_flat_shapes_with_clean_edges setup/action excerpt:

// It used to posterize every pixel after a blur: noise survived as speckle, edges were
        // ragged and smooth gradients banded instead of becoming flat paper.
        let noisy: Vec<Vec<f32>> = (0..32).map(|y| (0..64).map(|x| if x < 32 { 0.2 } else { 0.8 } + (hash01(x, y, 0, 7) - 0.5) * 0.16).collect()).collect();
        let out = cutout_on(&noisy, &[]);
        for (x0, x1) in [(2, 28), (36, 62)] {
            let vals: Vec<f32> = (2..30).flat_map(|y| out[y][x0..x1].to_vec()).collect();
            let spread = vals.iter().fold(f32::MIN, |a, &v| a.max(v)) - vals.iter().fold(f32::MAX, |a, &v| a.min(v));
            assert!(spread < 1e-4, "a noisy area should be one flat tone, spread {spread}");
        }
        // The cut is an anti-aliased pixel or two, not a blur.
        for y in 0..32 {
            let (lo, hi) = (out[y][16], out[y][48]);

Expected assertion context (overlapping windows merged):

assert!(spread < 1e-4, "a noisy area should be one flat tone, spread {spread}");
        }
        // The cut is an anti-aliased pixel or two, not a blur.
        for y in 0..32 {
            assert!(soft <= 2, "row {y}: {soft} edge pixels");
        }
        // A ramp becomes `numberOfLevels` flat steps (the palette is the image's own colours, so
        // one may go to the wide flat margins and merge as noise).
            assert!(tones.len() <= levels as usize && tones.len() + 1 >= levels as usize, "levels {levels}: tones {tones:?}");
            assert!(tones.windows(2).all(|t| t[0] < t[1]), "levels {levels}: tones {tones:?}");
        }
        // Edge Simplicity drops specks.
        let speck: Vec<Vec<f32>> = (0..32).map(|y| (0..32).map(|x| if (15..18).contains(&x) && (15..18).contains(&y) { 0.9 } else { 0.2 }).collect()).collect();
        assert!(detailed[16][16] - detailed[2][2] > 0.5, "the speck should survive at simplicity 0");
        assert!((simple[16][16] - simple[2][2]).abs() < 1e-4, "the speck should go at simplicity 10");
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-8039: Compose: Extend Softer outer glows past Size (#7899)

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

softer_outer_glow_keeps_a_fading_tail_past_its_size setup/action excerpt:

let mut shape = Map::new(220, 220, 0.0);
        for y in 0..shape.h {
            for x in 20..100 {
                shape.v[y * shape.w + x] = 1.0;
            }
        }
        let glow = Glow {
            common: photocraft_doc::FxCommon::new(BlendMode::Screen, 1.0),
            paint: FxPaint::Color(photocraft_color::Color::WHITE),
            technique: GlowTechnique::Softer,
            spread: 0.0,
            size: 60.0,

Expected assertion context (overlapping windows merged):

assert!(at(50) > 0.024, "the softer shoulder should stay visible: {}", at(50));
        assert!(at(60) > 1.0 / 255.0, "the glow must retain a visible tail at its Size: {}", at(60));
        assert!(at(62) > 0.5 / 255.0, "the glow must extend beyond its Size: {}", at(62));
        assert!(at(66) < 1e-6, "the added tail should fade to zero: {}", at(66));
    }

softer_outer_glow_margin_includes_its_tail_support setup/action excerpt:

let glow = Glow {
            common: photocraft_doc::FxCommon::new(BlendMode::Screen, 1.0),
            paint: FxPaint::Color(photocraft_color::Color::WHITE),
            technique: GlowTechnique::Softer,
            spread: 0.0,
            size: 60.0,
            contour: Contour::Linear,
            anti_alias: false,
            range: 0.5,
            jitter: 0.0,
            noise: 0.0,
            source: GlowSource::Edge,

Expected assertion context (overlapping windows merged):

assert_eq!(margin(&layer), 68);
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:
Historical source context (deduplicated excerpts; not additional cases or execution evidence)

oklab_round_trips_and_marks_the_gamut setup/action excerpt:

let white = oklab([1.0, 1.0, 1.0]);
        assert!((white[0] - 1.0).abs() < 1e-3 && white[1].abs() < 1e-3 && white[2].abs() < 1e-3, "{white:?}");
        assert!(oklab([0.0, 0.0, 0.0])[0].abs() < 1e-6);
        for p in [[0.2, 0.5, 0.9], [0.95, 0.1, 0.3], [0.5, 0.5, 0.5], [0.0, 1.0, 0.0]] {
            let back = srgb(oklab(p)).unwrap_or([-1.0; 3]);
            assert!(dist2(back, p) < 1e-6, "{p:?} came back as {back:?}");
        }
        // Very light and very colourful: no sRGB colour is that.
        assert_eq!(srgb([0.95, 0.3, 0.0]), None);
    }

Expected assertion context (overlapping windows merged):

assert!((white[0] - 1.0).abs() < 1e-3 && white[1].abs() < 1e-3 && white[2].abs() < 1e-3, "{white:?}");
        assert!(oklab([0.0, 0.0, 0.0])[0].abs() < 1e-6);
        for p in [[0.2, 0.5, 0.9], [0.95, 0.1, 0.3], [0.5, 0.5, 0.5], [0.0, 1.0, 0.0]] {
            let back = srgb(oklab(p)).unwrap_or([-1.0; 3]);
            assert!(dist2(back, p) < 1e-6, "{p:?} came back as {back:?}");
        }
        // Very light and very colourful: no sRGB colour is that.
        assert_eq!(srgb([0.95, 0.3, 0.0]), None);
    }

out_of_gamut_colours_lose_strength_not_hue setup/action excerpt:

let (l, a, b) = (0.95, 0.3, 0.1);
        let c = in_gamut(l, a, b);
        let lab = oklab(c);
        assert!((lab[0] - l).abs() < 0.01, "lightness kept: {lab:?}");
        let chroma = (lab[1] * lab[1] + lab[2] * lab[2]).sqrt();
        assert!(chroma > 0.01 && chroma < (a * a + b * b).sqrt(), "weaker but still coloured: {chroma}");
        assert!((lab[2] / lab[1] - b / a).abs() < 0.05, "same hue: {lab:?}");
        // In gamut it is just the conversion.
        assert_eq!(in_gamut(0.6, 0.02, -0.03), srgb([0.6, 0.02, -0.03]).unwrap_or_default());
    }

Expected assertion context (overlapping windows merged):

assert!((lab[0] - l).abs() < 0.01, "lightness kept: {lab:?}");
        let chroma = (lab[1] * lab[1] + lab[2] * lab[2]).sqrt();
        assert!(chroma > 0.01 && chroma < (a * a + b * b).sqrt(), "weaker but still coloured: {chroma}");
        assert!((lab[2] / lab[1] - b / a).abs() < 0.05, "same hue: {lab:?}");
        // In gamut it is just the conversion.
        assert_eq!(in_gamut(0.6, 0.02, -0.03), srgb([0.6, 0.02, -0.03]).unwrap_or_default());
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:
  • Fixture/setup: Synthetic label maps in cutout/regions.rs.

  • Action: run each command separately:

    • cargo test -p photocraft-algo --locked components_are_four_connected_and_numbered_in_raster_order -- --nocapture
    • cargo test -p photocraft-algo --locked small_regions_merge_into_the_neighbour_they_share_most_border_with -- --nocapture
    • cargo test -p photocraft-algo --locked slivers_merge_though_they_are_large -- --nocapture
    • cargo test -p photocraft-algo --locked local_colour_drifts_within_a_shape_but_never_crosses_a_cut -- --nocapture
  • Expected: Four-connected components have deterministic raster numbering; small regions choose the shared-border neighbour; narrow slivers merge despite area; colour variation never crosses a cut.

  • Separate coverage / prerequisites: The earlier Oklab test is supporting coverage, not the regions-stage oracle.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

  • Fixture/setup: Two-region boundaries, islands, antialiased edges, squares and open chains in cutout/edges.rs and curves.rs.

  • Action: run each command separately:

    • cargo test -p photocraft-algo --locked the_edge_between_two_regions_is_one_chain_between_junctions -- --nocapture
    • cargo test -p photocraft-algo --locked an_island_is_one_closed_chain -- --nocapture
    • cargo test -p photocraft-algo --locked an_anti_aliased_edge_is_placed_inside_its_pixel -- --nocapture
    • cargo test -p photocraft-algo --locked a_square_keeps_its_four_corners_and_straight_sides -- --nocapture
    • cargo test -p photocraft-algo --locked an_open_chain_ends_where_it_started_at_any_fidelity -- --nocapture
    • cargo test -p photocraft-algo --locked fitting_keeps_small_islands_and_their_winding -- --nocapture
    • cargo test -p photocraft-algo --locked cutout_makes_flat_shapes_with_clean_edges -- --nocapture
  • Expected: Shared boundaries are single chains; islands close; subpixel edge placement, square corners, open endpoints and winding survive fitting. Complete Cutout yields the asserted clean flat shapes.

  • Separate coverage / prerequisites: Run after the complete #8191 → #8192 → #8193 → #7903 chain.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-8903: Box-filter minified patterns in the CPU and GPU samplers (#8831)

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: minified pattern is filtered not aliased; pattern fill layers. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

minified_pattern_is_filtered_not_aliased setup/action excerpt:

// 1-px dark / light stripes (mean 0.5), shown at 25 %: every pixel covers 4 tile pixels.
        // The 0.125 px phase puts each pixel centre on a light tile pixel's centre, so a single
        // bilinear tap would return pure white.
        let mut s = Surface::new(PixelFormat::RGBA8);
        s.fill_rect(Rect::new(0, 0, 8, 8), &[1.0, 1.0, 1.0, 1.0]);
        for x in (0..8).step_by(2) {
            s.fill_rect(Rect::new(x, 0, x + 1, 8), &[0.0, 0.0, 0.0, 1.0]);
        }
        let t = Tile::new(&Pattern::new("stripes", s, 8, 8)).unwrap();
        let place = Placement::new(Rect::EMPTY, false, (0.125, 0.0), 0.25, 0.0);
        let (u, _) = place.map(0.5, 0.5);
        assert_eq!(t.sample(u, 0.5)[0], 1.0, "the single tap lands on a light stripe");

Expected assertion context (overlapping windows merged):

assert_eq!(t.sample(u, 0.5)[0], 1.0, "the single tap lands on a light stripe");
        for p in render(&t, &place, Rect::new(0, 0, 8, 1)) {
            assert!((p[0] - 0.5).abs() < 0.05, "{p:?}");
        }
        // 100 % stays exact.
        let one = Placement::new(Rect::EMPTY, false, (0.0, 0.0), 1.0, 0.0);
        assert_eq!((row[0][0], row[1][0]), (0.0, 1.0));
    }

pattern_fill_layers setup/action excerpt:

let Some(mut g) = gpu() else { return };
    let pat = checker_pattern();
    for (link, scale, angle, masked) in [(true, 1.0, 0.0, false), (false, 0.7, 25.0, true), (true, 1.6, -40.0, false), (true, 0.3, 10.0, false)] {
        let mut d = base_doc(70, 50);
        d.patterns.push(pat.clone());
        let fill = Fill::Pattern { name: pat.name.clone(), id: pat.id.clone(), scale, angle, link, phase: (3.0, -2.0) };
        let mut l = Layer::new("pat", LayerContent::Fill(fill));
        l.opacity = 0.85;
        l.blend = BlendMode::Overlay;
        if masked {
            l.mask = Some(mask(Rect::new(5, 5, 60, 45), 61, 0.0));
        }

Expected assertion context (overlapping windows merged):


  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9472: fix(io): import a Displace smart filter that names its map file without embedding it (#9162)

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

displace_psd_filter_with_only_a_map_path_renders_after_replace_contents setup/action excerpt:

use photocraft_psd::descriptor::{Descriptor, Id, UnicodeString, Value};
    let dir = std::env::temp_dir().join(format!("pcraft-9162-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let png = |name: &str, size: (u32, u32), c: [f32; 4]| {
        let d = Document::with_background(
            "m",
            photocraft_geom::Size::new(size.0, size.1),
            photocraft_color::ColorMode::Rgb,
            photocraft_color::SampleType::U8,
            photocraft_color::Color::rgba(c[0], c[1], c[2], c[3]),
        );
        let path = dir.join(name);

Expected assertion context (overlapping windows merged):

assert_eq!(out["warnings"], json!([]), "depth {depth}");
        let cache = smart(&s2.active().unwrap().doc).1.cache.unwrap();
        // 10 % of 128 px at full map contrast: the design (x 8..80) moves 12.8 px left.
        let alpha = |x: i32| cache.pixel(x, 30)[3];
        assert!(alpha(2) > 0.99 && alpha(75) < 0.01, "depth {depth}: {} {}", alpha(2), alpha(75));
    }
    std::fs::remove_dir_all(&dir).ok();
}

a_map_named_only_by_its_path_imports_as_map_path setup/action excerpt:

let with_file = |file: Value| {
            let mut i = item(Vec::new());
            let mut f = get_desc(&i, "Fltr").unwrap().clone();
            f.items.retain(|(k, _)| !k.is(MAP_KEY));
            set(&mut f, "DspF", file);
            set(&mut i, "Fltr", Value::Descriptor(f));
            i
        };
        let src = with_file(pth("C:\\maps\\repro-map.psd"));
        let f = filter_from_item(&src);
        assert_eq!(f.command, COMMAND);
        assert_eq!(f.params["mapPath"], json!("C:\\maps\\repro-map.psd"));

Expected assertion context (overlapping windows merged):

assert_eq!(f.command, COMMAND);
        assert_eq!(f.params["mapPath"], json!("C:\\maps\\repro-map.psd"));
        assert!(embedded_map(&f.params).is_none());
        assert_eq!(item_for_filter(&f).unwrap(), src, "written back as it was");
        let mut moved = f.clone();
        moved.params["mapPath"] = json!("/elsewhere/map.png");
        assert!(item_for_filter(&moved).is_err(), "another map path has no place in the template");
        let mut no_path = f.clone();
        no_path.params.as_object_mut().unwrap().remove("mapPath");
        assert!(item_for_filter(&no_path).is_err(), "a path-only template needs its mapPath, or the PSD names a map the render never read");
        // A record whose size field disagrees with its units, or without the final NUL.
        let mut wrong_size = pth("map.psd");
        if let Value::Path(b) = &mut wrong_size {
            assert_eq!(filter_from_item(&src).command, UNSUPPORTED_FILTER);
        }
        // A damaged embedded map is never replaced by a readable path.
        let mut damaged = item(vec![0; 4]);
        assert_eq!(filter_from_item(&damaged).command, UNSUPPORTED_FILTER);
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9509: fix: an edit at full coverage keeps its result over huge 32-bit values

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

full_coverage_adjustment_keeps_its_result_over_huge_hdr_values setup/action excerpt:

for v in [1.0e6_f32, 1.0e8, -1.0e6] {
        let mut d = Document::new("hdr", Size::new(2, 2), ColorMode::Rgb, SampleType::F32);
        let mut base = Layer::raster("b", PixelFormat::RGBA32F);
        base.surface_mut().unwrap().fill_rect(Rect::new(0, 0, 2, 2), &[v, v, v, 1.0]);
        let look = Adjustment::ColorLookup { name: "half".into(), lut: Some(std::sync::Arc::new(vec![0.5; 24])), size: 2, tetrahedral: false, dither: false };
        d.layers = vec![base, Layer::new("look", LayerContent::Adjustment(look))];
        let got = px(&d, 0, 0);
        for c in &got[..3] {
            assert!((c - 0.5).abs() < 1e-6, "{v}: {got:?}");
        }
    }
}

Expected assertion context (overlapping windows merged):

assert!((c - 0.5).abs() < 1e-6, "{v}: {got:?}");
        }
    }
}

reused_render_matches_previous_allocating_stack setup/action excerpt:

for depth in [SampleType::U8, SampleType::U16, SampleType::F32] {
        let mut doc = Document::new("equivalence", Size::new(513, 7), ColorMode::Rgb, depth);
        let rect = doc.bounds();
        for (name, color) in [("bottom", [0.2, 0.4, 0.8, 1.0]), ("top", [1.5, -0.25, 0.6, 0.5])] {
            let mut layer = Layer::raster(name, doc.pixel_format());
            layer.surface_mut().unwrap().fill_rect(rect, &color);
            doc.layers.push(layer);
        }
        let mut levels = Adjustment::identity_levels();
        if let Adjustment::Levels { master, .. } = &mut levels {
            master.gamma = 1.4;
        }

Expected assertion context (overlapping windows merged):


  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9512: fix(compose, gpu): adjustment layers apply Fill inside the special eight blend modes

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: adjustment fill changes the special blend modes; special eight fill on adjustment layers matches cpu. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

adjustment_fill_changes_the_special_blend_modes setup/action excerpt:

for (mode, want) in [(BlendMode::ColorDodge, [80, 160, 240]), (BlendMode::LinearBurn, [13, 77, 141])] {
        let mut d = Document::with_background("fill", Size::new(1, 1), ColorMode::Rgb, SampleType::U8, Color::WHITE);
        let mut base = Layer::raster("b", PixelFormat::RGBA8);
        base.surface_mut().unwrap().fill_rect(Rect::new(0, 0, 1, 1), &[64.0 / 255.0, 128.0 / 255.0, 192.0 / 255.0, 1.0]);
        let look = Adjustment::ColorLookup { name: "half".into(), lut: Some(std::sync::Arc::new(vec![0.5; 24])), size: 2, tetrahedral: false, dither: false };
        let mut adj = Layer::new("look", LayerContent::Adjustment(look));
        adj.blend = mode;
        adj.fill_opacity = 0.4;
        d.layers.push(base);
        d.layers.push(adj);
        let got = px(&d, 0, 0);
        let got: Vec<i32> = got[..3].iter().map(|v| (v * 255.0).round() as i32).collect();

Expected assertion context (overlapping windows merged):

assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() <= 1), "{mode:?}: {got:?} vs Photoshop {want:?}");
    }
}

special_eight_fill_on_adjustment_layers_matches_cpu setup/action excerpt:

let Some(mut g) = gpu() else { return };
    let special = [
        BlendMode::ColorBurn,
        BlendMode::LinearBurn,
        BlendMode::ColorDodge,
        BlendMode::LinearDodge,
        BlendMode::VividLight,
        BlendMode::LinearLight,
        BlendMode::HardMix,
        BlendMode::Difference,
    ];
    for mode in special {

Expected assertion context (overlapping windows merged):


  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9584: fix(compose, gpu): Blend If hard points match Photoshop at 16 bits and for Gray

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: blend if 16 bit cutoffs follow photoshops steps; blend if gray follows photoshops weights and rounding. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

blend_if_16_bit_cutoffs_follow_photoshops_steps setup/action excerpt:

let shown = |v16: u16, black: u8, white: u8| {
        let v = f32::from(v16) / 65535.0;
        let mut d = Document::new("t", Size::new(1, 1), ColorMode::Rgb, SampleType::U16);
        let layer16 = |name: &str, c: [f32; 4]| {
            let mut l = Layer::raster(name, PixelFormat::RGBA16);
            l.surface_mut().unwrap().fill_rect(Rect::new(0, 0, 1, 1), &c);
            l
        };
        d.layers.push(layer16("under", [v, v, v, 1.0]));
        let mut l = layer16("top", [1.0, 1.0, 1.0, 1.0]);
        l.blend_if.set(3, [FULL, range([black, black], [white, white])]);
        d.layers.push(l);

Expected assertion context (overlapping windows merged):

assert!(shown(at(step), c, 255), "black {c}: step {step} shows");
        assert!(!shown(at(step - 1), c, 255), "black {c}: step {} hides", step - 1);
        assert!(shown(at(step), 0, c), "white {c}: step {step} shows");
        assert!(!shown(at(step + 1), 0, c), "white {c}: step {} hides", step + 1);
    }
    assert!(shown(32895, 128, 255) && !shown(32894, 128, 255), "the reporter's RGB16 boundary");
}

blend_if_gray_follows_photoshops_weights_and_rounding setup/action excerpt:

let shown = |rgb: [u8; 3], black: u8, white: u8| {
        let mut d = Document::new("t", Size::new(1, 1), ColorMode::Rgb, SampleType::U8);
        let c = rgb.map(|v| f32::from(v) / 255.0);
        d.layers.push(solid_layer("under", Rect::new(0, 0, 1, 1), [c[0], c[1], c[2], 1.0]));
        let mut l = solid_layer("top", Rect::new(0, 0, 1, 1), [1.0, 1.0, 1.0, 1.0]);
        l.blend_if.set(0, [FULL, range([black, black], [white, white])]);
        d.layers.push(l);
        px(&d, 0, 0)[0] > 0.999 && px(&d, 0, 0)[2] > 0.999
    };
    // (colour hidden by a black point at `cut`, the next colour shown), single channels.
    for (cut, below, at) in [
        (20, [65, 0, 0], [66, 0, 0]),

Expected assertion context (overlapping windows merged):

assert!(!shown(below, cut, 255), "{below:?} hidden by black {cut}");
        assert!(shown(at, cut, 255), "{at:?} shown by black {cut}");
    }
    // The reporter's mixed colours, and the white point's half-down rounding.
    assert!(shown([123, 126, 151], 128, 255), "127.85 is 128");
    assert!(shown([120, 129, 149], 0, 128), "128.5 is 128");
    assert!(!shown([121, 129, 149], 0, 128), "128.8 is 129");
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9589: fix(compose, gpu): an adjustment layer's This Layer range judges its result before Opacity

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: adjustment blend if judges the result before opacity; blend if. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

adjustment_blend_if_judges_the_result_before_opacity setup/action excerpt:

for (black, white, want) in [(150u8, 255u8, [115, 128, 140]), (0, 150, [64, 128, 192])] {
        let mut d = Document::with_background("bi", Size::new(1, 1), ColorMode::Rgb, SampleType::U8, Color::WHITE);
        d.layers.push(solid_layer("base", Rect::new(0, 0, 1, 1), [64.0 / 255.0, 128.0 / 255.0, 192.0 / 255.0, 1.0]));
        let mut inv = Layer::new("invert", LayerContent::Adjustment(Adjustment::Invert));
        inv.opacity = 0.4;
        inv.blend_if.set(1, [range([black, black], [white, white]), FULL]);
        d.layers.push(inv);
        let got: Vec<i32> = px(&d, 0, 0)[..3].iter().map(|v| (v * 255.0).round() as i32).collect();
        assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() <= 1), "black {black} white {white}: {got:?} vs Photoshop {want:?}");
    }
}

Expected assertion context (overlapping windows merged):

assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() <= 1), "black {black} white {white}: {got:?} vs Photoshop {want:?}");
    }
}

blend_if setup/action excerpt:

use photocraft_doc::BlendRange;
    let Some(mut g) = gpu() else { return };
    let r = |b0: u8, b1: u8, w0: u8, w1: u8| BlendRange { black: [b0, b1], white: [w0, w1] };
    // (name, entry, [this layer, underlying], has a hard point)
    let cases: [(&str, usize, [BlendRange; 2], bool); 7] = [
        ("gray this hard", 0, [r(90, 90, 255, 255), BlendRange::FULL], true),
        ("gray under split", 0, [BlendRange::FULL, r(30, 140, 150, 230)], false),
        ("red both", 1, [r(0, 60, 200, 200), r(20, 20, 180, 250)], true),
        ("red both split", 1, [r(0, 60, 190, 230), r(10, 50, 180, 250)], false),
        ("green white split", 2, [r(0, 0, 100, 220), BlendRange::FULL], false),
        ("blue under hard", 3, [BlendRange::FULL, r(128, 128, 255, 255)], true),
        ("everything hidden", 0, [r(255, 255, 0, 0), BlendRange::FULL], true),

Expected assertion context (overlapping windows merged):

assert!(hits.prefix_hits > 0, "{depth:?} {name}: the edit above reuses the lower stack");

            // Masked, with channel restrictions.
            let mut m = l.clone();
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9595: fix(compose, gpu): a Pass Through group with Blend If renders isolated

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: pass through group with blend if is isolated; pass through group with blend if is isolated. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

pass_through_group_with_blend_if_is_isolated setup/action excerpt:

for (gated, want) in [(true, [64, 128, 192]), (false, [191, 127, 63])] {
        let mut d = Document::with_background("pt", Size::new(1, 1), ColorMode::Rgb, SampleType::U8, Color::WHITE);
        d.layers.push(solid_layer("base", Rect::new(0, 0, 1, 1), [64.0 / 255.0, 128.0 / 255.0, 192.0 / 255.0, 1.0]));
        let mut g = Layer::group("group", vec![Layer::new("invert", LayerContent::Adjustment(Adjustment::Invert))]);
        g.blend = BlendMode::PassThrough;
        if gated {
            g.blend_if.set(1, [FULL, range([0, 0], [110, 110])]);
        }
        d.layers.push(g);
        let got: Vec<i32> = px(&d, 0, 0)[..3].iter().map(|v| (v * 255.0).round() as i32).collect();
        assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() <= 1), "Blend If {gated}: {got:?} vs Photoshop {want:?}");
    }

Expected assertion context (overlapping windows merged):

assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() <= 1), "Blend If {gated}: {got:?} vs Photoshop {want:?}");
    }
}

pass_through_group_with_blend_if_is_isolated setup/action excerpt:

use photocraft_doc::BlendRange;
    let Some(mut g) = gpu() else { return };
    for gated in [true, false] {
        let mut d = base_doc(64, 48);
        d.layers[0] = noise_layer("bg", PixelFormat::RGBA8, Rect::from_xywh(0, 0, 64, 48), 5, 1.0);
        let mut grp = Layer::group("group", vec![Layer::new("inv", LayerContent::Adjustment(Adjustment::Invert))]);
        grp.blend = BlendMode::PassThrough;
        if gated {
            grp.blend_if.set(1, [BlendRange::FULL, BlendRange { black: [0, 0], white: [110, 110] }]);
        }
        d.layers.push(grp);
        check(&mut g, &d, &format!("pass-through group, Blend If {gated}"));

Expected assertion context (overlapping windows merged):


  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9651: perf(algo): row-parallel local_sharpen with fast-path protect_detail (#5426)

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

matches_scalar_sharpen_bit_for_bit_across_layouts_edges_and_hdr setup/action excerpt:

for (w, h) in [(1, 1), (3, 7), (37, 29)] {
        for ch in 1..=5 {
            for alpha in std::iter::once(None).chain((0..ch).map(Some)) {
                let data: Vec<f32> = (0..w * h * ch)
                    .map(|i| if Some(i % ch) == alpha { [0.0, 1e-8, 0.25, 0.75, 1.0][(i / ch) % 5] } else { ((i * 73 + 19) % 113) as f32 / 17.0 - 2.0 })
                    .collect();
                for win in [(0, 0, w, h), (w / 2, h / 2, w, h), (0, 0, 1, 1), (0, 0, 0, h)] {
                    for amount in [0.0, 0.5, 1.0, 2.5] {
                        for protect_detail in [false, true] {
                            let expected = scalar_local_sharpen(&data, w, h, ch, alpha, win, amount, protect_detail);
                            let actual = local_sharpen(&data, w, h, ch, alpha, win, amount, protect_detail);
                            assert_eq!(actual.len(), expected.len());

Expected assertion context (overlapping windows merged):

assert_eq!(actual.len(), expected.len());
                            for (i, (a, b)) in actual.iter().zip(&expected).enumerate() {
                                assert_eq!(
                                    a.to_bits(),
                                    b.to_bits(),
                                    "{w}x{h} ch={ch} alpha={alpha:?} win={win:?} amount={amount} protect={protect_detail} sample={i}"

empty_channel_layout_remains_empty setup/action excerpt:

assert!(local_sharpen(&[], 5, 5, 0, None, (0, 0, 5, 5), 1.0, true).is_empty());
}

Expected assertion context (overlapping windows merged):

assert!(local_sharpen(&[], 5, 5, 0, None, (0, 0, 5, 5), 1.0, true).is_empty());
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9703: Fix adjustment layer name clicks targeting masks

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

a_new_adjustment_or_fill_layer_shows_its_settings_until_the_mask_thumbnail_is_clicked setup/action excerpt:

for (command, settings) in [("layer.newAdjustmentLayer.curves", Some("Reset to defaults")), ("layer.newFillLayer.solidColor", None)] {
        let mut s = photocraft_engine::Session::new();
        s.execute("file.new", json!({"width": 200, "height": 100})).unwrap();
        s.execute("layer.new.layer", json!({})).unwrap();
        let mut h = harness(s, 0, 1.0, 290.0);
        let ctx = h.ctx.clone();
        let (req, _rx) = crate::control::ControlRequest::new("ui.set", json!({"dock": {"collapsed": ["color", "history", "navigator"]}}));
        crate::control::handle(h.state_mut(), &ctx, &req);
        h.state_mut().run(command, json!({})).unwrap();
        h.run_steps(6);
        let id = h.state().session.active().unwrap().active_layer.unwrap().0;
        assert!(h.state().ui.mask_target, "{command}: the new mask is targeted");

Expected assertion context (overlapping windows merged):

assert!(h.state().ui.mask_target, "{command}: the new mask is targeted");
        assert_eq!(crate::canvas::paint_target(h.state()), json!("mask"), "{command}: painting edits the mask");
        assert!(h.query_all_by_label("Density").next().is_none(), "{command}: Properties shows the layer, not its mask");
        if let Some(label) = settings {
            assert!(h.query_all_by_label(label).next().is_some(), "{command}: its settings show");
        }
        // Clicking the row (not a thumbnail) keeps the settings.
        let row = h.get_by_label(&layer(&h, id).name).rect().center();
        assert_eq!(h.state().ui.mask_target, command.starts_with("layer.newFillLayer."));
        assert!(!h.state().ui.vector_mask_target);
        assert!(h.query_all_by_label("Density").next().is_none(), "{command}: a row click keeps the settings");
        // The mask thumbnail switches Properties to the mask.
        let r = mask_rect(&h, id, MaskKind::Pixel);
        click_with(&mut h, r.center(), Modifiers::NONE);
        assert!(h.state().ui.mask_target);
        assert!(h.query_all_by_label("Density").next().is_some(), "{command}: the mask thumbnail shows the mask controls");
        if let Some(label) = settings {
            assert!(h.query_all_by_label(label).next().is_none());
        }
    }
}

adjustment_names_target_contents_instead_of_masks setup/action excerpt:

for command in ["layer.newAdjustmentLayer.hueSaturation", "layer.newAdjustmentLayer.curves"] {
        for ppp in [1.0, 2.0] {
            let (mut s, _, other) = session();
            s.execute(command, json!({})).unwrap();
            let id = s.active().unwrap().active_layer.unwrap().0;
            let path = json!({"subpaths": [{"knots": [[20, 10], [120, 10], [120, 90], [20, 90]]}]});
            s.execute("layer.vectorMask.add", json!({"layer": id, "path": path})).unwrap();
            let mut h = harness(s, 0, ppp, 360.0);
            for kind in [MaskKind::Pixel, MaskKind::Vector] {
                let at = mask_rect(&h, id, kind).center();
                click_with(&mut h, at, Modifiers::NONE);
                assert_eq!(h.state().ui.mask_target, kind == MaskKind::Pixel);

Expected assertion context (overlapping windows merged):

assert_eq!(h.state().ui.mask_target, kind == MaskKind::Pixel);
                assert_eq!(h.state().ui.vector_mask_target, kind == MaskKind::Vector);
                let name = crate::layer_row_ui::recorded(&h.ctx).into_iter().find(|r| r.layer == id).unwrap().name.unwrap().center();
                click_with(&mut h, name, Modifiers::NONE);
                assert!(!h.state().ui.mask_target && !h.state().ui.vector_mask_target, "{command} @{ppp}x after {kind:?}");
                let thumb = crate::layer_row_ui::recorded(&h.ctx).into_iter().find(|r| r.layer == id).unwrap().thumbnail.unwrap();
                assert!(near(super::brackets(&h.ctx).unwrap(), thumb.expand(3.0)), "the content thumbnail is selected");
                assert_eq!(h.state().session.active().unwrap().active_layer.unwrap().0, id);
            }
            let name = crate::layer_row_ui::recorded(&h.ctx).into_iter().find(|r| r.layer == other).unwrap().name.unwrap().center();
            click_with(&mut h, name, Modifiers::NONE);
            assert_eq!(h.state().session.active().unwrap().active_layer.unwrap().0, id);
            assert!(!h.state().ui.mask_target && !h.state().ui.vector_mask_target, "{command} @{ppp}x from another layer");
            assert_eq!(crate::canvas::paint_target(h.state()), json!("mask"));
            let content = layer(&h, id).content;
            h.state_mut().run("edit.fill", json!({"contents": "black"})).unwrap();
            assert_eq!(layer(&h, id).content, content, "filling must preserve the adjustment or fill content");
            assert_eq!(layer(&h, id).mask.unwrap().surface.sample_channel(50, 50, 0), 0.0, "Fill edits the mask");
            assert!(!h.state().ui.mask_target && !h.state().ui.vector_mask_target, "painting leaves the content thumbnail selected");
        }
    }
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3142: Read applied Sony RAW metadata and crop ILCE-6000 images

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

measured_image_areas_crop_the_borders_without_touching_the_rest setup/action excerpt:

let mut s = nef("NIKON D800E", 7424, 4924);
        apply(&mut s);
        assert_eq!(s.crop, Rect::new(8, 6, 7360, 4912));
        // Only the image area is measured: levels, colour and tone stay untouched.
        assert!(s.color.calibrations.is_empty() && s.tone_curve.is_empty());
        assert_eq!(s.white, [16383.0; 3]);

        let mut s = sensor("SONY", "ILCE-7", 6048, 4024);
        apply(&mut s);
        assert_eq!(s.crop, Rect::new(12, 12, 6000, 4000));

        let mut s = sensor("SONY", "ILCE-6000", 6048, 4024);

Expected assertion context (overlapping windows merged):

assert_eq!(s.crop, Rect::new(8, 6, 7360, 4912));
        // Only the image area is measured: levels, colour and tone stay untouched.
        assert!(s.color.calibrations.is_empty() && s.tone_curve.is_empty());
        assert_eq!(s.white, [16383.0; 3]);

        let mut s = sensor("SONY", "ILCE-7", 6048, 4024);
        apply(&mut s);
        assert_eq!(s.crop, Rect::new(12, 12, 6000, 4000));

        let mut s = sensor("SONY", "ILCE-6000", 6048, 4024);
        apply(&mut s);
        assert_eq!(s.crop, Rect::new(12, 12, 6000, 4000));
        assert!(s.color.calibrations.is_empty() && s.tone_curve.is_empty());
        // A file's explicit crop wins, including a different in-camera aspect ratio.
        s.crop = Rect::new(12, 324, 6000, 3376);
        apply(&mut s);
        assert_eq!(s.crop, Rect::new(12, 324, 6000, 3376));

        let mut s = sensor("SONY", "ILCE-6000", 3024, 2012);
        apply(&mut s);
        assert_eq!(s.crop, Rect::new(0, 0, 3024, 2012));

        // Another size or model of the same maker is left alone.
        let mut s = nef("NIKON D800E", 2496, 1646);
        assert_eq!(s.crop, Rect::new(0, 0, 2496, 1646));
        let mut s = nef("NIKON D800", 7424, 4924);
        apply(&mut s);
        assert_eq!(s.crop, Rect::new(0, 0, 7424, 4924));
    }

encrypted_applied_white_balance_and_black_reach_the_developer setup/action excerpt:

for be in [false, true] {
            let bytes = fixture(be, false, [512, 513, 514, 515], [3072, 1024, 1024, 2048]);
            let s = decode(&bytes, &Limits::default()).unwrap();
            assert_eq!(s.format, RawFormat::Arw);
            assert_eq!(s.camera_wb, Some([3.0, 1.0, 2.0]));
            assert_eq!(s.black.values, vec![512.0, 513.0, 514.0, 515.0]);
            let dev = develop_sensor(&s, &DevelopOptions::default()).unwrap();
            assert_eq!(dev.info.wb_multipliers, [3.0, 1.0, 2.0]);
            assert!(!dev.warnings.iter().any(|w| w.contains("grey world") || w.contains("assumed 0")));
            let explicit = develop_sensor(&s, &DevelopOptions { white_balance: WhiteBalance::Multipliers([3.0, 1.0, 2.0]), ..Default::default() }).unwrap();
            assert_eq!(dev.rgb, explicit.rgb);
        }

Expected assertion context (overlapping windows merged):

assert_eq!(s.format, RawFormat::Arw);
            assert_eq!(s.camera_wb, Some([3.0, 1.0, 2.0]));
            assert_eq!(s.black.values, vec![512.0, 513.0, 514.0, 515.0]);
            let dev = develop_sensor(&s, &DevelopOptions::default()).unwrap();
            assert_eq!(dev.info.wb_multipliers, [3.0, 1.0, 2.0]);
            assert!(!dev.warnings.iter().any(|w| w.contains("grey world") || w.contains("assumed 0")));
            let explicit = develop_sensor(&s, &DevelopOptions { white_balance: WhiteBalance::Multipliers([3.0, 1.0, 2.0]), ..Default::default() }).unwrap();
            assert_eq!(dev.rgb, explicit.rgb);
        }
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3387: feat(codecs): add lossless JPEG XL export for 8/16-bit images

  • Fixture/setup: Native 7×5 Gray/GrayA/RGB/RGBA U8/U16 fixtures plus 2/513/8193-pixel-wide alpha samples in codecs/tests/jxl_export.rs.

  • Action: run each command separately:

    • cargo test -p photocraft-codecs --locked jpeg_xl_decodes_losslessly_with_independent_decoder -- --nocapture
    • cargo test -p photocraft-codecs --locked jpeg_xl_preserves_sixteen_bit_alpha_across_dimension_header_sizes -- --nocapture
    • cargo test -p photocraft-codecs --locked jpeg_xl_encoding_is_deterministic_and_converts_float_to_integer -- --nocapture
    • cargo test -p photocraft-codecs --locked jpeg_xl_is_export_only_and_detects_both_signatures -- --nocapture
    • cargo test -p photocraft-codecs --locked jpeg_xl_rejects_one_pixel_images_without_panicking -- --nocapture
  • Expected: Independent jxl_oxide decoding equals original integer samples exactly, including alpha and GrayA widening. Dimensions/channels and deterministic output are correct; capabilities remain export-only; 1×1 refuses without panic.

  • Separate coverage / prerequisites: These native independent-decoder checks cannot be established by wasm or malformed-header rejection alone.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-3464: fix(vector): target selected saved paths and own Pen gestures

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

appended_work_and_saved_components_preserve_existing_geometry_and_undo setup/action excerpt:

for name in ["work", "Saved drawing"] {
        let mut session = session();
        let first = json!({"subpaths": [{"closed": false, "knots": [[20, 20], [80, 20]]}]});
        let second = json!({"subpaths": [{"closed": false, "knots": [[30, 60], [90, 60]]}]});
        session.execute("path.set", json!({"name": name, "path": first})).unwrap();
        let before = session.execute("path.info", json!({"name": name})).unwrap();
        session.execute("path.set", json!({"name": name, "path": second, "op": "combine"})).unwrap();
        let after = session.execute("path.info", json!({"name": name})).unwrap();
        assert_eq!(after["path"]["subpaths"].as_array().unwrap().len(), 2);
        assert_eq!(after["path"]["subpaths"][0], before["path"]["subpaths"][0]);
        session.execute("edit.undo", json!({})).unwrap();
        assert_eq!(session.execute("path.info", json!({"name": name})).unwrap(), before);

Expected assertion context (overlapping windows merged):

assert_eq!(after["path"]["subpaths"].as_array().unwrap().len(), 2);
        assert_eq!(after["path"]["subpaths"][0], before["path"]["subpaths"][0]);
        session.execute("edit.undo", json!({})).unwrap();
        assert_eq!(session.execute("path.info", json!({"name": name})).unwrap(), before);
        session.execute("edit.redo", json!({})).unwrap();
        assert_eq!(session.execute("path.info", json!({"name": name})).unwrap(), after);
    }
}

appending_valid_components_cannot_exceed_total_path_budget setup/action excerpt:

let mut session = session();
    let subpath = json!({"closed": false, "knots": []});
    session.execute("path.set", json!({"path": {"subpaths": vec![subpath.clone(); 2_049]}})).unwrap();
    let before = session.active().unwrap().doc.work_path.clone();
    let history = session.active().unwrap().history.entries().len();
    let error = session.execute("path.set", json!({"path": {"subpaths": vec![subpath; 2_048]}, "op": "combine"})).unwrap_err();
    assert!(error.to_string().contains("4096"));
    assert_eq!(session.active().unwrap().doc.work_path, before);
    assert_eq!(session.active().unwrap().history.entries().len(), history);
}

Expected assertion context (overlapping windows merged):

assert!(error.to_string().contains("4096"));
    assert_eq!(session.active().unwrap().doc.work_path, before);
    assert_eq!(session.active().unwrap().history.entries().len(), history);
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3519: feat(export): optionally trim transparent margins from layer files

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

layers_to_files_can_trim_transparent_margins_without_changing_the_document setup/action excerpt:

let dir = tmp("layers-trim");
    for depth in [8, 16, 32] {
        let mut s = session(12, 11, depth);
        s.execute("layer.new.layer", json!({"name": "Ink"})).unwrap();
        s.execute("select.rect", json!({"x": 3, "y": 2, "width": 4, "height": 5})).unwrap();
        s.execute("edit.fill", json!({"color": "#ff0000"})).unwrap();
        s.execute("select.deselect", json!({})).unwrap();

        let before = doc(&s).clone();
        let regular = s.execute("file.export.layersToFiles", json!({"dir": dir, "prefix": format!("full-{depth}"), "format": "png"})).unwrap();
        let full = std::fs::read(regular["files"][0].as_str().unwrap()).unwrap();
        let full = photocraft_io::import("full.png", &full).unwrap().document;

Expected assertion context (overlapping windows merged):

assert_eq!((full.size.width, full.size.height), (12, 11), "default is unchanged");

        let trimmed = s.execute("file.export.layersToFiles", json!({"dir": dir, "prefix": format!("trim-{depth}"), "format": "png", "trim": true})).unwrap();
        let image = std::fs::read(trimmed["files"][0].as_str().unwrap()).unwrap();
        assert_eq!((cropped.size.width, cropped.size.height), (4, 5), "depth {depth}");
        let px = photocraft_compose::render(&cropped, Rect::new(0, 0, 1, 1)).px[0];
        assert!(px[0] > 0.95 && px[1] < 0.05 && px[2] < 0.05, "depth {depth}: {px:?}");
        assert_eq!(*doc(&s), before, "export must not crop the original document");
    }
}

streamed_layer_export_respects_trim setup/action excerpt:

let mut s = session(12, 11, 8);
    s.execute("layer.new.layer", json!({"name": "Ink"})).unwrap();
    s.execute("select.rect", json!({"x": 3, "y": 2, "width": 4, "height": 5})).unwrap();
    s.execute("edit.fill", json!({"color": "#ff0000"})).unwrap();
    s.execute("select.deselect", json!({})).unwrap();
    let mut encoded = Vec::new();
    let names = super::export_layers_with(&s, &json!({"trim": true, "format": "png"}), |name, bytes| {
        encoded.push((name.to_string(), bytes.to_vec()));
        Ok(())
    })
    .unwrap();
    assert_eq!(names.len(), encoded.len());

Expected assertion context (overlapping windows merged):

assert_eq!(names.len(), encoded.len());
    let cropped = photocraft_io::import(&encoded[0].0, &encoded[0].1).unwrap().document;
    assert_eq!((cropped.size.width, cropped.size.height), (4, 5));
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-6383: Windows releases ship a symbols zip: the release build's PDBs for profilers (#688)

  • Fixture/setup: Portable/executable artifact and symbols ZIP from the SAME immutable final-SHA build run, per claimed architecture.

  • Action: run each command separately:

    • Perform the explicit artifact inspection below; mark BLOCKED if fixtures are absent.
  • Expected: ZIP has photocraft.pdb, photocraft_cli.pdb and symbols.txt. Each PDB GUID/age matches its same-run executable; debugger/profiler resolves a PhotoCraft function. Portable/MSI launches without optional symbols.

  • Separate coverage / prerequisites: Inspect PE CodeView/PDB identity, record run/job/artifact IDs and hashes. Same SHA rebuilt is not the same PDB identity. Source lines are not required because release omits line tables. Missing packaging artifacts/identity evidence is BLOCKED; do not publish a release just for fixtures.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-8892: ui: larger Align icons and overflow button; Save for Web keeps its width on an invalid Matte

  • Fixture/setup: Studio Dark and Studio Light, UI scale 100% and 200%, 1280x800 then 900x600 logical-point window; synthetic document with at least two selected layers, Move tool, overflowing document/panel tabs, and Save for Web. Record physical pixel dimensions and pixels-per-point.
  • Action: Inspect Move alignment controls and Align and distribute ellipsis. Force tab overflow and open its chevron menu. Open Save for Web, record dialog width, enter an invalid Matte value such as not-a-colour, and record width/error placement; correct the value.
  • Expected: After the combined #8892/#9004 ports, Move icons and ellipsis use 20-point artwork in 28-point hit targets (40/56 physical pixels at 2x). Tab overflow uses a 16-point chevrons-right icon in its 18-point slot (32/36 pixels at 2x), with reachable menu items. Controls remain legible/unclipped. Invalid Matte displays its error without expanding Save for Web width; correction clears it.
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester:

PR-8897: fix(io): read linked smart objects for PSD export as regular files only

  • Fixture/setup: Unix test-owned FIFO linked by synthetic 8×6 document; bounded-reader synthetic fixtures.

  • Action: run each command separately:

    • cargo test -p photocraft-io --locked a_linked_fifo_is_not_read -- --nocapture
    • cargo test -p photocraft-format --locked reads_stop_at_the_total_limit -- --nocapture
    • cargo test -p photocraft-format --locked an_exact_hint_is_not_over_allocated -- --nocapture
  • Expected: FIFO export completes within 30 seconds without a writer, exports cached pixels and warns of fallback/not-regular-file. Exact-limit read succeeds, over-limit/never-ending reads fail FileTooLarge; size hints do not overallocate.

  • Separate coverage / prerequisites: Use only owned temporary FIFO, never a real device. Non-Unix exclusion is BLOCKED for FIFO qualification, not PASSED.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-8986: Path tools: edit the saved path selected in the Paths panel (#8478)

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

direct_selection_drags_an_anchor_of_the_selected_saved_path setup/action excerpt:

let mut app = app(Tool::DirectSelection);
    with_saved_path(&mut app);
    let (before, n) = (saved(&app), steps(&app));
    drag(&mut app, [120.5, 120.5], [130.5, 135.5], Modifiers::NONE);
    let after = saved(&app);
    assert_eq!(after.subpaths[0].knots[2].anchor, Point::new(130.0, 135.0), "the dragged anchor moved");
    assert_eq!(after.subpaths[0].knots[0].anchor, Point::new(40.0, 40.0), "the others stay");
    assert_eq!(steps(&app), n + 1, "one history step");
    assert_eq!((app.ui.direct_selection.target, app.ui.direct_selection.anchors.clone()), (Some(PathRef::Saved(0)), vec![[0, 2]]));
    assert!(app.session.active().unwrap().doc.work_path.is_none(), "no work path was made");
    assert!(app.session.undo());
    assert_eq!(saved(&app), before);

Expected assertion context (overlapping windows merged):

assert_eq!(after.subpaths[0].knots[2].anchor, Point::new(130.0, 135.0), "the dragged anchor moved");
    assert_eq!(after.subpaths[0].knots[0].anchor, Point::new(40.0, 40.0), "the others stay");
    assert_eq!(steps(&app), n + 1, "one history step");
    assert_eq!((app.ui.direct_selection.target, app.ui.direct_selection.anchors.clone()), (Some(PathRef::Saved(0)), vec![[0, 2]]));
    assert!(app.session.active().unwrap().doc.work_path.is_none(), "no work path was made");
    assert!(app.session.undo());
    assert_eq!(saved(&app), before);
}

direct_selection_marquee_selects_and_arrow_params_name_the_saved_path setup/action excerpt:

let mut app = app(Tool::DirectSelection);
    with_saved_path(&mut app);
    drag(&mut app, [30.0, 30.0], [50.0, 50.0], Modifiers::NONE);
    assert_eq!((app.ui.direct_selection.target, app.ui.direct_selection.anchors.clone()), (Some(PathRef::Saved(0)), vec![[0, 0]]));
    assert_eq!(PathRef::Saved(0).params(&app), json!({"name": NAME}));
    // A path that is gone gets no name, which the engine refuses instead of editing another one.
    assert_eq!(PathRef::Saved(7).params(&app), json!({"name": ""}));
    assert!(app.run("path.moveAnchors", json!({"name": "", "anchors": [[0, 0]], "move": [1, 1]})).is_err());
}

Expected assertion context (overlapping windows merged):

assert_eq!((app.ui.direct_selection.target, app.ui.direct_selection.anchors.clone()), (Some(PathRef::Saved(0)), vec![[0, 0]]));
    assert_eq!(PathRef::Saved(0).params(&app), json!({"name": NAME}));
    // A path that is gone gets no name, which the engine refuses instead of editing another one.
    assert_eq!(PathRef::Saved(7).params(&app), json!({"name": ""}));
    assert!(app.run("path.moveAnchors", json!({"name": "", "anchors": [[0, 0]], "move": [1, 1]})).is_err());
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9339: fix(export): embed requested sRGB profile for untagged sources (#9325)

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

ico_export_is_available_and_rejects_oversize_output setup/action excerpt:

let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
        let mut f = Map::new();
        f.insert("format".into(), json!("ico"));
        f.insert("scale".into(), json!(100));
        assert!(FORMATS.iter().any(|(ext, _)| *ext == "ico"));
        assert!(default_export_document(&app.session.active().unwrap().doc, &f, None).err().is_some_and(|e| e.contains("256")));
        f.insert("scale".into(), json!(50));
        let resized = default_export_document(&app.session.active().unwrap().doc, &f, None).unwrap();
        assert_eq!((resized.size.width, resized.size.height), (150, 100));
    }

Expected assertion context (overlapping windows merged):

assert!(FORMATS.iter().any(|(ext, _)| *ext == "ico"));
        assert!(default_export_document(&app.session.active().unwrap().doc, &f, None).err().is_some_and(|e| e.contains("256")));
        f.insert("scale".into(), json!(50));
        let resized = default_export_document(&app.session.active().unwrap().doc, &f, None).unwrap();
        assert_eq!((resized.size.width, resized.size.height), (150, 100));
    }

nearest_neighbor_doubles_each_pixel_into_an_exact_block setup/action excerpt:

use photocraft_doc::SampleType;
        for depth in [SampleType::U8, SampleType::U16] {
            let doc = checker_doc(depth);
            let before = doc.clone();
            let mut f = Map::new();
            f.insert("format".into(), json!("png"));
            f.insert("scale".into(), json!(200));
            f.insert("resample".into(), json!("nearest"));
            let out = default_export_document(&doc, &f, None).unwrap();
            assert_eq!((out.size.width, out.size.height), (10, 6));
            assert_eq!(out.depth, depth);
            let surf = out.layers[0].surface().unwrap();

Expected assertion context (overlapping windows merged):

assert_eq!((out.size.width, out.size.height), (10, 6));
            assert_eq!(out.depth, depth);
            let surf = out.layers[0].surface().unwrap();
            for y in 0..6 {
                for x in 0..10 {
                    assert_eq!(surf.rgba(x, y), checker(x / 2, y / 2), "{depth:?} pixel {x},{y}");
                }
            }
            assert_eq!(doc, before, "{depth:?}: export leaves the source document unchanged");

            // The preview proxy keeps Nearest Neighbor's hard edges too.
            let proxy = default_export_document(&doc, &f, Some(512)).unwrap();
            assert_eq!(proxy.layers[0].surface().unwrap().rgba(1, 1), checker(0, 0), "{depth:?}: preview proxy is nearest as well");
            assert_eq!(preview_filter(&f), egui::TextureOptions::NEAREST);

            f.insert("resample".into(), json!(DEFAULT_RESAMPLE));
            let smooth = default_export_document(&doc, &f, None).unwrap();
            assert!(blended, "{depth:?}: Bicubic Automatic blends neighbouring squares");
            assert_eq!(preview_filter(&f), egui::TextureOptions::LINEAR);
        }
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9342: fix(io, engine): Embed Color Profile writes sRGB for an untagged document (#9325)

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

doc_save_rejects_a_bad_quality_instead_of_ignoring_or_clamping_it setup/action excerpt:

let dir = std::env::temp_dir().join(format!("pc-rpc-save-quality-{}", std::process::id()));
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join("x.webp");
        let mut h = Headless::trusted_local();
        h.handle("doc.new", json!({"width": 16, "height": 16})).unwrap();
        for q in [json!(85.0), json!("85"), json!(0), json!(500), json!(-5), json!(true)] {
            let err = h.handle("doc.save", json!({"path": path.to_string_lossy(), "quality": q})).unwrap_err().to_string();
            assert!(err.contains("quality"), "{q}: {err}");
            assert!(!path.exists(), "{q} wrote a file");
        }
        h.handle("doc.save", json!({"path": path.to_string_lossy(), "quality": 85})).unwrap();
        let bytes = std::fs::read(&path).unwrap();

Expected assertion context (overlapping windows merged):

assert!(err.contains("quality"), "{q}: {err}");
            assert!(!path.exists(), "{q} wrote a file");
        }
        h.handle("doc.save", json!({"path": path.to_string_lossy(), "quality": 85})).unwrap();
        let bytes = std::fs::read(&path).unwrap();
        assert!(bytes.windows(4).any(|c| c == b"VP8 ") && !bytes.windows(4).any(|c| c == b"VP8L"), "a WebP saved with a quality is lossy");
        std::fs::remove_dir_all(dir).unwrap();
    }

exif_srgb_jpeg_round_trip setup/action excerpt:

let mut s = Session::new();
        s.execute("edit.colorSettings", json!({"workingRgb": "display-p3"})).unwrap();
        // Embed Color Profile off: an untagged document is otherwise tagged sRGB on export (#9325).
        let mut opts = photocraft_io::ExportOptions::default();
        opts.encode.embed_icc = false;
        let r = photocraft_io::export(&untagged_camera_file(Some(1)), "x.jpg", &opts).unwrap();
        let back = photocraft_io::import("x.jpg", &r.bytes).unwrap().document;
        assert!(back.icc_profile.is_none(), "no ICC profile in the file");
        let (_, r) = s.open_document(back, None);
        assert_eq!((r["action"].as_str(), r["exif"].as_bool()), (Some("kept"), Some(true)));
        assert!(desc(&s.active().unwrap().doc).unwrap().starts_with("sRGB"));
    }

Expected assertion context (overlapping windows merged):

assert!(back.icc_profile.is_none(), "no ICC profile in the file");
        let (_, r) = s.open_document(back, None);
        assert_eq!((r["action"].as_str(), r["exif"].as_bool()), (Some("kept"), Some(true)));
        assert!(desc(&s.active().unwrap().doc).unwrap().starts_with("sRGB"));
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9401: fix(format): a save on macOS keeps the file's creation time (#9388)

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

write_keeps_the_replaced_files_creation_time setup/action excerpt:

use std::os::macos::fs::FileTimesExt;
        let (inside, _, workspace) = roots("created");
        let path = inside.join("doc.psd");
        workspace.write("doc.psd", b"original").unwrap();
        let born = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_600_000_000);
        std::fs::OpenOptions::new().write(true).open(&path).unwrap().set_times(std::fs::FileTimes::new().set_created(born)).unwrap();
        workspace.write("doc.psd", b"replacement").unwrap();
        assert_eq!(std::fs::read(&path).unwrap(), b"replacement");
        assert_eq!(std::fs::metadata(&path).unwrap().created().unwrap(), born, "the creation time survives the automation write");
    }

Expected assertion context (overlapping windows merged):

assert_eq!(std::fs::read(&path).unwrap(), b"replacement");
        assert_eq!(std::fs::metadata(&path).unwrap().created().unwrap(), born, "the creation time survives the automation write");
    }

keeps_the_original_creation_time setup/action excerpt:

use std::os::macos::fs::FileTimesExt;
    let d = TempDir::new("created");
    let p = d.0.join("doc.psd");
    std::fs::write(&p, ORIGINAL).unwrap();
    // A creation time well in the past, so a reset to "now" can't pass for it.
    let born = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_600_000_000);
    std::fs::OpenOptions::new().write(true).open(&p).unwrap().set_times(std::fs::FileTimes::new().set_created(born)).unwrap();
    assert_eq!(std::fs::metadata(&p).unwrap().created().unwrap(), born);
    atomic_write(&p, b"new").unwrap();
    assert_eq!(std::fs::read(&p).unwrap(), b"new");
    assert_eq!(std::fs::metadata(&p).unwrap().created().unwrap(), born, "the creation time survives the save");
    assert_no_temp(&d.0);

Expected assertion context (overlapping windows merged):

assert_eq!(std::fs::metadata(&p).unwrap().created().unwrap(), born);
    atomic_write(&p, b"new").unwrap();
    assert_eq!(std::fs::read(&p).unwrap(), b"new");
    assert_eq!(std::fs::metadata(&p).unwrap().created().unwrap(), born, "the creation time survives the save");
    assert_no_temp(&d.0);
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9416: Affinity reader: contiguous curve records and a cheaper object tag index

  • Fixture/setup: Synthetic repeated tags and fixed-record stream in affinity/src/tests.rs; licensed curve document with known topology/reference.

  • Action: run each command separately:

    • cargo test -p photocraft-affinity --locked lookup_keeps_first_match_for_every_object_size -- --nocapture
    • cargo test -p photocraft-affinity --locked a_fixed_size_array_is_one_buffer_and_one_value_per_record -- --nocapture
  • Expected: Tag lookup preserves first occurrence for sizes 0–59 and positional-tag exclusion; five 18-byte records occupy one buffer and report five elements. Full curve import preserves finite control points, nodes and topology.

  • Separate coverage / prerequisites: Also run full affinity suite and applicable pinned corpus tests. Record curve fixture hash and compare imported geometry/appearance; if no end-to-end contiguous-curve fixture exists, mark that subcase BLOCKED. Storage tests alone do not establish render fidelity.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-9590: fix(io): PSD layer-effect contours keep at most 16 points

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: psd contours keep at most 16 points and their peaks; a point on a tiny straight span is not a bend. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

psd_contours_keep_at_most_16_points_and_their_peaks setup/action excerpt:

let points: Vec<CurvePoint> = (0..29)
            .map(|i| {
                let x = i as f32 / 28.0;
                // Rings at x = 0.25 and 0.75, small wiggles between.
                let y = (1.0 - ((x - 0.25).abs() * 8.0)).max(0.0).max(1.0 - (x - 0.75).abs() * 8.0) + if i % 2 == 0 { 0.0 } else { 0.01 };
                CurvePoint { input: x, output: y.clamp(0.0, 1.0) }
            })
            .collect();
        let c = Contour::Custom { name: "Two rings".into(), points: points.clone(), corners: vec![false; 29] };
        let out = crv(&contour_value(&c));
        assert_eq!(out.len(), 16);
        assert_eq!(out.first().copied(), Some((0.0, f64::from(points[0].output) * 255.0)));

Expected assertion context (overlapping windows merged):

assert_eq!(out.len(), 16);
        assert_eq!(out.first().copied(), Some((0.0, f64::from(points[0].output) * 255.0)));
        assert_eq!(out.last().map(|p| p.0), Some(255.0));
        let peak = |x: f32| out.iter().any(|p| ((p.0 / 255.0) as f32 - x).abs() < 0.03 && p.1 > 240.0);
        assert!(peak(0.25) && peak(0.75), "both rings kept: {out:?}");
        assert!(out.windows(2).all(|w| w[0].0 < w[1].0), "in order: {out:?}");

        let short = Contour::Custom { name: "Short".into(), points: points[..16].to_vec(), corners: vec![false; 16] };
        assert_eq!(crv(&contour_value(&short)).len(), 16);
    }

a_point_on_a_tiny_straight_span_is_not_a_bend setup/action excerpt:

let p = |input: f32, output: f32| CurvePoint { input, output };
        // Three distinct inputs a few ulps apart near 0.5, on a straight rise from 0 to 1.
        let (a, b) = (0.5_f32, f32::from_bits(0.5_f32.to_bits() + 2));
        let mid = f32::from_bits(0.5_f32.to_bits() + 1);
        let mut points = vec![p(0.0, 0.0), p(0.2, 0.0)];
        points.extend([p(a, 0.0), p(mid, 0.5), p(b, 1.0)]);
        // Fourteen gentle bends after it, each one a real (if small) peak.
        points.extend((0..14).map(|k| p(0.55 + k as f32 * 0.03, if k % 2 == 0 { 1.0 } else { 0.9 })));
        points.push(p(1.0, 1.0));
        let kept = psd_contour_points(&points);
        assert_eq!(kept.len(), 16);
        assert!(!kept.contains(&3), "the point on the tiny straight span was kept over a real bend: {kept:?}");

Expected assertion context (overlapping windows merged):

assert_eq!(kept.len(), 16);
        assert!(!kept.contains(&3), "the point on the tiny straight span was kept over a real bend: {kept:?}");
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4763: refactor(liquify): reuse native brush resizing and anchored HUD feedback

  • Fixture/setup: Windows portable build, synthetic grid in Liquify, Warp tool, brush diameter 40 px, density 50%, preview zoom 100%; repeat at 200%. Record document/history and brush settings before the gesture.
  • Action: Hold Alt and right-drag without Shift, or Ctrl+Alt and primary-drag. Move 25 screen points right: at 100% zoom diameter becomes 90 px, at 200% zoom 65 px. From density 50%, move 100 screen points down: density becomes 100% at either zoom; 100 points up becomes 0%. Release, then Cancel Liquify. Alt+primary alone remains the opposite-tool gesture; Shift+Alt+right is not brush resize.
  • Expected: HUD and circle remain anchored at press origin, show diameter/density, and follow the shared zoom-corrected resize formula. Resizing starts on the press and creates no paint stroke/history; releasing ends resize. Cancel restores original document and history.
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester:

PR-4985: feat(ui): add Shift+right-click brush mode picker

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

shift_right_mode_picker_selects_journals_and_clear_erases setup/action excerpt:

use egui_kittest::kittest::Queryable;
    let mut h = harness(Some("erase"));
    let history = h.state().session.active().unwrap().history.past_len();
    let journal = h.state().session.journal.len();
    mode_right_click(&mut h, Modifiers::SHIFT);
    assert!(h.state().ui.brush_mode_menu.is_some());
    assert!(h.state().ui.brush_picker.is_none() && h.state().drag.is_none());
    assert_eq!(h.state().session.journal.len(), journal);
    assert_eq!(h.state().session.active().unwrap().history.past_len(), history);
    h.get_by_label("Multiply").click();
    h.run_steps(2);
    assert!(h.state().ui.brush_mode_menu.is_none());

Expected assertion context (overlapping windows merged):

assert!(h.state().ui.brush_mode_menu.is_some());
    assert!(h.state().ui.brush_picker.is_none() && h.state().drag.is_none());
    assert_eq!(h.state().session.journal.len(), journal);
    assert_eq!(h.state().session.active().unwrap().history.past_len(), history);
    h.get_by_label("Multiply").click();
    h.run_steps(2);
    assert!(h.state().ui.brush_mode_menu.is_none());
    assert_eq!(h.state().session.tools.brush.mode, photocraft_doc::BlendMode::Multiply);
    assert_eq!(h.state().session.journal.last().unwrap(), &("tools.setBrush".into(), json!({"brush": {"mode": "Multiply"}})));
    let (a, b) = drag(&mut h, PointerButton::Primary);
    let mid = a + (b - a) * 0.5;
    assert!(alpha_at(&h, mid) > 0.9);
    mode_right_click(&mut h, Modifiers::SHIFT);
    h.get_by_label("Clear").click();
    h.run_steps(2);
    assert!(h.state().ui.tool_options.brush_clear);
    assert_eq!(h.state().session.tools.brush.mode, photocraft_doc::BlendMode::Multiply, "Clear keeps the underlying mode, as the options bar does");
    drag(&mut h, PointerButton::Primary);
    assert_eq!(alpha_at(&h, mid), 0.0);
    assert!(strokes(&h).last().unwrap()["erase"].as_bool().unwrap());
}

mode_picker_outside_dismissal_does_not_paint_or_erase setup/action excerpt:

for button in [PointerButton::Primary, PointerButton::Secondary] {
        let mut h = harness(Some("erase"));
        drag(&mut h, PointerButton::Primary);
        let journal = h.state().session.journal.len();
        let history = h.state().session.active().unwrap().history.past_len();
        mode_right_click(&mut h, Modifiers::SHIFT);
        // A full outside drag must only dismiss, including its release click.
        drag(&mut h, button);
        assert!(h.state().ui.brush_mode_menu.is_none());
        assert_eq!(h.state().session.journal.len(), journal, "dismissal must not paint/erase with {button:?}");
        assert_eq!(h.state().session.active().unwrap().history.past_len(), history);
    }

Expected assertion context (overlapping windows merged):

assert!(h.state().ui.brush_mode_menu.is_none());
        assert_eq!(h.state().session.journal.len(), journal, "dismissal must not paint/erase with {button:?}");
        assert_eq!(h.state().session.active().unwrap().history.past_len(), history);
    }
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-5082: Mixer Brush Alt-click loads reservoir while preserving foreground

Historical PR title: fix(mixer-brush): Alt-click samples the foreground colour like the Brush (#4873). The acceptance behavior is reservoir loading with foreground unchanged.

  • Fixture/setup: Red/green canvas with blue foreground from alt_click_tests.rs; engine 80×40 transparent canvas with blue stroke and red foreground.

  • Action: run each command separately:

    • cargo test -p photocraft-ui-egui --locked alt_click_with_the_mixer_brush_loads_the_reservoir_not_the_foreground -- --nocapture
    • cargo test -p photocraft-engine --locked mixer_load_fills_the_reservoir_from_the_canvas_and_keeps_the_foreground -- --nocapture
  • Expected: Alt-click loads Current Brush Load/reservoir, preserving foreground and document revision and painting nothing. Dry stroke uses sampled blue until foreground changes; green foreground then paints green. Transparent sampling loads nothing; invalid coordinates preserve all state.

  • Separate coverage / prerequisites: Historical PR title is stale: this case follows reviewed reservoir behavior, not foreground replacement. Repeat Alt/Option-click natively as a separate NOT RUN subcase.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-5165: Fix: brush section selection

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

selecting_a_controls_section_keeps_its_enabled_flag_and_brush_history setup/action excerpt:

use egui_kittest::kittest::Queryable;

    for (section, name) in [(1, "Shape Dynamics"), (2, "Scattering"), (3, "Texture")] {
        let mut h = harness(0, 0);
        let before = h.state().session.tools.brush.clone();
        let journal = h.state().session.journal.len();

        h.get_by_label(name).click();
        h.run_steps(3);

        assert_eq!(h.state().ui.brush_section, section, "{name} settings are visible");
        assert_eq!(h.state().session.tools.brush, before, "{name} remains disabled");

Expected assertion context (overlapping windows merged):

assert_eq!(h.state().ui.brush_section, section, "{name} settings are visible");
        assert_eq!(h.state().session.tools.brush, before, "{name} remains disabled");
        assert_eq!(h.state().session.journal.len(), journal, "selecting {name} is not a brush edit");
    }
}

controls_section_checkbox_toggles_without_selecting_its_settings setup/action excerpt:

use egui_kittest::kittest::Queryable;

    let mut h = harness(0, 0);
    let row = h.get_by_label("Scattering").rect();
    let checkbox = egui::pos2(row.left() + 12.0, row.center().y);
    let journal = h.state().session.journal.len();

    for enabled in [true, false] {
        h.hover_at(checkbox);
        h.run_steps(1);
        h.drag_at(checkbox);
        h.run_steps(1);

Expected assertion context (overlapping windows merged):

assert_eq!(h.state().session.tools.brush.scattering.enabled, enabled);
        assert_eq!(h.state().ui.brush_section, 0, "the checkbox does not select Scattering");
        assert_eq!(last_journal(h.state()).unwrap().1["brush"], json!({"scattering": {"enabled": enabled}}));
    }
    assert_eq!(h.state().session.journal.len(), journal + 2);
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-6139: Fix painting workflows, File Info and layer selection

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

a_refresh_outside_command_execution_surfaces_the_denial_instead_of_rendering_unfiltered setup/action excerpt:

// link-watch and transform paths re-render through Session::edit, not Session::execute or
    // jobs::run; the gate must reach them and the denial must not silently pass pixels through
    // into the process-wide stack cache.
    for lookup in [false, true] {
        let f = Fixture::new();
        let (doc, layer) = f.saved_filter(lookup);
        let mut h = f.headless();
        h.open(Path::new("stored.pcraft")).unwrap();
        let before = h.session.active().unwrap().doc.clone();
        // Session::edit is the outside-execute mutation path (an edit closure the UI or a
        // linked-file change runs); the stored filter must be denied there too.
        assert_denied(h.session.edit("Re-render", |d, _| match smart_cmds::refresh(d, layer) {

Expected assertion context (overlapping windows merged):

assert!(Arc::ptr_eq(&before, &h.session.active().unwrap().doc));
        // A trusted session (no gate) still renders the same document.
        let mut trusted = Session::new();
        trusted.add_document(doc, None);
        assert!(smart_cmds::refresh(&mut d, layer).is_ok());
    }
}

behind_brush_and_pencil_preserve_foreground_and_round_trip_history setup/action excerpt:

for cmd in ["paint.stroke", "paint.pencil"] {
        let mut s = session(40, 20);
        s.execute(cmd, json!({"points": [[10, 10]], "size": 6, "color": "#ff0000", "hardness": 1.0})).unwrap();
        let before = surface(&s);
        s.execute(cmd, json!({"points": [[2, 10], [38, 10]], "size": 6, "color": "#0000ff", "mode": "behind", "hardness": 1.0})).unwrap();
        assert_eq!(rgba(&s, 10, 10), [1.0, 0.0, 0.0, 1.0], "{cmd}: existing opaque paint");
        assert_eq!(rgba(&s, 25, 10), [0.0, 0.0, 1.0, 1.0], "{cmd}: transparent pixels");
        let painted = surface(&s);
        s.execute("edit.undo", json!({})).unwrap();
        assert!(same_pixels(&surface(&s), &before, Rect::new(0, 0, 40, 20)));
        s.execute("edit.redo", json!({})).unwrap();
        assert!(same_pixels(&surface(&s), &painted, Rect::new(0, 0, 40, 20)));

Expected assertion context (overlapping windows merged):

assert_eq!(rgba(&s, 10, 10), [1.0, 0.0, 0.0, 1.0], "{cmd}: existing opaque paint");
        assert_eq!(rgba(&s, 25, 10), [0.0, 0.0, 1.0, 1.0], "{cmd}: transparent pixels");
        let painted = surface(&s);
        s.execute("edit.undo", json!({})).unwrap();
        assert!(same_pixels(&surface(&s), &before, Rect::new(0, 0, 40, 20)));
        s.execute("edit.redo", json!({})).unwrap();
        assert!(same_pixels(&surface(&s), &painted, Rect::new(0, 0, 40, 20)));
    }
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-7848: fix(retouch): preserve Smudge pickup with live-pixel stroke commits

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

pressure_shape_and_smoothing_match_replay setup/action excerpt:

let mut failures = Vec::new();
    for cmd in ["paint.smudge", "paint.blur", "paint.sharpen", "paint.dodge", "paint.burn", "paint.sponge"] {
        for case in ["pressure", "locked", "ellipse", "smoothing", "scattering"] {
            for depth in [8, 16, 32] {
                let mut s = setup(depth, "rgb");
                match case {
                    "locked" => {
                        s.edit("lock alpha", |doc, active| {
                            doc.layer_mut(active.unwrap()).unwrap().locks.transparency = true;
                            Ok(())
                        })
                        .unwrap();

Expected assertion context (overlapping windows merged):

assert_eq!(result["fromLive"], true);
                let got = pixels(&s);
                let count = got.iter().zip(&want).filter(|(a, b)| a != b).count();
                let max = got.iter().zip(&want).map(|(a, b)| (a - b).abs()).fold(0.0f32, f32::max);
    assert!(failures.is_empty(), "{}", failures.join("\n"));
}

native_modes_selection_and_lock_match_replay setup/action excerpt:

for mode in ["rgb", "gray", "cmyk", "lab"] {
        for depth in [8, 16, 32] {
            let mut s = setup(depth, mode);
            s.edit("lock alpha", |doc, active| {
                doc.layer_mut(active.unwrap()).unwrap().locks.transparency = true;
                Ok(())
            })
            .unwrap();
            s.execute("select.rect", json!({"x":30,"y":20,"width":110,"height":60})).unwrap();
            let points = [[-10.0, 30.0], [80.0, 50.0], [160.0, 80.0], [60.0, 60.0]];
            for cmd in ["paint.blur", "paint.sharpen", "paint.dodge", "paint.burn", "paint.sponge", "paint.smudge"] {
                let params = json!({"points":points,"size":30,"hardness":40,"strength":70,"opacity":37});

Expected assertion context (overlapping windows merged):

assert_eq!(result["fromLive"], true);
                assert_eq!(result["damage"], expected_damage);
                assert_eq!(pixels(&s), want, "{mode} {depth} {cmd}");
                s.execute("edit.undo", json!({})).unwrap();
            }
        }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9623: Pencil: keep a 1 px freehand line connected where it turns

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: pencil 1px freehand line never breaks where it turns. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

pencil_1px_freehand_line_never_breaks_where_it_turns setup/action excerpt:

// #8040: each segment of a freehand stroke samples its own line, so where the stroke turned
    // the last pixel of one segment and the first of the next could sit two pixels apart.
    let b = BrushSettings { aliased: true, hardness: 1.0, size: 1.0, ..brush() };
    let mut seed = 0x2545_f491_4f6c_dd1du64;
    let mut rnd = || {
        seed ^= seed << 13;
        seed ^= seed >> 7;
        seed ^= seed << 17;
        (seed >> 11) as f64 / (1u64 << 53) as f64
    };
    for _ in 0..300 {
        let mut pts = vec![StrokePoint::new(20.0 + rnd() * 8.0, 20.0 + rnd() * 8.0, 1.0)];

Expected assertion context (overlapping windows merged):

assert_eq!(reached.len(), painted.len(), "the line breaks along {path:?}");
    }
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3554: fix(packaging): launch bundled CLI directly from AppImage

  • Fixture/setup: Run the checked-in AppRun CLI test; it builds stand-in binaries under a path with spaces.
  • Action: bash packaging/linux/apprun-cli-test.sh
  • Expected: Argument boundaries, MCP stdio round trip, exit status 37 and unchanged GUI entry behavior all pass the script assertions.
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester:

PR-4742: fix(macos): bundle photocraft-cli and resolve it from droplet scripts

  • Fixture/setup: Frozen-SHA macOS bundle containing executable Contents/MacOS/photocraft-cli; synthetic droplet and image paths with spaces.

  • Action: run each command separately:

    • cargo test -p photocraft-engine --locked droplets_are_written_and_run -- --nocapture
  • Expected: Explicit PHOTOCRAFT_CLI wins, then supported /Applications and ~/Applications bundles, then PATH. Arguments remain separate; the intended action runs without standalone CLI installation; ordinary signature verification succeeds.

  • Separate coverage / prerequisites: Use an authorized test installation; do not replace user's app. Installed-bundle resolution is BLOCKED if required bundle is absent. Finder dropping onto a plain shell shim is not promised; app wrapper is separate.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-7743: packaging: the portable zip holds the files, not a folder with the files in it

  • Fixture/setup: Obtain an existing portable Windows ZIP built from the frozen integration; record its artifact identity.
  • Action: List root archive entries and extract into a new empty directory; launch the app from the extracted root.
  • Expected: Application files are directly at ZIP root without a redundant wrapping folder; extracted layout launches correctly.
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester:

PR-8985: Color panel and Color Picker: clip the round cursor to the colour field

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: field marker stays inside the field; color field cursor stays inside the field. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

field_marker_stays_inside_the_field setup/action excerpt:

// Black in the default "h" mode: the marker is at the bottom-left corner.
        let h = picker("#000000");
        let mut field = None;
        let mut circles = Vec::new();
        for clipped in &h.output().shapes {
            match &clipped.shape {
                egui::Shape::Mesh(m) if m.vertices.len() == 33 * 33 => {
                    // The component slider is a 33 x 33 mesh too, drawn after the field.
                    field.get_or_insert(m.calc_bounds());
                }
                egui::Shape::Circle(c) => circles.push((c.center, c.radius, clipped.clip_rect)),
                _ => {}

Expected assertion context (overlapping windows merged):

assert!(!circles.is_empty(), "the marker is drawn");
        for (center, radius, clip) in circles {
            let visible = Rect::from_center_size(center, vec2(2.0 * radius, 2.0 * radius)).intersect(clip);
            assert!(field.contains_rect(visible), "marker {visible:?} leaves the field {field:?}");
        }
    }

color_field_cursor_stays_inside_the_field setup/action excerpt:

let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        // Black is s = 0, v = 0: the bottom-left corner.
        app.session.tools.foreground = [0.0, 0.0, 0.0, 1.0];
        let h = field_harness(app);
        let mut field = None;
        let mut circles = Vec::new();
        for clipped in &h.output().shapes {
            match &clipped.shape {
                // The saturation/brightness field is the 17 x 17 mesh; the hue strip is a 25 x 25 grid.
                egui::Shape::Mesh(m) if m.vertices.len() == 17 * 17 => field = Some(m.calc_bounds()),
                egui::Shape::Circle(c) => circles.push((c.center, c.radius, clipped.clip_rect)),
                _ => {}

Expected assertion context (overlapping windows merged):

assert!(rings.iter().any(|(_, radius, _)| *radius == 5.0), "inner ring sits at the field's bottom-left corner");
        assert!(rings.iter().any(|(_, radius, _)| *radius == 6.5), "outer ring sits at the field's bottom-left corner");
        for (center, radius, clip) in rings {
            let visible = egui::Rect::from_center_size(center, vec2(2.0 * radius, 2.0 * radius)).intersect(clip);
            assert!(field.contains_rect(visible), "cursor ring {visible:?} leaves the field {field:?}");
        }
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9019: fix(xtask): reject unknown performance benchmark selections

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: unknown bench is rejected before updating the baseline. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

unknown_bench_is_rejected_before_updating_the_baseline setup/action excerpt:

let root = std::env::temp_dir().join(format!("photocraft-perf-unknown-bench-{}", std::process::id()));
        std::fs::create_dir(&root).unwrap();
        std::fs::create_dir(root.join("perf")).unwrap();
        std::fs::write(root.join(BUDGETS), "[[bench]]\nname = \"known\"\npackage = \"unused\"\nrequires = \"missing.rs\"\n").unwrap();
        let baseline = "{\"schema\":1,\"sentinel\":true}";
        std::fs::write(root.join(BASELINE), baseline).unwrap();
        let results = [
            run(&root, &["--bench", "unknown", "--reuse", "--update-baseline"]),
            run(&root, &["--bench", "known", "--bench", "unknown", "--quick", "--reuse", "--update-baseline"]),
        ];
        let after = std::fs::read_to_string(root.join(BASELINE)).unwrap();
        std::fs::remove_dir_all(root).unwrap();

Expected assertion context (overlapping windows merged):

assert_eq!(result, Err(format!("perf: unknown bench `unknown` (see {BUDGETS})")));
        }
        assert_eq!(after, baseline, "an unknown bench must not update the baseline");
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9110: test: GPU tests honour WGPU_BACKEND, so they run on Windows with an Intel GPU (#4)

  • Fixture/setup: Windows Intel host with functioning DX12; record OS, GPU and driver; frozen integration build.

  • Action: run each command separately:

    • cargo test -p photocraft-gpu --locked injected_fault_skips_the_gpu -- --nocapture
    • cargo test -p photocraft-ui-egui --locked layout_document_composites_on_the_gpu_like_the_cpu -- --nocapture
  • Expected: Every process honors WGPU_BACKEND=dx12. Example completes real composition; fault test renders before injecting loss and returns expected lost-device error; layout test executes CPU/GPU assertions.

  • Separate coverage / prerequisites: Run cargo run --release -p photocraft-gpu --locked --example bench_stack_reuse -- --quick with WGPU_BACKEND=dx12. Record adapter/vendor/device/backend/driver and proof each comparison executed. Silent early returns are BLOCKED. Do not load the known-crashing Intel Vulkan backend.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-9436: Startup: a failed launch says why in a dialog instead of closing silently (#7643)

  • Fixture/setup: startup_report.rs terminal/automation/display combinations and synthetic renderer/error/log text.

  • Action: run each command separately:

    • cargo test -p photocraft --locked a_desktop_launch_asks_the_user_and_a_script_does_not -- --nocapture
    • cargo test -p photocraft --locked the_text_names_the_error_renderer_version_and_log -- --nocapture
    • cargo test -p photocraft --locked a_fallback_that_was_not_the_preference_is_explained -- --nocapture
    • cargo test -p photocraft --locked path_names_the_attached_log_file -- --nocapture
  • Expected: Desktop launch with display and without terminal/control asks via dialog; terminal/headless/automation do not hang. Text contains error, attempted renderer, version, actual log path and applicable fallback reason; no invented log path.

  • Separate coverage / prerequisites: Native dialog subcase uses only an existing safe failure reproduction in temporary settings. If unavailable, mark BLOCKED; never force a GPU driver crash.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-2300: Scroll the viewport while outlining with Polygonal Lasso

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

polygon_edge_scroll_continues_with_button_up_and_keeps_vertices_fixed setup/action excerpt:

for (dx, dy, flip, rotation) in [
        (-1.0, 0.0, false, 0.0),
        (1.0, 0.0, false, 0.0),
        (0.0, -1.0, false, 0.0),
        (0.0, 1.0, false, 0.0),
        (-1.0, -1.0, false, 0.0),
        (1.0, -1.0, false, 0.0),
        (-1.0, 1.0, false, 0.0),
        (1.0, 1.0, false, 0.0),
        (1.0, 0.0, true, 0.0),
        (1.0, 0.0, false, 90.0),
    ] {

Expected assertion context (overlapping windows merged):

assert!(movement.dot(direction) > 0.1, "edge ({dx},{dy}), flip={flip}, rotation={rotation}: {movement:?}");
        assert!((movement.x * direction.y - movement.y * direction.x).abs() < 0.001, "scroll follows the displayed axes");
        assert!((next[0] - first[0]).hypot(next[1] - first[1]) > 0.1, "stationary pointer keeps scrolling");
        assert_eq!(h.state().ui.polygon, points, "view motion never adds or moves vertices");
        assert_eq!(steps(&h), before);
        assert!(selection(&h).is_none());
        assert!(!h.ctx.input(|i| i.pointer.any_down()));
        assert!(h.output().viewport_output[&egui::ViewportId::ROOT].repaint_delay.is_zero());
        h.event(egui::Event::PointerMoved(xf.rect.center()));
        h.run_steps(1);
        let stopped = h.state().ui.views[0].center;
        assert_eq!(h.state().ui.views[0].center, stopped, "returning to the interior stops scrolling");
    }

    let mut h = zoomed_polygon();
    assert!((center[0] - (400.0 - xf.rect.width() / (2.0 * xf.zoom))).abs() < 0.001, "Overscroll off stops at the image's right edge");
    h.run_steps(10);
    assert_eq!(h.state().ui.views[0].center, center, "a long edge hold stays clamped");
    assert_eq!(h.state().ui.polygon, points);
    assert_eq!(steps(&h), before);
}

polygon_edge_scroll_places_the_next_vertex_in_the_panned_view_and_commits_once setup/action excerpt:

let mut h = zoomed_polygon();
    let before = steps(&h);
    let original = ViewXform::active(h.state()).unwrap();
    h.event(egui::Event::PointerMoved(original.rect.right_center() - vec2(4.0, 0.0)));
    h.run_steps(10);
    let panned = ViewXform::active(h.state()).unwrap();
    assert!(panned.center[0] > original.center[0], "the zoomed image pans toward the right edge");
    let at = panned.rect.center() + vec2(60.0, 0.0);
    let expected = panned.to_doc(at);
    h.event(egui::Event::PointerMoved(at));
    h.run_steps(20);
    button(&mut h, at, true, Modifiers::NONE);

Expected assertion context (overlapping windows merged):

assert!(panned.center[0] > original.center[0], "the zoomed image pans toward the right edge");
    let at = panned.rect.center() + vec2(60.0, 0.0);
    let expected = panned.to_doc(at);
    h.event(egui::Event::PointerMoved(at));
    assert_eq!(h.state().ui.polygon[1], expected, "the next click uses the updated view transform");
    h.run_steps(30);
    click(&mut h, expected[0] as f32, 190.0, Modifiers::NONE);
    h.key_press(egui::Key::Enter);
    assert!(h.state().ui.polygon.is_empty());
    assert!(selection(&h).is_some_and(|r| near(r, 170, 130, expected[0].ceil() as i32, 190)), "{:?}", selection(&h));
    assert_eq!(steps(&h), before + 1);
    h.event(egui::Event::PointerMoved(original.rect.right_center()));
    h.run_steps(4);
    assert_eq!(h.state().ui.views[0].center, panned.center, "a finished polygon never scrolls");
    h.state_mut().run("edit.undo", json!({})).unwrap();
    assert!(selection(&h).is_none(), "one undo removes the completed selection");
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-5010: Layer Style: Photoshop's layout (three columns, OK/Cancel/New Style/Preview on the right, one-line rows) [depends on #4938]

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

dragging_the_title_bar_moves_the_dialog setup/action excerpt:

use egui_kittest::{Harness, kittest::Queryable};

        let app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        let mut harness = Harness::builder().with_size(egui::vec2(1400.0, 1100.0)).build_ui_state(|ui, app| show(app, ui.ctx()), app);
        PhotocraftApp::setup_context(&harness.ctx, crate::theme::ThemeKind::ALL[0]);
        harness.state_mut().ui.open_dialog(DialogKind::LayerStyle, serde_json::Map::new());
        harness.run_steps(3);
        let before = harness.get_by_label("Layer Style").rect();

        // Grab the title text itself: it must move the dialog, not select the text.
        let from = before.center();
        harness.hover_at(from);

Expected assertion context (overlapping windows merged):

assert!((moved - egui::vec2(-120.0, 80.0)).length() < 1.0, "dialog moved by {moved:?}");
        assert_eq!(harness.state().ui.dialogs.len(), 1, "dragging must not close the dialog");
    }

layer_style_keeps_one_size_on_every_page setup/action excerpt:

let mut h = layer_style_on_bevel(egui::vec2(1400.0, 900.0));
        let size = frame(&h).size();
        let dialog = h.state().ui.dialogs[0].id;
        for page in ["blendingOptions", "styles"] {
            h.state_mut().ui.dialog_mut(dialog).expect("still open").fields.insert("selected".into(), json!(page));
            h.run_steps(3);
            assert_eq!(frame(&h).size(), size, "{page}");
        }
        let grip = egui::Id::new(("dialog", dialog)).with("resize").with("__resize_corner");
        assert!(h.ctx.read_response(grip).is_none(), "no resize grip");
    }

Expected assertion context (overlapping windows merged):

assert_eq!(frame(&h).size(), size, "{page}");
        }
        let grip = egui::Id::new(("dialog", dialog)).with("resize").with("__resize_corner");
        assert!(h.ctx.read_response(grip).is_none(), "no resize grip");
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-5515: Selecting subject now automatically applies to selected layer, and us…

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

select_subject_samples_the_selected_layer_else_the_composite setup/action excerpt:

let (mut s, inside) = disc(8);
        // A layer above holding only a small square in the corner: the disc is not on it.
        s.execute("layer.new.layer", json!({})).unwrap();
        s.execute("paint.pencil", json!({"points": [[10, 10], [30, 10]], "size": 3, "color": "#000000"})).unwrap();
        s.execute("select.subject", json!({})).unwrap();
        assert!(cov(&s, 100, 80) < 0.5, "the disc on the layer below was selected");
        s.execute("select.deselect", json!({})).unwrap();
        // No layer selected: the whole image, so the disc is found.
        s.execute("select.deselectLayers", json!({})).unwrap();
        s.execute("select.subject", json!({})).unwrap();
        assert!(sel_iou(&s, 200, 160, &inside) > 0.9);
    }

Expected assertion context (overlapping windows merged):

assert!(cov(&s, 100, 80) < 0.5, "the disc on the layer below was selected");
        s.execute("select.deselect", json!({})).unwrap();
        // No layer selected: the whole image, so the disc is found.
        s.execute("select.deselectLayers", json!({})).unwrap();
        assert!(sel_iou(&s, 200, 160, &inside) > 0.9);
    }

select_subject_repeat_reuses_the_mask_until_pixels_change setup/action excerpt:

let (mut s, _) = disc(8);
        let sel = |s: &Session| {
            let mut v = Vec::new();
            s.active().unwrap().doc.selection.as_ref().unwrap().read_region_into(Rect::new(0, 0, 200, 160), &mut v);
            v
        };
        let cached = |s: &Session| s.subject_cache.region.clone().unwrap();
        s.execute("select.subject", json!({})).unwrap();
        let (first, mask) = (sel(&s), cached(&s));
        s.execute("select.deselect", json!({})).unwrap();
        s.execute("select.subject", json!({})).unwrap();
        assert!(std::sync::Arc::ptr_eq(&mask, &cached(&s)), "the repeat recomputed the mask");

Expected assertion context (overlapping windows merged):

assert!(std::sync::Arc::ptr_eq(&mask, &cached(&s)), "the repeat recomputed the mask");
        assert_eq!(sel(&s), first);
        // Another sampled target (the composite rather than the layer) is a miss.
        s.execute("select.subject", json!({"sampleAllLayers": true})).unwrap();
        assert!(!std::sync::Arc::ptr_eq(&mask, &cached(&s)));
        // A pixel edit drops the entry; the next run recomputes it.
        s.edit("paint", |doc, _| {
            let bg = doc.layers[0].surface_mut().unwrap();
        assert!(s.subject_cache.region.is_none());
        s.execute("select.subject", json!({})).unwrap();
        assert_eq!(sel(&s), first);
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-5650: Guides: ⌥ turns a dragged guide; dropping one on either ruler deletes it (#5034)

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

move_guide_turns_a_guide setup/action excerpt:

let mut s = Session::new();
        s.execute("file.new", json!({"width": 200, "height": 100})).unwrap();
        s.execute("view.newGuide", json!({"orientation": "horizontal", "position": 30, "color": "magenta"})).unwrap();
        s.execute("view.newGuide", json!({"orientation": "horizontal", "position": 70})).unwrap();
        s.execute("view.moveGuide", json!({"orientation": "horizontal", "index": 0, "position": "50%", "toOrientation": "vertical"})).unwrap();
        let g = &s.active().unwrap().doc.guides;
        assert_eq!((g.horizontal.clone(), g.vertical.clone()), (vec![70.0], vec![100.0]), "50% of the width");
        assert_eq!(g.color(true, 100.0), Some([255, 0, 255]), "the colour moves with the guide");
        assert_eq!(g.color(false, 30.0), None);
        // The same orientation is a plain move.
        s.execute("view.moveGuide", json!({"orientation": "vertical", "index": 0, "position": 20, "toOrientation": "vertical"})).unwrap();
        assert_eq!(s.active().unwrap().doc.guides.vertical, vec![20.0]);

Expected assertion context (overlapping windows merged):

assert_eq!((g.horizontal.clone(), g.vertical.clone()), (vec![70.0], vec![100.0]), "50% of the width");
        assert_eq!(g.color(true, 100.0), Some([255, 0, 255]), "the colour moves with the guide");
        assert_eq!(g.color(false, 30.0), None);
        // The same orientation is a plain move.
        s.execute("view.moveGuide", json!({"orientation": "vertical", "index": 0, "position": 20, "toOrientation": "vertical"})).unwrap();
        assert_eq!(s.active().unwrap().doc.guides.vertical, vec![20.0]);
        s.undo();
        s.undo();
        let g = &s.active().unwrap().doc.guides;
        assert_eq!((g.horizontal.clone(), g.vertical.clone()), (vec![30.0, 70.0], vec![]), "one undo step per move");
        // Bad params are refused and leave the guides alone.
        assert!(s.execute("view.moveGuide", json!({"orientation": "horizontal", "index": 0, "position": 5, "toOrientation": "diagonal"})).is_err());
        assert!(s.execute("view.moveGuide", json!({"orientation": "horizontal", "index": 0, "position": 5, "toOrientation": 1})).is_err());
        assert!(s.execute("view.newGuide", json!({"orientation": "horizontal", "position": 5, "toOrientation": "vertical"})).is_err());
        assert!(s.execute("view.moveGuide", json!({"orientation": "horizontal", "index": 9, "position": 5, "toOrientation": "vertical"})).is_err());
        let g = &s.active().unwrap().doc.guides;
        assert_eq!((g.horizontal.clone(), g.vertical.clone()), (vec![30.0, 70.0], vec![]));
    }

layer_panel_hover_outlines_layer_on_canvas setup/action excerpt:

let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        app.session.execute("file.new", json!({"width": 64, "height": 64})).unwrap();
        app.session.execute("layer.new.layer", json!({})).unwrap();
        app.session
            .edit("paint", |doc, a| {
                doc.layer_mut(a.unwrap()).unwrap().surface_mut().unwrap().fill_rect(Rect::new(8, 8, 24, 24), &[1.0, 0.0, 0.0, 1.0]);
                Ok(())
            })
            .unwrap();
        let target_layer = app.session.active().unwrap().active_layer.unwrap();
        let a = Rect::new(8, 8, 24, 24);

Expected assertion context (overlapping windows merged):

assert_eq!(active_outline(&mut app, None, false, false), None);
        app.hover_layer = Some(target_layer);
        assert_eq!(active_outline(&mut app, None, false, false), Some(a));

        // When a drag or transform is in progress, no outline:
        app.guide_drag = Some(crate::rulers::GuideDrag { vertical: false, index: None, pos: 10.0, ..Default::default() });
        assert_eq!(active_outline(&mut app, None, false, false), None);
        app.guide_drag = None;

        // When rollover preference is disabled, no outline:
        assert_eq!(active_outline(&mut app, None, false, false), None);
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-6026: Marquee: Fixed Size selects exactly the entered width and height (fixes #6015)

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: fixed size px is exactly the size; fixed size selects exactly the entered size. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

fixed_size_px_is_exactly_the_size setup/action excerpt:

use super::fixed_size_px;
        // Down-right from (10.4, 20.6): 10 × 10 from the closest pixel edges.
        assert_eq!(fixed_size_px([10.4, 20.6], [20.4, 30.6], 10.0, 10.0), [10.0, 21.0, 20.0, 31.0]);
        // Up-left: the far corner is the low one; still exactly the size.
        assert_eq!(fixed_size_px([10.4, 20.6], [-53.6, -11.4], 64.0, 32.0), [-54.0, -11.0, 10.0, 21.0]);
        // 1 × 1 is one pixel, and a fractional size rounds to whole pixels.
        assert_eq!(fixed_size_px([3.5, 3.5], [4.5, 4.5], 1.0, 1.0), [4.0, 4.0, 5.0, 5.0]);
        assert_eq!(fixed_size_px([0.0, 0.0], [2.6, 2.4], 2.6, 2.4), [0.0, 0.0, 3.0, 2.0]);
    }

Expected assertion context (overlapping windows merged):

assert_eq!(fixed_size_px([10.4, 20.6], [20.4, 30.6], 10.0, 10.0), [10.0, 21.0, 20.0, 31.0]);
        // Up-left: the far corner is the low one; still exactly the size.
        assert_eq!(fixed_size_px([10.4, 20.6], [-53.6, -11.4], 64.0, 32.0), [-54.0, -11.0, 10.0, 21.0]);
        // 1 × 1 is one pixel, and a fractional size rounds to whole pixels.
        assert_eq!(fixed_size_px([3.5, 3.5], [4.5, 4.5], 1.0, 1.0), [4.0, 4.0, 5.0, 5.0]);
        assert_eq!(fixed_size_px([0.0, 0.0], [2.6, 2.4], 2.6, 2.4), [0.0, 0.0, 3.0, 2.0]);
    }

fixed_size_selects_exactly_the_entered_size setup/action excerpt:

for tool in [Tool::RectMarquee, Tool::EllipseMarquee] {
        for (w, ht) in [(10, 10), (1, 1), (64, 32)] {
            let mut h = harness(tool);
            let o = &mut h.state_mut().ui.tool_options;
            o.marquee_style = "fixedSize".into();
            (o.marquee_width, o.marquee_height) = (w as f32, ht as f32);
            press_at(&mut h, 50.4, 60.6, Modifiers::NONE);
            assert!(readout(&h, w, ht), "{tool:?} {w} × {ht}: the readout shows the entered size");
            release_at(&mut h, 90.0, 90.0, Modifiers::NONE);
            assert_eq!(selection(&h), Rect::new(50, 61, 50 + w, 61 + ht), "{tool:?} {w} × {ht}: dragged");

            // A click: the button goes down and up at one point.

Expected assertion context (overlapping windows merged):

assert!(readout(&h, w, ht), "{tool:?} {w} × {ht}: the readout shows the entered size");
            release_at(&mut h, 90.0, 90.0, Modifiers::NONE);
            assert_eq!(selection(&h), Rect::new(50, 61, 50 + w, 61 + ht), "{tool:?} {w} × {ht}: dragged");

            // A click: the button goes down and up at one point.
            h.state_mut().run("select.deselect", json!({})).unwrap();
            assert_eq!(selection(&h), Rect::new(121, 100, 121 + w, 100 + ht), "{tool:?} {w} × {ht}: clicked");
        }
    }
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-6236: Shift+Alt+right-click opens Photoshop's HUD Color Picker (#6098)

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

shift_alt_right_press_opens_the_hud_not_the_picker setup/action excerpt:

let mut h = harness();
        let c = h.state().last_canvas_rect.center();
        h.event(egui::Event::PointerMoved(c));
        h.run_steps(1);
        button(&mut h, c, true, SHIFT_ALT);
        let hud = h.state().ui.color_hud.expect("the HUD opened");
        assert!(hud.hold);
        assert_eq!(hud.start, [0.0, 0.0, 1.0, 1.0], "seeded from the foreground");
        assert_eq!(h.state().ui.brush_picker, None, "the Brush Preset picker stays closed");
        // The HUD shows under the pointer, on screen.
        let (strip, square) = rects(&h);
        assert!(strip.height() > 100.0 && square.width() > 100.0);

Expected assertion context (overlapping windows merged):

assert!(hud.hold);
        assert_eq!(hud.start, [0.0, 0.0, 1.0, 1.0], "seeded from the foreground");
        assert_eq!(h.state().ui.brush_picker, None, "the Brush Preset picker stays closed");
        // The HUD shows under the pointer, on screen.
        let (strip, square) = rects(&h);
        assert!(strip.height() > 100.0 && square.width() > 100.0);
        assert!(square.right() < h.ctx.content_rect().right());
        // Nothing painted.
        assert!(!h.state().session.journal.iter().any(|(id, _)| id == "paint.stroke"));
    }

quick_selections_shift_alt_right_opens_only_the_selection_menu setup/action excerpt:

let mut h = harness();
        h.state_mut().ui.tool = Tool::QuickSelection;
        h.run_steps(1);
        let c = h.state().last_canvas_rect.center();
        h.event(egui::Event::PointerMoved(c));
        h.run_steps(1);
        button(&mut h, c, true, SHIFT_ALT);
        assert_eq!(h.state().ui.color_hud, None, "no HUD for Quick Selection");
        assert_eq!(h.state().ui.brush_picker, None, "and no Brush Preset picker");
        button(&mut h, c, false, SHIFT_ALT);
        assert_eq!(h.state().ui.color_hud, None, "the release does not open it either");
        assert!(h.state().ui.canvas_tool_menu.is_some(), "the selection menu opens, once");

Expected assertion context (overlapping windows merged):

assert_eq!(h.state().ui.color_hud, None, "no HUD for Quick Selection");
        assert_eq!(h.state().ui.brush_picker, None, "and no Brush Preset picker");
        button(&mut h, c, false, SHIFT_ALT);
        assert_eq!(h.state().ui.color_hud, None, "the release does not open it either");
        assert!(h.state().ui.canvas_tool_menu.is_some(), "the selection menu opens, once");
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-6510: Automation: resolve configured root aliases for absolute paths (#6426)

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

canonical_windows_prefixes_match_without_ambient_resolution setup/action excerpt:

for (request, root) in [
        (r"c:\root\new.bin", r"\\?\C:\root"),
        (r"\\?\C:\root\new.bin", r"C:\root"),
        (r"\\server\share\root\new.bin", r"\\?\UNC\SERVER\SHARE\root"),
        (r"c:\ROOT\new.bin", r"\\?\C:\root"),
        (r"\\?\C:\ROOT\new.bin", r"c:\root"),
        (r"\\server\share\ROOT\new.bin", r"\\?\UNC\SERVER\SHARE\root"),
    ] {
        let remaining = path_beneath_root(Path::new(request), Path::new(root)).unwrap();
        assert_eq!(remaining.as_path(), Path::new("new.bin"));
    }
    for request in [r"C:\root-sibling\new.bin", r"D:\root\new.bin", r"\\.\C:\root\new.bin", r"\\other\share\root\new.bin"] {

Expected assertion context (overlapping windows merged):

assert_eq!(remaining.as_path(), Path::new("new.bin"));
    }
    for request in [r"C:\root-sibling\new.bin", r"D:\root\new.bin", r"\\.\C:\root\new.bin", r"\\other\share\root\new.bin"] {
        assert!(path_beneath_root(Path::new(request), Path::new(r"\\?\C:\root")).is_none());
    }
}

windows_root_case_variants_keep_io_inside_the_grant setup/action excerpt:

let (inside, outside, workspace) = roots("root-case");
    let configured = PathBuf::from(inside.to_string_lossy().to_ascii_uppercase());
    let canonical = PathBuf::from(std::fs::canonicalize(&inside).unwrap().to_string_lossy().to_ascii_uppercase());
    for spelling in [&configured, &canonical] {
        let path = spelling.join("new.bin");
        workspace.write(&path.to_string_lossy(), b"inside").unwrap();
        assert_eq!(workspace.read(&path.to_string_lossy()).unwrap(), b"inside");
        assert!(workspace.write(&spelling.join("..").join("outside").join("blocked.bin").to_string_lossy(), b"no").is_err());
    }
    assert_eq!(std::fs::read(inside.join("new.bin")).unwrap(), b"inside");
    assert!(workspace.write(&configured.with_file_name("INSIDE-SIBLING").join("blocked.bin").to_string_lossy(), b"no").is_err());
    assert!(!outside.join("blocked.bin").exists());

Expected assertion context (overlapping windows merged):

assert_eq!(workspace.read(&path.to_string_lossy()).unwrap(), b"inside");
        assert!(workspace.write(&spelling.join("..").join("outside").join("blocked.bin").to_string_lossy(), b"no").is_err());
    }
    assert_eq!(std::fs::read(inside.join("new.bin")).unwrap(), b"inside");
    assert!(workspace.write(&configured.with_file_name("INSIDE-SIBLING").join("blocked.bin").to_string_lossy(), b"no").is_err());
    assert!(!outside.join("blocked.bin").exists());
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-7648: Pen: Path Operations menu, as in Photoshop (#1397)

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

merge_shape_components_bakes_a_paths_operations setup/action excerpt:

let mut s = session();
        s.execute("path.set", json!({"path": square()})).unwrap();
        let hole = json!({"subpaths": [{"closed": true, "knots": [[25, 25], [45, 25], [45, 45], [25, 45]]}]});
        s.execute("path.set", json!({"path": hole, "op": "subtract"})).unwrap();
        let before = work(&s);
        let r = s.execute("path.mergeShapeComponents", json!({})).unwrap();
        assert_eq!(r["name"], json!("work"));
        assert_eq!(s.active().unwrap().history.undo_label(), Some("Merge Shape Components"));
        let merged = work(&s);
        assert_eq!(merged.subpaths.len(), 2, "the outline and its hole");
        for (x, y) in [(15, 15), (35, 35), (55, 50), (5, 5)] {
            assert!((covered(&merged, x, y) - covered(&before, x, y)).abs() < 0.01, "({x}, {y})");

Expected assertion context (overlapping windows merged):

assert_eq!(r["name"], json!("work"));
        assert_eq!(s.active().unwrap().history.undo_label(), Some("Merge Shape Components"));
        let merged = work(&s);
        assert_eq!(merged.subpaths.len(), 2, "the outline and its hole");
        for (x, y) in [(15, 15), (35, 35), (55, 50), (5, 5)] {
            assert!((covered(&merged, x, y) - covered(&before, x, y)).abs() < 0.01, "({x}, {y})");
        }
        assert_eq!((covered(&merged, 35, 35), covered(&merged, 15, 15)), (0.0, 1.0), "the hole stays a hole");
        assert!(s.undo());
        assert_eq!(work(&s), before);
        // A saved path and a vector mask merge the same way; an inverted mask stays inverted.
        s.execute("path.set", json!({"name": "Mug", "path": square()})).unwrap();
        s.execute("path.mergeShapeComponents", json!({"name": "Mug"})).unwrap();
        assert!(vm.path.inverted && !vm.path.subpaths.is_empty());
    }

merge_shape_components_on_a_shape_layer_rerenders_it setup/action excerpt:

let mut s = session();
        let id = s.execute("shape.create", json!({"kind": "rect", "rect": [10, 10, 50, 50], "fill": "#ff0000"})).unwrap()["layer"].as_u64().unwrap();
        s.execute("shape.create", json!({"kind": "rect", "rect": [25, 25, 20, 20], "addTo": id, "op": "subtract"})).unwrap();
        s.execute("path.mergeShapeComponents", json!({"name": "layer", "layer": id})).unwrap();
        let st = s.active().unwrap();
        let photocraft_doc::LayerContent::Shape(ShapeLayer { path, cache, .. }) = &st.doc.layer(LayerId(id)).unwrap().content else { panic!("shape") };
        assert_eq!(covered(path, 35, 35), 0.0);
        assert!(cache.is_some());
    }

Expected assertion context (overlapping windows merged):

assert_eq!(covered(path, 35, 35), 0.0);
        assert!(cache.is_some());
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-8074: Transform: remember selected interpolation (#8067)

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: tool options survive a restart; interpolation selection is reused for the next transform. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

tool_options_survive_a_restart setup/action excerpt:

let (mut app, store) = app_with_store();
        let ctx = egui::Context::default();
        tick(&mut app, &ctx);
        app.ui.tool_options.move_auto_select = false;
        app.ui.tool_options.feather = 12.0;
        app.ui.tool_options.transform_interpolation = Some(photocraft_engine::prefs::Interpolation::Nearest);
        crate::tool_memory::persist(&mut app, &ctx);
        tick(&mut app, &ctx);
        // Only what differs from the defaults is stored.
        assert_eq!(stored(&store)["toolOptions"], json!({"move_auto_select": false, "feather": 12.0, "transform_interpolation": "nearestNeighbor"}));
        let saved = store.lock().unwrap().clone().unwrap();
        let (mut app2, _) = app_with_saved(Some(saved));

Expected assertion context (overlapping windows merged):

assert_eq!(stored(&store)["toolOptions"], json!({"move_auto_select": false, "feather": 12.0, "transform_interpolation": "nearestNeighbor"}));
        let saved = store.lock().unwrap().clone().unwrap();
        let (mut app2, _) = app_with_saved(Some(saved));
        tick(&mut app2, &ctx);
        assert!(!app2.ui.tool_options.move_auto_select);
        assert_eq!(app2.ui.tool_options.feather, 12.0);
        assert_eq!(app2.ui.tool_options.transform_interpolation, Some(photocraft_engine::prefs::Interpolation::Nearest));
        assert_eq!(app2.ui.tool_options.tolerance, crate::state::ToolOptions::default().tolerance);
        // A hand-edited file: unreadable fields keep their defaults, the good ones still load.
        for bad in [json!("junk"), json!({"move_auto_select": "no", "feather": 1e300, "tolerance": 5.0, "unknown": 1})] {
            let (mut app3, _) = app_with_saved(Some(json!({"toolOptions": bad}).to_string()));
            assert!(o.move_auto_select && o.feather == 0.0, "{bad}");
            assert_eq!(o.tolerance, if bad.is_object() { 5.0 } else { 32.0 });
        }
    }

interpolation_selection_is_reused_for_the_next_transform setup/action excerpt:

use egui::accesskit::Role;
        use egui_kittest::{Harness, kittest::Queryable};

        let app = app_with_square(64, photocraft_geom::Rect::new(8, 8, 24, 24));
        let mut h = Harness::builder().with_size(vec2(2400.0, 360.0)).build_ui_state(
            |ui, app: &mut PhotocraftApp| {
                if ui.ctx().fonts(|f| f.families().contains(&egui::FontFamily::Name("medium".into()))) {
                    ui.horizontal_centered(|ui| options_bar(app, ui));
                }
            },
            app,
        );

Expected assertion context (overlapping windows merged):

assert!(h.state().ui.transform.is_some(), "transform stays open while the bar renders");

        h.get_by_role(Role::ComboBox).click();
        h.run();
        assert!(h.state().ui.transform.is_some(), "opening interpolation dropdown keeps the transform open");
        h.key_press(egui::Key::ArrowDown);
        h.run();
        assert!(h.state().ui.transform.is_some(), "first interpolation step keeps the transform open");
        h.key_press(egui::Key::ArrowDown);
        h.run();
        assert!(h.state().ui.transform.is_some(), "second interpolation step keeps the transform open");
        assert_eq!(h.state().ui.transform.as_ref().unwrap().interpolation, "nearest");
        assert_eq!(h.state().ui.tool_options.transform_interpolation, Some(photocraft_engine::prefs::Interpolation::Nearest));
        assert_eq!(h.state().session.prefs().general.image_interpolation, photocraft_engine::prefs::Interpolation::BicubicAutomatic);

        let transform = h.state_mut().ui.transform.as_mut().unwrap();
        transform.quad = transform.quad.map(|[x, y]| [x + 1.0, y]);
        assert!(h.state().ui.transform.is_none());
        begin(h.state_mut(), &ctx).unwrap();
        assert_eq!(h.state().ui.transform.as_ref().unwrap().interpolation, "nearest");
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-8896: Select and Mask: Photoshop's full-window workspace (#7069, #8148)

  • Fixture/setup: Selected document/layer-mask fixtures in dialogs/select_mask_ui/tests.rs plus two-document controls; record viewport/theme/scale.

  • Action: run each command separately:

    • cargo test -p photocraft-ui-egui --locked opens_like_photoshop -- --nocapture
    • cargo test -p photocraft-ui-egui --locked opening_needs_a_selection_or_a_layer_mask -- --nocapture
    • cargo test -p photocraft-ui-egui --locked tools_make_edits_and_undo_and_redo_them -- --nocapture
    • cargo test -p photocraft-ui-egui --locked ok_is_one_step_and_matches_the_command -- --nocapture
    • cargo test -p photocraft-ui-egui --locked cancel_changes_nothing -- --nocapture
    • cargo test -p photocraft-ui-egui --locked decontaminate_outputs_to_a_new_layer -- --nocapture
    • cargo test -p photocraft-ui-egui --locked hostile_control_params_are_errors -- --nocapture
    • cargo test -p photocraft-ui-egui --locked ok_after_a_document_switch_changes_neither_document -- --nocapture
    • cargo test -p photocraft-ui-egui --locked ok_after_a_layer_switch_leaves_the_new_layer_alone -- --nocapture
    • cargo test -p photocraft-ui-egui --locked renders_photoshops_layout -- --nocapture
  • Expected: Workspace needs valid selection/mask; tools/preview affect intended target. OK matches engine output in one history step; Cancel changes nothing; decontamination outputs a new layer. Hostile controls refuse; stale document/layer targets edit neither new target.

  • Separate coverage / prerequisites: Native refinement → undo/redo → Cancel and commit are separate NOT RUN subcases. Capture the full workspace with reachable controls, not only an overlay helper.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-9493: fix(vector): a mask feather continues the mask past the canvas edge

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

feather_at_the_canvas_edge_continues_the_mask_past_it setup/action excerpt:

let mut layer = Layer::raster("edge", PixelFormat::RGBA8);
        let mut mask = photocraft_doc::LayerMask::hide_all();
        mask.surface.fill_rect(Rect::new(40, 20, 120, 60), &[1.0]);
        mask.feather = 32.0;
        layer.mask = Some(mask);
        let canvas = Rect::new(0, 0, 160, 80);
        let tight = combined_mask(&layer, canvas).unwrap().read_region(canvas);
        let tall = combined_mask(&layer, Rect::new(0, -200, 160, 280)).unwrap().read_region(canvas);
        let worst = tight.iter().zip(&tall).map(|(a, b)| (a - b).abs()).fold(0.0_f32, f32::max);
        assert!(worst < 1e-4, "the canvas edge changed the feather by {worst}");

        let mut half = photocraft_doc::LayerMask::hide_all();

Expected assertion context (overlapping windows merged):

assert!(worst < 1e-4, "the canvas edge changed the feather by {worst}");

        let mut half = photocraft_doc::LayerMask::hide_all();
        half.surface.fill_rect(Rect::new(0, 0, 80, 80), &[1.0]);
            assert!(row.iter().zip(middle).all(|(a, b)| (a - b).abs() < 1e-5), "row {y} differs from row 40");
        }
        assert!(middle[0] > 0.99, "the left edge stays white: {}", middle[0]);
    }

soft_shape_matches_an_equal_vector_mask setup/action excerpt:

// 40 px reaches past the canvas edge: both feather it the same way there (#9475).
    for (density, feather) in [(1.0, 10.0), (0.45, 0.0), (0.3, 6.5), (0.0, 0.0), (1.0, 0.0), (0.5, 3.0), (1.0, 40.0)] {
        for depth in [SampleType::U8, SampleType::U16] {
            let (s, f) = (shape_doc(depth, density, feather, None), fill_doc(depth, density, feather));
            for y in [0, 45, 50, 100, 151, 199] {
                // An 8-bit shape keeps its rendering in 8-bit pixels (the fill layer's mask is
                // 16-bit), which can round its alpha one level apart, as at any anti-aliased edge.
                let d8 = max_diff(&row(&s, y), &row(&f, y));
                assert!(d8 <= i32::from(depth == SampleType::U8), "{depth:?} density {density} feather {feather}, row {y}: {d8} levels");
                // The compositor keeps masks at 16 bits (an 8-bit shape's pixels are 8-bit).
                if depth == SampleType::U16 {
                    let d16 = max_diff(&row16(&s, y), &row16(&f, y));

Expected assertion context (overlapping windows merged):

assert!(d8 <= i32::from(depth == SampleType::U8), "{depth:?} density {density} feather {feather}, row {y}: {d8} levels");
                // The compositor keeps masks at 16 bits (an 8-bit shape's pixels are 8-bit).
                if depth == SampleType::U16 {
                    let d16 = max_diff(&row16(&s, y), &row16(&f, y));
                    assert!(d16 <= 2, "{depth:?} density {density} feather {feather}, row {y}: {d16} 16-bit levels");
                }
            }
        }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9457: raw: faster tone stage (Shadows, Highlights, Clarity, Texture, Dehaze), same bytes

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

gauss_gives_the_bits_of_the_tap_loop setup/action excerpt:

// Sizes below, around and above the kernel (edges only, a few interior samples, mostly interior).
        for (w, h) in [(1, 1), (5, 3), (13, 40), (64, 7), (65, 70), (131, 97), (300, 41)] {
            let v = noise(w * h, (w * 31 + h) as u64);
            for sigma in [0.04, 0.42, 1.0, 2.1, 4.2, 8.4, 13.44] {
                assert!(same(&gauss(&v, w, h, sigma), &gauss_ref(&v, w, h, sigma)), "{w}x{h} sigma {sigma}");
            }
        }
    }

Expected assertion context (overlapping windows merged):

assert!(same(&gauss(&v, w, h, sigma), &gauss_ref(&v, w, h, sigma)), "{w}x{h} sigma {sigma}");
            }
        }
    }

box_mean_gives_the_bits_of_the_column_loop setup/action excerpt:

for (w, h) in [(1, 1), (5, 3), (13, 40), (64, 7), (65, 70), (131, 97), (300, 41)] {
            let v = noise(w * h, (w * 17 + h) as u64);
            for r in [1, 2, 8, 32, 128] {
                assert!(same(&box_mean(&v, w, h, r), &box_mean_ref(&v, w, h, r)), "{w}x{h} r {r}");
            }
        }
    }

Expected assertion context (overlapping windows merged):

assert!(same(&box_mean(&v, w, h, r), &box_mean_ref(&v, w, h, r)), "{w}x{h} r {r}");
            }
        }
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9478: fix(text): vertical type sets roman letters upright, one per em, as Photoshop does (#9470)

  • Fixture/setup: Roman, ligature and half-width CJK fixtures in text/vertical_tests.rs; record fonts.

  • Action: run each command separately:

    • cargo test -p photocraft-text --locked latin_stands_upright_one_per_em_by_default -- --nocapture
    • cargo test -p photocraft-text --locked upright_roman_letters_stay_apart_without_ligatures -- --nocapture
    • cargo test -p photocraft-text --locked half_width_cjk_stays_rotated_beside_upright_roman -- --nocapture
    • cargo test -p photocraft-text --locked baseline_direction_round_trips -- --nocapture
  • Expected: Roman defaults upright at one-em advances without ligature collapse; half-width CJK retains specified rotation. Run BaselineDirection overrides sheet defaults and survives PSD persistence.

  • Separate coverage / prerequisites: Capture upright/rotated text at 1×/2× and save/reopen PSD separately. Font-unavailable geometry is BLOCKED; the flag round-trip alone is insufficient.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-9523: Fix Type on a Path wrapping and caret positioning across closed-path seams

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

closed_rectangle_left_edge_continues_past_the_top_left_seam setup/action excerpt:

let p = Subpath::polygon(&[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)]);
    let start = arc_length_at(&p, false, 0.0, 50.0).unwrap();
    assert!((start - 350.0).abs() < 1e-6);
    let f = PathFrame::new(&p, start as f32, false).unwrap();
    let m = f.frame(75.0).expect("glyph after the seam remains on the closed path");
    let q = centre(m, 75.0);
    assert!((q.0 - 25.0).abs() < 1e-6 && q.1.abs() < 1e-6, "{q:?}");
    assert_eq!(&m[..4], &[1.0, 0.0, -0.0, 1.0], "text now travels right on the top side");
    assert_eq!(f.apply(75.0, 0.0), q, "caret and glyph use the same wrapped parameter");
}

Expected assertion context (overlapping windows merged):

assert!((start - 350.0).abs() < 1e-6);
    let f = PathFrame::new(&p, start as f32, false).unwrap();
    let m = f.frame(75.0).expect("glyph after the seam remains on the closed path");
    let q = centre(m, 75.0);
    assert!((q.0 - 25.0).abs() < 1e-6 && q.1.abs() < 1e-6, "{q:?}");
    assert_eq!(&m[..4], &[1.0, 0.0, -0.0, 1.0], "text now travels right on the top side");
    assert_eq!(f.apply(75.0, 0.0), q, "caret and glyph use the same wrapped parameter");
}

closed_ellipse_upper_left_and_apex_cross_the_seam_in_both_directions setup/action excerpt:

for reverse in [false, true] {
        let mut p = ellipse();
        if reverse {
            p.knots.reverse();
            for k in &mut p.knots {
                std::mem::swap(&mut k.in_ctrl, &mut k.out_ctrl);
            }
        }
        for flip in [false, true] {
            let whole = PathFrame::new(&p, 0.0, flip).unwrap();
            let length = whole.length();
            for remaining in [0.001, 0.2, 30.0] {

Expected assertion context (overlapping windows merged):

assert!((actual.0 - point.0).hypot(actual.1 - point.1) < 1e-6);
                assert!((m[0] - tangent.0).abs() < 1e-6 && (m[1] - tangent.1).abs() < 1e-6);
                assert!((m[0] * m[3] - m[1] * m[2] - 1.0).abs() < 1e-6, "no reflection or scale jump");
                let before = f.apply(remaining - 0.001, 0.0);
                let after = f.apply(remaining + 0.001, 0.0);
                assert!((before.0 - after.0).hypot(before.1 - after.1) < 0.01, "seam is continuous");
            }
        }
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9594: fix(engine): copied type keeps its size across resolutions after an edit

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: copied type keeps its pixel size across resolutions after an edit. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

copied_type_keeps_its_pixel_size_across_resolutions_after_an_edit setup/action excerpt:

for (from, to) in [(72, 300), (300, 72)] {
        let mut s = Session::new();
        s.execute("file.new", json!({"width": 400, "height": 200, "resolution": from})).unwrap();
        let t = LayerId(s.execute("type.create", json!({"text": "H", "size": 24, "x": 128, "y": 100})).unwrap()["layer"].as_u64().unwrap());
        let original = bounds(&s, 0, t);
        s.execute("file.new", json!({"width": 400, "height": 200, "resolution": to})).unwrap();
        s.set_active(0);
        s.execute("layer.select", json!({"layer": t.0})).unwrap();
        let id = copied(&s.execute("layer.copyToDocument", json!({"document": 1})).unwrap());
        assert_eq!(bounds(&s, 1, id).size(), original.size(), "{from}→{to}: the copy");
        s.execute("type.edit", json!({"layer": id.0, "text": "M"})).unwrap();
        s.execute("type.edit", json!({"layer": id.0, "text": "H"})).unwrap();

Expected assertion context (overlapping windows merged):

assert_eq!(bounds(&s, 1, id).size(), original.size(), "{from}→{to}: the copy");
        s.execute("type.edit", json!({"layer": id.0, "text": "M"})).unwrap();
        s.execute("type.edit", json!({"layer": id.0, "text": "H"})).unwrap();
        let after = bounds(&s, 1, id);
        assert_eq!(after.size(), original.size(), "{from}→{to}: after an edit, {after:?} vs {original:?}");
    }
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9596: fix(io): drop a kept Txt2 once no type layers are left

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: a kept txt2 goes with the last type layer. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

a_kept_txt2_goes_with_the_last_type_layer setup/action excerpt:

let mut doc = Document::new("t", photocraft_geom::Size::new(8, 6), ColorMode::Rgb, SampleType::U8);
        doc.layers.push(Layer::new("Layer 1", LayerContent::Raster(Surface::new(doc.pixel_format()))));
        doc.metadata.psd_global_blocks = vec![(*b"8BIM", *b"Txt2", std::sync::Arc::new(b"<< /0 << /1 (stale) >> >>".to_vec()))];
        let file = document_to_psd(&doc);
        assert!(!file.global_blocks.iter().any(|b| b.key == *b"Txt2"), "no type layers: no Txt2");

        doc.layers.push(Layer::new("T", LayerContent::Text(photocraft_doc::TextLayer::default())));
        let file = document_to_psd(&doc);
        assert!(file.global_blocks.iter().any(|b| b.key == *b"Txt2"), "a type layer keeps its Txt2");
    }

Expected assertion context (overlapping windows merged):

assert!(!file.global_blocks.iter().any(|b| b.key == *b"Txt2"), "no type layers: no Txt2");

        doc.layers.push(Layer::new("T", LayerContent::Text(photocraft_doc::TextLayer::default())));
        let file = document_to_psd(&doc);
        assert!(file.global_blocks.iter().any(|b| b.key == *b"Txt2"), "a type layer keeps its Txt2");
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3461: Replay recorded layer visibility on the intended named layer

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

recorded_eye_visibility_replays_on_another_document_and_duplicate_names setup/action excerpt:

let mut s = Session::new();
        s.execute("file.new", json!({"width": 8, "height": 8})).unwrap();
        s.execute("layer.new.layer", json!({"name": "Same"})).unwrap();
        let hidden = s.execute("layer.new.layer", json!({"name": "Same"})).unwrap()["layer"].as_u64().unwrap();
        s.execute("actions.record", json!({"name": "Hide"})).unwrap();
        s.execute("layer.setProps", json!({"layer": hidden, "visible": false, "coalesce": "eye-sweep"})).unwrap();
        s.execute("actions.stop", json!({})).unwrap();
        s.execute("file.new", json!({"width": 8, "height": 8})).unwrap();
        let keep = s.execute("layer.new.layer", json!({"name": "Same"})).unwrap()["layer"].as_u64().unwrap();
        let target = s.execute("layer.new.layer", json!({"name": "Same"})).unwrap()["layer"].as_u64().unwrap();
        s.execute("select.deselectLayers", json!({})).unwrap();
        let result = s.execute("actions.play", json!({"action": "Hide"})).unwrap();

Expected assertion context (overlapping windows merged):

assert!(result.get("failed").is_none(), "{result}");
        assert!(s.active().unwrap().doc.layer(photocraft_doc::LayerId(keep)).unwrap().visible);
        assert!(!s.active().unwrap().doc.layer(photocraft_doc::LayerId(target)).unwrap().visible);
        s.execute("edit.undo", json!({})).unwrap();
        assert!(s.active().unwrap().doc.layer(photocraft_doc::LayerId(target)).unwrap().visible);
    }

shell_playback_resolves_recorded_eye_target_on_new_document setup/action excerpt:

let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        app.run("file.new", json!({"width": 8, "height": 8})).unwrap();
        app.run("layer.new.layer", json!({"name": "Target"})).unwrap();
        app.run("actions.record", json!({"name": "Hide"})).unwrap();
        let id = app.session.active().unwrap().active_layer.unwrap();
        app.run("layer.setProps", json!({"layer": id.0, "visible": false})).unwrap();
        app.run("actions.stop", json!({})).unwrap();
        app.run("file.new", json!({"width": 8, "height": 8})).unwrap();
        app.run("layer.new.layer", json!({"name": "Target"})).unwrap();
        let id = app.session.active().unwrap().active_layer.unwrap();
        let result = play(&mut app, &json!({"action": "Hide"})).unwrap();
        assert!(result.get("failed").is_none(), "{result}");

Expected assertion context (overlapping windows merged):

assert!(result.get("failed").is_none(), "{result}");
        assert!(!app.session.active().unwrap().doc.layer(id).unwrap().visible);
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3520: fix(ui): restore square color chips in Studio Dark theme

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: studio dark keeps square colour chips. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

studio_dark_keeps_square_colour_chips setup/action excerpt:

assert!(!Tokens::for_kind(ThemeKind::Studio).round_chips);
        // Only the dark variant changes; the light palette retains its layout.
        assert!(Tokens::for_kind(ThemeKind::StudioLight).round_chips);
        assert!(!Tokens::for_kind(ThemeKind::Pro).round_chips);
    }

Expected assertion context (overlapping windows merged):

assert!(!Tokens::for_kind(ThemeKind::Studio).round_chips);
        // Only the dark variant changes; the light palette retains its layout.
        assert!(Tokens::for_kind(ThemeKind::StudioLight).round_chips);
        assert!(!Tokens::for_kind(ThemeKind::Pro).round_chips);
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4035: fix(cmyk): Flatten Image and merges keep the inks of a tagged CMYK document (#3778)

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

flatten_keeps_the_inks_of_a_single_layer_exactly setup/action excerpt:

exact("Flatten Image", |s| {
        s.execute("layer.flattenImage", json!({})).unwrap();
    });
}

Expected assertion context (overlapping windows merged):


stamp_visible_keeps_the_inks_of_a_single_layer_exactly setup/action excerpt:

exact("Stamp Visible", |s| {
        s.execute("layer.stampVisible", json!({})).unwrap();
    });
}

Expected assertion context (overlapping windows merged):


  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4547: fix(icons): PhotoCraft icon corner radius matches other Craft apps (#4478)

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: windows and linux icons keep the round corners. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

windows_and_linux_icons_keep_the_round_corners setup/action excerpt:

// rx=112 of 512 leaves (10, 10) of a 256 px render transparent; the old crop filled it (#4478).
        let png = include_bytes!("../../../assets/app-icon/hicolor/256x256/apps/ai.storyteller.photocraft.png");
        let icon = eframe::icon_data::from_png_bytes(png).unwrap();
        assert_eq!(icon.width, 256);
        assert_eq!(icon.rgba[(10 * 256 + 10) * 4 + 3], 0, "corner is transparent");
        assert_eq!(icon.rgba[(128 * 256 + 2) * 4 + 3], 255, "the tile is full bleed");
    }

Expected assertion context (overlapping windows merged):

assert_eq!(icon.width, 256);
        assert_eq!(icon.rgba[(10 * 256 + 10) * 4 + 3], 0, "corner is transparent");
        assert_eq!(icon.rgba[(128 * 256 + 2) * 4 + 3], 255, "the tile is full bleed");
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4938: Layer Style: one dialog size on every page, and it fits short screens

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

dragging_the_title_bar_moves_the_dialog setup/action excerpt:

use egui_kittest::{Harness, kittest::Queryable};

        let app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        let mut harness = Harness::builder().with_size(egui::vec2(1400.0, 1100.0)).build_ui_state(|ui, app| show(app, ui.ctx()), app);
        PhotocraftApp::setup_context(&harness.ctx, crate::theme::ThemeKind::ALL[0]);
        harness.state_mut().ui.open_dialog(DialogKind::LayerStyle, serde_json::Map::new());
        harness.run_steps(3);
        let before = harness.get_by_label("Layer Style").rect();

        // Grab the title text itself: it must move the dialog, not select the text.
        let from = before.center();
        harness.hover_at(from);

Expected assertion context (overlapping windows merged):

assert!((moved - egui::vec2(-120.0, 80.0)).length() < 1.0, "dialog moved by {moved:?}");
        assert_eq!(harness.state().ui.dialogs.len(), 1, "dragging must not close the dialog");
    }

layer_style_keeps_one_size_on_every_page setup/action excerpt:

let mut h = layer_style_on_bevel(egui::vec2(1400.0, 900.0));
        let size = frame(&h).size();
        let dialog = h.state().ui.dialogs[0].id;
        for page in ["blendingOptions", "styles"] {
            h.state_mut().ui.dialog_mut(dialog).expect("still open").fields.insert("selected".into(), json!(page));
            h.run_steps(3);
            assert_eq!(frame(&h).size(), size, "{page}");
        }
        let grip = egui::Id::new(("dialog", dialog)).with("resize").with("__resize_corner");
        assert!(h.ctx.read_response(grip).is_none(), "no resize grip");
    }

Expected assertion context (overlapping windows merged):

assert_eq!(frame(&h).size(), size, "{page}");
        }
        let grip = egui::Id::new(("dialog", dialog)).with("resize").with("__resize_corner");
        assert!(h.ctx.read_response(grip).is_none(), "no resize grip");
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-5270: Layer Style: Gradient Overlay takes any number of colour stops (#5177)

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

gradient_overlay_takes_explicit_stops setup/action excerpt:

let mut s = session();
        let stops = json!([[0.0, "#ff0000"], [0.5, "#00ff00"], [1.0, "#0000ff"]]);
        s.execute("layer.layerStyle.gradientOverlay", json!({"stops": stops, "opacityStops": [[0, 100], [1, 50]]})).unwrap();
        let Effect::GradientOverlay { gradient, .. } = &effects(&s)[0] else { panic!() };
        assert_eq!(gradient.stops.len(), 3);
        assert_eq!(gradient.stops[1], (0.5, photocraft_color::Color::rgb(0.0, 1.0, 0.0)));
        assert_eq!(gradient.opacity_stops, vec![(0.0, 1.0), (1.0, 0.5)]);
        // Editing the same overlay: new stops replace the list (sorted), unsorted input is sorted,
        // and a geometry-only edit keeps the three stops.
        s.execute("layer.layerStyle.gradientOverlay", json!({"stops": [[1.0, "#ffffff"], [0.25, [0.0, 0.0, 0.0]]], "opacityStops": []})).unwrap();
        let Effect::GradientOverlay { gradient, .. } = &effects(&s)[0] else { panic!() };
        assert_eq!(gradient.stops.iter().map(|s| s.0).collect::<Vec<_>>(), vec![0.25, 1.0]);

Expected assertion context (overlapping windows merged):

assert_eq!(gradient.stops.len(), 3);
        assert_eq!(gradient.stops[1], (0.5, photocraft_color::Color::rgb(0.0, 1.0, 0.0)));
        assert_eq!(gradient.opacity_stops, vec![(0.0, 1.0), (1.0, 0.5)]);
        // Editing the same overlay: new stops replace the list (sorted), unsorted input is sorted,
        // and a geometry-only edit keeps the three stops.
        s.execute("layer.layerStyle.gradientOverlay", json!({"stops": [[1.0, "#ffffff"], [0.25, [0.0, 0.0, 0.0]]], "opacityStops": []})).unwrap();
        assert_eq!(gradient.stops.iter().map(|s| s.0).collect::<Vec<_>>(), vec![0.25, 1.0]);
        assert!(gradient.opacity_stops.is_empty(), "[] means opaque");
        s.execute("layer.layerStyle.gradientOverlay", json!({"stops": stops})).unwrap();
        // The dialog's path: the snapshot plus a geometry edit keeps the three stops.
        let fx = serde_json::to_value(&effects(&s)[0]).unwrap();
        assert_eq!((gradient.stops.len(), gradient.angle), (3, 45.0));
        let e = s.execute("layer.layerStyle.replace", json!({"effects": [{"kind": "gradientOverlay", "params": {"stops": [[0, "#ff0000"]]}}]})).unwrap_err();
        assert!(matches!(e, EngineError::BadParams { .. }), "{e}");
        // A gradient stroke takes stops too.
        s.execute("layer.layerStyle.stroke", json!({"size": 4, "stops": [[0, "#ff0000"], [0.5, "#00ff00"], [1, "#0000ff"]]})).unwrap();
        assert!(effects(&s).iter().any(|e| matches!(e, Effect::Stroke(st) if matches!(&st.paint, FxPaint::Gradient(g) if g.stops.len() == 3))));
        // Malformed lists are errors and change nothing.
        for bad in [
            json!({"stops": "red"}),
            assert!(matches!(e, EngineError::BadParams { .. }), "{bad}: {e}");
        }
        let Effect::GradientOverlay { gradient, .. } = &effects(&s)[0] else { panic!() };
        assert_eq!(gradient.stops.len(), 3, "a refused edit keeps the gradient");
        // Positions outside 0..1 are clamped.
        s.execute("layer.layerStyle.gradientOverlay", json!({"stops": [[-1, "#000000"], [2, "#ffffff"]]})).unwrap();
        let Effect::GradientOverlay { gradient, .. } = &effects(&s)[0] else { panic!() };
        assert_eq!(gradient.stops.iter().map(|s| s.0).collect::<Vec<_>>(), vec![0.0, 1.0]);
    }

layer_style_picker_control_binds_previews_confirms_and_cancels setup/action excerpt:

let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        let ctx = egui::Context::default();
        app.run("file.new", json!({"width":16,"height":16})).unwrap();
        let parent = call(&mut app, &ctx, "ui.dialog.open", json!({"kind":"layerStyle","effect":"gradientOverlay"}))["result"]["dialog"].as_u64();
        let parent = parent.unwrap();
        let before = app.ui.dialogs[0].fields.clone();
        // The Gradient Overlay's colours are its stops (#5177): `stop` picks one.
        for stop in [0, 1] {
            let reply = call(&mut app, &ctx, "ui.dialog.open", json!({"kind":"colorPicker","parent":parent,"effect":"fx1","field":"stops","stop":stop}));
            assert_eq!(reply["ok"], true, "{reply}");
            let picker = app.ui.dialogs.last().unwrap().id;
            let inspected = call(&mut app, &ctx, "ui.inspect", json!({}));

Expected assertion context (overlapping windows merged):

assert_eq!(reply["ok"], true, "{reply}");
            let picker = app.ui.dialogs.last().unwrap().id;
            let inspected = call(&mut app, &ctx, "ui.inspect", json!({}));
            assert!(inspected.to_string().contains("__layerStyleColor"));
            assert_eq!(call(&mut app, &ctx, "ui.dialog.confirm", json!({"dialog":parent}))["ok"], false);
            assert_eq!(call(&mut app, &ctx, "ui.dialog.set", json!({"dialog":picker,"field":"color","value":"#123456"}))["ok"], true);
            assert_eq!(call(&mut app, &ctx, "ui.dialog.confirm", json!({"dialog":picker}))["ok"], true);
            assert_eq!(app.ui.dialogs[0].fields["effects"][0]["params"]["stops"][stop][1], "#123456");
        }
        assert_ne!(app.ui.dialogs[0].fields, before);
        let bad = call(&mut app, &ctx, "ui.dialog.open", json!({"kind":"colorPicker","parent":parent,"effect":"fx1","field":"stops","stop":"x"}));
        assert_eq!(bad["ok"], false, "{bad}");
        let bad = call(&mut app, &ctx, "ui.dialog.open", json!({"kind":"colorPicker","parent":parent,"effect":"fx1","field":"stops","stop":9}));
        assert_eq!(bad["ok"], false, "{bad}");
        call(&mut app, &ctx, "ui.dialog.open", json!({"kind":"colorPicker","parent":parent,"effect":"fx1","field":"stops","stop":1}));
        assert_eq!(call(&mut app, &ctx, "ui.dialog.cancel", json!({"dialog":parent}))["ok"], true);
        assert!(app.ui.dialogs.is_empty());
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-5744: fix(ui): keep detached documents in one interactive window

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

torn_off_document_is_not_drawn_again_in_the_main_window setup/action excerpt:

for kind in [crate::theme::ThemeKind::Pro, crate::theme::ThemeKind::Studio] {
            let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
            app.run("file.new", json!({"width": 80, "height": 60, "name": "floating"})).unwrap();
            app.run("file.new", json!({"width": 80, "height": 60, "name": "docked"})).unwrap();
            app.sync_views();
            let ctx = egui::Context::default();
            crate::menus::invoke(&mut app, &ctx, "window.arrange.floatInWindow", json!({"document": 0})).unwrap();
            assert!(app.ui.windows[0].floating);
            assert_eq!(app.session.active_index(), Some(0), "{kind:?}: the floated page remains active");
            assert_eq!(main_canvas_document(&app), Some(1), "{kind:?}: the main canvas picks a docked page instead");
            let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(680.0, 480.0)).build_ui_state(
                |ui, (app, ready): &mut (PhotocraftApp, bool)| {

Expected assertion context (overlapping windows merged):

assert!(app.ui.windows[0].floating);
            assert_eq!(app.session.active_index(), Some(0), "{kind:?}: the floated page remains active");
            assert_eq!(main_canvas_document(&app), Some(1), "{kind:?}: the main canvas picks a docked page instead");
            let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(680.0, 480.0)).build_ui_state(
                |ui, (app, ready): &mut (PhotocraftApp, bool)| {
                    if *ready {
            assert_eq!(tabs, Some(vec![1]), "{kind:?}: only the docked document stays on the main tab strip");
        }
    }

tab_reorder_keeps_floating_documents_in_place setup/action excerpt:

let visible = [0, 2];
        assert_eq!(move_target_after_filter(&visible, 0, 1, 3), 2);
        assert_eq!(move_target_after_filter(&visible, 2, 0, 3), 0);
    }

Expected assertion context (overlapping windows merged):

assert_eq!(move_target_after_filter(&visible, 0, 1, 3), 2);
        assert_eq!(move_target_after_filter(&visible, 2, 0, 3), 0);
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-6387: Video Frames to Layers: every page of a multi-page TIFF (part of #6379)

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

video_frames_to_layers_takes_every_tiff_page setup/action excerpt:

let dir = tmpdir("tiff-pages");
    let path = format!("{dir}/pages.tif");
    write_tiff(&path, &[(2, 2, 9, 1), (6, 4, 51, 0), (3, 5, 204, 0)]);
    let mut s = session();
    let r = s.execute("file.import.videoFramesToLayers", json!({"path": path})).unwrap();
    assert_eq!(r["layers"], 2, "two pages, the thumbnail left out");
    let doc = &s.active().unwrap().doc;
    for (name, (w, h), level) in [("Frame 1", (6, 4), 51.0), ("Frame 2", (3, 5), 204.0)] {
        let l = doc.layers.iter().find(|l| l.name == name).unwrap_or_else(|| panic!("{name}"));
        let surf = l.surface().unwrap();
        let b = surf.content_bounds();
        assert_eq!((b.width(), b.height()), (w, h), "{name}");

Expected assertion context (overlapping windows merged):

assert_eq!(r["layers"], 2, "two pages, the thumbnail left out");
    let doc = &s.active().unwrap().doc;
    for (name, (w, h), level) in [("Frame 1", (6, 4), 51.0), ("Frame 2", (3, 5), 204.0)] {
        let l = doc.layers.iter().find(|l| l.name == name).unwrap_or_else(|| panic!("{name}"));
        assert_eq!((b.width(), b.height()), (w, h), "{name}");
        let px = surf.read_region(photocraft_geom::Rect::new(b.x0, b.y0, b.x0 + 1, b.y0 + 1));
        assert!((px[0] * 255.0 - level).abs() < 0.6 && px[3] == 1.0, "{name}: {px:?}");
    }
    let single = format!("{dir}/single.tif");
    write_tiff(&single, &[(4, 4, 120, 0)]);
    assert_eq!(s.execute("file.import.videoFramesToLayers", json!({"path": single})).unwrap()["layers"], 1);
}

video_frames_to_layers_refuses_a_tiff_with_an_incomplete_page_list setup/action excerpt:

let dir = tmpdir("tiff-loop");
    let path = format!("{dir}/loop.tif");
    write_tiff(&path, &[(2, 2, 51, 0), (2, 2, 204, 0)]);
    // The last directory's next-IFD offset points back to the first directory.
    let mut f = std::fs::read(&path).unwrap();
    let first: [u8; 4] = f[4..8].try_into().unwrap();
    let end = f.len();
    f[end - 4..].copy_from_slice(&first);
    std::fs::write(&path, f).unwrap();
    let mut s = session();
    let before = s.active().unwrap().doc.layers.len();
    let err = s.execute("file.import.videoFramesToLayers", json!({"path": path})).unwrap_err().to_string();

Expected assertion context (overlapping windows merged):

assert!(err.contains("only the first 2 pages"), "{err}");
    assert_eq!(s.active().unwrap().doc.layers.len(), before, "nothing imported");
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-6449: Hand tool: Cmd held zooms in and Alt zooms out, as in Photoshop

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

cmd_is_not_the_move_tool_for_selection_hand_zoom_pen_shape_or_type_tools setup/action excerpt:

for tool in [
        Tool::Hand,
        Tool::RotateView,
        Tool::Zoom,
        Tool::Pen,
        Tool::Rectangle,
        Tool::Type,
        Tool::VerticalType,
        Tool::Crop,
        Tool::PathSelection,
        Tool::DirectSelection,
        Tool::Move,

Expected assertion context (overlapping windows merged):

assert!(!cmd_moves(tool), "{tool:?}");
        let mut h = harness(tool);
        h.event(Event::ModifiersChanged(platform(Modifiers::COMMAND)));
        h.run_steps(1);
        assert_eq!(held_tool(h.state(), &h.ctx), (tool == Tool::Hand).then_some(Temporary::ZoomIn), "{tool:?}");
    }
    for tool in [Tool::Brush, Tool::Eraser, Tool::Gradient, Tool::CloneStamp, Tool::Eyedropper, Tool::MagneticLasso, Tool::QuickSelection] {
        assert!(cmd_moves(tool), "{tool:?}");
    }
}

cmd_and_alt_with_the_hand_tool_zoom_in_and_out_while_held setup/action excerpt:

let mut h = harness(Tool::Hand);
    let z0 = h.state().ui.views[0].zoom;
    hold_mods(&mut h, Modifiers::COMMAND);
    assert_eq!(held_tool(h.state(), &h.ctx), Some(Temporary::ZoomIn));
    click(&mut h, 200.0, 150.0, Modifiers::COMMAND);
    let z1 = h.state().ui.views[0].zoom;
    assert!(z1 > z0, "{z0} -> {z1}");
    hold_mods(&mut h, Modifiers::ALT);
    assert_eq!(held_tool(h.state(), &h.ctx), Some(Temporary::ZoomOut));
    click(&mut h, 200.0, 150.0, Modifiers::ALT);
    let z2 = h.state().ui.views[0].zoom;
    assert!(z2 < z1, "{z1} -> {z2}");

Expected assertion context (overlapping windows merged):

assert_eq!(held_tool(h.state(), &h.ctx), Some(Temporary::ZoomIn));
    click(&mut h, 200.0, 150.0, Modifiers::COMMAND);
    let z1 = h.state().ui.views[0].zoom;
    assert!(z1 > z0, "{z0} -> {z1}");
    hold_mods(&mut h, Modifiers::ALT);
    assert_eq!(held_tool(h.state(), &h.ctx), Some(Temporary::ZoomOut));
    click(&mut h, 200.0, 150.0, Modifiers::ALT);
    let z2 = h.state().ui.views[0].zoom;
    assert!(z2 < z1, "{z1} -> {z2}");
    hold_mods(&mut h, Modifiers::COMMAND | Modifiers::ALT);
    assert_eq!(held_tool(h.state(), &h.ctx), Some(Temporary::ZoomOut), "⌥ turns ⌘'s Zoom In around");
    hold_mods(&mut h, Modifiers::NONE);
    assert_eq!(held_tool(h.state(), &h.ctx), None);
    // Released: a drag pans again.
    let (center, zoom) = (h.state().ui.views[0].center, h.state().ui.views[0].zoom);
    drag_with(&mut h, Modifiers::NONE, 200.0, 150.0, 260.0, 190.0);
    assert_ne!(h.state().ui.views[0].center, center, "the Hand pans");
    assert_eq!(h.state().ui.views[0].zoom, zoom);
    assert_eq!(h.state().ui.tool, Tool::Hand);
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-7255: fix(engine): guard trim and duplicate merged against documents exceeding render limits (#7223)

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: trim and duplicate merged on too large document fail gracefully without panic. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

trim_and_duplicate_merged_on_too_large_document_fail_gracefully_without_panic setup/action excerpt:

let mut s = Session::new();
        s.execute("file.new", json!({"width": 40000, "height": 30000, "background": "transparent"})).unwrap();
        let initial_layers = doc(&s).layers.len();
        let initial_bounds = doc(&s).bounds();
        let initial_rev = s.active().unwrap().revision;
        let initial_docs = s.documents().len();

        let trim_res = s.execute("image.trim", json!({"basedOn": "topLeft"}));
        assert!(trim_res.is_err(), "trim on too-large document must return error");
        let err = trim_res.unwrap_err().to_string();
        assert!(err.contains("too large to trim"), "error should explain document is too large: {err}");
        assert_eq!(doc(&s).layers.len(), initial_layers, "layer count unchanged after failed trim");

Expected assertion context (overlapping windows merged):

assert!(trim_res.is_err(), "trim on too-large document must return error");
        let err = trim_res.unwrap_err().to_string();
        assert!(err.contains("too large to trim"), "error should explain document is too large: {err}");
        assert_eq!(doc(&s).layers.len(), initial_layers, "layer count unchanged after failed trim");
        assert_eq!(doc(&s).bounds(), initial_bounds, "canvas size unchanged after failed trim");
        assert_eq!(s.active().unwrap().revision, initial_rev, "revision unchanged after failed trim");

        let dup_res = s.execute("image.duplicate", json!({"mergedOnly": true}));
        assert!(dup_res.is_err(), "duplicate merged on too-large document must return error");
        let dup_err = dup_res.unwrap_err().to_string();
        assert!(dup_err.contains("too large to duplicate merged"), "error should explain document is too large: {dup_err}");
        assert_eq!(s.documents().len(), initial_docs, "document count unchanged after failed duplicate");
        assert_eq!(doc(&s).layers.len(), initial_layers, "layer count unchanged after failed duplicate");
        assert_eq!(doc(&s).bounds(), initial_bounds, "canvas size unchanged after failed duplicate");
        assert_eq!(s.active().unwrap().revision, initial_rev, "revision unchanged after failed duplicate");
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-8931: feat(algo): describe the Gaussian blur as data for other devices

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

the_description_is_the_cpus_blur setup/action excerpt:

for radius in RADII {
            let params = FilterParams::GaussianBlur { radius };
            let blur = Separable::of(&params).unwrap();
            match &blur {
                Separable::Kernel(k) => {
                    assert!(radius <= 4.0, "radius {radius}");
                    assert_eq!(k, &blur::gaussian_kernel(radius));
                    assert_eq!(k.len() % 2, 1);
                    assert!((k.iter().sum::<f32>() - 1.0).abs() < 1e-5);
                }
                Separable::Boxes(b) => {
                    assert!(radius > 4.0, "radius {radius}");

Expected assertion context (overlapping windows merged):

assert!(radius <= 4.0, "radius {radius}");
                    assert_eq!(k, &blur::gaussian_kernel(radius));
                    assert_eq!(k.len() % 2, 1);
                    assert!((k.iter().sum::<f32>() - 1.0).abs() < 1e-5);
                }
                Separable::Boxes(b) => {
                    assert!(radius > 4.0, "radius {radius}");
                    assert_eq!(Some(b.clone()), blur::box_widths(&params));
                    assert!(b.iter().all(|w| w % 2 == 1));
                }
            }
            let Halo::Radius(halo) = params.halo() else { panic!("a Gaussian has a radius halo") };
            assert!(blur.reach() <= halo, "radius {radius}: reach {} past the halo {halo}", blur.reach());
        }
        assert_eq!(Separable::of(&FilterParams::BoxBlur { radius: 9.0 }), None, "not a Gaussian (yet)");
        assert_eq!(Separable::Boxes(vec![usize::MAX, 3]).reach(), i32::MAX, "saturates");
    }

an_impulse_spreads_no_further_than_the_reach setup/action excerpt:

for radius in RADII {
            let params = FilterParams::GaussianBlur { radius };
            let reach = Separable::of(&params).unwrap().reach();
            let fmt = PixelFormat::new(ColorMode::Rgb, SampleType::F32, true);
            let size = 2 * reach + 41;
            let canvas = Rect::new(0, 0, size, size);
            let mut s = Surface::new(fmt);
            let dot = Rect::new(size / 2, size / 2, size / 2 + 1, size / 2 + 1);
            s.fill_rect(dot, &[1.0, 1.0, 1.0, 1.0]);
            let out = crate::apply_in_with(&s, &params, canvas, canvas, None, canvas, &Interrupt::NONE).unwrap();
            let mut px = Vec::new();
            out.read_region_into(canvas, &mut px);

Expected assertion context (overlapping windows merged):

assert!(dot.inflate(reach).contains_rect(&touched), "radius {radius}: {touched:?} past the reach {reach}");
            if matches!(Separable::of(&params), Some(Separable::Kernel(_))) {
                assert_eq!(touched, dot.inflate(reach), "radius {radius}");
            }
        }
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9004: fix(ui): enlarge Move tool alignment icons for legibility

  • Fixture/setup: Studio Dark and Studio Light at UI scales 100% and 200%, 1280x800 and 900x600 logical-point windows; synthetic document with two selected layers and Move tool. Capture disabled state with fewer selected layers too.
  • Action: Capture the six Move Align controls and Align and distribute ellipsis at each theme/scale/window combination. Measure icon artwork/hit rectangles from the captured scale and invoke Left Edges then Undo.
  • Expected: Artwork is 20 logical points within 28-point hit targets (40/56 physical pixels at 2x), without clipping or overlap. Enabled-state and command dispatch are unchanged; Left Edges aligns the selected layers and Undo restores their positions.
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester:

PR-9015: fix(xtask): reject leading zeroes in numeric prerelease identifiers

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: rejects numeric prerelease identifiers with leading zeroes. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

rejects_numeric_prerelease_identifiers_with_leading_zeroes setup/action excerpt:

for version in ["1.0.0-01", "1.0.0-rc.01", "1.0.0-00.1"] {
            assert!(validate(version).is_err(), "validator accepted invalid version {version}");
            assert!(replace(MANIFEST, version).is_err(), "accepted invalid version {version}");
        }
        for version in ["1.0.0-0", "1.0.0-rc.0", "1.0.0-01a", "1.0.0-01-2"] {
            let updated = replace(MANIFEST, version).expect("valid prerelease");
            assert_eq!(read(&updated).unwrap(), version);
        }
    }

Expected assertion context (overlapping windows merged):

assert!(validate(version).is_err(), "validator accepted invalid version {version}");
            assert!(replace(MANIFEST, version).is_err(), "accepted invalid version {version}");
        }
        for version in ["1.0.0-0", "1.0.0-rc.0", "1.0.0-01a", "1.0.0-01-2"] {
            let updated = replace(MANIFEST, version).expect("valid prerelease");
            assert_eq!(read(&updated).unwrap(), version);
        }
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9354: fix(ui): Full Screen Mode needs a document (#9349)

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

screen_mode_button_is_inert_without_a_document setup/action excerpt:

let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        app.run("file.new", serde_json::json!({"width": 64, "height": 48})).unwrap();
        let ctx = egui::Context::default();
        PhotocraftApp::setup_context(&ctx, crate::theme::ThemeKind::ALL[0]);
        frame(&mut app, &ctx, 0.0, vec![]);
        frame(&mut app, &ctx, 0.1, vec![]);
        let bx = if Tokens::get(&ctx).pro { 30.0 } else { 36.0 };
        // Found while a document makes it clickable: the toolbar's last button.
        let at = ctx
            .viewport(|v| {
                v.prev_pass
                    .widgets

Expected assertion context (overlapping windows merged):

assert_eq!(crate::view_cmds::is_enabled(&app, "view.screenMode.cycle"), Some(false));
        let pointer = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
        let mut sent = Vec::new();
        for (t, events) in [(2.0, vec![egui::Event::PointerMoved(at), pointer(true)]), (2.1, vec![pointer(false)]), (2.15, vec![])] {
        assert!(!sent.iter().any(|c| matches!(c, egui::ViewportCommand::Fullscreen(_))), "no screen mode change was sent: {sent:?}");
        assert_eq!(app.ui.view.screen_mode, "standard");
    }

long_press_screen_mode_opens_the_modes_without_cycling setup/action excerpt:

let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        // A document open: Full Screen Mode needs one (#9349), so without it a click can't cycle.
        app.run("file.new", serde_json::json!({"width": 64, "height": 48})).unwrap();
        let ctx = egui::Context::default();
        PhotocraftApp::setup_context(&ctx, crate::theme::ThemeKind::ALL[0]);
        frame(&mut app, &ctx, 0.0, vec![]);
        frame(&mut app, &ctx, 0.1, vec![]);
        let bx = if Tokens::get(&ctx).pro { 30.0 } else { 36.0 };
        // The toolbar's last button: Change Screen Mode, after Quick Mask.
        let at = ctx
            .viewport(|v| {
                v.prev_pass

Expected assertion context (overlapping windows merged):

assert!(standard(&app));
        // Hold, then release: the list opens and stays open, and the mode is unchanged.
        frame(&mut app, &ctx, 1.0, vec![egui::Event::PointerMoved(at), pointer(at, true)]);
        frame(&mut app, &ctx, 1.2, vec![]);
        assert!(!menu_open(&ctx), "not before the hold time");
        frame(&mut app, &ctx, 1.4, vec![]);
        assert!(menu_open(&ctx), "open while still held");
        frame(&mut app, &ctx, 1.5, vec![pointer(at, false)]);
        frame(&mut app, &ctx, 1.55, vec![]);
        assert!(menu_open(&ctx), "the release keeps it open");
        assert!(standard(&app), "the release doesn't cycle the mode");
        // Escape closes it; a plain click cycles the mode and opens nothing.
        frame(
            &mut app,
        assert!(!menu_open(&ctx));
        frame(&mut app, &ctx, 3.0, vec![egui::Event::PointerMoved(at), pointer(at, true)]);
        frame(&mut app, &ctx, 3.1, vec![pointer(at, false)]);
        frame(&mut app, &ctx, 3.15, vec![]);
        assert!(!standard(&app), "a click cycles");
        assert!(!menu_open(&ctx));
        // Right-click opens it too.
        let secondary = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Secondary, pressed, modifiers: Default::default() };
        frame(&mut app, &ctx, 4.0, vec![secondary(true)]);
        assert!(menu_open(&ctx));
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9418: Color Lookup: honour the .3dl Mesh header's cube edge and output depth (#9078, #9073)

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

three_dl_without_a_header_reads_low_nodes_as_12_bit setup/action excerpt:

let table = |peak: u32| -> String {
            let mut text = String::from("0 1023\n");
            for i in 0..8 {
                let on = |bit: u32| if i & bit != 0 { peak } else { 0 };
                text.push_str(&format!("{} {} {}\n", on(4), on(2), on(1)));
            }
            text
        };
        for peak in [255, 1023, 4095] {
            let white = parse_3dl(&table(peak)).unwrap().data[21..24].to_vec();
            assert_eq!(white, [peak as f32 / 4095.0; 3], "peak {peak}");
        }

Expected assertion context (overlapping windows merged):

assert_eq!(white, [peak as f32 / 4095.0; 3], "peak {peak}");
        }
        // A header keeps the inferred 10-bit depth. The bare `3DMESH` magic is a header without a
        // cube declaration; `Mesh 1 10` would declare a 3³ cube and refuse this 2³ table (#9078).
        assert_eq!(white, [1.0; 3]);
    }

three_dl_mesh_three_point_shaper_is_not_a_node setup/action excerpt:

// #9078: Photoshop loads a 3³ cube whose shaper line has three values; counting it as a
        // 28th node rejected the whole file.
        let l = parse_3dl(&mesh_3dl(1, 12, 3, 4095.0, true)).unwrap();
        assert_eq!(l.size, 3);
        assert_eq!(l.data.len(), 3 * 3 * 3 * 3);
        // The inverse identity: node (0,0,0) is full, node (2,2,2) black.
        assert!((l.data[0] - 1.0).abs() < 1e-6 && (l.data[1] - 1.0).abs() < 1e-6 && (l.data[2] - 1.0).abs() < 1e-6);
        let last = &l.data[l.data.len() - 3..];
        assert_eq!(last, [0.0, 0.0, 0.0]);
        // The same 27 nodes without the shaper row still load (the node-only control).
        assert_eq!(parse_3dl(&mesh_3dl(1, 12, 3, 4095.0, false)).unwrap().data, l.data);
    }

Expected assertion context (overlapping windows merged):

assert_eq!(l.size, 3);
        assert_eq!(l.data.len(), 3 * 3 * 3 * 3);
        // The inverse identity: node (0,0,0) is full, node (2,2,2) black.
        assert!((l.data[0] - 1.0).abs() < 1e-6 && (l.data[1] - 1.0).abs() < 1e-6 && (l.data[2] - 1.0).abs() < 1e-6);
        let last = &l.data[l.data.len() - 3..];
        assert_eq!(last, [0.0, 0.0, 0.0]);
        // The same 27 nodes without the shaper row still load (the node-only control).
        assert_eq!(parse_3dl(&mesh_3dl(1, 12, 3, 4095.0, false)).unwrap().data, l.data);
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9481: fix(ui): new layer effects start from the Make Default values

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: new effects start from the made default. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

new_effects_start_from_the_made_default setup/action excerpt:

let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        app.run("file.new", json!({"width": 32, "height": 32})).unwrap();
        app.run("layer.new.layer", json!({})).unwrap();
        app.run("layer.layerStyle.makeDefault", json!({"kind": "stroke", "params": {"size": 7, "position": "outside"}})).unwrap();

        let id = open(&mut app, Some("stroke")).unwrap();
        let mut f = app.ui.dialogs.iter().find(|d| d.id == id).unwrap().fields.clone();
        add_instance(&mut f, "stroke");
        add_instance(&mut f, "dropShadow");
        let fx = effects_of(&f);
        let strokes: Vec<_> = fx.iter().filter(|e| e["kind"] == json!("stroke")).collect();
        assert_eq!(strokes.len(), 2);

Expected assertion context (overlapping windows merged):

assert_eq!(strokes.len(), 2);
        for e in strokes {
            assert_eq!((&e["params"]["size"], &e["params"]["position"]), (&json!(7), &json!("outside")));
            assert_eq!(e["params"]["opacity"], defaults("stroke")["opacity"], "unsaved keys keep the factory value");
        }
        let shadow = fx.iter().find(|e| e["kind"] == json!("dropShadow")).unwrap();
        assert_eq!(shadow["params"]["size"], defaults("dropShadow")["size"]);
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9504: fix(ui): Keyboard Shortcuts records ⌘Z instead of undoing the capture

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: cmd z is recorded while capturing and undoes otherwise. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

cmd_z_is_recorded_while_capturing_and_undoes_otherwise setup/action excerpt:

let (mut h, id) = dialog(1.0, "edit.search", "search…");
    capture(&mut h, &crate::shortcuts::pretty("Cmd+F"), Key::ControlLeft, Modifiers::COMMAND, Key::Z);
    assert_eq!(field(&h, id, "overrides")["edit.search"], json!("Cmd+Z"));
    assert_eq!(field(&h, id, "capture"), json!(false));
    key(&mut h, Key::Z, true, Modifiers::COMMAND);
    h.run_steps(2);
    assert_ne!(field(&h, id, "overrides")["edit.search"], json!("Cmd+Z"), "⌘Z outside a capture undoes the change");
    assert_eq!(field(&h, id, "capture"), json!(false), "and doesn't wait for keys again");
    // CodeRabbit on #9504: clearing the restored capture isn't a new edit, so Redo still works.
    h.run_steps(2);
    key(&mut h, Key::Z, true, Modifiers::COMMAND | Modifiers::SHIFT);
    h.run_steps(2);

Expected assertion context (overlapping windows merged):

assert_eq!(field(&h, id, "overrides")["edit.search"], json!("Cmd+Z"));
    assert_eq!(field(&h, id, "capture"), json!(false));
    key(&mut h, Key::Z, true, Modifiers::COMMAND);
    h.run_steps(2);
    assert_ne!(field(&h, id, "overrides")["edit.search"], json!("Cmd+Z"), "⌘Z outside a capture undoes the change");
    assert_eq!(field(&h, id, "capture"), json!(false), "and doesn't wait for keys again");
    // CodeRabbit on #9504: clearing the restored capture isn't a new edit, so Redo still works.
    h.run_steps(2);
    key(&mut h, Key::Z, true, Modifiers::COMMAND | Modifiers::SHIFT);
    assert_eq!(field(&h, id, "overrides")["edit.search"], json!("Cmd+Z"), "⇧⌘Z redoes the recorded shortcut");
    assert_eq!(field(&h, id, "capture"), json!(false));
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9517: fix(ui): Layer Style scrolls on a short window so OK stays on screen

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: layer style buttons stay on a short window. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

layer_style_buttons_stay_on_a_short_window setup/action excerpt:

use egui_kittest::{Harness, kittest::Queryable};

        let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        app.run("file.new", serde_json::json!({"width": 64, "height": 64})).unwrap();
        app.run("layer.new.layer", serde_json::json!({})).unwrap();
        let mut h = Harness::builder().with_size(egui::vec2(1435.0, 858.0)).build_ui_state(|ui, app| show(app, ui.ctx()), app);
        PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::ALL[0]);
        crate::layer_style::open(h.state_mut(), Some("bevelEmboss")).unwrap();
        h.run_steps(4);
        for label in ["OK", "Cancel"] {
            let r = h.get_by_label(label).rect();
            assert!(r.bottom() <= 858.0, "{label} is off the window: {r:?}");

Expected assertion context (overlapping windows merged):

assert!(r.bottom() <= 858.0, "{label} is off the window: {r:?}");
        }
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9597: fix(ui): the layer list doesn't move with no layer selected

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: layer rows do not move when no layer is selected. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

layer_rows_do_not_move_when_no_layer_is_selected setup/action excerpt:

for theme in [crate::theme::ThemeKind::Studio, crate::theme::ThemeKind::Classic, crate::theme::ThemeKind::ProMedium] {
        let mut h = harness();
        PhotocraftApp::setup_context(&h.ctx, theme);
        h.run_steps(4);
        let row_top = |h: &Harness<'_, PhotocraftApp>| by_role(h, "Paint", Role::Button).map(|r| r.top());
        let selected = row_top(&h);
        h.state_mut().session.execute("select.deselectLayers", json!({})).unwrap();
        h.run_steps(4);
        let none = row_top(&h);
        assert!(selected.is_some(), "{theme:?}: the layer row");
        assert_eq!(none, selected, "{theme:?}: the row moved with no layer selected");
    }

Expected assertion context (overlapping windows merged):

assert!(selected.is_some(), "{theme:?}: the layer row");
        assert_eq!(none, selected, "{theme:?}: the row moved with no layer selected");
    }
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9598: fix(ui): Liquify's Twirl Counterclockwise gets a mirrored twirl icon

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: twirl counterclockwise has its own mirrored icon; alt reversed twirl survives replay undo and redo. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

twirl_counterclockwise_has_its_own_mirrored_icon setup/action excerpt:

assert_eq!(tool_icon(LiquifyTool::TwirlCcw), "rotate-ccw");
        let svg = |name: &str| crate::icon_data::ICONS.iter().find(|(n, _)| *n == name).map(|(_, b)| String::from_utf8_lossy(b).into_owned());
        let (cw, ccw) = (svg("rotate-cw").unwrap(), svg("rotate-ccw").expect("the rotate-ccw icon"));
        assert!(ccw.contains("M3 12a9 9 0 1 0 9-9") && cw.contains("M21 12a9 9 0 1 1-9-9"), "mirror images");
        for t in [LiquifyTool::ForwardWarp, LiquifyTool::TwirlCw, LiquifyTool::TwirlCcw, LiquifyTool::Pucker, LiquifyTool::Bloat] {
            assert!(svg(tool_icon(t)).is_some(), "{t:?}: {}", tool_icon(t));
        }
    }

Expected assertion context (overlapping windows merged):

assert_eq!(tool_icon(LiquifyTool::TwirlCcw), "rotate-ccw");
        let svg = |name: &str| crate::icon_data::ICONS.iter().find(|(n, _)| *n == name).map(|(_, b)| String::from_utf8_lossy(b).into_owned());
        let (cw, ccw) = (svg("rotate-cw").unwrap(), svg("rotate-ccw").expect("the rotate-ccw icon"));
        assert!(ccw.contains("M3 12a9 9 0 1 0 9-9") && cw.contains("M21 12a9 9 0 1 1-9-9"), "mirror images");
        for t in [LiquifyTool::ForwardWarp, LiquifyTool::TwirlCw, LiquifyTool::TwirlCcw, LiquifyTool::Pucker, LiquifyTool::Bloat] {
            assert!(svg(tool_icon(t)).is_some(), "{t:?}: {}", tool_icon(t));
        }
    }

alt_reversed_twirl_survives_replay_undo_and_redo setup/action excerpt:

let ctx = egui::Context::default();
        let mut app = app_with_layer();
        open(&mut app, &ctx).unwrap();
        control(&mut app, &json!({"tool": "twirlCw", "size": 36, "rate": 80})).unwrap();

        // Releasing Alt while dragging must not change the tool recorded at pointer-down.
        pointer(&mut app, ToolEvent::Down { x: 53.0, y: 40.0, pressure: 1.0 }, egui::Modifiers::ALT);
        pointer(&mut app, ToolEvent::Move { x: 58.0, y: 41.0, pressure: 1.0 }, egui::Modifiers::NONE);
        pointer(&mut app, ToolEvent::Up { x: 58.0, y: 41.0 }, egui::Modifiers::NONE);
        let d = app.distort.liquify.as_ref().unwrap();
        assert_eq!(d.opts.tool, LiquifyTool::TwirlCw, "Alt must not change the selected tool");
        assert_eq!(d.strokes.len(), 1);

Expected assertion context (overlapping windows merged):

assert_eq!(d.opts.tool, LiquifyTool::TwirlCw, "Alt must not change the selected tool");
        assert_eq!(d.strokes.len(), 1);
        assert_eq!(d.strokes[0].tool, LiquifyTool::TwirlCcw);
        assert_eq!(LiquifyField::from_strokes(d.canvas, d.cell, &d.strokes), d.field);

        // A following stroke without Alt records the original tool.
        pointer(&mut app, ToolEvent::Down { x: 70.0, y: 40.0, pressure: 1.0 }, egui::Modifiers::NONE);
        assert_eq!(d.strokes.len(), 2);
        assert_eq!(d.strokes[1].tool, LiquifyTool::TwirlCw);
        assert_eq!(LiquifyField::from_strokes(d.canvas, d.cell, &d.strokes), d.field);

        control(&mut app, &json!({"undo": true})).unwrap();
        let d = app.distort.liquify.as_ref().unwrap();
        assert_eq!(d.strokes.len(), 1);
        assert_eq!(d.strokes[0].tool, LiquifyTool::TwirlCcw);
        assert_eq!(d.redo[0].tool, LiquifyTool::TwirlCw);
        assert_eq!(LiquifyField::from_strokes(d.canvas, d.cell, &d.strokes), d.field);

        control(&mut app, &json!({"redo": true})).unwrap();
        let d = app.distort.liquify.as_ref().unwrap();
        assert_eq!(d.strokes.len(), 2);
        assert!(d.redo.is_empty());
        assert_eq!(d.strokes[0].tool, LiquifyTool::TwirlCcw);
        assert_eq!(d.strokes[1].tool, LiquifyTool::TwirlCw);
        assert_eq!(LiquifyField::from_strokes(d.canvas, d.cell, &d.strokes), d.field);
    }
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9606: fix(ui): the Color panel remembers its options across restarts

Historical source context (deduplicated excerpts; not additional cases or execution evidence)

color_panel_options_survive_a_restart setup/action excerpt:

let mut first = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
    first.ui.color_panel.mode = ColorPanelMode::HsbSliders;
    first.ui.color_panel.ramp = ColorRamp::GrayscaleRamp;
    crate::color_panel_ui::persist(&mut first);
    let saved = first.session.prefs().clone();

    let mut next = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
    next.session.prefs.edit(|p| *p = saved);
    crate::color_panel_ui::restore(&mut next);
    assert_eq!((next.ui.color_panel.mode, next.ui.color_panel.ramp), (ColorPanelMode::HsbSliders, ColorRamp::GrayscaleRamp));

    let mut fresh = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());

Expected assertion context (overlapping windows merged):

assert_eq!((next.ui.color_panel.mode, next.ui.color_panel.ramp), (ColorPanelMode::HsbSliders, ColorRamp::GrayscaleRamp));

    let mut fresh = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
    crate::color_panel_ui::restore(&mut fresh);
    assert_eq!(fresh.ui.color_panel.mode, ColorPanelMode::HueCube, "no saved options: the default");
}

an_outside_preference_change_reaches_the_live_panel setup/action excerpt:

let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
    app.ui.color_panel.mode = ColorPanelMode::HsbSliders;
    crate::color_panel_ui::persist(&mut app);
    // As prefs.set or a reset to defaults would: the stored options change under the panel.
    let mut other = app.ui.color_panel;
    other.mode = ColorPanelMode::GrayscaleSlider;
    app.session.prefs.edit(|p| p.color_panel = serde_json::to_value(other).unwrap());
    crate::color_panel_ui::persist(&mut app);
    assert_eq!(app.ui.color_panel.mode, ColorPanelMode::GrayscaleSlider, "the live panel follows the preference");
    let stored: crate::state::ColorPanelState = serde_json::from_value(app.session.prefs().color_panel.clone()).unwrap();
    assert_eq!(stored.mode, ColorPanelMode::GrayscaleSlider, "and the preference isn't overwritten");
    // A reset to defaults clears the stored options: the panel goes back to its defaults too.

Expected assertion context (overlapping windows merged):

assert_eq!(app.ui.color_panel.mode, ColorPanelMode::GrayscaleSlider, "the live panel follows the preference");
    let stored: crate::state::ColorPanelState = serde_json::from_value(app.session.prefs().color_panel.clone()).unwrap();
    assert_eq!(stored.mode, ColorPanelMode::GrayscaleSlider, "and the preference isn't overwritten");
    // A reset to defaults clears the stored options: the panel goes back to its defaults too.
    app.session.prefs.edit(|p| p.color_panel = serde_json::Value::Null);
    crate::color_panel_ui::persist(&mut app);
    assert_eq!(app.ui.color_panel.mode, ColorPanelMode::HueCube, "reset: the default mode");
    crate::color_panel_ui::persist(&mut app);
    assert_eq!(app.ui.color_panel.mode, ColorPanelMode::HueCube, "and it stays reset");
}
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA: