pkg/models/mock.go
Rama main · réplica de lectura del repositorio original.
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 }