Files
logan 5ca6cad45a
Build & publish Arch package / arch-package (push) Successful in 2m8s
CI / check (push) Failing after 1m56s
CI / e2e (push) Skipped
Search index maintenance / maintain-index (push) Successful in 13s
feat(harness): agent-drivable dev harness and CI that gates
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.
2026-08-10 23:20:42 -04:00

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
}