승인된 execution preset을 Edge 조정 경계와 Node workspace/tool 실행 경계로 연결해 단일 요청 수명주기와 관측 계약을 일관되게 처리한다.
189 lines
6.4 KiB
Go
189 lines
6.4 KiB
Go
package workspace
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zaptest/observer"
|
|
|
|
iop "iop/proto/gen/iop"
|
|
)
|
|
|
|
func TestWorkspaceObservation(t *testing.T) {
|
|
root := t.TempDir()
|
|
core, logs := observer.New(zap.InfoLevel)
|
|
runtime, err := NewRuntime([]*iop.WorkspaceConfig{testWorkspaceConfig(root)}, "darwin", zap.New(core))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = runtime.Close() })
|
|
if _, err := runtime.Open(testRequestAuthority("request-observation")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const sentinel = "SECRET_PATH_COMMAND_OUTPUT_BEARER"
|
|
if result := runtime.Write("request-observation", sentinel, []byte(sentinel)); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("write result = %+v", result)
|
|
}
|
|
cleanup := runtime.Cleanup(t.Context(), "request-observation")
|
|
if cleanup.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("cleanup result = %+v", cleanup)
|
|
}
|
|
|
|
allowed := map[string]bool{
|
|
"correlation": true, "event": true, "operation": true, "outcome": true, "error_code": true,
|
|
"duration_ms": true, "truncated": true, "process_count": true, "artifact_count": true,
|
|
}
|
|
entries := logs.All()
|
|
if len(entries) != 2 {
|
|
t.Fatalf("workspace logs = %d, want 2", len(entries))
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.Message != workspaceObservationLogKey {
|
|
t.Fatalf("log message = %q", entry.Message)
|
|
}
|
|
if strings.Contains(strings.ToLower(fmt.Sprint(entry.ContextMap()["correlation"])), "secret") {
|
|
t.Fatalf("secret correlation leaked: %+v", entry)
|
|
}
|
|
if len(entry.Context) != len(allowed) {
|
|
t.Fatalf("log field count = %d, want %d", len(entry.Context), len(allowed))
|
|
}
|
|
for _, field := range entry.Context {
|
|
if !allowed[field.Key] {
|
|
t.Fatalf("unexpected log key %q", field.Key)
|
|
}
|
|
if strings.Contains(strings.ToLower(field.String), "secret") {
|
|
t.Fatalf("secret sentinel leaked in %q", field.Key)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWorkspaceObservationFailureIsolation(t *testing.T) {
|
|
runtime, err := NewRuntime([]*iop.WorkspaceConfig{testWorkspaceConfig(t.TempDir())}, "darwin", zap.NewNop())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = runtime.Close() })
|
|
if _, err := runtime.Open(testRequestAuthority("request-failure-isolation")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runtime.observer = &workspaceSafeObserver{inner: workspaceObserverFunc(func(workspaceObservation) error {
|
|
panic("observer panic")
|
|
})}
|
|
if result := runtime.Write("request-failure-isolation", "result.txt", []byte("kept")); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("write changed by observer panic: %+v", result)
|
|
}
|
|
if cleanup := runtime.Cleanup(t.Context(), "request-failure-isolation"); cleanup.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("cleanup changed by observer panic: %+v", cleanup)
|
|
}
|
|
if got := runtime.observer.failureCount(); got != 2 {
|
|
t.Fatalf("isolated panic failures = %d, want 2", got)
|
|
}
|
|
|
|
runtime.observer = &workspaceSafeObserver{inner: workspaceObserverFunc(func(workspaceObservation) error { return errors.New("observer error") })}
|
|
if result := runtime.Write("unknown-request", "result.txt", nil); result.Code != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST {
|
|
t.Fatalf("expected normal invalid result, got %+v", result)
|
|
}
|
|
if got := runtime.observer.failureCount(); got != 1 {
|
|
t.Fatalf("isolated error failures = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
type workspaceObserverFunc func(workspaceObservation) error
|
|
|
|
func (fn workspaceObserverFunc) Emit(observation workspaceObservation) error { return fn(observation) }
|
|
|
|
type capturingObserver struct {
|
|
mu sync.Mutex
|
|
observations []workspaceObservation
|
|
}
|
|
|
|
func (c *capturingObserver) Emit(obs workspaceObservation) error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.observations = append(c.observations, obs)
|
|
return nil
|
|
}
|
|
|
|
func TestWorkspaceObservationCorrelationSurvivesCleanupOverlap(t *testing.T) {
|
|
root := t.TempDir()
|
|
runtime, err := NewRuntime([]*iop.WorkspaceConfig{testWorkspaceConfig(root)}, "darwin", zap.NewNop())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = runtime.Close() })
|
|
if _, err := runtime.Open(testRequestAuthority("request-cleanup-overlap")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request, err := runtime.Request("request-cleanup-overlap")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expectedCorrelation := request.correlation
|
|
if expectedCorrelation == "" {
|
|
t.Fatalf("expected non-empty correlation at open, got empty")
|
|
}
|
|
capturer := &capturingObserver{}
|
|
runtime.observer = &workspaceSafeObserver{inner: capturer}
|
|
entered := make(chan struct{})
|
|
release := make(chan struct{})
|
|
request.entry.beforeRename = func() error {
|
|
close(entered)
|
|
<-release
|
|
return nil
|
|
}
|
|
writeDone := make(chan Result, 1)
|
|
go func() {
|
|
writeDone <- runtime.Write("request-cleanup-overlap", "result.txt", []byte("overlap"))
|
|
}()
|
|
<-entered
|
|
cleanup := runtime.Cleanup(t.Context(), "request-cleanup-overlap")
|
|
if cleanup.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("cleanup result = %+v, want success", cleanup)
|
|
}
|
|
close(release)
|
|
writeResult := <-writeDone
|
|
if writeResult.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("write after cleanup overlap = %+v, want success", writeResult)
|
|
}
|
|
|
|
capturer.mu.Lock()
|
|
defer capturer.mu.Unlock()
|
|
if len(capturer.observations) != 2 {
|
|
t.Fatalf("observations count = %d, want 2 (1 cleanup + 1 tool)", len(capturer.observations))
|
|
}
|
|
var cleanupCount, toolCount int
|
|
var cleanupCorrelation, toolCorrelation string
|
|
for _, obs := range capturer.observations {
|
|
switch obs.event {
|
|
case workspaceObservationCleanup:
|
|
cleanupCount++
|
|
cleanupCorrelation = obs.correlation
|
|
case workspaceObservationTool:
|
|
toolCount++
|
|
toolCorrelation = obs.correlation
|
|
}
|
|
}
|
|
if cleanupCount != 1 {
|
|
t.Fatalf("cleanup observation count = %d, want 1", cleanupCount)
|
|
}
|
|
if toolCount != 1 {
|
|
t.Fatalf("tool observation count = %d, want 1", toolCount)
|
|
}
|
|
if cleanupCorrelation == "" {
|
|
t.Fatalf("cleanup correlation is empty")
|
|
}
|
|
if toolCorrelation == "" {
|
|
t.Fatalf("tool correlation is empty after cleanup overlap")
|
|
}
|
|
if cleanupCorrelation != toolCorrelation {
|
|
t.Fatalf("cleanup correlation=%q != tool correlation=%q, want shared correlation", cleanupCorrelation, toolCorrelation)
|
|
}
|
|
if cleanupCorrelation != expectedCorrelation {
|
|
t.Fatalf("correlation=%q != expected=%q, want immutable request-local correlation", cleanupCorrelation, expectedCorrelation)
|
|
}
|
|
}
|