# StyloBot Release Series: Testing the Thing That Won't Sit Still

*Come si testa un rilevatore la cui risposta dovrebbe cambiare mentre impara? Questo post è su BDF, il formato di definizione comportamentale che rende lo StyloBot testabile : un file definisce una classe di trafficoM SK3 poi guida una riproduzione di regressioneMSC4 generatura di caricoMNK5 e un'udizione di calibrazioneMRK6*

[<img src="/articleimages/stylobot-logo.svg" alt="StyloBot" width="120" />](https://www.stylobot.net)

> **StyloBot Release Series**
> 
> 1. [**Comportamento, Non identità**](/blog/stylobot-fingerprint): perché StyloBot modella i clienti comportamentalmente
> 2. [**Comportamento-Aware ASP.NET UI**](/blog/behaviour-aware-ux): il server- restituisce la superficie su quel risultato di rilevamento.
> 3. [**Trovare e riparare la crescita senza limiti nei servizi NET Long-Running .**](/blog/stylobot-release-reliability): la disciplina di affidabilità che mantiene il motore noioso nella produzione.
> 4. [**Comportamento-Conoscere l'interfaccia grafica del tipoScript**](/blog/typescript-sdk): ExpressM SK1 Fastify, e componenti del browser
> 5. [**L'architettura della Sidecar**](/blog/sidecar-architecture): come il motore di rilevazione si collega a stack non -.NET
> 6. [**Imparare a diventare più veloce**](/blog/stylobot-release-learning): il sistema di apprendimento adattativo, quattroM SK2 memoria a livello di livello+, e la cassa del verdetto
> 7. **Provare a fare ciò che non si ferma.**: la disciplina di verificazioneM SK1 un file BDF guida la regressione , carico, e calibrazione
> 8. [**StyloExtract - un convertitore locale di HTML per Markdown**](/blog/stylobot-release-styloextract): l'HTML→Layere Markdown che si combina con il rilevatore , il walkeder bug lucidVIEW catturatoM SK3 e la catena di cibo per cani che lo rende onesto

<!--category-- ASP.NET, StyloBot, Bot Detection, Testing, Architecture -->
<datetime class="hidden">2026-06-01T10:30</datetime>

La disciplina della affidabilità è in [Trovare e fissare la crescita senza limiti](/blog/stylobot-release-reliability); il sistema di apprendimento adaptivo in [Imparare a diventare più veloce](/blog/stylobot-release-learning); fonte a [github.comM SK1scottgal/stylobot](https://github.com/scottgal/stylobot).

[TOC]

---


## Come si testa qualcosa che impara?

Questa è la domanda scomoda dietro lo StyloBot, e non è retorica.

Un test convenzionale punta una funzione al posto: input X restituisce output YM SK1 Funziona quando la cosa sotto prova è stabile. StyloBot non è intenzionalmente stabile a quel livelloMSC3 Accumula prove comportamentaliMST4 una singola richiestaMSP5 il giudizio dipende dalla richiesta stessaMSR6 l'impronta digitaleMRS7 la deriva dall'ancora dell'archetipo imparatoM SR8 il comportamento della sessione MSR9 e se il sistema ha già visto abbastanza per saltare drittamente verso il percorso veloceMSL10 Ogni impronta digitale inizia a puntare alla superficie più vicina all'archeipo Msl11 la sua precedenteMRSS12 quando si atterrano nuove osservazioni si muoveMSSR13 e il movimento stesso è il segnaleMRSA14 Il conciliatore delle impronte digitali metastabili risolve un vettore rumoroso ad un'identità stabile attraverso due match di passaggi, il cui Pass MSRS16 può rivedere il Pass |MRS17 | L'allocazione |MSRS18 | MSRS19 | l'archedipo |SRS20 | il modello dell'ancraggio |SMRS21 | la superficie di derivazione \MSRS22 | e le impronte digitale metastabile sono ricoperte [Imparare a diventare più veloce](/blog/stylobot-release-learning).)

Il verdetto per la richiesta 20, allora, non è una funzione della richiesta ♫20. E' una funzione delle richieste ♫ 1 attraverso ♫ МSK4 Quindi l'obiettivo di test non è affatto una richiesta \. È il comportamento del sistema su una sequenza di queste

Questo non è possibile. `Assert.Equal`. La domanda che un test può porre non è più " richiede X risposta giudizi YM SK2 Diventerà ♫" data questa classe di comportamento ♫ , il sistema si converge alla risposta giusta ♫

Gli analogi .NET più vicini sono: [Verify](https://github.com/VerifyTests/Verify) e [FsCheck](https://github.com/fscheck/FsCheck), ma il BDF non è né uno né l'altroM SK1 Verify approva un artefatto , poi fallisce nel costruire su qualsiasi diffusivo preciso. FsCheck asserts properties over randomly generated inputsMNK4 Il BDF si trova tra loroMMK5 l'artefatto approvato è una definizione comportamentaleMBK6 ma la condizione di passaggio è probabilisticaMGK7 Non " ha corrisponso il risultato a questo scatto riproduttivo??" ma SNK10 questa distribuzione si converge al lato attenduto della confine?MNK11 con i segnali giusti presenti.?"

## Una definizione comportamentale, tre sistemi di verifica.

Il trucco che rende questo file BDF risolvibile: non è un caso di prova. È una *Contratto comportamentale eseguibile*, una definizione di come si comporta una classe di cliente . Scritto una volta, che un file è consumato in tre modiM SK3

1. **Riprodotto** attraverso il righello di integrazione, una richiesta alla volta contro l'orchestratore reale, per catturare il segnaleM SK2 rigressioni di flusso che la suite dell'unità non riesce a vedere
2. **Re-sampled** sotto [k6](https://k6.io/), molti utenti virtuali contemporaneamente disegnano un nuovo tempo dalla stessa definizioneM SK1 per generare una carica realistica.
3. **Controllato** contro i segnali una corsa vera misurata in realtà, per catturare il drift di calibrazione.

```mermaid
flowchart TD
    classDef def fill:none,stroke:#3b82f6,stroke-width:2px
    classDef rig fill:none,stroke:#a855f7,stroke-width:2px
    classDef out fill:none,stroke:#22c55e,stroke-width:2px

    BDF["BDF behavioural definition<br/>clientProfile · timingProfile<br/>requests · evidence · labels"]:::def

    Replay["Integration replay<br/>slim form · real orchestrator<br/>cache disabled · identity reset"]:::rig
    K6["k6 load harness<br/>full form · re-sampled per VU<br/>burst + jitter"]:::rig
    Calibration["Calibration audit<br/>claimed evidence<br/>vs measured signals"]:::rig

    Signals["Signal-flow regressions caught<br/>merged ev.Signals reaches<br/>dashboard · persistence · threat report"]:::out
    Metrics["Load envelope verified<br/>latency · detection_rate<br/>burst_detected"]:::out
    Drift["Calibration drift surfaced<br/>stale claims · aged signatures<br/>moved detection surface"]:::out

    BDF --> Replay --> Signals
    BDF --> K6 --> Metrics
    BDF --> Calibration --> Drift
```

Rigressione, carico, e calibrazione sono di solito tre sistemi di test con tre fonti di verità che si allontananoM SK2 Qui ci sono tre letturas di un file . Il resto del post è ciascuna lettura a turnoMSC4

## Perché gli esami delle unità perdono la classe di fallimento?

La prima versione di questo lavoro aveva centinaia di test per ogni unità di rilevatore con contesti modellati e titoli in canne.. They were fast, deterministic, and blind to the failure class I cared about

L'orchestratore fonde i contributi in un singolo. `ev.Signals` dizionario che i consumatori sottocorrenti (dashboard, persistenzaM SK2 narrative builder , threat reportMST4 reading fromMSC5 Un refactore che scende `primary_signature` dalla superficie fusionata non va male per-test dell'unità di rilevatore:il rilevatore continuava a funzionare,la contribuzione continuava ad trasportare il segnale,ha semplicemente smesso di raggiungere chiunque ne avesse bisogno.Il pannello dashboard'l'impronta digitale del tavolo è vuoto.La persistenza salta fuori dalla sequenza.L'unidad suite rimane verde perché nessuno si avvicina alla fusione.

Questo non è un'ipotesi.. [Signature-documento di contratti](https://github.com/scottgal/stylobot/blob/main/docs/architecture/signal-contracts.md) registra esattamente questa regressione: un cambiamento che ha fermato la fusione dei segnali " ha sopravvissuto per sei giorni in produzione nonostante 1957 sia passato gli esperimenti di unitàM SK3 perché `evidence.Signals`."

Il test di integrazione a [`BdfReplayTests.Integration.cs`](https://github.com/scottgal/stylobot/blob/main/src/Mostlylucid.BotDetection.Orchestration.Tests/Integration/BdfReplayTests.Integration.cs) è diretto sul perché esiste:

> Questo rigo esiste perché la classe di fallimento che cattura ( i consumatori in aval `ev.Signals` si degrada silenziosamente quando l'orchestratore smette di fondere i segnali) non fa fallire nessun test di unità.

L'orchestratore non è una funzione; è un tubo di tubature il cui valore è quello che viene fuori dalla fusione dopo che ogni contributo ha fatto funzionare. L'unico modo per affermarlo è fare una richiesta reale attraverso un vero orchestratore e sondare la superficie fusionataM SK2 La mozione della fusione sconfigge l'esperimento

## BDF: una definizione comportamentale, non un test scritto

Un file BDF (Behavioural Definition Format) descrive come un *Classe* Le parti interessanti del schema sono statistiche: un profilo cliente che cattura l'identità di distribuzione, un profil temporale che definisce una scoppiatura - conM SK5 la regola di campionamento di vibrazioni piuttosto che dei ritardo fissoMSC6 una serie di prove di predicati ponderati sui segnali comportamentaliMST7 e una fiducia precedenteMst8 Qui c'è una vera firma MSt9[`bot-signatures/python-requests-bdf.json`](https://github.com/scottgal/stylobot/blob/main/bot-signatures/python-requests-bdf.json)):

```json
{
  "scenarioName": "python-requests-bdf",
  "scenario": "A bot/scraper using python-requests/2.31.0 with specific behavior patterns.",
  "confidence": 0.85,

  "clientProfile": {
    "userAgent": "python-requests/2.31.0",
    "cookieMode": "none",
    "headerCompleteness": "minimal",
    "clientHintsPresent": false,
    "robotsConsulted": false
  },

  "timingProfile": {
    "burstRequests": 10,
    "delayAfterMs":      { "min":   20, "max":   150 },
    "pauseAfterBurstMs": { "min":  500, "max":  2000 }
  },

  "requests": [
    { "method": "GET",  "path": "/",                "expectedStatusAny": [200,301,302], "expectedOutcome": "indexing", "successCondition": "any 2xx" },
    { "method": "HEAD", "path": "/admin",           "expectedStatusAny": [200,403],     "expectedOutcome": "indexing", "successCondition": "any 2xx" },
    { "method": "GET",  "path": "/api/data?page=1", "expectedStatusAny": [200,403],     "expectedOutcome": "indexing", "successCondition": "any 2xx" },
    { "method": "GET",  "path": "/api/data?page=2", "expectedStatusAny": [200,403],     "expectedOutcome": "indexing", "successCondition": "any 2xx" },
    { "method": "GET",  "path": "/api/data?page=3", "expectedStatusAny": [403,404],     "expectedOutcome": "indexing", "successCondition": "any 4xx" }
  ],

  "labels": ["Scraper", "RobotsIgnore"],

  "evidence": [
    { "signal": "interval_ms_p95", "op": "<", "value": 200,           "weight": 0.35 },
    { "signal": "requestInterval", "op": "<", "value": "burst <150ms", "weight": 0.70 }
  ],

  "patterns":  { "requestInterval": "burst <150ms" },
  "reasoning": "The bot/scraper uses python-requests/2.31.0 to access various endpoints, including the root path and admin pages, while also enumerating API paths and testing different HTTP methods."
}
```

La maggior parte della superficie è statistica, e la maggior parte è ciò che rende il BDF un *Definizione* Invece di un codice di testo. (Il schema del campo completoM SK2parlo diMSC3 è [`docs/bdf-v2-schema.json`](https://github.com/scottgal/stylobot/blob/main/docs/bdf-v2-schema.json).)

**`confidence` non è un'affermazione.** 0.85 dice " questo dovrebbe atterrare in alto- il robot di fiducia quando il sistema è sanoM SK3 Il rig non controlla che il punteggio maturo sia uguale a 0.85; controlla come il verdetto atterri sulla parte del bot della confineMSC5 Il precedente è la fascia la firmaMST6 l'autore SST7LLM o l'uomoM ST8 pensa che il sistema dovrebbe raggiungereMSS9 La deriva qui è una storia di calificazioneMSST10 non un'unitàMSR11un fallimento di provaMRS12

**`clientProfile` è una categoria di cliente.** `cookieMode: none` E' una cosa importante. *categorie* di comportamento (no cookie jarM SK1 ogni richiesta inizia fresca), non un'etichetta specifica . `headerCompleteness: minimal` dice " una richiesta di questo cliente porta solo quello che le biblioteche della classe - hanno stabilitoM SK2 un fatto sulla popolazione delle richieste che questo cliente emette, non una lista fissa di titoliMSC4 Il convertitore k 6 li materializza in pacchetti di titoli al tempo di esecuzioneMNK6 il BDF conserva *un po' diverso.* di cliente.

**`timingProfile` è una regola di campionamento.** `burstRequests: 10` più. `delayAfterMs: {min: 20, max: 150}` più. `pauseAfterBurstMs: {min: 500, max: 2000}` definisce un generatore: dieci richiedi con un'uniformitàM SK1 spazi casuali 20 a 150msMSC4 poi un'unità\- pausa casuale da 500 ad |2000ms~, ripetereMNK9 Lo stesso BDF ripetuto due volte produce due diversi flussi di richiede con la stessa distribuzione statisticaMST10 Questo è il comportamento reale StyloBotMSSK11 il rilevatore di periodicità e la sessione\MST12 il compactore del vettore stanno cercando di riconoscereMSS13 un vettore di ritardo fisso avrebbe testato una distribuzione diversa interamente\MSS14

**`evidence` è un predicato ponderato sui segnali.** Ogni annota è una richiesta del modulo. `signal OP value, weight w`. `{signal: "interval_ms_p95", op: "<", value: 200, weight: 0.35}` dice "l'intervallo di richiesta per questo scenario dovrebbe essere sotto 200msM SK4 e questo è il valore 0.35 del verdetto". Il BDF sostiene a livello statisticoMSC7 non M"request P7 returned botMSL10trueMLS11 ma MSSL12la popolazione che questo cliente genera dovrebbe produrre una distribuzione di intervalli la cui pMSSL13 scende sotto \200ms\MSL15 Un scenario la cui prova afferma di essere diverso da quello che misura il sistema in funzione è una firma che si è buttata fuori dalla calibrazioneMSS16

**`labels` sono taxonomia.** `[Scraper, RobotsIgnore]` è la classe per cui il scenario è stato generato per . Selezione del scenario di drive delle etichette nell'attrezzatura di carico (run solo `Scraper` scenario, escludere `RobotsIgnore`) senza che nessuno scrivesse un regex sui nomi del scenario.

**`requests` descrivere cosa fa il cliente, non quello che dovrebbe succedere dopo.** `expectedStatusAny: [200, 403]` tollera una fetta successiva o un blocco ravvicinato, perché entrambi sono produzioni valide di una sonda del percorso ostile. `expectedOutcome: indexing` E' l'elemento più importante. *l'intenzione del cliente'* (enumerare le pagine APIM SK1 non il server' la rispostaMSC3 `successCondition: "any 4xx"` su `/api/data?page=3` è il cliente' l'euristica per " ha fatto questo lavoroM SK2 un scraper che riceve 4xx sulla terza pagina ha successo nel suo lavoro di recensionamento ♫( ha scoperto la scoglieraMSC5 Il BDF cattura l'assimmetria tra quello che il cliente sta cercando di fare e ciò che il sistema dovrebbe fare a riguardoMST6

### Un BDF e un centroide: la stessa ideaM SK1 invertito.

La parola. *Definizione* e collega il BDF al concetto al centro del rilevatore del StyloBot. Il motore classifica il traffico contro i comportamenti. **Centroidi**: punti di riferimento nello spazio dimensionale 130+ da [Comportamento, Non identità](/blog/stylobot-fingerprint), ciascuna è un'ancora imparata per una classe di clienti che si muovono allo stesso modo.

Un BDF descrive la stessa cosa ( una classe di cliente) con il ruolo invertitoM SK2 Il centroide è il **Ricognizione**: chiede " questa richiesta assomiglia a quella classe? **generatore.**: risponde "produce un flusso di richiesta da quella classeM SK2 Lo stesso comportamento, direzione oppostaMSC4 che è esattamente quello che rende un BDF riproducibile e un centroide nonMNK5

Il motore's [`SignatureToBdfMapper`](https://github.com/scottgal/stylobot/blob/main/src/Mostlylucid.BotDetection/Behavioral/SignatureToBdfMapper.cs) bridges the two: prende una firma comportamentale catturata dal traffico reale e la scrive fuori come un BDF che si può leggere e ripetere. Un LLM fa lo stesso lavoro dall'altro latoM SK2 trasformando la descrizione di un attaquanto in una nuova . Che dà un circuitoMSC4 il traffico osservato diventa una firmaMNK5 la firma diventa un BAFMRK6 e il BDF può ripeterne il comportamento che l'ha prodottoMسک7

Quindi [`bot-signatures/`](https://github.com/scottgal/stylobot/tree/main/bot-signatures) Il corpus non è solo un sistema di prova; è una biblioteca di definizioni comportamentaliM SK1 un file per classe di cliente StyloBot ragioni su. Le signature lì sono state generate da un modello (`ministral-3:3b`, per il directoryM SK1s README) dati spunti che descrivono una famiglia di clienti che colpisce un insieme di punti finali , e la stessa superficie è aperta ad un autore umanoMSC4 Scrivere un BDF per una nuova famiglia di smascheratori e avete una definizione comportamentaleMNK5 uno scenario di regressioneMZK6 e una caricaMMK7 un test inserimento in un singolo fileMRK8

Una forma di riproduzione più bassa, -, vive sotto. [`test-suites/{bots,humans,adversarial}/*.bdf.json`](https://github.com/scottgal/stylobot/tree/main/test-suites) mantenendo solo `requests[].method/path/headers/delayAfter` In più, un soft. `expectedDetection`. Quel sottoset è quello che il righello dell'integrazione mette al punto finale di riproduzione, che non ha modo di sintetizzare il comportamento di distribuzione da un singolo riproiettamento ( nessun VU simultaneoMSC3 solo loopbackM SK4 quindi TLSMNK5 le dimensioni delle impronte digitali TCP si degradano con la costruzioneMRK6 La forma più ricca guida l'attrezzatura della caricaMMK7 dove i VU simultani possono effettivamente realizzare la distribuzione del tempoMسک8

## Il punto finale della riproduzione passa attraverso l'orchestratore reale.

L'impianto di integrazione mette ogni scenario in `POST /bot-detection/bdf-replay/replay`, un punto finale che vive nel prodotto ([`BdfReplayEndpoints.cs`](https://github.com/scottgal/stylobot/blob/main/src/Mostlylucid.BotDetection/Endpoints/BdfReplayEndpoints.cs)), non nell'esperimento di provaM SK1 Il posizionamento è carico-portareMSC3 il punto finale si risolve `IDetectionOrchestrator` dal DI e va attraverso qualunque orchestratore sia attualmente registrato, sotto `DetectionPolicy.Default`. La versione precedente ha codificato un orchestratore specifico e ha mascherato le regressioni nell'alternativa.

Poiché il punto di fine vive nel prodotto, è confuso come una superficie del prodotto . `BdfReplay` è spento per default; quando è attivato, le chiamate richiedono un valido `X-BdfReplay-Api-Key` header e passare un per-IP-limito di tasso, così che il percorso di riproduzione non sia un bypass di rilevamento rimasto in giroM SK2 L'attrezzatura authentica con quella tastiera . Il punto è di esercitare la strada reale

```mermaid
flowchart LR
    classDef test fill:none,stroke:#3b82f6,stroke-width:2px
    classDef proc fill:none,stroke:#a855f7,stroke-width:2px
    classDef out fill:none,stroke:#22c55e,stroke-width:2px

    Test["BDF replay rig<br/>authenticated with API key"]:::test --> Endpoint["Product replay endpoint<br/>identity reset · cache disabled"]:::proc
    Endpoint --> Orch["Current DI-registered<br/>orchestrator"]:::proc
    Orch --> Signals["Merged ev.Signals"]:::proc
    Signals --> Assert["Contract assertions<br/>verdict · signal probes<br/>convergence bound"]:::out
```

Una politica deliberata sovraride: il cache per la signature verdict è disattivato per ripartire.

```csharp
var replayPolicy = Policies.DetectionPolicy.Default with
{
    SignatureCache = Policies.DetectionPolicy.Default.SignatureCache with { Enabled = false }
};
```

Il cache's Skip path bypasses the matcher entirely once a primary signature has a confident cached verdictM SK1 In produzione questo è esattamente quello che volete (it is the whole point of [Imparare a diventare più veloce](/blog/stylobot-release-learning)); per un rig che misura l'accuratezza di rilevamento e il flusso del segnale nasconde il comportamento di richiesta per ogni -request che il rig sta cercando di applicare a

I scenari sono anche isolati l'uno dall'altro. Ogni scenario riceve una unica IP sintetica derivata da un xxHash deterministico di nome (a `192.0.x.y` L'indirizzo del RFC 5737 TESTM SK1 NET range), quindi la reputazione subnettaMSC3 non sanguina mai tra i scenari . E il rig chiama `POST /bot-detection/bdf-replay/reset-identity` prima di ogni scenario per truncare l'impronta digitale ; senza questo , scenario N eredita gli scenari di impronte digitali 1..N-1 creati e le affermazioni di stabilità per M-scenario diventano ordinantiM SK5dipendenteMSC6

## Affermazione su una superficie non deterministica

Il rig fa tre affermazioni su ogni scenario. Ognuno è un modello per testare sistemi come questo.

**Verdetto maturo, non per-verdetto di richiestaM SK2** Esisteno scenari di bot `last.Actual.IsBot` è vero; i scenari umani sostengono che *la maggioranza.* di richieste classificate come umane. Affermare su ogni singola richiesta associasse l'esperimento alla traiettoria di deriva lontano dall'ancra dell'archetipoM SK1 rilassarsi a " fissata alla fine " testare il contratto realeMSC4

```csharp
// Some heuristics legitimately escalate on outlier rates; assert majority human, not all.
var humanCount = response.Results.Count(r => r.Actual is { IsBot: false });
var botCount = response.Results.Count - humanCount;
Assert.True(humanCount >= botCount,
    $"{response.ScenarioName}: {botCount}/{response.Results.Count} requests classified as bot, " +
    $"expected majority human. Last verdict: {last.Actual!.RiskBand} prob={last.Actual.BotProbability:F2}");
```

**Probe segnali chiamate, non conta i segnaliM SK1** Il flusso di segnali viene analizzato per chiave, non in totale. L'affermazione di conteggio è fragileM SK2 un nuovo rilevatore che emette un nuovo segnale maschera la perdita di uno esistante critico ( il conteggio rimane lo stessoMSC4 sparitoMST5 l'identità della chiave è invisibileM ST6 Una sonda per M st7 identifica il consumatore che si rompe MST8 nel messaggio di fallimentoMst9

```csharp
Assert.True(probes.TryGetValue(SignalKeys.PrimarySignature, out var hasSig) && hasSig,
    $"{scenarioName}: {SignalKeys.PrimarySignature} missing from ev.Signals — " +
    "RequestPersistenceService skips persistence, dashboard fingerprint table goes blank");
```

Il messaggio di fallimento è il test's spec. Tre mesi dopo non dovete ricordare perché il segnale ha avuto importanzaM SK2 l'affermazione vi dice

**Convergenza limitata, non uguaglianza accurataM SK1** Il conciliatore metastabile delle impronte digitali risolve un vettore rumoroso ad un'identità stabile. La composizione del vettore include le dimensioni di sessione (entropia del percorsoM SK2età della sessione ) che si sposta per richiestaMSC4 così che il corrispondenzamento dei due passeMNK5 può occasionalmente cadere fuori dalla sua banda libera e allocareMMK6 Affermare su un singolo id di impronte digitale per tutte le richieste sarebbe sbagliatoMEK7 Affirmare MNK8no holeMK9 e convergere a non più di `ceil(N/2)` impronte digitali distinte" è il contratto reale.

```csharp
var distinctFps = withFingerprints
    .Select(r => r.Actual!.IdentityFingerprintId!)
    .Distinct(StringComparer.OrdinalIgnoreCase)
    .Count();
var allowed = Math.Max(1, (int)Math.Ceiling(response.Results.Count / 2.0));
Assert.True(distinctFps <= allowed,
    $"{scenarioName}: {distinctFps} distinct fingerprints across {response.Results.Count} requests " +
    $"(allowed {allowed}). The matcher isn't converging — every request is allocating new, suggesting " +
    "vector composition is unstable or LooseThreshold is unreachable.");
```

`ceil(N/2)` non è magico. codifica una politica: la prima richiesta assegna sempre l'allocazioneM SK2 le richieste successive dovrebbero più o meno corrispondere a L 1 confermare o Pass MSC4 L'allocuzione occasionale sotto alta varianza di percorso è accettabile *ogni anno.* la richiesta è una regressione. Il confine è abbastanza libero da assorbire il rumore il conciliatore è progettato per assorbirlo, abbastanza stretto da catturare il modo di fallimento dove smette di convergere a tutti

Tutti i tre modelli condividono una proprietà: affermano sul contratto che il comportamento dovrebbe essere soddisfatto , non sui numeri specifici che la attuale implementazione produceM SK2 Quando la implementzione cambia, l'esperimento è ancora valido se il contratto lo è ancoraMSC4 Questo è ciò che rende stabile un test non deterministicoMST5Mst6

## Spegnere: lo stesso corpus sotto pressione

Un file BDF è solo JSON. Il righello di integrazione consuma la forma flessibileM SK1 La presa di carico consuma la completa forma statistica: [`scripts/convert-bdf-to-k6-v2.csx`](https://github.com/scottgal/stylobot/blob/main/scripts/convert-bdf-to-k6-v2.csx) legge un directorio di firma e emette uno script k6 che *Re-esempi* Ogni firma's distribuzione per VU per iterazione.

Re-esempi sta facendo un lavoro reale in quella frase . Il script kM SK2 non ripete una traccia catturata; sta realizzando `clientProfile` e `timingProfile` come generatore vivente. Ogni iterazione VU sceglie una firma, costruisce i titoli dalla propria. `headerCompleteness` e `clientHintsPresent` flags, attacca un barattolo di biscotti che corrisponde al suo `cookieMode`, prende `robots.txt` Se `robotsConsulted` è vero, poi disegna nuovi per-delitti di richiesta da `delayAfterMs.min..max` e una nuova pausa inter-- `pauseAfterBurstMs.min..max`. Due VU con la stessa firma emetteno due diverse correnti di richiesta con la misma distribuzione statistica, che è esattamente quello che il tubo di rilevamento dovrebbe riconoscere come uno. *un po' diverso.* di cliente.

```javascript
// Main test function - each VU picks random scenario and replays with burst/jitter.
// Multiple VUs running concurrently provide natural request interleaving.
export default function() {
    const sig = signatures[Math.floor(Math.random() * signatures.length)];
    // ... robots.txt, cookie jar, header bundle built from sig.clientProfile ...

    for (let i = 0; i < sig.requests.length; i++) {
        const req = sig.requests[i];
        const url = `${TARGET_URL}${req.path}`;
        const headers = buildHeaders(req.headers || {}, sig.clientProfile);
        const res = http.request(req.method, url, null, params);

        if (sig.timingProfile) {
            if (requestCount < sig.timingProfile.burstRequests) {
                sleep(randomBetween(
                    sig.timingProfile.delayAfterMs.min / 1000,
                    sig.timingProfile.delayAfterMs.max / 1000));
            } else {
                sleep(randomBetween(
                    sig.timingProfile.pauseAfterBurstMs.min / 1000,
                    sig.timingProfile.pauseAfterBurstMs.max / 1000));
                requestCount = 0;
                burstRate.add(1);
            }
        }
    }
}
```

La proprietà del titolo: **Il corpus che testate per la correttezza è il corpus che stressate per il risultato.**. Non ci sono passazioni per i test di integrazione, ma il traffico di produzione non assomiglia ai test di integrazione. *Si tratta di un sistema multifunzionale.* il generatore di traffico. Quando un cliente segnala una famiglia di bot mancante, aggiungete un BDF e si unisce sia alla suite di regressione che al test di caricoM SK2 Nessuna traduzione , nessuna seconda fonte di veritàMST4 nessun driftMSS5

Le metriche k6 parlano la stessa lingua che la superficie del BDF.

| kM SK1 metrologico | Cosa misura |
|---|---|
| `bot_scenarios` / `human_scenarios` | Contatori per classe di scenario |
| `detection_rate` | Frazione di scenari della classe botM SK1 segnati al bordo |
| `interval_ms` | InterM SK1trend di gap request; controlla il profilo temporale mantenuto sotto carico |
| `sensitive_path_rate` | Frazione di richieste colpite `/admin`, `/api`, file di punti |
| `burst_detected` | I colpi di confine di scoppio derivati dal profilo del tempo |
| `http_req_duration` | Istogramma standard della latenza per i soglia di p95/pM SK2 |

I limiti su questi diventano uno specifico esecuzionebile per l'enveloppe di carico:

```javascript
thresholds: {
    http_req_duration: ['p(95)<1000'],
    http_req_failed: ['rate<0.1'],
    'detection_rate': ['rate>0.3'],
},
```

Un refactore che regressisce l'accuratezza della rilevazione sotto carico (la guardia del cache del verdicto chiede di sfuggire alle visite. `detection_rate`. Un refactore che introduce un percorso lento durante i viaggi di contenzione. `http_req_duration p95`. I dati della stessa fonteM SK1 sono state catture due regressioni.

## Calibrazione: la terza applicazione dello stesso file

A questo punto il BDF ha già fatto due compiti: ha controllato l'orchestratore' il contratto di segnale sotto riproduzioneM SK2 e generato una carica realistica sotto kMSC3 Il terzo uso è quello che trovo più interessante , perché qui il BAF viene controllato contro *da sé stesso.*.

Ogni elemento di prova è una richiesta del modulo. `signal OP value, weight w`. Una volta riproiettata una firma ( sotto loopback o kM SK2 il sistema ha prodotto una distribuzione misurata per gli stessi segnali `interval_ms_p95 < 200` La domanda è controllabile contro il p95. misurato. `cookie_count >= 2` è controllabile contro il conteggio dei cookie della richiesta realmente portata. `header_count >= 8` è controllabile contro i titoli che sono stati atterrati. M SK1I segnali che possono apparire in `evidence` sono enumerati nel [BDF v2 schema](https://github.com/scottgal/stylobot/blob/main/docs/bdf-v2-schema.json): `interval_ms_p95`, `interval_ms_p50`, `sensitive_path_rate`, `error_rate`, `burst_detected`, `header_count`, `cookie_count`.)

Quando le misurazioni divergeno da quelle sostenute, la firma è fuori calibrazione. O è stata sovra-specificata per il sistema contro cui è stata approvataM SK2 o il sistema si è spostato sotto di esso . Entrambi sono utiliMSC4 la prima dice di riproducere la firma dall'osservazioneMNK6 la seconda dice che un refactore ha spostato la superficie della detezione in modo che nessun test funzionale avrebbe catturato.

L'informazione figura nella parte dispositiva. `python-requests-bdf.json` mostrato prima è un piccolo esempio dal vivo del perché la revisione è necessaria per niente. La seconda fila della prova porta una stringa `value` (`"burst <150ms"`) dove il schema richiede un numero, e la `requestInterval` Il segnale che si chiama non è affatto nell'enum delle prove. Un modello ha scritto che la sequenza , e nessun test di unità la respingeM SK2 Solo una corsa misurata che confronta le prove sostenute contro i segnali osservati supera una affermazione che prima di tutto non era mai controllabile .

Questo trasforma il BDF da un artefatto di regressione in un artefacto di calibrazione. Le firme sotto `bot-signatures/` se LLM- è stata generata contro una versione precedente del tubo di rilevatore; le loro affermazioni di prove codificano ciò che *che* Version thought distinguished each client family. Re-la calibrazione in corso oggi vi dice quali revendicazioni sono ancora validi e quali sono ormai vecchieM SK2 nello stesso modo in cui un'ancora archetica a cui nulla si è avvicinato per settimane cessà di essere utile primaMSC3 Il corpo stesso -

## Sei regole per testare sistemi non deterministici.

I sistemi non determinanti non richiedono test non determinativi1;MSK3MSK4MSK5MSK6MSK7MSK8MSK9MSK10MSK11MSK12MSK13MSK14MSK15MSK16MSK17MSK18MSK19MSK20MSK21MSK23MSK24MSK25MSK26MSK28MSK27MSK29MSK30MSK31MSK32MSK33MSK35MSK34MSK36MSK42MSK43MSK44MSK45MSK46MSK47MSK48MSK52MSK49MSK50MSK51MSK54MSK55MSK53MSK56MSK58MSK59MSK60MSK64MSK65MSK66MSK62MSK63MSK67MSK69MSK70MSK61MSK71MSK72MSK68MSK75MSK80MSK76MSK86MSK88MSK90MSK87MSK81MSK83MSK84MSK89MSK85MSK99MSK91MSK96-MSK40MSK41MSK94MSK95MSK98MSK97MSK101MSK100MSK00MSK39MSK82MSK92MSK93MSK04MSK22MSK38MSK37MSK74MSK57MSK73MSK77MSK78MSK79MSKXMSK07MSK09MSK08MSK110MSK01MSK05MSK03MSK06MSK105MSK120-MMK10Msk10MMK11MNK11MMK12MMK60MK11Msk0MMK0Msk11MRK12Msk12MNK10MZK11M SK10MNK12MRK11MSC10MRK10MOKMSKJ11MK10MCK11MTK11MZKMSKMSK119MSK02MSK *costruita* One. I modelli che funzionano per StyloBot generalizzano

1. **Definire l'input come una distribuzione, non una traccia.** A. `timingProfile` con min/max gaps è un generatore; una traccia di richiesta catturata è una trazione da essaM SK2 Testare contro la traccia e voi testate la trazione , non la distribuzioneMSC4 La stessa logica per il profilo cliente MSSK5il modo dei cookies e l'integrità delle sequenze descrivono una popolazioneMST6non una lista fissa di sequenziMSS7
2. **Esprime il contratto come predicati ponderati.** L'informazione figura nella parte dispositiva. `evidence` l'array è la cosa più vicina che il sistema ha ad una unità-test assertionM SK1 e i predicati sono sopra i segnali di distribuzione (`interval_ms_p95 < 200`), non valori puntiniM SK1 Predicate con pesci compongono; affermazioni di uguaglianza donMSC3tMNK4
3. **Assert on the destination, not the path.** Per i sistemi il cui stato è un'impronta digitale che si sposta da un archetypo precedente, per-asserzioni di passaggio coperte con l'implementazione
4. **Probe la superficie fusa, non i componenti.** I modelli di classe fallimento che non riescono a catturare sono quelli in cui i componenti sono individualmente corretti ma la composizione lascia qualcosa fuori.
5. **Costruire la convergenza, non la riparare.** Un corrispondente che risolve l'input rumoroso ad un'identità stabile allocaterà occasionalmente. L'affermazione è M SK1 rimane sotto il confine ", scelto dalla politica, non dai numeri osservatiMSC4
6. **Condividere il formato di input attraverso il rig, caricoM SK1 e calibrazione.** Quando il scenario è un contratto comportamentale eseguibile piuttosto che uno script, lo stesso file guida una righistica di regressione, un'attrezzatura perfM SK2 e una revisione di calibrazione . Il corpus non si divide

## Dove questo si inserisce nella serie di release?

[Trovare e fissare la crescita senza limiti](/blog/stylobot-release-reliability) La memoria è ristretta; [Imparare a diventare più veloce](/blog/stylobot-release-learning) Made Repet Detection cheap. This is the third leg: verifying a system whose output won 't sit stillM SK3

Il metodo è un unico corpus. Le stesse signature BDF guidano la regressione, il caricoM SK2 e l'etalonizzazioneMSC3 così la manutenzione di test si trasforma in manutenzione di corpus . Una nuova firma è qualcosa che un LLM può aiutare a disegnare da una descrizione di un attaquanto

Quindi questa è la risposta a "come si può testare qualcosa come il StyloBot?" Non lo si congelaM SK2 si definisce una classe di traffico , si fa funzionare il sistema reale contro di esso , e si verificano tre coseMSC5 che il verdetto si converge ancoraMska6 che i segnali di cui i consumatori dipendono arrivano ancora, e che le prove che la definizione afferma ancora corrispondono a quelle misurate durante l'esecuzioneMST8 L'esame non è un pinMst9 È un circuitoMSt10

---


*L'attrezzatura di riproduzione del BDF vive a [`BdfReplayTests.Integration.cs`](https://github.com/scottgal/stylobot/blob/main/src/Mostlylucid.BotDetection.Orchestration.Tests/Integration/BdfReplayTests.Integration.cs). Scenari di riproduzione sottili sono sotto [`test-suites/{bots,humans,adversarial}/*.bdf.json`](https://github.com/scottgal/stylobot/tree/main/test-suites); le complete firme statistiche (con `clientProfile`, `timingProfile`, `evidence`) sono sotto [`bot-signatures/*.json`](https://github.com/scottgal/stylobot/tree/main/bot-signatures). Il convertitore k6 è [`scripts/convert-bdf-to-k6-v2.csx`](https://github.com/scottgal/stylobot/blob/main/scripts/convert-bdf-to-k6-v2.csx). Il punto finale di riproduzione che entrambi usano è [`BdfReplayEndpoints.cs`](https://github.com/scottgal/stylobot/blob/main/src/Mostlylucid.BotDetection/Endpoints/BdfReplayEndpoints.cs). Il contratto di segnale che proteggono questi test è documentato in [`docs/architecture/signal-contracts.md`](https://github.com/scottgal/stylobot/blob/main/docs/architecture/signal-contracts.md). Tutte le fonti a [github.comM SK1scottgal/stylobot](https://github.com/scottgal/stylobot). Motore viventeM SK1 Armatura dashboard, e controlli commerciali a [stylobot.net](https://www.stylobot.net).*