statuses update

This commit is contained in:
sanek5g
2026-06-24 18:58:35 +03:00
parent fdddacf534
commit 0a9bfd0799
39 changed files with 2099 additions and 12 deletions

View File

@@ -17,6 +17,7 @@ type Config struct {
RabbitURL string
Exchange string
RoutingKey string
StatusQueue string
}
func Load() Config {
@@ -31,6 +32,7 @@ func Load() Config {
RabbitURL: getEnv("RABBITMQ_URL", "amqp://guest:guest@localhost:5672/"),
Exchange: getEnv("RABBITMQ_EXCHANGE", "audio_pipeline"),
RoutingKey: getEnv("RABBITMQ_ROUTING_KEY", "audio.new"),
StatusQueue: getEnv("STATUS_QUEUE", "pipeline.status"),
}
}

View File

@@ -0,0 +1,47 @@
package config
import (
"testing"
"time"
)
func TestLoadDefaults(t *testing.T) {
t.Setenv("STORAGE_ROOT", "")
t.Setenv("RABBITMQ_URL", "")
t.Setenv("STATUS_QUEUE", "")
cfg := Load()
if cfg.StorageRoot != "/data/storage" {
t.Fatalf("StorageRoot: got %q", cfg.StorageRoot)
}
if cfg.StatusQueue != "pipeline.status" {
t.Fatalf("StatusQueue: got %q", cfg.StatusQueue)
}
if cfg.PollInterval != 5*time.Second {
t.Fatalf("PollInterval: got %v", cfg.PollInterval)
}
if cfg.StableChecks != 3 {
t.Fatalf("StableChecks: got %d", cfg.StableChecks)
}
}
func TestLoadFromEnv(t *testing.T) {
t.Setenv("STORAGE_ROOT", "/tmp/storage")
t.Setenv("POLL_INTERVAL", "10s")
t.Setenv("STABLE_CHECKS", "5")
t.Setenv("STATUS_QUEUE", "custom.status")
cfg := Load()
if cfg.StorageRoot != "/tmp/storage" {
t.Fatalf("StorageRoot: got %q", cfg.StorageRoot)
}
if cfg.PollInterval != 10*time.Second {
t.Fatalf("PollInterval: got %v", cfg.PollInterval)
}
if cfg.StableChecks != 5 {
t.Fatalf("StableChecks: got %d", cfg.StableChecks)
}
if cfg.StatusQueue != "custom.status" {
t.Fatalf("StatusQueue: got %q", cfg.StatusQueue)
}
}

View File

@@ -0,0 +1,57 @@
package pipelinestatus
import (
"context"
"encoding/json"
"fmt"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
const (
StatusPending = "pending"
StatusInProgress = "in_progress"
StatusDone = "done"
StatusError = "error"
)
const (
StageQueued = "queued"
StageTranscribing = "transcribing"
StageAnalysing = "analysing"
StageTagging = "tagging"
StageCompleted = "completed"
)
type Event struct {
TaskID string `json:"task_id"`
Filename string `json:"filename,omitempty"`
Status string `json:"status"`
Stage string `json:"stage"`
Error string `json:"error,omitempty"`
Timestamp int64 `json:"timestamp"`
}
func DeclareQueue(ch *amqp.Channel, queue string) error {
_, err := ch.QueueDeclare(queue, true, false, false, false, nil)
return err
}
func Publish(ctx context.Context, ch *amqp.Channel, queue string, ev Event) error {
if ev.Timestamp == 0 {
ev.Timestamp = time.Now().Unix()
}
body, err := json.Marshal(ev)
if err != nil {
return err
}
if err := ch.PublishWithContext(ctx, "", queue, false, false, amqp.Publishing{
ContentType: "application/json",
Body: body,
DeliveryMode: amqp.Persistent,
}); err != nil {
return fmt.Errorf("publish status: %w", err)
}
return nil
}

View File

@@ -0,0 +1,65 @@
package pipelinestatus
import (
"encoding/json"
"testing"
)
func TestEventJSON(t *testing.T) {
ev := Event{
TaskID: "01HXABCDEF",
Filename: "call.wav",
Status: StatusPending,
Stage: StageQueued,
Timestamp: 1717843200,
}
body, err := json.Marshal(ev)
if err != nil {
t.Fatal(err)
}
var got Event
if err := json.Unmarshal(body, &got); err != nil {
t.Fatal(err)
}
if got.TaskID != ev.TaskID || got.Status != StatusPending || got.Stage != StageQueued {
t.Fatalf("unexpected event: %+v", got)
}
if got.Error != "" {
t.Fatalf("expected empty error, got %q", got.Error)
}
}
func TestEventJSONWithError(t *testing.T) {
ev := Event{
TaskID: "01HX",
Status: StatusError,
Stage: StageTranscribing,
Error: "nexara timeout",
}
body, err := json.Marshal(ev)
if err != nil {
t.Fatal(err)
}
if !json.Valid(body) {
t.Fatal("invalid json")
}
var got Event
if err := json.Unmarshal(body, &got); err != nil {
t.Fatal(err)
}
if got.Error != "nexara timeout" {
t.Fatalf("error field: got %q", got.Error)
}
}
func TestStatusConstants(t *testing.T) {
statuses := []string{StatusPending, StatusInProgress, StatusDone, StatusError}
seen := make(map[string]bool)
for _, s := range statuses {
if seen[s] {
t.Fatalf("duplicate status %q", s)
}
seen[s] = true
}
}

View File

@@ -0,0 +1,43 @@
package publisher
import (
"encoding/json"
"testing"
"time"
)
func TestAudioTaskJSON(t *testing.T) {
task := AudioTask{
TaskID: "01HXABCDEF",
FilePath: "/data/storage/processing/01HX.wav",
Filename: "call.wav",
Size: 1024,
CreatedAt: 1717843200,
}
body, err := json.Marshal(task)
if err != nil {
t.Fatal(err)
}
var got AudioTask
if err := json.Unmarshal(body, &got); err != nil {
t.Fatal(err)
}
if got.TaskID != task.TaskID || got.FilePath != task.FilePath || got.Size != 1024 {
t.Fatalf("unexpected task: %+v", got)
}
}
func TestAudioTaskCreatedAtDefault(t *testing.T) {
task := AudioTask{TaskID: "x", Filename: "a.wav"}
if task.CreatedAt != 0 {
t.Fatal("zero value should have CreatedAt=0 before publish")
}
before := time.Now().Unix()
if task.CreatedAt == 0 {
task.CreatedAt = time.Now().Unix()
}
if task.CreatedAt < before {
t.Fatal("timestamp should be set")
}
}

View File

@@ -0,0 +1,162 @@
package scanner
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func testScanner(t *testing.T) (*Scanner, string) {
t.Helper()
root := t.TempDir()
s := New(Config{
StorageRoot: root,
IncomingDir: "incoming",
ProcessingDir: "processing",
FailedDir: "failed",
StableWindow: time.Millisecond,
StableChecks: 1,
})
if err := s.EnsureDirs(); err != nil {
t.Fatalf("EnsureDirs: %v", err)
}
return s, root
}
func TestEnsureDirs(t *testing.T) {
_, root := testScanner(t)
for _, dir := range []string{"incoming", "processing", "failed"} {
p := filepath.Join(root, dir)
if st, err := os.Stat(p); err != nil || !st.IsDir() {
t.Fatalf("dir %s missing: %v", dir, err)
}
}
}
func TestScanOnceClaimsStableAudio(t *testing.T) {
s, root := testScanner(t)
incoming := filepath.Join(root, "incoming")
if err := os.WriteFile(filepath.Join(incoming, "call.wav"), []byte("audio"), 0o644); err != nil {
t.Fatal(err)
}
claimed, err := s.ScanOnce()
if err != nil {
t.Fatal(err)
}
if len(claimed) != 1 {
t.Fatalf("claimed %d files, want 1", len(claimed))
}
cf := claimed[0]
if cf.Filename != "call.wav" {
t.Fatalf("filename: got %q", cf.Filename)
}
if cf.TaskID == "" {
t.Fatal("empty task_id")
}
if !strings.HasPrefix(cf.FilePath, filepath.Join(root, "processing")) {
t.Fatalf("unexpected path: %s", cf.FilePath)
}
if _, err := os.Stat(cf.FilePath); err != nil {
t.Fatalf("claimed file missing: %v", err)
}
if _, err := os.Stat(filepath.Join(incoming, "call.wav")); !os.IsNotExist(err) {
t.Fatal("source file should be moved")
}
}
func TestScanOnceSkipsUnsupportedAndHidden(t *testing.T) {
s, root := testScanner(t)
incoming := filepath.Join(root, "incoming")
files := map[string][]byte{
".hidden.wav": []byte("x"),
"notes.txt": []byte("x"),
"upload.tmp": []byte("x"),
"valid.mp3": []byte("audio"),
}
for name, data := range files {
if err := os.WriteFile(filepath.Join(incoming, name), data, 0o644); err != nil {
t.Fatal(err)
}
}
claimed, err := s.ScanOnce()
if err != nil {
t.Fatal(err)
}
if len(claimed) != 1 {
t.Fatalf("claimed %d files, want 1 (valid.mp3)", len(claimed))
}
if claimed[0].Filename != "valid.mp3" {
t.Fatalf("filename: got %q", claimed[0].Filename)
}
}
func TestScanOnceEmptyIncoming(t *testing.T) {
s, _ := testScanner(t)
claimed, err := s.ScanOnce()
if err != nil {
t.Fatal(err)
}
if len(claimed) != 0 {
t.Fatalf("expected 0 claimed, got %d", len(claimed))
}
}
func TestRollbackToIncoming(t *testing.T) {
s, root := testScanner(t)
processing := filepath.Join(root, "processing")
incoming := filepath.Join(root, "incoming")
processingFile := filepath.Join(processing, "01TASK.wav")
if err := os.WriteFile(processingFile, []byte("data"), 0o644); err != nil {
t.Fatal(err)
}
if err := s.RollbackToIncoming(processingFile, "call.wav"); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(incoming, "call.wav")); err != nil {
t.Fatalf("file not restored to incoming: %v", err)
}
if _, err := os.Stat(processingFile); !os.IsNotExist(err) {
t.Fatal("processing file should be gone")
}
}
func TestMoveToFailed(t *testing.T) {
s, root := testScanner(t)
processing := filepath.Join(root, "processing")
processingFile := filepath.Join(processing, "broken.wav")
if err := os.WriteFile(processingFile, []byte("data"), 0o644); err != nil {
t.Fatal(err)
}
if err := s.MoveToFailed(processingFile, "broken.wav"); err != nil {
t.Fatal(err)
}
failedPath := filepath.Join(root, "failed", "broken.wav")
if _, err := os.Stat(failedPath); err != nil {
t.Fatalf("file not in failed: %v", err)
}
}
func TestIsStableStableFile(t *testing.T) {
s, root := testScanner(t)
path := filepath.Join(root, "incoming", "stable.wav")
if err := os.WriteFile(path, []byte("fixed-content"), 0o644); err != nil {
t.Fatal(err)
}
if !s.isStable(path) {
t.Fatal("expected stable file")
}
}
func TestIsStableMissingFile(t *testing.T) {
s, root := testScanner(t)
path := filepath.Join(root, "incoming", "missing.wav")
if s.isStable(path) {
t.Fatal("missing file should not be stable")
}
}