Files
Lucas WintherandClaude Opus 5.5 f0249995cf fix(align): review round 1 for the alignment page
- server: collapse leading slashes so //host/x is never proxied off-host
  or redirected cross-origin; 1 MB body limit; max 10k anchors; a corrupt
  alignment.json no longer 500s /api/songs (song falls back to auto with
  a warning); comments on cookie scope and the loopback inner server
- warp (ruling A6): adding/moving a manual anchor removes autos only
  within 10 s (MIDI) of the new/old position, never past a manual, plus
  any auto that would break audio monotonicity
- page: first anchor clamped at >= 0 s; ruler scrubbing seeks on release;
  wording for the new auto-removal rule; removed-count excludes the
  dragged anchor; shows server warnings
- manifest: unique tmp name for alignment.json; clear error for a
  corrupt file
- resample test: speed asserted as a ratio to the direct path

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:48:32 +01:00

72 lines
3.3 KiB
TypeScript

import { test, expect, describe } from "bun:test";
import { dragAudio, dragMidi, hitAnchor, anchorAtCursor, MIN_GAP_SEC } from "../../../src/align/page/edit.ts";
import { makeWarp, type Anchor } from "../../../src/align/warp.ts";
const m = (midiSec: number, audioSec: number): Anchor => ({ midiSec, audioSec, kind: "manual" });
const a = (midiSec: number, audioSec: number): Anchor => ({ midiSec, audioSec, kind: "auto" });
const base = [m(10, 11), m(20, 22), m(30, 32)];
describe("dragAudio (recording end)", () => {
test("moves audioSec, keeps midiSec, returns the anchor's new index", () => {
const r = dragAudio(base, 1, 23);
expect(r.anchors[r.index]).toEqual(m(20, 23));
expect(r.anchors).toHaveLength(3);
});
test("cannot cross manual neighbours", () => {
expect(dragAudio(base, 1, 5).anchors[1]!.audioSec).toBeCloseTo(11 + MIN_GAP_SEC, 9);
expect(dragAudio(base, 1, 99).anchors[1]!.audioSec).toBeCloseTo(32 - MIN_GAP_SEC, 9);
});
test("the first anchor cannot be dragged before the start of the recording", () => {
expect(dragAudio(base, 0, -3).anchors[0]!.audioSec).toBe(0);
});
test("dragging an auto anchor makes it manual and drops the autos in its stretches", () => {
const list = [m(0, 0), a(5, 5.2), a(10, 10.3), a(15, 15.1), m(20, 20)];
const r = dragAudio(list, 2, 10.5);
expect(r.anchors).toEqual([m(0, 0), m(10, 10.5), m(20, 20)]);
expect(r.index).toBe(1);
});
});
describe("dragMidi (MIDI end): the grabbed MIDI moment follows the mouse, audioSec stays", () => {
for (const [grab, to] of [[22, 23.5], [22, 20.5], [21, 25], [24, 21]] as const) {
test(`grab at ${grab}s, drag to ${to}s`, () => {
const m0 = makeWarp(base).toMidi(grab);
const r = dragMidi(base, 1, m0, to);
expect(r.anchors[r.index]!.audioSec).toBe(22);
expect(makeWarp(r.anchors).toAudio(m0)).toBeCloseTo(to, 6);
});
}
test("no drag = no change", () => {
const r = dragMidi(base, 1, 20, 22);
expect(r.anchors[1]!.midiSec).toBeCloseTo(20, 6);
});
test("clamped between manual neighbours' MIDI times", () => {
const r = dragMidi(base, 1, 20, 31.9);
expect(r.anchors[1]!.midiSec).toBeGreaterThanOrEqual(10 + MIN_GAP_SEC - 1e-9);
const s = dragMidi(base, 1, 20, 11.1);
expect(s.anchors[1]!.midiSec).toBeLessThanOrEqual(30 - MIN_GAP_SEC + 1e-9);
});
test("works for the first and only anchor (edge extrapolation)", () => {
const r = dragMidi([m(10, 11)], 0, 10, 12);
expect(makeWarp(r.anchors).toAudio(10)).toBeCloseTo(12, 6);
expect(r.anchors[0]!.audioSec).toBe(11);
const f = dragMidi(base, 0, 10, 12);
expect(makeWarp(f.anchors).toAudio(10)).toBeCloseTo(12, 6);
});
});
test("hitAnchor picks the nearest anchor line within tolerance", () => {
const v = { start: 0, pxPerSec: 10, width: 500 };
expect(hitAnchor(base, v, 221, 6)).toBe(1);
expect(hitAnchor(base, v, 113, 6)).toBe(0);
expect(hitAnchor(base, v, 160, 6)).toBe(-1);
});
test("anchorAtCursor adds a manual anchor where the current warp maps the cursor", () => {
const r = anchorAtCursor(base, 27);
const k = r.anchors.findIndex((x) => x.audioSec === 27);
expect(r.index).toBe(k);
expect(r.anchors[k]).toEqual(m(makeWarp(base).toMidi(27), 27));
expect(() => anchorAtCursor(base, 22)).toThrow(); // already an anchor there
});