fix(site): add reCAPTCHA v3, main landmark, and docs sitemap#469
fix(site): add reCAPTCHA v3, main landmark, and docs sitemap#469
Conversation
- Add reCAPTCHA v3 integration to contact form (hidden token before each submission) — fixes Formcarry validation error - Wrap page content in <main> landmark (PageSpeed accessibility fix) - Add docs sitemap to robots.txt for Google discovery - Block /_assets/ from indexing (hashed build artifacts)
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.OpenSSF Scorecard
Scanned Files
|
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the site's robustness and user experience by integrating reCAPTCHA v3 for improved spam protection on the contact form, boosting accessibility by adding a landmark to page content, and optimizing search engine discoverability of documentation through sitemap inclusion in robots.txt. Additionally, it prevents unnecessary indexing of build assets.Highlights
Changelog
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds Disallow and sitemap entries to robots.txt, integrates Google reCAPTCHA v3 into the contact form (client-side token fetch), exposes an enableRecaptcha prop and script injection in Base layout, moves slot into , renders Footer from Base, and removes Footer usage from two pages.Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Browser as Site (contact page)
participant Recaptcha as Google reCAPTCHA
participant Server as Site backend
User->>Browser: Click submit on contact form
Browser->>Recaptcha: grecaptcha.ready -> grecaptcha.execute(action:"contact", site_key)
Recaptcha-->>Browser: returns token (g-recaptcha-response)
Browser->>Browser: insert token into hidden input
Browser->>Server: POST form data (includes g-recaptcha-response)
Server->>Recaptcha: verify token with reCAPTCHA API
Recaptcha-->>Server: verification result
Server-->>Browser: respond with success/error
Browser-->>User: display result
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
📝 Coding Plan
Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces several useful features, including reCAPTCHA v3 integration, accessibility improvements with a <main> landmark, and SEO enhancements through robots.txt and sitemap additions. The changes are well-intentioned, but I've identified a critical issue regarding the hardcoding of the reCAPTCHA site key in two separate files. This poses a security and maintainability risk. My review comments provide specific code suggestions to refactor this using environment variables, which is the standard and recommended practice for managing such keys in Astro applications.
| try { | ||
| // Get fresh reCAPTCHA v3 token before submission | ||
| if (typeof grecaptcha !== "undefined") { | ||
| const token = await grecaptcha.execute("6LdQkossAAAAADwxZo5bt8p6az7P3M6I_8k4_ypu", { action: "contact" }); |
There was a problem hiding this comment.
This reCAPTCHA site key is hardcoded. To improve security and maintainability, it's best to avoid hardcoding keys. Instead, you should load it from an environment variable and pass it to this client-side script.
A common pattern in Astro is to:
- Read the environment variable in the component's frontmatter (e.g.,
const recaptchaSiteKey = import.meta.env.PUBLIC_RECAPTCHA_SITE_KEY;). - Pass it to the template via a data attribute on the form:
<form data-recaptcha-key={recaptchaSiteKey} ...>. - Access it in the script via
form.dataset.recaptchaKey.
Remember to also add a check to ensure form.dataset.recaptchaKey is available before using it.
const token = await grecaptcha.execute(form.dataset.recaptchaKey, { action: "contact" });
site/src/layouts/Base.astro
Outdated
| <!-- GitHub buttons (https://buttons.github.io/) --> | ||
| <script async defer src="https://buttons.github.io/buttons.js"></script> | ||
| <!-- Google reCAPTCHA v3 (contact form spam protection) --> | ||
| <script async defer src="https://www.google.com/recaptcha/api.js?render=6LdQkossAAAAADwxZo5bt8p6az7P3M6I_8k4_ypu"></script> |
There was a problem hiding this comment.
Hardcoding API keys, even public ones like a reCAPTCHA site key, is not recommended. It makes managing keys across different environments (e.g., development, production) difficult and error-prone. Please store this key in an environment variable and access it using Astro's import.meta.env feature. Remember that public client-side environment variables must be prefixed with PUBLIC_.
<script async defer src={`https://www.google.com/recaptcha/api.js?render=${import.meta.env.PUBLIC_RECAPTCHA_SITE_KEY}`}></script>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@site/src/components/ContactForm.astro`:
- Around line 111-116: The submission currently proceeds when grecaptcha is
undefined, so update the submit handler in ContactForm.astro to detect when
typeof grecaptcha === "undefined" and stop the submission flow: prevent the form
submit/return early, set a visible error message (reuse the component's existing
form error state/UI) informing the user that reCAPTCHA failed to load and they
should retry or disable blockers, and do not call
form.querySelector("#captchaResponse") or proceed to send data; ensure the check
occurs before the grecaptcha.execute call and references grecaptcha,
captchaInput and the form submit handler to locate where to add this
early-return and UI error display.
- Around line 112-113: The TypeScript compiler complains about the global
grecaptcha used in ContactForm.astro; add a proper type declaration for
grecaptcha to satisfy strict TS. Either install the `@types/grecaptcha` dev
package (npm install --save-dev `@types/grecaptcha`) or add a local declaration
file (e.g., env.d.ts) that declares the global grecaptcha with ready and execute
signatures so the call to grecaptcha.execute(...) in ContactForm.astro is typed
and the build error is resolved.
In `@site/src/layouts/Base.astro`:
- Around line 72-73: Extract the hard-coded reCAPTCHA site key into a single
shared constant (for example PUBLIC_RECAPTCHA_SITE_KEY) and reference that
constant in both Base.astro and ContactForm.astro; implement it by placing the
key in an environment variable exposed to the client (e.g., via
import.meta.env.PUBLIC_RECAPTCHA_SITE_KEY) or a shared config module, then
replace the inline literal in Base.astro's script src and the literal used in
ContactForm.astro (line ~113) with the shared variable (e.g., use
?render=${PUBLIC_RECAPTCHA_SITE_KEY}) so updates happen in one place.
- Around line 67-69: The Base.astro layout currently wraps the page slot in
<main> causing any slotted <footer> (Footer component) to be nested inside
<main>; update Base.astro so <main> contains only the primary <slot /> and
render the Footer component as a sibling after </main> in Base.astro, then
remove Footer usage from individual pages that currently slot it (so pages only
provide main content to the slot). Ensure you reference the layout's <main> and
the Footer component when making the change so the Footer is no longer nested
inside <main>.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: eb11854a-85e9-4813-bc39-8d514df72328
📒 Files selected for processing (3)
site/public/robots.txtsite/src/components/ContactForm.astrosite/src/layouts/Base.astro
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (python)
🔇 Additional comments (2)
site/public/robots.txt (1)
3-6: LGTM!The
Disallow: /_assets/directive correctly prevents indexing of Astro's hashed build artifacts, and the additional sitemap entry enables search engine discovery of the documentation pages.site/src/components/ContactForm.astro (1)
63-63: LGTM!The hidden input for the reCAPTCHA token is correctly placed with appropriate
idandnameattributes for Formcarry integration.
site/src/layouts/Base.astro
Outdated
| <!-- Google reCAPTCHA v3 (contact form spam protection) --> | ||
| <script async defer src="https://www.google.com/recaptcha/api.js?render=6LdQkossAAAAADwxZo5bt8p6az7P3M6I_8k4_ypu"></script> |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider extracting the reCAPTCHA site key to a shared constant.
The site key 6LdQkossAAAAADwxZo5bt8p6az7P3M6I_8k4_ypu is duplicated here and in ContactForm.astro (line 113). If the key ever changes, both locations need updating.
Consider defining it once (e.g., in an environment variable or a shared config) and referencing it in both places.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@site/src/layouts/Base.astro` around lines 72 - 73, Extract the hard-coded
reCAPTCHA site key into a single shared constant (for example
PUBLIC_RECAPTCHA_SITE_KEY) and reference that constant in both Base.astro and
ContactForm.astro; implement it by placing the key in an environment variable
exposed to the client (e.g., via import.meta.env.PUBLIC_RECAPTCHA_SITE_KEY) or a
shared config module, then replace the inline literal in Base.astro's script src
and the literal used in ContactForm.astro (line ~113) with the shared variable
(e.g., use ?render=${PUBLIC_RECAPTCHA_SITE_KEY}) so updates happen in one place.
- Extract reCAPTCHA site key to env var (PUBLIC_RECAPTCHA_SITE_KEY) with fallback, passed via data attribute — no more hardcoded duplication - Install @types/grecaptcha for TypeScript compilation - Block form submission when reCAPTCHA unavailable (ad-blocker, network) with user-friendly error message - Move Footer to Base layout outside <main> — fixes semantic HTML nesting - Remove Footer import/usage from individual pages (now in layout)
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@site/src/components/ContactForm.astro`:
- Around line 116-127: The current reCAPTCHA logic calls grecaptcha.execute
without waiting for grecaptcha.ready and proceeds if the `#captchaResponse` input
is missing; change it to call grecaptcha.ready() first (use
grecaptcha.ready(async () => { ... })) before calling
grecaptcha.execute(siteKey, { action: "contact" }), ensure you immediately
error/return if form.dataset.recaptchaKey is falsy or if
form.querySelector("#captchaResponse") returns null (do not continue
submission), and keep the existing errorMsg update/focus when
readiness/token/getting the captcha input fails so submission fails closed;
reference grecaptcha.ready, grecaptcha.execute, form.dataset.recaptchaKey, and
captchaInput/#captchaResponse to find the code to modify.
In `@site/src/layouts/Base.astro`:
- Around line 75-76: Add an optional prop to Base.astro's Props interface named
enableRecaptcha?: boolean, wrap the existing reCAPTCHA script element in a
conditional that only renders when enableRecaptcha is true (e.g.,
{enableRecaptcha && <script ...>}), and update index.astro to pass
enableRecaptcha={true} when rendering Base so only the page that contains
ContactForm loads the third‑party reCAPTCHA script; leave other pages unchanged
(do not pass the prop or pass false).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 40da3d9b-2387-49b6-abe7-e6d3b719f082
⛔ Files ignored due to path filters (1)
site/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (5)
site/package.jsonsite/src/components/ContactForm.astrosite/src/layouts/Base.astrosite/src/pages/get/index.astrosite/src/pages/index.astro
💤 Files with no reviewable changes (2)
- site/src/pages/get/index.astro
- site/src/pages/index.astro
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (python)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-03-15T11:48:14.867Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T11:48:14.867Z
Learning: Applies to web/** : Web dashboard: Node.js 20+, dependencies in web/package.json (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, fast-check, ESLint, vue-tsc).
Applied to files:
site/package.json
🔇 Additional comments (2)
site/package.json (1)
17-20: Looks good.No issues found in this manifest change.
site/src/layouts/Base.astro (1)
69-72: Good semantic landmark structure.
<main>now contains primary content and<Footer />is correctly outside it.
site/src/layouts/Base.astro
Outdated
| <!-- Google reCAPTCHA v3 (contact form spam protection) --> | ||
| <script async defer src={`https://www.google.com/recaptcha/api.js?render=${recaptchaSiteKey}`}></script> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify current reCAPTCHA script scope and layout usage.
rg -n --type=astro 'recaptcha/api.js|enableRecaptcha' site/src/layouts/Base.astro
rg -n --type=astro '<ContactForm' site/src/pages site/src/components
rg -n --type=astro '<Base' site/src/pagesRepository: Aureliolo/synthorg
Length of output: 151
🏁 Script executed:
#!/bin/bash
# Verify reCAPTCHA script and layout usage in Astro files
# First, check if the Base.astro file exists and view the relevant lines
echo "=== Checking Base.astro content around lines 75-76 ==="
if [ -f "site/src/layouts/Base.astro" ]; then
sed -n '70,80p' "site/src/layouts/Base.astro"
else
echo "Base.astro not found"
fi
echo ""
echo "=== Searching for recaptcha/api.js references ==="
rg 'recaptcha/api.js' site/
echo ""
echo "=== Searching for enableRecaptcha ==="
rg 'enableRecaptcha' site/
echo ""
echo "=== Searching for ContactForm component usage ==="
rg '<ContactForm' site/src/pages site/src/components 2>/dev/null || rg 'ContactForm' site/src/pages site/src/components
echo ""
echo "=== Searching for Base layout usage ==="
rg '<Base\s' site/src/pages 2>/dev/null || rg 'Base layout\|import.*Base' site/src/pagesRepository: Aureliolo/synthorg
Length of output: 1223
🏁 Script executed:
#!/bin/bash
# Check Base.astro Props interface and search all pages
echo "=== Base.astro Props interface (first 30 lines) ==="
head -30 site/src/layouts/Base.astro
echo ""
echo "=== All pages using Base layout ==="
rg -l 'Base' site/src/pages
echo ""
echo "=== All ContactForm usages ==="
rg -l 'ContactForm' site/src/pages site/src/components
echo ""
echo "=== Count of .astro files in site/src/pages ==="
find site/src/pages -name "*.astro" -type f | wc -lRepository: Aureliolo/synthorg
Length of output: 1282
Scope reCAPTCHA loading to pages that actually need it.
Currently, reCAPTCHA loads on all 4 pages using Base.astro, but ContactForm only exists on index.astro. This means 3 pages (install-sh.astro, install-ps1.astro, get/index.astro) unnecessarily load a third-party script, adding avoidable performance and privacy overhead.
Move the reCAPTCHA script to only load when needed by:
- Adding
enableRecaptcha?: booleanprop to the Props interface - Wrapping the script in a conditional:
{enableRecaptcha && <script ...>} - Setting
enableRecaptcha={true}only in index.astro
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@site/src/layouts/Base.astro` around lines 75 - 76, Add an optional prop to
Base.astro's Props interface named enableRecaptcha?: boolean, wrap the existing
reCAPTCHA script element in a conditional that only renders when enableRecaptcha
is true (e.g., {enableRecaptcha && <script ...>}), and update index.astro to
pass enableRecaptcha={true} when rendering Base so only the page that contains
ContactForm loads the third‑party reCAPTCHA script; leave other pages unchanged
(do not pass the prop or pass false).
…A loading - Wrap grecaptcha.execute in grecaptcha.ready() to ensure library is initialized before use - Fail-closed: block submission if siteKey or captchaInput is missing - Add enableRecaptcha prop to Base layout — reCAPTCHA script only loads on pages that need it (index.astro), not /get/ or view pages
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@site/src/components/ContactForm.astro`:
- Line 125: The grecaptcha.ready() await can hang indefinitely; wrap the
existing await new Promise<void>((resolve) => grecaptcha.ready(() =>
resolve())); in a timeout guard (e.g., Promise.race between the grecaptcha.ready
promise and a setTimeout that resolves/rejects after a short interval) so the
wait fails fast if reCAPTCHA never becomes ready, then handle the timeout path
in the ContactForm.astro submit flow (stop the "Sending..." state and surface an
error message). Ensure you reference the grecaptcha.ready wrapper and the submit
handler in ContactForm.astro when adding the timeout and the error handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 318db28a-1e20-4279-899c-7d61fda3399e
📒 Files selected for processing (3)
site/src/components/ContactForm.astrosite/src/layouts/Base.astrosite/src/pages/index.astro
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (python)
🔇 Additional comments (6)
site/src/layouts/Base.astro (3)
71-74: LGTM! Semantic HTML structure is now correct.The
<Footer />is properly rendered as a sibling of<main>, not nested within it. This correctly addresses the HTML landmark semantics where<footer>should be at the page level.
77-80: LGTM! Conditional reCAPTCHA loading is well-implemented.The script only loads when
enableRecaptchais true, avoiding unnecessary third-party script loading on pages that don't need it. This addresses both performance and privacy concerns.
23-23: Site key duplication still exists between Base.astro and ContactForm.astro.The
recaptchaSiteKeyconstant with the same fallback value is defined in both files. While both now use the environment variable, the hardcoded fallback is duplicated.This was already flagged in a past review. Consider extracting to a shared config module if this becomes a maintenance concern.
site/src/pages/index.astro (1)
8-8: LGTM! reCAPTCHA correctly enabled for the contact form page.Passing
enableRecaptcha={true}ensures the reCAPTCHA script loads only on this page whereContactFormis used, optimizing performance for other pages.site/src/components/ContactForm.astro (2)
116-127: LGTM! Fail-closed reCAPTCHA integration is well-implemented.The implementation correctly:
- Validates all required elements (
grecaptcha,siteKey,captchaInput) before proceeding- Uses
grecaptcha.ready()beforeexecute()per the official API contract- Blocks submission with a user-friendly error if security verification is unavailable
68-68: LGTM! Hidden input correctly configured for reCAPTCHA v3.The hidden input with
name="g-recaptcha-response"follows the standard field name that form processors like Formcarry expect for reCAPTCHA token validation.
Prevents form hanging on "Sending..." if reCAPTCHA never becomes ready (partial script load, network stall). Falls through to the existing catch block which resets the button and shows the error.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@site/src/components/ContactForm.astro`:
- Around line 125-128: The Promise.race timer created with setTimeout should be
cleared when grecaptcha.ready resolves to avoid a lingering timeout; modify the
block around grecaptcha.ready()/Promise.race to assign the timeout to a variable
(e.g., const timer = setTimeout(...)) and call clearTimeout(timer) inside the
grecaptcha.ready resolve path (the resolver passed to new Promise that calls
grecaptcha.ready), ensuring the timeout is cancelled when grecaptcha.ready wins;
keep the existing reject path for the timeout so it still rejects if
grecaptcha.ready doesn't fire.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 602d9e58-632a-413d-a7ed-88e0d80fc175
📒 Files selected for processing (1)
site/src/components/ContactForm.astro
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (python)
🔇 Additional comments (2)
site/src/components/ContactForm.astro (2)
2-3: Good reCAPTCHA key wiring to form dataset.Line 2 + Line 10 cleanly expose the site key to the submit handler without changing the component API.
Also applies to: 10-10
68-68: Correct hidden field name for Formcarry reCAPTCHA token.Line 68 uses
name="g-recaptcha-response", which is the expected payload field for reCAPTCHA verification handoff.
🤖 I have created a release *beep* *boop* --- ## [0.2.6](v0.2.5...v0.2.6) (2026-03-15) ### Features * add intra-loop stagnation detector ([#415](#415)) ([#458](#458)) ([8e9f34f](8e9f34f)) * add RFC 9457 structured error responses (Phase 1) ([#457](#457)) ([6612a99](6612a99)), closes [#419](#419) * implement AgentStateRepository for runtime state persistence ([#459](#459)) ([5009da7](5009da7)) * **site:** add SEO essentials, contact form, early-access banner ([#467](#467)) ([11b645e](11b645e)), closes [#466](#466) ### Bug Fixes * CLI improvements — config show, completion install, enhanced doctor, Sigstore verification ([#465](#465)) ([9e08cec](9e08cec)) * **site:** add reCAPTCHA v3, main landmark, and docs sitemap ([#469](#469)) ([fa6d35c](fa6d35c)) * use force-tag-creation instead of manual tag creation hack ([#462](#462)) ([2338004](2338004)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Summary
g-recaptcha-responsefield + fresh token generated before each submission. Fixes Formcarry "Google reCAPTCHA Validation Failed" error.<main>landmark: Wrap page content in<main>tag — fixes PageSpeed "Document does not have a main landmark" accessibility warning.https://synthorg.io/docs/sitemap.xmltorobots.txtso Google discovers the 35 documentation pages built by Zensical./_assets/: Prevent indexing of Astro's hashed CSS/JS build artifacts.Test plan