<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
 
 <title>Paul Hammant's blog</title>
 <link href="https://paulhammant.com/atom.xml" rel="self"/>
 <link href="https://paulhammant.com"/>
 <rights>Copyright (c) 2002 - 2015, Paul James Hammant</rights>
 <updated>2026-04-02T22:13:26+00:00</updated>
 <id>https://paulhammant.com</id>
 <author>
   <name>Paul Hammant</name>
   <email>paul@hammant.org</email>
 </author>

 
 <entry>
   <title>Live Verify</title>
   <link href="https://paulhammant.com/2026/03/17/live-verify/"/>
   <updated>2026-03-17T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2026/03/17/live-verify</id>
   <content type="html">&lt;p&gt;Nobody cares about a thing in the tech world until it is successful, and I have something here that could be, but isn’t yet. Its a brand new idea and has an immense chasm to cross. Most of it is an adoption/patronage chasm, but there are technical challenges too.&lt;/p&gt;

&lt;p&gt;I’ve blogged before on this enough to have timed out any change of filing a patent on it. So there’s no way I could make a SaaS that
would have some exclusivity to operate based on patent. Not that patents really protect business ideas these days anyway.&lt;/p&gt;

&lt;h1 id=&quot;live-verify-links&quot;&gt;Live verify links&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;GitHub home page: &lt;a href=&quot;https://live-verify.github.io/live-verify/&quot;&gt;live-verify.github.io/live-verify/&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;GitHub Repo: &lt;a href=&quot;https://github.com/live-verify/live-verify&quot;&gt;github.com/live-verify/live-verify&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is me and ClaudeCode mostly. Me reminding Claude that I really really like automated tests.&lt;/p&gt;

&lt;p&gt;The home page has screenshots of live-verify in action (before, after verification, trust statement), and some real videos of tests running. Some of those videos are me showing camera apps, and some are voiceless screen recordings of a larger multi-tier simulation of all the tech pieces. The home page also links to 480 or so use cases (and has a search feature).&lt;/p&gt;

&lt;h1 id=&quot;what-is-it&quot;&gt;What is it?&lt;/h1&gt;

&lt;p&gt;A system to verify a document or part of a document in your hands that might be way away from the system that produced it. And verify means a claim within - could be just a rectangle of text mid way through the document. Could be 20 rectangles and 20 verifications. The could also be from something that’s printed and in front of you, using a camera-using phone app. Perhaps only for small rectangles of text. Most of the time you’ve the document as a digital document. Say a PDF or a web page. In that case the verification is via a Chrome extension. In time the same tech is built into Adobe, Outlook, MsWord etc. At some point it is in the OS and the need for a chrome extension disappears.&lt;/p&gt;

&lt;p&gt;There’s 480 use cases that are listed. Maybe that’s only 280 after consolidation.  Uses cases are anti-fraud, safety, and accountability. To say more, use cases cover preventing document fraud (fake certificates, forged receipts), verifying safety credentials (building inspectors, healthcare workers, equipment compliance), and enforcing accountability (audit trails, regulatory compliance, chain of custody). “Accountability” is a catch-all though. It captures the compliance, ethics, audit, and delegated-authority themes that don’t fit neatly under fraud or safety.&lt;/p&gt;

&lt;p&gt;Most of the time it is about reducing costs of things by rolling back fraud, because fraudulent claims would now be easier and quicker to identify and avoid the consequences of. Yes, “claim” in an insurance thing, but I’m talking about more general use like this claim “I, Jimmy Cricket, worked at Microsoft in Seattle from 2010 to 2013 in the DevOps team”, or “Mr Alex E Spooner earned his millions from his family’s corner shop and not from selling drugs at all. He would make a great investor”.  Other uses, could be ID systems with a camera app confirming the person with the eInk badge-ID is actually a police officer. That’d need rules of conduct for those verification moments.&lt;/p&gt;

&lt;p&gt;The system rests on Human eyes being quickly able to scan a claim, and wonder “I would trust this if only there was a way of verifying it as true”, then a button press and some maths suggesting it is true (is “verified”) and a chain of trust that human eyes can also quickly scan toward another trust or not trust decision.  And a system that’s much more trustworthy than presentation and use of QR code.&lt;/p&gt;

&lt;p&gt;No personal data ever leaves your device. The text is captured, normalized, and hashed entirely on-device. Only the hash — a one-way fingerprint that reveals nothing about the document — is sent over the network. The verification endpoint never sees your degree certificate, salary receipt, or passport. Tens of thousands of cryptography PhDs could testify in court that hashing is irreversible.&lt;/p&gt;

&lt;p&gt;The maths is hashing - SHA256 by default, but could be stronger or weaker. Some education needed for ordinary people to broadly understand why it is one-way. The hashes are all in public and not indexed. You know the hash, you can see the payload (default would be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{ &quot;status&quot;:&quot;verified&quot; }&lt;/code&gt;). You don’t know the hash, you can’t discover it, subject to the plain-text’s entropy.&lt;/p&gt;

&lt;p&gt;Revocation is built in. “Verified” isn’t permanent. An issuer can change the status to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revoked&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;suspended&lt;/code&gt;, or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;expired&lt;/code&gt; at any time — just update what the endpoint returns. A doctor loses their license, a certificate is superseded, an employee is terminated: the next person who verifies sees the current status, not a stale “OK.” This is what static digital signatures can’t do.&lt;/p&gt;

&lt;p&gt;Trust rests on the domain, not the app. There’s no central authority or certificate registry. You decide whether &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed.ac.uk&lt;/code&gt; is an authority for Edinburgh degrees, or whether &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;coned.com&lt;/code&gt; is an authority for utility worker badges. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;verify:&lt;/code&gt; line on the document tells you whose domain is making the claim — and that domain can declare who authorized it. For example, in the automated tests there’s a &lt;strong&gt;James Whitfield bank statement&lt;/strong&gt; from &lt;strong&gt;Meridian National Bank&lt;/strong&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;verify:meridian-national.bank.us/statements&lt;/code&gt;. The extension verifies the hash, then walks the authority chain: Meridian National Bank says it’s authorized by the &lt;strong&gt;FDIC&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fdic.gov&lt;/code&gt;), which in turn is authorized by the &lt;strong&gt;US Treasury&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;treasury.gov&lt;/code&gt;). The verifier sees the full chain and decides whether to trust it.&lt;/p&gt;

&lt;p&gt;So we have &lt;strong&gt;two camera-using phone apps&lt;/strong&gt;, and a &lt;strong&gt;Chrome extension&lt;/strong&gt;. Ideally we’d go on to make some plugin for Acrobat and Outlook and more. The Chrome extension is working well, but the camera-apps have upper limits. One is size of “document” to be verified which is understandable as OCR though a camera lens isn’t perfect. The other is tabular data - read on for more on that.&lt;/p&gt;

&lt;p&gt;I wish there was more shared code, between the implementations:&lt;/p&gt;

&lt;table class=&quot;table table-striped&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Platform&lt;/th&gt;
      &lt;th&gt;Normalization&lt;/th&gt;
      &lt;th&gt;Hashing&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Web app&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;public/normalize.js&lt;/code&gt;)&lt;/td&gt;
      &lt;td&gt;JavaScript (canonical)&lt;/td&gt;
      &lt;td&gt;Web Crypto API&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Chrome extension&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shared/normalize.js&lt;/code&gt;)&lt;/td&gt;
      &lt;td&gt;JavaScript (auto-generated copy via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scripts/sync-shared.js&lt;/code&gt;)&lt;/td&gt;
      &lt;td&gt;Web Crypto API&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;iOS app&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;JavaScript via JSBridge (runs normalize.js directly)&lt;/td&gt;
      &lt;td&gt;CryptoKit (native Swift)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Android app&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;JavaScript via Rhino (runs transpiled normalize.js)&lt;/td&gt;
      &lt;td&gt;Native &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MessageDigest&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The web-app version was were I started with this some months ago. It used Tesseract OCR and would run online just asking for the camera. Tesseract (via WASM) was really problematic so I shifted to proper apps ahead of schedule.&lt;/p&gt;

&lt;p&gt;I also have a reference backend that we use in the built-in automated tests. The possibly hundreds of SaaS companies that supply into this space may make different choices. The value is the open standard here.&lt;/p&gt;

&lt;h2 id=&quot;post-verification-actions&quot;&gt;Post-Verification Actions&lt;/h2&gt;

&lt;p&gt;Verification doesn’t have to stop at “verified.” There are two sources of follow-up actions.&lt;/p&gt;

&lt;p&gt;The first is the &lt;strong&gt;app or phone itself&lt;/strong&gt;. If you’re scanning a coffee shop receipt and you have Expensify installed, the app can offer “Send to Expensify?” — that’s a client-side decision based on context, and a choice for you to accept or ignore. The receipt issuer doesn’t know or control what’s offered. The second is the &lt;strong&gt;verification endpoint&lt;/strong&gt; deliberately returning actions in its response. A building inspector’s badge could offer a form for the homeowner to record visit details (areas accessed, duration, any concerns). A lawyer’s credentials could link to the bar association’s public disciplinary record. These are issuer choices — the endpoint decides what to offer.&lt;/p&gt;

&lt;p&gt;The endpoint pattern scales from light (a link) to strong (a POST form for reporting). The strong version matters where there’s a power dynamic — an inspector at your door, a healthcare worker with a vulnerable patient. The verification response tells the verifier “you may record details of this interaction” and explicitly says they will never be told not to.&lt;/p&gt;

&lt;h2 id=&quot;current-tech-problems&quot;&gt;Current Tech Problems&lt;/h2&gt;

&lt;p&gt;Live Verify’s camera mode works beautifully for &lt;strong&gt;prose documents&lt;/strong&gt; — certificates, references, claims where text flows continuously line by line. Point your phone, tap verify, done.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;tabular data&lt;/strong&gt; — receipts, invoices, anything with left-aligned descriptions and right-aligned prices — breaks the pipeline. Many of the use cases involve tabular data.&lt;/p&gt;

&lt;h3 id=&quot;what-works-prose&quot;&gt;What Works: Prose&lt;/h3&gt;

&lt;p&gt;An employment reference like this verifies perfectly:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;I, Paul Hammant, worked for Kevin Behr in
his role as CIO of HedgeServ in New York City
in 2015 and 2016
verify:paulhammant.com/refs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The text flows left-to-right with no gaps. The OCR engine (Apple Vision on iOS, Google ML Kit on Android) sees one contiguous block of text. Verification succeeds.&lt;/p&gt;

&lt;h3 id=&quot;what-breaks-tabular-receipts&quot;&gt;What Breaks: Tabular Receipts&lt;/h3&gt;

&lt;p&gt;A coffee shop receipt like this fails:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Flat White                £3.40
Almond Croissant          £3.25
SUBTOTAL:                 £6.65
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/live_verify_snafu-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Where a human sees one line, the OCR engine sees two separate text blocks — “Flat White” and “£3.40” — because the visual gap signals “these are separate regions.” A receipt that should be one block becomes 10+ fragments. Both iOS and Android camera apps I have made attempting to use Live-Text features of the OS show up to 10 fragments. And that’s the best case sometimes there’s 7 and some crucial numbers on the right hand side are omitted completely.&lt;/p&gt;

&lt;h3 id=&quot;where-this-leaves-us&quot;&gt;Where This Leaves Us&lt;/h3&gt;

&lt;table class=&quot;table table-striped&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Document Type&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Clip Mode (Browser)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Camera (Android)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Camera (iOS)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Short prose (peer references, badge claims)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Works&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Works&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Works&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Longer prose (certificates, full letters)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Works&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Works (OCR errors creep in)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Works (OCR errors creep in)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Tabular (receipts, invoices)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Works&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Works (stitching)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Broken (single-rectangle)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Clip mode&lt;/strong&gt; — the browser extension — handles everything perfectly because it operates on digital text, not pixels. No OCR, no rectangle fragmentation.&lt;/p&gt;

&lt;h3 id=&quot;the-real-fix-registration-marks-for-tabular-data&quot;&gt;The Real Fix: Registration Marks for Tabular Data&lt;/h3&gt;

&lt;p&gt;Our stitching on Android is a workaround. It handles the common case but it’s fragile — font size changes, multi-column layouts, and unusual spacing can all defeat Y-coordinate grouping.&lt;/p&gt;

&lt;p&gt;The proper solution is for Apple and Google to support &lt;strong&gt;registration marks for tabular data&lt;/strong&gt; in their OCR APIs. Two marks at diagonal corners define a bounding rectangle. The OCR engine sees the marks, treats everything inside as one text block, and strips the marks from the output — like a QR finder pattern that the camera uses for orientation but doesn’t include in the payload.&lt;/p&gt;

&lt;p&gt;We already have one mark: the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfy:&lt;/code&gt; line at the bottom-left of every verifiable region. A Unicode corner character — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;⌝&lt;/code&gt; (U+231D, upper right corner) — on the first line at the right margin provides the opposing corner:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;8 Market Square                 ⌝
Henley-on-Thames RG9 2AA
Flat White                  £3.40
Almond Croissant            £3.25
SUBTOTAL:                   £6.65
vfy:r.the-daily-grind.co.uk
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It’s already in the pic I took above.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;⌝&lt;/code&gt; is a control mark, not content. The OCR engine consumes it for bounds detection and omits it from the text output, just as it would omit a QR finder square. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfy:&lt;/code&gt; line stays — it’s already part of the verification protocol and already in the OCR output.&lt;/p&gt;

&lt;p&gt;Why a Unicode character rather than an image or a special printed mark? Because it works everywhere text works: HTML, PDF, LaTeX, Word, thermal receipt printers. Any system that can render U+231D can print the mark. No image embedding, no special font, no binary format dependency.&lt;/p&gt;

&lt;p&gt;This is future work — it requires Apple and Google to recognize the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;⌝&lt;/code&gt; + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfy:&lt;/code&gt; pair as a single region boundary in their Vision and ML Kit frameworks. But the convention is simple, the marks are unobtrusive on the printed document, and the benefit is large: receipts, invoices, lab results, bank statements, and every other tabular document becomes verifiable by camera.&lt;/p&gt;

&lt;p&gt;Until then, camera apps work better for prose, and clip mode works for everything - including most of the anti-fraud cases.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Did You Send This - for module phone SMS/Voice</title>
   <link href="https://paulhammant.com/2025/11/30/did-you-send-this-challenge/"/>
   <updated>2025-11-30T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2025/11/30/did-you-send-this-challenge</id>
   <content type="html">&lt;p&gt;This article is a comprehensive 2025 update to the original 2006 post:
“&lt;a href=&quot;/blog/did-you-send-this.html&quot;&gt;Did you send this - another weapon against spam?&lt;/a&gt;”&lt;/p&gt;

&lt;h2 id=&quot;applying-dyst-to-mobile-communication&quot;&gt;Applying DYST to Mobile Communication&lt;/h2&gt;

&lt;p&gt;The original DYST idea could theoretically be adapted by Google and Apple,
who could implement it unilaterally for mobile calls and messages. Rather than
relying on telecommunication carriers, they could use their existing TCP/IP
infrastructure for verification. An app on the sending device would get a
temporary token from an Apple/Google server, and the receiving device would check
that token with the same central server before alerting the user.&lt;/p&gt;

&lt;h3 id=&quot;dyst-supporting-phone-or-ios-or-android&quot;&gt;DYST supporting phone or (iOS or Android)&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Here is a sequence diagram illustrating the back-channel exchange for a genuine call&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant AliceTeleco as Alice&apos;s&amp;lt;br&amp;gt;Teleco
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Alice-&amp;gt;&amp;gt;AliceTeleco: 1. Alice actually&amp;lt;br&amp;gt;Initiates call
    AliceTeleco-&amp;gt;&amp;gt;Bob: Call Signalling
    activate Bob
    Note over Bob: Receives signalling,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is fully DYST-enabled.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge
    activate Alice
    Note over Alice: Alice does not sees&amp;lt;br/&amp;gt;&quot;Are you placing a call&amp;lt;br&amp;gt;to &amp;lt;bobs phone numb&amp;gt;&quot;&amp;lt;br&amp;gt;question. And handset&amp;lt;br&amp;gt;silently confirms she is
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Confirmed)
    deactivate Alice
    Note over Alliance: Alice&apos;s auto&amp;lt;br&amp;gt;confirmation received.
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Success
    deactivate Alliance
    activate Bob
    Note over Bob: Now rings or displays&amp;lt;br&amp;gt;message. Call/Text&amp;lt;br&amp;gt;connection established.&amp;lt;br/&amp;gt;Phone buzzes or rings
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If Bob is in the address book, then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;bobs phone numb&amp;gt;&lt;/code&gt; is replaced with Bob’s name&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here is a sequence diagram illustrating the back-channel exchange for a genuine Apple/Google “Messages” (not SMS)&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Alice-&amp;gt;&amp;gt;Bob: 1. Alice actually&amp;lt;br&amp;gt;sends text msg
    activate Bob
    Note over Bob: Receives signalling,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is fully DYST-enabled.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge
    activate Alice
    Note over Alice: Alice does not see&amp;lt;br/&amp;gt;&quot;Did you send this&amp;lt;br&amp;gt;to &amp;lt;bob phone num&amp;gt;&quot;&amp;lt;br&amp;gt;question. And handset&amp;lt;br&amp;gt;silently confirms she did
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Confirmed)
    deactivate Alice
    Note over Alliance: Alice&apos;s auto&amp;lt;br&amp;gt;confirmation received.
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Success
    deactivate Alliance
    activate Bob
    Note over Bob: Now rings or displays&amp;lt;br&amp;gt;message. Call/Text&amp;lt;br&amp;gt;connection established.&amp;lt;br/&amp;gt;Phone buzzes
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Apple launched the iPhone with iMessage (for other iPhone users). Google did the same for Android uses, and these 
days Rich Communication Services (RCS) allows both to interoperate. In the diagram above the teleco’s systems are not shown because they are
not involved for the routing of “smart” (non SMS) messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here is a sequence diagram illustrating the back-channel exchange for a fake call&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Eve as Eve&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant EveVOIP as Eve&apos;s VoIP&amp;lt;br&amp;gt;Gateway
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Eve-&amp;gt;&amp;gt;EveVOIP: 1. Eve actually&amp;lt;br&amp;gt;Initiates call&amp;lt;br&amp;gt;caller-id = Alice
    EveVOIP-&amp;gt;&amp;gt;Bob: Call Signalling
    activate Bob
    Note over Bob: Receives signalling,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is fully DYST-enabled.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge
    activate Alice
    Note over Alice: Alice does not see&amp;lt;br/&amp;gt;&quot;Are you placing a call&amp;lt;br&amp;gt;to &amp;lt;bobs phone num&amp;gt;&quot;&amp;lt;br&amp;gt;message. And handset&amp;lt;br&amp;gt;silently DENIES she is
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Denied)
    deactivate Alice
    Note over Alliance: Alice&apos;s auto&amp;lt;br&amp;gt;denial received.
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Failure
    deactivate Alliance
    activate Bob
    Note over Bob: Drops the call without&amp;lt;br&amp;gt;notifying Bob and doesnt&amp;lt;br&amp;gt;place it in recent calls
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The sequence diagram for messages would look very similar.&lt;/p&gt;

&lt;h3 id=&quot;smartphone-supporting-rcs-but-not-yet-supporting-dyst-older-ios-or-android-version-perhaps&quot;&gt;Smartphone supporting RCS but not yet supporting DYST (older iOS or Android version perhaps)&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Legit call via RCS (no DYST)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant AliceTeleco as Alice&apos;s&amp;lt;br&amp;gt;Teleco
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Alice-&amp;gt;&amp;gt;AliceTeleco: 1. Alice actually&amp;lt;br&amp;gt;Initiates call
    AliceTeleco-&amp;gt;&amp;gt;Bob: Call Signalling
    activate Bob
    Note over Bob: Receives signalling,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is RCS-enabled but not&amp;lt;br/&amp;gt;DYST-enabled.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge (RCS)
    activate Alice
    Note over Alice: Alice sees interactive&amp;lt;br/&amp;gt;&quot;Are you placing a call&amp;lt;br&amp;gt;to &amp;lt;bobs phone numb&amp;gt;&quot;&amp;lt;br&amp;gt;question. And she&amp;lt;br&amp;gt;manually confirms.
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Confirmed)
    deactivate Alice
    Note over Alliance: Alice&apos;s manual&amp;lt;br&amp;gt;confirmation received.
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Success
    deactivate Alliance
    activate Bob
    Note over Bob: Now rings or displays&amp;lt;br&amp;gt;message. Call/Text&amp;lt;br&amp;gt;connection established.&amp;lt;br/&amp;gt;Phone buzzes or rings
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Legit message via RCS (no DYST)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant AliceTeleco as Alice&apos;s&amp;lt;br&amp;gt;Teleco
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Alice-&amp;gt;&amp;gt;AliceTeleco: 1. Alice actually&amp;lt;br&amp;gt;sends text msg
    AliceTeleco-&amp;gt;&amp;gt;Bob: Message signalling
    activate Bob
    Note over Bob: Receives signalling,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is RCS-enabled but not&amp;lt;br/&amp;gt;DYST-enabled.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge (RCS)
    activate Alice
    Note over Alice: Alice sees interactive&amp;lt;br/&amp;gt;&quot;Did you send this&amp;lt;br&amp;gt;to &amp;lt;bob phone num&amp;gt;&quot;&amp;lt;br&amp;gt;question. And she&amp;lt;br&amp;gt;manually confirms.
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Confirmed)
    deactivate Alice
    Note over Alliance: Alice&apos;s manual&amp;lt;br&amp;gt;confirmation received.
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Success
    deactivate Alliance
    activate Bob
    Note over Bob: Now rings or displays&amp;lt;br&amp;gt;message. Call/Text&amp;lt;br&amp;gt;connection established.&amp;lt;br/&amp;gt;Phone buzzes
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Fake caller ID call via RCS (no DYST)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Eve as Eve&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant EveVOIP as Eve&apos;s VoIP&amp;lt;br&amp;gt;Gateway
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Eve-&amp;gt;&amp;gt;EveVOIP: 1. Eve actually&amp;lt;br&amp;gt;Initiates call&amp;lt;br&amp;gt;caller-id = Alice
    EveVOIP-&amp;gt;&amp;gt;Bob: Call Signalling
    activate Bob
    Note over Bob: Receives signalling,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is RCS-enabled but not&amp;lt;br/&amp;gt;DYST-enabled.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge (RCS)
    activate Alice
    Note over Alice: Alice sees interactive&amp;lt;br/&amp;gt;&quot;Are you placing a call&amp;lt;br&amp;gt;to &amp;lt;bobs phone num&amp;gt;&quot;&amp;lt;br&amp;gt;message. And she&amp;lt;br&amp;gt;manually DENIES.
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Denied)
    deactivate Alice
    Note over Alliance: Alice&apos;s manual&amp;lt;br&amp;gt;denial received.
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Failure
    deactivate Alliance
    activate Bob
    Note over Bob: Drops the call without&amp;lt;br&amp;gt;notifying Bob and doesnt&amp;lt;br&amp;gt;place it in recent calls
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&quot;phone-not-supporting-rcs-nor-dyst-much-older-ios-or-android-version-perhaps&quot;&gt;Phone not supporting RCS nor DYST (much older iOS or Android version perhaps)&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Legit call via SMS (no RCS, no DYST)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant AliceTeleco as Alice&apos;s&amp;lt;br&amp;gt;Teleco
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Alice-&amp;gt;&amp;gt;AliceTeleco: 1. Alice actually&amp;lt;br&amp;gt;Initiates call
    AliceTeleco-&amp;gt;&amp;gt;Bob: Call Signalling
    activate Bob
    Note over Bob: Receives signalling,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is not RCS or DYST enabled.&amp;lt;br&amp;gt;Will use SMS.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge (SMS)
    activate Alice
    Note over Alice: Alice sees SMS asking if&amp;lt;br/&amp;gt;she&apos;s calling&amp;lt;br&amp;gt;&amp;lt;bobs phone num&amp;gt;.&amp;lt;br/&amp;gt;She replies &apos;YES&apos;.
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Confirmed)
    deactivate Alice
    Note over Alliance: Alice&apos;s SMS reply&amp;lt;br&amp;gt;of &apos;YES&apos; received.
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Success
    deactivate Alliance
    activate Bob
    Note over Bob: Now rings or displays&amp;lt;br&amp;gt;message. Call/Text&amp;lt;br&amp;gt;connection established.&amp;lt;br/&amp;gt;Phone buzzes or rings
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Legit message via SMS (no RCS, no DYST)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant AliceTeleco as Alice&apos;s&amp;lt;br&amp;gt;Teleco
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Alice-&amp;gt;&amp;gt;AliceTeleco: 1. Alice actually&amp;lt;br&amp;gt;sends SMS msg
    AliceTeleco-&amp;gt;&amp;gt;Bob: SMS received
    activate Bob
    Note over Bob: Receives SMS,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is not RCS or DYST enabled.&amp;lt;br&amp;gt;Will use SMS.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge (SMS)
    activate Alice
    Note over Alice: Alice sees SMS asking if&amp;lt;br/&amp;gt;she sent message to&amp;lt;br&amp;gt;&amp;lt;bobs phone num&amp;gt;.&amp;lt;br/&amp;gt;She replies &apos;YES&apos;.
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Confirmed)
    deactivate Alice
    Note over Alliance: Alice&apos;s SMS reply&amp;lt;br&amp;gt;of &apos;YES&apos; received.
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Success
    deactivate Alliance
    activate Bob
    Note over Bob: Now displays message.&amp;lt;br/&amp;gt;Phone buzzes
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Fake caller ID call via SMS (no RCS, no DYST)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Eve as Eve&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant EveVOIP as Eve&apos;s VoIP&amp;lt;br&amp;gt;Gateway
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Eve-&amp;gt;&amp;gt;EveVOIP: 1. Eve actually&amp;lt;br&amp;gt;Initiates call&amp;lt;br&amp;gt;caller-id = Alice
    EveVOIP-&amp;gt;&amp;gt;Bob: Call Signalling
    activate Bob
    Note over Bob: Receives signalling,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is not RCS or DYST enabled.&amp;lt;br&amp;gt;Will use SMS.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge (SMS)
    activate Alice
    Note over Alice: Alice sees SMS asking if&amp;lt;br/&amp;gt;she is calling &amp;lt;bobs phone num&amp;gt;.&amp;lt;br/&amp;gt;She ignores it or replies &apos;NO&apos;.
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Denied)
    deactivate Alice
    Note over Alliance: Alice&apos;s SMS reply&amp;lt;br&amp;gt;of &apos;NO&apos; received (or timeout).
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Failure
    deactivate Alliance
    activate Bob
    Note over Bob: Drops the call without&amp;lt;br&amp;gt;notifying Bob and doesnt&amp;lt;br&amp;gt;place it in recent calls
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Of course a text reply of “No” would be inferred for anyting other that ‘Y’, y, YES, 是, 对, हाँ (haan), sí, oui, نعم, হ্যাঁ , da, sim, ہاں, ya, ja, はい, ndiyo, हो , అవును , evet, vâng / dạ&lt;/p&gt;

&lt;p&gt;This system could be enhanced with network intelligence. For IP-based messages,
the receiving service’s backend (e.g., Apple’s servers) could perform a traceroute
back to the sender’s IP. This wouldn’t reveal the user’s precise location but
would identify the network path. A path from a reputable mobile carrier like O2
in the UK would contribute to a “low-probability spammer” score, whereas a path
from a data center known for fraudulent activity would raise suspicion, all
without exposing the sender’s private location.&lt;/p&gt;

&lt;p&gt;A user could configure their device to “block all unverified calls”, “warn”, or
perform no checks. However, limitations of this solution would including the risk of blocking
legitimate calls if the verification service has an outage and the small delay
the check adds to every communication. To counter denial-of-service attacks
where bad actors flood users with fake verification messages to create fatigue
and distrust, the system would rely on server-side intelligence from the central
servers to detect and rate-limit anomalous floods of verification requests.&lt;/p&gt;

&lt;p&gt;A challenge remains communication with “legacy users” on older devices.
While most phones would handle the fallback SMS correctly, older feature phones
(“dumbphones”) in particular might display the verification message as multiple
confusing parts or silently drop it due to full inboxes, undermining the system.
To build trust for this fallback, a logical conclusion would be for participating
companies to form a “Spam Alliance”, complete with a website listing members, and
brand the message with this neutral alliance name.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;2026 footnote:&lt;/strong&gt; The DYST challenge-response pattern — “did you send this?” queried back to the originator’s domain — is the same model underpinning &lt;a href=&quot;https://live-verify.github.io/live-verify/&quot;&gt;Live Verify&lt;/a&gt; (&lt;a href=&quot;https://github.com/live-verify/live-verify&quot;&gt;source&lt;/a&gt;), but for documents instead of calls. A bank statement, police ID card, or sanctions attestation is normalized, SHA-256 hashed, and looked up at the issuer’s domain: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET /v/{hash}&lt;/code&gt; — effectively asking the issuer “did you issue this?” The domain-as-authority principle is identical: the originating server is the source of truth, no central registry needed. Where DYST proposes a Spam Alliance coordinating telcos, Live Verify proposes &lt;a href=&quot;https://live-verify.github.io/live-verify/docs/authority-chain-spec&quot;&gt;authority chains&lt;/a&gt; — issuer endorsed by regulator endorsed by sovereign root — to establish trust without a single coordinating body.&lt;/p&gt;

&lt;p&gt;The telcos’ resistance would likely be because their large customers, such as
call centers using VoIP, would face significant hurdles. Much of their software
may be old and difficult to update to support a new verification protocol.
A call center vendor might even configure their system to automatically answer
“yes” to all challenges, legitimate or not. This would necessitate another layer
of defense, led by the Spam Alliance, to apply reputation scoring to the responders
themselves. If an endpoint blindly says “yes” to everything, its attestations
would eventually be deemed untrustworthy.&lt;/p&gt;

&lt;p&gt;Finally, it is instructive to look at the history of DMARC, SPF, and DKIM for
email. These technologies did not eliminate spam, but they largely solved the
problem of direct domain spoofing. Similarly, a DYST-like system for voice and SMS
would not be a silver bullet for all unwanted communication, but it could effectively
end caller-ID spoofing of legitimate numbers, forcing bad actors onto more easily
traceable and blockable channels.&lt;/p&gt;

&lt;h2 id=&quot;example-dyst-messages&quot;&gt;Example DYST Messages&lt;/h2&gt;

&lt;p&gt;As RCS (Rich Communication Services) supports rich cards and suggested actions,
the user experience for a DYST-style verification could be significantly improved
over a plain SMS fallback.&lt;/p&gt;

&lt;h3 id=&quot;challenge-json-payload-to-claimed-originator-who-has-a-dyst-enabled-smartphone&quot;&gt;Challenge (JSON payload) to claimed originator who has a DYST-enabled Smartphone&lt;/h3&gt;
&lt;p&gt;When the claimed originator’s Smartphone &lt;em&gt;is&lt;/em&gt; DYST-enabled, the verification happens
silently in the background between the originating service’s server and the handset.
This interaction uses a JSON payload, and the handset automatically answers the
challenge without user intervention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recieving handset sends this JSON payload to the DYST-enabled handset of the claimed originator:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;dyst_challenge&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;protocol_version&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;1.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;challenge_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;dyst-challenge-12345&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;timestamp&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2025-11-29T10:30:00Z&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;originator_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;+14155550110&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;recipient_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;+12025550148&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;communication_type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;voice_call&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;communication_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;call-abc-789&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;message_digest&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;sha256:abcdef12345...&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;ttl_seconds&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Claimed originators DYST-enabled handset responds silently:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
  &quot;dyst_response&quot;: {
    &quot;challenge_id&quot;: &quot;dyst-challenge-12345&quot;,
    &quot;status&quot;: &quot;confirmed&quot;,
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Or if they had not placed the voice call (someone else faked caller ID):&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
  &quot;dyst_response&quot;: {
    &quot;challenge_id&quot;: &quot;dyst-challenge-12345&quot;,
    &quot;status&quot;: &quot;denied&quot;,
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;challenge-rcs-message-to-claimed-originator-who-has-a-smartphone-thats-not-dyst-enabled-a-fallback&quot;&gt;Challenge RCS message to claimed originator who has a Smartphone that’s not DYST enabled (a fallback)&lt;/h3&gt;

&lt;p&gt;This is what the person &lt;em&gt;making&lt;/em&gt; the call would receive on their device if the
recipient doesn’t recognize them. It’s designed to be simple and quick.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Spam Alliance Verification ✓
---------------------------------------
🛡️ Did you just try to contact `+1-202-555-0148`?

To connect your call, please confirm it was you.

[ ✅ Yes, that was me ]  [ ❌ No, not me ]
---------------------------------------
&amp;lt;small&amp;gt;Sent by the Spam Alliance. [Learn More]&amp;lt;/small&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;challenge-sms-message-to-claimed-originator-with-a-non-rcs-phone&quot;&gt;Challenge SMS message to claimed originator with a non-RCS phone&lt;/h3&gt;
&lt;p&gt;If the originator’s phone does not support RCS (e.g., a “dumbphone”), the system
must fall back to plain SMS. This experience is the most basic and relies on the
user to manually reply.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Spam Alliance: Did you just try to contact +1-202-555-0148? To connect, reply YES to this message.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;language-selection-for-a-global-dyst-system&quot;&gt;Language Selection for a Global DYST System&lt;/h2&gt;

&lt;p&gt;For a global anti-spam system to be effective, it must communicate with users
in their native language. The DYST system would handle this differently depending
on the originator’s device capabilities.&lt;/p&gt;

&lt;h3 id=&quot;1-first-class-dyst-enabled-handset&quot;&gt;1. First-Class (DYST-enabled Handset)&lt;/h3&gt;
&lt;p&gt;This is the simplest scenario. The silent JSON payload exchanged between the
services is machine-readable and language-agnostic. If the originator’s handset
needs to display any notification related to the verification, it uses its own
local OS language setting to do so. No language information needs to be
transmitted in the challenge itself.&lt;/p&gt;

&lt;h3 id=&quot;2-rcs-fallback-non-dyst-smartphone&quot;&gt;2. RCS Fallback (non-DYST Smartphone)&lt;/h3&gt;
&lt;p&gt;When falling back to a user-facing RCS message, the system must send the
challenge in the correct language. This would be solved by having the originator’s
device report its language preference (e.g., ‘es-MX’ for Mexican Spanish) as part
of its standard RCS capabilities. The Spam Alliance’s service would then deliver
a pre-translated, interactive message in that specific language.&lt;/p&gt;

&lt;h3 id=&quot;3-sms-fallback-non-rcs-phone&quot;&gt;3. SMS Fallback (non-RCS phone)&lt;/h3&gt;
&lt;p&gt;This is the most challenging scenario. Like the RCS fallback, the system would
attempt to determine the device’s language. However, without the rich capabilities
of an RCS client, this may not be possible. As a last resort, the system would
have to make an educated guess based on the phone number’s country code, which is
less reliable but better than defaulting to a single language like English.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Modern CV Technology: JSON Resume embedded in HTML</title>
   <link href="https://paulhammant.com/2025/10/12/modern-cv-tech-json-resume-schema/"/>
   <updated>2025-10-12T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2025/10/12/modern-cv-tech-json-resume-schema</id>
   <content type="html">&lt;p&gt;Problem: uploading your resume/CV to a job portal should yield a perfectly parsed CV but often does not. This is true even if your template .docx is a claimed good starting point for later ingesting into such systems.&lt;/p&gt;

&lt;h1 id=&quot;building-the-future-of-digital-resumes-a-technical-deep-dive&quot;&gt;Building the Future of Digital Resumes: A Technical Deep Dive&lt;/h1&gt;

&lt;p&gt;In an era where Applicant Tracking Systems (ATS) and job portals increasingly dominate the recruitment landscape, the traditional PDF resume is showing its age. What if we could create a resume format that’s simultaneously machine-readable, human-friendly, and completely self-contained? Well, that ended up being a side project - read on.&lt;/p&gt;

&lt;h2 id=&quot;the-problem-with-traditional-resumes&quot;&gt;The Problem with Traditional Resumes&lt;/h2&gt;

&lt;p&gt;Traditional resume formats present a fundamental challenge:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;PDFs&lt;/strong&gt; look great but are difficult for ATS systems to parse accurately, even in the nascent AI era.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Word documents&lt;/strong&gt; are editable but inconsistent across platforms&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Plain text&lt;/strong&gt; is machine-readable but lacks visual appeal&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Linked In&lt;/strong&gt; would like to own this space, but they’re way too much lock in, and spend too much trying to keep your engagement in their pages, vs get you a job.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;the-solution-json-resume-schema--interactive-html&quot;&gt;The Solution: JSON Resume Schema + Interactive HTML&lt;/h2&gt;

&lt;p&gt;This repository showcases a evolutionary approach that combines the best of two words - mnachine parsable and appealing to human eyes.  The raw storage of the CV/resume data:&lt;/p&gt;

&lt;p&gt;Every resume uses the official &lt;a href=&quot;https://jsonresume.org/&quot;&gt;JSON Resume&lt;/a&gt; standard:&lt;/p&gt;
&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;$schema&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://raw.githubusercontent.com/jsonresume/resume-schema/v1.0.0/schema.json&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;basics&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;contact&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;info and summary elemnts&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;work&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;employment&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;history blah blah blah&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;education&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;degrees&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;and certifications&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;skills&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;technical&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;abilities yada yada&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;and&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ten more standardized sections&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each resume/CV is a single HTML file containing:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Embedded JSON data above&lt;/strong&gt; in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag for ATS extraction&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Inlined CSS&lt;/strong&gt; (~1000 lines) for complete visual control&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Inlined JavaScript&lt;/strong&gt; (~1200 lines) for interactive features&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Zero external dependencies&lt;/strong&gt; for viewing (optional CDN for PDF generation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same file serves two masters:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Machines&lt;/strong&gt;: Extract structured JSON data for database import. It could have as easily been XML or YAML, but JSON parsing in web pages is built in.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Humans&lt;/strong&gt;: Styled and responsive HTML with some interactive features&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;some-interactivity-to-control-verbosity&quot;&gt;Some interactivity to control verbosity&lt;/h2&gt;

&lt;p&gt;You can click a pen to go into edit mode. Editing isn’t text, it is contract (-) and expand (+) affordances to 
reduce the verbosity of sections. While I may gush about the origin story of Selenium an in-firm recruiter, then agent who’s taken my CV to them, the downstream interviewers are spectacularly uninterested in that so they’ll hit (-) to collapse that section. This will persist if you go on to print the resume/CV, but not if you close the tab.&lt;/p&gt;

&lt;p&gt;Collapse affordance shown:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/princess_leia1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;That clicked, expand affordance shown:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/princess_leia2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is only useful for someone wanting customize the document for a purpose. For example, and interview stage with the candidate or a discussion with a colleague about take forward in the process or decline.&lt;/p&gt;

&lt;h3 id=&quot;responsive-design-with-print-optimization&quot;&gt;Responsive Design with Print Optimization&lt;/h3&gt;

&lt;p&gt;The CSS includes specialized media queries to ensures URLs are visible in printed versions—crucial for ATS systems and recruiters.  Versus just hyperlinks when that’s in a browser. No big deal perhaps.&lt;/p&gt;

&lt;h3 id=&quot;pdf-generation-pipeline&quot;&gt;PDF Generation Pipeline&lt;/h3&gt;

&lt;p&gt;For enhanced PDF output, the system dynamically loads:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;html2canvas&lt;/strong&gt;: Renders HTML to canvas with pixel-perfect accuracy&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;jsPDF&lt;/strong&gt;: Converts canvas to professional PDF format&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ignoring this lazy load of JavaScript from CDNs, this was otherwise a zero dependency tech.&lt;/p&gt;

&lt;h2 id=&quot;sample-cvs-in-the-repository&quot;&gt;Sample CVs in the repository&lt;/h2&gt;

&lt;p&gt;Current collection includes &lt;strong&gt;14 example resumes&lt;/strong&gt; featuring fictional characters:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Technical roles&lt;/strong&gt;: Tony Stark (Genius/Inventor), Harold Finch (Software Engineer)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Leadership positions&lt;/strong&gt;: T’Challa (Head of State), Princess Leia (Rebel Leader)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Diverse backgrounds&lt;/strong&gt;: Hermione Granger (Academic), Mulan (Military Officer)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Take a look at &lt;strong&gt;Sam “Root” Groves&lt;/strong&gt; from “Person of Interest” TV series; &lt;a href=&quot;https://paul-hammant.github.io/better-cv-tech/Samantha_Groves_Resume.html&quot;&gt;paul-hammant.github.io/better-cv-tech/Samantha_Groves_Resume.html&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;implementation-highlights&quot;&gt;Implementation Highlights&lt;/h2&gt;

&lt;h3 id=&quot;markdown-support-within-json&quot;&gt;Markdown Support Within JSON&lt;/h3&gt;

&lt;p&gt;The system supports basic markdown in key text fields:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;**bold text**&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*italic text*&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[link text](https://example.com)&lt;/code&gt; for clickable links&lt;/li&gt;
  &lt;li&gt;Paragraph breaks with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\n\n&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This enhances human readability while maintaining ATS compatibility. Well, maybe.&lt;/p&gt;

&lt;h2 id=&quot;ats-integration-strategy&quot;&gt;ATS Integration Strategy&lt;/h2&gt;

&lt;p&gt;For ATS systems to adopt this format, they need minimal changes:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Extract resume data from HTML&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;resumeScript&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;cv-data-json&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;resumeData&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;resumeScript&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;textContent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// Now import structured data directly into database&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Likely they’d just be snipping out of the unparsed source file though. That would go into their databases, but also maybe a pipeline that uses a JSON Resume to HTML pipeline. Either way, this is &lt;strong&gt;orders of magnitude&lt;/strong&gt; more reliable than PDF or .docx text extraction or HTML scraping. Even with claimed AI on their side in 2025&lt;/p&gt;

&lt;h2 id=&quot;performance-characteristics&quot;&gt;Performance Characteristics&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;File size&lt;/strong&gt;: ~80KB per resume (including all assets)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Load time&lt;/strong&gt;: Near instant (no external requests for viewing)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Browser support&lt;/strong&gt;: Modern browsers with JavaScript turned on (ES6+ required)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Mobile responsive&lt;/strong&gt;: Breakpoints at 768px and 480px&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;security-considerations&quot;&gt;Security Considerations&lt;/h2&gt;

&lt;p&gt;For recruiters receiving HTML resumes:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;✅ &lt;strong&gt;No file system access&lt;/strong&gt; - Pure DOM manipulation&lt;/li&gt;
  &lt;li&gt;✅ &lt;strong&gt;No external requests&lt;/strong&gt; - Self-contained execution&lt;/li&gt;
  &lt;li&gt;✅ &lt;strong&gt;Standard JavaScript&lt;/strong&gt; - No eval() or dangerous APIs&lt;/li&gt;
  &lt;li&gt;✅ &lt;strong&gt;Data transparency&lt;/strong&gt; - JSON visible in source&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Likely there will be some over-cautiousness. Bigger companies could verify the JavaScript within each uploaded CV/resume if they really wanted to. A whitelist of sorts (extract &amp;gt; lint &amp;gt; pretty-print &amp;gt; SHA256 &amp;gt; check against whitelist). Or just&lt;/p&gt;

&lt;h2 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h2&gt;

&lt;p&gt;To create your own resume:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Copy a template&lt;/strong&gt;: Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Lorem_Ipsum_Resume.html&lt;/code&gt; as your starting point&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Replace JSON data&lt;/strong&gt;: Update the embedded resume data with your information&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Inline css and javascript&lt;/strong&gt;: They are separate in the repo, as there is fourteen or so sample CV/resumes.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Test thoroughly&lt;/strong&gt;: Verify print output and mobile responsiveness&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Name appropriately&lt;/strong&gt;: Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FirstName_LastName_Resume.html&lt;/code&gt; format&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Or get AI to take the constituent pieces and make the page for you. It did so for mine in a 
couple of mins. Prompt is in the repo.&lt;/p&gt;

&lt;h2 id=&quot;links&quot;&gt;Links:&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repository&lt;/strong&gt;: &lt;a href=&quot;https://github.com/paul-hammant/better-cv-tech&quot;&gt;paul-hammant/better-cv-tech&lt;/a&gt;&lt;br /&gt;
&lt;strong&gt;Live Demo&lt;/strong&gt;: &lt;a href=&quot;https://paul-hammant.github.io/better-cv-tech/&quot;&gt;GitHub Pages Gallery - 14 resume/CVs&lt;/a&gt;&lt;br /&gt;
&lt;strong&gt;Schema&lt;/strong&gt;: &lt;a href=&quot;https://jsonresume.org/schema/&quot;&gt;JSON Resume v1.0.0&lt;/a&gt;&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Building a Secure Container Sandbox on ChromeOS for Testing Untrusted Code</title>
   <link href="https://paulhammant.com/2025/09/18/workstation-sandbox-blues/"/>
   <updated>2025-09-18T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2025/09/18/workstation-sandbox-blues</id>
   <content type="html">&lt;h2 id=&quot;the-problem-running-random-github-code-safely&quot;&gt;The Problem: Running Random GitHub Code Safely&lt;/h2&gt;

&lt;p&gt;As developers, we frequently encounter interesting GitHub repositories, development tools, or scripts that we want to test. However, running` untrusted code directly on our development machines poses significant security risks:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Supply chain attacks&lt;/strong&gt;: Malicious code that modifies system binaries or installs backdoors (like the 2025 Chalk npm package compromise that affected millions of downloads)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Data theft&lt;/strong&gt;: Scripts that scan for SSH keys (passphrase protected or not), API tokens, or sensitive files&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;System compromise&lt;/strong&gt;: Privilege escalation attacks that gain persistent access&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Resource abuse&lt;/strong&gt;: Cryptocurrency miners or botnet participation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Traditional solutions like virtual machines are heavyweight and slow to reset. Docker provides isolation but shares the kernel and can be escaped. ChromeOS’s unique architecture offers a compelling alternative through its layered container system.&lt;/p&gt;

&lt;h2 id=&quot;chromeos-container-architecture-defense-in-depth&quot;&gt;ChromeOS Container Architecture: Defense in Depth&lt;/h2&gt;

&lt;p&gt;ChromeOS provides multiple layers of isolation that make it ideal for secure sandboxing:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;┌────────────────────────────────────────-─────┐
│              ChromeOS (Host)                 │
│  ┌─────────────────────────────────────────┐ │
│  │           Termina VM                    │ │
│  │  ┌─────────────┐  ┌─────────────────────┤ │
│  │  │   Penguin   │  │        OSS          │ │
│  │  │  (Trusted)  │  │   (Untrusted)       │ │
│  │  │             │  │                     │ │
│  │  │ - Your work │  │ - Random GitHub     │ │
│  │  │ - SSH keys  │  │   repositories      │ │
│  │  │ - Configs   │  │ - Untested tools    │ │
│  │  │             │  │ - No sensitive      │ │
│  │  │             │  │   data              │ │
│  │  └─────────────┘  └─────────────────────┤ │
│  └─────────────────────────────────────────┘ │
└─────────────────────────────────────────-────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Or a second ascii-art way of outlining the same situation, with more detail ()&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ChromeOS (host)
 └─ crosvm (VM with KVM acceleration if hardware supports it)
     └─ Termina (tiny VM OS, runs LXD daemon and socket)
         │    └- uses LXD API to query kernel level cgroups/namespaces
         │    └- avoids trusting oss&apos;s /bin/ps /bin/find etc
         │         
         ├─ oss       (Debian container, untrusted)
         │    └─ [supply-chain attacker could replace ps/find/ls]
         │
         └─ penguin   (main Debian container, trusted code)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;complete-setup-process&quot;&gt;Complete Setup Process&lt;/h2&gt;

&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;

&lt;p&gt;This setup must be run in ChromeOS’s Termina VM, not inside a container. You can access this by pressing ctrl-alt-t and the resulting terminal should say “Welcome to crosh, the ChromeOS developer shell.” You should see a prompt like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crosh&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;h3 id=&quot;quick-start-script&quot;&gt;Quick Start Script&lt;/h3&gt;

&lt;p&gt;There’s no vi, emacs or nano in chrosh. You’ll prepare scripts elsewhere and email them to yourself. You’ll use ChromeOS’ regular mail app to read those, as you logged in with your google account after all. The ChromeOS text editor is a bit weak for my liking, and I like to keep a copy of things that may get refined and are not canonically in source control.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /tmp/filename_as_instructed.sh &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SETUP_SCRIPT_EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos; | bash
The bash code below 
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SETUP_SCRIPT_EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Save this as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/tmp/setup-secure-containers.sh&lt;/code&gt; and run it with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bash /tmp/setup-secure-containers.sh&lt;/code&gt; as you can’t make it executable:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# ChromeOS Secure Container Setup&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# This creates two containers:&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# - penguin (default/trusted) - your main work environment  &lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# - oss (untrusted) - for running cloned repos and untrusted code&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# IMPORTANT: This script must be run in Termina (the ChromeOS VM)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# because lxc commands don&apos;t work from inside containers&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Colors for output&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;\033[0;31m&apos;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;GREEN&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;\033[0;32m&apos;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;YELLOW&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;\033[1;33m&apos;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;\033[0m&apos;&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# No Color&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;=================================================&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ChromeOS Secure Container Setup&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;=================================================&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;This script will create a two-container security setup:&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;- penguin: Your trusted container (already exists)&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;- oss: Untrusted container for running random code&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Verify we&apos;re in Termina&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;command&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; lxc &amp;amp;&amp;gt; /dev/null&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ERROR: lxc command not found. This script must be run in Termina&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Open the Terminal app in ChromeOS and run this script there.&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
&lt;span class=&quot;k&quot;&gt;fi

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Environment check passed - lxc command found&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Step 1: Show current containers&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 1: Current container status&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;================================&quot;&lt;/span&gt;
lxc list

&lt;span class=&quot;c&quot;&gt;# Step 2: Get available images&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 2: Finding Debian image...&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;===============================&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;IMAGE_FINGERPRINT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc image list &lt;span class=&quot;nt&quot;&gt;--format&lt;/span&gt; csv | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; debian | &lt;span class=&quot;nb&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;,&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f2&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$IMAGE_FINGERPRINT&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;No Debian image found. Available images:&quot;&lt;/span&gt;
    lxc image list
    &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Step 3: Create the oss container&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 3: Creating oss container...&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;==================================&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Check if oss container already exists&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;lxc info oss &amp;amp;&amp;gt;/dev/null&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Container &apos;oss&apos; already exists, skipping...&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;else
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Creating &apos;oss&apos; container...&quot;&lt;/span&gt;
    lxc launch &lt;span class=&quot;nv&quot;&gt;$IMAGE_FINGERPRINT&lt;/span&gt; oss
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Waiting for container to start...&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;sleep &lt;/span&gt;5
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Show updated container list&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
lxc list

&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 4: Setting resource limits for oss container...&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;====================================================&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Set resource limits and security restrictions&lt;/span&gt;
lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss limits.cpu 2
lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss limits.memory 2GB
lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss security.nesting &lt;span class=&quot;nb&quot;&gt;false
&lt;/span&gt;lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss security.privileged &lt;span class=&quot;nb&quot;&gt;false

echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Resource limits and security restrictions applied to oss container&quot;&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 5: Setting up oss container for untrusted code...&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;=====================================================&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Install packages in oss container&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; apt-get update
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; apt-get &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; git build-essential python3 python3-pip curl wget nodejs npm
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /workspace
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; bash &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;echo &apos;Untrusted code workspace - Only run random GitHub repos here!&apos; &amp;gt; /workspace/README.txt&quot;&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;OSS container setup complete&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Step 6: Set up baseline in trusted container (penguin)&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 6: Creating baseline in penguin (trusted container)...&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;==========================================================&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Create baseline hashes for binary integrity monitoring&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;penguin &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; bash &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;find /bin /usr/bin -type f -executable -exec sha256sum {} &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; &amp;gt; /root/baseline_hashes.txt&quot;&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;penguin &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; bash &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;echo &apos;Baseline created at &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&apos; &amp;gt;&amp;gt; /root/baseline_hashes.txt&quot;&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;penguin &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; bash &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;wc -l &amp;lt; /root/baseline_hashes.txt | xargs -I {} echo &apos;Baseline hash count: {}&apos;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Ensure penguin has essential tools&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;penguin &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; apt-get update
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;penguin &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; apt-get &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; vim git

&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Baseline created in penguin container&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Step 7: Create monitoring script in Termina&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 7: Creating Termina-based monitoring script...&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;================================================&quot;&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /tmp/monitor-containers.sh &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MONITOR_EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;
#!/bin/bash
# Container Security Monitor - Runs in Termina
# Monitors the oss (untrusted) and penguin (trusted) containers for supply chain attacks

RED=&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\0&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;33[0;31m&apos;
GREEN=&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\0&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;33[0;32m&apos;
YELLOW=&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\0&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;33[1;33m&apos;
NC=&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\0&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;33[0m&apos;

function setup_baseline() {
    local baseline_file=&quot;/tmp/.container_baseline&quot;
    
    if [ ! -f &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$baseline_file&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; ]; then
        echo &quot;Creating baseline for binary integrity monitoring...&quot;
        echo &quot;# Container Security Baseline - Created &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; &amp;gt; &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$baseline_file&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
        
        # Create baseline for both containers
        for container in oss penguin; do
            if lxc info &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; &amp;amp;&amp;gt;/dev/null; then
                echo &quot;# &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; container baseline&quot; &amp;gt;&amp;gt; &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$baseline_file&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
                for binary in /bin/bash /bin/sh /usr/bin/python3 /usr/bin/node /usr/bin/git; do
                    hash=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sha256sum&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$binary&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{print $1}&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
                    if [ -n &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$hash&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; ]; then
                        echo &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$binary&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$hash&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; &amp;gt;&amp;gt; &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$baseline_file&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
                    fi
                done
            fi
        done
        echo &quot;Baseline created at &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$baseline_file&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    fi
}

function check_binary_integrity() {
    local container=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    local found_issues=0
   
    echo -e &quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;YELLOW&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;Binary Integrity Check - &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    
    # Check key binaries against baseline
    while IFS=: read -r base_container base_binary base_hash; do
        if [[ &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$base_container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; == &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; ]] &amp;amp;&amp;amp; [[ &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$base_binary&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; =~ ^/.*&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;]]; then
            current_hash=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sha256sum&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$base_binary&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{print $1}&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
            if [ -n &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$current_hash&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; ] &amp;amp;&amp;amp; [ &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$current_hash&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; != &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$base_hash&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; ]; then
                echo -e &quot;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;!!!  Modified: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$base_binary&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
                echo &quot;    Expected: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$base_hash&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
                echo &quot;    Current:  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$current_hash&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
                found_issues=1
            fi
        fi
    done &amp;lt; &quot;/tmp/.container_baseline&quot; 2&amp;gt;/dev/null
    
    if [ &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$found_issues&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; -eq 0 ]; then
        echo &quot;  All monitored binaries match baseline&quot;
    fi
    
    return &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$found_issues&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
}

function check_processes() {
    local container=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    echo -e &quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;YELLOW&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;Process Check - &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    echo &quot;  Top processes:&quot;
    
    # Show top CPU-consuming processes
    lxc exec &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; -- ps aux --sort=-%cpu 2&amp;gt;/dev/null | head -6 | while IFS= read -r line; do
        echo &quot;    &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    done
}

function check_network() {
    local container=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    echo -e &quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;YELLOW&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;Network Connections - &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
   
    # Count listening ports and established connections
    listening=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; ss &lt;span class=&quot;nt&quot;&gt;-tlnp&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;LISTEN | &lt;span class=&quot;nb&quot;&gt;wc&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    connections=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; ss &lt;span class=&quot;nt&quot;&gt;-tnp&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;ESTAB | &lt;span class=&quot;nb&quot;&gt;wc&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    
    echo &quot;  Listening ports: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$listening&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    echo &quot;  Active connections: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$connections&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    
    # Alert on any external connections from untrusted container
    if [ &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; = &quot;oss&quot; ] &amp;amp;&amp;amp; [ &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$connections&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; -gt 0 ]; then
        echo -e &quot;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;!!!  External connections detected in untrusted container:&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
        lxc exec &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; -- ss -tnp 2&amp;gt;/dev/null | grep ESTAB | while IFS= read -r line; do
            echo &quot;    &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
        done
    fi
}

function check_recent_files() {
    local container=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    echo -e &quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;YELLOW&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;Recently Modified Files - &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    echo &quot;  Files modified in last 10 minutes:&quot;
   
    # Focus on system directories for supply chain attacks
    lxc exec &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; -- find /bin /usr/bin /lib -xdev -type f -mmin -10 2&amp;gt;/dev/null | while IFS= read -r line; do
        echo &quot;    &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    done
    
    # Also check workspace for oss
    if [ &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; = &quot;oss&quot; ]; then
        echo &quot;  Workspace files:&quot;
        lxc exec &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; -- find /workspace -xdev -type f -mmin -10 2&amp;gt;/dev/null | head -5 | while IFS= read -r line; do
            echo &quot;    &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
        done
    fi
}

function check_suspicious_files() {
    local container=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    echo -e &quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;YELLOW&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;Suspicious Files Check - &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    
    # Look for hidden files in tmp directories
    suspicious_count=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; find /tmp /var/tmp /dev/shm &lt;span class=&quot;nt&quot;&gt;-name&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;.*&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-type&lt;/span&gt; f 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;wc&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    
    if [ &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$suspicious_count&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; -gt 0 ]; then
        echo -e &quot;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;!!!  Found &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$suspicious_count&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; hidden files in temp directories&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    else
        echo &quot;  No suspicious hidden files found&quot;
    fi
}

function check_suid_changes() {
    local container=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    
    # Check for new SUID binaries
    lxc exec &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; -- find / -xdev -perm -4000 -type f 2&amp;gt;/dev/null | while read suid_file; do
        echo &quot;    SUID: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$suid_file&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    done
}

function setup_suid_baseline() {
    for container in oss penguin; do
        if lxc info &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; &amp;amp;&amp;gt;/dev/null; then
            if [ ! -f &quot;/tmp/.known_suid_&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; ]; then
                lxc exec &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; -- find / -xdev -perm -4000 -type f 2&amp;gt;/dev/null &amp;gt; &quot;/tmp/.known_suid_&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
            fi
        fi
    done
}

# Main monitoring loop
echo &quot;=================================================&quot;
echo &quot;Container Security Monitor&quot;
echo &quot;=================================================&quot;
echo &quot;Monitoring for supply chain attacks in:&quot;
echo &quot;  - oss (untrusted) - where you run random code&quot;
echo &quot;  - penguin (trusted) - your main environment&quot;
echo &quot;&quot;

# Setup baselines on first run
setup_baseline
setup_suid_baseline

while true; do
    clear
    echo &quot;Container Security Monitor - &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    echo &quot;=====================================&quot;
    echo &quot;&quot;
   
    # Monitor the untrusted container more closely
    echo -e &quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;=== OSS Container (UNTRUSTED) ===&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    if ! check_binary_integrity &quot;oss&quot;; then
        echo -e &quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;!!!  ALERT: Binary modification detected in OSS container!&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
        echo -e &quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;This could indicate a supply chain attack from recently run code&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    fi
    echo &quot;&quot;
    check_processes &quot;oss&quot;
    echo &quot;&quot;
    check_network &quot;oss&quot;
    echo &quot;&quot;
    check_recent_files &quot;oss&quot;
    echo &quot;&quot;
    check_suspicious_files &quot;oss&quot;
   
    echo &quot;&quot;
    echo -e &quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;GREEN&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;=== Penguin Container (TRUSTED) ===&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    if ! check_binary_integrity &quot;penguin&quot;; then
        echo -e &quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;!!!  CRITICAL: Binary modification in TRUSTED container!&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    fi
    echo &quot;&quot;
    check_network &quot;penguin&quot;
   
    echo &quot;&quot;
    echo &quot;Next scan in 30 seconds... (Press Ctrl+C to exit)&quot;
    sleep 30
done
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MONITOR_EOF

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x /tmp/monitor-containers.sh
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Monitoring script created at /tmp/monitor-containers.sh&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Step 8: Create helper scripts&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 8: Creating helper scripts...&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;==================================&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Create a script to enter each container&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /tmp/enter-oss.sh &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ENTER_OSS_EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;
#!/bin/bash
echo &quot;=========================================&quot;
echo &quot;Entering OSS (UNTRUSTED) Container&quot;
echo &quot;=========================================&quot;
echo &quot;!!!  SECURITY WARNING:&quot;
echo &quot;- Only run untrusted code here&quot;
echo &quot;- No sensitive files or keys&quot;
echo &quot;- Monitor with: bash /tmp/monitor-containers.sh&quot;
echo &quot;=========================================&quot;
lxc exec oss -- bash
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ENTER_OSS_EOF

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x /tmp/enter-oss.sh

&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /tmp/enter-penguin.sh &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ENTER_PENGUIN_EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;
#!/bin/bash
echo &quot;=========================================&quot;
echo &quot;Entering Penguin (TRUSTED) Container&quot;
echo &quot;=========================================&quot;
echo &quot;This is your trusted development environment&quot;
echo &quot;=========================================&quot;
lxc exec penguin -- bash
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ENTER_PENGUIN_EOF

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x /tmp/enter-penguin.sh

&lt;span class=&quot;c&quot;&gt;# Create status script&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /tmp/status.sh &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;STATUS_EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;
#!/bin/bash
echo &quot;Container Security Setup Status&quot;
echo &quot;===============================&quot;
lxc list
echo &quot;&quot;
echo &quot;OSS Container Security Settings:&quot;
echo &quot;  CPU limit: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc config get oss limits.cpu&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
echo &quot;  Memory limit: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc config get oss limits.memory&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
echo &quot;  Privileged: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc config get oss security.privileged&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
echo &quot;  Nesting: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc config get oss security.nesting&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
echo &quot;&quot;
echo &quot;Quick security check:&quot;
for container in oss penguin; do
    echo -n &quot;  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; binary integrity: &quot;
    ps_hash=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sha256sum&lt;/span&gt; /bin/ps 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{print substr($1,1,8)}&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    echo &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ps_hash&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
done
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;STATUS_EOF

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x /tmp/status.sh

&lt;span class=&quot;c&quot;&gt;# Create a reset script for the oss container&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /tmp/reset-oss.sh &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;RESET_EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;
#!/bin/bash
echo &quot;This will destroy and recreate the OSS container&quot;
echo &quot;Any data in the OSS container will be lost!&quot;
read -p &quot;Are you sure? (y/N): &quot; -n 1 -r
echo
if [[ &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$REPLY&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; =~ ^[Yy]&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;]]; then
    echo &quot;Resetting OSS container...&quot;
    lxc stop oss
    lxc delete oss
    
    # Recreate with same settings
    IMAGE_FP=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc image list &lt;span class=&quot;nt&quot;&gt;--format&lt;/span&gt; csv | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; debian | &lt;span class=&quot;nb&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;,&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f2&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    lxc launch &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$IMAGE_FP&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; oss
    
    # Reapply security settings
    lxc config set oss limits.cpu 2
    lxc config set oss limits.memory 2GB
    lxc config set oss security.nesting false
    lxc config set oss security.privileged false
    
    # Reinstall packages
    lxc exec oss -- apt-get update
    lxc exec oss -- apt-get install -y git build-essential python3 python3-pip curl wget nodejs npm
    lxc exec oss -- mkdir -p /workspace
    
    echo &quot;OSS container reset complete&quot;
    
    # Update baseline
    rm -f /tmp/.container_baseline
    echo &quot;Run: bash /tmp/monitor-containers.sh to recreate baseline&quot;
fi
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;RESET_EOF

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x /tmp/reset-oss.sh

&lt;span class=&quot;c&quot;&gt;# Step 9: Optional SSH Setup for OSS Container&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 9: Setting up SSH access to OSS (optional)...&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;==================================================&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Install and configure SSH&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; apt-get &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; openssh-server
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; /etc/ssh/sshd_not_to_be_run
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; ssh-keygen &lt;span class=&quot;nt&quot;&gt;-A&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; systemctl restart ssh
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; systemctl &lt;span class=&quot;nb&quot;&gt;enable &lt;/span&gt;ssh

&lt;span class=&quot;c&quot;&gt;# Create non-root user&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; useradd &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; /bin/bash dev 2&amp;gt;/dev/null &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true
&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; bash &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;echo &apos;dev:changeme&apos; | chpasswd&quot;&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; usermod &lt;span class=&quot;nt&quot;&gt;-aG&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;dev

&lt;span class=&quot;c&quot;&gt;# Get IP address&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;OSS_IP&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc list oss &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; csv &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; 4 | &lt;span class=&quot;nb&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos; &apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;SSH access configured:&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;  ssh dev@&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$OSS_IP&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;  Default password: changeme (change immediately!)&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;!!!  WARNING: Never use SSH agent forwarding (-A) with untrusted containers!&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Final summary&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Setup Complete!&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;===============&quot;&lt;/span&gt;
lxc list
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Available commands:&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;  bash /tmp/monitor-containers.sh - Start security monitoring&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;  bash /tmp/enter-oss.sh         - Enter untrusted container&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;  bash /tmp/enter-penguin.sh     - Enter trusted container&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;  bash /tmp/status.sh            - Check security status&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;  bash /tmp/reset-oss.sh         - Reset OSS container (if compromised)&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Usage pattern:&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;1. Run random code ONLY in oss container (bash /tmp/enter-oss.sh)&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;2. Keep monitoring running in another terminal (bash /tmp/monitor-containers.sh)&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;3. If monitor detects modified binaries, consider using reset script&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;The monitor will detect:&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;- Modified system binaries (supply chain attacks)&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;- Suspicious processes and network connections&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;- Recently modified files in system directories&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;- Hidden files in temporary directories&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;🔒 Your trusted work remains safe in the penguin container!&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;manual-setup-steps&quot;&gt;Manual Setup Steps&lt;/h3&gt;

&lt;p&gt;If you prefer to understand each step, here’s the manual process:&lt;/p&gt;

&lt;h4 id=&quot;1-create-the-untrusted-container&quot;&gt;1. Create the Untrusted Container&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Get available image&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;IMAGE_FP&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc image list &lt;span class=&quot;nt&quot;&gt;--format&lt;/span&gt; csv | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; debian | &lt;span class=&quot;nb&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;,&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f2&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Create OSS container&lt;/span&gt;
lxc launch &lt;span class=&quot;nv&quot;&gt;$IMAGE_FP&lt;/span&gt; oss

&lt;span class=&quot;c&quot;&gt;# Set security restrictions&lt;/span&gt;
lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss limits.cpu 2
lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss limits.memory 2GB
lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss security.nesting &lt;span class=&quot;nb&quot;&gt;false
&lt;/span&gt;lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss security.privileged &lt;span class=&quot;nb&quot;&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;2-install-development-tools&quot;&gt;2. Install Development Tools&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Update and install common development tools&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; apt-get update
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; apt-get &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; git build-essential python3 python3-pip curl wget nodejs npm

&lt;span class=&quot;c&quot;&gt;# Create workspace directory&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /workspace
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;3-create-monitoring-script&quot;&gt;3. Create Monitoring Script&lt;/h4&gt;

&lt;p&gt;The monitoring script runs from Termina and watches both containers for signs of compromise:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Create the monitoring script&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /tmp/monitor-containers.sh &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;
#!/bin/bash
# [Full monitoring script from above]
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOF

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x /tmp/monitor-containers.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;troubleshooting-common-issues&quot;&gt;Troubleshooting Common Issues&lt;/h2&gt;

&lt;h3 id=&quot;termina-filesystem-issues&quot;&gt;Termina Filesystem Issues&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: Scripts fail with “Read-only file system” error. Termina’s home directory (~/) is read-only:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Use /tmp for all scripts which is not read only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: no editors&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt; pipe to file trick as mentioned.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Correct - use /tmp&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /tmp/script.sh &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;
...
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOF
&lt;/span&gt;bash /tmp/script.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;ssh-setup&quot;&gt;SSH Setup&lt;/h3&gt;

&lt;p&gt;SSH service needs manual setup in containers:&lt;/p&gt;

&lt;p&gt;You get into the container using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lxc exec container_name -- bash&lt;/code&gt; from Termina (crosh):&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# In the target container (run via lxc exec)&lt;/span&gt;
apt-get &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; openssh-server
&lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; /etc/ssh/sshd_not_to_be_run  &lt;span class=&quot;c&quot;&gt;# Remove startup blocker&lt;/span&gt;
ssh-keygen &lt;span class=&quot;nt&quot;&gt;-A&lt;/span&gt;                       &lt;span class=&quot;c&quot;&gt;# Generate host keys&lt;/span&gt;
systemctl restart ssh
systemctl &lt;span class=&quot;nb&quot;&gt;enable &lt;/span&gt;ssh
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;root:changeme&apos;&lt;/span&gt; | chpasswd    &lt;span class=&quot;c&quot;&gt;# Set password&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;lxc-command-not-found&quot;&gt;LXC Command Not Found&lt;/h3&gt;

&lt;p&gt;Do NOT run from inside penguin or any other container - you need to be in crosh: ctrl-alt-t&lt;/p&gt;

&lt;h2 id=&quot;secure-git-access-patterns&quot;&gt;Secure Git Access Patterns&lt;/h2&gt;

&lt;p&gt;You don’t want you SSH private key on the container that may be taken over by ‘chalk’ style actions. At least you don’t want it without a lengthy passphrase, but there are traditional solutions:&lt;/p&gt;

&lt;h3 id=&quot;the-ssh-agent-forwarding-security-risk&quot;&gt;The SSH Agent Forwarding Security Risk&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Critical Warning&lt;/strong&gt;: SSH agent forwarding allows untrusted code to use your keys!&lt;/p&gt;

&lt;p&gt;While your SSH session with -A flag is active:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Malicious code can push to ANY repo you have write access to&lt;/li&gt;
  &lt;li&gt;Can clone ANY private repo you have access to&lt;/li&gt;
  &lt;li&gt;Cannot steal your key, but can USE it&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;safer-alternatives-for-git-access&quot;&gt;Safer Alternatives for Git Access&lt;/h3&gt;

&lt;h4 id=&quot;option-1-read-only-deploy-keys-recommended&quot;&gt;Option 1: Read-Only Deploy Keys (RECOMMENDED)&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Create separate key for untrusted work&lt;/span&gt;
ssh-keygen &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; ed25519 &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ~/.ssh/oss_readonly_key &lt;span class=&quot;nt&quot;&gt;-N&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Add to GitHub as deploy key (READ-ONLY) for specific repos&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Copy ONLY this key to oss container&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;penguin &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; /home/USER/.ssh/oss_readonly_key | &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; bash &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;cat &amp;gt; /home/dev/.ssh/id_ed25519 &amp;amp;&amp;amp; chmod 600 /home/dev/.ssh/id_ed25519&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;option-2-fine-grained-personal-access-tokens&quot;&gt;Option 2: Fine-Grained Personal Access Tokens&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Create token with minimal permissions (public_repo only)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Use in oss container:&lt;/span&gt;
git clone https://TOKEN@github.com/user/repo.git
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;option-3-time-limited-agent-forwarding-use-sparingly&quot;&gt;Option 3: Time-Limited Agent Forwarding (USE SPARINGLY)&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Only when absolutely necessary for push operations&lt;/span&gt;
ssh &lt;span class=&quot;nt&quot;&gt;-A&lt;/span&gt; dev@[oss-ip]
&lt;span class=&quot;c&quot;&gt;# Do your git operation&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# EXIT IMMEDIATELY&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;exit&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Or use confirmation-required keys&lt;/span&gt;
ssh-add &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; ~/.ssh/id_ed25519  &lt;span class=&quot;c&quot;&gt;# Requires confirmation for each use&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;security-best-practices&quot;&gt;Security Best Practices&lt;/h2&gt;

&lt;h3 id=&quot;what-to-never-do&quot;&gt;What to NEVER Do&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Don’t store sensitive data in the OSS container&lt;/strong&gt;:
    &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# NEVER DO THIS&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cp&lt;/span&gt; ~/.ssh/id_rsa /path/to/oss/container
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Don’t run trusted code in OSS container&lt;/strong&gt;:
    &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# NEVER DO THIS&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; git clone git@github.com:yourcompany/private-repo.git
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Don’t disable monitoring&lt;/strong&gt;:
    &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# NEVER DO THIS - Always keep monitoring running&lt;/span&gt;
pkill monitor-containers
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;what-to-always-do&quot;&gt;What to ALWAYS Do&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Reset compromised containers&lt;/strong&gt;:
    &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# If monitor detects issues&lt;/span&gt;
bash /tmp/reset-oss.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
    &lt;p&gt;Don’t attempt to repair them. Heck, maybe reset with some regularity anyway.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Regularly update baselines&lt;/strong&gt;:
    &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# After legitimate updates&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; /tmp/.container_baseline
bash /tmp/monitor-containers.sh  &lt;span class=&quot;c&quot;&gt;# Recreates baseline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;container-isolation-rules&quot;&gt;Container Isolation Rules&lt;/h3&gt;

&lt;table class=&quot;table table-striped&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Container&lt;/th&gt;
      &lt;th&gt;Purpose&lt;/th&gt;
      &lt;th&gt;SSH Keys&lt;/th&gt;
      &lt;th&gt;Git Configs&lt;/th&gt;
      &lt;th&gt;Sensitive Data&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;penguin&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Trusted development&lt;/td&gt;
      &lt;td&gt;Safe&lt;/td&gt;
      &lt;td&gt;Safe&lt;/td&gt;
      &lt;td&gt;Safe&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;oss&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Untrusted testing&lt;/td&gt;
      &lt;td&gt;Never&lt;/td&gt;
      &lt;td&gt;Never&lt;/td&gt;
      &lt;td&gt;Never&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;git-security-matrix&quot;&gt;Git Security Matrix&lt;/h3&gt;

&lt;table class=&quot;table table-striped&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Operation&lt;/th&gt;
      &lt;th&gt;Penguin (Trusted)&lt;/th&gt;
      &lt;th&gt;OSS (Untrusted)&lt;/th&gt;
      &lt;th&gt;Method&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Clone public repos&lt;/td&gt;
      &lt;td&gt;y&lt;/td&gt;
      &lt;td&gt;y&lt;/td&gt;
      &lt;td&gt;HTTPS&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Clone private repos&lt;/td&gt;
      &lt;td&gt;y&lt;/td&gt;
      &lt;td&gt;!&lt;/td&gt;
      &lt;td&gt;Deploy keys only&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Push to repos&lt;/td&gt;
      &lt;td&gt;y&lt;/td&gt;
      &lt;td&gt;N&lt;/td&gt;
      &lt;td&gt;Never from OSS&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Store SSH keys&lt;/td&gt;
      &lt;td&gt;y&lt;/td&gt;
      &lt;td&gt;N&lt;/td&gt;
      &lt;td&gt;Never&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Store PATs&lt;/td&gt;
      &lt;td&gt;y&lt;/td&gt;
      &lt;td&gt;!&lt;/td&gt;
      &lt;td&gt;Limited scope only&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Agent forwarding&lt;/td&gt;
      &lt;td&gt;N/A&lt;/td&gt;
      &lt;td&gt;!️&lt;/td&gt;
      &lt;td&gt;Brief sessions only&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;container-access-patterns&quot;&gt;Container Access Patterns&lt;/h3&gt;

&lt;h4 id=&quot;daily-workflow&quot;&gt;Daily workflow&lt;/h4&gt;

&lt;ol&gt;
  &lt;li&gt;Terminal App -&amp;gt; penguin        # Normal development&lt;/li&gt;
  &lt;li&gt;Ctrl+Alt+T -&amp;gt; vsh termina -&amp;gt; bash /tmp/enter-oss.sh  # Testing&lt;/li&gt;
  &lt;li&gt;Or ssh from penguin to oss&lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;security-monitoring&quot;&gt;Security monitoring&lt;/h4&gt;

&lt;p&gt;Ctrl+Alt+T -&amp;gt; vsh termina -&amp;gt; bash /tmp/monitor-containers.sh&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Never create shortcuts that bypass security&lt;/li&gt;
  &lt;li&gt;Don’t alias direct access to OSS in penguin&lt;/li&gt;
  &lt;li&gt;Don’t auto-start monitoring (review alerts manually)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;lessons-from-production-use&quot;&gt;Lessons from Production Use&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Key Insight&lt;/strong&gt;: The separation between Termina (VM host) and containers is crucial. Many security solutions try to work entirely within containers, but the real power comes from leveraging the host-level view.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we learned&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Container isolation is only as good as your monitoring for breaches&lt;/li&gt;
  &lt;li&gt;Host-level monitoring provides better security visibility&lt;/li&gt;
  &lt;li&gt;ChromeOS’s architecture is designed for this type of security model&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;understanding-the-monitoring-system&quot;&gt;Understanding the Monitoring System&lt;/h2&gt;

&lt;p&gt;The monitoring system watches for several types of compromise:&lt;/p&gt;

&lt;h3 id=&quot;1-binary-integrity-monitoring&quot;&gt;1. Binary Integrity Monitoring&lt;/h3&gt;

&lt;p&gt;I grant you this is underdeveloped at the point of this blog entry.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Creates baseline hashes of critical binaries&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;binary &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; /bin/bash /bin/sh /usr/bin/python3 /usr/bin/node /usr/bin/git&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;hash&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sha256sum&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$binary&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{print $1}&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$binary&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$hash&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.container_baseline
&lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What it detects&lt;/strong&gt;: Supply chain attacks that modify system binaries&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-world example&lt;/strong&gt;: The 2024 Chalk npm package compromise replaced legitimate packages with malicious versions that could modify Node.js binaries or install backdoors. Our monitoring would detect such changes immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example alert&lt;/strong&gt;:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;!!!  Modified: /usr/bin/python3
Expected: a1b2c3d4e5f6...
Current:  x9y8z7w6v5u4...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;2-process-monitoring&quot;&gt;2. Process Monitoring&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Shows CPU-intensive processes&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; ps aux &lt;span class=&quot;nt&quot;&gt;--sort&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;-%cpu | &lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What it detects&lt;/strong&gt;: Cryptocurrency miners, botnet activity, unexpected daemons&lt;/p&gt;

&lt;h3 id=&quot;3-network-connection-analysis&quot;&gt;3. Network Connection Analysis&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Counts active connections&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;connections&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; ss &lt;span class=&quot;nt&quot;&gt;-tnp&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;ESTAB | &lt;span class=&quot;nb&quot;&gt;wc&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What it detects&lt;/strong&gt;: Data exfiltration, command &amp;amp; control communication, unexpected servers&lt;/p&gt;

&lt;h3 id=&quot;4-file-system-changes&quot;&gt;4. File System Changes&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Finds recently modified system files&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; find /bin /usr/bin /lib &lt;span class=&quot;nt&quot;&gt;-xdev&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-type&lt;/span&gt; f &lt;span class=&quot;nt&quot;&gt;-mmin&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What it detects&lt;/strong&gt;: System file tampering, backdoor installation&lt;/p&gt;

&lt;h3 id=&quot;5-hidden-file-detection&quot;&gt;5. Hidden File Detection&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Searches for hidden files in temp directories&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; find /tmp /var/tmp /dev/shm &lt;span class=&quot;nt&quot;&gt;-name&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;.*&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-type&lt;/span&gt; f
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What it detects&lt;/strong&gt;: Malware staging areas, credential harvesting tools&lt;/p&gt;

&lt;h2 id=&quot;advanced-usage-patterns&quot;&gt;Advanced Usage Patterns&lt;/h2&gt;

&lt;h3 id=&quot;testing-suspicious-github-repositories&quot;&gt;Testing Suspicious GitHub Repositories&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 1. Start monitoring (separate terminal)&lt;/span&gt;
bash /tmp/monitor-containers.sh

&lt;span class=&quot;c&quot;&gt;# 2. Enter untrusted container&lt;/span&gt;
bash /tmp/enter-oss.sh

&lt;span class=&quot;c&quot;&gt;# 3. In OSS container, test the repo&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; /workspace
git clone https://github.com/suspicious/repo.git
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;repo
./setup.sh  &lt;span class=&quot;c&quot;&gt;# This runs in isolation&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 4. Monitor terminal will alert on any suspicious changes&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 5. If compromised, reset the container&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;exit&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;# Leave OSS container&lt;/span&gt;
bash /tmp/reset-oss.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;development-tool-testing&quot;&gt;Development Tool Testing&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Test a new development tool safely&lt;/span&gt;
bash /tmp/enter-oss.sh

&lt;span class=&quot;c&quot;&gt;# In OSS container&lt;/span&gt;
curl &lt;span class=&quot;nt&quot;&gt;-fsSL&lt;/span&gt; https://some-tool.com/install.sh | bash
some-new-tool &lt;span class=&quot;nt&quot;&gt;--help&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Monitor for:&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# - Modified binaries&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# - Network connections&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# - Hidden files&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# - Unexpected processes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;multi-container-workflow&quot;&gt;Multi-Container Workflow&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Terminal 1: Monitoring&lt;/span&gt;
bash /tmp/monitor-containers.sh

&lt;span class=&quot;c&quot;&gt;# Terminal 2: Trusted work&lt;/span&gt;
bash /tmp/enter-penguin.sh
&lt;span class=&quot;c&quot;&gt;# Do your normal development here&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Terminal 3: Untrusted testing&lt;/span&gt;
bash /tmp/enter-oss.sh
&lt;span class=&quot;c&quot;&gt;# Test random GitHub projects here&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Terminal 4: Status checking&lt;/span&gt;
bash /tmp/status.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;performance-considerations&quot;&gt;Performance Considerations&lt;/h2&gt;

&lt;h3 id=&quot;resource-limits&quot;&gt;Resource Limits&lt;/h3&gt;

&lt;p&gt;The OSS container is intentionally limited to prevent resource abuse:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Current limits (modify as needed)&lt;/span&gt;
lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss limits.cpu 2        &lt;span class=&quot;c&quot;&gt;# 2 CPU cores max&lt;/span&gt;
lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss limits.memory 2GB   &lt;span class=&quot;c&quot;&gt;# 2GB RAM max&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;monitoring-overhead&quot;&gt;Monitoring Overhead&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Monitoring script uses minimal resources&lt;/li&gt;
  &lt;li&gt;Scans every 30 seconds (configurable)&lt;/li&gt;
  &lt;li&gt;Focuses on security-critical changes only&lt;/li&gt;
  &lt;li&gt;Can run continuously without impact&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;container-reset-speed&quot;&gt;Container Reset Speed&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Full OSS container reset takes ~2-3 minutes&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;time &lt;/span&gt;bash /tmp/reset-oss.sh
&lt;span class=&quot;c&quot;&gt;# Includes: stop, delete, recreate, configure, install packages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;integration-with-development-workflow&quot;&gt;Integration with Development Workflow&lt;/h2&gt;

&lt;h3 id=&quot;ide-integration&quot;&gt;IDE Integration&lt;/h3&gt;

&lt;p&gt;You can configure your IDE to work with the container setup:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# VS Code with Remote-Containers&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Point to penguin container for trusted development&lt;/span&gt;
code &lt;span class=&quot;nt&quot;&gt;--folder-uri&lt;/span&gt; vscode-remote://attached-container+penguin/path/to/project

&lt;span class=&quot;c&quot;&gt;# For untrusted code testing, always use terminal access&lt;/span&gt;
bash /tmp/enter-oss.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;git-configuration&quot;&gt;Git Configuration&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# In penguin (trusted) - normal git config&lt;/span&gt;
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.name &lt;span class=&quot;s2&quot;&gt;&quot;Your Name&quot;&lt;/span&gt;
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.email &lt;span class=&quot;s2&quot;&gt;&quot;your.email@domain.com&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# In OSS (untrusted) - minimal or fake config only&lt;/span&gt;
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.name &lt;span class=&quot;s2&quot;&gt;&quot;Test User&quot;&lt;/span&gt;
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.email &lt;span class=&quot;s2&quot;&gt;&quot;test@example.com&quot;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Never configure real credentials&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;file-sharing-between-containers&quot;&gt;File Sharing Between Containers&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Share files from trusted to untrusted (one-way only)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Copy from penguin to OSS for testing&lt;/span&gt;
lxc file push /path/in/penguin/container/file.txt oss/workspace/

&lt;span class=&quot;c&quot;&gt;# NEVER copy from OSS to penguin without verification&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Instead, manually recreate verified files in penguin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;extending-the-security-model&quot;&gt;Extending the Security Model&lt;/h2&gt;

&lt;h3 id=&quot;custom-monitoring-rules&quot;&gt;Custom Monitoring Rules&lt;/h3&gt;

&lt;p&gt;Add your own detection rules to the monitoring script:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Example: Monitor for specific file types&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;function &lt;/span&gt;check_crypto_miners&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;local &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;container&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;miners&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; find /tmp &lt;span class=&quot;nt&quot;&gt;-name&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;*mine*&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-name&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;*crypto*&quot;&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;wc&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$miners&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-gt&lt;/span&gt; 0 &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
        &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;!!!  Potential cryptocurrency miner detected&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;log-integration&quot;&gt;Log Integration&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Enhanced logging in monitoring script&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;LOG_FILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$HOME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/container-security.log&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;function &lt;/span&gt;log_alert&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;+%Y-%m-%d %H:%M:%S&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; ALERT: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;tee&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-a&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$LOG_FILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;ChromeOS’s layered container architecture provides an excellent foundation for safely testing untrusted code. This setup gives you:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;True isolation&lt;/strong&gt;: Each container is properly sandboxed&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Real-time monitoring&lt;/strong&gt;: Immediate detection of compromise attempts&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Easy recovery&lt;/strong&gt;: Quick container reset when needed&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Maintained productivity&lt;/strong&gt;: Your trusted environment stays clean&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key insight is using ChromeOS’s existing security model rather than fighting it. By running the monitoring from Termina and isolating untrusted code in its own container, you get enterprise-grade security with developer-friendly workflows.&lt;/p&gt;

&lt;p&gt;All that said, I’m unlikely to use it without the Terminal integration (see below). I’d like to open Terminal then click &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;oss&lt;/code&gt; a line below &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;penguin&lt;/code&gt;.  Instead, I’m probably use a container in podman inside penguin - that seems hardened as a solution and workable today.&lt;/p&gt;

&lt;h2 id=&quot;the-terminal-app-limitation&quot;&gt;The Terminal App Limitation&lt;/h2&gt;

&lt;p&gt;CromeOS’ Terminal app’s inability to add custom container entries is a significant UX gap. 
The fact that only penguin appears as a clickable row means:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;No visual distinction between trusted/untrusted environments&lt;/li&gt;
  &lt;li&gt;Can’t theme the OSS terminal differently (red background would be perfect!)&lt;/li&gt;
  &lt;li&gt;Extra friction to access the untrusted container&lt;/li&gt;
  &lt;li&gt;No way to know at a glance which container you’re in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SSH workaround (penguin -&amp;gt; ssh dev@oss-ip) adds complexity to what should be a single click.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security Implications&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These UX limitations create real security risks:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Command confusion&lt;/strong&gt;: Without visual distinction, you might accidentally run trusted commands (like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git push&lt;/code&gt; with your real credentials) in the untrusted container&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Increased attack surface&lt;/strong&gt;: The SSH workaround opens network ports and adds authentication complexity that could be exploited&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Remote access temptation&lt;/strong&gt;: Lack of remote access means you might be tempted to run untrusted code directly on your primary machine when traveling, defeating the entire security model&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I would really want to be able to make a second container from within the Terminal app. The UX hints 
that it should be possible but the feature is not there. A huge shame given how incredibly strong the
dev experience on ChromeBooks (that have enough RAM and SSD).&lt;/p&gt;

&lt;h2 id=&quot;the-chrome-remote-desktop-problem&quot;&gt;The Chrome Remote Desktop Problem&lt;/h2&gt;

&lt;p&gt;This is even more frustrating. A ChromeOS Flex machine effectively becomes an island because, only 
Windows and Mac are &lt;strong&gt;first-class&lt;/strong&gt; choices for destination for Chrome Remote Desktop. HOST-OS Linux 
is a &lt;strong&gt;second class&lt;/strong&gt; choice because one seems to need PhD-level understanding of X11/Wayland/DISPLAY. Multi-user 
possibilities might be one of complexities, and systemd could be on the “complicating” mix too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chrome Remote Desktop TO ChromeOS/Linux (the very platform Google controls!) is not supported at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yet, Chrome Remote Desktop FROM ChromeOS to Windows or Mac is supported.&lt;/p&gt;

&lt;p&gt;This means you can’t remotely access your secure container (in ChromeOS Flex on a spare PC) from your Chromebook when traveling, defeating much of the purpose of having a dedicated security testing environment.
Potential Workarounds (all imperfect). Or from your Mac or Windows laptop which support Chrome Remoting as an origin just fine.&lt;/p&gt;

&lt;p&gt;For Terminal access I could create a web-based terminal (ttyd or similar) in 
each container, accessible via different ports, but I’d rather be in my terminal of choice: Terminal&lt;/p&gt;

&lt;p&gt;I have other VNC server in penguin or Guacamole, too, but I wish this were a mainstream feature of ChromeOS once you’ve enabled developer features.&lt;/p&gt;

&lt;p&gt;The irony is that Google has all the pieces (Crostini, Chrome Remote Desktop, Terminal app) but hasn’t connected them properly. A simple “Add Container to Terminal” button and proper Chrome Remote Desktop support would solve everything.
It’s particularly galling because ChromeOS is supposed to be the “simple, secure” option, yet these limitations push us toward complex workarounds that probably decrease security.&lt;/p&gt;

&lt;h2 id=&quot;googlers&quot;&gt;Googlers&lt;/h2&gt;

&lt;p&gt;And if any Googlers have got this far: can you have an explicit “Disable trackpad while typing” setting as macOS Sierra had. It was removed after sierra. Chromebooks have plastic chassis and mild weight adjacent to the trackpad while typing can cause a click at current pointer position.&lt;/p&gt;

&lt;h2 id=&quot;updates&quot;&gt;Updates&lt;/h2&gt;

&lt;p&gt;Jan 2026: The “Multi-container” ChromeOS flag &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#crostini-multi-container&lt;/code&gt; has moved from a hidden experiment to a core feature then deprecated. Now there’s a neg &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;baugutte&lt;/code&gt; 
direction that is containerless - wee VMs instead. Its early days with that, and I’m playing with &lt;a href=&quot;https://github.com/aldur/nixos-crostini/&quot;&gt;nixos-crostini&lt;/a&gt; toward the same goals.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Starting RexxJS</title>
   <link href="https://paulhammant.com/2025/09/15/starting-rexxjs/"/>
   <updated>2025-09-15T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2025/09/15/starting-rexxjs</id>
   <content type="html">&lt;p&gt;Repo: &lt;a href=&quot;https://github.com/RexxJS/RexxJS/&quot;&gt;github.com/RexxJS/RexxJS/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yes, &lt;a href=&quot;https://en.wikipedia.org/wiki/Mike_Cowlishaw&quot;&gt;Mike Cowlishaw’s&lt;/a&gt; interpreted language from 1979 that’s line-centric, starts indexes at 1 not 0, where vars are “weak” &amp;amp; global dominant, which isn’t OO or functional, and has a dangerous “eval” equivalent. Yes, I do have reservations, but I wanted an in-the-DOM interpreted “glue language” that also works on the command line (mac, win and lin). I wanted it because have unreleased agentic-ai applications that will be better for having such a language for a use case, and I’m trying to work out whether this is compatible with model-context-protocols, alien to them, competitive, enabling of, etc. It could all be folly of course.  In short, a familiar to me language that included a “Control Bus” that could work at distance was my goal.&lt;/p&gt;

&lt;h2 id=&quot;rexxjs&quot;&gt;RexxJS&lt;/h2&gt;

&lt;p&gt;RexxJS’s innovation: It combines ARexx’s lightweight string-based messaging with modern browser capabilities (iframes, workers, JSON-RPC - via that postMessage see below), plus adds progress monitoring and fault tolerance through a CHECKPOINT system. That’s one run target (pure DOM; connects iFrames). The other is on the command line via NodeJS. Building this I worked toward the iFrame/postMessage goal (with integration tests, and pure specs) and then back to other language and library and tooling ecosystem. It’s all in JavaScript obviously. Unit tests of Rexx can be via JavaScript instantiating the Rexx interpreter and parser, too. Halfway through I thought it was time for tests in Rexx itself, so there’s a test framework and very experimental expectations capability - those are dogfood tests and there will be more and more of them. At some stage I could be rash and migrate much more of the jest tests to rexx tests, but I’m sceptical about my ability to sort out an invitable change-one-line that breaks 3000 highly derived tests. Anyway, this is alpha quality right now - don’t put apps live using it. Of course, ClaudeCode, JulesAgent, OpenAI (via the excellent AiderChat) and Gemini-cli have helped a lot.&lt;/p&gt;

&lt;h3 id=&quot;agentic-ai-concerns&quot;&gt;Agentic AI concerns&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;Will this have a state synchronization complexity? ARexx worked because AmigaOS applications shared memory space. With distributed nodes, we’ll need robust conflict resolution when scripts modify shared context concurrently. How will this handle split-brain scenarios?&lt;/li&gt;
  &lt;li&gt;Security surface - Self-sending executable code is possible, powerful but risky. That’s more for something with a shell at the other rather than APIs of some sort. MCP typically uses structured message passing rather than arbitrary code execution. For DOM execution at least I already have it working inside iframe sandboxes.&lt;/li&gt;
  &lt;li&gt;Security surface 2 - ARexx ports in 2025 are a hackers dream. Generally available ones for an app would need tokens, cryptographic signature tools, declarative privilege request meta-data.&lt;/li&gt;
  &lt;li&gt;Protocol overhead - ARexx’s beauty was its simplicity. Modern LLM interactions involve complex token management, conversation history, and tool calling. Can RexxJS’s scripting model be expressive enough without becoming any more verbose than it already is?&lt;/li&gt;
  &lt;li&gt;Testing challenges - distributed systems with progresive state changes are notoriously hard to test deterministically. new classes of integration tests may be needed. Possibly also property-based testing for the coordination logic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Strengths, otherwise, include the bidirectional progressive reporting which “could” handle streaming responses naturally, (critical for LLM interactions), running in both DOM and CLI gives this flexibility that most MCP implementations lack BUT the ADDRESS fu for command-line has not been built yet.&lt;/p&gt;

&lt;h2 id=&quot;rexx-history-that-influenced-me&quot;&gt;Rexx history that influenced me&lt;/h2&gt;

&lt;p&gt;The RexxJS “Control Bus” draws inspiration from ARexx’s revolutionary ADDRESS/PORT model on the Amiga home computer by Commodore, which pioneered lightweight inter-application scripting. 1987 ARexx allowed any application to expose a named “port” that could receive text commands from Rexx scripts, enabling system-wide automation through simple string messaging. &lt;a href=&quot;https://en.wikipedia.org/wiki/ARexx&quot;&gt;William Hawes was the author of ARexx&lt;/a&gt; outside Commodore, but it was so good it was subsequently bundled with the OS in 1990/91.&lt;/p&gt;

&lt;h3 id=&quot;before-arexx--prior-art&quot;&gt;Before ARexx / Prior Art&lt;/h3&gt;

&lt;p&gt;Of course just because I thought ARexx’s address system was revolutionary did not mean it was without precedent.&lt;/p&gt;

&lt;h4 id=&quot;ibm-rexx-mainframe--vm-cms-exec&quot;&gt;IBM Rexx (mainframe / VM CMS exec)&lt;/h4&gt;

&lt;p&gt;ADDRESS already existed before ARexx — used to send commands to different environments (ADDRESS TSO, ADDRESS ISPEXEC, etc.). Lots of concepts that did not need peer within modern client/server unix-land software engineering.&lt;/p&gt;

&lt;p&gt;ARexx extended this idea to user applications and arbitrary “ports.” That was without a TCP/IP subsystem for the Amiga in 1987.&lt;/p&gt;

&lt;h4 id=&quot;unix-shell--stdinout-pipes-1970s&quot;&gt;Unix shell + stdin/out pipes (1970s)&lt;/h4&gt;

&lt;p&gt;Not the same, but the philosophy is similar: treat text as the lingua franca, and let a shell script send commands and collect results. What ARexx innovated was the naming of live applications as endpoints rather than just processes and pipes.&lt;/p&gt;

&lt;h4 id=&quot;forth-message-passing-early-80s&quot;&gt;Forth message passing (early 80s)&lt;/h4&gt;

&lt;p&gt;Forth systems often had message/event words for controlling devices. Not system-wide like ARexx ports, but conceptually related.&lt;/p&gt;

&lt;h4 id=&quot;tcls-send-1990s&quot;&gt;Tcl’s send (1990s)&lt;/h4&gt;

&lt;p&gt;Direct analogue allowing Tcl interpreters to send strings to named targets&lt;/p&gt;

&lt;h4 id=&quot;smalltalk-image-messaging&quot;&gt;Smalltalk image messaging&lt;/h4&gt;

&lt;p&gt;Everything is a message, but it’s intra-image, not inter-application. However, some Smalltalks apparently had “workspace to morphic world” messaging similar to ARexx’s openness.&lt;/p&gt;

&lt;h3 id=&quot;modern-analogues&quot;&gt;Modern Analogues&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;AppleScript (1993)&lt;/strong&gt; - Similar concept but heavier (object models + AppleEvents vs simple strings)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;window.postMessage (2008)&lt;/strong&gt; - Browser API that essentially recreates ARexx ports for web contexts (my key target)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Comlink (Google lib, ~2017)&lt;/strong&gt; - a library for this exact things, that one guesses their own web apps use. “Call methods on remote iframes/workers as if local.” it is said. It is strongly typed (Promises), and less string-oriented.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;WebAssembly runtimes with messaging&lt;/strong&gt; - Wasm modules talking to host via postMessage.  Not standardized for cross-iframe yet, but evolving.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Electron IPC&lt;/strong&gt; (2010’s; Node &amp;lt;-&amp;gt; Renderer). Very similar to ARexx ports: each renderer process is a “port” you can send strings to. Structured messages, not always text-based, but philosophically close.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Innovation around IPC keeps happening too. &lt;a href=&quot;https://github.com/eclipse-iceoryx/iceoryx2&quot;&gt;github.com/eclipse-iceoryx/iceoryx2&lt;/a&gt; - “Eclipse iceoryx2 true zero-copy inter-process-communication with a Rust core” - is current, and make me remember those days of the Amiga with an ARexx script controlling multiple full apps.&lt;/p&gt;

&lt;h2 id=&quot;other-languages-that-could-implement-the-same-iframepostmessage-thing&quot;&gt;Other languages that could implement the same iframe/postMessage thing&lt;/h2&gt;

&lt;p&gt;Languages with potential to emulate ARexx ADDRESS/PORT:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Lisp/Scheme: (&lt;a href=&quot;https://github.com/biwascheme/biwascheme&quot;&gt;BiwaScheme&lt;/a&gt; - current, &lt;a href=&quot;https://github.com/santoshrajan/lispyscript&quot;&gt;LispyScript&lt;/a&gt; - paused): Very easy to extend the environment with primitives.  You could define (send “frame1” “(do-something)”). Since Lisp interpreters already treat strings as code, it maps almost 1:1 to ARexx’s “send command string”.&lt;/li&gt;
  &lt;li&gt;Lua: (&lt;a href=&quot;https://github.com/fengari-lua/fengari&quot;&gt;Fengari&lt;/a&gt; - status?): Lua has a built-in concept of coroutines and message loops. Adding postMessage/onmessage as primitives would make iframe scripting natural. You could write something like address(“frame1”, “command”).&lt;/li&gt;
  &lt;li&gt;Forth: (tiny Forths in JS): Forth is literally token streaming. Easy to define PORT words that wrap postMessage. Minimal, but you may lose ARexx-style structured results unless you build a return protocol.&lt;/li&gt;
  &lt;li&gt;Python (well, a subset of): Skulpt and Brython are Python interpreters in JavaScript. &lt;a href=&quot;https://github.com/pyodide&quot;&gt;Pyodide&lt;/a&gt; too, which I am also delegating to in an “extra”.&lt;/li&gt;
  &lt;li&gt;Blasts from the past: Several small BASIC-in-JS interpreters exist (TinyBASIC.js .. current link?). &lt;a href=&quot;https://github.com/tau-prolog/tau-prolog&quot;&gt;Tau Prolog&lt;/a&gt; (paused?) is a Prolog interpreter written in JS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of those are more advanced languages than Rexx itself. I need help with this. It is likely only to be interesting to people with prior Rexx and more recent JavaScript framework/library/tool-building experience.&lt;/p&gt;

&lt;p&gt;Lastly I’m likely to continue to refer to REXX as Rexx.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>SwiftUI Component Testing with Appium & Test Harnesses</title>
   <link href="https://paulhammant.com/2025/06/30/swiftui-component-testing/"/>
   <updated>2025-06-30T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2025/06/30/swiftui-component-testing</id>
   <content type="html">&lt;p&gt;Not sure this the final entry in a series exploring my “UI Component Testing” (started in 2017), but here goes. Over the last couple of weeks, I’ve shown implementations using &lt;a href=&quot;https://paulhammant.com/2025/06/17/ui-component-testing-revisited/&quot;&gt;Playwright&lt;/a&gt;, &lt;a href=&quot;https://paulhammant.com/2025/06/20/cypress-component-testing/&quot;&gt;Cypress&lt;/a&gt;, &lt;a href=&quot;https://paulhammant.com/2025/06/22/selenium-component-testing/&quot;&gt;Selenium&lt;/a&gt;, and &lt;a href=&quot;https://paulhammant.com/2025/06/25/nightwatch-component-testing/&quot;&gt;NightWatch&lt;/a&gt; for a React web application. Now, we leave the web behind and see how the same principles apply to native desktop development with &lt;strong&gt;SwiftUI&lt;/strong&gt;. At true to the oiginal blog entry a credit card component and a couple address-of-credit card component.&lt;/p&gt;

&lt;p&gt;The core idea remains the same: test UI components in the “smallest reasonable rectangle” using dedicated test harnesses, enabling fast, isolated, and reliable tests before integrating them into a full application, but more visibly showing the things that would feed into a component and the outcomes of interactions with it.&lt;/p&gt;

&lt;h1 id=&quot;the-application-a-native-swiftui-payment-form&quot;&gt;The Application: A Native SwiftUI Payment Form&lt;/h1&gt;

&lt;p&gt;Repo: &lt;a href=&quot;https://github.com/paul-hammant/swiftui-component-testing-with-appium&quot;&gt;github.com/paul-hammant/swiftui-component-testing-with-appium&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To get going with this repo, you’ll need Developer tools (the full app) on a recent Mac, as well as Node 22+. and Appium. After &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt;, you’ll need to do &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appium driver install mac2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead of the “Car Doppler” web app, this example uses a simple macOS payment application built entirely in SwiftUI. The application consists of two main components, and an pseudo app:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;CreditCardView&lt;/strong&gt;: A form for entering credit card details.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;BillingAddressView&lt;/strong&gt;: A form for entering a billing address.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;CompositePaymentApp&lt;/strong&gt;: The “real” application that combines both components (pseudo)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The components are combined in a final &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CompositePaymentApp&lt;/code&gt; to illustrate a complete payment screen, is is just a mockup. Just the &lt;em&gt;placement&lt;/em&gt; of the two MVVM components above in an application that you could ship to customers were it finished and useful.&lt;/p&gt;

&lt;p&gt;The composite app, outside of test automation:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/contrived_swift_app.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;test-automation-for-swiftui&quot;&gt;Test Automation for SwiftUI&lt;/h1&gt;

&lt;p&gt;We have two tiers of testing: blazing fast unit tests and UI component testing in the style I have been blogging about.&lt;/p&gt;

&lt;h2 id=&quot;swift-unit--integration-tests-swift-test&quot;&gt;Swift Unit &amp;amp; Integration Tests (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;swift test&lt;/code&gt;)&lt;/h2&gt;

&lt;p&gt;These tests operate directly on the data models in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UIComponentTestingLib&lt;/code&gt; without launching any UI. They are incredibly fast. We could run hundreds of tests a second, but we don’t have that many in this repo. Being integrated into the Swift environment, these will compile prod and test code if needed before running.&lt;/p&gt;

&lt;h2 id=&quot;component-test-via-appium-npm-test&quot;&gt;Component Test via Appium (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm test&lt;/code&gt;)&lt;/h2&gt;

&lt;p&gt;To automate our SwiftUI test harnesses, we use &lt;strong&gt;Appium&lt;/strong&gt; with its Mac2 driver. This gives us a powerful, Selenium/Webdriver-like ability to drive our native macOS application from an external script—in this case, JavaScript with WebdriverIO. WebDriver is familiar to me already, of course.&lt;/p&gt;

&lt;p&gt;The key to achieving our component testing goal is to not totally rely on brittle UI interactions like typing. Here we use a combination of accessibility identifiers and fast data injection, to do some heavy lifting. To aid injection of data, we have a&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TextEditor&lt;/code&gt; view for JSON test data and a “Load Test Data” button to take that and push it into the model using regular functions of the production UI. Via Appium, this is quite smooth,&lt;/p&gt;

&lt;p&gt;These tests launch the actual test compoenent harness app(s) and interact with them through the UI layer. They are slower but provide higher confidence that the components are visually correct and interactive. They are more representative of testing the full, compiled application in a way a user would interact with it (albeit with our data injection shortcut). And you have to remember to compile the swift pieces as NPM/Node and JS is a different world toward the scripted testing of this substantially MacOS app.&lt;/p&gt;

&lt;p&gt;Anyway, here’s the video of UI component testing in the test harness:&lt;/p&gt;

&lt;iframe width=&quot;640&quot; height=&quot;480&quot; src=&quot;https://www.youtube.com/embed/3MmhtzhDce4?si=FDlTrc1TEes5iCzL&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;

&lt;p&gt;Appium wants the whole screen. It places a shade layer over everything with an “Automation Running” message, to give you the distinct impression that you should take your hands off the mouse/kbd for the duration.&lt;/p&gt;

&lt;h1 id=&quot;macos-apps-and-appium&quot;&gt;MacOS apps and Appium&lt;/h1&gt;

&lt;p&gt;It has been difficult ironing the kinks out of this demo. There are a few processes that need to be orchestrated which feels harder than treating everything as a lib for the language ecosystem in question. And also some permissions in MacOS settings (Settings -&amp;gt; Security &amp;amp; Privacy -&amp;gt; Accessibility) that I must say I still have not resolved the all the permutations of. I run regular Terminal on the Mac, as well as VsCode and JetBrains’ “Fleet”. The runner of the Appium script is NodeJs (node the executable). Node is off in a Homebrew manage folde for me. One which could easily change with a &lt;strong&gt;brew update/upgrade&lt;/strong&gt; some time later. The registration is via fully qualified path like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/opt/homebrew/Cellar/node/&amp;lt;version&amp;gt;/bin/node&lt;/code&gt;. A path that is not easy to enter into the same Mac settings UI that manages what has elevated privs and what does not. Then, after you’ve entered it, the fully qualified path disappears and you’re going to see it as just “node” in the list. This is inauthentic in my opinion and a hole in the Mac’s claimed impregnable security armour, but that is an aside. Running the suite, Appium could fail to find the test app and that could be because of this missing privilege. Or it could be something else. It may have worked for you last week, but because of one brew-upgrade it may not today. I am not sure that Appium can be changed to mind-read what the root cause problem could be. Even without version upgrades it is unclear whether how many runners I have to register in there - VsCode and Node and Fleet? Seems to broad.&lt;/p&gt;

&lt;p&gt;My beefy ChromeBook plus has great separations between the OS and the VM I’m developing in. Easy to redo things too if I mess them up. Importantly, it’s hard on the Chromebook for a rogue developer-installed thing to take over the whole OS. I can’t have a Mac VM within the Chromebook though (not even the Docker-OSX thing or related), but I feel VMs are the way forward. Ideally, I’d have a MacOS VM within MacOS just for the programmatic testing of developed apps under Appium-like control. This would (should IMO) take only megabytes of disk, not gigabytes and be lean enough to run on 8GB RAM machines. Inside that Mac VM, I would want all permissions without fiddling with settings post install. I’d want a script to run on boot from an overlay that Appium would setup. Could be that we have that in Tahoe that we know ships with better containers, but I’ve not upgraded yet as it is in beta. It is also for Linux containers only, but a subsequent beta release will likely be for lightweight Mac VMs too.&lt;/p&gt;

&lt;p&gt;It could be I already have lean Firecracker-style VMs capability for Mac Sequoia (v15) with “Lume” - see &lt;a href=&quot;https://news.ycombinator.com/item?id=42908061&quot;&gt;Show HN: Lume – OS lightweight CLI for MacOS and Linux VMs on Apple Silicon&lt;/a&gt;. It is Apple silicon only and I have the last of the Intel Macs &lt;strong&gt;for now&lt;/strong&gt;. It is also for pre-built images (they have some Packer tech too). I’d be really happy with “same as host” retrictions on the VM in order to get to the megabyte place. The BSDs do this well I think with “jails”. I’ve talked of the &lt;a href=&quot;https://paulhammant.com/2016/12/14/principles-of-containment/&quot;&gt;principles of containment before&lt;/a&gt; (2016) and this all feels in that direction. To carve a larger screen into VMs seems doable: when the mouse is in that rectangle, it gets mouse/keyboard/camera/mic and can use speakers and video (as far as the scaled rectangle allows). When the mouse is outside that rectangle, some of those are lost, but not in a “USB unplugged” way. And I’d also want real touchpad as virtual touchpad, not re-presented as a mouse. Sandboxes are quite well understood by now, but the containing system should be able to easily grant more than defaults.&lt;/p&gt;

&lt;p&gt;Anyway, enjoy this component tests with test harness for a (fat) MacOs app … that is closer to the 2017 blog entry punditing around this testing stuff.&lt;/p&gt;

&lt;h1 id=&quot;ps-pseudo-declarative-uis&quot;&gt;PS: Pseudo-declarative UIs&lt;/h1&gt;

&lt;p&gt;It is also worth mentioning that I love pseudo-declarative markup languages like SwiftUI and have a 30 blog entries on the concept. Gazing at the main.swift sources in &lt;a href=&quot;https://github.com/paul-hammant/swiftui-component-testing-with-appium/blob/main/Sources/&quot;&gt;github.com/paul-hammant/swiftui-component-testing-with-appium/blob/main/Sources/&lt;/a&gt; is where you’d see this markup style.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>NightWatch Component Testing and visual documentation</title>
   <link href="https://paulhammant.com/2025/06/25/nightwatch-component-testing/"/>
   <updated>2025-06-25T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2025/06/25/nightwatch-component-testing</id>
   <content type="html">&lt;p&gt;This blog entry follows my recent exploration of &lt;a href=&quot;https://paulhammant.com/2025/06/17/ui-component-testing-revisited/&quot;&gt;Playwright&lt;/a&gt;, &lt;a href=&quot;https://paulhammant.com/2025/06/20/cypress-component-testing/&quot;&gt;Cypress&lt;/a&gt;, and &lt;a href=&quot;https://paulhammant.com/2025/06/22/selenium-component-testing/&quot;&gt;Selenium-WebDriver&lt;/a&gt; for component testing, I’ve now completed a migration to NightWatch.js. This post documents the complete transition from Selenium WebDriver to NightWatch for a particular test-harness pattern for component testing.&lt;/p&gt;

&lt;h1 id=&quot;nightwatch-component-testing-migration&quot;&gt;NightWatch Component Testing Migration&lt;/h1&gt;

&lt;p&gt;Branch: &lt;a href=&quot;https://github.com/paul-hammant/car-doppler/tree/nightwatch_instead_of_selenium&quot;&gt;nightwatch_instead_of_selenium&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note: I didn’t start with the Playwright branch for this one - I started with the canonical selenium-webdriver one, because NightWatch is closer to that ecosystem than it is to anything else.&lt;/p&gt;

&lt;h2 id=&quot;the-migration-challenge&quot;&gt;The Migration Challenge&lt;/h2&gt;

&lt;p&gt;Starting with a fully functional Selenium WebDriver test suite covering both component tests and e2e tests, the goal was to migrate everything to NightWatch.js while preserving:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;All test functionality and coverage&lt;/li&gt;
  &lt;li&gt;Screenshot capabilities for visual documentation&lt;/li&gt;
  &lt;li&gt;The same test harness pattern for component testing&lt;/li&gt;
  &lt;li&gt;Performance optimizations from the Selenium implementation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;why-nightwatchjs&quot;&gt;Why NightWatch.js?&lt;/h2&gt;

&lt;p&gt;NightWatch.js offers several advantages over raw Selenium WebDriver:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Cleaner more modern syntax&lt;/strong&gt;: More readable test code with built-in assertions&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Better error reporting&lt;/strong&gt;: Detailed failure messages with stack traces&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Integrated screenshots&lt;/strong&gt;: Built-in screenshot capabilities with failure capture&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Configuration simplicity&lt;/strong&gt;: Single configuration file vs multiple setup files&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Browser management&lt;/strong&gt;: Automatic WebDriver lifecycle management (yes, it uses selenium-webDriver under the hood)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Parallel execution&lt;/strong&gt;: Built-in support for parallel test execution (though we’re not using that here)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;nightwatch-migration-work&quot;&gt;NightWatch Migration work&lt;/h2&gt;

&lt;h3 id=&quot;component-tests-hundreds-of-assertions&quot;&gt;Component Tests: hundreds of assertions&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Controls Component&lt;/strong&gt;: 91 assertions across 5 scenarios&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;DebugConsole Component&lt;/strong&gt;: 125 assertions across 6 scenarios&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;UnitsConversion Component&lt;/strong&gt;: 57 assertions across 3 scenarios&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: Same visual test harness pattern with optimized navigation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;e2e-tests-127-assertions-less-important-for-this-blog-entry&quot;&gt;E2E Tests: 127 assertions (less important for this blog entry)&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Doppler App Tests&lt;/strong&gt;: 96 assertions covering main app functionality&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Audio Processing Tests&lt;/strong&gt;: 31 assertions covering file upload and audio features&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Responsive Testing&lt;/strong&gt;: Mobile viewport and cross-browser compatibility&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;visual-implementation-identical-test-documentation&quot;&gt;Visual Implementation: Identical Test Documentation&lt;/h2&gt;

&lt;p&gt;The NightWatch implementation preserves the same visual-first approach, generating detailed screenshots for each test interaction. Here’s the Test Harness Component Testing pattern now powered by NightWatch. Those are gated on an env-var so could be turned off.&lt;/p&gt;

&lt;h3 id=&quot;example-component-state-testing&quot;&gt;Example: Component State Testing&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Initial State: Component Ready&lt;/strong&gt;
&lt;img src=&quot;https://paulhammant.com/images/nightwatch/Controls-initial-state.png&quot; alt=&quot;NightWatch test harness showing component under test in blue border, harness state in green border, and event log in yellow border&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recording Toggle Interaction&lt;/strong&gt;
&lt;img src=&quot;https://paulhammant.com/images/nightwatch/Controls-recording-toggle-started.png&quot; alt=&quot;Shows the component state change from &amp;quot;Start Listening&amp;quot; to &amp;quot;Stop Listening&amp;quot; with corresponding harness state updates and event logging&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The NightWatch implementation maintains the same three-section visual pattern:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Component Under Test&lt;/strong&gt; (blue border) - The actual React component being tested&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Test Harness State&lt;/strong&gt; (green border) - Shows parent component state reflecting real app conditions&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Event Log&lt;/strong&gt; (yellow border) - Complete interaction history for debugging and verification&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;component-and-e2e-tests-via-nightwatch&quot;&gt;Component and E2e tests via NightWatch&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Component Test Utils&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nightwatch-utils.js&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Test harness navigation and interaction&lt;/li&gt;
  &lt;li&gt;Component-specific assertions&lt;/li&gt;
  &lt;li&gt;Screenshot management for test documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;E2E Test Utils&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nightwatch-e2e-utils.js&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Full application navigation&lt;/li&gt;
  &lt;li&gt;Cross-component integration testing&lt;/li&gt;
  &lt;li&gt;Mobile responsive testing utilities&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;performance-optimizations-preserved&quot;&gt;Performance Optimizations Preserved&lt;/h3&gt;

&lt;p&gt;The NightWatch migration maintained all performance optimizations from the Selenium implementation:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Shared browser instances&lt;/strong&gt;: Single Firefox instance per test suite. I am not sure if I am doing this in an idiomatically correct way for a forced serial use of NightWatchJs.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Fast page updates&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;window.location.replace()&lt;/code&gt; instead of full navigation&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Optimized waits&lt;/strong&gt;: Implicit timeouts of 1-2 seconds vs default 10+ seconds&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Strategic screenshots&lt;/strong&gt;: Only when not in CI or when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SKIP_SCREENSHOTS&lt;/code&gt; is false&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;custom-dependency-nightwatchreact-fork&quot;&gt;Custom Dependency: @nightwatch/react Fork&lt;/h3&gt;

&lt;p&gt;This project uses a custom fork &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;@nightwatch/react&quot;: &quot;github:paul-hammant/nightwatch-plugin-react#main&quot;&lt;/code&gt; to update transitive dependencies that were several major versions behind, resolving React 18+ compatibility issues and security vulnerabilities while maintaining full API compatibility. Fingers crossed the Nightwatch team will process the pull request, and I get to delete the section.&lt;/p&gt;

&lt;h2 id=&quot;running-the-component-tests&quot;&gt;Running the component tests&lt;/h2&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
&amp;gt; react-app@2.1.2 test:ct
&amp;gt; npm run build:server --silent &amp;amp;&amp;amp; nightwatch --config nightwatch.conf.js src/components/__tests__/**/*.ct.nightwatch.test.js

CSS imports will be handled by the server
Setting up NightWatch test environment...


[Controls Ct Nightwatch Test] Test Suite
───────────────────────────────────────────────────────────────────────────────
- Starting GeckoDriver on port 4444...

ℹ Connected to GeckoDriver on port 4444 (1542ms).
  Using: firefox (140.0) on LINUX.

- Loading url: http://localhost:3001/render-component/ControlsTestHarness?testName=Initial

  ℹ Loaded url http://localhost:3001/render-component/ControlsTestHarness?testName=Initial in 128ms
  ✔ Element &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; was present after 29 milliseconds.

  Running renders in test harness with initial state visible:
───────────────────────────────────────────────────────────────────────────────────────────────────
  ✔ Element &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; was present after 24 milliseconds.
  ✔ Element &amp;lt;[data-testid=&quot;record-button&quot;]&amp;gt; was present after 11 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;record-button&quot;]&amp;gt; inner text equals &apos;Start
Listening&apos; (11ms)
  ✔ Element &amp;lt;[data-testid=&quot;unit-toggle-button&quot;]&amp;gt; was present after 5 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;unit-toggle-button&quot;]&amp;gt; inner text equals &apos;Switch to
mph&apos; (11ms)
  ✔ Element &amp;lt;[data-testid=&quot;harness-recording-state&quot;]&amp;gt; was present after 4 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;harness-recording-state&quot;]&amp;gt; inner text equals &apos;Recording: OFF&apos; (9ms)
  ✔ Element &amp;lt;[data-testid=&quot;harness-units-state&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;harness-units-state&quot;]&amp;gt; inner text equals &apos;Units: METRIC (km/h)&apos; (8ms)
  ✔ Element &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; inner text equals &apos;Test: Initial State Visibility&apos; (9ms)

  ✨ PASSED. 11 assertions. (220ms)

  Running demonstrates event coupling - recording toggle:
───────────────────────────────────────────────────────────────────────────────────────────────────
  ✔ Element &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; was present after 21 milliseconds.
  ✔ Element &amp;lt;[data-testid=&quot;record-button&quot;]&amp;gt; was present after 4 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;record-button&quot;]&amp;gt; inner text equals &apos;Start
Listening&apos; (17ms)
  ✔ Element &amp;lt;[data-testid=&quot;harness-recording-state&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;harness-recording-state&quot;]&amp;gt; inner text equals &apos;Recording: OFF&apos; (9ms)
  ✔ Element &amp;lt;[data-testid=&quot;event-log&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;event-log&quot;]&amp;gt; inner text equals &apos;No events yet...&apos; (11ms)
  ✔ Element &amp;lt;[data-testid=&quot;record-button&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Element &amp;lt;[data-testid=&quot;record-button&quot;]&amp;gt; was visible after 12 milliseconds.

  PASSED: 9 passed (481ms)


[Debug Console Ct Nightwatch Test] Test Suite
───────────────────────────────────────────────────────────────────────────────
- Starting GeckoDriver on port 4444...

ℹ Connected to GeckoDriver on port 4444 (1580ms).
  Using: firefox (140.0) on LINUX.

- Loading url: http://localhost:3001/render-component/DebugConsoleTestHarness?testName=Initial

  ℹ Loaded url http://localhost:3001/render-component/DebugConsoleTestHarness?testName=Initial in 113ms
  ✔ Element &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; was present after 19 milliseconds.

  Running loadDebugTestHarness:
───────────────────────────────────────────────────────────────────────────────────────────────────
  ✔ Element &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; was present after 24 milliseconds.

  ✨ PASSED. 1 assertions. (39ms)

  Running comprehensive debug console functionality and states:
───────────────────────────────────────────────────────────────────────────────────────────────────
  ✔ Element &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; was present after 19 milliseconds.
  ✔ Element &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; was present after 8 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; inner text equals &apos;Show Debug Console&apos; (15ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-console-container&quot;]&amp;gt; to not be present - element was not found (1011ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; to have attribute &quot;aria-label&quot; which equals: &quot;Show Debug Console&quot; (12ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; to have attribute &quot;class&quot; which contains: &quot;debug-toggle-button&quot; (8ms)
  ✔ Element &amp;lt;[data-testid=&quot;harness-log-count&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;harness-log-count&quot;]&amp;gt; inner text equals &apos;Log Count: 4&apos; (10ms)
  ✔ Element &amp;lt;[data-testid=&quot;harness-intercept-state&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;harness-intercept-state&quot;]&amp;gt; inner text equals &apos;Intercept Console: NO&apos; (8ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;event-log&quot;]&amp;gt; to be visible (8ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; to be present (3ms)

  ✨ PASSED. 12 assertions. (1.222s)

  Running handles empty logs state:
───────────────────────────────────────────────────────────────────────────────────────────────────
  ✔ Element &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; was present after 19 milliseconds.
  ✔ Element &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; inner text equals &apos;Show Debug Console&apos; (11ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-console-container&quot;]&amp;gt; to not be present - element was not found (1004ms)
  ✔ Element &amp;lt;[data-testid=&quot;harness-log-count&quot;]&amp;gt; was present after 6 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;harness-log-count&quot;]&amp;gt; inner text equals &apos;Log Count: 0&apos; (13ms)

  ✨ PASSED. 6 assertions. (1.098s)

  Running handles large number of log entries:
───────────────────────────────────────────────────────────────────────────────────────────────────
  ✔ Element &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; was present after 27 milliseconds.
  ✔ Element &amp;lt;[data-testid=&quot;harness-log-count&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;harness-log-count&quot;]&amp;gt; inner text equals &apos;Log Count: 50&apos; (8ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; inner text equals &apos;Show Debug Console&apos; (8ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-console-container&quot;]&amp;gt; to not be present - element was not found (1011ms)

  ✨ PASSED. 6 assertions. (1.144s)

  Running debug console with production-like log scenarios:
───────────────────────────────────────────────────────────────────────────────────────────────────
  ✔ Element &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; was present after 25 milliseconds.
  ✔ Element &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; was present after 5 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; inner text equals &apos;Show Debug Console&apos; (12ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-console-container&quot;]&amp;gt; to not be present - element was not found (1006ms)
  ✔ Element &amp;lt;[data-testid=&quot;harness-log-count&quot;]&amp;gt; was present after 5 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;harness-log-count&quot;]&amp;gt; inner text equals &apos;Log Count: 10&apos; (10ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;event-log&quot;]&amp;gt; to be present (3ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; to be present (3ms)
  ✔ Element &amp;lt;[data-testid=&quot;harness-log-count&quot;]&amp;gt; was present after 5 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;harness-log-count&quot;]&amp;gt; inner text equals &apos;Log Count: 10&apos; (10ms)

  ✨ PASSED. 10 assertions. (1.182s)

  Running expanded debug console with production-like content:
───────────────────────────────────────────────────────────────────────────────────────────────────
  ✔ Element &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; was present after 21 milliseconds.
  ✔ Element &amp;lt;[data-testid=&quot;harness-log-count&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;harness-log-count&quot;]&amp;gt; inner text equals &apos;Log Count: 10&apos; (10ms)
  ✔ Element &amp;lt;[data-testid=&quot;harness-expanded-state&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;harness-expanded-state&quot;]&amp;gt; inner text equals &apos;Debug Console State: EXPANDED (for testing)&apos; (9ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-console-container&quot;]&amp;gt; to be visible (8ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; inner text equals &apos;Hide Debug Console&apos; (7ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-log-entry-0&quot;]&amp;gt; to be visible (8ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-log-entry-4&quot;]&amp;gt; to be visible (10ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-log-entry-9&quot;]&amp;gt; to be visible (8ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-0&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-0&quot;]&amp;gt; contains text &apos;Application startup complete&apos; (8ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-4&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-4&quot;]&amp;gt; contains text &apos;FFT processing timeout&apos; (8ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-6&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-6&quot;]&amp;gt; contains text &apos;Audio processing restored&apos; (9ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-8&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-8&quot;]&amp;gt; contains text &apos;Speed calculation: 25.3 mph&apos; (8ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-9&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-9&quot;]&amp;gt; contains text &apos;Doppler shift detected: +127 Hz&apos; (9ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-4&quot;]&amp;gt; was present after 1 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-4&quot;]&amp;gt; contains text &apos;ERROR&apos; (8ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-4&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-4&quot;]&amp;gt; contains text &apos;FFT processing timeout&apos; (10ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-7&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-7&quot;]&amp;gt; contains text &apos;WARN&apos; (17ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-7&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-7&quot;]&amp;gt; contains text &apos;High CPU usage detected&apos; (10ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-6&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-6&quot;]&amp;gt; contains text &apos;SUCCESS&apos; (11ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-6&quot;]&amp;gt; was present after 4 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-6&quot;]&amp;gt; contains text &apos;Audio processing restored&apos; (11ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-log-container&quot;]&amp;gt; to be visible (8ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-fft-status&quot;]&amp;gt; to be visible (10ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-clear-button&quot;]&amp;gt; to be visible (13ms)

  ✨ PASSED. 36 assertions. (441ms)

  Running debug console supports dynamic log updates after initial load:
───────────────────────────────────────────────────────────────────────────────────────────────────
  ✔ Element &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; was present after 17 milliseconds.
  ✔ Element &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; inner text equals &apos;Hide Debug Console&apos; (11ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-console-container&quot;]&amp;gt; to be visible (11ms)
  ✔ Element &amp;lt;[data-testid=&quot;harness-log-count&quot;]&amp;gt; was present after 5 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;harness-log-count&quot;]&amp;gt; inner text equals &apos;Log Count: 2&apos; (10ms)
  ✔ Element &amp;lt;[data-testid=&quot;harness-expanded-state&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;harness-expanded-state&quot;]&amp;gt; inner text equals &apos;Debug Console State: EXPANDED (for testing)&apos; (9ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-0&quot;]&amp;gt; was present after 7 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-0&quot;]&amp;gt; contains text &apos;ADDED AFTER 1&apos; (9ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-0&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-0&quot;]&amp;gt; contains text &apos;INFO&apos; (8ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-0&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-0&quot;]&amp;gt; contains text &apos;10:30:00&apos; (9ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-1&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-1&quot;]&amp;gt; contains text &apos;ADDED AFTER 2&apos; (8ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-1&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-1&quot;]&amp;gt; contains text &apos;INFO&apos; (8ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-1&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-1&quot;]&amp;gt; contains text &apos;10:30:05&apos; (9ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; to be present (2ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;event-log&quot;]&amp;gt; to be present (3ms)
  ✔ Element &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; was present after 22 milliseconds.
  ✔ Element &amp;lt;[data-testid=&quot;harness-log-count&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;harness-log-count&quot;]&amp;gt; inner text equals &apos;Log Count: 5&apos; (10ms)
  ✔ Element &amp;lt;[data-testid=&quot;harness-expanded-state&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;harness-expanded-state&quot;]&amp;gt; inner text equals &apos;Debug Console State: EXPANDED (for testing)&apos; (9ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-0&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-0&quot;]&amp;gt; contains text &apos;ADDED AFTER 1&apos; (10ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-0&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-0&quot;]&amp;gt; contains text &apos;INFO&apos; (9ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-0&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-0&quot;]&amp;gt; contains text &apos;10:30:00&apos; (8ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-1&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-1&quot;]&amp;gt; contains text &apos;ADDED AFTER 2&apos; (7ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-1&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-1&quot;]&amp;gt; contains text &apos;INFO&apos; (9ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-1&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-1&quot;]&amp;gt; contains text &apos;10:30:05&apos; (8ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-2&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-2&quot;]&amp;gt; contains text &apos;Collaborator: High memory usage detected&apos; (9ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-2&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-2&quot;]&amp;gt; contains text &apos;WARN&apos; (7ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-2&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-2&quot;]&amp;gt; contains text &apos;10:30:10&apos; (9ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-3&quot;]&amp;gt; was present after 4 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-3&quot;]&amp;gt; contains text &apos;System: Network timeout occurred&apos; (9ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-3&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-3&quot;]&amp;gt; contains text &apos;ERROR&apos; (8ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-3&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-3&quot;]&amp;gt; contains text &apos;10:30:15&apos; (9ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-4&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-4&quot;]&amp;gt; contains text &apos;User: Speed detection started&apos; (9ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-4&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-4&quot;]&amp;gt; contains text &apos;INFO&apos; (9ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-log-entry-4&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element &amp;lt;[data-testid=&quot;debug-log-entry-4&quot;]&amp;gt; contains text &apos;10:30:20&apos; (10ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;event-log&quot;]&amp;gt; to be present (2ms)
  ✔ Expected element &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; to be present (2ms)
  ✔ Element &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; was present after 2 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;debug-toggle-button&quot;]&amp;gt; inner text equals &apos;Hide Debug Console&apos; (9ms)

  ✨ PASSED. 61 assertions. (590ms)


[Units Conversion Ct Nightwatch Test] Test Suite
───────────────────────────────────────────────────────────────────────────────
- Starting GeckoDriver on port 4444...

ℹ Connected to GeckoDriver on port 4444 (1780ms).
  Using: firefox (140.0) on LINUX.

- Loading url: http://localhost:3001/render-component/ControlsTestHarness?testName=Initial

  ℹ Loaded url http://localhost:3001/render-component/ControlsTestHarness?testName=Initial in 100ms
  ✔ Element &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; was present after 13 milliseconds.

  Running demonstrates mph → km/h → mph conversion cycle with full visibility:
───────────────────────────────────────────────────────────────────────────────────────────────────
  ✔ Element &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; was present after 26 milliseconds.
  ✔ Element &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; was present after 5 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;test-name&quot;]&amp;gt; inner text equals &apos;Test: Initial&apos; (11ms)
  ✔ Element &amp;lt;[data-testid=&quot;unit-toggle-button&quot;]&amp;gt; was present after 4 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;unit-toggle-button&quot;]&amp;gt; inner text equals &apos;Switch to
mph&apos; (10ms)
  ✔ Element &amp;lt;[data-testid=&quot;harness-units-state&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;harness-units-state&quot;]&amp;gt; inner text equals &apos;Units: METRIC (km/h)&apos; (8ms)
  ✔ Element &amp;lt;[data-testid=&quot;event-log&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Testing if element&apos;s &amp;lt;[data-testid=&quot;event-log&quot;]&amp;gt; inner text equals &apos;No events yet...&apos; (7ms)
  ✔ Element &amp;lt;[data-testid=&quot;unit-toggle-button&quot;]&amp;gt; was present after 3 milliseconds.
  ✔ Element &amp;lt;[data-testid=&quot;unit-toggle-button&quot;]&amp;gt; was visible after 11 milliseconds.

  PASSED: 11 passed (544ms)

───────────────────────────────────────────────────────────────────────────────────────────────────

  ️TEST FAILURE (14.615s): 
   - 0 assertions failed; 166 passed
   - 5 skipped

   ✖ 1) Controls.ct.nightwatch.test

   – demonstrates event coupling - recording toggle (481ms)

    SKIPPED (at runtime):
    - demonstrates event coupling - units toggle
    - shows processing state affecting component
    - complex scenario - multiple interactions with full trace
   ✖ 2) UnitsConversion.ct.nightwatch.test

   – demonstrates mph → km/h → mph conversion cycle with full visibility (544ms)

    SKIPPED (at runtime):
    - demonstrates units state with initial imperial mode
    - demonstrates units toggle with processing state

 Wrote HTML report file to: /home/paul/scm/car-doppler/test-results/nightwatch/nightwatch-html-report/index.html

Tearing down NightWatch test environment...

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;nightwatch-vs-selenium-webdriver-js&quot;&gt;NightWatch vs Selenium-WebDriver JS&lt;/h2&gt;

&lt;p&gt;They’re the same “Selenium”, so both are using real browsers locally or remotely. NightWatch has a slightly different grammar
tries to do more with less config files. It also has smooth built-in error reporting with automatic screenshots. I’m not
showing the HTML report for that but it is pretty. You could make automatic failure screenshots for selenium-webdriver but
it would require some coding setup. Nightwatch can also automate browser lifecycle for you. In my case I wanted one Firefox
left open for all tests in a run, and that’s not (yet) configured in my project.&lt;/p&gt;

&lt;p&gt;NightWatch.js strikes an excellent balance between power and simplicity, making it a solid choice for JavaScript teams wanting robust browser automation with a little less complexity of regular selenium-webdriver, yet still keeping the “real browser” selling point. It is also closer to the speed of Cypress.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Selenium Component Testing and visual documentation</title>
   <link href="https://paulhammant.com/2025/06/22/selenium-component-testing/"/>
   <updated>2025-06-22T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2025/06/22/selenium-component-testing</id>
   <content type="html">&lt;p&gt;Five days ago I posted &lt;a href=&quot;https://paulhammant.com/2025/06/17/ui-component-testing-revisited/&quot;&gt;UI component testing revisited featuring a React web app and Playwright component tests&lt;/a&gt; and two days ago&lt;a href=&quot;https://paulhammant.com/2025/06/20/cypress-component-testing/&quot;&gt;Cypress component testing&lt;/a&gt; for a Cypress version of the same (and a refresher on the ideas). Now…&lt;/p&gt;

&lt;h1 id=&quot;selenium-component-testing-of-aa-react-web-app&quot;&gt;Selenium Component Testing of aa React web app&lt;/h1&gt;

&lt;p&gt;Branch: &lt;a href=&quot;https://github.com/paul-hammant/car-doppler/tree/selenium_instead_of_playwright&quot;&gt;selenium_instead_of_playwright&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;selenium-visual-implementation-complete-test-documentation-through-screenshots&quot;&gt;Selenium Visual Implementation: Complete Test Documentation Through Screenshots&lt;/h2&gt;

&lt;p&gt;The Selenium implementation takes a similar visual-first approach, generating detailed screenshots for each test interaction. Here’s the identical Test Harness Component Testing pattern implemented with Selenium WebDriver, producing the same visual layout as both Playwright and Cypress implementations.&lt;/p&gt;

&lt;h3 id=&quot;recap---what-the-deployed-app-looks-like&quot;&gt;Recap - what the deployed app looks like&lt;/h3&gt;

&lt;p&gt;The app deployed in Safari on an smaller-screen iPhone:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/car-doppler-app.png&quot; alt=&quot;main app pic on iPhone&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;example-initial-component-state&quot;&gt;Example Initial Component State&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/selenium/Controls-initial-state.png&quot; alt=&quot;shows test harness with 3-section layout: blue component, green harness state, yellow event log&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The Selenium implementation produces the same visual pattern as Playwright and Cypress:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Component Under Test&lt;/strong&gt; (blue border) - The actual React component being tested&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Test Harness State&lt;/strong&gt; (green border) - Shows the parent component state that would exist in the real app&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Event Log&lt;/strong&gt; (yellow border) - Traces the complete interaction history for debugging&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;units-conversion-cycle-component-test&quot;&gt;Units Conversion Cycle Component Test&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Initial State: Metric Mode (Switch to mph available)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/selenium/UnitsConversion-cycle-initial-metric.png&quot; alt=&quot;shows test harness with 3-section layout: blue component, green harness state, yellow event log&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After Click: Imperial Mode (Switch to km/h available)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/selenium/UnitsConversion-cycle-switched-imperial.png&quot; alt=&quot;shows test harness with 3-section layout: blue component, green harness state, yellow event log&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After Second Click: Back to Metric Mode&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/selenium/UnitsConversion-cycle-back-to-metric.png&quot; alt=&quot;shows test harness with 3-section layout: blue component, green harness state, yellow event log&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The Selenium implementation captures the same interaction flow:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Button text changing from “Switch to mph” → “Switch to km/h” → “Switch to mph”&lt;/li&gt;
  &lt;li&gt;Harness state updating from “METRIC (km/h)” → “IMPERIAL (mph)” → “METRIC (km/h)”&lt;/li&gt;
  &lt;li&gt;Event log accumulating each interaction: “Units changed to imperial” → both events visible&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;architecture-external-test-server-vs-built-in-mounting&quot;&gt;Architecture: External Test Server vs Built-in Mounting&lt;/h2&gt;

&lt;p&gt;As an experiment in testing framework diversity, the car-doppler project also implemented the same Test Harness Component Testing approach using &lt;strong&gt;Selenium WebDriver with Jest&lt;/strong&gt;, providing an interesting comparison point to the Playwright implementation.&lt;/p&gt;

&lt;p&gt;The Selenium approach uses a different architecture from Playwright’s built-in component mounting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Selenium + Jest Setup:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Spawns an external component test server (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server.ts&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;Tests run against real HTTP endpoints&lt;/li&gt;
  &lt;li&gt;More complex setup but closer to production environment&lt;/li&gt;
  &lt;li&gt;Browser automation via WebDriver protocol&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Architectural Difference:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Component-Under-Test == CUT&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Playwright: mount(&amp;lt;CUT-&amp;gt;) → virtual DOM → browser context
Selenium:   CUT in a page, on a HTTP server → real DOM → WebDriver → browser automation
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;performance-trade-offs-and-optimization-journey&quot;&gt;Performance Trade-offs and Optimization Journey&lt;/h2&gt;

&lt;p&gt;For the Selenium Implementation:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;One Shared WebDriver instance across all tests&lt;/li&gt;
  &lt;li&gt;Single browser navigation + page replacement strategy&lt;/li&gt;
  &lt;li&gt;Optional screenshot generation (SKIP_SCREENSHOTS env var)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Selenium Performance Analysis (100 iterations of identical ‘best case’ test, headless):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;WITH Screenshots: 0.585s average per test (4.9 tests per second)&lt;/li&gt;
  &lt;li&gt;WITHOUT Screenshots: 1.248s average per test (7.99 tests per second)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Browser was Firefox (headless) and tests control component within the test harness. Admittedly, the test doesn’t have any interactions, so more interactive tests would be slower.&lt;/p&gt;

&lt;h3 id=&quot;the-performance-gap&quot;&gt;The Performance Gap&lt;/h3&gt;

&lt;p&gt;Despite aggressive optimization, the 100-iteration analysis reveals Selenium’s consistent performance disadvantage due to architectural differences. While Selenium is only marginally slower than Playwright without screenshots (0.585s vs 0.517s), the gap widens with screenshots. Both are much slower than Cypress’s optimized performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Process Architecture - The “Hops” Problem:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Selenium (Many Hops):&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Jest Process → HTTP → Express Server → Server-Side React Render → 
HTTP Response → WebDriver JSON Protocol → geckodriver → Firefox Process → 
DOM Updates → WebDriver Response → HTTP → Jest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Playwright Component Testing (Minimal Hops):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Jest Process → Vite Dev Server (same process) → 
Direct Component Mount → Embedded Chromium → DOM → Direct Response
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;practical-advice-for-selenium-using-teams-today&quot;&gt;Practical Advice for Selenium-using Teams Today&lt;/h3&gt;

&lt;p&gt;If you need Selenium-level browser coverage, accept the 1-second tax as the cost of cross-browser compatibility. Definately bypass all interstitial steps/pages and go directly to the component under test.&lt;/p&gt;

&lt;h2 id=&quot;when-selenium-component-testing-makes-sense&quot;&gt;When Selenium Component Testing Makes Sense&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use Cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;High-stakes UI components&lt;/strong&gt; where visual regression is critical&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Stakeholder communication&lt;/strong&gt; - screenshots are self-explanatory&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Complex interaction flows&lt;/strong&gt; that benefit from step-by-step visual documentationgit&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;integration-with-webdriver-infrastructure&quot;&gt;Integration with WebDriver Infrastructure&lt;/h2&gt;

&lt;p&gt;For teams already using Selenium for E2E testing, extending the same infrastructure to component testing provides consistency:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Same browser automation patterns&lt;/li&gt;
  &lt;li&gt;Some shared WebDriver utilities and helpers&lt;/li&gt;
  &lt;li&gt;Consistent screenshot/video capture approaches&lt;/li&gt;
  &lt;li&gt;Single testing technology stack&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;component-test-base-playwright-vs-selenium&quot;&gt;Component test-base: Playwright vs Selenium&lt;/h2&gt;

&lt;table class=&quot;table table-striped table-bordered&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Topic&lt;/th&gt;
      &lt;th&gt;&lt;strong&gt;Playwright test-base&lt;/strong&gt;&lt;/th&gt;
      &lt;th&gt;&lt;strong&gt;Selenium test-base&lt;/strong&gt;&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Core library&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@playwright/test&lt;/code&gt; + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@playwright/experimental-ct-react&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;selenium-webdriver&lt;/code&gt; (Jest/Mocha wrapper)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Browser control&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;In-process Playwright browsers (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chromium&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;firefox&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;webkit&lt;/code&gt;)&lt;/td&gt;
      &lt;td&gt;Remote/WebDriver sessions (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Builder&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;By&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;until&lt;/code&gt;)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Startup / teardown&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Handled by Playwright fixtures; no manual server code&lt;/td&gt;
      &lt;td&gt;Helpers &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;startTestServer()&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stopTestServer()&lt;/code&gt; spin up dev server on demand&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Global fixtures&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page&lt;/code&gt;, automatic context per test&lt;/td&gt;
      &lt;td&gt;Custom &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;driver&lt;/code&gt; via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getDriver()&lt;/code&gt;; shared helpers&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;DOM access&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page.locator()&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page.getByRole()&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page.getByTestId()&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;driver.findElement(By.css(&apos;[data-testid=&quot;…&quot;]&apos;))&lt;/code&gt; + helper wrappers&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Wait strategy&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Auto-wait built in; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;expect(locator).toBeVisible()&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Explicit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;driver.wait(until.elementLocated())&lt;/code&gt; for every action/assert&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Assertions&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Playwright’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;expect&lt;/code&gt; matchers&lt;/td&gt;
      &lt;td&gt;Node &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert&lt;/code&gt; / Jest &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;expect&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Timeouts&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Defaults ~ 30 s per Playwright&lt;/td&gt;
      &lt;td&gt;Test-suite timeout manually bumped (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jest.setTimeout(60000)&lt;/code&gt;)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Artifacts&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Trace viewer, video, screenshots configurable&lt;/td&gt;
      &lt;td&gt;No built-ins; screenshots would need extra code&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;File naming&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*.ct.playwright.test.ts(x)&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*.ct.selenium.test.ts(x)&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Dependencies&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@playwright/*&lt;/code&gt;, Playwright CT config&lt;/td&gt;
      &lt;td&gt;+ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;selenium-webdriver&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ts-jest&lt;/code&gt;, @types/selenium-webdriver&lt;br /&gt;– all Playwright packages&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Typical helpers (from diff)&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;N/A (built into Playwright)&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loadTestHarness(url)&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findElementByTestId(id)&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clickElementByTestId(id)&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getTextByTestId(id)&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Speed / flakiness&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Faster, less boilerplate, auto-waiting reduces flake&lt;/td&gt;
      &lt;td&gt;Slower, more boilerplate; explicit waits are best practice, though a default wait can be set too&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Overall, the Use version replicates Playwright functionality but requires custom scaffolding for server control, waiting, and assertions, resulting in more verbose and potentially slower tests.&lt;/p&gt;

&lt;h2 id=&quot;serial-vs-parallel-test-execution&quot;&gt;Serial vs parallel test execution&lt;/h2&gt;

&lt;p&gt;The testing in these blog entries was for &lt;strong&gt;serial execution&lt;/strong&gt; on a single machine or vm. Parallel and distributed execution capabilities vary significantly, and are worth summarizing:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Cypress&lt;/strong&gt;: Limited parallel execution (Cypress Cloud/Dashboard for CI, but component tests typically run serially)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Playwright&lt;/strong&gt;: Excellent built-in parallel execution (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--workers=4&lt;/code&gt;), can distribute across multiple machines&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Selenium&lt;/strong&gt;: Superior distributed execution via Selenium Grid, supports large-scale parallel execution across browser farms. That and 10+ SaaS vendors that made commercial Selenium grids (and more).&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;

&lt;p&gt;Selenium component testing feel more production-like, even with testing environments. While it comes with performance trade-offs compared to Cypress or (less so) Playwright, it is still compelling test evidence for multiple browsers potentially that’s immediately understandable to stakeholders. And with parallel/distributed execution via the likes of Selenium grid and commercial services, delays on good or bad could be reduced.&lt;/p&gt;

&lt;p&gt;The key insight is that Test Harness Component Testing works equally well across different automation frameworks, allowing teams to choose based on their specific priorities: speed (Cypress), cross-browser capabilities (Playwright), or comprehensive visual documentation and production-like environments (Selenium).&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Cypress Component Testing - Changing from Playwright for a demo repo</title>
   <link href="https://paulhammant.com/2025/06/20/cypress-component-testing/"/>
   <updated>2025-06-20T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2025/06/20/cypress-component-testing</id>
   <content type="html">&lt;p&gt;A few days ago I posted &lt;a href=&quot;https://paulhammant.com/2025/06/17/ui-component-testing-revisited/&quot;&gt;UI component testing revisited&lt;/a&gt; on some component testing patterns I’ve been interested in for many years. The test application was React in TypeScript. The component-testing technology I focused on was Playwright. In this blog entry I explore Cypress as an alternative to Playwright for component testing.&lt;/p&gt;

&lt;h1 id=&quot;quick-refresher-test-harness-component-testing-2017--2025&quot;&gt;Quick Refresher: Test Harness Component Testing (2017 → 2025)&lt;/h1&gt;

&lt;p&gt;Back in 2017, I introduced the concept of testing UI components within test harnesses rather than in isolation. The key insight was testing components in the “smallest reasonable rectangle” while maintaining realistic event coupling to parent components. Instead of mocking everything, you create a test harness that simulates how the component would actually be used in the real application. Could be that someone thought of it before me, of course. That’s usually the case.&lt;/p&gt;

&lt;p&gt;The pattern involves dual assertions: testing both the component’s visual state AND the test harness state to verify that events flow correctly between child and parent. This bridges the gap between fast unit tests and comprehensive integration tests, providing confidence that components work correctly when integrated while maintaining reasonable test speeds.&lt;/p&gt;

&lt;p&gt;Eight years later, this approach has become mainstream for React/Angular teams, with modern tooling making visual verification through screenshots a powerful addition for stakeholder communication and debugging.&lt;/p&gt;

&lt;h1 id=&quot;cypress-component-testing&quot;&gt;Cypress Component Testing&lt;/h1&gt;

&lt;p&gt;Branch with code: &lt;a href=&quot;https://github.com/paul-hammant/car-doppler/tree/cypress_instead_of_playwright&quot;&gt;cypress_instead_of_playwright&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;component-testing&quot;&gt;Component testing&lt;/h2&gt;

&lt;p&gt;While the examples in this article demonstrate the pattern using Playwright, the actual implementation in the cypress_instead_of_playwright branch of the car-doppler repo uses Cypress component testing instead, unsurprisingly. Here’s how the two approaches compare:&lt;/p&gt;

&lt;h3 id=&quot;comparison-criteria&quot;&gt;Comparison Criteria&lt;/h3&gt;

&lt;p&gt;For evaluating component testing frameworks, we’ll assess each on:&lt;/p&gt;

&lt;table class=&quot;table table-striped table-bordered&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Criterion&lt;/th&gt;
      &lt;th&gt;Weight&lt;/th&gt;
      &lt;th&gt;Why It Matters&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;High&lt;/td&gt;
      &lt;td&gt;Fast feedback during development&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Developer Experience&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;High&lt;/td&gt;
      &lt;td&gt;Learning curve and debugging ease&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Browser Support&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Medium&lt;/td&gt;
      &lt;td&gt;Cross-browser validation needs&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Ecosystem Integration&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Medium&lt;/td&gt;
      &lt;td&gt;CI/CD and tooling compatibility&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Visual Testing&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Medium&lt;/td&gt;
      &lt;td&gt;Screenshot and visual regression capabilities&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Setup Complexity&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Low&lt;/td&gt;
      &lt;td&gt;One-time cost, varies by team expertise&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;This framework will guide our Playwright ↔ Cypress ↔ Selenium comparisons.&lt;/p&gt;

&lt;h3 id=&quot;testing-framework-differences&quot;&gt;Testing Framework Differences&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cypress Component Testing:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Integrated test runner with excellent developer experience&lt;/li&gt;
  &lt;li&gt;Built-in browser automation and debugging tools&lt;/li&gt;
  &lt;li&gt;Simpler setup for React component testing&lt;/li&gt;
  &lt;li&gt;Real-time test runner with automatic re-runs&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Exceptional performance&lt;/strong&gt; for component testing (17x faster than Playwright)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Playwright Component Testing:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Cross-browser testing capabilities (Chrome, Firefox, Safari)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Better for cross-browser validation&lt;/strong&gt; (Cypress is Chromium-focused)&lt;/li&gt;
  &lt;li&gt;Better CI/CD integration options&lt;/li&gt;
  &lt;li&gt;More flexible browser configuration&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Broader ecosystem support&lt;/strong&gt; and active development&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;performance-comparison&quot;&gt;Performance Comparison&lt;/h3&gt;

&lt;p&gt;Cypress by default uses an embedded browser in Electron, though real Chromium or Firefox can be configured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cypress Implementation (Actual):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Specs: 3 component test files
Tests: 10 total tests across components  
Duration: ~1.6 seconds total execution
Performance: ~6.25 tests per second
Browser: Electron 130 (headless)
E2E: 11 passing tests in ~10 seconds (with some skipped tests)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Cypress Performance Analysis (100 iterations of identical test):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;WITHOUT Screenshots: 0.030s average per test (33.3 tests per second)
WITH Screenshots: 0.190s average per test (5.26 tests per second)
Browser: Electron 130 (headless)
Test: Controls component with Test Harness Component Testing
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Playwright Implementation (Article Examples):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Performance: 1.37-1.93 tests per second
WITH Screenshots: 0.730s average per test
WITHOUT Screenshots: 0.517s average per test
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Performance Comparison:&lt;/strong&gt;&lt;/p&gt;

&lt;table class=&quot;table table-striped table-bordered&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Framework&lt;/th&gt;
      &lt;th&gt;WITHOUT Screenshots&lt;/th&gt;
      &lt;th&gt;WITH Screenshots&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Cypress&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;0.030s&lt;/strong&gt; (33.3/sec)&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;0.190s&lt;/strong&gt; (5.26/sec)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Playwright&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;0.517s&lt;/strong&gt; (1.93/sec)&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;0.730s&lt;/strong&gt; (1.37/sec)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Advantage&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;Cypress 17x faster&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;Cypress 3.8x faster&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The Cypress implementation dramatically outperforms Playwright, likely due to:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Optimized component mounting&lt;/strong&gt;: Direct React component injection vs external browser process&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Command batching&lt;/strong&gt;: Multiple operations sent as coordinated sequences&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Electron efficiency&lt;/strong&gt;: Highly optimized for automation workloads&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Reduced protocol overhead&lt;/strong&gt;: Fewer round-trips between test runner and browser - batching of long series of interactions with the page under test is a Cypress special-sauce.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both implementations successfully achieve the core goal: testing components in the “smallest reasonable rectangle” with realistic event coupling and visual verification capabilities.&lt;/p&gt;

&lt;h3 id=&quot;when-to-choose-cypress&quot;&gt;When to Choose Cypress&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Fast development feedback loops&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The 17x performance advantage significantly reduces wait times during development, if you’re running all component tests of that class. If you know the test you want to run, or have a smart way of automatically selecting impacted tests, that advantage is less relevant.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Interactive debugging&lt;/strong&gt;: Time-travel debugging is compelling too, for complex interaction flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You might not choose Cypress if:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Cross-browser validation&lt;/strong&gt;: A limitation to Chromium-based browsers primarily might be problematic if you needed Safari/Firefox&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Legacy web applications&lt;/strong&gt;: Better suited for modern JavaScript frameworks and much harder for (say) ASP.NET-MVC apps.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;cypresss-command-batching-a-speed-advantage-with-mind-shift-requirements&quot;&gt;Cypress’s Command Batching: A Speed Advantage with Mind-Shift Requirements&lt;/h3&gt;

&lt;p&gt;One of Cypress’s unique architectural advantages is its &lt;strong&gt;command batching and queuing system&lt;/strong&gt;. Unlike traditional testing frameworks that execute commands synchronously, Cypress batches operations and sends them to the browser for execution as a coordinated sequence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Batching Advantage:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Cypress batches these commands and sends them together:&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;cy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;[data-testid=&quot;record-button&quot;]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;cy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;[data-testid=&quot;unit-toggle-button&quot;]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; 
&lt;span class=&quot;nx&quot;&gt;cy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;[data-testid=&quot;record-button&quot;]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;cy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;[data-testid=&quot;unit-toggle-button&quot;]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Browser receives: &quot;click these 4 elements in sequence&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// vs traditional approach: 4 separate round-trips&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The performance impact ia a reduced round-trip latency as multiple operations sent in one batch, which leads to a network efficiency that can’t be beaten. Large form filling and wait-for strategies are particularly suited to batching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Mind-Shift in Simple Terms:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Thinking synchronously (won&apos;t work):&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;[data-testid=&quot;button&quot;]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Returns a command, not text!&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Start&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;cm&quot;&gt;/* This fails */&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Thinking in Cypress chains:&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;cy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;[data-testid=&quot;button&quot;]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;should&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;contain.text&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Start&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// Assertion happens when command runs&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;                         &lt;span class=&quot;c1&quot;&gt;// Click happens after text verification&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Why This Matters: Once you understand the queuing concept, Cypress tests become more reliable because assertions auto-retry and commands execute in guaranteed order. I tried the same thing (ish) with FluentSelenium way back. Well the retry of chains of locators, but the batching - it was very chatty over however many hops you had between the test and the browser. I’m sure lots of seasoned QE folks initially try to program the old way, before deciding to relent and do things the Cypress way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Playwright Comparison:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Playwright executes commands more immediately but with more round-trip overhead. For simple tests the difference is negligible, but for complex component interactions the batching advantage compounds.&lt;/p&gt;

&lt;h2 id=&quot;cypress-visual-implementation-the-same-pattern-different-tool&quot;&gt;Cypress Visual Implementation: The Same Pattern, Different Tool&lt;/h2&gt;

&lt;p&gt;Just as the post a couple of days ago demonstrated with “Test Harness Component Testing with Playwright” screenshots from a particular test, here’s the identical implementation using &lt;strong&gt;Cypress&lt;/strong&gt; component testing. The visual layout and testing approach remain exactly the same - demonstrating the framework-agnostic nature of the pattern.&lt;/p&gt;

&lt;h3 id=&quot;recap---what-the-deployed-app-looks-like&quot;&gt;Recap - what the deployed app looks like&lt;/h3&gt;

&lt;p&gt;The app deployed in Safari on an smaller-screen iPhone:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/car-doppler-app.png&quot; alt=&quot;main app pic on iPhone&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;initial-component-state&quot;&gt;Initial Component State&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/cypress/Controls-initial-state-cypress.png&quot; alt=&quot;shows test harness with 3-section layout: blue component, green harness state, yellow event log&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The Cypress implementation produces the same visual pattern as Playwright:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Component Under Test&lt;/strong&gt; (blue border) - The actual React component being tested&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Test Harness State&lt;/strong&gt; (green border) - Shows the parent component state that would exist in the real app&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Event Log&lt;/strong&gt; (yellow border) - Traces the complete interaction history for debugging&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;units-conversion-cycle-demonstration&quot;&gt;Units Conversion Cycle Demonstration&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Initial State: Metric Mode (Switch to mph available)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/cypress/UnitsConversion-cycle-initial-metric-cypress.png&quot; alt=&quot;shows test harness with 3-section layout: blue component, green harness state, yellow event log&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After Click: Imperial Mode (Switch to km/h available)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/cypress/UnitsConversion-cycle-switched-imperial-cypress.png&quot; alt=&quot;shows test harness with 3-section layout: blue component, green harness state, yellow event log&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After Second Click: Back to Metric Mode&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/cypress/UnitsConversion-cycle-back-to-metric-cypress.png&quot; alt=&quot;shows test harness with 3-section layout: blue component, green harness state, yellow event log&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The Cypress implementation captures the same interaction flow:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Button text changing from “Switch to mph” → “Switch to km/h” → “Switch to mph”&lt;/li&gt;
  &lt;li&gt;Harness state updating from “METRIC (km/h)” → “IMPERIAL (mph)” → “METRIC (km/h)”&lt;/li&gt;
  &lt;li&gt;Event log accumulating each interaction: “Units changed to imperial” → both events visible&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;a-potential-reader-questions-why-not-just-use-storybook&quot;&gt;A potential reader questions: “Why not just use Storybook?”&lt;/h2&gt;

&lt;p&gt;Yes, I don’t have Storybook configured as part of the ‘car-doppler’ solution. Storybook and Test Harness Component Testing serve different but complementary purposes, I think:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storybook excels at:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Visual component documentation and design system management. Perhaps allowing a menu-centric picking of components for multiple larger web applications.&lt;/li&gt;
  &lt;li&gt;Interactive component exploration for designers and stakeholders&lt;/li&gt;
  &lt;li&gt;Visual regression testing (Chromatic or similar tools)&lt;/li&gt;
  &lt;li&gt;Component isolation for development and review&lt;/li&gt;
  &lt;li&gt;And yes it is an EASY mounting for GET-centric test-automation purposes - I’ve been in teams that have loved this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;A Test Harness Component Testing setup excels at:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Automated verification of all component behaviors and event coupling&lt;/li&gt;
  &lt;li&gt;Testing realistic parent-child component interactions&lt;/li&gt;
  &lt;li&gt;Continuous integration validation with assertion-based testing&lt;/li&gt;
  &lt;li&gt;Debugging component integration issues through event tracing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Reality, though: Many teams would use both. Storybook for documentation and design review, Test Harness Component Testing for automated quality assurance. They’re solving different problems in the component development lifecycle.&lt;/p&gt;

&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;

&lt;p&gt;Cypress component testing offers significant performance advantages for teams focused on fast development feedback loops, particularly when working with React/Angular applications. The 17x speed improvement over Playwright for component testing makes it an attractive choice for development workflows that prioritize rapid iteration.&lt;/p&gt;

&lt;p&gt;The framework-agnostic nature of Test Harness Component Testing means the same testing patterns and visual verification approaches work across different tools, allowing teams to choose based on their specific needs rather than being locked into a particular testing philosophy.&lt;/p&gt;

&lt;p&gt;For teams building component-heavy applications where development speed is critical and cross-browser testing can be handled separately, Cypress component testing provides an excellent balance of performance, developer experience, and debugging capabilities.&lt;/p&gt;

&lt;p&gt;The diff between the two branches is not so easy to see what went where: &lt;a href=&quot;https://github.com/paul-hammant/car-doppler/compare/main...cypress_instead_of_playwright&quot;&gt;github.com/paul-hammant/car-doppler/compare/main…cypress_instead_of_playwright&lt;/a&gt;, but you can get a sense how how much was changed.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>UI Component Testing Revisited: Modern Implementation with Visual Verification</title>
   <link href="https://paulhammant.com/2025/06/17/ui-component-testing-revisited/"/>
   <updated>2025-06-17T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2025/06/17/ui-component-testing-revisited</id>
   <content type="html">&lt;p&gt;Eight years ago, I wrote about &lt;a href=&quot;https://paulhammant.com/2017/02/01/ui-component-testing/&quot;&gt;UI Component Testing&lt;/a&gt; - the idea of testing UI components in isolation within test harnesses, focusing on the smallest reasonable rectangle while maintaining realistic evenThis blogt coupling. The goal was to be closer to the speed of unit tests with the confidence of integration tests, targeting “multiple tests per second” throughput.&lt;/p&gt;

&lt;p&gt;Fast forward to 2025, and I’ve implemented this pattern in a real-world React TypeScript application with some fascinating modern twists. This post explores both the evolution of the testing approach and the unique challenges of building a browser-based Doppler speed detection app.&lt;/p&gt;

&lt;h2 id=&quot;the-application-doppler-speed-detection-in-the-browser&quot;&gt;The Application: Doppler Speed Detection in the Browser&lt;/h2&gt;

&lt;p&gt;Before diving into the testing implementation, let me describe the application that provided the testing canvas. &lt;a href=&quot;https://paul-hammant.github.io/car-doppler/&quot;&gt;“Car doppler”&lt;/a&gt; is a React + TypeScript web application that attempts to detect vehicle speeds using Doppler shift analysis through your device’s microphone (showing on an smaller-screen iPhone):&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/car-doppler-app.png&quot; alt=&quot;main app pic on iPhone&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The app analyzes audio in real-time, looking for frequency shifts that indicate approaching or receding vehicles. When a car drives past at 40 mph in a 30 mph zone, the app attempts to calculate the speed based on the Doppler effect - though as prominently warned throughout the application, &lt;strong&gt;the calculations are wildly inaccurate and should never be used for law enforcement or any official speed measurements.&lt;/strong&gt; This is an experimental proof-of-concept for browser audio processing, not a precision instrument.&lt;/p&gt;

&lt;h3 id=&quot;why-a-web-app-instead-of-native-mobile&quot;&gt;Why a Web App Instead of Native Mobile?&lt;/h3&gt;

&lt;p&gt;This type of application would arguably have better user experience as a native iOS or Android app - better microphone access, background processing, and more natural mobile UX patterns. However, as any developer who’s dealt with app store submissions knows, the overhead is substantial:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;App store review processes and policies&lt;/li&gt;
  &lt;li&gt;Developer account fees and maintenance&lt;/li&gt;
  &lt;li&gt;Platform-specific development or cross-platform complexity&lt;/li&gt;
  &lt;li&gt;Distribution and update mechanisms&lt;/li&gt;
  &lt;li&gt;Content policy compliance (speed detection might raise flags)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A web app sidesteps all of this drama. Deploy to GitHub Pages, share a URL, and users can immediately access the functionality. The trade-off is dealing with browser limitations and web platform constraints.&lt;/p&gt;

&lt;h2 id=&quot;technical-challenges-wasm-simd-and-the-browser-environment&quot;&gt;Technical Challenges: WASM-SIMD and the Browser Environment&lt;/h2&gt;

&lt;p&gt;The audio processing pipeline presented several interesting technical challenges that influenced the component testing approach and at fundamental choices for techs.&lt;/p&gt;

&lt;h3 id=&quot;wasm-with-simd-performance-at-a-cost&quot;&gt;WASM with SIMD: Performance at a Cost&lt;/h3&gt;

&lt;p&gt;For the computationally intensive FFT (Fast Fourier Transform) operations required for Doppler analysis, the app uses WebAssembly with SIMD (Single Instruction, Multiple Data) optimizations. This provides near-native performance for audio processing, but comes with complexity. Presently, making a Node solution use a WASM piece and work a) in Node itself for compile/test, and b) on the web itself is hard.  Hard enough for me to hive off the algorithm stuff to a separate project and use “Runtime-Linking” (not build-time linking) to bring this into the solution as an effect doppler library. That as opposed to a TypeScript component/service among many within a solution. My project for that is https://github.com/paul-hammant/Car-Speed-Via-Doppler-Library and it auto-deploys to GitHub-Pages on git-push. The FFT tech itself is from https://github.com/echogarden-project/pffft-wasm which purports to deliver a .wasm file for use in a web environment. The prod-code is web-centric (no npm, no nodejs). The test suite is a bunch of regular npm/node choices. Tests run in a real browser via PlayWright. The build is of pffft-wasm is hard, so I checked in two .wasm files to just shortcut the rest of that repo’s build a little. One of the in there WASMs is for SIMD-enabled, and one for SIMD-disabled.  You can go press “test now” buttons for the three implementations in the GH-P site: https://paul-hammant.github.io/Car-Speed-Via-Doppler-Library/ &lt;strong&gt;Despite significant effort, I could not get the SIMD-enabled implementation working reliably across browsers&lt;/strong&gt; - it remains a known limitation.&lt;/p&gt;

&lt;p&gt;Another challenge is that SIMD support and performance varies across browsers and devices. The app maybe needs to gracefully degrade through multiple fallback strategies, and each path needs testing. This is where the component testing approach becomes valuable - you can test the UI behavior across different WASM loading states without actually loading WASM in every test.&lt;/p&gt;

&lt;h3 id=&quot;nodejs-modules-in-the-browser-the-require-problem&quot;&gt;Node.js Modules in the Browser: The require() problem&lt;/h3&gt;

&lt;p&gt;Modern JavaScript development often involves libraries originally written for Node.js that use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;require()&lt;/code&gt; statements. Browsers don’t support &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;require()&lt;/code&gt; - they use ES6 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import/export&lt;/code&gt; syntax. The car-speed-via-doppler-analysis library had to be carefully architected to avoid Node.js-specific patterns:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// ❌ Not allowed in browser context&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;fs&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SomeUtil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./utils&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// ✅ Browser-compatible ES6 modules&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SomeUtil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./utils.js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This constraint is well known: &lt;a href=&quot;https://github.com/nodejs/node/issues/33954&quot;&gt;When will CommonJS modules (require) be deprecated and removed?&lt;/a&gt; - issue on GitHub.&lt;/p&gt;

&lt;p&gt;The complexity for me is that getting AIs to help a dual need of integration tests must pass under node execution AND in a first class browser is hard. Not only in the browser but coupled to three modes of operation 1. WASM &amp;amp; SIMD enabled, 2. WASM &amp;amp; SIMD disabled, 3. Pure JavaScript. That is a lot of permutations to juggle concurrently and have a non-functional requirement that “all tests must pass”. So at one point I abandoned the node/npm -centric core of the doppler library - https://github.com/echogarden-project/pffft-wasm, and moved to runtime-linkage on the web only.&lt;/p&gt;

&lt;h2 id=&quot;modern-component-testing-the-2025-implementation&quot;&gt;Modern Component Testing: The 2025 Implementation&lt;/h2&gt;

&lt;p&gt;The 2017 blog post described the concept; here’s how it looks implemented with modern tooling.&lt;/p&gt;

&lt;h3 id=&quot;component-under-test-in-a-test-harness&quot;&gt;“Component Under Test” in a test Harness&lt;/h3&gt;

&lt;p&gt;Following the original article, each component test creates a test harness that simulates how the component would be used in the real application. The key insight remains the same: test both the component AND the test harness state through realistic event coupling.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/playwright/Controls-initial-state.png&quot; alt=&quot;shows test harness with 3-section layout: blue component, green harness state, yellow event log&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The visual layout implements the pattern perfectly:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Component Under Test&lt;/strong&gt; (blue border) - The actual React component being tested&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Test Harness State&lt;/strong&gt; (green border) - Shows the parent component state that would exist in the real app&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Event Log&lt;/strong&gt; (yellow border) - Traces the complete interaction history for debugging&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;dual-assertions-component--harness&quot;&gt;Dual Assertions: Component + Harness&lt;/h3&gt;

&lt;p&gt;Here’s what the 2017 concept looks like in modern TypeScript with Playwright:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;demonstrates event coupling - recording toggle&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;mount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;component&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;mount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ControlsTestHarness&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;testName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Recording Toggle Event Coupling&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Assert on COMPONENT (traditional component testing)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getByTestId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;record-button&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toContainText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Start&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  
  &lt;span class=&quot;c1&quot;&gt;// Assert on TEST HARNESS (the 2017 insight)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getByTestId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;harness-recording-state&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toContainText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;OFF&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// 5 second pause to see initial state&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;waitForTimeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// User interaction&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getByTestId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;record-button&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  
  &lt;span class=&quot;c1&quot;&gt;// 5 second pause to see the change&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;waitForTimeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Assert BOTH updated via event coupling&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getByTestId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;record-button&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toContainText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Stop&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getByTestId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;harness-recording-state&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toContainText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ON&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  
  &lt;span class=&quot;c1&quot;&gt;// Assert on EVENT COUPLING trace&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getByTestId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;event-log&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toContainText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Recording started&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;visual-verification-screenshots&quot;&gt;Visual Verification: Screenshots&lt;/h3&gt;

&lt;p&gt;One major testing-tech evolution since 2017 is the ability to &lt;strong&gt;easily&lt;/strong&gt; capture visual verification. Each test automatically generates:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Screenshots at each interaction point&lt;/li&gt;
  &lt;li&gt;Interactive browser sessions for debugging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This addresses one of the original challenges: convincing stakeholders that the component testing provides real value. When you can show screenshots of the test harness demonstrating realistic user interactions, it becomes more tangible to non-developing collaborators and stakeholders.&lt;/p&gt;

&lt;p&gt;Of course Nx, Cypress, StoryBook popularized this way for TypeScript/JavaScript development. And that hasn’t stood still either - you can now mock interactions with backend services in the same tech.  We’re using Playwright as mentioned (not Cypress). And not Selenium (I was co-creator of v1), but either of those two would be possible here, and possibly just as fast in execution.&lt;/p&gt;

&lt;h3 id=&quot;test-execution-multiple-modes&quot;&gt;Test Execution: Multiple Modes&lt;/h3&gt;

&lt;p&gt;The testing setup supports different execution modes for different purposes:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Bottom of the test pyramid - fastest units/specs&lt;/span&gt;
npm &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Fast feedback during component testing during development&lt;/span&gt;
npm run &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt;:ct

&lt;span class=&quot;c&quot;&gt;# Visual verification and demos - I slowed this down to take screenshots.&lt;/span&gt;
npm run &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt;:ct:headed

&lt;span class=&quot;c&quot;&gt;# Interactive eyeball debugging&lt;/span&gt;
npm run &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt;:ct:ui

&lt;span class=&quot;c&quot;&gt;# Specific component test focus&lt;/span&gt;
npm run &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt;:ct:headed &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; UnitsConversion.ct.test.tsx
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note: React Testing Library (RTL) is used under component tests, and itself is a standard choice for React work.&lt;/p&gt;

&lt;h3 id=&quot;real-world-example-units-conversion-testing&quot;&gt;Real-World Example: Units Conversion Testing&lt;/h3&gt;

&lt;p&gt;The units conversion functionality (mph to/from km/h) provides a perfect example of the pattern in action:&lt;/p&gt;

&lt;p&gt;Before Playwright interaction:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/playwright/UnitsConversion-cycle-initial-metric.png&quot; alt=&quot;shows test harness with 3-section layout: blue component, green harness state, yellow event log&quot; /&gt;&lt;/p&gt;

&lt;p&gt;After Playwright interaction:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/playwright/UnitsConversion-cycle-switched-imperial.png&quot; alt=&quot;shows test harness with 3-section layout: blue component, green harness state, yellow event log&quot; /&gt;&lt;/p&gt;

&lt;p&gt;After one more Playwright interaction:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/playwright/UnitsConversion-cycle-back-to-metric.png&quot; alt=&quot;shows test harness with 3-section layout: blue component, green harness state, yellow event log&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The test demonstrates:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Initial state: “Switch to mph” button, harness shows “METRIC (km/h)”&lt;/li&gt;
  &lt;li&gt;After click: “Switch to km/h” button, harness shows “IMPERIAL (mph)”&lt;/li&gt;
  &lt;li&gt;After second click: Back to original state, event log shows complete history&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This single test verifies:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Component visual state changes ✓&lt;/li&gt;
  &lt;li&gt;Parent component state management ✓&lt;/li&gt;
  &lt;li&gt;Event coupling integrity ✓&lt;/li&gt;
  &lt;li&gt;User interaction flow ✓&lt;/li&gt;
  &lt;li&gt;Debugging trace ✓&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;performance-achieving-the-2017-prediction&quot;&gt;Performance: Achieving the 2017 prediction&lt;/h2&gt;

&lt;p&gt;The original post aimed for “multiple tests per second.” With modern hardware and optimized tooling, we’re achieving:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Unit tests&lt;/strong&gt;: 2-5ms each (200-500 tests per second)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Component tests&lt;/strong&gt;: sub-second each (ignoring browser startup)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Full app tests&lt;/strong&gt;: 5-15 seconds (complete user workflows)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m cheating a little - running 100 of the same component test then dividing that time by 100 in order to eliminate test startup time. Breaking down the CT times:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;WITH Screenshots we have an average per test of 0.730s (1.37 tests per second)&lt;/li&gt;
  &lt;li&gt;WITHOUT Screenshots we have an average per test of 0.517s (1.93 tests per second)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At least on my Chromebook (i7-1265U w/ 32 GB RAM), that’s the perf. Component tests of this nature might hit 6+ tests a second on an M4 Mac. “More importantly, they hit visual verification, realistic event coupling, and stakeholder-demonstrable test evidence.&lt;/p&gt;

&lt;p&gt;The fundamental component testing insight remains the same vs the 2017 blog entry: test the smallest reasonable rectangle with representative event coupling to other UI even if that last is not prod code. The tooling these days makes this easy.&lt;/p&gt;

&lt;h2 id=&quot;lessons-learned&quot;&gt;Lessons Learned&lt;/h2&gt;

&lt;h3 id=&quot;what-worked-well&quot;&gt;What Worked Well&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Visual Verification&lt;/strong&gt; - Screenshots and videos provide compelling evidence of test value&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Event Coupling&lt;/strong&gt; - Testing both component and harness state catches integration bugs unit tests miss&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Stakeholder Communication&lt;/strong&gt; - Non-technical stakeholders can understand what these tests verify&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Debugging Aid&lt;/strong&gt; - Event logs provide clear audit trails for interaction flows&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;challenges-and-trade-offs&quot;&gt;Challenges and Trade-offs&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Test Speed&lt;/strong&gt; - Component tests are slower than unit tests, requiring careful test pyramid balance&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Maintenance&lt;/strong&gt; - Test harnesses require maintenance as components evolve&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Tooling Complexity&lt;/strong&gt; - Setting up browser-based testing requires more infrastructure&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Flakiness&lt;/strong&gt; - Browser tests can be less reliable than pure unit tests&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;when-to-use-this-pattern&quot;&gt;When to Use This Pattern&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Good Candidates:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Complex interactive components with state management&lt;/li&gt;
  &lt;li&gt;Components with multiple interaction modes&lt;/li&gt;
  &lt;li&gt;Critical user workflow components&lt;/li&gt;
  &lt;li&gt;Components that integrate with external systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Poor Candidates:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Simple presentational components&lt;/li&gt;
  &lt;li&gt;Pure computational functions&lt;/li&gt;
  &lt;li&gt;Components with minimal user interaction&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;the-future-ai-and-component-testing&quot;&gt;The Future: AI and Component Testing&lt;/h2&gt;

&lt;p&gt;Looking forward, I’m curious how AI coding assistants will interact with this testing pattern. The visual nature of the test harnesses and event coupling traces might provide excellent training data for AI systems to understand component behavior and generate more sophisticated tests.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/playwright-results.png&quot; alt=&quot;playwright results frame&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;The UI component testing pattern from 2017 is mainstream now, for TypeScript/Reach/Angular teams at least.&lt;/p&gt;

&lt;p&gt;The potential for visual verification capabilities has transform this class of tests from pure validation tools into communication and documentation aids. When stakeholders can review test screenshots and see exactly how the component behaves under different conditions, the value proposition becomes undeniable. Again Nx/Storybook/Cypress really pushed this for many years.&lt;/p&gt;

&lt;p&gt;For teams building complex React or Angular applications, especially those dealing with real-time data processing, audio/video manipulation, or other browser-constrained scenarios, this testing approach provides a robust foundation for maintaining component quality while supporting rapid development iteration.&lt;/p&gt;

&lt;h1 id=&quot;repos&quot;&gt;Repos&lt;/h1&gt;

&lt;p&gt;The complete React-app source code and test implementations are available in the &lt;a href=&quot;https://github.com/paul-hammant/car-doppler&quot;&gt;github.com/paul-hammant/car-doppler&lt;/a&gt; repo.&lt;/p&gt;

&lt;p&gt;The doppler library is in its own repo &lt;a href=&quot;https://github.com/paul-hammant/Car-Speed-Via-Doppler-Library&quot;&gt;github.com/paul-hammant/Car-Speed-Via-Doppler-Library&lt;/a&gt;, but it need needs better WAV files for testing. And more strategies for detecting various things.&lt;/p&gt;
</content>
 </entry>
 
 
</feed>