From 04493c7490dc4d33722a185ea904bfabe0d3ef12 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:27:36 +0100 Subject: [PATCH] Make weekly Patrol summary help usable and evidence-aware Replace the old build and commercial narrative with setup, recipient safety, schedule checks and honest retained-data limits in the existing Intelligence guide. Keep the old repository guide as a pointer and verify the shipped Markdown with meaningful actual-parent controls. Contract-Neutral: Existing-surface help and rendered-documentation verification only; no runtime, route, permission or interface change. Change-source: pulse-maintainer --- docs/AI.md | 116 ++++++++++++++++++ docs/PATROL_WEEKLY_DIGEST.md | 107 +--------------- frontend-modern/public/docs/AI.md | 116 ++++++++++++++++++ .../utils/__tests__/patrolDigestHelp.test.ts | 90 ++++++++++++++ 4 files changed, 326 insertions(+), 103 deletions(-) create mode 100644 frontend-modern/src/utils/__tests__/patrolDigestHelp.test.ts diff --git a/docs/AI.md b/docs/AI.md index 5ee09e222f..bce42d5bd5 100644 --- a/docs/AI.md +++ b/docs/AI.md @@ -494,6 +494,122 @@ When control level is **Controlled**, write actions pause for approval: --- +## Weekly summary and email + +Pulse's **This week** summary adds up retained Patrol activity over the last +seven days. Open **Patrol → Activity** to read it; use **Settings → Reporting** +to schedule the same summary by email. The card, weekly email and **Report type** +selector are included in **v6.5.0**. An older build can lack these controls. + +This is a summary of recorded activity, not a complete audit or proof that every +resource is healthy. Use individual findings, run history and **Actions** for +the result of a particular investigation or fix. + +### Read the summary + +The card normally says **Last 7 days**: a rolling window, not a calendar week. +If it says **Since … (older runs are no longer kept)**, its run history covers +only part of that window. Refreshing the card reads existing records; it does +not run Patrol or recover missing history. + +| Line | What it means and what to check | +| --- | --- | +| **Patrol runs** | Retained runs, checks and failures in the window. The resource count is the largest number checked in one run, not a count of distinct resources across the week. Read run history for failed or missing checks. | +| **New issues** | New-finding totals recorded by those runs. The open severity counts concern findings detected within the window; older open findings are not included. Check the full findings list before concluding that nothing needs attention. | +| **Issues resolved** | Recorded automatic resolutions plus manual resolutions still retained. Dismissed and muted findings are described separately. A cleared finding does not establish that an action ran or was independently verified. | +| **Investigated** | Recorded investigation outcomes on retained findings. “Fix run, result unknown” is not a verified fix; open the finding for its result and remaining work. | +| **Fixes run** | Actions recorded as successfully executed. “Verified afterwards” is a separate count; review failed actions and waiting approvals in **Actions**. Do not run an action again merely to increase the count. | +| **Estimated spend** | Recorded Patrol model calls and their estimated USD cost, not an invoice or all Assistant usage. If some calls have no known price, the displayed amount is incomplete, not a zero-cost result. Check [Cost Tracking](AI.md#cost-tracking). | + +The displayed mode is Patrol's **current** mode, not a history of mode changes. +Watch-only mode can explain why investigations or fixes were not performed, +but changing mode is not a repair for an empty summary. + +### Schedule the weekly email + +Scheduling requires the **advanced reporting** entitlement and an account +allowed to manage report schedules. Reading the Patrol card does not itself +grant scheduling access. See [Plans and entitlements](PULSE_PRO.md). + +1. Check that email notifications are **enabled and configured** under + **Alerts → Notifications**. Listing recipients on a schedule does not + configure a mail provider or enable email. +2. Open **Settings → Reporting**, choose **Create schedule**, then set + **Report type** to **Patrol weekly summary**. +3. Enter a schedule name, weekday, time and an IANA **Timezone**, such as + `Europe/London`. The summary is weekly, covers the last seven days, and is + sent as HTML and plain text email. It is not a PDF/CSV attachment, a + resource/tag-filtered report, or a saved disk copy. +4. Set **Email recipients** deliberately. Blank uses the existing email + notification recipients; if none are configured, delivery can fall back + to the sender address. Use explicit authorised recipients to avoid that + ambiguity. The email can contain resource names, findings and costs. +5. Save with **Enabled** selected for automatic runs. **Run now** sends a real + email; it is not a preview or a test of future scheduled delivery. Use it + only when those recipients should receive the summary. + +The Report type choice appears when the session reports AI as enabled. An +existing summary keeps its type when edited after AI is switched off. If the +choice is missing, check the installed version, current workspace, AI settings +and reporting access; do not change Patrol's autonomy mode, re-enrol agents or +create an API token just to reveal it. + +### Check a missing email or empty summary + +- **No recorded runs:** check Patrol's enabled state, configured provider, + ordinary run history and errors. “Patrol has not run” can also reflect an + unavailable Patrol service, not proof that the environment had no problems. + Do not force a run, restart Pulse or change retention just to fill the card. +- **Failed schedule:** read **Last run** and the saved error in Reporting. On + a narrow screen, use the same signed-in page at a wider viewport to see + that column. A missing Patrol service and missing email configuration are + different failures. Preserve the original error; do not repeatedly use + Run now or send synthetic alerts to diagnose them. +- **Run marked OK but no email received:** that status means Pulse's sender + returned success, not that the message reached the recipient's inbox. + Check the selected recipients and the mail provider's delivery or rejection + record, then the mailbox's filtering. A successful alert Test does not prove + that this schedule ran or used the intended recipients. +- **Email links missing:** links depend on the configured Pulse public URL. + Open Patrol in your existing authenticated browser; do not make Pulse + publicly accessible or disable TLS to make an email link work. + +Keep full mail errors, recipient addresses, credentials and exports private. +Share only a locally reviewed, redacted error and the relevant schedule time +and outcome; see [Getting Help](TROUBLESHOOTING.md#-getting-help). + +### Honest limits of the data + +- Run history retains at most **100 runs**. Event-triggered activity can + exhaust it before seven days. The partial-window notice concerns run + retention only; its absence does not certify all other lines as complete. +- Resolved findings can be purged after **24 hours**, and dismissed findings + after **30 days**. Manual resolutions, dismissals and investigation outcomes + on purged findings can disappear from these totals. Automatic resolution + totals come from retained runs, so the lines need not reconcile exactly. +- Actions have their own retained audit records, but the digest reads a bounded + set and can show an empty action line when that source is unavailable. + Check **Actions** itself; zero in this summary is not proof that no action + took place. Pending approvals can include actions created before the window. +- Model spend depends on recorded usage and known prices. This summary cannot + establish that caching, a budget limit or a provider's bill is correct. + +### API + +For an existing authenticated integration, `GET /api/ai/patrol/digest` requires +`ai:execute` scope. Optional `days` is an integer from **1 to 30**, default +**7**; invalid values return 400. Scheduled email always uses seven days. +The response groups `window`, `mode`, `runs`, `findings`, `investigations`, +`actions`, `alerts` and `spend`. `window.history_complete` and +`window.history_since` describe the run-retention limitation above, not a +whole-report completeness guarantee. + +A zero response can be returned when the Patrol service is unavailable. +Authentication, scope or transport errors are not a zero result. Prefer the +signed-in card for a one-off check; never extract a session cookie or put an +API token in a command, URL or public report. See [API authentication](API.md#-authentication) +for the private header-file approach when an integration needs it. + ## Configuration Configure providers in the UI: **Settings → Pulse Intelligence → Provider & Models** diff --git a/docs/PATROL_WEEKLY_DIGEST.md b/docs/PATROL_WEEKLY_DIGEST.md index bcca2943e4..939236cea6 100644 --- a/docs/PATROL_WEEKLY_DIGEST.md +++ b/docs/PATROL_WEEKLY_DIGEST.md @@ -1,105 +1,6 @@ # Patrol Weekly Digest -Status: building — endpoint and in-app "This week" card on `main` (PRs #1856 -and #1860, 2026-09-02); the weekly email is the `patrol_digest` report -schedule kind, and Settings > Reporting creates it through the "Report type" -choice on the schedule form (2026-10-02). Demand ledger: -`pulse-pro/FEATURE_REQUESTS.md`, "Patrol weekly digest (what Patrol did for -you)", a named bet under the Patrol operations loop. - -## The job, in the customer's words - -"Show me what I am paying for." A Pro customer turns Patrol on, it runs about -five times a day, and nothing in Pulse ever adds that up. They see individual -findings when they open the page and individual emails when a finding fires, -but never the week: how often Patrol looked, what it caught, what it fixed, -what it cost. The 2026-09-01 assessment found 33 of 175 Pro subscriptions past -due and 80 percent of paying installs never seeing the paid loop fire. A -customer who cannot see the work stops paying for it. - -The least-expert plausible reader is a homelab operator who set Patrol up once -and opens Pulse when an alert email arrives. They do not know what an -"investigation", a "verdict", or an "evidence class" is. The digest must read -as a plain weekly account, and every number must lead to something they can -click or a decision they can make. - -## What the digest reports - -Everything below is computed from data Pulse already records. No new -telemetry fields; the readout task grades this bet from support and issue -mentions. - -| Line | Source | What the reader does with it | -| --- | --- | --- | -| Runs: how many times Patrol checked, how many checks in total, how many resources it covered, when it last ran | `PatrolRunRecord` history (`internal/ai/patrol.go`, capped at 100 runs) | Confirms Patrol is alive. If runs are zero or failed, the card says so and points at Patrol setup. | -| Mode: monitor, approval, assisted, or full | `Service.GetEffectivePatrolAutonomyLevel` | Explains why actions were or were not taken. Monitor mode with fixable findings is the upgrade prompt, stated plainly. | -| New issues this week, and how many are still open by severity | run `new_findings` totals plus the findings store for still-tracked findings detected in the window | Open critical or warning issues are the thing to look at; the count links to the findings list. | -| Issues resolved and how many Patrol resolved on its own | run `resolved_findings` totals plus manual resolutions still in the findings store | This is the "it did something" line. | -| Issues you dismissed or muted | finding lifecycle events (`dismissed`, `suppressed`) in the window | Reminds the reader what they told Patrol to ignore. | -| Investigations and their outcomes | finding lifecycle `investigation_outcome` events in the window | Shows the paid loop firing; outcomes that need attention are named. | -| Actions proposed, approved, executed, verified, failed, and still waiting | canonical action audit records with `origin.surface = patrol` (`internal/unifiedresources`) | "Waiting" links straight to Actions and approvals. | -| Alerts Patrol looked into | alert-triggered runs (`alert_fired`, `alert_flapping`) and their distinct alert identifiers | Shows Patrol responding to the reader's own alerts, not just its schedule. | -| Estimated model spend, tokens, and calls | `internal/ai/cost` usage events with `use_case = patrol` | Answers "what does this cost me" without opening the AI cost dashboard. Marked as an estimate; unknown pricing is stated, never zeroed. | - -Deliberately left out: per-run tool call traces, evidence classes, verdict -strings, model names, and anything the reader cannot act on. Those stay in run -history and the Actions audit, one click away. - -## Honest limits of the data - -- Run history keeps the last 100 runs. At five or six runs a day a week fits, - but a busy install with event-triggered runs can overflow. The payload - carries `window.history_complete` and `window.history_since`; the card says - "since " instead of "this week" when the window is cut short. -- Resolved findings are purged from the findings store 24 hours after - resolution, and dismissed findings after 30 days. Resolution totals therefore - come from run records, and severity is only reported for findings still - tracked. Investigation outcomes are read from lifecycle events on tracked - findings, so a finding that was investigated, fixed, and purged more than a - day ago no longer contributes. Executed and verified actions do not have this - gap: action audit records are durable. -- Spend is `cost.EstimateUSD` over recorded usage events. When the model has - no known price the digest says pricing is unknown rather than reporting a - smaller number. - -## Surface - -**In-app card first.** A "This week" card at the top of the Patrol page's -Activity tab, above Verified outcomes -(`frontend-modern/src/features/patrol/PatrolWeeklyDigestCard.tsx`, backed by -`GET /api/ai/patrol/digest?days=7`). It costs nothing to deliver, every paying -install can see it, and the Activity tab is already where "what happened" -questions are answered. The Inbox stays a decision surface and does not gain a -summary card. - -**Weekly email second.** The past-due population is the population that has -stopped opening Pulse, so the email is the slice that reaches them. It is a -report schedule kind, `patrol_digest`, on the report schedule API -(`POST /api/admin/reports/schedules` with `"kind": "patrol_digest"`): the -existing scheduler, cadence, recipients, and Pro reporting entitlement are -reused, and the run renders the same digest as plain-language HTML and text -(`internal/api/patrol_digest_email.go`) through the tenant email config via -`SendEmailWithRetry`. Digest schedules are weekly and email-only; nothing is -written to disk. A run without an email destination fails with a message that -names the missing setting instead of silently doing nothing. Webhook and -Apprise channels are out of scope. - -In Settings > Reporting, "Create schedule" offers a "Report type" choice -between the performance report and the Patrol weekly summary. The choice only -appears while the session reports that Patrol can run (AI on with a provider -configured, the same capability that shows the Patrol navigation), and an -existing summary schedule keeps showing its type so it can be edited after AI -is switched off. Choosing the summary reduces the form to a name, weekday, -time, timezone, and optional recipients: cadence is pinned to weekly, delivery -to email, and the format, resources, tag filter, retention, attachment, and -save-copy controls are hidden because the server fixes those values. The -schedules table describes a summary's scope as "Patrol activity, last 7 days". - -## API - -`GET /api/ai/patrol/digest` — requires `ai:execute` scope like the other -Patrol read endpoints. Query `days` (1 to 30, default 7). Returns the window, -current mode, and the run, finding, investigation, action, alert, and spend -rollups described above. Empty history returns zero counts with -`window.history_complete = true`; a missing AI or Patrol service returns the -same zero shape so the card can render its "Patrol has not run" state. +For setup, interpretation, email checks and retained-data limits, see +[Weekly summary and email](AI.md#weekly-summary-and-email) in the Pulse +Intelligence guide. The summary uses existing Patrol activity; it is not a +complete audit or proof that every resource is healthy. diff --git a/frontend-modern/public/docs/AI.md b/frontend-modern/public/docs/AI.md index 5ee09e222f..bce42d5bd5 100644 --- a/frontend-modern/public/docs/AI.md +++ b/frontend-modern/public/docs/AI.md @@ -494,6 +494,122 @@ When control level is **Controlled**, write actions pause for approval: --- +## Weekly summary and email + +Pulse's **This week** summary adds up retained Patrol activity over the last +seven days. Open **Patrol → Activity** to read it; use **Settings → Reporting** +to schedule the same summary by email. The card, weekly email and **Report type** +selector are included in **v6.5.0**. An older build can lack these controls. + +This is a summary of recorded activity, not a complete audit or proof that every +resource is healthy. Use individual findings, run history and **Actions** for +the result of a particular investigation or fix. + +### Read the summary + +The card normally says **Last 7 days**: a rolling window, not a calendar week. +If it says **Since … (older runs are no longer kept)**, its run history covers +only part of that window. Refreshing the card reads existing records; it does +not run Patrol or recover missing history. + +| Line | What it means and what to check | +| --- | --- | +| **Patrol runs** | Retained runs, checks and failures in the window. The resource count is the largest number checked in one run, not a count of distinct resources across the week. Read run history for failed or missing checks. | +| **New issues** | New-finding totals recorded by those runs. The open severity counts concern findings detected within the window; older open findings are not included. Check the full findings list before concluding that nothing needs attention. | +| **Issues resolved** | Recorded automatic resolutions plus manual resolutions still retained. Dismissed and muted findings are described separately. A cleared finding does not establish that an action ran or was independently verified. | +| **Investigated** | Recorded investigation outcomes on retained findings. “Fix run, result unknown” is not a verified fix; open the finding for its result and remaining work. | +| **Fixes run** | Actions recorded as successfully executed. “Verified afterwards” is a separate count; review failed actions and waiting approvals in **Actions**. Do not run an action again merely to increase the count. | +| **Estimated spend** | Recorded Patrol model calls and their estimated USD cost, not an invoice or all Assistant usage. If some calls have no known price, the displayed amount is incomplete, not a zero-cost result. Check [Cost Tracking](AI.md#cost-tracking). | + +The displayed mode is Patrol's **current** mode, not a history of mode changes. +Watch-only mode can explain why investigations or fixes were not performed, +but changing mode is not a repair for an empty summary. + +### Schedule the weekly email + +Scheduling requires the **advanced reporting** entitlement and an account +allowed to manage report schedules. Reading the Patrol card does not itself +grant scheduling access. See [Plans and entitlements](PULSE_PRO.md). + +1. Check that email notifications are **enabled and configured** under + **Alerts → Notifications**. Listing recipients on a schedule does not + configure a mail provider or enable email. +2. Open **Settings → Reporting**, choose **Create schedule**, then set + **Report type** to **Patrol weekly summary**. +3. Enter a schedule name, weekday, time and an IANA **Timezone**, such as + `Europe/London`. The summary is weekly, covers the last seven days, and is + sent as HTML and plain text email. It is not a PDF/CSV attachment, a + resource/tag-filtered report, or a saved disk copy. +4. Set **Email recipients** deliberately. Blank uses the existing email + notification recipients; if none are configured, delivery can fall back + to the sender address. Use explicit authorised recipients to avoid that + ambiguity. The email can contain resource names, findings and costs. +5. Save with **Enabled** selected for automatic runs. **Run now** sends a real + email; it is not a preview or a test of future scheduled delivery. Use it + only when those recipients should receive the summary. + +The Report type choice appears when the session reports AI as enabled. An +existing summary keeps its type when edited after AI is switched off. If the +choice is missing, check the installed version, current workspace, AI settings +and reporting access; do not change Patrol's autonomy mode, re-enrol agents or +create an API token just to reveal it. + +### Check a missing email or empty summary + +- **No recorded runs:** check Patrol's enabled state, configured provider, + ordinary run history and errors. “Patrol has not run” can also reflect an + unavailable Patrol service, not proof that the environment had no problems. + Do not force a run, restart Pulse or change retention just to fill the card. +- **Failed schedule:** read **Last run** and the saved error in Reporting. On + a narrow screen, use the same signed-in page at a wider viewport to see + that column. A missing Patrol service and missing email configuration are + different failures. Preserve the original error; do not repeatedly use + Run now or send synthetic alerts to diagnose them. +- **Run marked OK but no email received:** that status means Pulse's sender + returned success, not that the message reached the recipient's inbox. + Check the selected recipients and the mail provider's delivery or rejection + record, then the mailbox's filtering. A successful alert Test does not prove + that this schedule ran or used the intended recipients. +- **Email links missing:** links depend on the configured Pulse public URL. + Open Patrol in your existing authenticated browser; do not make Pulse + publicly accessible or disable TLS to make an email link work. + +Keep full mail errors, recipient addresses, credentials and exports private. +Share only a locally reviewed, redacted error and the relevant schedule time +and outcome; see [Getting Help](TROUBLESHOOTING.md#-getting-help). + +### Honest limits of the data + +- Run history retains at most **100 runs**. Event-triggered activity can + exhaust it before seven days. The partial-window notice concerns run + retention only; its absence does not certify all other lines as complete. +- Resolved findings can be purged after **24 hours**, and dismissed findings + after **30 days**. Manual resolutions, dismissals and investigation outcomes + on purged findings can disappear from these totals. Automatic resolution + totals come from retained runs, so the lines need not reconcile exactly. +- Actions have their own retained audit records, but the digest reads a bounded + set and can show an empty action line when that source is unavailable. + Check **Actions** itself; zero in this summary is not proof that no action + took place. Pending approvals can include actions created before the window. +- Model spend depends on recorded usage and known prices. This summary cannot + establish that caching, a budget limit or a provider's bill is correct. + +### API + +For an existing authenticated integration, `GET /api/ai/patrol/digest` requires +`ai:execute` scope. Optional `days` is an integer from **1 to 30**, default +**7**; invalid values return 400. Scheduled email always uses seven days. +The response groups `window`, `mode`, `runs`, `findings`, `investigations`, +`actions`, `alerts` and `spend`. `window.history_complete` and +`window.history_since` describe the run-retention limitation above, not a +whole-report completeness guarantee. + +A zero response can be returned when the Patrol service is unavailable. +Authentication, scope or transport errors are not a zero result. Prefer the +signed-in card for a one-off check; never extract a session cookie or put an +API token in a command, URL or public report. See [API authentication](API.md#-authentication) +for the private header-file approach when an integration needs it. + ## Configuration Configure providers in the UI: **Settings → Pulse Intelligence → Provider & Models** diff --git a/frontend-modern/src/utils/__tests__/patrolDigestHelp.test.ts b/frontend-modern/src/utils/__tests__/patrolDigestHelp.test.ts new file mode 100644 index 0000000000..3f8901a2b8 --- /dev/null +++ b/frontend-modern/src/utils/__tests__/patrolDigestHelp.test.ts @@ -0,0 +1,90 @@ +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +import { renderDocMarkdown } from '@/features/docs/docMarkdown'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../..'); +const read = (relative: string) => readFileSync(path.join(root, relative), 'utf8'); +const markdown = read('docs/AI.md'); +const article = document.createElement('article'); +const rendered = document.createElement('article'); +rendered.innerHTML = renderDocMarkdown(markdown, 'AI'); +for ( + let element = rendered.querySelector('#weekly-summary-and-email'); + element; + element = element.nextElementSibling +) { + if (element.tagName === 'H2' && element.id !== 'weekly-summary-and-email') break; + article.append(element.cloneNode(true)); +} +const text = article.textContent!.replace(/\s+/g, ' '); + +// Exercise the actual shipped Markdown, not a second copy of the advice. +describe('Patrol weekly summary help', () => { + it('ships operator help instead of a private build or sales plan', () => { + expect(read('frontend-modern/public/docs/AI.md')).toBe(markdown); + expect(text).toContain('included in v6.5.0'); + expect(text).not.toMatch(/Status: building|past due|stops paying|FEATURE_REQUESTS/); + expect(read('docs/PATROL_WEEKLY_DIGEST.md')).toContain('(AI.md#weekly-summary-and-email)'); + expect(article.querySelector('#read-the-summary')).not.toBeNull(); + expect(article.querySelector('#check-a-missing-email-or-empty-summary')).not.toBeNull(); + }); + + it('makes enabled mail and reporting access prerequisites of the setup flow', () => { + const steps = Array.from(article.querySelectorAll('ol li'), (item) => + item.textContent!.replace(/\s+/g, ' '), + ); + expect(steps).toHaveLength(5); + expect(text).toContain('advanced reporting entitlement'); + expect(steps[0]).toContain( + 'Listing recipients on a schedule does not configure a mail provider or enable email', + ); + expect(steps[1]).toContain('Report type to Patrol weekly summary'); + expect(steps[2]).toContain('Timezone'); + expect(steps[2]).toContain('not a PDF/CSV attachment'); + }); + + it('warns about recipient fallback and real Run now delivery', () => { + expect(text).toContain('delivery can fall back to the sender address'); + expect(text).toContain('Run now sends a real email; it is not a preview'); + expect(text).toContain("Pulse's sender returned success"); + }); + + it('does not turn run or action counters into whole-estate acceptance', () => { + expect(text).toContain('largest number checked in one run, not a count of distinct resources'); + expect(text).toContain('older open findings are not included'); + expect(text).toContain('current mode, not a history of mode changes'); + expect(text).toContain('zero in this summary is not proof that no action took place'); + expect(text).toContain('Pending approvals can include actions created before the window'); + }); + + it('separates missing inputs, partial history, unknown spend and inbox receipt', () => { + expect(text).toContain('partial-window notice concerns run retention only'); + expect(text).toContain('displayed amount is incomplete, not a zero-cost result'); + expect(text).toContain('not that the message reached the recipient'); + expect(text).toContain('unavailable Patrol service, not proof'); + expect(text).toContain( + 'Manual resolutions, dismissals and investigation outcomes on purged findings', + ); + }); + + it('preserves API errors and routes private evidence to real help anchors', () => { + expect(text).toContain('ai:execute scope'); + expect(text).toContain('invalid values return 400'); + expect(text).toContain('Authentication, scope or transport errors are not a zero result'); + expect(text).toContain('never extract a session cookie'); + for (const [slug, fragment] of [ + ['AI', 'cost-tracking'], + ['API', '-authentication'], + ['TROUBLESHOOTING', '-getting-help'], + ]) { + const link = article.querySelector(`a[href="/docs/${slug}#${fragment}"]`); + expect(link?.hasAttribute('data-doc-link')).toBe(true); + const target = document.createElement('article'); + target.innerHTML = renderDocMarkdown(read(`docs/${slug}.md`), slug); + expect(target.querySelector(`[id="${fragment}"]`)).not.toBeNull(); + } + }); +});