Ctrl K
ComunidadGuías…

pkg/models/mock.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package models

import (
	"encoding/gob"
	"maps"
	"net/url"
	"slices"
	"strings"
	"time"

	"go.keploy.io/server/v3/pkg/models/mysql"
	"go.keploy.io/server/v3/pkg/models/postgres"
	"go.mongodb.org/mongo-driver/v2/bson"
)

func init() {
	// gob.RegisterName with an EXPLICIT wire name, not gob.Register, so the name
	// gob writes into every mocks.gob (and the interface payloads streamed
	// between the keploy binaries) is fixed here rather than derived from the Go
	// package path and type name. gob.Register(&T{}) keys the wire on the live
	// identifier, so renaming or moving one of these types silently changes the
	// on-disk format and makes previously recorded mocks fail to decode — the
	// stability gap env-vars.md flags for the gob format. Each literal below is
	// exactly the name gob.Register produced before, so this is byte-identical on
	// the wire (verified by the round-trip + golden tests in gob_register_test.go)
	// and existing mocks keep decoding; the difference is only that a future
	// rename can no longer move it. Frozen: change a literal only with intent, and
	// update the golden test.
	gob.RegisterName("go.mongodb.org/mongo-driver/v2/bson.D", bson.D{})
	gob.RegisterName("go.mongodb.org/mongo-driver/v2/bson.E", bson.E{})
	gob.RegisterName("go.mongodb.org/mongo-driver/v2/bson.A", bson.A{})
	gob.RegisterName("go.mongodb.org/mongo-driver/v2/bson.Binary", bson.Binary{})
	gob.RegisterName("go.mongodb.org/mongo-driver/v2/bson.M", bson.M{})
	gob.RegisterName("go.mongodb.org/mongo-driver/v2/bson.ObjectID", bson.ObjectID{})
}

type Kind string

const (
	HTTP       Kind = "Http"
	HTTP2      Kind = "Http2"
	GENERIC    Kind = "Generic"
	REDIS      Kind = "Redis"
	KAFKA      Kind = "Kafka"
	MySQL      Kind = "MySQL"
	Postgres   Kind = "Postgres"
	PostgresV2 Kind = "PostgresV2"

	// PostgresV3 is the single top-level Kind for the v3 Postgres parser.
	// The sub-type (session / catalog / data / query / generator) lives in
	// Spec.PostgresV3.Type; consumers discriminate there instead of on Kind.
	// See integrations/pkg/postgres/v3/README.md for the replay architecture.
	PostgresV3 Kind = "PostgresV3"

	GRPC_EXPORT Kind = "gRPC"
	Mongo       Kind = "Mongo"
	DNS         Kind = "DNS"

	// Aerospike covers all Aerospike traffic — info text frames,
	// AS_MSG binary frames, and their compressed wrapper. It is an
	// ENTERPRISE-ONLY parser (enterprise-native tier, like Redis/Kafka):
	// OSS core carries only this Kind string. The enterprise parser
	// stores its captured frames in the generic GenericRequests /
	// GenericResponses payload slices and owns the typed frame model +
	// the YAML mapper, so no Aerospike-specific type lives in OSS core.
	Aerospike Kind = "Aerospike"

	// ConnectionFailure records that the application's attempt to open a
	// connection to a dependency failed in a way it could see (refused,
	// unreachable, timed out, closed before any data came back). The details
	// are in Spec.ConnFailure; see connfailure.go.
	//
	// The name has NO HYPHEN, on purpose. Every keploy since 2023 skips a mock
	// kind containing "-" at Debug level as "does not belong to open source
	// version", BEFORE it reaches the unknown-kind branch that logs an ERROR
	// naming the kind. A hyphenated name would make a keploy too old to replay
	// these mocks drop them in silence; without the hyphen its YAML reader
	// says, once per document, which kind it cannot read (its JSON reader
	// skips an unknown kind at Debug either way). Pinned by
	// TestConnectionFailureKindReachesTheUnknownKindError.
	//
	// For the keploy that records and replays the kind: never write it to a
	// gob mock file, and send it to the agent only when the agent says it
	// supports it. A gob reader up to v3.6.107, and an agent that predates the
	// kind, read it as a mock of no kind and no metadata.destAddr, so its port
	// is unknown and recorded.unknown is set: every port then counts as
	// recorded, which turns loopback refusal off for the whole test set. This
	// keploy never sends one to the agent (see AgentClient.StoreMocks).
	ConnectionFailure Kind = "ConnectionFailure"

	// RevokedTests is a RESERVED control Kind — it is NOT a real mock and no
	// traffic parser ever produces it. The recorder emits a Mock with this Kind
	// on the /outgoing stream to tell the CLI to revoke (delete) test cases
	// whose owned mock was capacity-dropped AFTER the TC had already streamed
	// (the deferred-orphan case that stream-time suppression cannot catch). The
	// revoked TC names ride in Spec.Metadata. The CLI diverts this frame into a
	// revoke set instead of persisting it; the agent emits it ONLY to a CLI that
	// negotiated OutgoingOptions.SupportsDroppedRevoke, so an older CLI (which
	// never sets that flag) never receives it and cannot mis-persist it.
	RevokedTests Kind = "keploy-revoked-tests"
)

// MockName constants for the PostgresV3 parser. The integrations-side
// recorder currently hardcodes these values as string literals when
// stamping Mock.Name; exposing them on the keploy side lets a future
// integrations-repo commit migrate to the shared constants without
// drifting the spelling (a typo in the recorder would silently split
// the pool into two effectively unrelated subsets).
//
// Mock.Name values are identifiers, not display text — they participate
// in hit-count indexing, dedup, and by-name lookups in MockManager.
// Keep these exact strings stable across releases; if the spelling ever
// needs to change it must be coordinated with the integrations repo and
// with any recorded YAML artefacts that reference the old names.
const (
	MockNamePostgresV3Query   = "PostgresV3Query"
	MockNamePostgresV3Session = "PostgresV3Session"
)

// StartupMockTestCaseWindow is the number of leading UNIQUE recorded test cases
// whose mocks are treated as "startup mocks". Every mock captured from app boot
// up to and including the recording of the Nth unique test case is preserved
// wholesale: it is exempt from the record-side static-dedup pruning (so dedup
// effectively begins at test N+1) and from replay-side RemoveUnusedMocks
// pruning. The record side keys off this via SyncMockManager.resolvedTestCount
// (it tags such mocks TestModeInfo.IsStartup); the replay side derives an
// equivalent timestamp boundary from the on-disk test cases. Both must agree on
// N, so it lives here as the single source of truth. Fixed in code rather than
// exposed as a flag — change here to retune.
const StartupMockTestCaseWindow = 5

type Mock struct {
	Version      Version      `json:"Version,omitempty" bson:"Version,omitempty"`
	Name         string       `json:"Name,omitempty" bson:"Name,omitempty"`
	Kind         Kind         `json:"Kind,omitempty" bson:"Kind,omitempty"`
	Spec         MockSpec     `json:"Spec,omitempty" bson:"Spec,omitempty"`
	TestModeInfo TestModeInfo `json:"TestModeInfo,omitempty"  bson:"TestModeInfo,omitempty"` // Map for additional test mode information
	ConnectionID string       `json:"ConnectionId,omitempty" bson:"ConnectionId,omitempty"`
	// SourcePID is the kernel PID of the process that made this outgoing call,
	// carried transiently from capture to the recorder so `keploy mock record`
	// can attribute the mock to the test WORKER that made it (per-PID scoping
	// for parallel runners). Runtime-only: never serialized (yaml/json/bson "-")
	// — it is an in-process hint, not part of the recorded mock. 0 if unknown.
	SourcePID uint32 `json:"-" yaml:"-" bson:"-"`
	// EndedInstance is, under the proven switch, the keploy instance the
	// call's connection was stamped with when that test worker had ended by
	// the time the call was captured (Instance is then 0): a value is given
	// to one claim only, so the call is from a process in no live worker,
	// and record charges it as it charges one from a process in no worker,
	// naming the ended worker. 0 for none. Like SourcePID it only travels,
	// and it sits in that field's padding, so a Mock is no bigger for it.
	EndedInstance uint32 `json:"-" yaml:"-" bson:"-"`
	Start         string `json:"start,omitempty" yaml:"start,omitempty" bson:"start,omitempty"`
	StartRef      string `json:"-" yaml:"-" bson:"-"`
	// Origin is, under the proven switch, where the call came from: the clock
	// its ReqTimestampMock is on, and the process that made it. Only a record
	// under the switch sets it; a file's is read and
	// carried whatever the switch (models.Origin). A recording's file holds it
	// as its document's origin block (yaml.NetworkTrafficDoc.Origin), so it is
	// yaml "-" here. Set it whole, never edit one in place: ShallowCopy shares
	// it, as it shares Spec's pointers.
	Origin *Origin `json:"origin,omitempty" yaml:"-" bson:"origin,omitempty"`
	// Instance is, under the proven switch, the keploy instance (a test
	// worker's) the process that made the call was in when it connected,
	// as the kernel stamped its connection: record charges the call to
	// that worker's test only (ScopeWindow.Instance). 0 for none. Like
	// SourcePID it only travels, to keploy record, and is never stored.
	Instance uint32 `json:"-" yaml:"-" bson:"-"`
	// OutsideRequests says the agent knows no incoming request was open while
	// this call ran, so no request made it: it is the app's own background
	// work, such as a poll on a timer. Only an agent that can tell sets it;
	// false means "not known", never "a request's", so a mock from an agent
	// that does not set it is taken for a request's. Like SourcePID it only
	// travels, to the recorder, and is never stored; it sits in that field's
	// padding, so a Mock is no bigger for it.
	OutsideRequests bool `json:"-" yaml:"-" bson:"-"`

	// pooled is set when the replay mock manager takes the mock into a staging,
	// or stores it in a pool that matchers read, and is never cleared. The
	// staging that takes it stamps it before any matcher can reach it; from then
	// on matchers may hold the mock and copy it, so nothing may write it again:
	// the manager copies a pooled mock that it is handed to stage again instead
	// of stamping it, and the matchers in this repository store an updated copy
	// rather than edit the mock. (keploy/integrations' HTTP/2 parser still
	// re-stamps the pooled mock it updates in place.) Runtime only: unexported,
	// so no encoder carries it, and DeepCopy and ShallowCopy start their copies
	// unpooled. It sits after SourcePID, in that field's padding, so a Mock is
	// no bigger for it.
	pooled bool

	// Noise holds exact-match regex patterns for obfuscated values.
	// During mock matching, any stored value matching a pattern in this
	// list is skipped (treated as noise). Written by the enterprise
	// secret-protection obfuscator.
	Noise []string `json:"Noise,omitempty" bson:"noise,omitempty" yaml:"noise,omitempty"`

	// responseHydrator lazily loads an elided response (HTTPResp/MongoResponses)
	// at serve time; set only for spilled per-test mocks by the agent disk store.
	// Unexported so gob ignores it — never crosses the wire or a recording.
	responseHydrator func() (*HTTPResp, []MongoResponse, error)

	// storedSize is the bytes the agent's disk store keeps the mock in, its
	// record and its parked response together, set by the store as it reads
	// the mock (SetStoredSize); 0 where no store read it. Unexported so no
	// encoder carries it.
	storedSize int64

	// onDisk is where a replay's read left the mock's payload, set only on
	// the CLI side (LeavePayloadOnDisk); WithPayload reads it back to send
	// the mock. Unexported so no encoder carries it.
	onDisk *payloadOnDisk
}

// Pooled reports whether the replay mock manager has stored m in a pool that
// matchers read (see MarkPooled).
func (m *Mock) Pooled() bool { return m.pooled }

// MarkPooled records that m is being stored in a pool that matchers read. Only
// the mock manager calls it, as it takes or stores the mock and before any
// matcher can reach it; a mock that is already pooled is left unwritten.
func (m *Mock) MarkPooled() {
	if !m.pooled {
		m.pooled = true
	}
}

// SetResponseHydrator installs the lazy response loader (agent disk-residency).
func (m *Mock) SetResponseHydrator(fn func() (*HTTPResp, []MongoResponse, error)) {
	m.responseHydrator = fn
}

// SetStoredSize records the bytes the agent's disk store keeps m in: its
// record and its parked response together (StoredSize).
func (m *Mock) SetStoredSize(n int64) { m.storedSize = n }

// StoredSize is the bytes the agent's disk store keeps m in, its record and
// its parked response together, as the store read it: what reading m's
// whole payload back takes, known before its response is read back. 0
// where no store read m (one made in memory): its size is not known before
// its payload is read.
func (m *Mock) StoredSize() int64 { return m.storedSize }

// HasSpilledResponse reports whether the response is elided (needs hydration).
func (m *Mock) HasSpilledResponse() bool { return m.responseHydrator != nil }

// IsAsync reports whether this mock is async-egress — i.e. it carries an
// AsyncMeta block. Presence of the block IS the signal (see AsyncMeta); this
// is the single predicate the record mapping, replay collection, and engine
// load all discriminate on.
func (m *Mock) IsAsync() bool { return m.Spec.Async != nil }

// HydrateResponse loads an elided response into Spec; no-op if not spilled, so
// serve paths can call it unconditionally before reading the response.
func (m *Mock) HydrateResponse() error {
	fn := m.responseHydrator
	if fn == nil {
		return nil
	}
	httpResp, mongoResp, err := fn()
	if err != nil {
		return err
	}
	if httpResp != nil {
		m.Spec.HTTPResp = httpResp
	}
	if mongoResp != nil {
		m.Spec.MongoResponses = mongoResp
	}
	m.responseHydrator = nil
	return nil
}

// TestModeInfo is in-memory-only bookkeeping attached to each Mock once it
// enters a runtime pool (disk load, live recorder, agent StoreMocks).
// None of these fields are serialised to the YAML wire format — the
// platform/yaml NetworkTrafficDoc does not embed TestModeInfo — so this
// struct is the right home for cached derived state that must not bleed
// into recordings.
//
// Lifetime was added by the unification plan: it is the typed, cached form
// of the on-disk Spec.Metadata["type"] tag so hot-path matchers never probe
// the metadata map. Match counts are not kept here: matchers copy pooled mocks
// whole while other connections count matches against them, so the replay
// mock manager keeps the counts itself.
type TestModeInfo struct {
	ID         int  `json:"Id,omitempty" bson:"Id,omitempty"`
	IsFiltered bool `json:"isFiltered,omitempty" bson:"isFiltered,omitempty"`

	// LoadSeq is the mock's place, counted from 1, in the set the agent's
	// mock stream handed it (Agent.StoreMocksStream): the order the client
	// sent the set in, the order its mocks were recorded in. The agent keeps
	// each per-test mock in memory or on disk, and a read lists those kept in
	// memory ahead of those loaded from disk, so where one time ties mocks
	// (the frames of one pipelined write) the order a read holds them in is
	// not the order recorded; this is, whichever tier holds the mock. Zero on
	// a mock no stream handed. A uint32 in IsFiltered's word's padding, so a
	// Mock stays in its size class; the stream refuses a set with more mocks
	// than it counts. Runtime-only, like Lifetime and the fields below it:
	// gob carries it into the agent's on-disk store and back, and no file
	// keeps it.
	LoadSeq uint32 `json:"-" bson:"-"`

	SortOrder int64 `json:"sortOrder,omitempty" bson:"SortOrder,omitempty"`

	// Lifetime classifies the mock (per-test / session / connection)
	// once at ingest via (*Mock).DeriveLifetime. Matchers read this
	// field directly — do NOT re-probe Spec.Metadata["type"] in hot
	// paths. The field is intentionally untagged for JSON/BSON/YAML:
	// it is derived from on-disk state on every load, so persisting it
	// would create a second source of truth.
	Lifetime Lifetime `json:"-" bson:"-"`

	// LifetimeDerived is set to true the first time DeriveLifetime
	// completes on this mock. Without this flag, DeriveLifetime cannot
	// distinguish "never derived" from "derived to LifetimePerTest"
	// (they share the zero value) and would re-run on every call for
	// per-test mocks — a minor but avoidable cost given DeriveLifetime
	// runs at every ingest site (disk load, StoreMocks, syncMock).
	// Runtime-only, untagged; re-derived fresh on each reload.
	LifetimeDerived bool `json:"-" bson:"-"`

	// Consume classifies how the matcher consumes this mock across repeated
	// identical requests — ConsumeReuse (serve the same response every time;
	// the default for session/config/connection mocks) or ConsumeCursorSaturate
	// (serve successive recorded responses in record order, then saturate on
	// the last; for stateful data-plane mocks). Derived at ingest by
	// DeriveLifetime alongside Lifetime; runtime-only, untagged, re-derived on
	// each load for the same reason Lifetime is.
	Consume ConsumeMode `json:"-" bson:"-"`

	// IsStartup marks startup-window traffic: a mock captured either before
	// the first inbound request (classic app-bootstrap, e.g. an AWS Secret
	// Manager fetch at process boot) OR while fewer than
	// StartupMockTestCaseWindow unique test cases have been recorded. Such
	// mocks must survive the record-side syncMock reapers (dedup cleanup,
	// the ResolveRange keep=false / out-of-window / stale-cutoff rescues, the
	// memory-pressure wipe) rather than being dropped — that is what keeps
	// the boot-through-Nth-test mock corpus complete and effectively defers
	// static-dedup pruning to the (N+1)-th test case.
	//
	// This is a RECORD-side cleanup signal only, with no replay-time
	// meaning, which is why it is NOT modelled as a Lifetime value:
	// Lifetime is derived from the on-disk Spec.Metadata tag and drives
	// replay-time pool routing, whereas IsStartup is set live at ingest in
	// SyncMockManager.AddMock and is only ever read on buffered, live-
	// captured mocks before they are persisted. (Replay's own startup-mock
	// preservation is timestamp-based — see replay.startupMockCutoff — not
	// keyed off this flag.) Like the sibling runtime-only fields, the
	// json/bson tags keep it out of the text formats, and the gob store
	// (encoding/gob ignores struct tags) clears it on write and on read
	// (mockdb's clearRuntimeOnly), so a disk-loaded mock never carries it.
	IsStartup bool `json:"-" bson:"-"`

	// Concurrent is, on a copy exact serving serves an open test
	// (KEPLOY_PROVEN), set for a recording the test shares with the tests whose
	// requests were open beside its own at record time
	// (ExactServing.Concurrent): a candidate, answered at most once in the
	// staging by whichever of them makes its call, never required of the
	// test. Its matcher prefers the test's own recordings to it. Runtime-only,
	// like the fields above. It sits in IsStartup's padding, which leaves room
	// for RepeatRead: a Mock stays in its size class.
	Concurrent bool `json:"-" bson:"-"`

	// ServedIn is, on a copy exact serving serves an open test
	// (KEPLOY_PROVEN), the number of that test's view; zero on every other mock. A
	// consume of a copy an earlier view served (a previous attempt of the
	// same test) is refused. Runtime-only, like the fields above.
	ServedIn uint64 `json:"-" bson:"-"`

	// StagedSet is the agent's test-set generation whose staging pooled the
	// mock (MockManager.ArmSetFence); zero on a mock no staging pooled. A
	// consume of a mock pooled for an earlier set (a consume racing the next
	// set's staging) is refused: it was of that set's pool, under that set's
	// mock names. Runtime-only, like the fields above.
	StagedSet uint64 `json:"-" bson:"-"`

	// RepeatRead is, on a test's own recording under exact serving
	// (KEPLOY_PROVEN), the repeated read the replay named it in
	// (ScopeTableReq.RepeatReads, MarkRepeatReads): the test recorded that
	// read at least twice, every time with one answer. Exact serving
	// answers such a read once more than recorded, and does not charge one
	// of its recordings left unmade (one fewer). Two reads the replay names
	// alike are two reads (RepeatReadMark.At). nil, no read, on every other
	// recording (ReadMark). The recordings of one read share one mark: set
	// it whole, never edit one in place, as ShallowCopy shares it.
	// Runtime-only, like the fields above.
	RepeatRead *RepeatReadMark `json:"-" bson:"-"`
}

func (m *Mock) GetKind() string {
	return string(m.Kind)
}

// RecordedDestination reads the upstream authority a recorded mock was
// captured against — "which host did this mock's call go to?" — for the
// destination-scope diagnostic (see pkg/agent/proxy/integrations/mismatch).
//
// ok is false when the answer is UNKNOWN, and the caller must treat that as
// "undecidable", never as "no destination": one mock whose host cannot be
// read could be the very mock that targeted the live call, so it has to veto
// the verdict rather than count as evidence. Today only HTTP mocks carry a
// readable destination — the other protocols' recorded specs keep no
// authority at all (Mongo/MySQL/Postgres store wire payloads, Generic stores
// opaque buffers) — so every non-HTTP kind answers "unknown".
//
// Header Host first, URL authority second: the recorder stores an HTTP
// request path-only ("/api/orders?id=90") with the authority in the
// Host header, but a mock captured through a forward proxy carries an
// absolute URL instead.
func (m *Mock) RecordedDestination() (string, bool) {
	if m == nil || m.Kind != Kind(HTTP) || m.Spec.HTTPReq == nil {
		return "", false
	}
	// Case-insensitive lookup: recorded mock headers are a plain map (no
	// textproto canonicalisation), so the key's case depends on whichever
	// recorder wrote it.
	//
	// The two conventional spellings are probed directly first: the fallback
	// scan below is O(headers) with a strings.EqualFold per key, and a real
	// recorded request carries far more headers than a test fixture does, so
	// the scan's cost grows with them while these two lookups do not. The
	// scan still runs for anything else ("HOST", "hOst"), so no recording
	// stops being readable.
	h := m.Spec.HTTPReq.Header
	if v := h["Host"]; v != "" {
		return v, true
	}
	if v := h["host"]; v != "" {
		return v, true
	}
	for k, v := range h {
		if strings.EqualFold(k, "Host") && v != "" {
			return v, true
		}
	}
	if u, err := url.Parse(m.Spec.HTTPReq.URL); err == nil && u.Host != "" {
		return u.Host, true
	}
	return "", false
}

type MockSpec struct {
	Metadata            map[string]string   `json:"Metadata,omitempty" bson:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
	GenericRequests     []Payload           `json:"RequestBin,omitempty" bson:"generic_requests,omitempty"`
	GenericResponses    []Payload           `json:"ResponseBin,omitempty" bson:"generic_responses,omitempty"`
	HTTPReq             *HTTPReq            `json:"Req,omitempty" bson:"http_req,omitempty"`
	HTTPResp            *HTTPResp           `json:"Res,omitempty" bson:"http_resp,omitempty"`
	Created             int64               `json:"Created,omitempty" bson:"created,omitempty"`
	MongoRequests       []MongoRequest      `json:"MongoRequests,omitempty" bson:"mongo_requests,omitempty"`
	MongoResponses      []MongoResponse     `json:"MongoResponses,omitempty" bson:"mongo_responses,omitempty"`
	PostgresRequestsV2  []postgres.Request  `json:"PostgresRequestsV2,omitempty" bson:"postgres_requests_v2,omitempty"`
	PostgresResponsesV2 []postgres.Response `json:"PostgresResponsesV2,omitempty" bson:"postgres_responses_v2,omitempty"`
	// gRPC
	GRPCReq        *GrpcReq         `json:"gRPCRequest,omitempty" bson:"grpc_req,omitempty"`
	GRPCResp       *GrpcResp        `json:"grpcResponse,omitempty" bson:"grpc_resp,omitempty"`
	MySQLRequests  []mysql.Request  `json:"MySqlRequests,omitempty" bson:"my_sql_requests,omitempty"`
	MySQLResponses []mysql.Response `json:"MySqlResponses,omitempty" bson:"my_sql_responses,omitempty"`
	DNSReq         *DNSReq          `json:"dnsReq,omitempty" bson:"dns_req,omitempty"`
	DNSResp        *DNSResp         `json:"dnsResp,omitempty" bson:"dns_resp,omitempty"`
	// HTTP/2
	HTTP2Req         *HTTP2Req  `json:"http2Req,omitempty" bson:"http2_req,omitempty"`
	HTTP2Resp        *HTTP2Resp `json:"http2Resp,omitempty" bson:"http2_resp,omitempty"`
	ReqTimestampMock time.Time  `json:"reqTimestampMock,omitempty" bson:"req_timestamp_mock,omitempty"`
	ResTimestampMock time.Time  `json:"resTimestampMock,omitempty" bson:"res_timestamp_mock,omitempty"`

	// PostgresV3 is the single discriminated spec for the v3 Postgres parser.
	// Exactly one sub-pointer is populated; Type names which. See PostgresV3Spec.
	PostgresV3 *PostgresV3Spec `yaml:"postgresV3,omitempty" json:"postgresV3,omitempty" bson:"postgres_v3,omitempty"`

	// ConnFailure is the body of a ConnectionFailure mock (see connfailure.go);
	// nil for every other kind. An older keploy's gob decoder drops it, as
	// encoding/gob drops any field the receiving struct lacks.
	ConnFailure *ConnFailureSpec `yaml:"connFailure,omitempty" json:"connFailure,omitempty" bson:"conn_failure,omitempty"`

	// Fault is how the call ended without its whole answer (see fault.go);
	// nil for a call that got one. Carried on an HTTP mock's spec.
	Fault *Fault `yaml:"fault,omitempty" json:"fault,omitempty" bson:"fault,omitempty"`

	// Aerospike (enterprise-only) stores its captured frames in the
	// generic GenericRequests / GenericResponses payload slices above,
	// like Redis/Kafka — so OSS core needs no Aerospike-specific field.

	// ReqBodyNoise is the single, kind-agnostic home for field-path request-body
	// noise detected during schema-based auto-replay matching
	// (config.Test.SchemaNoiseDetection). EVERY parser stores it here — HTTP and
	// non-HTTP (Pulsar/Kafka/Redis/…) alike — so the learn/enforce flow is
	// uniform across protocols (see pkg/agent/proxy/integrations/schemanoise).
	// fieldpath ("body.user.id") -> regex list, where an empty list means
	// "ignore this whole field". Distinct from Mock.Noise ([]string value-regexes
	// written by the enterprise obfuscator): this records which request-body
	// fields drift between recording and replay.
	ReqBodyNoise map[string][]string `json:"ReqBodyNoise,omitempty" yaml:"req_body_noise,omitempty" bson:"req_body_noise,omitempty"`

	// Correlations records request→response value echoes on this dependency mock
	// (see FieldCorrelation): a value the application mints and sends in the
	// request that the dependency reflects back in its response (a UUID, nonce,
	// idempotency key). Learned on the auto-replay pass and honored at serve
	// time — the matcher captures the live request value and renders it into the
	// served response, instead of replaying the recorded value (a mismatch) or
	// masking it as noise (which hides real regressions). Kind-agnostic, same
	// home as ReqBodyNoise. Additive / omitempty: an older replayer ignores it.
	Correlations []FieldCorrelation `json:"Correlations,omitempty" yaml:"correlations,omitempty" bson:"correlations,omitempty"`

	// Variables names the places of this call that hold a test variable's
	// value (see MockVariables). Kind-agnostic, carried on the doc envelope as
	// a top-level `variables:` block like noise; nil for a call that holds
	// none. Additive / omitempty: an older keploy ignores it.
	Variables MockVariables `json:"Variables,omitempty" yaml:"variables,omitempty" bson:"variables,omitempty"`

	// Clocks names the request places of this call the learn step learned
	// as a read of the application's clock: a plain read (a zero offset,
	// ClockOffset.Plain) or a read plus a constant offset (an expiry thirty
	// minutes after now), by the field paths ReqBodyNoise uses. They are
	// compared, never noise: a replay checks the value its call carries
	// there is a read made in its own call's window on the application's
	// clock plus that offset (ClockOffset.Holds), and refuses it otherwise,
	// or where it cannot tell the application's clock. Kind-agnostic,
	// carried on the doc envelope as a top-level `clocks:` block like
	// variables; nil for a call that holds none. Additive / omitempty: a
	// recording without it loads as before, and an older keploy ignores it.
	Clocks map[string]ClockOffset `json:"Clocks,omitempty" yaml:"clocks,omitempty" bson:"clocks,omitempty"`

	// learned and learnedBinds are, on a mock the learn step of record's
	// full round just paired (agent side, in memory), the variables it
	// learned on the mock and those it learned that are bound at an earlier
	// mock of the same test (by that mock's name). They ride out on the
	// consumed mock's state (MockState.Variables, MockState.Binds), and are
	// never encoded (unexported: no gob, JSON or YAML of a mock carries
	// them) nor served: a reused mock the learn step paired is the same
	// recording to the next call.
	learned      MockVariables
	learnedBinds map[string]MockVariables

	// Async, when non-nil, marks this mock as async-egress and carries the
	// engine's bookkeeping (lane, order, anchor, poll/duration) in its own
	// block — kept OUT of the flat parser Metadata above. Serialized as a
	// top-level `async:` block on the recorded doc. See AsyncMeta.
	Async *AsyncMeta `json:"Async,omitempty" yaml:"async,omitempty" bson:"async,omitempty"`
}

// FieldCorrelation ties a value in a dependency mock's REQUEST to where it is
// echoed in that mock's RESPONSE, so replay can reproduce an app-minted value
// (UUID / nonce / idempotency key) the dependency reflects back. On replay the
// matcher matches the request field by presence + ValueClass (not value),
// binds the live value, and replaces RecordedValue with it in the served
// response. RecordedValue is the value as recorded (never a live/secret value)
// and ValueClass names its shape (e.g. "uuid", "hex", "nonce") for the
// presence/type match.
//
// An HTTP mock's paths use the dotted vocabulary of ReqBodyNoise (body.<path>,
// header.<name>, url.query.<name>). A Mongo mock's are section.<i>.<path>,
// where i is the OP_MSG section's index. ResponsePaths says where the recording
// answered the value, for a reader: the render goes by RecordedValue, and no
// replay reads it. A Mongo list holds at most a few places of a value, on the
// value's first correlation, and is empty on the value's others.
type FieldCorrelation struct {
	RequestPath   string   `json:"requestPath" yaml:"request_path" bson:"request_path"`
	ResponsePaths []string `json:"responsePaths" yaml:"response_paths" bson:"response_paths"`
	RecordedValue string   `json:"recordedValue,omitempty" yaml:"recorded_value,omitempty" bson:"recorded_value,omitempty"`
	ValueClass    string   `json:"valueClass,omitempty" yaml:"value_class,omitempty" bson:"value_class,omitempty"`
}

// PostgresV3Spec is the single discriminated Spec for the five v3
// mock sub-types. Exactly one of the pointer fields is non-nil, and
// Type names which. A nil PostgresV3Spec or a Type that doesn't match
// the populated pointer is a hard-reject at BuildIndex time.
type PostgresV3Spec struct {
	Type string `yaml:"type" json:"type" bson:"type"` // "session" | "catalog" | "data" | "query" | "generator"

	Session   *PostgresV3SessionSpec   `yaml:"session,omitempty"   json:"session,omitempty"   bson:"session,omitempty"`
	Catalog   *PostgresV3CatalogSpec   `yaml:"catalog,omitempty"   json:"catalog,omitempty"   bson:"catalog,omitempty"`
	Data      *PostgresV3DataSpec      `yaml:"data,omitempty"      json:"data,omitempty"      bson:"data,omitempty"`
	Query     *PostgresV3QuerySpec     `yaml:"query,omitempty"     json:"query,omitempty"     bson:"query,omitempty"`
	Generator *PostgresV3GeneratorSpec `yaml:"generator,omitempty" json:"generator,omitempty" bson:"generator,omitempty"`
}

// PostgresV3 sub-type discriminator values (Spec.PostgresV3.Type).
const (
	PostgresV3TypeSession   = "session"
	PostgresV3TypeCatalog   = "catalog"
	PostgresV3TypeData      = "data"
	PostgresV3TypeQuery     = "query"
	PostgresV3TypeGenerator = "generator"
)

// ============================================================================
// PostgresV3 specs — deterministic replay payloads.
//
// See integrations/pkg/postgres/v3/types/contracts.go for the in-memory
// type hierarchy these structs serialize from.
// ============================================================================

// PostgresV3SessionSpec — startup handshake + ParameterStatus bundle.
// Exactly one per recording; emitted on every replay client connection
// after a trust-mode AuthOk.
type PostgresV3SessionSpec struct {
	ProtocolVersion  string            `json:"protocolVersion,omitempty" yaml:"protocolVersion,omitempty" bson:"protocol_version,omitempty"`
	SSLResponse      string            `json:"sslResponse,omitempty" yaml:"sslResponse,omitempty" bson:"ssl_response,omitempty"`
	ServerVersion    string            `json:"serverVersion,omitempty" yaml:"serverVersion,omitempty" bson:"server_version,omitempty"`
	ParameterStatus  map[string]string `json:"parameterStatus,omitempty" yaml:"parameterStatus,omitempty" bson:"parameter_status,omitempty"`
	BackendProcessID int32             `json:"backendProcessID,omitempty" yaml:"backendProcessID,omitempty" bson:"backend_process_id,omitempty"`
	BackendSecretKey int32             `json:"backendSecretKey,omitempty" yaml:"backendSecretKey,omitempty" bson:"backend_secret_key,omitempty"`
	ObservedAuthMode string            `json:"observedAuthMode,omitempty" yaml:"observedAuthMode,omitempty" bson:"observed_auth_mode,omitempty"`
}

// PostgresV3CatalogSpec — structured pg_catalog + information_schema
// snapshot consulted by the replayer's L5 catalog engine for ORM
// metadata probes.
type PostgresV3CatalogSpec struct {
	Schemas        []PostgresV3Schema         `json:"schemas,omitempty" yaml:"schemas,omitempty" bson:"schemas,omitempty"`
	Types          []PostgresV3PgType         `json:"types,omitempty" yaml:"types,omitempty" bson:"types,omitempty"`
	Sequences      []PostgresV3Sequence       `json:"sequences,omitempty" yaml:"sequences,omitempty" bson:"sequences,omitempty"`
	Extensions     []string                   `json:"extensions,omitempty" yaml:"extensions,omitempty" bson:"extensions,omitempty"`
	MigrationState []PostgresV3MigrationTable `json:"migrationState,omitempty" yaml:"migrationState,omitempty" bson:"migration_state,omitempty"`
}

type PostgresV3Schema struct {
	Name   string               `json:"name" yaml:"name" bson:"name"`
	Tables []PostgresV3TableDef `json:"tables,omitempty" yaml:"tables,omitempty" bson:"tables,omitempty"`
}

type PostgresV3TableDef struct {
	Name        string                 `json:"name" yaml:"name" bson:"name"`
	Columns     []PostgresV3Column     `json:"columns,omitempty" yaml:"columns,omitempty" bson:"columns,omitempty"`
	Indexes     []PostgresV3IndexDef   `json:"indexes,omitempty" yaml:"indexes,omitempty" bson:"indexes,omitempty"`
	Constraints []PostgresV3Constraint `json:"constraints,omitempty" yaml:"constraints,omitempty" bson:"constraints,omitempty"`
}

type PostgresV3Column struct {
	Name      string `json:"name" yaml:"name" bson:"name"`
	TypeOID   uint32 `json:"typeOid" yaml:"typeOid" bson:"type_oid"`
	TypeName  string `json:"typeName" yaml:"typeName" bson:"type_name"`
	NotNull   bool   `json:"notNull,omitempty" yaml:"notNull,omitempty" bson:"not_null,omitempty"`
	Default   string `json:"default,omitempty" yaml:"default,omitempty" bson:"default,omitempty"`
	IsPrimary bool   `json:"isPrimary,omitempty" yaml:"isPrimary,omitempty" bson:"is_primary,omitempty"`
	AttNum    int16  `json:"attNum" yaml:"attNum" bson:"att_num"`
}

type PostgresV3IndexDef struct {
	Name    string   `json:"name" yaml:"name" bson:"name"`
	Columns []string `json:"columns" yaml:"columns" bson:"columns"`
	Unique  bool     `json:"unique,omitempty" yaml:"unique,omitempty" bson:"unique,omitempty"`
}

type PostgresV3Constraint struct {
	Name    string   `json:"name" yaml:"name" bson:"name"`
	Type    string   `json:"type" yaml:"type" bson:"type"`
	Columns []string `json:"columns" yaml:"columns" bson:"columns"`
}

type PostgresV3PgType struct {
	Name string `json:"name" yaml:"name" bson:"name"`
	OID  uint32 `json:"oid" yaml:"oid" bson:"oid"`
	Size int16  `json:"size" yaml:"size" bson:"size"`
	Kind string `json:"kind" yaml:"kind" bson:"kind"`
}

type PostgresV3Sequence struct {
	Schema    string `json:"schema" yaml:"schema" bson:"schema"`
	Name      string `json:"name" yaml:"name" bson:"name"`
	Start     int64  `json:"start" yaml:"start" bson:"start"`
	Increment int64  `json:"increment" yaml:"increment" bson:"increment"`
	LastValue int64  `json:"lastValue" yaml:"lastValue" bson:"last_value"`
}

type PostgresV3MigrationTable struct {
	Name    string            `json:"name" yaml:"name" bson:"name"`
	Columns []string          `json:"columns" yaml:"columns" bson:"columns"`
	Rows    []PostgresV3Cells `json:"rows,omitempty" yaml:"rows,omitempty" bson:"rows,omitempty"`
}

// PostgresV3DataSpec — one per seeded user table. Carries the row-store
// seed for L4's transactional engine.
type PostgresV3DataSpec struct {
	Schema     string            `json:"schema" yaml:"schema" bson:"schema"`
	Table      string            `json:"table" yaml:"table" bson:"table"`
	PrimaryKey []string          `json:"primaryKey,omitempty" yaml:"primaryKey,omitempty" bson:"primary_key,omitempty"`
	Columns    []string          `json:"columns" yaml:"columns" bson:"columns"`
	Rows       []PostgresV3Cells `json:"rows,omitempty" yaml:"rows,omitempty" bson:"rows,omitempty"`
	Truncated  bool              `json:"truncated,omitempty" yaml:"truncated,omitempty" bson:"truncated,omitempty"`
	RowLimit   int               `json:"rowLimit,omitempty" yaml:"rowLimit,omitempty" bson:"row_limit,omitempty"`
}

// PostgresV3QuerySpec — one invocation of a recorded query, keyed in the
// replay-time index by sqlAstHash.
//
// Historical note: earlier recordings stamped a `scope` field
// ("connection" / "session" / "test:<name>") alongside Class / Lifetime.
// The field was retired after 28069e28 moved pool routing to
// lifetime-first and pgmatch.DeriveLifetime became the single source
// of truth. Old YAML mocks that still contain `scope: ...` continue to
// load cleanly: gopkg.in/yaml.v3 is non-strict by default, so the
// unknown key is silently skipped at unmarshal time. NEW recordings
// MUST NOT re-introduce a scope field.
type PostgresV3QuerySpec struct {
	// Classification
	Class      string `json:"class,omitempty" yaml:"class,omitempty" bson:"class,omitempty"`
	Lifetime   string `json:"lifetime,omitempty" yaml:"lifetime,omitempty" bson:"lifetime,omitempty"`
	SQLAstHash string `json:"sqlAstHash" yaml:"sqlAstHash" bson:"sql_ast_hash"`

	// SQL
	SQLNormalized     string   `json:"sqlNormalized" yaml:"sqlNormalized" bson:"sql_normalized"`
	Relations         []string `json:"relations,omitempty" yaml:"relations,omitempty" bson:"relations,omitempty"`
	ParamOIDs         []uint32 `json:"paramOids,omitempty" yaml:"paramOids,omitempty" bson:"param_oids,omitempty"`
	VolatilePositions []int    `json:"volatilePositions,omitempty" yaml:"volatilePositions,omitempty" bson:"volatile_positions,omitempty"`

	// InvocationID uniquely identifies one invocation of this query
	// within a single record-session. Recorders should emit the
	// canonical "<connID>:<seq>" form — e.g. "0:1968" for the 1968th
	// invocation on connection 0. No hash prefix, no wall-clock
	// timestamp: a repeat recording of identical traffic will produce
	// identical InvocationIDs, which keeps diffs of re-recorded
	// mocks.yaml quiet. The field is stored as a plain string and is
	// not validated, so older or non-canonical values (e.g. legacy
	// "sha256:..." forms) still load cleanly for backward compatibility.
	// Uniqueness is local to one recording; the <sqlAstHash,
	// invocationId> pair is globally unique across all mocks in the
	// same file.
	InvocationID     string `json:"invocationId" yaml:"invocationId" bson:"invocation_id"`
	PrecedingTxState string `json:"precedingTxState,omitempty" yaml:"precedingTxState,omitempty" bson:"preceding_tx_state,omitempty"`

	// BindValues holds the client-supplied bind parameters for this
	// invocation, one entry per placeholder. Cells are stored as
	// logical Go values irrespective of the wire format the client
	// used. The supported in-memory types track the PostgresV3Cell
	// gob tag table (see postgres_v3_cell.go for the authoritative
	// list): int8 / int16 / int32 / int / int64 (matching pgtype's
	// int2 / int4 / int8 mapping plus the int8/int alias surfaces),
	// uint16 / uint32 / uint64 (for OID / XID / CID and xid8-style
	// values pgtype hands back from pg_catalog metadata queries),
	// float32 / float64 (PG float4 / float8), bool, string, time.Time,
	// []byte, []interface{} (PG ARRAY columns — text[], int4[],
	// composite-row, multi-dim arrays), map[string]interface{}
	// (PG json / jsonb), pgtype.Numeric, pgtype.Interval, pgtype.Time,
	// pgtype.Bits, pgtype.TID, pgtype.TSVector, pgtype.Hstore,
	// pgtype.Range[any], pgtype.Multirange[Range[any]], the
	// geometric union (Point/Line/Lseg/Box/Path/Polygon/Circle),
	// netip.Prefix, net.HardwareAddr, and PostgresV3CellRaw for
	// unknown OIDs. PostgresV3Cell.MarshalYAML picks the appropriate
	// scalar tag per value so common types stay eyeballable and
	// greppable in mocks.yaml while bytea, raw-OID, and structured
	// payloads land as !!binary or as nested mappings/sequences.
	// Note that yaml.v3's resolver decodes every !!int back to int64,
	// so int8/int16/int32/int and uint16/uint32/uint64 cells round-
	// trip through the gob path with their original Go width but
	// widen to int64 through the YAML path — that's safe because the
	// codec on the integration side encodes to the wire using the
	// column OID, not the Go width. NULL binds are distinguished
	// from empty-string binds via Value == nil / cell.IsNull().
	// BindFormats records the client's per-placeholder format flag
	// (0=text, 1=binary) so the replayer can re-encode the logical
	// value on the wire in the form the live client expects,
	// regardless of which form the recorder originally captured.
	BindValues  PostgresV3Cells `json:"bindValues,omitempty" yaml:"bindValues,omitempty" bson:"bind_values,omitempty"`
	BindFormats []int           `json:"bindFormats,omitempty" yaml:"bindFormats,omitempty" bson:"bind_formats,omitempty"`

	// Literals holds the constants an extended-protocol statement carries
	// in its SQL text (the SQL its Parse sent), in their order there, as
	// their text values; a NULL constant is a NULL cell. SQLNormalized has
	// them as $N placeholders and SQLAstHash ignores them, so without
	// Literals a recording cannot say which values it was made for: a
	// statement built by string concatenation (pgjdbc's Statement, which
	// pgjdbc sends over the extended protocol) recorded for
	// name = 'alice' would match a replay for name = 'carol'. Nil for a
	// statement with no constants, and for a simple-Query invocation,
	// whose constants are its BindValues; a recording made before the
	// field existed has none either.
	Literals PostgresV3Cells `json:"literals,omitempty" yaml:"literals,omitempty" bson:"literals,omitempty"`

	// ResultFormats — per-column format codes the client requested at
	// Bind time (via the Bind packet's ResultFormatCodes field).
	// Semantics match PG wire: len=0 means "all text", len=1 means
	// "broadcast f[0] to every result column", len=N means per-column.
	// Required at replay so the dispatcher can synthesise a
	// RowDescription with correct Format fields when the client did
	// not issue Describe before Execute (INSERT…RETURNING-style flow
	// under lib/pq / database/sql). Without this field, the replayer
	// has no way to know whether the recorded DataRow bytes are text
	// or binary, and drivers that expect binary int4 will fail text-
	// parsing on "\x00\x00\x00\x01" with "invalid syntax".
	ResultFormats []int `json:"resultFormats,omitempty" yaml:"resultFormats,omitempty" bson:"result_formats,omitempty"`

	// Wire response
	Response *PostgresV3Response `json:"response,omitempty" yaml:"response,omitempty" bson:"response,omitempty"`

	// State effects
	SideEffects *PostgresV3SideEffects `json:"sideEffects,omitempty" yaml:"sideEffects,omitempty" bson:"side_effects,omitempty"`
}

// MarshalYAML — see PostgresV3Notice.MarshalYAML for the rationale.
// SQLNormalized carries the recorded query text, which (after the
// integrations restoreRawSQL pass) can preserve embedded tabs and
// newlines from the original statement. Left as a plain string, the
// yaml.v3 v3.0.1 encoder picks a literal block scalar (`|N-`) for such
// values; because yaml.Node.Encode marshals then immediately re-parses
// its own output, that block scalar fails to round-trip with "found a
// tab character where an indentation space is expected" — and the
// failure happens inside EncodeMock's `Spec.Encode`, BEFORE the
// post-encode sanitizeYAMLStringNodes walk can run, so the node-tree
// sanitizer cannot rescue it. Routing SQLNormalized through
// PostgresV3SafeString forces DoubleQuotedStyle on the WRITE side
// (escaping \t / \n) so the self-reparse and the on-disk re-read both
// succeed. The model field stays `string` so the integrations
// recorder/replayer assignment sites (NormalizeForHash results,
// index_loader copies) keep compiling unchanged; the alias is
// YAML-side only. The alias spells out every YAML field explicitly
// (mirroring PostgresV3Notice/PostgresV3Error): yaml.v3 v3.0.1 panics
// on a duplicated key, so the embed-and-shadow idiom is unavailable —
// the only field whose type changes is SQLNormalized (to the safe
// wrapper); every other field keeps its original type and tag so the
// on-disk shape is byte-for-byte identical to the struct-tag encoding.
func (q PostgresV3QuerySpec) MarshalYAML() (any, error) {
	type alias struct {
		Class             string                 `yaml:"class,omitempty"`
		Lifetime          string                 `yaml:"lifetime,omitempty"`
		SQLAstHash        string                 `yaml:"sqlAstHash"`
		SQLNormalized     PostgresV3SafeString   `yaml:"sqlNormalized"`
		Relations         []string               `yaml:"relations,omitempty"`
		ParamOIDs         []uint32               `yaml:"paramOids,omitempty"`
		VolatilePositions []int                  `yaml:"volatilePositions,omitempty"`
		InvocationID      string                 `yaml:"invocationId"`
		PrecedingTxState  string                 `yaml:"precedingTxState,omitempty"`
		BindValues        PostgresV3Cells        `yaml:"bindValues,omitempty"`
		BindFormats       []int                  `yaml:"bindFormats,omitempty"`
		Literals          PostgresV3Cells        `yaml:"literals,omitempty"`
		ResultFormats     []int                  `yaml:"resultFormats,omitempty"`
		Response          *PostgresV3Response    `yaml:"response,omitempty"`
		SideEffects       *PostgresV3SideEffects `yaml:"sideEffects,omitempty"`
	}
	return alias{
		Class:             q.Class,
		Lifetime:          q.Lifetime,
		SQLAstHash:        q.SQLAstHash,
		SQLNormalized:     PostgresV3SafeString(q.SQLNormalized),
		Relations:         q.Relations,
		ParamOIDs:         q.ParamOIDs,
		VolatilePositions: q.VolatilePositions,
		InvocationID:      q.InvocationID,
		PrecedingTxState:  q.PrecedingTxState,
		BindValues:        q.BindValues,
		BindFormats:       q.BindFormats,
		Literals:          q.Literals,
		ResultFormats:     q.ResultFormats,
		Response:          q.Response,
		SideEffects:       q.SideEffects,
	}, nil
}

type PostgresV3Response struct {
	RowDescription []PostgresV3ColumnDescriptor `json:"rowDescription,omitempty" yaml:"rowDescription,omitempty" bson:"row_description,omitempty"`
	// Rows stores each row as a []PostgresV3Cell via PostgresV3Cells.
	// See PostgresV3Cell for the on-disk encoding rules (plain YAML
	// string for UTF-8-safe text, !!binary for anything else, explicit
	// native YAML null for SQL NULL). Mutually exclusive with CopyOut /
	// CopyIn — a single Query response emits either DataRow traffic
	// OR CopyData traffic, never both; the replay emitter enforces
	// the invariant at wire time.
	Rows            []PostgresV3Cells `json:"rows,omitempty" yaml:"rows,omitempty" bson:"rows,omitempty"`
	CommandComplete string            `json:"commandComplete,omitempty" yaml:"commandComplete,omitempty" bson:"command_complete,omitempty"`
	Error           *PostgresV3Error  `json:"error,omitempty" yaml:"error,omitempty" bson:"error,omitempty"`

	// CopyOut is set when the server responded with a CopyOutResponse
	// ('H') + zero or more CopyData ('d') chunks + CopyDone ('c')
	// sequence, as produced by COPY ... TO STDOUT / TO PROGRAM /
	// TO '<file>' queries. Zero CopyData chunks is valid — COPY of an
	// empty table or an empty-result expression produces no data
	// packets between CopyOutResponse and CopyDone. nil on non-COPY
	// responses; omitempty keeps the serialised form clean for them.
	CopyOut *PostgresV3CopyOutPayload `json:"copyOut,omitempty" yaml:"copyOut,omitempty" bson:"copy_out,omitempty"`

	// CopyIn is set when the server responded with a CopyInResponse
	// ('G'), signalling that it is ready to receive client-streamed
	// data via CopyData/CopyDone (COPY ... FROM STDIN). Only the
	// server-side "ready" metadata is persisted — the client's
	// subsequent CopyData payload is not replayed back, since the
	// replay side stands in as the server and the real client will
	// resend its own bytes. nil on non-COPY responses.
	CopyIn *PostgresV3CopyInPayload `json:"copyIn,omitempty" yaml:"copyIn,omitempty" bson:"copy_in,omitempty"`

	// Notices holds NoticeResponse ('N') messages the server emitted
	// while answering this invocation. Postgres interleaves notices
	// with any command response (e.g. "NOTICE: relation already
	// exists, skipping" after CREATE TABLE IF NOT EXISTS). ORMs and
	// drivers that surface NOTICE/WARNING to the caller depend on
	// these being replayed back, so we persist them rather than
	// silently drop them. Ordering matches wire arrival. nil/omit
	// when the invocation produced no notices.
	Notices []PostgresV3Notice `json:"notices,omitempty" yaml:"notices,omitempty" bson:"notices,omitempty"`

	// Notifications captures NotificationResponse ('A') messages —
	// async LISTEN/NOTIFY deliveries the server injected into the
	// response stream. These are connection-scoped in PG (a
	// NotificationResponse can arrive between any two bundles), but
	// the capture attaches them to whichever invocation is currently
	// in flight; replay re-emits them before CommandComplete on the
	// same invocation. Notifications that arrive when no invocation
	// is pending (true async between-command delivery) are dropped
	// — supporting those would require a connection-level sideband
	// and a separate injection scheduler the replayer does not yet
	// have. nil/omit when this invocation observed none.
	Notifications []PostgresV3Notification `json:"notifications,omitempty" yaml:"notifications,omitempty" bson:"notifications,omitempty"`

	// FunctionCall captures a legacy fastpath FunctionCall ('F') +
	// FunctionCallResponse ('V') exchange. Mutually exclusive with
	// Rows / CopyOut / CopyIn — an invocation is either a SQL query
	// or a fastpath function invocation, never both. nil/omit on
	// non-FunctionCall invocations.
	FunctionCall *PostgresV3FunctionCall `json:"functionCall,omitempty" yaml:"functionCall,omitempty" bson:"function_call,omitempty"`
}

// PostgresV3Notice is one NoticeResponse ('N') message. Wire format
// mirrors ErrorResponse — same field codes (S/V/C/M/D/H/P/W) but
// severity is always one of NOTICE/WARNING/INFO/DEBUG/LOG rather than
// ERROR/FATAL/PANIC. Only the fields we observe in practice are
// persisted; unused fields stay empty/omit to keep the YAML lean.
type PostgresV3Notice struct {
	Severity string `json:"severity,omitempty" yaml:"severity,omitempty" bson:"severity,omitempty"`
	Code     string `json:"code,omitempty" yaml:"code,omitempty" bson:"code,omitempty"`
	Message  string `json:"message,omitempty" yaml:"message,omitempty" bson:"message,omitempty"`
	Detail   string `json:"detail,omitempty" yaml:"detail,omitempty" bson:"detail,omitempty"`
	Hint     string `json:"hint,omitempty" yaml:"hint,omitempty" bson:"hint,omitempty"`
}

// MarshalYAML routes Notice's free-form text fields through
// PostgresV3SafeString so the on-disk YAML never picks the literal
// block-scalar style for content that yaml.v3's parser then rejects
// inside a sequence (Notices live in PostgresV3Response.Notices,
// which is the offending shape). Field types stay `string` so JSON,
// gob, BSON, and direct Go-to-Go comparisons keep working — the
// alias is YAML-side only.
func (n PostgresV3Notice) MarshalYAML() (any, error) {
	type alias struct {
		Severity PostgresV3SafeString `yaml:"severity,omitempty"`
		Code     PostgresV3SafeString `yaml:"code,omitempty"`
		Message  PostgresV3SafeString `yaml:"message,omitempty"`
		Detail   PostgresV3SafeString `yaml:"detail,omitempty"`
		Hint     PostgresV3SafeString `yaml:"hint,omitempty"`
	}
	return alias{
		Severity: PostgresV3SafeString(n.Severity),
		Code:     PostgresV3SafeString(n.Code),
		Message:  PostgresV3SafeString(n.Message),
		Detail:   PostgresV3SafeString(n.Detail),
		Hint:     PostgresV3SafeString(n.Hint),
	}, nil
}

// PostgresV3Notification is one NotificationResponse ('A') message
// (async LISTEN/NOTIFY delivery). Wire layout: int32 backendPID +
// cstring channel + cstring payload. We persist all three so the
// replay can reproduce what the real backend produced, including
// the PID (some drivers gate on it matching the session's backend
// PID before surfacing the notification to application code).
type PostgresV3Notification struct {
	BackendPID int32  `json:"backendPID,omitempty" yaml:"backendPID,omitempty" bson:"backend_pid,omitempty"`
	Channel    string `json:"channel" yaml:"channel" bson:"channel"`
	Payload    string `json:"payload,omitempty" yaml:"payload,omitempty" bson:"payload,omitempty"`
}

// PostgresV3FunctionCall is the legacy fastpath FunctionCall ('F') →
// FunctionCallResponse ('V') pair. Modern drivers use SELECT fn(...)
// queries and never touch this API, but libpq's PQfn, pgBouncer's
// internal pings, and a few ORM internals (especially around large-
// object access) do emit it — a recorder that treats the tag as
// "unknown" would terminate the stream there.
//
// All three fields are optional in the on-disk YAML:
//   - OID identifies the server-side function (often
//     lo_creat/lo_open/lo_read/lo_write/lo_close for large-object APIs)
//   - Args preserves argument values (logical Go form, see
//     PostgresV3Cell — text or binary wire format per ArgFormats)
//   - Result holds the server's response value (NULL → Cell.Value == nil,
//     i.e. Cell.IsNull() == true; the wire-level int32(-1) sentinel is
//     translated to/from this representation by the codec)
type PostgresV3FunctionCall struct {
	// OID — server-side function OID being invoked.
	OID uint32 `json:"oid" yaml:"oid" bson:"oid"`
	// ArgFormats — per-argument wire format (0 text, 1 binary). Spec
	// allows len=0 (all text), len=1 (broadcast), len=N (per-arg);
	// we preserve exactly what the client sent.
	ArgFormats []int16 `json:"argFormats,omitempty" yaml:"argFormats,omitempty" bson:"arg_formats,omitempty"`
	// Args — ordered list of argument values. A NULL argument is
	// represented by Cell.Value == nil (Cell.IsNull() == true); non-
	// NULL args carry the logical Go value chosen by the codec for
	// the argument's OID (int64, string, []byte, etc.).
	Args []PostgresV3Cell `json:"args,omitempty" yaml:"args,omitempty" bson:"args,omitempty"`
	// ResultFormat — wire format of the result (0 text, 1 binary).
	ResultFormat int16 `json:"resultFormat,omitempty" yaml:"resultFormat,omitempty" bson:"result_format,omitempty"`
	// Result — FunctionCallResponse payload. Cell.Value == nil
	// (Cell.IsNull() == true) represents the wire int32(-1) sentinel
	// (NULL); a non-NULL result carries the codec-chosen logical Go
	// value for the function's return OID.
	Result PostgresV3Cell `json:"result,omitempty" yaml:"result,omitempty" bson:"result,omitempty"`
}

// PostgresV3CopyOutPayload captures a full server-side CopyOut burst:
// the CopyOutResponse header plus the ordered list of CopyData chunks
// the backend produced, terminated by an implicit CopyDone. The raw
// bytes are stored as []byte slices — yaml.v3 base64-encodes byte
// slices automatically, so non-printable bytes and NULs round-trip
// without sentinel games. Each element of Data is exactly one wire
// CopyData ('d') packet's body, preserved in arrival order, because
// clients observe packet boundaries when streaming the output.
type PostgresV3CopyOutPayload struct {
	// OverallFormat mirrors the first byte of CopyOutResponse. 0 = text
	// mode (psql \COPY default), 1 = binary mode. Required so the replay
	// emitter reproduces the header exactly — drivers branch on this.
	OverallFormat byte `json:"overallFormat" yaml:"overallFormat" bson:"overall_format"`
	// ColumnFormatCodes mirrors the per-column format array from
	// CopyOutResponse. len == 0 is valid (COPY of a tableless expression
	// can produce no columns). Each code is 0 = text or 1 = binary. We
	// use uint16 to match the wire encoding exactly.
	ColumnFormatCodes []uint16 `json:"columnFormatCodes,omitempty" yaml:"columnFormatCodes,omitempty" bson:"column_format_codes,omitempty"`
	// Data is the ordered list of CopyData packet bodies the server
	// produced between CopyOutResponse and CopyDone. Each []byte is one
	// packet's payload bytes (NOT concatenated); preserving packet
	// boundaries matches what a real client's wire read loop would see.
	// An empty / nil slice represents a COPY that produced zero rows.
	Data [][]byte `json:"data,omitempty" yaml:"data,omitempty" bson:"data,omitempty"`
}

// PostgresV3CopyInPayload captures only the CopyInResponse header —
// format + per-column format codes. During replay the server side
// emits CopyInResponse, then reads and discards whatever CopyData the
// client produces, then closes the sub-protocol with a CommandComplete
// + ReadyForQuery derived from the invocation's CommandComplete /
// txTransition fields. The client-produced bytes themselves are not
// persisted because they are re-generated on every replay by the
// client under test.
type PostgresV3CopyInPayload struct {
	OverallFormat     byte     `json:"overallFormat" yaml:"overallFormat" bson:"overall_format"`
	ColumnFormatCodes []uint16 `json:"columnFormatCodes,omitempty" yaml:"columnFormatCodes,omitempty" bson:"column_format_codes,omitempty"`
}

type PostgresV3ColumnDescriptor struct {
	Name       string `json:"name" yaml:"name" bson:"name"`
	TableOID   uint32 `json:"tableOid,omitempty" yaml:"tableOid,omitempty" bson:"table_oid,omitempty"`
	ColAttrNum int16  `json:"colAttrNum,omitempty" yaml:"colAttrNum,omitempty" bson:"col_attr_num,omitempty"`
	TypeOID    uint32 `json:"typeOid" yaml:"typeOid" bson:"type_oid"`
	TypeSize   int16  `json:"typeSize,omitempty" yaml:"typeSize,omitempty" bson:"type_size,omitempty"`
	TypeMod    int32  `json:"typeMod,omitempty" yaml:"typeMod,omitempty" bson:"type_mod,omitempty"`
	Format     int16  `json:"format,omitempty" yaml:"format,omitempty" bson:"format,omitempty"`
}

type PostgresV3Error struct {
	Severity string `json:"severity,omitempty" yaml:"severity,omitempty" bson:"severity,omitempty"`
	Code     string `json:"code,omitempty" yaml:"code,omitempty" bson:"code,omitempty"`
	Message  string `json:"message,omitempty" yaml:"message,omitempty" bson:"message,omitempty"`
	Detail   string `json:"detail,omitempty" yaml:"detail,omitempty" bson:"detail,omitempty"`
	Hint     string `json:"hint,omitempty" yaml:"hint,omitempty" bson:"hint,omitempty"`
}

// MarshalYAML — see PostgresV3Notice.MarshalYAML. Same rationale: SQL
// errors carry server-formatted multi-line detail/hint strings that
// can include tabs (e.g. constraint diagnostic listings), so the YAML
// emit needs the safe-string wrapper to stay round-trippable.
func (e PostgresV3Error) MarshalYAML() (any, error) {
	type alias struct {
		Severity PostgresV3SafeString `yaml:"severity,omitempty"`
		Code     PostgresV3SafeString `yaml:"code,omitempty"`
		Message  PostgresV3SafeString `yaml:"message,omitempty"`
		Detail   PostgresV3SafeString `yaml:"detail,omitempty"`
		Hint     PostgresV3SafeString `yaml:"hint,omitempty"`
	}
	return alias{
		Severity: PostgresV3SafeString(e.Severity),
		Code:     PostgresV3SafeString(e.Code),
		Message:  PostgresV3SafeString(e.Message),
		Detail:   PostgresV3SafeString(e.Detail),
		Hint:     PostgresV3SafeString(e.Hint),
	}, nil
}

type PostgresV3SideEffects struct {
	SequenceEmissions []PostgresV3SeqEmission `json:"sequenceEmissions,omitempty" yaml:"sequenceEmissions,omitempty" bson:"sequence_emissions,omitempty"`
	RowMutations      []PostgresV3RowMutation `json:"rowMutations,omitempty" yaml:"rowMutations,omitempty" bson:"row_mutations,omitempty"`
	TxTransition      string                  `json:"txTransition,omitempty" yaml:"txTransition,omitempty" bson:"tx_transition,omitempty"`
}

type PostgresV3SeqEmission struct {
	Sequence string `json:"sequence" yaml:"sequence" bson:"sequence"`
	Value    int64  `json:"value" yaml:"value" bson:"value"`
}

type PostgresV3RowMutation struct {
	Op      string            `json:"op" yaml:"op" bson:"op"`
	Schema  string            `json:"schema" yaml:"schema" bson:"schema"`
	Table   string            `json:"table" yaml:"table" bson:"table"`
	PK      []string          `json:"pk,omitempty" yaml:"pk,omitempty" bson:"pk,omitempty"`
	Columns map[string]string `json:"columns,omitempty" yaml:"columns,omitempty" bson:"columns,omitempty"`
}

// PostgresV3GeneratorSpec — one deterministic volatile-value stream
// (sequence, clock, uuid).
type PostgresV3GeneratorSpec struct {
	Name           string   `json:"name" yaml:"name" bson:"name"`
	Type           string   `json:"type" yaml:"type" bson:"type"`
	RecordedValues []string `json:"recordedValues,omitempty" yaml:"recordedValues,omitempty" bson:"recorded_values,omitempty"`
	Policy         string   `json:"policy,omitempty" yaml:"policy,omitempty" bson:"policy,omitempty"`
}

// OutputBinary store the encoded binary output of the egress calls as base64-encoded strings
type OutputBinary struct {
	Type string `json:"type" bson:"type" yaml:"type"`
	Data string `json:"data" bson:"data" yaml:"data"`
}

type OriginType string

// constant for mock origin
const (
	FromServer OriginType = "server"
	FromClient OriginType = "client"
)

type MockUsage string

const (
	Updated MockUsage = "updated"
	Deleted MockUsage = "deleted"
)

type Payload struct {
	Origin  OriginType     `json:"Origin,omitempty" yaml:"origin" bson:"origin,omitempty"`
	Message []OutputBinary `json:"Message,omitempty" yaml:"message" bson:"message,omitempty"`
}

type MockState struct {
	Name             string    `json:"name"`
	Kind             Kind      `json:"kind"`
	Usage            MockUsage `json:"usage"`
	IsFiltered       bool      `json:"isFiltered"`
	SortOrder        int64     `json:"sortOrder"`
	Type             string    `json:"type"`
	Timestamp        int64     `json:"timestamp"`
	ReqTimestampMock string    `json:"reqTimestampMock,omitempty"`
	ResTimestampMock string    `json:"resTimestampMock,omitempty"`
	// AnswerPlaced says the mock is placed at its answer (IsAnswerPlaced):
	// the replay attributes it by its response time, never by the in-flight
	// rule.
	AnswerPlaced bool `json:"answerPlaced,omitempty"`
	// Lifetime carries the recorder-derived mock tier (per-test, session,
	// connection, …) through GetConsumedMocks so the test-mode mapping
	// writer can carve out session/connection mocks from its
	// record-time-window filter — those mocks are recorded during app
	// boot before any test fires, so their ReqTimestampMock falls
	// outside every test window even when they're consumed by a test.
	// Without this, mappings.yaml comes back empty for postgres v3
	// workloads whose recorder classifies handshake / pool-warmup /
	// catalog probes as session-tier (#empty-mapping bug).
	Lifetime Lifetime `json:"lifetime,omitempty"`
	// ReqBodyNoise carries field-path request-body noise detected during
	// schema-based auto-replay matching (config.Test.SchemaNoiseDetection)
	// back from the agent to the replay service so UpdateMocks /
	// PersistMockNoise can persist it onto the mock. HTTP mocks store it on
	// HTTPReq.ReqBodyNoise; non-HTTP integrations (Pulsar/Kafka/Redis/Generic)
	// store it on the kind-agnostic MockSpec.ReqBodyNoise field. fieldpath
	// ("body.user.id") -> regex list; empty list means "ignore the whole field".
	ReqBodyNoise map[string][]string `json:"reqBodyNoise,omitempty"`
	// CarryOver marks a mock consumed outside the running test's window through
	// a path that exists for exactly that (see RegisterCarryOver): taken from
	// the carry-over tier, or a registered kind's per-test mock consumed out of
	// window through MarkMockAsUsed (a server push delivered late). The replay
	// attributes such a mock to its own recorded window — mapped to that
	// window's test, and kept from the prune whatever the running test's
	// verdict — instead of to the test that happened to be running.
	CarryOver bool `json:"carryOver,omitempty"`
	// DataPlane says the mock is a test mock (not Mock.IsProtocolPhase)
	// though its Type says otherwise: a Generic exchange its recorder stated
	// per-test keeps the type "config". Under the proven switch its
	// consumption counts toward its test's. An agent that predates it reports
	// none, and a newer replay then finds such a test mock unconsumed: the
	// agent and the CLI are one keploy.
	DataPlane bool `json:"dataPlane,omitempty"`
	// Variables carries the mock's variables (MockSpec.Variables) back from
	// the agent under the proven switch, as ReqBodyNoise carries its learned
	// noise: those the learn step of record's full round learned on it are
	// written onto the recording.
	Variables MockVariables `json:"variables,omitempty"`
	// Clocks carries the places of the mock's call the learn step learned as
	// reads of the application's clock (MockSpec.Clocks) back from the agent
	// under the proven switch, as Variables does: record's full round writes
	// them onto the recording, where a replay compares them through
	// ClockOffset.Holds. An agent that predates it learns such a place as
	// noise, as it did.
	Clocks map[string]ClockOffset `json:"clocks,omitempty"`
	// Binds are the variables the learn step learned, while answering this
	// mock, that are bound at an earlier mock of the same test (MockSpec.LearnedBinds),
	// by that mock's name: the replay writes them onto that mock.
	Binds map[string]MockVariables `json:"binds,omitempty"`
	// Concurrent says the mock answered as one of the recordings the open
	// test shares with the tests that ran beside it at record time
	// (ExactServing.Concurrent), not as one of its own: whose call it was is
	// settled at the set's end. An agent that predates it reports none.
	Concurrent bool `json:"concurrent,omitempty"`
	// LearnedAfterEnd says the parser that consumed the mock reported, after
	// the test set's learn step ended (MockFilterParams.LearnEnded: a retry
	// cycle, an out-of-order streaming pass), learning the copy exact serving
	// handed it out by did not carry: variables or binds its learn step
	// learned, or noise paths or variables beyond the recording's and the
	// round's kept learning. Its answer was served through that learning, so
	// the replay gives the tests that consumed it no verdict, with that
	// reason. An agent that predates it reports none; the agent and the CLI
	// are one keploy.
	LearnedAfterEnd bool `json:"learnedAfterEnd,omitempty"`
	// TaughtIn is, on a state record's full round's learn step kept from
	// calls made in other windows than its recording's, for learning only
	// (the agent's LearnedElsewhere), each window such a call taught it in:
	// whose window it was and when (TeachingWindow). Learning is scoped to
	// a window, so whatever reads a window's learning (undoing what a round
	// learned past a point it could not read) reads such learning as that
	// window's, not the recording owner's. Empty on every other state.
	TaughtIn []TeachingWindow `json:"taughtIn,omitempty"`
}

// ShallowCopy returns a new, unpooled Mock with m's fields: the same Spec
// (requests, responses and maps are shared, not copied) and its own
// TestModeInfo, so the copy's tree ID and sort order can be set without
// touching m.
func (m *Mock) ShallowCopy() *Mock {
	if m == nil {
		return nil
	}
	c := *m
	c.pooled = false
	return &c
}

// DeepCopy returns a new, unpooled Mock with every field of m, sharing no
// map, slice or pointer with it that a caller can edit: a copy's headers,
// form values, stream chunks, gRPC headers and messages, DNS records,
// generic packets, noise, variables and learning are its own. The one
// exception is the decoded wire messages of the database protocols (each
// Mongo, MySQL and Postgres request's and response's header, message and
// meta, and a Postgres v3 spec's contents below its parts): the copy has its
// own lists of them but shares each message. A parser writes those once, as
// it decodes them, and nothing edits one after; they are a query's rows, and
// replay copies every mock of a test's pool, so copying them would cost each
// test their whole size.
func (m *Mock) DeepCopy() *Mock {
	if m == nil {
		return nil
	}

	// Copy top-level fields explicitly to avoid copying embedded lock fields.
	// TestModeInfo is carried over whole: Lifetime + LifetimeDerived are
	// classification state, and skipping LifetimeDerived would cause
	// DeriveLifetime to re-run on the copy and double-increment
	// LegacyKindFallbackFires for untagged kinds.
	c := Mock{
		Version:      m.Version,
		Name:         m.Name,
		Kind:         m.Kind,
		Spec:         m.Spec,
		TestModeInfo: m.TestModeInfo,
		ConnectionID: m.ConnectionID,
		SourcePID:    m.SourcePID,
		Start:        m.Start,
		StartRef:     m.StartRef,
	}
	c.Spec.learned, c.Spec.learnedBinds = m.Spec.learned.Clone(), cloneBinds(m.Spec.learnedBinds)

	// Carry the loader so tree copies can still hydrate their elided response.
	c.responseHydrator = m.responseHydrator
	c.onDisk = m.onDisk
	c.storedSize = m.storedSize

	// Where the call came from (proven switch): the copy's own, so a copy
	// written later (the async gob writer's) holds what the mock held.
	c.Origin = m.Origin.clone()
	// And the repeated read it is one of (TestModeInfo.RepeatRead), its
	// own copy: ShallowCopy shares the mark, a deep copy shares nothing.
	if r := m.TestModeInfo.RepeatRead; r != nil {
		mark := *r
		c.TestModeInfo.RepeatRead = &mark
	}
	// And the test worker it came from, which record charges it by.
	c.Instance, c.EndedInstance = m.Instance, m.EndedInstance
	c.OutsideRequests = m.OutsideRequests

	// Deep copy the Noise slice so mutations to one copy don't affect the other.
	if len(m.Noise) > 0 {
		c.Noise = make([]string, len(m.Noise))
		copy(c.Noise, m.Noise)
	}

	// 2. Deep copy the map by creating a new one and copying key-value pairs.
	if m.Spec.Metadata != nil {
		c.Spec.Metadata = make(map[string]string, len(m.Spec.Metadata))
		for k, v := range m.Spec.Metadata {
			c.Spec.Metadata[k] = v
		}
	}

	// Deep copy the kind-agnostic request-body schema-noise map (used by EVERY
	// parser — HTTP and non-HTTP alike). Started from m.Spec by value above, so
	// the clone would otherwise share this map and its value slices — and
	// DeepCopy runs before async gob writes and when building runtime mock
	// pools, so a learned-noise mutation on one copy could bleed into another or
	// race a persistence read.
	if m.Spec.ReqBodyNoise != nil {
		c.Spec.ReqBodyNoise = make(map[string][]string, len(m.Spec.ReqBodyNoise))
		for k, v := range m.Spec.ReqBodyNoise {
			vc := make([]string, len(v))
			copy(vc, v)
			c.Spec.ReqBodyNoise[k] = vc
		}
	}

	// Deep copy the request→response correlations (and each ResponsePaths slice)
	// for the same reason as ReqBodyNoise: the clone must not share the slices.
	if len(m.Spec.Correlations) > 0 {
		c.Spec.Correlations = make([]FieldCorrelation, len(m.Spec.Correlations))
		copy(c.Spec.Correlations, m.Spec.Correlations)
		for i := range c.Spec.Correlations {
			if rp := m.Spec.Correlations[i].ResponsePaths; len(rp) > 0 {
				c.Spec.Correlations[i].ResponsePaths = append([]string(nil), rp...)
			}
		}
	}

	c.Spec.Variables = m.Spec.Variables.Clone()
	c.Spec.Clocks = maps.Clone(m.Spec.Clocks)

	// 3. Deep copy all slices by creating new slices and copying the elements.
	// This gives each copy its own separate backing array.
	c.Spec.GenericRequests = clonePayloads(m.Spec.GenericRequests)
	c.Spec.GenericResponses = clonePayloads(m.Spec.GenericResponses)

	c.Spec.MongoRequests = make([]MongoRequest, len(m.Spec.MongoRequests))
	copy(c.Spec.MongoRequests, m.Spec.MongoRequests)

	c.Spec.MongoResponses = make([]MongoResponse, len(m.Spec.MongoResponses))
	copy(c.Spec.MongoResponses, m.Spec.MongoResponses)

	c.Spec.MySQLRequests = make([]mysql.Request, len(m.Spec.MySQLRequests))
	copy(c.Spec.MySQLRequests, m.Spec.MySQLRequests)

	c.Spec.MySQLResponses = make([]mysql.Response, len(m.Spec.MySQLResponses))
	copy(c.Spec.MySQLResponses, m.Spec.MySQLResponses)

	c.Spec.PostgresRequestsV2 = make([]postgres.Request, len(m.Spec.PostgresRequestsV2))
	copy(c.Spec.PostgresRequestsV2, m.Spec.PostgresRequestsV2)

	c.Spec.PostgresResponsesV2 = make([]postgres.Response, len(m.Spec.PostgresResponsesV2))
	copy(c.Spec.PostgresResponsesV2, m.Spec.PostgresResponsesV2)

	// 4. Deep copy all pointers by creating a new object and copying the value.
	c.Spec.HTTPReq = m.Spec.HTTPReq.clone()
	c.Spec.HTTPResp = m.Spec.HTTPResp.clone()
	if m.Spec.Async != nil {
		asyncCopy := *m.Spec.Async
		c.Spec.Async = &asyncCopy
	}
	c.Spec.GRPCReq = m.Spec.GRPCReq.clone()
	c.Spec.GRPCResp = m.Spec.GRPCResp.clone()
	if m.Spec.ConnFailure != nil {
		connFailureCopy := *m.Spec.ConnFailure
		c.Spec.ConnFailure = &connFailureCopy
	}
	if m.Spec.Fault != nil {
		faultCopy := *m.Spec.Fault
		c.Spec.Fault = &faultCopy
	}
	if m.Spec.DNSReq != nil {
		dnsReqCopy := *m.Spec.DNSReq
		c.Spec.DNSReq = &dnsReqCopy
	}
	if m.Spec.DNSResp != nil {
		dnsRespCopy := *m.Spec.DNSResp
		if m.Spec.DNSResp.Answers != nil {
			dnsRespCopy.Answers = make([]string, len(m.Spec.DNSResp.Answers))
			copy(dnsRespCopy.Answers, m.Spec.DNSResp.Answers)
		}
		dnsRespCopy.Ns = slices.Clone(m.Spec.DNSResp.Ns)
		dnsRespCopy.Extra = slices.Clone(m.Spec.DNSResp.Extra)
		c.Spec.DNSResp = &dnsRespCopy
	}

	if m.Spec.HTTP2Req != nil {
		http2ReqCopy := *m.Spec.HTTP2Req
		if m.Spec.HTTP2Req.Headers != nil {
			http2ReqCopy.Headers = make(map[string]string, len(m.Spec.HTTP2Req.Headers))
			for k, v := range m.Spec.HTTP2Req.Headers {
				http2ReqCopy.Headers[k] = v
			}
		}
		c.Spec.HTTP2Req = &http2ReqCopy
	}
	if m.Spec.HTTP2Resp != nil {
		http2RespCopy := *m.Spec.HTTP2Resp
		if m.Spec.HTTP2Resp.Headers != nil {
			http2RespCopy.Headers = make(map[string]string, len(m.Spec.HTTP2Resp.Headers))
			for k, v := range m.Spec.HTTP2Resp.Headers {
				http2RespCopy.Headers[k] = v
			}
		}
		if m.Spec.HTTP2Resp.Trailers != nil {
			http2RespCopy.Trailers = make(map[string]string, len(m.Spec.HTTP2Resp.Trailers))
			for k, v := range m.Spec.HTTP2Resp.Trailers {
				http2RespCopy.Trailers[k] = v
			}
		}
		c.Spec.HTTP2Resp = &http2RespCopy
	}

	// PostgresV3 spec: clone the top-level discriminator plus whichever
	// sub-pointer is populated. Each sub-spec is copied by value; that
	// detaches the pointer identity so async gob-write paths and other
	// race-sensitive consumers cannot mutate the original through a
	// shared pointer. Nested slice/map fields (e.g. Query.Response.Rows)
	// are carried by value — they are treated as immutable after ingest
	// on both the record and replay sides, matching how the other
	// *Spec fields above share backing slices.
	if m.Spec.PostgresV3 != nil {
		pgV3Copy := *m.Spec.PostgresV3
		if m.Spec.PostgresV3.Session != nil {
			sessionCopy := *m.Spec.PostgresV3.Session
			pgV3Copy.Session = &sessionCopy
		}
		if m.Spec.PostgresV3.Catalog != nil {
			catalogCopy := *m.Spec.PostgresV3.Catalog
			pgV3Copy.Catalog = &catalogCopy
		}
		if m.Spec.PostgresV3.Data != nil {
			dataCopy := *m.Spec.PostgresV3.Data
			pgV3Copy.Data = &dataCopy
		}
		if m.Spec.PostgresV3.Query != nil {
			queryCopy := *m.Spec.PostgresV3.Query
			pgV3Copy.Query = &queryCopy
		}
		if m.Spec.PostgresV3.Generator != nil {
			generatorCopy := *m.Spec.PostgresV3.Generator
			pgV3Copy.Generator = &generatorCopy
		}
		c.Spec.PostgresV3 = &pgV3Copy
	}

	return &c
}

// SetLearned records on s, in memory only, the variables the learn step
// learned on its mock (vars) and those bound at earlier mocks (binds): see
// MockSpec.learned.
func (s *MockSpec) SetLearned(vars MockVariables, binds map[string]MockVariables) {
	s.learned, s.learnedBinds = vars, binds
}

// Learned is the variables the learn step learned on s's mock (SetLearned).
func (s *MockSpec) Learned() MockVariables { return s.learned }

// LearnedBinds is the variables the learn step learned while answering s's
// mock that are bound at an earlier mock, by its name (SetLearned).
func (s *MockSpec) LearnedBinds() map[string]MockVariables { return s.learnedBinds }