ZCCQ4P5T2AMJAPBDWZVHXIUKLI5U2E5GNDXRCWXEOJQRWPSJJFEQC TSOJUMHVLPASHBAVCTUK6WSGZOSBDZIC47FYILGQ2QAU7Z4BUZMAC VU3KBTQ6AFJV36WVQ4A7BM7Q3MLJQX4DBCGMIZJXNPMEKLIBGHZAC GVOVKH5R27K75VXGSZCP3X62FGNCSMDVFEKLR3LFXERFB54CHTUQC KZKLAINJJWZ64T5MUZT34LJVQIKBTKZ6EJGD7C7TTSSDGCHEDPMAC 3DVPQOKB6BX63XSBIYYCPWBL2RBG3LXZS3XPQBANJP2FWVRAOVZQC IFLKNMMP2NMJG46W3MKRLCUSPAW73N7QSDXQLJBAWPYNVEQP6KXQC T2WZBTVFHVWPKL6AKEWSEVQBR3HWWWUPUNUP2MULF4WXEAZP46KQC BZ6KQRYDMP4PWYJRL62XXIUXLTBKEASIKSAJIQPZS6DKDSYKA76QC RUVJ3V4N5V4Z3HSH2YYESKQF5G7RIHBFB5TLV2IPDWXSGJDRD54AC ZOSYO3IBH5SCB27UP642O2SGCWQ7ZTQJSZ3PXKZVFQP72S34U3IQC QFPEKXL5OUKLT4WECMATSOHWYM24QPHKS6WZAAI5BAEQSAGAK6CQC JAT3DXOLENZZGXE2NYFF3TVQAQIXMMNYO234ETKQGC2CRHJVZERQC I4CMOMXFJ3Y4AY5LPA7MDLWVHJ674IRFYLXCEXCC5ZARLCWSKCAAC }// assertStringSlice checks that got matches want (order-insensitive).func assertStringSlice(t *testing.T, label string, got, want []string) {t.Helper()if len(want) == 0 && len(got) == 0 {return}if len(got) != len(want) {t.Errorf("%s: got %v, want %v", label, got, want)return}for _, w := range want {found := slices.Contains(got, w)if !found {t.Errorf("%s: missing %q in %v", label, w, got)}}
t.Run("valid mapping - no errors", func(t *testing.T) {mapping := MappingFile{"GSK": {Species: "Roroa", Calltypes: map[string]string{"brrr": "brrr"}},"K-M": {Species: "Kiwi"},}dataSpecies := map[string]bool{"GSK": true, "K-M": true}dataCT := map[string]map[string]bool{"GSK": {"brrr": true},"K-M": {"song": true},}result, err := ValidateMappingAgainstDB(db, mapping, dataSpecies, dataCT)if err != nil {t.Fatalf("unexpected error: %v", err)}if result.HasErrors() {t.Errorf("expected no errors, got: %v", result)}})t.Run("missing species in mapping", func(t *testing.T) {mapping := MappingFile{"GSK": {Species: "Roroa"},}dataSpecies := map[string]bool{"GSK": true, "K-M": true}result, err := ValidateMappingAgainstDB(db, mapping, dataSpecies, nil)if err != nil {t.Fatalf("unexpected error: %v", err)}if len(result.MissingSpecies) != 1 || result.MissingSpecies[0] != "K-M" {t.Errorf("expected [K-M] missing, got %v", result.MissingSpecies)}})t.Run("mapped species not in DB", func(t *testing.T) {mapping := MappingFile{"PHANTOM": {Species: "Phantom"},}dataSpecies := map[string]bool{"PHANTOM": true}result, err := ValidateMappingAgainstDB(db, mapping, dataSpecies, nil)if err != nil {t.Fatalf("unexpected error: %v", err)}if len(result.MissingDBSpecies) != 1 || result.MissingDBSpecies[0] != "Phantom" {t.Errorf("expected [Phantom] missing from DB, got %v", result.MissingDBSpecies)}})t.Run("sentinel species excluded from DB check", func(t *testing.T) {mapping := MappingFile{"noise": {Species: MappingNegative},"ignore": {Species: MappingIgnore},}dataSpecies := map[string]bool{"noise": true, "ignore": true}result, err := ValidateMappingAgainstDB(db, mapping, dataSpecies, nil)if err != nil {t.Fatalf("unexpected error: %v", err)}if len(result.MissingDBSpecies) > 0 {t.Errorf("sentinels should not be checked against DB, got: %v", result.MissingDBSpecies)}})
tests := []struct {name stringmapping MappingFiledataSpecies map[string]booldataCT map[string]map[string]boolhasErrors boolmissingSpecies []stringmissingDBSpecies []stringmissingCalltypeCT string // substring expected in MissingCalltypes keyerrorContains string // substring expected in result.Error()}{{name: "valid mapping - no errors",mapping: MappingFile{"GSK": {Species: "Roroa", Calltypes: map[string]string{"brrr": "brrr"}},"K-M": {Species: "Kiwi"},},dataSpecies: map[string]bool{"GSK": true, "K-M": true},dataCT: map[string]map[string]bool{"GSK": {"brrr": true}, "K-M": {"song": true}},},{name: "missing species in mapping",mapping: MappingFile{"GSK": {Species: "Roroa"}},dataSpecies: map[string]bool{"GSK": true, "K-M": true},hasErrors: true,missingSpecies: []string{"K-M"},},{name: "mapped species not in DB",mapping: MappingFile{"PHANTOM": {Species: "Phantom"}},dataSpecies: map[string]bool{"PHANTOM": true},hasErrors: true,missingDBSpecies: []string{"Phantom"},},{name: "sentinel species excluded from DB check",mapping: MappingFile{"noise": {Species: MappingNegative}, "ignore": {Species: MappingIgnore}},dataSpecies: map[string]bool{"noise": true, "ignore": true},},{name: "missing calltype in DB",mapping: MappingFile{"K-M": {Species: "Kiwi", Calltypes: map[string]string{"song": "song", "phantom": "phantom"}},},dataSpecies: map[string]bool{"K-M": true},dataCT: map[string]map[string]bool{"K-M": {"song": true, "phantom": true}},hasErrors: true,missingCalltypeCT: "phantom",errorContains: "phantom",},}
t.Run("missing calltype in DB", func(t *testing.T) {mapping := MappingFile{"K-M": {Species: "Kiwi", Calltypes: map[string]string{"song": "song", "phantom": "phantom"}},}dataSpecies := map[string]bool{"K-M": true}dataCT := map[string]map[string]bool{"K-M": {"song": true, "phantom": true},}result, err := ValidateMappingAgainstDB(db, mapping, dataSpecies, dataCT)if err != nil {t.Fatalf("unexpected error: %v", err)}if len(result.MissingCalltypes) == 0 {t.Error("expected missing calltype for phantom")}if !strings.Contains(result.Error(), "phantom") {t.Errorf("error should mention phantom: %s", result.Error())}})
for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {result, err := ValidateMappingAgainstDB(db, tt.mapping, tt.dataSpecies, tt.dataCT)if err != nil {t.Fatalf("unexpected error: %v", err)}if result.HasErrors() != tt.hasErrors {t.Errorf("HasErrors()=%v, want %v", result.HasErrors(), tt.hasErrors)}assertStringSlice(t, "MissingSpecies", result.MissingSpecies, tt.missingSpecies)assertStringSlice(t, "MissingDBSpecies", result.MissingDBSpecies, tt.missingDBSpecies)if tt.missingCalltypeCT != "" && len(result.MissingCalltypes) == 0 {t.Error("expected missing calltype")}if tt.errorContains != "" && !strings.Contains(result.Error(), tt.errorContains) {t.Errorf("error should contain %q: %s", tt.errorContains, result.Error())}})}
m.stopPlayer()if m.state.ConfirmLabel() {if err := m.state.Save(); err != nil {m.err = err.Error()return m, nil}}return m.advanceOrQuit()
return m.handleConfirmLabel()
// handleBookmarkToggle toggles the bookmark and saves.func (m Model) handleBookmarkToggle() (tea.Model, tea.Cmd) {m.state.ToggleBookmark()if err := m.state.Save(); err != nil {m.err = err.Error()}return m, nil}// handleBookmarkNav navigates to a bookmark. navFn returns true if found.func (m Model) handleBookmarkNav(navFn func() bool) (tea.Model, tea.Cmd) {m.stopPlayer()if navFn() {return m, m.segmentChangeCmd()}m.err = "No bookmarks found"return m, nil}// handleConfirmLabel confirms the current label and advances.func (m Model) handleConfirmLabel() (tea.Model, tea.Cmd) {m.stopPlayer()if m.state.ConfirmLabel() {if err := m.state.Save(); err != nil {m.err = err.Error()return m, nil}}return m.advanceOrQuit()}
speciesIDMap, err := loadSpeciesIDs(q, mapping, uniqueSpecies)if err != nil {return nil, nil, err}calltypeIDMap, err := loadCalltypeIDs(q, mapping, uniqueCalltypes)if err != nil {return nil, nil, err}return speciesIDMap, calltypeIDMap, nil}// loadSpeciesIDs queries the DB for species IDs matching the mapped species labels.func loadSpeciesIDs(q db.Querier, mapping utils.MappingFile, uniqueSpecies map[string]bool) (map[string]string, error) {
// Load species IDsif len(dbSpeciesSet) > 0 {dbSpeciesList := make([]string, 0, len(dbSpeciesSet))for s := range dbSpeciesSet {dbSpeciesList = append(dbSpeciesList, s)}
if len(dbSpeciesSet) == 0 {return speciesIDMap, nil}dbSpeciesList := make([]string, 0, len(dbSpeciesSet))for s := range dbSpeciesSet {dbSpeciesList = append(dbSpeciesList, s)}
query := `SELECT id, label FROM species WHERE label IN (` + db.Placeholders(len(dbSpeciesList)) + `) AND active = true`args := make([]any, len(dbSpeciesList))for i, s := range dbSpeciesList {args[i] = s}
query := `SELECT id, label FROM species WHERE label IN (` + db.Placeholders(len(dbSpeciesList)) + `) AND active = true`args := make([]any, len(dbSpeciesList))for i, s := range dbSpeciesList {args[i] = s}
rows, err := q.Query(query, args...)if err != nil {return nil, nil, fmt.Errorf("failed to query species: %w", err)}defer rows.Close()
rows, err := q.Query(query, args...)if err != nil {return nil, fmt.Errorf("failed to query species: %w", err)}defer rows.Close()
// Load calltype IDs
// loadCalltypeIDs queries the DB for calltype IDs matching the mapped calltype labels.func loadCalltypeIDs(q db.Querier, mapping utils.MappingFile, uniqueCalltypes map[string]map[string]bool) (map[string]map[string]string, error) {calltypeIDMap := make(map[string]map[string]string)
}})}// --- createOutputDir ---func TestCreateOutputDir(t *testing.T) {t.Run("creates parent directory", func(t *testing.T) {base := t.TempDir()outputPath := filepath.Join(base, "subdir", "output.duckdb")if err := createOutputDir(outputPath); err != nil {t.Fatalf("unexpected error: %v", err)}if _, err := os.Stat(filepath.Join(base, "subdir")); os.IsNotExist(err) {t.Error("subdirectory was not created")}})t.Run("current dir passes", func(t *testing.T) {if err := createOutputDir("output.duckdb"); err != nil {t.Errorf("current dir should pass: %v", err)}})}// --- createEventLogFile ---func TestCreateEventLogFile(t *testing.T) {t.Run("creates empty file", func(t *testing.T) {base := t.TempDir()path := filepath.Join(base, "test.duckdb")if err := createEventLogFile(path); err != nil {t.Fatalf("unexpected error: %v", err)}data, err := os.ReadFile(path + ".events.jsonl")if err != nil {t.Fatalf("failed to read event file: %v", err)
// Open source database (read-only for safety)
orderedTables, err := prepareExport(input, &output)if err != nil {return output, err}// If dry-run, return nowif input.DryRun {output.Message = fmt.Sprintf("Would export dataset '%s' (%s)", output.DatasetName, input.DatasetID)return output, nil}return executeExport(ctx, input, orderedTables, output)}// prepareExport validates inputs, opens the source DB, and counts rows.func prepareExport(input ExportDatasetInput, output *ExportDatasetOutput) ([]TableRelationship, error) {
// If dry-run, return nowif input.DryRun {sourceDB.Close()output.Message = fmt.Sprintf("Would export dataset '%s' (%s)", datasetName, input.DatasetID)return output, nil}// Close source DB before creating output (DuckDB can't attach same file twice)
// Close before creating output (DuckDB can't attach same file twice)
// Create empty event log fileeventLogPath := input.Output + ".events.jsonl"eventFile, err := os.Create(eventLogPath)if err != nil {return output, fmt.Errorf("failed to create event log file: %w", err)}if err := eventFile.Close(); err != nil {return output, fmt.Errorf("failed to close event log file: %w", err)
if err := createEventLogFile(input.Output); err != nil {return output, err
// createEventLogFile creates an empty .events.jsonl file next to the export.func createEventLogFile(outputPath string) error {eventLogPath := outputPath + ".events.jsonl"eventFile, err := os.Create(eventLogPath)if err != nil {return fmt.Errorf("failed to create event log file: %w", err)}if err := eventFile.Close(); err != nil {return fmt.Errorf("failed to close event log file: %w", err)}return nil}
}// --- validateFolderInput ---func TestValidateFolderInput(t *testing.T) {tests := []struct {name stringinput CallsPropagateFolderInputwantErr bool}{{name: "missing folder", input: CallsPropagateFolderInput{FromFilter: "a", ToFilter: "b", Species: "Kiwi"}, wantErr: true},{name: "missing from", input: CallsPropagateFolderInput{Folder: "/tmp", ToFilter: "b", Species: "Kiwi"}, wantErr: true},{name: "missing to", input: CallsPropagateFolderInput{Folder: "/tmp", FromFilter: "a", Species: "Kiwi"}, wantErr: true},{name: "missing species", input: CallsPropagateFolderInput{Folder: "/tmp", FromFilter: "a", ToFilter: "b"}, wantErr: true},{name: "from==to", input: CallsPropagateFolderInput{Folder: "/tmp", FromFilter: "x", ToFilter: "x", Species: "Kiwi"}, wantErr: true},{name: "not a dir", input: CallsPropagateFolderInput{Folder: "/dev/null", FromFilter: "a", ToFilter: "b", Species: "Kiwi"}, wantErr: true},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {var output CallsPropagateFolderOutputerr := validateFolderInput(&output, tt.input)if (err != nil) != tt.wantErr {t.Errorf("validateFolderInput() error=%v, wantErr=%v", err, tt.wantErr)}if tt.wantErr && output.Error == "" {t.Error("expected output.Error to be set on validation failure")}})}}// --- accumulatePropagateResult ---func TestAccumulatePropagateResult(t *testing.T) {t.Run("filters missing skips file", func(t *testing.T) {var output CallsPropagateFolderOutputaccumulatePropagateResult("test.data", CallsPropagateOutput{FiltersMissing: true}, &output)if output.FilesSkippedNoFilter != 1 {t.Errorf("expected 1 skipped, got %d", output.FilesSkippedNoFilter)}if output.FilesWithBothFilters != 0 {t.Error("should not count as having both filters")}})t.Run("normal result aggregates counts", func(t *testing.T) {var output CallsPropagateFolderOutputaccumulatePropagateResult("test.data", CallsPropagateOutput{TargetsExamined: 5,Propagated: 2,SkippedNoOverlap: 2,SkippedConflict: 1,Conflicts: []PropagateConflict{{TargetStart: 1.0, TargetEnd: 2.0}},}, &output)if output.TargetsExamined != 5 || output.Propagated != 2 {t.Errorf("counts wrong: %+v", output)}if output.FilesChanged != 1 {t.Errorf("should mark 1 file changed, got %d", output.FilesChanged)}if len(output.Conflicts) != 1 || output.Conflicts[0].File != "test.data" {t.Errorf("conflict file not tagged: %+v", output.Conflicts)}})t.Run("zero propagated not counted as changed", func(t *testing.T) {var output CallsPropagateFolderOutputaccumulatePropagateResult("test.data", CallsPropagateOutput{TargetsExamined: 3,}, &output)if output.FilesChanged != 0 {t.Errorf("should not count as changed, got %d", output.FilesChanged)}})
if input.Folder == "" {output.Error = "--folder is required"return output, fmt.Errorf("%s", output.Error)}if input.FromFilter == "" {output.Error = "--from is required"return output, fmt.Errorf("%s", output.Error)}if input.ToFilter == "" {output.Error = "--to is required"return output, fmt.Errorf("%s", output.Error)}if input.Species == "" {output.Error = "--species is required"return output, fmt.Errorf("%s", output.Error)
if err := validateFolderInput(&output, input); err != nil {return output, err
info, err := os.Stat(input.Folder)if err != nil {output.Error = fmt.Sprintf("folder not found: %s", input.Folder)return output, fmt.Errorf("%s", output.Error)}if !info.IsDir() {output.Error = fmt.Sprintf("not a directory: %s", input.Folder)return output, fmt.Errorf("%s", output.Error)}
if fileOut.FiltersMissing {output.FilesSkippedNoFilter++continue
accumulatePropagateResult(f, fileOut, &output)}return output, nil}// validateFolderInput checks required fields and that the folder exists.func validateFolderInput(output *CallsPropagateFolderOutput, input CallsPropagateFolderInput) error {checks := []struct {val stringmsg string}{{input.Folder, "--folder is required"},{input.FromFilter, "--from is required"},{input.ToFilter, "--to is required"},{input.Species, "--species is required"},}for _, c := range checks {if c.val == "" {output.Error = c.msgreturn fmt.Errorf("%s", c.msg)
output.FilesWithBothFilters++output.TargetsExamined += fileOut.TargetsExaminedoutput.Propagated += fileOut.Propagatedoutput.SkippedNoOverlap += fileOut.SkippedNoOverlapoutput.SkippedConflict += fileOut.SkippedConflictif fileOut.Propagated > 0 {output.FilesChanged++}for _, c := range fileOut.Conflicts {c.File = foutput.Conflicts = append(output.Conflicts, c)}
}if input.FromFilter == input.ToFilter {output.Error = "--from and --to must differ"return fmt.Errorf("%s", output.Error)}info, err := os.Stat(input.Folder)if err != nil {output.Error = fmt.Sprintf("folder not found: %s", input.Folder)return fmt.Errorf("%s", output.Error)}if !info.IsDir() {output.Error = fmt.Sprintf("not a directory: %s", input.Folder)return fmt.Errorf("%s", output.Error)
return output, nil
// accumulatePropagateResult merges a per-file result into the folder-level aggregate.func accumulatePropagateResult(filePath string, fileOut CallsPropagateOutput, output *CallsPropagateFolderOutput) {if fileOut.FiltersMissing {output.FilesSkippedNoFilter++return}output.FilesWithBothFilters++output.TargetsExamined += fileOut.TargetsExaminedoutput.Propagated += fileOut.Propagatedoutput.SkippedNoOverlap += fileOut.SkippedNoOverlapoutput.SkippedConflict += fileOut.SkippedConflictif fileOut.Propagated > 0 {output.FilesChanged++}for _, c := range fileOut.Conflicts {c.File = filePathoutput.Conflicts = append(output.Conflicts, c)}
package cmdimport ("strings""testing")func TestRunCalls_NoArgs(t *testing.T) {err := RunCalls([]string{})if err == nil {t.Error("expected error for no args")}if !strings.Contains(err.Error(), "subcommand required") {t.Errorf("unexpected error: %v", err)}}func TestRunCalls_UnknownSubcommand(t *testing.T) {err := RunCalls([]string{"nonexistent"})if err == nil {t.Error("expected error for unknown subcommand")}if !strings.Contains(err.Error(), "unknown") {t.Errorf("unexpected error: %v", err)}}func TestCallsSubcommandsComplete(t *testing.T) {// Ensure the map has all expected subcommandsexpected := []string{"from-preds", "from-birda", "from-raven", "show-images","classify", "clip", "modify", "push-certainty","detect-anomalies", "propagate", "summarise", "clip-labels",}for _, name := range expected {if _, ok := callsSubcommands[name]; !ok {t.Errorf("callsSubcommands missing: %s", name)}}if len(callsSubcommands) != len(expected) {t.Errorf("callsSubcommands has %d entries, expected %d", len(callsSubcommands), len(expected))}}
// callsSubcommands maps subcommand names to their handler functions.var callsSubcommands = map[string]func([]string) error{"from-preds": runCallsFromPreds,"from-birda": runCallsFromBirda,"from-raven": runCallsFromRaven,"show-images": runCallsShowImages,"classify": RunCallsClassify,"clip": RunCallsClip,"modify": RunCallsModify,"push-certainty": runCallsPushCertainty,"detect-anomalies": runCallsDetectAnomalies,"propagate": runCallsPropagate,"summarise": runCallsSummarise,"clip-labels": runCallsClipLabels,}
switch args[0] {case "from-preds":return runCallsFromPreds(args[1:])case "from-birda":return runCallsFromBirda(args[1:])case "from-raven":return runCallsFromRaven(args[1:])case "show-images":return runCallsShowImages(args[1:])case "classify":return RunCallsClassify(args[1:])case "clip":return RunCallsClip(args[1:])case "modify":return RunCallsModify(args[1:])case "push-certainty":return runCallsPushCertainty(args[1:])case "detect-anomalies":return runCallsDetectAnomalies(args[1:])case "propagate":return runCallsPropagate(args[1:])case "summarise":return runCallsSummarise(args[1:])case "clip-labels":return runCallsClipLabels(args[1:])default:
handler, ok := callsSubcommands[args[0]]if !ok {