Live Trading Boundary의 남은 account-sync/audit-trail 작업을 완료해 운영자 headless workflow와 roadmap 완료 후보 상태를 함께 반영한다.
664 lines
18 KiB
Go
664 lines
18 KiB
Go
package operator
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
|
|
)
|
|
|
|
func submittedLiveOrder(id string) *altv1.LiveOrder {
|
|
return &altv1.LiveOrder{
|
|
Id: id,
|
|
AccountId: "live-acct-1",
|
|
InstrumentId: "KRX:005930",
|
|
Side: "buy",
|
|
Type: "kis_best_limit",
|
|
Status: "submitted",
|
|
BrokerId: "broker-lo-1",
|
|
BrokerStatus: "SUBMITTED",
|
|
}
|
|
}
|
|
|
|
func canceledLiveOrder(id string) *altv1.LiveOrder {
|
|
o := submittedLiveOrder(id)
|
|
o.Status = "canceled"
|
|
o.BrokerStatus = "CANCELED"
|
|
return o
|
|
}
|
|
|
|
func TestRunSubmitLiveOrderConfirmationRequiredTypedError(t *testing.T) {
|
|
api := &fakeAPI{submitLiveOrderResp: &altv1.SubmitLiveOrderResponse{
|
|
Error: &altv1.ErrorInfo{Code: "invalid_request", Message: "operator confirmation is required"},
|
|
}}
|
|
url := startFakeAPIServer(t, api)
|
|
sc := &Scenario{
|
|
Name: "live_order_lifecycle",
|
|
Steps: []Step{
|
|
{
|
|
ID: "submit",
|
|
Action: ActionSubmitLiveOrder,
|
|
Request: Request{
|
|
AccountID: "live-acct-1",
|
|
InstrumentID: "KRX:005930",
|
|
Side: "buy",
|
|
Quantity: "1",
|
|
OrderType: "kis_best_limit",
|
|
OperatorConfirmed: false,
|
|
},
|
|
Expect: Expect{Status: "error", ErrorCode: "invalid_request"},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, code := runScenario(t, sc, url)
|
|
if code != codeOK {
|
|
t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
|
|
}
|
|
if !strings.Contains(out, "error.code=invalid_request") {
|
|
t.Errorf("output %q missing error.code=invalid_request", out)
|
|
}
|
|
}
|
|
|
|
func TestRunSubmitLiveOrderPreservesCustomOrderType(t *testing.T) {
|
|
order := submittedLiveOrder("lo-live-acct-1-1")
|
|
order.Type = "kis_best_limit"
|
|
api := &fakeAPI{submitLiveOrderResp: &altv1.SubmitLiveOrderResponse{Order: order}}
|
|
url := startFakeAPIServer(t, api)
|
|
sc := &Scenario{
|
|
Name: "live_order_lifecycle",
|
|
Steps: []Step{
|
|
{
|
|
ID: "submit",
|
|
Action: ActionSubmitLiveOrder,
|
|
Request: Request{
|
|
AccountID: "live-acct-1",
|
|
InstrumentID: "KRX:005930",
|
|
Side: "buy",
|
|
Quantity: "1",
|
|
OrderType: "kis_best_limit",
|
|
OperatorConfirmed: true,
|
|
OperatorID: "operator-1",
|
|
},
|
|
Expect: Expect{Status: "ok"},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, code := runScenario(t, sc, url)
|
|
if code != codeOK {
|
|
t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
|
|
}
|
|
if !strings.Contains(out, "live_order_id=lo-live-acct-1-1") {
|
|
t.Errorf("output %q missing live_order_id=lo-live-acct-1-1", out)
|
|
}
|
|
got := api.lastSubmitLiveOrderReq()
|
|
if got == nil {
|
|
t.Fatal("server did not receive a submit live order request")
|
|
}
|
|
if got.GetIntent().GetType() != "kis_best_limit" {
|
|
t.Errorf("server received order type = %q, want kis_best_limit", got.GetIntent().GetType())
|
|
}
|
|
if !got.GetOperatorConfirmation().GetConfirmed() {
|
|
t.Error("server received confirmed = false, want true")
|
|
}
|
|
}
|
|
|
|
func TestRunSubmitLiveOrderCancelGetInterpolation(t *testing.T) {
|
|
orderID := "lo-live-acct-1-1"
|
|
api := &fakeAPI{
|
|
submitLiveOrderResp: &altv1.SubmitLiveOrderResponse{Order: submittedLiveOrder(orderID)},
|
|
getLiveOrderResp: &altv1.GetLiveOrderResponse{Order: submittedLiveOrder(orderID)},
|
|
cancelLiveOrderResp: &altv1.CancelLiveOrderResponse{Order: canceledLiveOrder(orderID)},
|
|
}
|
|
url := startFakeAPIServer(t, api)
|
|
sc := &Scenario{
|
|
Name: "live_order_lifecycle",
|
|
Steps: []Step{
|
|
{
|
|
ID: "submit",
|
|
Action: ActionSubmitLiveOrder,
|
|
Request: Request{
|
|
AccountID: "live-acct-1",
|
|
InstrumentID: "KRX:005930",
|
|
Side: "buy",
|
|
Quantity: "1",
|
|
OrderType: "kis_best_limit",
|
|
OperatorConfirmed: true,
|
|
OperatorID: "operator-1",
|
|
},
|
|
Expect: Expect{Status: "ok", LiveOrderStatus: "submitted"},
|
|
},
|
|
{
|
|
ID: "get",
|
|
Action: ActionGetLiveOrder,
|
|
Request: Request{
|
|
AccountID: "live-acct-1",
|
|
LiveOrderID: "{{steps.submit.live_order.id}}",
|
|
},
|
|
Expect: Expect{Status: "ok"},
|
|
},
|
|
{
|
|
ID: "cancel",
|
|
Action: ActionCancelLiveOrder,
|
|
Request: Request{
|
|
AccountID: "live-acct-1",
|
|
LiveOrderID: "{{steps.submit.live_order.id}}",
|
|
},
|
|
Expect: Expect{Status: "ok", LiveOrderStatus: "canceled"},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, code := runScenario(t, sc, url)
|
|
if code != codeOK {
|
|
t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
|
|
}
|
|
for _, want := range []string{
|
|
"step=submit action=submit_live_order status=ok live_order_id=lo-live-acct-1-1 live_order_status=submitted",
|
|
"step=get action=get_live_order status=ok live_order_id=lo-live-acct-1-1",
|
|
"step=cancel action=cancel_live_order status=ok live_order_id=lo-live-acct-1-1 live_order_status=canceled",
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("output %q missing %q", out, want)
|
|
}
|
|
}
|
|
|
|
// cancel step must receive the interpolated order id, not the raw placeholder
|
|
got := api.lastCancelLiveOrderReq()
|
|
if got == nil {
|
|
t.Fatal("server did not receive a cancel live order request")
|
|
}
|
|
if got.GetLiveOrderId() != orderID {
|
|
t.Errorf("cancel received live_order_id = %q, want interpolated %q", got.GetLiveOrderId(), orderID)
|
|
}
|
|
}
|
|
|
|
func TestRunGetLiveRiskPolicy(t *testing.T) {
|
|
api := &fakeAPI{
|
|
getLiveRiskPolicyResp: &altv1.GetLiveRiskPolicyResponse{
|
|
Policy: &altv1.LiveRiskPolicy{MaxDailyOrders: 5, MaxOpenOrders: 3, AllowShortSelling: false},
|
|
},
|
|
}
|
|
url := startFakeAPIServer(t, api)
|
|
sc := &Scenario{
|
|
Name: "live_risk_kill_switch",
|
|
Steps: []Step{
|
|
{
|
|
ID: "get_policy",
|
|
Action: ActionGetLiveRiskPolicy,
|
|
Request: Request{AccountID: "live-acct-1"},
|
|
Expect: Expect{Status: "ok"},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, code := runScenario(t, sc, url)
|
|
if code != codeOK {
|
|
t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
|
|
}
|
|
if !strings.Contains(out, "max_daily_orders=5") {
|
|
t.Errorf("output %q missing max_daily_orders=5", out)
|
|
}
|
|
if !strings.Contains(out, "max_open_orders=3") {
|
|
t.Errorf("output %q missing max_open_orders=3", out)
|
|
}
|
|
if !strings.Contains(out, "allow_short_selling=false") {
|
|
t.Errorf("output %q missing allow_short_selling=false", out)
|
|
}
|
|
}
|
|
|
|
func TestRunGetLiveKillSwitch(t *testing.T) {
|
|
api := &fakeAPI{
|
|
getLiveKillSwitchResp: &altv1.GetLiveKillSwitchResponse{
|
|
State: &altv1.LiveKillSwitchState{Halted: true, Reason: "kill switch active by default"},
|
|
},
|
|
}
|
|
url := startFakeAPIServer(t, api)
|
|
sc := &Scenario{
|
|
Name: "live_risk_kill_switch",
|
|
Steps: []Step{
|
|
{
|
|
ID: "get_ks",
|
|
Action: ActionGetLiveKillSwitch,
|
|
Request: Request{AccountID: "live-acct-1"},
|
|
Expect: Expect{Status: "ok"},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, code := runScenario(t, sc, url)
|
|
if code != codeOK {
|
|
t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
|
|
}
|
|
if !strings.Contains(out, "kill_switch_halted=true") {
|
|
t.Errorf("output %q missing kill_switch_halted=true", out)
|
|
}
|
|
}
|
|
|
|
func TestRunSetLiveKillSwitchResume(t *testing.T) {
|
|
api := &fakeAPI{
|
|
setLiveKillSwitchResp: &altv1.SetLiveKillSwitchResponse{
|
|
State: &altv1.LiveKillSwitchState{Halted: false, Reason: "operator cleared"},
|
|
},
|
|
}
|
|
url := startFakeAPIServer(t, api)
|
|
sc := &Scenario{
|
|
Name: "live_risk_kill_switch",
|
|
Steps: []Step{
|
|
{
|
|
ID: "set_ks",
|
|
Action: ActionSetLiveKillSwitch,
|
|
Request: Request{
|
|
AccountID: "live-acct-1",
|
|
Halted: false,
|
|
KillSwitchReason: "operator cleared",
|
|
},
|
|
Expect: Expect{Status: "ok"},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, code := runScenario(t, sc, url)
|
|
if code != codeOK {
|
|
t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
|
|
}
|
|
if !strings.Contains(out, "kill_switch_halted=false") {
|
|
t.Errorf("output %q missing kill_switch_halted=false", out)
|
|
}
|
|
|
|
got := api.lastSetLiveKillSwitchReq()
|
|
if got == nil {
|
|
t.Fatal("server did not receive a set_live_kill_switch request")
|
|
}
|
|
if got.GetHalted() != false {
|
|
t.Errorf("server received halted = true, want false")
|
|
}
|
|
if got.GetReason() != "operator cleared" {
|
|
t.Errorf("server received reason = %q, want %q", got.GetReason(), "operator cleared")
|
|
}
|
|
}
|
|
|
|
func TestLiveRiskKillSwitchFixtureIsValid(t *testing.T) {
|
|
yamlPath := filepath.Join("..", "..", "testdata", "operator", "live_risk_kill_switch.yaml")
|
|
if _, err := LoadScenario(yamlPath); err != nil {
|
|
t.Fatalf("live_risk_kill_switch.yaml failed validation: %v", err)
|
|
}
|
|
|
|
expectedPath := filepath.Join("..", "..", "testdata", "operator", "expected", "live_risk_kill_switch.jsonl")
|
|
f, err := os.Open(expectedPath)
|
|
if err != nil {
|
|
t.Fatalf("open expected fixture: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
sawPolicy := false
|
|
sawKillSwitch := false
|
|
sawRiskDecision := false
|
|
sawSummary := false
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
var rec map[string]any
|
|
if err := json.Unmarshal([]byte(line), &rec); err != nil {
|
|
t.Fatalf("expected fixture line not valid JSON: %v", err)
|
|
}
|
|
if rec["action"] == "get_live_risk_policy" && rec["max_daily_orders"] != nil {
|
|
sawPolicy = true
|
|
}
|
|
if rec["kill_switch_halted"] != nil {
|
|
sawKillSwitch = true
|
|
}
|
|
if rec["risk_decision_allowed"] != nil {
|
|
sawRiskDecision = true
|
|
}
|
|
if rec["type"] == "summary" {
|
|
sawSummary = true
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
t.Fatalf("scan expected fixture: %v", err)
|
|
}
|
|
if !sawPolicy {
|
|
t.Error("expected fixture missing a get_live_risk_policy line with max_daily_orders")
|
|
}
|
|
if !sawKillSwitch {
|
|
t.Error("expected fixture missing a kill_switch_halted field")
|
|
}
|
|
if !sawRiskDecision {
|
|
t.Error("expected fixture missing a risk_decision_allowed field (risk-blocked submit evidence)")
|
|
}
|
|
if !sawSummary {
|
|
t.Error("expected fixture missing a summary line")
|
|
}
|
|
}
|
|
|
|
func TestRunSubmitLiveOrderRiskBlockedIncludesDecision(t *testing.T) {
|
|
api := &fakeAPI{submitLiveOrderResp: &altv1.SubmitLiveOrderResponse{
|
|
Order: &altv1.LiveOrder{
|
|
Id: "lo-live-acct-1-1",
|
|
Status: "rejected",
|
|
BrokerId: "",
|
|
},
|
|
Error: &altv1.ErrorInfo{Code: "invalid_request", Message: "live order blocked by risk policy: kill switch is active"},
|
|
RiskDecision: &altv1.LiveRiskDecision{Allowed: false, Reason: "kill switch is active"},
|
|
}}
|
|
url := startFakeAPIServer(t, api)
|
|
sc := &Scenario{
|
|
Name: "live_risk_kill_switch",
|
|
Steps: []Step{
|
|
{
|
|
ID: "submit_risk_blocked",
|
|
Action: ActionSubmitLiveOrder,
|
|
Request: Request{
|
|
AccountID: "live-acct-1",
|
|
InstrumentID: "KRX:005930",
|
|
Side: "buy",
|
|
Quantity: "1",
|
|
OrderType: "market",
|
|
OperatorConfirmed: true,
|
|
OperatorID: "operator-1",
|
|
},
|
|
Expect: Expect{Status: "error", ErrorCode: "invalid_request"},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, code := runScenario(t, sc, url)
|
|
if code != codeOK {
|
|
t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
|
|
}
|
|
if !strings.Contains(out, "live_order_id=lo-live-acct-1-1") {
|
|
t.Errorf("output %q missing live_order_id=lo-live-acct-1-1", out)
|
|
}
|
|
if !strings.Contains(out, "live_order_status=rejected") {
|
|
t.Errorf("output %q missing live_order_status=rejected", out)
|
|
}
|
|
if !strings.Contains(out, "risk_decision_allowed=false") {
|
|
t.Errorf("output %q missing risk_decision_allowed=false", out)
|
|
}
|
|
if !strings.Contains(out, "kill switch is active") {
|
|
t.Errorf("output %q missing kill switch reason", out)
|
|
}
|
|
}
|
|
|
|
func TestLiveOrderLifecycleFixtureIsValid(t *testing.T) {
|
|
yamlPath := filepath.Join("..", "..", "testdata", "operator", "live_order_lifecycle.yaml")
|
|
if _, err := LoadScenario(yamlPath); err != nil {
|
|
t.Fatalf("live_order_lifecycle.yaml failed validation: %v", err)
|
|
}
|
|
|
|
expectedPath := filepath.Join("..", "..", "testdata", "operator", "expected", "live_order_lifecycle.jsonl")
|
|
f, err := os.Open(expectedPath)
|
|
if err != nil {
|
|
t.Fatalf("open expected fixture: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
sawLiveOrderID := false
|
|
sawCanceled := false
|
|
sawSummary := false
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
var rec map[string]any
|
|
if err := json.Unmarshal([]byte(line), &rec); err != nil {
|
|
t.Fatalf("expected fixture line not valid JSON: %v", err)
|
|
}
|
|
if rec["live_order_id"] != nil {
|
|
sawLiveOrderID = true
|
|
}
|
|
if rec["live_order_status"] == "canceled" {
|
|
sawCanceled = true
|
|
}
|
|
if rec["type"] == "summary" {
|
|
sawSummary = true
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
t.Fatalf("scan expected fixture: %v", err)
|
|
}
|
|
if !sawLiveOrderID {
|
|
t.Error("expected fixture missing a live_order_id line")
|
|
}
|
|
if !sawCanceled {
|
|
t.Error("expected fixture missing a live_order_status=canceled transition")
|
|
}
|
|
if !sawSummary {
|
|
t.Error("expected fixture missing a summary line")
|
|
}
|
|
}
|
|
|
|
func TestRunSyncLiveAccount(t *testing.T) {
|
|
snap := &altv1.LiveAccountSnapshot{
|
|
AccountId: "op-alias-001",
|
|
Broker: "kis",
|
|
SyncedAtUnixMs: 1749340800000,
|
|
Cash: []*altv1.LiveCashBalance{
|
|
{Currency: altv1.Currency_CURRENCY_KRW, Amount: &altv1.Decimal{Value: "5000000"}},
|
|
},
|
|
}
|
|
api := &fakeAPI{
|
|
syncLiveAccountResp: &altv1.SyncLiveAccountResponse{Snapshot: snap},
|
|
}
|
|
url := startFakeAPIServer(t, api)
|
|
sc := &Scenario{
|
|
Name: "live_account_sync_test",
|
|
Steps: []Step{
|
|
{
|
|
ID: "sync",
|
|
Action: ActionSyncLiveAccount,
|
|
Request: Request{AccountID: "op-alias-001"},
|
|
Expect: Expect{Status: "ok"},
|
|
},
|
|
},
|
|
}
|
|
out, code := runScenario(t, sc, url)
|
|
if code != 0 {
|
|
t.Fatalf("expected exit 0, got %d\noutput:\n%s", code, out)
|
|
}
|
|
req := api.lastSyncLiveAccountReq()
|
|
if req == nil {
|
|
t.Fatal("expected server to receive SyncLiveAccountRequest")
|
|
}
|
|
if req.GetAccountId() != "op-alias-001" {
|
|
t.Errorf("account_id mismatch: %q", req.GetAccountId())
|
|
}
|
|
}
|
|
|
|
func TestRunGetLiveAccountSnapshot(t *testing.T) {
|
|
snap := &altv1.LiveAccountSnapshot{
|
|
AccountId: "op-alias-001",
|
|
Broker: "kis",
|
|
SyncedAtUnixMs: 1749340800000,
|
|
}
|
|
api := &fakeAPI{
|
|
getLiveAccountSnapResp: &altv1.GetLiveAccountSnapshotResponse{Snapshot: snap},
|
|
}
|
|
url := startFakeAPIServer(t, api)
|
|
sc := &Scenario{
|
|
Name: "live_account_get_test",
|
|
Steps: []Step{
|
|
{
|
|
ID: "get",
|
|
Action: ActionGetLiveAccountSnapshot,
|
|
Request: Request{AccountID: "op-alias-001"},
|
|
Expect: Expect{Status: "ok"},
|
|
},
|
|
},
|
|
}
|
|
out, code := runScenario(t, sc, url)
|
|
if code != 0 {
|
|
t.Fatalf("expected exit 0, got %d\noutput:\n%s", code, out)
|
|
}
|
|
req := api.lastGetLiveAccountSnapReq()
|
|
if req == nil {
|
|
t.Fatal("expected server to receive GetLiveAccountSnapshotRequest")
|
|
}
|
|
if req.GetAccountId() != "op-alias-001" {
|
|
t.Errorf("account_id mismatch: %q", req.GetAccountId())
|
|
}
|
|
}
|
|
|
|
func TestRunSyncLiveAccountValidationRejectsMissingAccountID(t *testing.T) {
|
|
sc := Scenario{
|
|
Name: "live_account_missing_id",
|
|
Steps: []Step{
|
|
{
|
|
ID: "sync",
|
|
Action: ActionSyncLiveAccount,
|
|
},
|
|
},
|
|
}
|
|
err := sc.Validate()
|
|
if err == nil {
|
|
t.Fatal("expected validation error for missing account_id")
|
|
}
|
|
}
|
|
|
|
func TestRunGetLiveAccountSnapshotValidationRejectsMissingAccountID(t *testing.T) {
|
|
sc := Scenario{
|
|
Name: "live_account_snap_missing_id",
|
|
Steps: []Step{
|
|
{
|
|
ID: "get",
|
|
Action: ActionGetLiveAccountSnapshot,
|
|
},
|
|
},
|
|
}
|
|
err := sc.Validate()
|
|
if err == nil {
|
|
t.Fatal("expected validation error for missing account_id")
|
|
}
|
|
}
|
|
|
|
func TestRunListLiveAuditEvents(t *testing.T) {
|
|
api := &fakeAPI{
|
|
listLiveAuditEventsResp: &altv1.ListLiveAuditEventsResponse{
|
|
Events: []*altv1.LiveAuditEvent{
|
|
{EventId: "aud-1", Type: "submit_confirmed", AccountId: "op-audit-test-001", OrderId: "lo-1"},
|
|
},
|
|
},
|
|
}
|
|
url := startFakeAPIServer(t, api)
|
|
sc := &Scenario{
|
|
Name: "live_audit_query",
|
|
Steps: []Step{
|
|
{
|
|
ID: "list_audit",
|
|
Action: ActionListLiveAuditEvents,
|
|
Request: Request{AccountID: "op-audit-test-001"},
|
|
Expect: Expect{Status: "ok"},
|
|
},
|
|
},
|
|
}
|
|
out, code := runScenario(t, sc, url)
|
|
if code != codeOK {
|
|
t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
|
|
}
|
|
if !strings.Contains(out, "audit_event_count=1") {
|
|
t.Errorf("output %q missing audit_event_count=1", out)
|
|
}
|
|
req := api.lastListLiveAuditEventsReq()
|
|
if req == nil {
|
|
t.Fatal("server did not receive ListLiveAuditEventsRequest")
|
|
}
|
|
if req.GetAccountId() != "op-audit-test-001" {
|
|
t.Errorf("account_id mismatch: %q", req.GetAccountId())
|
|
}
|
|
}
|
|
|
|
func TestLiveAuditQueryFixtureIsValid(t *testing.T) {
|
|
yamlPath := filepath.Join("..", "..", "testdata", "operator", "live_audit_query.yaml")
|
|
if _, err := LoadScenario(yamlPath); err != nil {
|
|
t.Fatalf("live_audit_query.yaml failed validation: %v", err)
|
|
}
|
|
|
|
expectedPath := filepath.Join("..", "..", "testdata", "operator", "expected", "live_audit_query.jsonl")
|
|
f, err := os.Open(expectedPath)
|
|
if err != nil {
|
|
t.Fatalf("open expected fixture: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
sawAuditCount := false
|
|
sawSummary := false
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
var rec map[string]any
|
|
if err := json.Unmarshal([]byte(line), &rec); err != nil {
|
|
t.Fatalf("expected fixture line not valid JSON: %v", err)
|
|
}
|
|
if rec["action"] == "list_live_audit_events" && rec["audit_event_count"] != nil {
|
|
sawAuditCount = true
|
|
}
|
|
if rec["type"] == "summary" {
|
|
sawSummary = true
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
t.Fatalf("scan expected fixture: %v", err)
|
|
}
|
|
if !sawAuditCount {
|
|
t.Error("expected fixture missing a list_live_audit_events line with audit_event_count")
|
|
}
|
|
if !sawSummary {
|
|
t.Error("expected fixture missing a summary line")
|
|
}
|
|
}
|
|
|
|
func TestRunListLiveAuditEventsValidationRejectsMissingAccountID(t *testing.T) {
|
|
sc := Scenario{
|
|
Name: "live_audit_missing_id",
|
|
Steps: []Step{
|
|
{
|
|
ID: "list",
|
|
Action: ActionListLiveAuditEvents,
|
|
},
|
|
},
|
|
}
|
|
err := sc.Validate()
|
|
if err == nil {
|
|
t.Fatal("expected validation error for missing account_id")
|
|
}
|
|
}
|
|
|
|
func TestEvaluateLiveAccountTypedError(t *testing.T) {
|
|
ev := StepEvent{Action: string(ActionSyncLiveAccount)}
|
|
step := Step{Expect: Expect{Status: "ok"}}
|
|
errInfo := &altv1.ErrorInfo{Code: "not_found", Message: "snapshot not found"}
|
|
ev2, code := evaluateLiveAccount(ev, step, errInfo, nil)
|
|
if code == codeOK {
|
|
t.Error("expected non-OK code for typed error")
|
|
}
|
|
if ev2.ErrorCode != "not_found" {
|
|
t.Errorf("error_code mismatch: %q", ev2.ErrorCode)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateLiveAccountExpectedError(t *testing.T) {
|
|
ev := StepEvent{Action: string(ActionGetLiveAccountSnapshot)}
|
|
step := Step{Expect: Expect{Status: "error", ErrorCode: "not_found"}}
|
|
errInfo := &altv1.ErrorInfo{Code: "not_found", Message: "snapshot not found"}
|
|
ev2, code := evaluateLiveAccount(ev, step, errInfo, nil)
|
|
if code != codeOK {
|
|
t.Errorf("expected codeOK for matched typed error, got %d", code)
|
|
}
|
|
if ev2.Status != statusOK {
|
|
t.Errorf("expected status ok, got %q", ev2.Status)
|
|
}
|
|
}
|