Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/go-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Go Test

on:
pull_request:
push:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: backend/go.mod
cache-dependency-path: backend/go.sum

- name: Build
run: go build ./...

- name: Vet
run: go vet ./...

- name: Test
run: go test ./...
65 changes: 65 additions & 0 deletions backend/generator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import "testing"

// TestNormalizeTags asserts observable behavior only, so it survives the
// planned Phase 3 refactor that hoists the regexes to package level.
func TestNormalizeTags(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
// Each alias -> canonical mapping.
{"sigh", "hello [sigh] world", "hello [sighs] world"},
{"laughing", "[laughing] ok", "[laughs] ok"},
{"sarcasm", "sure [sarcasm]", "sure [sarcastic]"},
{"whispering", "[whispering] quiet", "[whispers] quiet"},
{"mischievous", "[mischievous] grin", "[mischievously] grin"},
{"amazement", "[amazement] wow", "[amazed] wow"},
{"excitement", "[excitement] yay", "[excited] yay"},

// Case-insensitivity.
{"uppercase sigh", "[SIGH]", "[sighs]"},
{"mixed case laughing", "[LaUgHiNg]", "[laughs]"},

// Multiple tags in one string.
{"multiple tags", "[sigh] and [laughing]", "[sighs] and [laughs]"},

// Already-canonical tags are left untouched (no over-matching).
{"canonical sighs untouched", "[sighs]", "[sighs]"},
{"canonical laughs untouched", "[laughs]", "[laughs]"},

// No over-matching: substrings of words are not tags.
{"word sigh not a tag", "she gave a sigh", "she gave a sigh"},
{"no brackets", "laughing out loud", "laughing out loud"},

// Untouched, unrelated tags pass through.
{"unknown tag untouched", "[pause] here", "[pause] here"},

// Empty input.
{"empty", "", ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := normalizeTags(tt.in); got != tt.want {
t.Errorf("normalizeTags(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
171 changes: 171 additions & 0 deletions backend/storage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"encoding/binary"
"strings"
"testing"
)

func TestAddWavHeader(t *testing.T) {
tests := []struct {
name string
data []byte
sampleRate uint32
numChannels uint16
bitsPerSample uint16
}{
{
name: "typical mono 24kHz 16-bit PCM",
data: []byte{0x01, 0x02, 0x03, 0x04},
sampleRate: 24000,
numChannels: 1,
bitsPerSample: 16,
},
{
name: "stereo 48kHz 16-bit PCM",
data: []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},
sampleRate: 48000,
numChannels: 2,
bitsPerSample: 16,
},
{
name: "empty PCM payload",
data: []byte{},
sampleRate: 16000,
numChannels: 1,
bitsPerSample: 16,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
out := addWavHeader(tt.data, tt.sampleRate, tt.numChannels, tt.bitsPerSample)

// Header is always 44 bytes, followed by the raw data.
const headerLen = 44
if len(out) != headerLen+len(tt.data) {
t.Fatalf("total length = %d, want %d", len(out), headerLen+len(tt.data))
}

// The raw PCM data must be preserved verbatim after the header.
if got := out[headerLen:]; string(got) != string(tt.data) {
t.Errorf("payload not preserved: got %v, want %v", got, tt.data)
}

// ASCII markers at their canonical offsets.
assertMarker(t, out, 0, "RIFF")
assertMarker(t, out, 8, "WAVE")
assertMarker(t, out, 12, "fmt ")
assertMarker(t, out, 36, "data")

dataLen := uint32(len(tt.data))

// fileSize field (bytes 4-8) == len(data) + 36.
if got := binary.LittleEndian.Uint32(out[4:8]); got != dataLen+36 {
t.Errorf("fileSize = %d, want %d", got, dataLen+36)
}

// Subchunk1Size (bytes 16-20) == 16 for PCM.
if got := binary.LittleEndian.Uint32(out[16:20]); got != 16 {
t.Errorf("Subchunk1Size = %d, want 16", got)
}

// AudioFormat (bytes 20-22) == 1 for PCM.
if got := binary.LittleEndian.Uint16(out[20:22]); got != 1 {
t.Errorf("AudioFormat = %d, want 1", got)
}

// NumChannels (bytes 22-24).
if got := binary.LittleEndian.Uint16(out[22:24]); got != tt.numChannels {
t.Errorf("NumChannels = %d, want %d", got, tt.numChannels)
}

// SampleRate (bytes 24-28).
if got := binary.LittleEndian.Uint32(out[24:28]); got != tt.sampleRate {
t.Errorf("SampleRate = %d, want %d", got, tt.sampleRate)
}

// ByteRate (bytes 28-32) == sampleRate * numChannels * bitsPerSample/8.
wantByteRate := tt.sampleRate * uint32(tt.numChannels) * uint32(tt.bitsPerSample/8)
if got := binary.LittleEndian.Uint32(out[28:32]); got != wantByteRate {
t.Errorf("ByteRate = %d, want %d", got, wantByteRate)
}

// BlockAlign (bytes 32-34) == numChannels * bitsPerSample/8.
wantBlockAlign := tt.numChannels * (tt.bitsPerSample / 8)
if got := binary.LittleEndian.Uint16(out[32:34]); got != wantBlockAlign {
t.Errorf("BlockAlign = %d, want %d", got, wantBlockAlign)
}

// BitsPerSample (bytes 34-36).
if got := binary.LittleEndian.Uint16(out[34:36]); got != tt.bitsPerSample {
t.Errorf("BitsPerSample = %d, want %d", got, tt.bitsPerSample)
}

// data subchunk size (bytes 40-44) == len(data).
if got := binary.LittleEndian.Uint32(out[40:44]); got != dataLen {
t.Errorf("data subchunk size = %d, want %d", got, dataLen)
}
})
}
}

func assertMarker(t *testing.T, out []byte, offset int, want string) {
t.Helper()
if got := string(out[offset : offset+len(want)]); got != want {
t.Errorf("marker at offset %d = %q, want %q", offset, got, want)
}
}

func TestGenerateFilename(t *testing.T) {
extTests := []struct {
name string
mimeType string
wantExt string
}{
{"wav mime", "audio/wav", ".wav"},
{"wav substring", "audio/x-wav", ".wav"},
{"ogg mime", "audio/ogg", ".ogg"},
{"mp3 mime defaults", "audio/mpeg", ".mp3"},
{"unknown defaults to mp3", "application/octet-stream", ".mp3"},
{"empty defaults to mp3", "", ".mp3"},
}

for _, tt := range extTests {
t.Run(tt.name, func(t *testing.T) {
got := generateFilename(tt.mimeType)
if !strings.HasPrefix(got, "take-") {
t.Errorf("filename %q does not have %q prefix", got, "take-")
}
if !strings.HasSuffix(got, tt.wantExt) {
t.Errorf("filename %q does not have extension %q", got, tt.wantExt)
}
})
}

t.Run("unique across calls", func(t *testing.T) {
const n = 1000
seen := make(map[string]struct{}, n)
for i := 0; i < n; i++ {
name := generateFilename("audio/wav")
if _, dup := seen[name]; dup {
t.Fatalf("duplicate filename generated: %q", name)
}
seen[name] = struct{}{}
}
})
}
Loading