feat(T1.6, T1.7): comprehensive integration tests and audit logging
T1.6: Comprehensive Integration Tests for Concurrency - Add tests/concurrency_integration_test.go - Test concurrent workflows on shared resources - Test board validation concurrency - Test state tracking under concurrent access - Test snapshot creation and restoration concurrency - Test pause/resume under load - Test data consistency with concurrent access - Test network flakiness simulation - Test cross-workflow isolation - Benchmark concurrent snapshot and state operations - 15 integration tests, all passing T1.7: Immutable Audit Logging - Add internal/audit package for decision tracking - Implement AuditLogger with append-only JSONL logs - Log planner decisions with reasoning - Log judge verdicts with reasoning - Log implementer changes with file lists - Query by task ID (queryable by task) - Query by workflow ID - Query by actor (planner/judge/implementer) - Query by timestamp range - Full audit trail retrieval - Event counting and statistics - 14 audit tests, all passing Audit Features: - Immutable append-only JSONL logs - Event ID generation - Timestamp tracking (exact recovery point) - Full reasoning and context preservation - Metadata storage for extensibility - Thread-safe concurrent logging - Fast queries by task/workflow/actor/time Test Coverage: - 15 concurrency integration tests (workflows, board, state, snapshots) - 14 audit logging tests (decisions, verdicts, queries, immutability) - 29 total T1.6+T1.7 tests, all passing - Concurrent access patterns verified - Data consistency under load verified - Query functionality comprehensive T1 Milestone: 8/8 tasks COMPLETE (100%)
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AuditEvent represents an immutable audit log entry
|
||||
type AuditEvent struct {
|
||||
EventID string `json:"event_id"`
|
||||
EventType string `json:"event_type"` // "planner_decision", "judge_verdict", "implementer_change"
|
||||
WorkflowID string `json:"workflow_id"`
|
||||
TaskID string `json:"task_id"`
|
||||
Actor string `json:"actor"` // "planner", "judge", "implementer"
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Action string `json:"action"` // Description of what was decided/done
|
||||
Reasoning string `json:"reasoning"` // Why this decision was made
|
||||
Input map[string]interface{} `json:"input,omitempty"`
|
||||
Output map[string]interface{} `json:"output,omitempty"`
|
||||
Status string `json:"status"` // "success", "failure", "pending"
|
||||
Error string `json:"error,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// AuditLogger logs immutable audit events
|
||||
type AuditLogger struct {
|
||||
mu sync.Mutex
|
||||
basePath string
|
||||
logFile string
|
||||
}
|
||||
|
||||
// NewAuditLogger creates a new audit logger
|
||||
func NewAuditLogger(basePath string) *AuditLogger {
|
||||
return &AuditLogger{
|
||||
basePath: basePath,
|
||||
logFile: filepath.Join(basePath, "audit", "audit.jsonl"),
|
||||
}
|
||||
}
|
||||
|
||||
// LogEvent logs an audit event (immutable append-only)
|
||||
func (al *AuditLogger) LogEvent(event *AuditEvent) error {
|
||||
if event == nil {
|
||||
return fmt.Errorf("event cannot be nil")
|
||||
}
|
||||
|
||||
al.mu.Lock()
|
||||
defer al.mu.Unlock()
|
||||
|
||||
// Set timestamp if not already set
|
||||
if event.Timestamp.IsZero() {
|
||||
event.Timestamp = time.Now()
|
||||
}
|
||||
|
||||
// Generate event ID if not set
|
||||
if event.EventID == "" {
|
||||
event.EventID = fmt.Sprintf("%s-%d", event.WorkflowID, event.Timestamp.UnixNano())
|
||||
}
|
||||
|
||||
// Create audit directory if it doesn't exist
|
||||
if err := os.MkdirAll(filepath.Dir(al.logFile), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Marshal to JSON
|
||||
data, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Append to file (immutable log)
|
||||
f, err := os.OpenFile(al.logFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.Write(append(data, '\n'))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogPlannerDecision logs a planner decision
|
||||
func (al *AuditLogger) LogPlannerDecision(workflowID, taskID string, decision string, reasoning string, metadata map[string]interface{}) error {
|
||||
event := &AuditEvent{
|
||||
EventType: "planner_decision",
|
||||
WorkflowID: workflowID,
|
||||
TaskID: taskID,
|
||||
Actor: "planner",
|
||||
Timestamp: time.Now(),
|
||||
Action: decision,
|
||||
Reasoning: reasoning,
|
||||
Status: "success",
|
||||
Metadata: metadata,
|
||||
}
|
||||
return al.LogEvent(event)
|
||||
}
|
||||
|
||||
// LogJudgeVerdict logs a judge verdict
|
||||
func (al *AuditLogger) LogJudgeVerdict(workflowID, taskID string, verdict string, reasoning string, metadata map[string]interface{}) error {
|
||||
event := &AuditEvent{
|
||||
EventType: "judge_verdict",
|
||||
WorkflowID: workflowID,
|
||||
TaskID: taskID,
|
||||
Actor: "judge",
|
||||
Timestamp: time.Now(),
|
||||
Action: verdict,
|
||||
Reasoning: reasoning,
|
||||
Status: "success",
|
||||
Metadata: metadata,
|
||||
}
|
||||
return al.LogEvent(event)
|
||||
}
|
||||
|
||||
// LogImplementerChange logs an implementer change
|
||||
func (al *AuditLogger) LogImplementerChange(workflowID, taskID string, changeDesc string, filesModified []string, metadata map[string]interface{}) error {
|
||||
output := map[string]interface{}{
|
||||
"files_modified": filesModified,
|
||||
}
|
||||
|
||||
event := &AuditEvent{
|
||||
EventType: "implementer_change",
|
||||
WorkflowID: workflowID,
|
||||
TaskID: taskID,
|
||||
Actor: "implementer",
|
||||
Timestamp: time.Now(),
|
||||
Action: changeDesc,
|
||||
Output: output,
|
||||
Status: "success",
|
||||
Metadata: metadata,
|
||||
}
|
||||
return al.LogEvent(event)
|
||||
}
|
||||
|
||||
// QueryByTask retrieves all events for a specific task
|
||||
func (al *AuditLogger) QueryByTask(taskID string) ([]*AuditEvent, error) {
|
||||
al.mu.Lock()
|
||||
defer al.mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(al.logFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var events []*AuditEvent
|
||||
var inLine []byte
|
||||
|
||||
for _, ch := range data {
|
||||
if ch == '\n' {
|
||||
if len(inLine) > 0 {
|
||||
var event AuditEvent
|
||||
if err := json.Unmarshal(inLine, &event); err == nil {
|
||||
if event.TaskID == taskID {
|
||||
events = append(events, &event)
|
||||
}
|
||||
}
|
||||
}
|
||||
inLine = nil
|
||||
} else {
|
||||
inLine = append(inLine, ch)
|
||||
}
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// QueryByWorkflow retrieves all events for a specific workflow
|
||||
func (al *AuditLogger) QueryByWorkflow(workflowID string) ([]*AuditEvent, error) {
|
||||
al.mu.Lock()
|
||||
defer al.mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(al.logFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var events []*AuditEvent
|
||||
var inLine []byte
|
||||
|
||||
for _, ch := range data {
|
||||
if ch == '\n' {
|
||||
if len(inLine) > 0 {
|
||||
var event AuditEvent
|
||||
if err := json.Unmarshal(inLine, &event); err == nil {
|
||||
if event.WorkflowID == workflowID {
|
||||
events = append(events, &event)
|
||||
}
|
||||
}
|
||||
}
|
||||
inLine = nil
|
||||
} else {
|
||||
inLine = append(inLine, ch)
|
||||
}
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// QueryByActor retrieves all events by a specific actor
|
||||
func (al *AuditLogger) QueryByActor(actor string) ([]*AuditEvent, error) {
|
||||
al.mu.Lock()
|
||||
defer al.mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(al.logFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var events []*AuditEvent
|
||||
var inLine []byte
|
||||
|
||||
for _, ch := range data {
|
||||
if ch == '\n' {
|
||||
if len(inLine) > 0 {
|
||||
var event AuditEvent
|
||||
if err := json.Unmarshal(inLine, &event); err == nil {
|
||||
if event.Actor == actor {
|
||||
events = append(events, &event)
|
||||
}
|
||||
}
|
||||
}
|
||||
inLine = nil
|
||||
} else {
|
||||
inLine = append(inLine, ch)
|
||||
}
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// QueryByTimeRange retrieves events within a time range
|
||||
func (al *AuditLogger) QueryByTimeRange(start, end time.Time) ([]*AuditEvent, error) {
|
||||
al.mu.Lock()
|
||||
defer al.mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(al.logFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var events []*AuditEvent
|
||||
var inLine []byte
|
||||
|
||||
for _, ch := range data {
|
||||
if ch == '\n' {
|
||||
if len(inLine) > 0 {
|
||||
var event AuditEvent
|
||||
if err := json.Unmarshal(inLine, &event); err == nil {
|
||||
if event.Timestamp.After(start) && event.Timestamp.Before(end) {
|
||||
events = append(events, &event)
|
||||
}
|
||||
}
|
||||
}
|
||||
inLine = nil
|
||||
} else {
|
||||
inLine = append(inLine, ch)
|
||||
}
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// GetAuditTrail retrieves the full audit trail
|
||||
func (al *AuditLogger) GetAuditTrail() ([]*AuditEvent, error) {
|
||||
al.mu.Lock()
|
||||
defer al.mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(al.logFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var events []*AuditEvent
|
||||
var inLine []byte
|
||||
|
||||
for _, ch := range data {
|
||||
if ch == '\n' {
|
||||
if len(inLine) > 0 {
|
||||
var event AuditEvent
|
||||
if err := json.Unmarshal(inLine, &event); err == nil {
|
||||
events = append(events, &event)
|
||||
}
|
||||
}
|
||||
inLine = nil
|
||||
} else {
|
||||
inLine = append(inLine, ch)
|
||||
}
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// GetEventCount returns the total number of audit events
|
||||
func (al *AuditLogger) GetEventCount() (int, error) {
|
||||
events, err := al.GetAuditTrail()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(events), nil
|
||||
}
|
||||
Reference in New Issue
Block a user