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., // header., url.query.). A Mongo mock's are section.., // 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:") 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 ":" 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 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 '' 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 }