A coding agent could develop this repo's Go packages and could not develop the application: every path to running YellowJacket ended in a blocking GTK window, so 265 bound methods, 46 events, 33 component directories and 13 stores had exactly one form of verification available — `tsc --noEmit`. The unlock is that `wails dev`'s dev server on :34115 serves the real frontend with the real generated bindings against the same Go backend a desktop window attaches to, so a plain Chromium under Xvfb gets a fully functional app. Four test tiers now exist, cheapest first: - `make ui-test` — 313 Vitest tests in a real browser in ~2 s, no app, no backend, no display. Works because `frontend/wailsjs/` is a pure passthrough to `window.go`/`window.runtime`, so faking just those two globals runs the real bindings and the real store code. - `make test` — services in-process, asserting on the payload the frontend would receive, via a new `events.Emit` wrapper. - `make dev-headless` + `playwright-cli` — the real app, driven interactively, with an event bridge on `window.__yjEvents` and a dev-only control surface at `/__test/`. - `make e2e` — 19 of those flows frozen as Playwright specs. `events.Emit(ctx, …)` replaces all 35 direct `runtime.EventsEmit` call sites: wails' `getEvents` `log.Fatalf`s on any context without its runtime, so those paths could not run under test and a background worker could take the app down. Four packages had each hand-rolled the same guard; nine more guarded on `ctx != nil`, which does not help. `TestNoDirectRuntimeEmits` fails the build on a new one. Fixtures are generated, not committed (`make testdata`), and seeds are built by *running the app* — never by hand-writing config and DB rows, which would be a second description of a valid YJ_HOME. `.gitea/workflows/ci.yml` is the first workflow here that tests anything; the other three only package, so `gitea_ci` reported only packaging jobs and misled anyone asking whether a push was healthy. Both jobs were prototyped to green in a bare ubuntu:24.04 container before the YAML was written, which immediately caught `make lint` linting three configurations that nothing builds: all three passes omitted `webkit2_41`, so wails resolved webkit2gtk-4.0 — which Arch still ships and Ubuntu 24.04 dropped. Operational instructions live in `.pi/skills/yellowjacket-dev/`, measured discoveries in `.planning/NOTES.md`, and architecture in `CLAUDE.md` — split by tense, not by topic, because a topical split gives every new fact two plausible homes. `make skill-check` fails a commit if the skill cites a make target that does not exist.
305 lines
6.8 KiB
Go
305 lines
6.8 KiB
Go
package metadata
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"yellowjacket/internal/testfixtures"
|
|
)
|
|
|
|
// testFlacFiles returns every .flac in the generated fixture library
|
|
// (`make testdata`).
|
|
//
|
|
// Sourced from the manifest rather than by walking test_data/, which
|
|
// used to sweep up the deliberately malformed fixtures — a zero-byte
|
|
// .flac is there to prove the scanner survives it, not to be handed to
|
|
// a duration parser.
|
|
func testFlacFiles(t *testing.T) []string {
|
|
t.Helper()
|
|
|
|
m := testfixtures.Load(t)
|
|
|
|
var files []string
|
|
|
|
for _, track := range m.Tracks {
|
|
if track.Format == "flac" {
|
|
files = append(files, m.Abs(track.Path))
|
|
}
|
|
}
|
|
|
|
if len(files) == 0 {
|
|
t.Skip("no .flac fixtures in the manifest")
|
|
}
|
|
|
|
return files
|
|
}
|
|
|
|
// TestGetFlacDuration_BasicParsing verifies that getFlacDuration
|
|
// returns a positive duration for every FLAC test fixture.
|
|
func TestGetFlacDuration_BasicParsing(t *testing.T) {
|
|
for _, path := range testFlacFiles(t) {
|
|
t.Run(filepath.Base(path), func(t *testing.T) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
t.Fatalf("open: %v", err)
|
|
}
|
|
|
|
defer func() { _ = f.Close() }()
|
|
|
|
ms, props, err := getFlacDuration(f)
|
|
if err != nil {
|
|
t.Fatalf("getFlacDuration: %v", err)
|
|
}
|
|
|
|
if ms <= 0 {
|
|
t.Errorf(
|
|
"expected positive duration, got %d",
|
|
ms,
|
|
)
|
|
}
|
|
|
|
if props == nil {
|
|
t.Fatal("expected non-nil AudioProperties")
|
|
|
|
return
|
|
}
|
|
|
|
if props.SampleRate <= 0 {
|
|
t.Errorf(
|
|
"expected positive sample rate, got %d",
|
|
props.SampleRate,
|
|
)
|
|
}
|
|
|
|
if props.BitDepth <= 0 {
|
|
t.Errorf(
|
|
"expected positive bit depth, got %d",
|
|
props.BitDepth,
|
|
)
|
|
}
|
|
|
|
if props.Channels <= 0 {
|
|
t.Errorf(
|
|
"expected positive channels, got %d",
|
|
props.Channels,
|
|
)
|
|
}
|
|
|
|
t.Logf(
|
|
"duration: %dms rate: %dHz depth: %d ch: %d",
|
|
ms, props.SampleRate, props.BitDepth,
|
|
props.Channels,
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetFlacDuration_MatchesBeepDecode verifies that the fast
|
|
// header-only parser produces a duration within 1 second of the full
|
|
// decode via beep, for every FLAC test fixture.
|
|
func TestGetFlacDuration_MatchesBeepDecode(t *testing.T) {
|
|
for _, path := range testFlacFiles(t) {
|
|
t.Run(filepath.Base(path), func(t *testing.T) {
|
|
refMS, err := GetTrackLengthMillis(path)
|
|
if err != nil {
|
|
t.Fatalf("beep decode failed: %v", err)
|
|
}
|
|
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
t.Fatalf("open: %v", err)
|
|
}
|
|
|
|
defer func() { _ = f.Close() }()
|
|
|
|
fastMS, _, err := getFlacDuration(f)
|
|
if err != nil {
|
|
t.Fatalf("getFlacDuration: %v", err)
|
|
}
|
|
|
|
diffMS := refMS - fastMS
|
|
if diffMS < 0 {
|
|
diffMS = -diffMS
|
|
}
|
|
|
|
const toleranceMS = 1000
|
|
|
|
t.Logf(
|
|
"beep=%dms fast=%dms diff=%dms",
|
|
refMS, fastMS, diffMS,
|
|
)
|
|
|
|
if diffMS > toleranceMS {
|
|
t.Errorf(
|
|
"duration mismatch: beep=%dms "+
|
|
"fast=%dms (diff %dms "+
|
|
"exceeds %dms tolerance)",
|
|
refMS, fastMS, diffMS, toleranceMS,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetFlacDuration_WithPrependedID3v2 creates a temporary FLAC
|
|
// file with a synthetic ID3v2 tag prepended and verifies that
|
|
// getFlacDuration correctly skips it and parses the duration.
|
|
func TestGetFlacDuration_WithPrependedID3v2(t *testing.T) {
|
|
files := testFlacFiles(t)
|
|
|
|
// Use the first test fixture as our source.
|
|
src := files[0]
|
|
|
|
srcData, err := os.ReadFile(src)
|
|
if err != nil {
|
|
t.Fatalf("reading source: %v", err)
|
|
}
|
|
|
|
// Build a minimal ID3v2.3 header with 256 bytes of padding.
|
|
//nolint:mnd // synthetic tag construction.
|
|
paddingSize := 256
|
|
|
|
id3Header := buildID3v2Header(paddingSize)
|
|
|
|
// Write: ID3v2 header + padding + original FLAC data.
|
|
tmpDir := t.TempDir()
|
|
tmpPath := filepath.Join(tmpDir, "test_id3v2.flac")
|
|
|
|
out := make([]byte, 0, len(id3Header)+paddingSize+len(srcData))
|
|
out = append(out, id3Header...)
|
|
out = append(out, make([]byte, paddingSize)...)
|
|
out = append(out, srcData...)
|
|
|
|
if err := os.WriteFile(tmpPath, out, 0o644); err != nil {
|
|
t.Fatalf("writing temp file: %v", err)
|
|
}
|
|
|
|
// Get reference duration from original file.
|
|
origF, err := os.Open(src)
|
|
if err != nil {
|
|
t.Fatalf("open original: %v", err)
|
|
}
|
|
|
|
defer func() { _ = origF.Close() }()
|
|
|
|
origMS, _, err := getFlacDuration(origF)
|
|
if err != nil {
|
|
t.Fatalf("getFlacDuration on original: %v", err)
|
|
}
|
|
|
|
// Parse the ID3v2-wrapped file.
|
|
tmpF, err := os.Open(tmpPath)
|
|
if err != nil {
|
|
t.Fatalf("open temp: %v", err)
|
|
}
|
|
|
|
defer func() { _ = tmpF.Close() }()
|
|
|
|
wrappedMS, _, err := getFlacDuration(tmpF)
|
|
if err != nil {
|
|
t.Fatalf(
|
|
"getFlacDuration on ID3v2-wrapped file: %v", err,
|
|
)
|
|
}
|
|
|
|
if origMS != wrappedMS {
|
|
t.Errorf(
|
|
"duration mismatch: original=%dms wrapped=%dms",
|
|
origMS, wrappedMS,
|
|
)
|
|
}
|
|
|
|
t.Logf(
|
|
"original=%dms wrapped=%dms", origMS, wrappedMS,
|
|
)
|
|
}
|
|
|
|
// TestParseFlacStreamInfo verifies the bit-level parsing of sample
|
|
// rate and total samples from a known StreamInfo block.
|
|
func TestParseFlacStreamInfo(t *testing.T) {
|
|
// Construct a 34-byte StreamInfo with known values.
|
|
// Layout of bytes 10-17 (64 bits, big-endian):
|
|
// bits 0-19: sample rate (20 bits)
|
|
// bits 20-22: channels - 1 (3 bits)
|
|
// bits 23-27: bps - 1 (5 bits)
|
|
// bits 28-63: total samples (36 bits)
|
|
//
|
|
// Test values:
|
|
// sample rate = 44100 (0x0AC44)
|
|
// channels = 2 (stored as 1, 0b001)
|
|
// bps = 16 (stored as 15, 0b01111)
|
|
// total samples = 11614366 (0x00B1389E)
|
|
//
|
|
// Packed: 0x0AC442F000B1389E
|
|
// byte 10 = 0x0A byte 14 = 0x00
|
|
// byte 11 = 0xC4 byte 15 = 0xB1
|
|
// byte 12 = 0x42 byte 16 = 0x38
|
|
// byte 13 = 0xF0 byte 17 = 0x9E
|
|
//
|
|
//nolint:mnd // byte values from manual FLAC spec packing.
|
|
var si [streamInfoLength]byte
|
|
|
|
si[10] = 0x0A
|
|
si[11] = 0xC4
|
|
si[12] = 0x42
|
|
si[13] = 0xF0
|
|
si[14] = 0x00
|
|
si[15] = 0xB1
|
|
si[16] = 0x38
|
|
si[17] = 0x9E
|
|
|
|
sr, total, ch, bps := parseFlacStreamInfo(si)
|
|
|
|
//nolint:mnd // expected test values.
|
|
const (
|
|
wantSR = 44100
|
|
wantTotal = 11614366
|
|
wantChannels = 2
|
|
wantBPS = 16
|
|
)
|
|
|
|
if sr != wantSR {
|
|
t.Errorf("sample rate: got %d, want %d", sr, wantSR)
|
|
}
|
|
|
|
if total != wantTotal {
|
|
t.Errorf(
|
|
"total samples: got %d, want %d",
|
|
total, wantTotal,
|
|
)
|
|
}
|
|
|
|
if ch != wantChannels {
|
|
t.Errorf(
|
|
"channels: got %d, want %d", ch, wantChannels,
|
|
)
|
|
}
|
|
|
|
if bps != wantBPS {
|
|
t.Errorf(
|
|
"bits per sample: got %d, want %d", bps, wantBPS,
|
|
)
|
|
}
|
|
}
|
|
|
|
// buildID3v2Header creates a minimal 10-byte ID3v2.3 header with
|
|
// the given payload size encoded as a syncsafe integer.
|
|
//
|
|
//nolint:mnd // byte offsets from the ID3v2 spec.
|
|
func buildID3v2Header(payloadSize int) []byte {
|
|
header := []byte{
|
|
'I', 'D', '3', // signature
|
|
3, 0, // version 2.3.0
|
|
0, // flags
|
|
0, 0, 0, 0, // size (syncsafe, filled below)
|
|
}
|
|
|
|
header[6] = byte((payloadSize >> 21) & 0x7F)
|
|
header[7] = byte((payloadSize >> 14) & 0x7F)
|
|
header[8] = byte((payloadSize >> 7) & 0x7F)
|
|
header[9] = byte(payloadSize & 0x7F)
|
|
|
|
return header
|
|
}
|