package runtime import ( "context" "database/sql" "encoding/json" "os" "path/filepath" "strings" "testing" "browser.local/run/protocol" ) func TestSQLiteSchemaProbeExecutesOnlyAgainstScopedLogicalTarget(t *testing.T) { root := t.TempDir() assignment := sqliteSchemaProbeAssignment() scope, err := NewWorkspaceResolver(root).Scope(assignment.ServerInstanceID, assignment.ExecutionInput.WorkspaceScope) if err != nil { t.Fatalf("create workspace scope: %v", err) } databasePath := filepath.Join(scope, "databases", "current.db") createSQLiteProbeFixture(t, databasePath) database, err := sql.Open(sqliteSchemaProbeDriver, "file:"+databasePath+"?mode=ro") if err != nil { t.Fatal(err) } if _, err := inspectSQLiteSchema(context.Background(), database, assignment.ExecutionInput.SQLiteSchemaProbe.Limits); err != nil { t.Fatalf("inspect fixture directly: %v", err) } database.Close() result := NewSQLiteSchemaProbeExecutor(root).Execute(context.Background(), assignment) if result.State != lifecycleResultStateSucceeded || result.ExecutionResult.SQLiteSchemaProbe == nil { t.Fatalf("expected successful probe envelope, got %+v", result) } probe := result.ExecutionResult.SQLiteSchemaProbe if probe.JobID != assignment.JobID || probe.Binding != assignment.ExecutionInput.SQLiteSchemaProbe.Binding || probe.Status != "succeeded" || probe.ObservedAt.IsZero() || probe.ResultDigest == "" || probe.SourceFingerprint == "" || probe.SchemaFingerprint == "" { t.Fatalf("unexpected probe identity envelope: %+v", probe) } if len(probe.Objects) != 2 || probe.Objects[0].ApproximateRows == nil || len(probe.Objects[1].SampleFingerprints) == 0 { t.Fatalf("expected bounded table evidence, got %+v", probe.Objects) } serialized := mustJSON(t, probe) for _, value := range []string{"current.db", "members", "alpha", root, "SELECT", "sqlite:"} { if strings.Contains(serialized, value) { t.Fatalf("probe leaked protected source material %q: %s", value, serialized) } } if result.ExecutionResult.Content != "" { t.Fatalf("probe must not return content: %+v", result.ExecutionResult) } } func TestSQLiteSchemaProbeRejectsUnscopedOrUnsafeRequests(t *testing.T) { assignment := sqliteSchemaProbeAssignment() assignment.TargetKey = "/tmp/current.db" result := NewSQLiteSchemaProbeExecutor(t.TempDir()).Execute(context.Background(), assignment) if result.ErrorCode != "invalid_request" || result.ExecutionResult.SQLiteSchemaProbe == nil { t.Fatalf("expected safe invalid probe failure, got %+v", result) } assignment = sqliteSchemaProbeAssignment() assignment.ExecutionInput.SQLiteSchemaProbe.Binding.DatabaseIdentity = "C:/host/path" result = NewSQLiteSchemaProbeExecutor(t.TempDir()).Execute(context.Background(), assignment) if result.ErrorCode != "invalid_request" || strings.Contains(result.Message, "C:/") { t.Fatalf("expected redacted binding rejection, got %+v", result) } } func TestSQLiteSchemaProbeEnforcesResultLimit(t *testing.T) { root := t.TempDir() assignment := sqliteSchemaProbeAssignment() scope, err := NewWorkspaceResolver(root).Scope(assignment.ServerInstanceID, assignment.ExecutionInput.WorkspaceScope) if err != nil { t.Fatal(err) } createSQLiteProbeFixture(t, filepath.Join(scope, "databases", "current.db")) assignment.ExecutionInput.SQLiteSchemaProbe.Limits.MaxResultBytes = 1 result := NewSQLiteSchemaProbeExecutor(root).Execute(context.Background(), assignment) if result.ErrorCode != "result_limit_exceeded" || result.State != lifecycleResultStateFailed { t.Fatalf("expected result bound failure, got %+v", result) } } func sqliteSchemaProbeAssignment() protocol.RunJobAssignment { assignment := lifecycleAssignment(protocol.RunCapabilityRemoteRunDBSQLiteProbe) assignment.TargetKey, assignment.MaxAttempts, assignment.FencingToken = "databases/current.db", 1, 7 assignment.ExecutionInput.WorkspaceScope = "profile-default" assignment.ExecutionInput.SQLiteSchemaProbe = &protocol.SQLiteSchemaProbeRequest{RequestID: "probe-1", Binding: protocol.SQLiteSchemaProbeBinding{ServerInstanceID: assignment.ServerInstanceID, RunBindingID: "binding-1", RunEndpointID: assignment.RunEndpointID, PluginID: "game.example", PluginVersion: "1.0.0", AdapterVersion: "adapter-1", GameVersion: "1.0", DatabaseIdentity: "database-1"}, Limits: protocol.SQLiteSchemaProbeLimits{MaxObjects: 8, MaxColumnsPerObject: 8, MaxIndexesPerObject: 8, MaxForeignKeys: 8, MaxCardinalityReads: 8, MaxSampleRows: 2, TimeoutMS: 1000, MaxResultBytes: 128 * 1024}} return assignment } func createSQLiteProbeFixture(t *testing.T, path string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { t.Fatal(err) } database, err := sql.Open(sqliteSchemaProbeDriver, path) if err != nil { t.Fatal(err) } defer database.Close() if _, err := database.Exec("CREATE TABLE members (id INTEGER PRIMARY KEY, name TEXT NOT NULL); CREATE TABLE groups (id INTEGER PRIMARY KEY, member_id INTEGER REFERENCES members(id)); CREATE UNIQUE INDEX members_name ON members(name); INSERT INTO members(name) VALUES ('alpha'), ('beta');"); err != nil { t.Fatal(err) } } func mustJSON(t *testing.T, value any) string { t.Helper() body, err := json.Marshal(value) if err != nil { t.Fatal(err) } return string(body) }