gomcp/internal/infrastructure/hardware/leash_test.go

233 lines
6.1 KiB
Go

// Copyright 2026 Syntrex Lab. All rights reserved.
// Use of this source code is governed by an Apache-2.0 license
// that can be found in the LICENSE file.
package hardware
import (
"context"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/syntrex-lab/gomcp/internal/domain/alert"
)
func testConfig(dir string) LeashConfig {
return LeashConfig{
KeyPath: filepath.Join(dir, ".sentinel_key"),
LeashPath: filepath.Join(dir, ".sentinel_leash"),
CheckInterval: 100 * time.Millisecond,
MissThreshold: 3,
SignalDir: dir,
}
}
func TestLeash_ArmedWhenKeyExists(t *testing.T) {
dir := t.TempDir()
cfg := testConfig(dir)
os.WriteFile(cfg.KeyPath, nil, 0o644)
bus := alert.NewBus(10)
leash := NewLeash(cfg, bus, nil, nil)
ctx, cancel := context.WithCancel(context.Background())
go leash.Start(ctx)
time.Sleep(120 * time.Millisecond)
assert.Equal(t, LeashArmed, leash.Status())
cancel()
}
func TestLeash_TriggersOnMissingKey(t *testing.T) {
dir := t.TempDir()
cfg := testConfig(dir)
os.WriteFile(cfg.KeyPath, nil, 0o644)
var triggered atomic.Int32
bus := alert.NewBus(10)
leash := NewLeash(cfg, bus, nil, func() { triggered.Add(1) })
ctx, cancel := context.WithCancel(context.Background())
go leash.Start(ctx)
time.Sleep(120 * time.Millisecond)
os.Remove(cfg.KeyPath)
time.Sleep(350 * time.Millisecond)
assert.GreaterOrEqual(t, triggered.Load(), int32(1))
assert.Equal(t, LeashTriggered, leash.Status())
cancel()
}
func TestLeash_ReArmsWhenKeyRestored(t *testing.T) {
dir := t.TempDir()
cfg := testConfig(dir)
os.WriteFile(cfg.KeyPath, nil, 0o644)
bus := alert.NewBus(10)
leash := NewLeash(cfg, bus, nil, nil)
ctx, cancel := context.WithCancel(context.Background())
go leash.Start(ctx)
time.Sleep(120 * time.Millisecond)
os.Remove(cfg.KeyPath)
time.Sleep(120 * time.Millisecond)
os.WriteFile(cfg.KeyPath, nil, 0o644)
time.Sleep(120 * time.Millisecond)
assert.Equal(t, LeashArmed, leash.Status())
cancel()
}
func TestLeash_SignalExtract(t *testing.T) {
dir := t.TempDir()
cfg := testConfig(dir)
os.WriteFile(cfg.KeyPath, nil, 0o644)
var extracted atomic.Int32
bus := alert.NewBus(10)
leash := NewLeash(cfg, bus, func() { extracted.Add(1) }, nil)
ctx, cancel := context.WithCancel(context.Background())
go leash.Start(ctx)
time.Sleep(120 * time.Millisecond)
os.WriteFile(filepath.Join(dir, "signal_extract"), nil, 0o644)
time.Sleep(120 * time.Millisecond)
assert.GreaterOrEqual(t, extracted.Load(), int32(1))
cancel()
}
func TestLeash_SignalApoptosis(t *testing.T) {
dir := t.TempDir()
cfg := testConfig(dir)
os.WriteFile(cfg.KeyPath, nil, 0o644)
var triggered atomic.Int32
bus := alert.NewBus(10)
leash := NewLeash(cfg, bus, nil, func() { triggered.Add(1) })
ctx, cancel := context.WithCancel(context.Background())
go leash.Start(ctx)
time.Sleep(120 * time.Millisecond)
os.WriteFile(filepath.Join(dir, "signal_apoptosis"), nil, 0o644)
time.Sleep(120 * time.Millisecond)
assert.GreaterOrEqual(t, triggered.Load(), int32(1))
assert.Equal(t, LeashTriggered, leash.Status())
cancel()
}
func TestLeashStatus_String(t *testing.T) {
assert.Equal(t, "DISARMED", LeashDisarmed.String())
assert.Equal(t, "ARMED", LeashArmed.String())
assert.Equal(t, "TRIGGERED", LeashTriggered.String())
}
// --- v3.2 State Machine Tests ---
func TestParseMode(t *testing.T) {
assert.Equal(t, ModeArmed, ParseMode("ARMED"))
assert.Equal(t, ModeArmed, ParseMode("armed"))
assert.Equal(t, ModeArmed, ParseMode("anything"))
assert.Equal(t, ModeArmed, ParseMode(""))
assert.Equal(t, ModeZeroG, ParseMode("ZERO-G"))
assert.Equal(t, ModeZeroG, ParseMode("ZEROG"))
assert.Equal(t, ModeZeroG, ParseMode("ZERO_G"))
assert.Equal(t, ModeZeroG, ParseMode(" zero-g "))
assert.Equal(t, ModeSafe, ParseMode("SAFE"))
assert.Equal(t, ModeSafe, ParseMode("READ-ONLY"))
assert.Equal(t, ModeSafe, ParseMode("READONLY"))
}
func TestSystemMode_String(t *testing.T) {
assert.Equal(t, "ARMED", ModeArmed.String())
assert.Equal(t, "ZERO-G", ModeZeroG.String())
assert.Equal(t, "SAFE", ModeSafe.String())
}
func TestLeash_ModeDefault(t *testing.T) {
dir := t.TempDir()
cfg := testConfig(dir)
os.WriteFile(cfg.KeyPath, nil, 0o644)
bus := alert.NewBus(10)
leash := NewLeash(cfg, bus, nil, nil)
ctx, cancel := context.WithCancel(context.Background())
go leash.Start(ctx)
time.Sleep(120 * time.Millisecond)
// No .sentinel_leash file → ModeArmed.
assert.Equal(t, ModeArmed, leash.Mode())
cancel()
}
func TestLeash_ModeZeroG(t *testing.T) {
dir := t.TempDir()
cfg := testConfig(dir)
os.WriteFile(cfg.KeyPath, nil, 0o644)
os.WriteFile(cfg.LeashPath, []byte("ZERO-G"), 0o644)
bus := alert.NewBus(10)
leash := NewLeash(cfg, bus, nil, nil)
ctx, cancel := context.WithCancel(context.Background())
go leash.Start(ctx)
time.Sleep(120 * time.Millisecond)
assert.Equal(t, ModeZeroG, leash.Mode())
cancel()
}
func TestLeash_ModeSafe(t *testing.T) {
dir := t.TempDir()
cfg := testConfig(dir)
os.WriteFile(cfg.KeyPath, nil, 0o644)
os.WriteFile(cfg.LeashPath, []byte("SAFE"), 0o644)
bus := alert.NewBus(10)
leash := NewLeash(cfg, bus, nil, nil)
ctx, cancel := context.WithCancel(context.Background())
go leash.Start(ctx)
time.Sleep(120 * time.Millisecond)
assert.Equal(t, ModeSafe, leash.Mode())
cancel()
}
func TestLeash_ModeTransition(t *testing.T) {
dir := t.TempDir()
cfg := testConfig(dir)
os.WriteFile(cfg.KeyPath, nil, 0o644)
var transitions atomic.Int32
bus := alert.NewBus(10)
leash := NewLeash(cfg, bus, nil, nil)
leash.SetModeChangeCallback(func(m SystemMode) {
transitions.Add(1)
})
ctx, cancel := context.WithCancel(context.Background())
go leash.Start(ctx)
time.Sleep(120 * time.Millisecond)
// ARMED → ZERO-G.
require.NoError(t, os.WriteFile(cfg.LeashPath, []byte("ZERO-G"), 0o644))
time.Sleep(200 * time.Millisecond)
assert.Equal(t, ModeZeroG, leash.Mode())
// ZERO-G → SAFE.
require.NoError(t, os.WriteFile(cfg.LeashPath, []byte("SAFE"), 0o644))
time.Sleep(200 * time.Millisecond)
assert.Equal(t, ModeSafe, leash.Mode())
assert.GreaterOrEqual(t, transitions.Load(), int32(2))
cancel()
}