Skip to content

docs: tell a client that pins its own CA how to trust keploy's - #931

Draft
slayerjain wants to merge 1 commit into
mainfrom
docs/ca-pinned-clients
Draft

slayerjain wants to merge 1 commit into
mainfrom
docs/ca-pinned-clients

Conversation

@slayerjain

Copy link
Copy Markdown
Member

What has changed?

  • Added a section, "A client that pins its own CA", to the TLS page (versioned_docs/version-4.0.0/running-keploy/docker-tls.md). It covers clients that trust only a CA file they name themselves, such as Python requests with verify=<file> or a Go client with its own RootCAs.
    • It shows the warning keploy record now gives when such a client rejects Keploy's certificate.
    • It explains how to point the client at a file that holds Keploy's per-run CA as well as its own: in Docker and Compose from /tmp/keploy-tls/keploy-ca.crt, and natively from the file SSL_CERT_FILE names.
  • The warning's next_step links to this section (fix(record): say when a client rejected keploy's certificate, with the switch off keploy#4853). Until now the page covered only automatic trust injection and libraries that read the OS trust store.

Type of change

  • Documentation update (if none of the other choices apply).

How Has This Been Tested?

  • Prettier: the page passes the repo's Prettier 2.8.8 check.
  • Build: not built locally.
  • Docker / Compose: the Compose example as written was run against keploy main in a lane container. Both pinned calls answered 200 and were recorded, with no warning.
  • Native: checked that SSL_CERT_FILE names a file holding one certificate, "Keploy MITM CA".

Checklist:

  • My code follows the style guidelines of this project.
  • I have performed a self-review of my own code.

A client that trusts only a CA file it names itself (Python requests with
verify=<file>, a Go client with its own RootCAs) rejects the certificate
keploy answers its HTTPS calls with, and keploy record does not record
the call. keploy record now warns of it at the end of a recording, naming
the test cases, and its next_step links here (keploy/keploy#4853). Nothing
in the docs said what to do: the TLS page covers keploy's automatic trust
injection and OS-trust-store libraries only.

The new section "A client that pins its own CA" shows the warning and how
to point such a client at a file that holds keploy's per-run CA as well
as its own:
- in Docker and Docker Compose, from /tmp/keploy-tls/keploy-ca.crt, which
  keploy mounts in the app container;
- natively, from the file SSL_CERT_FILE names in the app's environment.

Checked against keploy main in a lane container. The compose example as
written makes both pinned calls answer 200 and be recorded, with no
warning. Natively, SSL_CERT_FILE names a file holding one certificate,
"Keploy MITM CA <run id>".

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
slayerjain added a commit to keploy/keploy that referenced this pull request Oct 19, 2026
…e switch off

A client that does not trust keploy's CA rejects the certificate keploy's
proxy answers its TLS handshake with. Examples are a client that pins its
own CA file (Python requests with verify=<file>, a Go client with its own
RootCAs) and a library that reads only the OS trust store. Its call
fails, and keploy neither records nor can replay it. With the proven
switch on, such a handshake (tls_handshake_failed) fails the recording.
With it off, keploy said nothing. The recording ended green, its test
cases held the app's error response for those calls, and a replay of
them passed by replaying that error: a silent false pass.

With the switch off, keploy record now says so once, at WARN, when the
recording ends:

  N TLS handshake(s) with keploy's proxy failed while this recording ran
  (a client that does not trust keploy's CA rejects keploy's
  certificate); those calls are not recorded

It names the test cases recorded while those handshakes failed, how many
failed after the last test case, and the last one's destination. Its
next_step points at how a client that pins its own CA trusts keploy's,
natively and in Docker (https://keploy.io/docs/running-keploy/docker-tls/
#a-client-that-pins-its-own-ca, added by keploy/docs#931). The agent
already counts these handshakes with the switch off. The watch the record
path already drives (caRejectWatch, in captureWatch) now reads that count:
- as capture begins;
- as each test case is saved (a handshake's failure is the test case's it
  was made in, models.PerTest);
- once more after the last.
The reads are made as the switch-on watch's are. A stop ends record's
context before the drain saves its last test cases and before the final
read, so the reads use a context the stop does not end; only the second
interrupt, which gives the stop's waits up, ends them. They are made one
at a time, each applied before the next is made, so a late answer is
never taken for an agent that was started again. A read that fails leaves
its count to the next read, at Debug. It changes nothing else. No test case, recording, verdict, status or exit
code differs, and with the switch on, captureWatch is as it was. It
counts only failed handshakes. A client that hung up on its handshake
(tls_handshake_abandoned) cannot be told from one that gave up, so that
is left to the proven switch. A count it cannot read is left unsaid.

Replicated in a lane container on 192.168.116.142 with keploy main, in
compose. A Flask app calls an HTTPS upstream two ways: /pinned with
verify=/certs/ca.pem (its own CA), and /env with REQUESTS_CA_BUNDLE, which
keploy's injected trust covers. Calls made: /pinned, /env, /pinned.
- Before: the record exits 0 with nothing said. The /pinned test cases hold
  the app's 500, and the app logged CERTIFICATE_VERIFY_FAILED.
- After: the record still exits 0, and says once: "2 TLS handshake(s) with
  keploy's proxy failed while this recording ran (...); those calls are
  not recorded", testCases [get-pinned-1, get-pinned-2], last "to
  172.19.0.2:8443", next_step the CA docs. get-env-1 is not named.
- A pinned call the app makes 1.5 s after its last test case's response,
  before the stop (SIGINT): the WARN says 3, with afterTheLastTestCase 1.
- Following the docs (the app's CA file combined with
  /tmp/keploy-tls/keploy-ca.crt at container start): both /pinned calls
  answer 200, are recorded, and nothing is said.

Tests (pkg/service/record/ca_reject_warn_test.go):
- with the switch off, failed handshakes during two of three test cases
  and one after the last give one WARN: 4, those two test cases, 1 after
  the last, its destination, and the docs. The recording is not failed;
- none failed: nothing said;
- a start that cannot be read: nothing said, nothing read after;
- a check that cannot be read: the next read counts what it missed, and
  a failed read is no count of 0;
- an agent started again (its count from nothing): counted;
- around the stop, with a reader that ends with its context as the
  agent client's does: a failure in a test case the drain saves, and two
  after the last, are said;
- a test case's read and the final read asked at once: the final one is
  not asked until the other is counted;
- abandoned handshakes: nothing said;
- the fields: a test case's destination, no afterTheLastTestCase with none
  after it, and with every failure after the last, no test cases named
  and "record again";
- with the switch on, the WARN is not said, and the failed handshake fails
  the recording as before.
TestTheCaptureWatchIsOffWithoutTheSwitchOrACount now expects the switch-off
watch to read the count for this WARN and to say nothing of other losses.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
slayerjain added a commit to keploy/keploy that referenced this pull request Oct 19, 2026
…e switch off

A client that does not trust keploy's CA rejects the certificate keploy's
proxy answers its TLS handshake with. Examples are a client that pins its
own CA file (Python requests with verify=<file>, a Go client with its own
RootCAs) and a library that reads only the OS trust store. Its call
fails, and keploy neither records nor can replay it. With the proven
switch on, such a handshake (tls_handshake_failed) fails the recording.
With it off, keploy said nothing. The recording ended green, its test
cases held the app's error response for those calls, and a replay of
them passed by replaying that error: a silent false pass.

With the switch off, keploy record now says so once, at WARN, when the
recording ends:

  N TLS handshake(s) with keploy's proxy failed while this recording ran
  (a client that does not trust keploy's CA rejects keploy's
  certificate); those calls are not recorded

It names the test cases recorded while those handshakes failed, how many
failed after the last test case, and the last one's destination. Its
next_step points at how a client that pins its own CA trusts keploy's,
natively and in Docker (https://keploy.io/docs/running-keploy/docker-tls/
#a-client-that-pins-its-own-ca, added by keploy/docs#931). The agent
already counts these handshakes with the switch off. The watch the record
path already drives (caRejectWatch, in captureWatch) now reads that count:
- as capture begins;
- as each test case is saved (a handshake's failure is the test case's it
  was made in, models.PerTest);
- once more after the last.
The base read is made as capture begins, on record's context, and waited
for: a stop before capture began ends it, as does the bound below, and
nothing is then watched. Every
later read is asked for and not waited for: a test case's as the consumer
saves it, and the final one once the app has stopped, at the three places
the switch-on watch makes its own final read. They are made on one
goroutine of the watch's, one at a time and in the order asked, so an
answer is never applied after a later one, and a read that lags counts
every test case saved since the last together. finish, after every drain,
waits for those still out, saying at Info that it does.

No read holds the recording or the stop's drains up; finish's wait is as
long as the reads still out, each bounded below. The watch stands no offer
of the stop's (utils.OfferStopNow): the final read is asked before the
drain in a native or docker stop, and an offer it stood there would have
the user's press give that drain up. A read is not ended by the stop, which
ends record's context before the drain saves its last test cases and
before the final read. It is ended by the interrupt that gave the stop's
waits up (utils.StopNow, a drain's own offer answered), and given up once
keploy had stall.HangBudget of its own runnable time to run without an
answer, so a stop no one presses Ctrl+C at (CI's pkill -INT) ends too, and
a starved keploy is not taken for a silent agent. A read given up, or that
failed, leaves its test cases to the next read; an answer that comes after
is not counted.

What failed after the last test case is what the final read saw beyond
the last read that counted test cases, when no such read came after it: in
a native or docker stop the final read comes before the drain saves its
last test cases, and a handshake that failed in one of those is that test
case's, and named. A count lower than the last (an agent started again) is
taken whole.

It changes nothing else: no test case, verdict, status, exit code or hook
differs, and with the switch on, captureWatch is as it was. It counts only
failed handshakes. A client that hung up on its handshake
(tls_handshake_abandoned) cannot be told from one that gave up, so that is
left to the proven switch. A count it cannot read is left unsaid (Debug).

Replicated in a lane container on 192.168.116.142 with keploy main, in
compose. A Flask app calls an HTTPS upstream two ways: /pinned with
verify=/certs/ca.pem (its own CA), and /env with REQUESTS_CA_BUNDLE, which
keploy's injected trust covers. Calls made: /pinned, /env, /pinned.
- Before: the record exits 0 with nothing said. The /pinned test cases hold
  the app's 500, and the app logged CERTIFICATE_VERIFY_FAILED.
- After: the record still exits 0, and says once: "2 TLS handshake(s) with
  keploy's proxy failed while this recording ran (...); those calls are
  not recorded", testCases [get-pinned-1, get-pinned-2], last "to
  172.19.0.2:8443", next_step the CA docs. get-env-1 is not named.
- A pinned call the app makes 1.5 s after its last test case's response,
  before the stop (SIGINT): the WARN says 3, with afterTheLastTestCase 1.
- Following the docs (the app's CA file combined with
  /tmp/keploy-tls/keploy-ca.crt at container start): both /pinned calls
  answer 200, are recorded, and nothing is said.

Tests (pkg/service/record/ca_reject_warn_test.go):
- with the switch off, failed handshakes during two of three test cases
  and one after the last give one WARN: 4, those two test cases, 1 after
  the last, its destination, and the docs. The recording is not failed;
- none failed: nothing said; a start that cannot be read: nothing said,
  nothing read after; a read that fails: the next read counts its test
  cases, and it is no count of 0; an agent started again, during the
  recording or before the final read: counted whole;
- around the stop, with a reader that ends with its context as the agent
  client's does: a failure in a test case the drain saves, and two after
  the last, are said (compose's order); in the native and docker order,
  the test case the drain saves after the final read is named, and
  nothing is after the last; a stop before capture began: no watch;
- the drain: with the final read out (never answered) in the native
  order, the ask returns at once, a press while the drain saves answers no
  offer, and the drain saves all 50 mocks the agent held; finish says it
  waits and stands no offer either. Against the previous version of this
  change, the same test saved 0 of 50, 3 runs of 3;
- a read the agent never answers: the ask returns at once, wall time that
  keploy could not run gives nothing up, and keploy's own HangBudget does,
  the test case then counted by the next read; a press that answers a
  drain's offer (utils.StopNow) ends a read at once; a read that does not
  end with its context is given up all the same, and its later answer not
  counted;
- reads asked while one is out: made after it, in order, the final one
  not asked until then; a lagging read names every test case it counts;
- no goroutine of the watch's outlives its reads (20 recordings);
- abandoned handshakes: nothing said; the fields: a test case's
  destination, no afterTheLastTestCase with none after, and with every
  failure after the last, no test cases named and "record again";
- with the switch on, the WARN is not said, and the failed handshake fails
  the recording as before.
TestTheCaptureWatchIsOffWithoutTheSwitchOrACount now expects the switch-off
watch to read the count for this WARN and to say nothing of other losses.
All 23 compile-safe mutants fail a test.

The switch-on watch (captureWatch) has the same unbounded reads: its base
and final reads with no offer standing, and its test cases' reads with no
escape at all. That is left to its own change, before the switch flips.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
slayerjain added a commit to keploy/keploy that referenced this pull request Oct 19, 2026
…e switch off

A client that does not trust keploy's CA rejects the certificate keploy's
proxy answers its TLS handshake with. Examples are a client that pins its
own CA file (Python requests with verify=<file>, a Go client with its own
RootCAs) and a library that reads only the OS trust store. Its call
fails, and keploy neither records nor can replay it. With the proven
switch on, such a handshake (tls_handshake_failed) fails the recording.
With it off, keploy said nothing. The recording ended green, its test
cases held the app's error response for those calls, and a replay of
them passed by replaying that error: a silent false pass.

With the switch off, keploy record now says so once, at WARN, when the
recording ends:

  N TLS handshake(s) with keploy's proxy failed while this recording ran
  (a client that does not trust keploy's CA rejects keploy's
  certificate); those calls are not recorded

It names the test cases recorded while those handshakes failed, how many
failed after the last test case, and the last one's destination. Its
next_step points at how a client that pins its own CA trusts keploy's,
natively and in Docker (https://keploy.io/docs/running-keploy/docker-tls/
#a-client-that-pins-its-own-ca, added by keploy/docs#931). The agent
already counts these handshakes with the switch off. The watch the record
path already drives (caRejectWatch, in captureWatch) now reads that count:
- as capture begins;
- as each test case is saved (a handshake's failure is the test case's it
  was made in, models.PerTest);
- once more after the last.
The base read is made as capture begins, on record's context, and waited
for: a stop before capture began ends it, as does the bound below, and
nothing is then watched. Every
later read is asked for and not waited for: a test case's as the consumer
saves it, and the final one once the app has stopped, at the three places
the switch-on watch makes its own final read. They are made on one
goroutine of the watch's, one at a time and in the order asked, so an
answer is never applied after a later one, and a read that lags counts
every test case saved since the last together. finish, after every drain,
waits for those still out, saying at Info that it does.

No read holds the recording or the stop's drains up; finish's wait is as
long as the reads still out, each bounded below. The watch stands no offer
of the stop's (utils.OfferStopNow): the final read is asked before the
drain in a native or docker stop, and an offer it stood there would have
the user's press give that drain up. A read is not ended by the stop, which
ends record's context before the drain saves its last test cases and
before the final read. It is ended by the interrupt that gave the stop's
waits up (utils.StopNow, a drain's own offer answered), and given up once
keploy had stall.HangBudget of its own runnable time to run without an
answer, so a stop no one presses Ctrl+C at (CI's pkill -INT) ends too, and
a starved keploy is not taken for a silent agent. A read given up, or that
failed, leaves its test cases to the next read; an answer that comes after
is not counted.

What failed after the last test case is what the final read saw beyond
the last read that counted test cases, when no such read came after it: in
a native or docker stop the final read comes before the drain saves its
last test cases, and a handshake that failed in one of those is that test
case's, and named. A count lower than the last (an agent started again) is
taken whole.

It changes nothing else: no test case, verdict, status, exit code or hook
differs, and with the switch on, captureWatch is as it was. It counts only
failed handshakes. A client that hung up on its handshake
(tls_handshake_abandoned) cannot be told from one that gave up, so that is
left to the proven switch. A count it cannot read is left unsaid (Debug).

Replicated in a lane container on 192.168.116.142 with keploy main, in
compose. A Flask app calls an HTTPS upstream two ways: /pinned with
verify=/certs/ca.pem (its own CA), and /env with REQUESTS_CA_BUNDLE, which
keploy's injected trust covers. Calls made: /pinned, /env, /pinned.
- Before: the record exits 0 with nothing said. The /pinned test cases hold
  the app's 500, and the app logged CERTIFICATE_VERIFY_FAILED.
- After: the record still exits 0, and says once: "2 TLS handshake(s) with
  keploy's proxy failed while this recording ran (...); those calls are
  not recorded", testCases [get-pinned-1, get-pinned-2], last "to
  172.19.0.2:8443", next_step the CA docs. get-env-1 is not named.
- A pinned call the app makes 1.5 s after its last test case's response,
  before the stop (SIGINT): the WARN says 3, with afterTheLastTestCase 1.
- Following the docs (the app's CA file combined with
  /tmp/keploy-tls/keploy-ca.crt at container start): both /pinned calls
  answer 200, are recorded, and nothing is said.

Tests (pkg/service/record/ca_reject_warn_test.go):
- with the switch off, failed handshakes during two of three test cases
  and one after the last give one WARN: 4, those two test cases, 1 after
  the last, its destination, and the docs. The recording is not failed;
- none failed: nothing said; a start that cannot be read: nothing said,
  nothing read after; a read that fails: the next read counts its test
  cases, and it is no count of 0; an agent started again, during the
  recording or before the final read: counted whole;
- around the stop, with a reader that ends with its context as the agent
  client's does: a failure in a test case the drain saves, and two after
  the last, are said (compose's order); in the native and docker order,
  the test case the drain saves after the final read is named, and
  nothing is after the last; a stop before capture began: no watch;
- the drain: with the final read out (never answered) in the native
  order, the ask returns at once, a press while the drain saves answers no
  offer, and the drain saves all 50 mocks the agent held; finish says it
  waits and stands no offer either. Against the previous version of this
  change, the same test saved 0 of 50, 3 runs of 3;
- a read the agent never answers: the ask returns at once, wall time that
  keploy could not run gives nothing up, and keploy's own HangBudget does,
  the test case then counted by the next read; a press that answers a
  drain's offer (utils.StopNow) ends a read at once; a read that does not
  end with its context is given up all the same, and its later answer not
  counted;
- reads asked while one is out: made after it, in order, the final one
  not asked until then; a lagging read names every test case it counts;
- no goroutine of the watch's outlives its reads (20 recordings);
- abandoned handshakes: nothing said; the fields: a test case's
  destination, no afterTheLastTestCase with none after, and with every
  failure after the last, no test cases named and "record again";
- with the switch on, the WARN is not said, and the failed handshake fails
  the recording as before.
TestTheCaptureWatchIsOffWithoutTheSwitchOrACount now expects the switch-off
watch to read the count for this WARN and to say nothing of other losses.
All 23 compile-safe mutants fail a test.

The switch-on watch (captureWatch) has the same unbounded reads: its base
and final reads with no offer standing, and its test cases' reads with no
escape at all. That is left to its own change, before the switch flips.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
slayerjain added a commit to keploy/keploy that referenced this pull request Oct 19, 2026
…e switch off

A client that does not trust keploy's CA rejects the certificate keploy's
proxy answers its TLS handshake with. Examples are a client that pins its
own CA file (Python requests with verify=<file>, a Go client with its own
RootCAs) and a library that reads only the OS trust store. Its call
fails, and keploy neither records nor can replay it. With the proven
switch on, such a handshake (tls_handshake_failed) fails the recording.
With it off, keploy said nothing. The recording ended green, its test
cases held the app's error response for those calls, and a replay of
them passed by replaying that error: a silent false pass.

With the switch off, keploy record now says so once, at WARN, when the
recording ends:

  N TLS handshake(s) with keploy's proxy failed while this recording ran
  (a client that does not trust keploy's CA rejects keploy's
  certificate); those calls are not recorded

It names the test cases recorded while those handshakes failed, how many
failed after the last test case, and the last one's destination. Its
next_step points at how a client that pins its own CA trusts keploy's,
natively and in Docker (https://keploy.io/docs/running-keploy/docker-tls/
#a-client-that-pins-its-own-ca, added by keploy/docs#931). The agent
already counts these handshakes with the switch off. The watch the record
path already drives (caRejectWatch, in captureWatch) now reads that count:
- as capture begins;
- as each test case is saved (a handshake's failure is the test case's it
  was made in, models.PerTest);
- once more after the last.
The base read is made as capture begins, on record's context, and waited
for (bounded as below): a stop during it ends it, and the watch is then
off for the recording. Every later read is asked for and not waited for:
a test case's as the consumer saves it, and the final one once the app has
stopped, at the three places the switch-on watch makes its own final read.
They are made on one goroutine of the watch's, one at a time and in the
order asked, so an answer is never applied after a later one, and a read
that lags counts every test case saved since the last together. Those
reads are not ended by the stop, which ends record's context before the
drain saves its last test cases and before the final read; the interrupt
that gave the stop's waits up (utils.StopNow, a drain's own offer
answered) ends them. Every read is given up once keploy had
stall.HangBudget of its own runnable time to run without an answer, so a
stop no one presses Ctrl+C at (CI's pkill -INT) ends too, and a starved
keploy is not taken for a silent agent. A read given up, or that failed,
leaves its test cases to the next read; an answer that comes after is not
counted.

While a drain may still save what the agent held, the watch stands no
offer of the stop's (utils.OfferStopNow): the final read is asked before
the drain in a native or docker stop, and an offer it stood there would
have the user's press give that drain up. Once the frame drains are over,
the stop waits for the reads still out while the agent is up (after
drainFrames natively, before compose takes the stack down, before the
agent is told to go), saying so at Info and offering nothing, since a
record drain may still select on utils.StopNow; each read is bounded, so
that wait is too, up to HangBudget of runnable time for each read still
out. finish, after every drain, waits for any left, and there, once it has
said so, one more Ctrl+C stops the wait now.

What failed after the last test case is what the final read saw beyond
the last read that counted test cases, when no such read came after it: in
a native or docker stop the final read comes before the drain saves its
last test cases, and a handshake that failed in one of those is that test
case's, and named, once its read comes. A test case whose read never came
(the agent went first) is named as not checked, and what the final read
saw beyond the last counted read is said to be in it or after the last
test case. A count lower than the last (an agent started again) is taken
whole; one started again whose count has passed the last by the next read
is not told from one that was not.

It changes nothing else: no test case, verdict, status, exit code or hook
differs, and with the switch on, captureWatch is as it was. It counts only
failed handshakes. A client that hung up on its handshake
(tls_handshake_abandoned) cannot be told from one that gave up, so that is
left to the proven switch. A count it cannot read is left unsaid (Debug).

Replicated in a lane container on 192.168.116.142 with keploy main, in
compose. A Flask app calls an HTTPS upstream two ways: /pinned with
verify=/certs/ca.pem (its own CA), and /env with REQUESTS_CA_BUNDLE, which
keploy's injected trust covers. Calls made: /pinned, /env, /pinned.
- Before: the record exits 0 with nothing said. The /pinned test cases hold
  the app's 500, and the app logged CERTIFICATE_VERIFY_FAILED.
- With an earlier, synchronous version of this watch (the same WARN): the
  record still exits 0, and says once: "2 TLS handshake(s) with keploy's
  proxy failed while this recording ran (...); those calls are not
  recorded", testCases [get-pinned-1, get-pinned-2], last "to
  172.19.0.2:8443", next_step the CA docs; get-env-1 is not named. A
  pinned call 1.5 s after the last test case's response, before the stop
  (SIGINT): 3, with afterTheLastTestCase 1. Following the docs (the app's
  CA file combined with /tmp/keploy-tls/keploy-ca.crt at container start):
  both /pinned calls answer 200, are recorded, and nothing is said.
  This version's reads are the unit tests' below; the lane is to be run
  again on it.

Tests (pkg/service/record/ca_reject_warn_test.go):
- with the switch off, failed handshakes during two of three test cases
  and one after the last give one WARN: 4, those two test cases, 1 after
  the last, its destination, and the docs. The recording is not failed;
- none failed: nothing said; a start that cannot be read: nothing said,
  nothing read after; a read that fails: the next read counts its test
  cases, and it is no count of 0; an agent started again, during the
  recording or before the final read: counted whole;
- around the stop, with a reader that ends with its context as the agent
  client's does: a failure in a test case the drain saves, and two after
  the last, are said (compose's order); in the native and docker order,
  the test case the drain saves after the final read is named, and
  nothing is after the last; one whose read never came is named as not
  checked, and what the final read saw beyond is said to be in it or after
  the last (and it is not named when the final read saw nothing beyond);
  a stop before capture began: no watch; a base read the agent never
  answers is given up on its stall;
- the drain: with the final read out (never answered) in the native
  order, the ask returns at once, a press while the drain saves answers no
  offer, and the drain saves all 50 mocks the agent held; the stop's wait
  for the read after the drains says it waits and stands no offer either.
  Against the previous version of this change, the same test saved 0 of
  50, 3 runs of 3; finish, after every drain, offers one more Ctrl+C,
  which ends it and the read, and withdraws the offer as it ends;
- a read the agent never answers: the ask returns at once, wall time that
  keploy could not run gives nothing up, and keploy's own HangBudget does,
  the test case then counted by the next read; a press that answers a
  drain's offer (utils.StopNow) ends a read at once; a read that does not
  end with its context is given up all the same, and its later answer not
  counted;
- reads asked while one is out: made after it, in order, the final one
  not asked until then; a lagging read names every test case it counts;
- no goroutine of the watch's outlives its reads (20 recordings); an ask
  after finish reads nothing;
- abandoned handshakes: nothing said; the fields: a test case's
  destination, no afterTheLastTestCase with none after, and with every
  failure after the last, no test cases named and "record again";
- with the switch on, the WARN is not said, and the failed handshake fails
  the recording as before.
TestTheCaptureWatchIsOffWithoutTheSwitchOrACount now expects the switch-off
watch to read the count for this WARN and to say nothing of other losses.
Of 28 compile-safe mutants, 27 fail a test; the one left drops the check
in an ask after finish, which the read goroutine's own check makes
redundant (it then ends without a read). The three calls in record.go
that wait for the reads after the frame drains have no test of their own.

The switch-on watch (captureWatch) has the same unbounded reads: its base
and final reads with no offer standing, and its test cases' reads with no
escape at all. That is left to its own change, before the switch flips.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
slayerjain added a commit to keploy/keploy that referenced this pull request Oct 19, 2026
…e switch off

A client that does not trust keploy's CA rejects the certificate keploy's
proxy answers its TLS handshake with. Examples are a client that pins its
own CA file (Python requests with verify=<file>, a Go client with its own
RootCAs) and a library that reads only the OS trust store. Its call
fails, and keploy neither records nor can replay it. With the proven
switch on, such a handshake (tls_handshake_failed) fails the recording.
With it off, keploy said nothing. The recording ended green, its test
cases held the app's error response for those calls, and a replay of
them passed by replaying that error: a silent false pass.

With the switch off, keploy record now says so once, at WARN, when the
recording ends:

  N TLS handshake(s) with keploy's proxy failed while this recording ran
  (a client that does not trust keploy's CA rejects keploy's
  certificate); those calls are not recorded

It names the test cases recorded while those handshakes failed, how many
failed after the last test case, and the last one's destination. Its
next_step points at how a client that pins its own CA trusts keploy's,
natively and in Docker (https://keploy.io/docs/running-keploy/docker-tls/
#a-client-that-pins-its-own-ca, added by keploy/docs#931). The agent
already counts these handshakes with the switch off. The watch the record
path already drives (caRejectWatch, in captureWatch) now reads that count:
- as capture begins;
- as each test case is saved (a handshake's failure is the test case's it
  was made in, models.PerTest);
- once more after the last.
The base read is made as capture begins, on record's context, and waited
for (bounded as below): a stop during it ends it, and the watch is then
off for the recording. Every later read is asked for and not waited for:
a test case's as the consumer saves it, and the final one at the three
places the switch-on watch makes its own final read (once the app has
stopped, but in a docker compose stop with the switch off, where the app
still runs, once the drain is over).
They are made on one goroutine of the watch's, one at a time and in the
order asked, so an answer is never applied after a later one, and a read
that lags counts every test case saved since the last together. Those
reads are not ended by the stop, which ends record's context before the
drain saves its last test cases and before the final read; the interrupt
that gave the stop's waits up (utils.StopNow, a drain's own offer
answered) ends them. Every read is given up once keploy had
stall.HangBudget of its own runnable time to run without an answer, so a
stop no one presses Ctrl+C at (CI's pkill -INT) ends too, and a starved
keploy is not taken for a silent agent. A read given up, or that failed,
leaves its test cases to the next read; an answer that comes after is not
counted.

While a drain may still save what the agent held, the watch stands no
offer of the stop's (utils.OfferStopNow): the final read is asked before
the drain in a native or docker stop, and an offer it stood there would
have the user's press give that drain up. Once the frame drains are over,
the stop waits for the reads still out before it lets the agent go (after
drainFrames natively, before compose takes the stack down, before the
agent is told to go: an agent that goes as its streams close may be gone
already, and its reads then fail), saying so at Info. That wait stands no
offer, since a record drain may still select on utils.StopNow: the user's
next Ctrl+C after it began (utils.InterruptedAfter) stops it, and ends the
watch's reads alone. Each read is bounded, so the wait is too: up to
HangBudget of runnable time for each read still out or asked while it
waits. finish, after every drain, waits for any left, and there, once it
has said so, one more Ctrl+C stops the wait now. A read a press ended, or
one still out as finish ends, leaves its test cases not checked.

What failed after the last test case is what the final read saw beyond
the last read that counted test cases, when no such read came after it: in
a native or docker stop the final read comes before the drain saves its
last test cases, and a handshake that failed in one of those is that test
case's, and named, once its read comes. A test case whose read never came
(the agent went first, or the user stopped the wait) is named as not
checked, and what the final read saw beyond the last counted read is said
to be in it or after the last test case. In compose's order a read that
failed leaves its test cases to the final read, which then names them for
what failed after the last too: an extra test case to re-record, never one
left out. A count lower than the last (an agent started again) is taken
whole; one started again whose count has passed the last by the next read
is not told from one that was not.

It changes nothing else: no test case, verdict, status, exit code or hook
differs, and with the switch on, captureWatch is as it was. It counts only
failed handshakes. A client that hung up on its handshake
(tls_handshake_abandoned) cannot be told from one that gave up, so that is
left to the proven switch. A count it cannot read is left unsaid (Debug).

Replicated in a lane container on 192.168.116.142 with keploy main, in
compose. A Flask app calls an HTTPS upstream two ways: /pinned with
verify=/certs/ca.pem (its own CA), and /env with REQUESTS_CA_BUNDLE, which
keploy's injected trust covers. Calls made: /pinned, /env, /pinned.
- Before: the record exits 0 with nothing said. The /pinned test cases hold
  the app's 500, and the app logged CERTIFICATE_VERIFY_FAILED.
- After (run on this commit's code): the record still exits 0, and says
  once: "2 TLS handshake(s) with keploy's proxy failed while this
  recording ran (...); those calls are not recorded", testCases
  [get-pinned-1, get-pinned-2], last "to 172.19.0.2:8443", next_step the
  CA docs; get-env-1 is not named.
- A pinned call the app makes after its last test case's response, before
  the stop (SIGINT): the WARN says 3, with afterTheLastTestCase 1.
- Following the docs (the app's CA file combined with
  /tmp/keploy-tls/keploy-ca.crt at container start): both /pinned calls
  answer 200, are recorded, and nothing is said.
- Native: SSL_CERT_FILE in the app's environment names a file holding
  keploy's per-run CA (the docs' native recipe).

Tests (pkg/service/record/ca_reject_warn_test.go):
- with the switch off, failed handshakes during two of three test cases
  and one after the last give one WARN: 4, those two test cases, 1 after
  the last, its destination, and the docs. The recording is not failed;
- none failed: nothing said; a start that cannot be read: nothing said,
  nothing read after; a read that fails: the next read counts its test
  cases, and it is no count of 0; an agent started again, during the
  recording or before the final read: counted whole;
- around the stop, with a reader that ends with its context as the agent
  client's does: a failure in a test case the drain saves, and two after
  the last, are said (compose's order); in the native and docker order,
  the test case the drain saves after the final read is named, and
  nothing is after the last; one whose read never came is named as not
  checked, and what the final read saw beyond is said to be in it or after
  the last (and it is not named when the final read saw nothing beyond);
  a stop before capture began: no watch; a base read the agent never
  answers is given up on its stall;
- the drain: with the final read out (never answered) in the native
  order, the ask returns at once, a press while the drain saves answers no
  offer, and the drain saves all 50 mocks the agent held; the stop's wait
  for the read after the drains says it waits and stands no offer either.
  Against the previous version of this change, the same test saved 0 of
  50, 3 runs of 3; finish, after every drain, offers one more Ctrl+C,
  which ends it and the read, and withdraws the offer as it ends;
- a press at finish's offer while a test case's read is out: it is named
  not checked, not dropped; the stop's wait after the frame drains stands
  no offer, and the next Ctrl+C stops it and ends even a read that does
  not end with its context; with the final read failed, unread test cases
  are named not checked beside those counted, and with nothing counted
  nothing is said;
- a read the agent never answers: the ask returns at once, wall time that
  keploy could not run gives nothing up, and keploy's own HangBudget does,
  the test case then counted by the next read; a press that answers a
  drain's offer (utils.StopNow) ends a read at once; a read that does not
  end with its context is given up all the same, and its later answer not
  counted;
- reads asked while one is out: made after it, in order, the final one
  not asked until then; a lagging read names every test case it counts;
- no goroutine of the watch's outlives its reads (20 recordings); an ask
  after finish reads nothing;
- abandoned handshakes: nothing said; the fields: a test case's
  destination, no afterTheLastTestCase with none after, and with every
  failure after the last, no test cases named and "record again";
- with the switch on, the WARN is not said, and the failed handshake fails
  the recording as before.
TestTheCaptureWatchIsOffWithoutTheSwitchOrACount now expects the switch-off
watch to read the count for this WARN and to say nothing of other losses.
Of 33 compile-safe mutants, 32 fail a test; the one left drops the check
in an ask after finish, which the read goroutine's own check makes
redundant (it then ends without a read). The three calls in record.go
that wait for the reads after the frame drains have no test of their own.

The switch-on watch (captureWatch) has the same unbounded reads: its base
and final reads with no offer standing, and its test cases' reads with no
escape at all. That is left to its own change, before the switch flips.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
slayerjain added a commit to keploy/keploy that referenced this pull request Oct 19, 2026
…e switch off

A client that does not trust keploy's CA rejects the certificate keploy's
proxy answers its TLS handshake with. Examples are a client that pins its
own CA file (Python requests with verify=<file>, a Go client with its own
RootCAs) and a library that reads only the OS trust store. Its call
fails, and keploy neither records nor can replay it. With the proven
switch on, such a handshake (tls_handshake_failed) fails the recording.
With it off, keploy said nothing. The recording ended green, its test
cases held the app's error response for those calls, and a replay of
them passed by replaying that error: a silent false pass.

With the switch off, keploy record now says so once, at WARN, when the
recording ends:

  N TLS handshake(s) with keploy's proxy failed while this recording ran
  (a client that does not trust keploy's CA rejects keploy's
  certificate); those calls are not recorded

It names the test cases recorded while those handshakes failed, how many
failed after the last test case, and the last one's destination. Its
next_step points at how a client that pins its own CA trusts keploy's,
natively and in Docker (https://keploy.io/docs/running-keploy/docker-tls/
#a-client-that-pins-its-own-ca, added by keploy/docs#931). The agent
already counts these handshakes with the switch off. The watch the record
path already drives (caRejectWatch, in captureWatch) now reads that count:
- as capture begins;
- as each test case is saved (a handshake's failure is the test case's it
  was made in, models.PerTest);
- once more after the last.
The base read is made as capture begins, on record's context, and waited
for (bounded as below): a stop during it ends it, and the watch is then
off for the recording. Every later read is asked for and not waited for:
a test case's as the consumer saves it, and the final one at the three
places the switch-on watch makes its own final read (once the app has
stopped, but in a docker compose stop with the switch off, where the app
still runs, once the drain is over).
They are made on one goroutine of the watch's, one at a time and in the
order asked, so an answer is never applied after a later one, and a read
that lags counts every test case saved since the last together. Those
reads are not ended by the stop, which ends record's context before the
drain saves its last test cases and before the final read; the interrupt
that gave the stop's waits up (utils.StopNow, a drain's own offer
answered) ends them. Every read is given up once keploy had
stall.HangBudget of its own runnable time to run without an answer, so a
stop no one presses Ctrl+C at (CI's pkill -INT) ends too, and a starved
keploy is not taken for a silent agent. A read given up, or that failed,
leaves its test cases to the next read; an answer that comes after is not
counted.

While a drain may still save what the agent held, the watch stands no
offer of the stop's (utils.OfferStopNow): the final read is asked before
the drain in a native or docker stop, and an offer it stood there would
have the user's press give that drain up. Once the frame drains are over,
the stop waits for the reads still out before it lets the agent go (after
drainFrames natively, before compose takes the stack down, before the
agent is told to go: an agent that goes as its streams close may be gone
already, and its reads then fail), saying so at Info. That wait stands no
offer, since a record drain may still select on utils.StopNow: the user's
next Ctrl+C once the run has been interrupted (utils.InterruptedAfter)
stops it, and ends the watch's reads alone; the wait says so only once it
holds (a stop no signal began, the record timer or an app that exited, has
the user's first Ctrl+C as the run's first signal, which only marks it
interrupted). Each read is bounded, so the wait is too: up to
HangBudget of runnable time for each read still out or asked while it
waits. finish, after every drain, waits for any left, and there, once it
has said so, one more Ctrl+C stops the wait now. A read a press ended, or
one still out as finish ends, leaves its test cases not checked.

What failed after the last test case is what the final read saw beyond
the last read that counted test cases, when no such read came after it: in
a native or docker stop the final read comes before the drain saves its
last test cases, and a handshake that failed in one of those is that test
case's, and named, once its read comes. A test case whose read never came
(the agent went first, or the user stopped the wait) is named as not
checked, and what the final read saw beyond the last counted read is said
to be in it or after the last test case. In compose's order a read that
failed leaves its test cases to the final read, which then names them for
what failed after the last too: an extra test case to re-record, never one
left out. A count lower than the last (an agent started again) is taken
whole; one started again whose count has passed the last by the next read
is not told from one that was not.

It changes nothing else: no test case, verdict, status, exit code or hook
differs, and with the switch on, captureWatch is as it was. It counts only
failed handshakes. A client that hung up on its handshake
(tls_handshake_abandoned) cannot be told from one that gave up, so that is
left to the proven switch. A count it cannot read is left unsaid (Debug).

Replicated in a lane container on 192.168.116.142 with keploy main, in
compose. A Flask app calls an HTTPS upstream two ways: /pinned with
verify=/certs/ca.pem (its own CA), and /env with REQUESTS_CA_BUNDLE, which
keploy's injected trust covers. Calls made: /pinned, /env, /pinned.
- Before: the record exits 0 with nothing said. The /pinned test cases hold
  the app's 500, and the app logged CERTIFICATE_VERIFY_FAILED.
- After (run on this commit's code): the record still exits 0, and says
  once: "2 TLS handshake(s) with keploy's proxy failed while this
  recording ran (...); those calls are not recorded", testCases
  [get-pinned-1, get-pinned-2], last "to 172.19.0.2:8443", next_step the
  CA docs; get-env-1 is not named.
- A pinned call the app makes after its last test case's response, before
  the stop (SIGINT): the WARN says 3, with afterTheLastTestCase 1.
- Following the docs (the app's CA file combined with
  /tmp/keploy-tls/keploy-ca.crt at container start): both /pinned calls
  answer 200, are recorded, and nothing is said.
- Native (the docs' native recipe's premise, not the WARN): SSL_CERT_FILE
  in the app's environment names a file holding keploy's per-run CA.

Tests (pkg/service/record/ca_reject_warn_test.go):
- with the switch off, failed handshakes during two of three test cases
  and one after the last give one WARN: 4, those two test cases, 1 after
  the last, its destination, and the docs. The recording is not failed;
- none failed: nothing said; a start that cannot be read: nothing said,
  nothing read after; a read that fails: the next read counts its test
  cases, and it is no count of 0; an agent started again, during the
  recording or before the final read: counted whole;
- around the stop, with a reader that ends with its context as the agent
  client's does: a failure in a test case the drain saves, and two after
  the last, are said (compose's order); in the native and docker order,
  the test case the drain saves after the final read is named, and
  nothing is after the last; one whose read never came is named as not
  checked, and what the final read saw beyond is said to be in it or after
  the last (and it is not named when the final read saw nothing beyond);
  a stop before capture began: no watch; a base read the agent never
  answers is given up on its stall;
- the drain: with the final read out (never answered) in the native
  order, the ask returns at once, a press while the drain saves answers no
  offer, and the drain saves all 50 mocks the agent held; the stop's wait
  for the read after the drains says it waits and stands no offer either.
  Against the previous version of this change, the same test saved 0 of
  50, 3 runs of 3; finish, after every drain, offers one more Ctrl+C,
  which ends it and the read, and withdraws the offer as it ends;
- a press at finish's offer while a test case's read is out: it is named
  not checked, not dropped; the stop's wait after the frame drains stands
  no offer, and the next Ctrl+C stops it and ends even a read that does
  not end with its context, with no read after; a stop no signal began
  says how to stop that wait only once the run has been interrupted, and
  the next press then does; with the final read failed, unread test cases
  are named not checked beside those counted, and with nothing counted
  nothing is said;
- a read the agent never answers: the ask returns at once, wall time that
  keploy could not run gives nothing up, and keploy's own HangBudget does,
  the test case then counted by the next read; a press that answers a
  drain's offer (utils.StopNow) ends a read at once; a read that does not
  end with its context is given up all the same, and its later answer not
  counted;
- reads asked while one is out: made after it, in order, the final one
  not asked until then; a lagging read names every test case it counts;
- no goroutine of the watch's outlives its reads (20 recordings); an ask
  after finish reads nothing;
- abandoned handshakes: nothing said; the fields: a test case's
  destination, no afterTheLastTestCase with none after, and with every
  failure after the last, no test cases named and "record again";
- with the switch on, the WARN is not said, and the failed handshake fails
  the recording as before.
TestTheCaptureWatchIsOffWithoutTheSwitchOrACount now expects the switch-off
watch to read the count for this WARN and to say nothing of other losses.
Of 35 compile-safe mutants, 34 fail a test; the one left drops the check
in an ask after finish, which the read goroutine's own check makes
redundant (it then ends without a read). The three calls in record.go
that wait for the reads after the frame drains have no test of their own.

The switch-on watch (captureWatch) has the same unbounded reads: its base
and final reads with no offer standing, and its test cases' reads with no
escape at all. That is left to its own change, before the switch flips.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
slayerjain added a commit to keploy/keploy that referenced this pull request Oct 19, 2026
…e switch off

A client that does not trust keploy's CA rejects the certificate keploy's
proxy answers its TLS handshake with. Examples are a client that pins its
own CA file (Python requests with verify=<file>, a Go client with its own
RootCAs) and a library that reads only the OS trust store. Its call
fails, and keploy neither records nor can replay it. With the proven
switch on, such a handshake (tls_handshake_failed) fails the recording.
With it off, keploy said nothing. The recording ended green, its test
cases held the app's error response for those calls, and a replay of
them passed by replaying that error: a silent false pass.

With the switch off, keploy record now says so once, at WARN, when the
recording ends:

  N TLS handshake(s) with keploy's proxy failed while this recording ran
  (a client that does not trust keploy's CA rejects keploy's
  certificate); those calls are not recorded

It names the test cases recorded while those handshakes failed, how many
failed after the last test case, and the last one's destination. Its
next_step points at how a client that pins its own CA trusts keploy's,
natively and in Docker (https://keploy.io/docs/running-keploy/docker-tls/
#a-client-that-pins-its-own-ca, added by keploy/docs#931). The agent
already counts these handshakes with the switch off. The watch the record
path already drives (caRejectWatch, in captureWatch) now reads that count:
- as capture begins;
- as each test case is saved (a handshake's failure is the test case's it
  was made in, models.PerTest);
- once more after the last.
The base read is made as capture begins, on record's context, and waited
for (bounded as below): a stop during it ends it, and the watch is then
off for the recording. Every later read is asked for and not waited for:
a test case's as the consumer saves it, and the final one at the three
places the switch-on watch makes its own final read (once the app has
stopped, but in a docker compose stop with the switch off, where the app
still runs, once the drain is over).
They are made on one goroutine of the watch's, one at a time and in the
order asked, so an answer is never applied after a later one, and a read
that lags counts every test case saved since the last together. Those
reads are not ended by the stop, which ends record's context before the
drain saves its last test cases and before the final read; the interrupt
that gave the stop's waits up (utils.StopNow, a drain's own offer
answered) ends them. Every read is given up once keploy had
stall.HangBudget of its own runnable time to run without an answer, so a
stop no one presses Ctrl+C at (CI's pkill -INT) ends too, and a starved
keploy is not taken for a silent agent. A read given up, or that failed,
leaves its test cases to the next read; an answer that comes after is not
counted.

While a drain may still save what the agent held, the watch stands no
offer of the stop's (utils.OfferStopNow): the final read is asked before
the drain in a native or docker stop, and an offer it stood there would
have the user's press give that drain up. Once the frame drains are over,
the stop waits for the reads still out before it lets the agent go (after
drainFrames natively, before compose takes the stack down, before the
agent is told to go: an agent that goes as its streams close may be gone
already, and its reads then fail), saying so at Info. That wait stands no
offer, since a record drain may still select on utils.StopNow, but a
whole notice period after it saw the run interrupted (as
utils.StopNowNotice paces its offer, so the copy of the stop's signal that
sudo hands on is not taken for a press) it hears the next Ctrl+C
(utils.InterruptedAfter), which stops it and ends the watch's reads alone.
It says so from then on, and not before; a press before then is not
heard. A stop no signal began (the record timer, an app that exited) has
the user's first Ctrl+C as the run's first signal, which begins keploy's
own stop rather than ending this wait. Each read is bounded, so the wait is too: up to
HangBudget of runnable time for each read still out or asked while it
waits. finish, after every drain, waits for any left, and there, once it
has said so, one more Ctrl+C stops the wait now. A read a press ended, or
one still out as finish ends, leaves its test cases not checked.

What failed after the last test case is what the final read saw beyond
the last read that counted test cases, when no such read came after it: in
a native or docker stop the final read comes before the drain saves its
last test cases, and a handshake that failed in one of those is that test
case's, and named, once its read comes. A test case whose read never came
(the agent went first, or the user stopped the wait) is named as not
checked, and what the final read saw beyond the last counted read is said
to be in it or after the last test case. In compose's order a read that
failed leaves its test cases to the final read, which then names them for
what failed after the last too: an extra test case to re-record, never one
left out. A count lower than the last (an agent started again) is taken
whole; one started again whose count has passed the last by the next read
is not told from one that was not.

It changes nothing else: no test case, verdict, status, exit code or hook
differs, and with the switch on, captureWatch is as it was. It counts only
failed handshakes. A client that hung up on its handshake
(tls_handshake_abandoned) cannot be told from one that gave up, so that is
left to the proven switch. A count it cannot read is left unsaid (Debug).

Replicated in a lane container on 192.168.116.142 with keploy main, in
compose. A Flask app calls an HTTPS upstream two ways: /pinned with
verify=/certs/ca.pem (its own CA), and /env with REQUESTS_CA_BUNDLE, which
keploy's injected trust covers. Calls made: /pinned, /env, /pinned.
- Before: the record exits 0 with nothing said. The /pinned test cases hold
  the app's 500, and the app logged CERTIFICATE_VERIFY_FAILED.
- After (run on this commit's code): the record still exits 0, and says
  once: "2 TLS handshake(s) with keploy's proxy failed while this
  recording ran (...); those calls are not recorded", testCases
  [get-pinned-1, get-pinned-2], last "to 172.19.0.2:8443", next_step the
  CA docs; get-env-1 is not named.
- A pinned call the app makes after its last test case's response, before
  the stop (SIGINT): the WARN says 3, with afterTheLastTestCase 1.
- Following the docs (the app's CA file combined with
  /tmp/keploy-tls/keploy-ca.crt at container start): both /pinned calls
  answer 200, are recorded, and nothing is said.
- Native (the docs' native recipe's premise, not the WARN): SSL_CERT_FILE
  in the app's environment names a file holding keploy's per-run CA.

Tests (pkg/service/record/ca_reject_warn_test.go):
- with the switch off, failed handshakes during two of three test cases
  and one after the last give one WARN: 4, those two test cases, 1 after
  the last, its destination, and the docs. The recording is not failed;
- none failed: nothing said; a start that cannot be read: nothing said,
  nothing read after; a read that fails: the next read counts its test
  cases, and it is no count of 0; an agent started again, during the
  recording or before the final read: counted whole;
- around the stop, with a reader that ends with its context as the agent
  client's does: a failure in a test case the drain saves, and two after
  the last, are said (compose's order); in the native and docker order,
  the test case the drain saves after the final read is named, and
  nothing is after the last; one whose read never came is named as not
  checked, and what the final read saw beyond is said to be in it or after
  the last (and it is not named when the final read saw nothing beyond);
  a stop before capture began: no watch; a base read the agent never
  answers is given up on its stall;
- the drain: with the final read out (never answered) in the native
  order, the ask returns at once, a press while the drain saves answers no
  offer, and the drain saves all 50 mocks the agent held; the stop's wait
  for the read after the drains says it waits and stands no offer either.
  Against the previous version of this change, the same test saved 0 of
  50, 3 runs of 3; finish, after every drain, offers one more Ctrl+C,
  which ends it and the read, and withdraws the offer as it ends;
- a press at finish's offer while a test case's read is out: it is named
  not checked, not dropped; the stop's wait after the frame drains stands
  no offer, and the next Ctrl+C stops it and ends even a read that does
  not end with its context, with no read after; a stop no signal began
  says how to stop that wait only once the run has been interrupted, and
  the next press then does; a copy of the stop's signal, coming with it,
  does not stop that wait, which hears presses only a notice period on; with the final read failed, unread test cases
  are named not checked beside those counted, and with nothing counted
  nothing is said;
- a read the agent never answers: the ask returns at once, wall time that
  keploy could not run gives nothing up, and keploy's own HangBudget does,
  the test case then counted by the next read; a press that answers a
  drain's offer (utils.StopNow) ends a read at once; a read that does not
  end with its context is given up all the same, and its later answer not
  counted;
- reads asked while one is out: made after it, in order, the final one
  not asked until then; a lagging read names every test case it counts;
- no goroutine of the watch's outlives its reads (20 recordings); an ask
  after finish reads nothing;
- abandoned handshakes: nothing said; the fields: a test case's
  destination, no afterTheLastTestCase with none after, and with every
  failure after the last, no test cases named and "record again";
- with the switch on, the WARN is not said, and the failed handshake fails
  the recording as before.
TestTheCaptureWatchIsOffWithoutTheSwitchOrACount now expects the switch-off
watch to read the count for this WARN and to say nothing of other losses.
Of 36 compile-safe mutants, 35 fail a test; the one left drops the check
in an ask after finish, which the read goroutine's own check makes
redundant (it then ends without a read). The three calls in record.go
that wait for the reads after the frame drains have no test of their own.

The switch-on watch (captureWatch) has the same unbounded reads: its base
and final reads with no offer standing, and its test cases' reads with no
escape at all. That is left to its own change, before the switch flips.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
slayerjain added a commit to keploy/keploy that referenced this pull request Oct 19, 2026
…e switch off

A client that does not trust keploy's CA rejects the certificate keploy's
proxy answers its TLS handshake with. Examples are a client that pins its
own CA file (Python requests with verify=<file>, a Go client with its own
RootCAs) and a library that reads only the OS trust store. Its call
fails, and keploy neither records nor can replay it. With the proven
switch on, such a handshake (tls_handshake_failed) fails the recording.
With it off, keploy said nothing. The recording ended green, its test
cases held the app's error response for those calls, and a replay of
them passed by replaying that error: a silent false pass.

With the switch off, keploy record now says so once, at WARN, when the
recording ends:

  N TLS handshake(s) with keploy's proxy failed while this recording ran
  (a client that does not trust keploy's CA rejects keploy's
  certificate); those calls are not recorded

It names the test cases recorded while those handshakes failed, how many
failed after the last test case, and the last one's destination. Its
next_step points at how a client that pins its own CA trusts keploy's,
natively and in Docker (https://keploy.io/docs/running-keploy/docker-tls/
#a-client-that-pins-its-own-ca, added by keploy/docs#931). The agent
already counts these handshakes with the switch off. The watch the record
path already drives (caRejectWatch, in captureWatch) now reads that count:
- as capture begins;
- as each test case is saved (a handshake's failure is the test case's it
  was made in, models.PerTest);
- once more after the last.
The base read is made as capture begins, on record's context, and waited
for (bounded as below): a stop during it ends it, and the watch is then
off for the recording. Every later read is asked for and not waited for:
a test case's as the consumer saves it, and the final one at the three
places the switch-on watch makes its own final read (once the app has
stopped, but in a docker compose stop with the switch off, where the app
still runs, once the drain is over).
They are made on one goroutine of the watch's, one at a time and in the
order asked, so an answer is never applied after a later one, and a read
that lags counts every test case saved since the last together. Those
reads are not ended by the stop, which ends record's context before the
drain saves its last test cases and before the final read (but a compose
agent that goes on its own has its drain and final read run with no stop
at all); the interrupt
that gave the stop's waits up (utils.StopNow, a drain's own offer
answered) ends them. Every read is given up once keploy had
stall.HangBudget of its own runnable time to run without an answer, so a
stop no one presses Ctrl+C at (CI's pkill -INT) ends too, and a starved
keploy is not taken for a silent agent. A read given up, or that failed,
leaves its test cases to the next read; an answer that comes after is not
counted.

While a drain may still save what the agent held, the watch stands no
offer of the stop's (utils.OfferStopNow): the final read is asked before
the drain in a native or docker stop, and an offer it stood there would
have the user's press give that drain up. Once the frame drains are over,
the stop waits for the reads still out before it lets the agent go (after
drainFrames natively, before compose takes the stack down, before the
agent is told to go: an agent that goes as its streams close may be gone
already, and its reads then fail), saying so at Info, with how long it can
take. No interrupt ends that wait. It stands no offer, since a record
drain may still select on utils.StopNow; and it hears no press of its own,
since nothing keploy can see tells the user's Ctrl+C from the copy of the
stop's own signal that sudo hands on: a signal a user process sends (CI's
`sudo pkill -INT -f keploy`) reaches keploy directly and again through sudo,
which relays it (sudo(8): SIGINT is relayed when sent by a user process, or
under a new pty), Go's os/signal gives no signal's sender (and signalfd
cannot hold SIGINT, which the runtime unblocks on every thread), and only
time could tell them apart, which a loaded host does not keep. (An escape
on a signal no stop sends, Ctrl+\, or the sender read off a kernel
tracepoint, would tell them apart; neither is worth its cost for a
diagnostic.) So its line
says that Ctrl+C does not cut it short, and each read's bound ends it: up
to HangBudget of runnable time for each read still out or asked while it
waits. finish, after every drain, waits for any left, and there, once it
has said so, one more Ctrl+C stops the wait now (a copy of the stop's
signal taken for that press costs no more than the count, as every offer
of the stop's must). A read a press ended, or one still out as finish
ends, leaves its test cases not checked, and they are named at WARN even
when nothing was counted: unknown is not none.

What failed after the last test case is what the final read saw beyond
the last read that counted test cases, when no such read came after it: in
a native or docker stop the final read comes before the drain saves its
last test cases, and a handshake that failed in one of those is that test
case's, and named, once its read comes. A test case whose read never came
(the agent went first, or the user stopped the wait) is named as not
checked, and what the final read saw beyond the last counted read is said
to be in it or after the last test case. In compose's order a read that
failed leaves its test cases to the final read, which then names them for
what failed after the last too: an extra test case to re-record, never one
left out. A count lower than the last (an agent started again) is taken
whole; one started again whose count has passed the last by the next read
is not told from one that was not.

It changes nothing else: no test case, verdict, status, exit code or hook
differs, and with the switch on, captureWatch is as it was. It counts only
failed handshakes. A client that hung up on its handshake
(tls_handshake_abandoned) cannot be told from one that gave up, so that is
left to the proven switch. A count it cannot read is left unsaid (Debug).

Replicated in a lane container on 192.168.116.142 with keploy main, in
compose. A Flask app calls an HTTPS upstream two ways: /pinned with
verify=/certs/ca.pem (its own CA), and /env with REQUESTS_CA_BUNDLE, which
keploy's injected trust covers. Calls made: /pinned, /env, /pinned.
- Before: the record exits 0 with nothing said. The /pinned test cases hold
  the app's 500, and the app logged CERTIFICATE_VERIFY_FAILED.
- After (run on this commit's code): the record still exits 0, and says
  once: "2 TLS handshake(s) with keploy's proxy failed while this
  recording ran (...); those calls are not recorded", testCases
  [get-pinned-1, get-pinned-2], last "to 172.19.0.2:8443", next_step the
  CA docs; get-env-1 is not named.
- A pinned call the app makes after its last test case's response, before
  the stop (SIGINT): the WARN says 3, with afterTheLastTestCase 1.
- Following the docs (the app's CA file combined with
  /tmp/keploy-tls/keploy-ca.crt at container start): both /pinned calls
  answer 200, are recorded, and nothing is said.
- Native (the docs' native recipe's premise, not the WARN): SSL_CERT_FILE
  in the app's environment names a file holding keploy's per-run CA.

Tests (pkg/service/record/ca_reject_warn_test.go):
- with the switch off, failed handshakes during two of three test cases
  and one after the last give one WARN: 4, those two test cases, 1 after
  the last, its destination, and the docs. The recording is not failed;
- none failed: nothing said; a start that cannot be read: nothing said,
  nothing read after; a read that fails: the next read counts its test
  cases, and it is no count of 0; an agent started again, during the
  recording or before the final read: counted whole;
- around the stop, with a reader that ends with its context as the agent
  client's does: a failure in a test case the drain saves, and two after
  the last, are said (compose's order); in the native and docker order,
  the test case the drain saves after the final read is named, and
  nothing is after the last; one whose read never came is named as not
  checked, and what the final read saw beyond is said to be in it or after
  the last (and it is not named when the final read saw nothing beyond);
  a stop before capture began: no watch; a base read the agent never
  answers is given up on its stall;
- the drain: with the final read out (never answered) in the native
  order, the ask returns at once, a press while the drain saves answers no
  offer, and the drain saves all 50 mocks the agent held; the stop's wait
  for the read after the drains says it waits and stands no offer either.
  Against the previous version of this change, the same test saved 0 of
  50, 3 runs of 3; finish, after every drain, offers one more Ctrl+C,
  which ends it and the read, and withdraws the offer as it ends;
- a press at finish's offer while a test case's read is out: it is named
  not checked, not dropped, also when nothing was counted; the stop's wait
  after the frame drains stands no offer, says Ctrl+C does not cut it
  short, and no interrupt does (the read ends on its own bound); with the
  final read failed, unread test cases are named not checked beside those
  counted, and with nothing counted they are named all the same;
- a read the agent never answers: the ask returns at once, wall time that
  keploy could not run gives nothing up, and keploy's own HangBudget does,
  the test case then counted by the next read; a press that answers a
  drain's offer (utils.StopNow) ends a read at once; a read that does not
  end with its context is given up all the same, and its later answer not
  counted;
- reads asked while one is out: made after it, in order, the final one
  not asked until then; a lagging read names every test case it counts;
- no goroutine of the watch's outlives its reads (20 recordings); an ask
  after finish reads nothing;
- abandoned handshakes: nothing said; the fields: a test case's
  destination, no afterTheLastTestCase with none after, and with every
  failure after the last, no test cases named and "record again";
- with the switch on, the WARN is not said, and the failed handshake fails
  the recording as before.
TestTheCaptureWatchIsOffWithoutTheSwitchOrACount now expects the switch-off
watch to read the count for this WARN and to say nothing of other losses.
Of 32 compile-safe mutants, 31 fail a test; the one left drops the check
in an ask after finish, which the read goroutine's own check makes
redundant (it then ends without a read). The three calls in record.go
that wait for the reads after the frame drains have no test of their own.

The switch-on watch (captureWatch) has the same unbounded reads: its base
and final reads with no offer standing, and its test cases' reads with no
escape at all. That is left to its own change, before the switch flips.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
slayerjain added a commit to keploy/keploy that referenced this pull request Oct 19, 2026
…e switch off

A client that does not trust keploy's CA rejects the certificate keploy's
proxy answers its TLS handshake with. Examples are a client that pins its
own CA file (Python requests with verify=<file>, a Go client with its own
RootCAs) and a library that reads only the OS trust store. Its call
fails, and keploy neither records nor can replay it. With the proven
switch on, such a handshake (tls_handshake_failed) fails the recording.
With it off, keploy said nothing. The recording ended green, its test
cases held the app's error response for those calls, and a replay of
them passed by replaying that error: a silent false pass.

With the switch off, keploy record now says so once, at WARN, when the
recording ends:

  N TLS handshake(s) with keploy's proxy failed while this recording ran
  (a client that does not trust keploy's CA rejects keploy's
  certificate); those calls are not recorded

It names the test cases recorded while those handshakes failed, how many
failed after the last test case, and the last one's destination. Its
next_step points at how a client that pins its own CA trusts keploy's,
natively and in Docker (https://keploy.io/docs/running-keploy/docker-tls/
#a-client-that-pins-its-own-ca, added by keploy/docs#931). The agent
already counts these handshakes with the switch off. The watch the record
path already drives (caRejectWatch, in captureWatch) now reads that count:
- as capture begins;
- as each test case is saved (a handshake's failure is the test case's it
  was made in, models.PerTest);
- once more after the last.
The base read is made as capture begins, on record's context, and waited
for (bounded as below): a stop during it ends it, and the watch is then
off for the recording. Every later read is asked for and not waited for:
a test case's as the consumer saves it, and the final one at the three
places the switch-on watch makes its own final read (once the app has
stopped, but in a docker compose stop with the switch off, where the app
still runs, once the drain is over).
They are made on one goroutine of the watch's, one at a time and in the
order asked, so an answer is never applied after a later one, and a read
that lags counts every test case saved since the last together. Those
reads are not ended by the stop, which ends record's context before the
drain saves its last test cases and before the final read (but a compose
agent that goes on its own has its drain and final read run with no stop
at all); the interrupt
that gave the stop's waits up (utils.StopNow, a drain's own offer
answered) ends them. Every read is given up once keploy had
stall.HangBudget of its own runnable time to run without an answer, so a
stop no one presses Ctrl+C at (CI's pkill -INT) ends too, and a starved
keploy is not taken for a silent agent. A read given up, or that failed,
leaves its test cases to the next read; an answer that comes after is not
counted.

While a drain may still save what the agent held, the watch stands no
offer of the stop's (utils.OfferStopNow): the final read is asked before
the drain in a native or docker stop, and an offer it stood there would
have the user's press give that drain up. Once the frame drains are over,
the stop waits for the reads still out before it lets the agent go (after
drainFrames natively, before compose takes the stack down, before the
agent is told to go: an agent that goes as its streams close may be gone
already, and its reads then fail), saying so at Info, with how long it can
take. No interrupt ends that wait. It stands no offer, since a record
drain may still select on utils.StopNow; and it hears no press of its own,
since nothing keploy can see tells the user's Ctrl+C from the copy of the
stop's own signal that sudo hands on: a signal a user process sends (CI's
`sudo pkill -INT -f keploy`) reaches keploy directly and again through sudo,
which relays it (sudo(8): SIGINT is relayed when sent by a user process, or
under a new pty), Go's os/signal gives no signal's sender (and signalfd
cannot hold SIGINT, which the runtime unblocks on every thread), and only
time could tell them apart, which a loaded host does not keep. (An escape
on a signal no stop sends, Ctrl+\, or the sender read off a kernel
tracepoint, would tell them apart; neither is worth its cost for a
diagnostic.) So its line
says that Ctrl+C does not cut it short, and each read's bound ends it: up
to HangBudget of runnable time for each read still out or asked while it
waits. finish, after every drain, waits for any left, and there, once it
has said so, one more Ctrl+C stops the wait now (a copy of the stop's
signal taken for that press costs no more than the count, as every offer
of the stop's must). A read a press ended, or one still out as finish
ends, leaves its test cases not checked, and they are named at WARN even
when nothing was counted: unknown is not none. A read that answered after
the final read was asked, with nothing beyond the last counted, covers
them (their calls came before the stop), and they are not named.

What failed after the last test case is what the final read saw beyond
the last read that counted test cases, when no such read came after it: in
a native or docker stop the final read comes before the drain saves its
last test cases, and a handshake that failed in one of those is that test
case's, and named, once its read comes. A test case whose read never came
(the agent went first, or the user stopped the wait) is named as not
checked, and what the final read saw beyond the last counted read is said
to be in it or after the last test case. In compose's order a read that
failed leaves its test cases to the final read, which then names them for
what failed after the last too: an extra test case to re-record, never one
left out. A count lower than the last (an agent started again) is taken
whole; one started again whose count has passed the last by the next read
is not told from one that was not.

It changes nothing else: no test case, verdict, status, exit code or hook
differs, and with the switch on, captureWatch is as it was. It counts only
failed handshakes. A client that hung up on its handshake
(tls_handshake_abandoned) cannot be told from one that gave up, so that is
left to the proven switch. A count it cannot read is left unsaid (Debug).

Replicated in a lane container on 192.168.116.142 with keploy main, in
compose. A Flask app calls an HTTPS upstream two ways: /pinned with
verify=/certs/ca.pem (its own CA), and /env with REQUESTS_CA_BUNDLE, which
keploy's injected trust covers. Calls made: /pinned, /env, /pinned.
- Before: the record exits 0 with nothing said. The /pinned test cases hold
  the app's 500, and the app logged CERTIFICATE_VERIFY_FAILED.
- After (run on this commit's code): the record still exits 0, and says
  once: "2 TLS handshake(s) with keploy's proxy failed while this
  recording ran (...); those calls are not recorded", testCases
  [get-pinned-1, get-pinned-2], last "to 172.19.0.2:8443", next_step the
  CA docs; get-env-1 is not named.
- A pinned call the app makes after its last test case's response, before
  the stop (SIGINT): the WARN says 3, with afterTheLastTestCase 1.
- Following the docs (the app's CA file combined with
  /tmp/keploy-tls/keploy-ca.crt at container start): both /pinned calls
  answer 200, are recorded, and nothing is said.
- Native (the docs' native recipe's premise, not the WARN): SSL_CERT_FILE
  in the app's environment names a file holding keploy's per-run CA.

Tests (pkg/service/record/ca_reject_warn_test.go):
- with the switch off, failed handshakes during two of three test cases
  and one after the last give one WARN: 4, those two test cases, 1 after
  the last, its destination, and the docs. The recording is not failed;
- none failed: nothing said; a start that cannot be read: nothing said,
  nothing read after; a read that fails: the next read counts its test
  cases, and it is no count of 0; an agent started again, during the
  recording or before the final read: counted whole;
- around the stop, with a reader that ends with its context as the agent
  client's does: a failure in a test case the drain saves, and two after
  the last, are said (compose's order); in the native and docker order,
  the test case the drain saves after the final read is named, and
  nothing is after the last; one whose read never came is named as not
  checked, and what the final read saw beyond is said to be in it or after
  the last (and it is not named when the final read saw nothing beyond);
  a stop before capture began: no watch; a base read the agent never
  answers is given up on its stall;
- the drain: with the final read out (never answered) in the native
  order, the ask returns at once, a press while the drain saves answers no
  offer, and the drain saves all 50 mocks the agent held; the stop's wait
  for the read after the drains says it waits and stands no offer either.
  Against the previous version of this change, the same test saved 0 of
  50, 3 runs of 3; finish, after every drain, offers one more Ctrl+C,
  which ends it and the read, and withdraws the offer as it ends;
- a press at finish's offer while a test case's read is out: it is named
  not checked, not dropped, also when nothing was counted; the stop's wait
  after the frame drains stands no offer, says Ctrl+C does not cut it
  short and how long it can take, and no interrupt does (it waits for the
  read, which ends on its own bound); with the final read failed, unread
  test cases are named not checked beside those counted, and with nothing
  counted they are named all the same; a read after the final one, which
  saw nothing more, covers a test case whose own read never came;
- a read the agent never answers: the ask returns at once, wall time that
  keploy could not run gives nothing up, and keploy's own HangBudget does,
  the test case then counted by the next read; a press that answers a
  drain's offer (utils.StopNow) ends a read at once; a read that does not
  end with its context is given up all the same, and its later answer not
  counted;
- reads asked while one is out: made after it, in order, the final one
  not asked until then; a lagging read names every test case it counts;
- no goroutine of the watch's outlives its reads (20 recordings); an ask
  after finish reads nothing;
- abandoned handshakes: nothing said; the fields: a test case's
  destination, no afterTheLastTestCase with none after, and with every
  failure after the last, no test cases named and "record again";
- with the switch on, the WARN is not said, and the failed handshake fails
  the recording as before.
TestTheCaptureWatchIsOffWithoutTheSwitchOrACount now expects the switch-off
watch to read the count for this WARN and to say nothing of other losses.
Of 35 compile-safe mutants, 34 fail a test; the one left drops the check
in an ask after finish, which the read goroutine's own check makes
redundant (it then ends without a read). The three calls in record.go
that wait for the reads after the frame drains have no test of their own.

The switch-on watch (captureWatch) has the same unbounded reads: its base
and final reads with no offer standing, and its test cases' reads with no
escape at all. That is left to its own change, before the switch flips.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>
slayerjain added a commit to keploy/keploy that referenced this pull request Oct 19, 2026
…e switch off

A client that does not trust keploy's CA rejects the certificate keploy's
proxy answers its TLS handshake with. Examples are a client that pins its
own CA file (Python requests with verify=<file>, a Go client with its own
RootCAs) and a library that reads only the OS trust store. Its call
fails, and keploy neither records nor can replay it. With the proven
switch on, such a handshake (tls_handshake_failed) fails the recording.
With it off, keploy said nothing. The recording ended green, its test
cases held the app's error response for those calls, and a replay of
them passed by replaying that error: a silent false pass.

With the switch off, keploy record now says so once, at WARN, when the
recording ends:

  N TLS handshake(s) with keploy's proxy failed while this recording ran
  (a client that does not trust keploy's CA rejects keploy's
  certificate); those calls are not recorded

It names the test cases recorded while those handshakes failed, how many
failed after the last test case, and the last one's destination. Its
next_step points at how a client that pins its own CA trusts keploy's,
natively and in Docker (https://keploy.io/docs/running-keploy/docker-tls/
#a-client-that-pins-its-own-ca, added by keploy/docs#931). The agent
already counts these handshakes with the switch off. The watch the record
path already drives (caRejectWatch, in captureWatch) now reads that count:
- as capture begins;
- as each test case is saved (a handshake's failure is the test case's it
  was made in, models.PerTest);
- once more after the last.
The base read is made as capture begins, on record's context, and waited
for (bounded as below): a stop during it ends it, and the watch is then
off for the recording. Every later read is asked for and not waited for:
a test case's as the consumer saves it, and the final one at the three
places the switch-on watch makes its own final read (once the app has
stopped, but in a docker compose stop with the switch off, where the app
still runs, once the drain is over).
They are made on one goroutine of the watch's, one at a time and in the
order asked, so an answer is never applied after a later one, and a read
that lags counts every test case saved since the last together. Those
reads are not ended by the stop, which ends record's context before the
drain saves its last test cases and before the final read (but a compose
agent that goes on its own has its drain and final read run with no stop
at all); the interrupt
that gave the stop's waits up (utils.StopNow, a drain's own offer
answered) ends them. Every read is given up once keploy had
stall.HangBudget of its own runnable time to run without an answer, so a
stop no one presses Ctrl+C at (CI's pkill -INT) ends too, and a starved
keploy is not taken for a silent agent. A read given up, or that failed,
leaves its test cases to the next read; an answer that comes after is not
counted.

While a drain may still save what the agent held, the watch stands no
offer of the stop's (utils.OfferStopNow): the final read is asked before
the drain in a native or docker stop, and an offer it stood there would
have the user's press give that drain up. Once the frame drains are over,
the stop waits for the reads still out before it lets the agent go (after
drainFrames natively, before compose takes the stack down, before the
agent is told to go: an agent that goes as its streams close may be gone
already, and its reads then fail), saying so at Info, with each read's
bound. No interrupt ends that wait. It stands no offer, since a record
drain may still select on utils.StopNow; and it hears no press of its own,
since nothing keploy can see tells the user's Ctrl+C from the copy of the
stop's own signal that sudo hands on: a signal a user process sends (CI's
`sudo pkill -INT -f keploy`) reaches keploy directly and again through sudo,
which relays it (sudo(8): SIGINT is relayed when sent by a user process, or
under a new pty), Go's os/signal gives no signal's sender (and signalfd
cannot hold SIGINT, which the runtime unblocks on every thread), and only
time could tell them apart, which a loaded host does not keep. (An escape
on a signal no stop sends, Ctrl+\, or the sender read off a kernel
tracepoint, would tell them apart; neither is worth its cost for a
diagnostic.) So its line
says that Ctrl+C does not cut it short, and each read's bound ends it: up
to HangBudget of runnable time for each read still out or asked while it
waits. finish, after every drain, waits for any left, and there, once it
has said so, one more Ctrl+C stops the wait now (a copy of the stop's
signal taken for that press costs no more than the count, as every offer
of the stop's must). A read a press ended, or one still out as finish
ends, leaves its test cases not checked, and they are named at WARN even
when nothing was counted: unknown is not none. A read made after the
final read was asked (taken after it, not merely answered after it), with
nothing beyond the last counted, covers them (their calls came before the
stop), and they are not named.

What failed after the last test case is what the final read saw beyond
the last read that counted test cases, when no such read came after it: in
a native or docker stop the final read comes before the drain saves its
last test cases, and a handshake that failed in one of those is that test
case's, and named, once its read comes. A test case whose read never came
(the agent went first, or the user stopped the wait) is named as not
checked, and what the final read saw beyond the last counted read is said
to be in it or after the last test case. In compose's order a read that
failed leaves its test cases to the final read, which then names them for
what failed after the last too: an extra test case to re-record, never one
left out. A count lower than the last (an agent started again) is taken
whole; one started again whose count has passed the last by the next read
is not told from one that was not.

It changes nothing else: no test case, verdict, status, exit code or hook
differs, and with the switch on, captureWatch is as it was. It counts only
failed handshakes. A client that hung up on its handshake
(tls_handshake_abandoned) cannot be told from one that gave up, so that is
left to the proven switch. A count it cannot read is left unsaid (Debug).

Replicated in a lane container on 192.168.116.142 with keploy main, in
compose. A Flask app calls an HTTPS upstream two ways: /pinned with
verify=/certs/ca.pem (its own CA), and /env with REQUESTS_CA_BUNDLE, which
keploy's injected trust covers. Calls made: /pinned, /env, /pinned.
- Before: the record exits 0 with nothing said. The /pinned test cases hold
  the app's 500, and the app logged CERTIFICATE_VERIFY_FAILED.
- After (run on this commit's code): the record still exits 0, and says
  once: "2 TLS handshake(s) with keploy's proxy failed while this
  recording ran (...); those calls are not recorded", testCases
  [get-pinned-1, get-pinned-2], last "to 172.19.0.2:8443", next_step the
  CA docs; get-env-1 is not named.
- A pinned call the app makes after its last test case's response, before
  the stop (SIGINT): the WARN says 3, with afterTheLastTestCase 1.
- Following the docs (the app's CA file combined with
  /tmp/keploy-tls/keploy-ca.crt at container start): both /pinned calls
  answer 200, are recorded, and nothing is said.
- Native (the docs' native recipe's premise, not the WARN): SSL_CERT_FILE
  in the app's environment names a file holding keploy's per-run CA.

Tests (pkg/service/record/ca_reject_warn_test.go):
- with the switch off, failed handshakes during two of three test cases
  and one after the last give one WARN: 4, those two test cases, 1 after
  the last, its destination, and the docs. The recording is not failed;
- none failed: nothing said; a start that cannot be read: nothing said,
  nothing read after; a read that fails: the next read counts its test
  cases, and it is no count of 0; an agent started again, during the
  recording or before the final read: counted whole;
- around the stop, with a reader that ends with its context as the agent
  client's does: a failure in a test case the drain saves, and two after
  the last, are said (compose's order); in the native and docker order,
  the test case the drain saves after the final read is named, and
  nothing is after the last; one whose read never came is named as not
  checked, and what the final read saw beyond is said to be in it or after
  the last (and it is not named when the final read saw nothing beyond);
  a stop before capture began: no watch; a base read the agent never
  answers is given up on its stall;
- the drain: with the final read out (never answered) in the native
  order, the ask returns at once, a press while the drain saves answers no
  offer, and the drain saves all 50 mocks the agent held; the stop's wait
  for the read after the drains says it waits and stands no offer either.
  Against the previous version of this change, the same test saved 0 of
  50, 3 runs of 3; finish, after every drain, offers one more Ctrl+C,
  which ends it and the read, and withdraws the offer as it ends;
- a press at finish's offer while a test case's read is out: it is named
  not checked, not dropped, also when nothing was counted; the stop's wait
  after the frame drains stands no offer, says Ctrl+C does not cut it
  short and each read's bound, and no interrupt does (it waits for the
  read, which ends on its own bound); with the final read failed, unread
  test cases are named not checked beside those counted, and with nothing
  counted they are named all the same; a read made after the final one,
  which saw nothing more, covers a test case whose own read never came,
  and one taken before it, answered after, covers nothing;
- a read the agent never answers: the ask returns at once, wall time that
  keploy could not run gives nothing up, and keploy's own HangBudget does,
  the test case then counted by the next read; a press that answers a
  drain's offer (utils.StopNow) ends a read at once; a read that does not
  end with its context is given up all the same, and its later answer not
  counted;
- reads asked while one is out: made after it, in order, the final one
  not asked until then; a lagging read names every test case it counts;
- no goroutine of the watch's outlives its reads (20 recordings); an ask
  after finish reads nothing;
- abandoned handshakes: nothing said; the fields: a test case's
  destination, no afterTheLastTestCase with none after, and with every
  failure after the last, no test cases named and "record again";
- with the switch on, the WARN is not said, and the failed handshake fails
  the recording as before.
TestTheCaptureWatchIsOffWithoutTheSwitchOrACount now expects the switch-off
watch to read the count for this WARN and to say nothing of other losses.
Of 36 compile-safe mutants, 35 fail a test; the one left drops the check
in an ask after finish, which the read goroutine's own check makes
redundant (it then ends without a read). The three calls in record.go
that wait for the reads after the frame drains have no test of their own.

The switch-on watch (captureWatch) has the same unbounded reads: its base
and final reads with no offer standing, and its test cases' reads with no
escape at all. That is left to its own change, before the switch flips.

Signed-off-by: slayerjain <shubhamkjain@outlook.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant