docs(07): research phase validation and ux polish
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,493 @@
|
||||
# Phase 7: Validation & UX Polish - Research
|
||||
|
||||
**Researched:** 2026-03-31
|
||||
**Domain:** React / react-hook-form / Zod v4 / Tailwind CSS inline validation and tooltip patterns
|
||||
**Confidence:** HIGH
|
||||
|
||||
<user_constraints>
|
||||
## User Constraints (from CONTEXT.md)
|
||||
|
||||
### Locked Decisions
|
||||
|
||||
**Validation scope:**
|
||||
- Azure account name (`account` field, azureblob): 3-24 lowercase alphanumeric characters — validate with regex `/^[a-z0-9]{3,24}$/`
|
||||
- S3 region (`region` field, s3): valid AWS region format (lowercase letters, digits, hyphens) — validate with regex `/^[a-z][a-z0-9-]+[a-z0-9]$/`
|
||||
- GCS project number (`project_number` field, gcs): digits only — validate with regex `/^\d+$/`
|
||||
- S3-compatible endpoint: non-empty only (existing required behavior) — no URL format validation
|
||||
- S3-compatible region: skip — optional field, no universal format rule
|
||||
- No other backends/fields need format validation
|
||||
|
||||
**Validation rule location:**
|
||||
- Add `validate?: { regex: RegExp; message: string }` to `FieldDef` in `src/schemas/registry.ts`
|
||||
- `buildZodSchema()` in `src/schemas/index.ts` reads `field.validate` and adds `.regex(...)` to the Zod string
|
||||
- Registry entries for the 3 validated fields get their `validate` rule inline
|
||||
|
||||
**Tooltip trigger:**
|
||||
- Info icon (ⓘ) placed next to the field label — click/tap toggles explanation
|
||||
- Inline reveal: explanation expands directly below the field input, pushing content down
|
||||
- Toggle state is local to the field component (no global state needed)
|
||||
- Tooltip stays visible until ⓘ is clicked again (not dismissed on blur)
|
||||
|
||||
**Tooltip data location:**
|
||||
- Add optional `tooltipText?: string` to `FieldDef` interface
|
||||
- `helpText` remains unchanged (brief hint, always visible below the field)
|
||||
- `tooltipText` is the longer plain-language explanation shown on ⓘ click
|
||||
- `FieldRenderer` and `PasswordField` render the ⓘ icon + inline expand when `tooltipText` is present
|
||||
|
||||
**Fields that get tooltips:**
|
||||
- `azureblob sas_url`: explain SAS URL vs access key — SAS URL bundles endpoint + time-limited token, scoped to containers; access key is the full account credential
|
||||
- `azureblob key` (access key): explain it's the full storage account key — different from SAS, full access
|
||||
- `sftp` auth method: explain password vs key-based auth — SftpAuthToggle UI needs an ⓘ on the tab labels or above the toggle
|
||||
- `onedrive token`: explain what the JSON token is and how to obtain it via `rclone authorize "onedrive"`
|
||||
|
||||
### Claude's Discretion
|
||||
- Exact Tailwind styling of the ⓘ icon and inline tooltip panel
|
||||
- Whether SftpAuthToggle gets its ⓘ on the tab label or as a standalone line above the tabs
|
||||
- ⓘ icon source (Heroicons, inline SVG, or text character)
|
||||
- Exact wording of tooltip content (stay close to existing helpText tone)
|
||||
|
||||
### Deferred Ideas (OUT OF SCOPE)
|
||||
None — discussion stayed within phase scope.
|
||||
</user_constraints>
|
||||
|
||||
<phase_requirements>
|
||||
## Phase Requirements
|
||||
|
||||
| ID | Description | Research Support |
|
||||
|----|-------------|-----------------|
|
||||
| VALID-01 | User sees inline validation error when a field value violates backend-specific format rules (Azure account name: 3–24 lowercase alphanumeric; S3 region: valid format; etc.) | Zod v4 `.regex()` chaining verified; `buildZodSchema()` extension pattern confirmed; error display already exists in FieldRenderer |
|
||||
| UX-01 | User can view a contextual tooltip on sensitive or complex fields (SAS token vs. access key, region codes, SFTP auth method, OneDrive token) | React `useState` toggle pattern sufficient; `FieldDef` extension with `tooltipText?` is the correct approach; PasswordField and FieldRenderer both need ⓘ treatment |
|
||||
</phase_requirements>
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 7 is a narrowly-scoped surgical enhancement to an already-working wizard. Two independent concerns: (1) format validation wired through the schema registry into Zod, and (2) per-field inline tooltip panels wired through the registry into the field renderer components. Both concerns touch the same three-file chain (`registry.ts` → `index.ts` → `FieldRenderer.tsx`/`PasswordField.tsx`), plus `SftpAuthToggle.tsx` for the auth-method tooltip.
|
||||
|
||||
The project uses **Zod v4.3.6** (not v3). The `.regex()` method is available on `z.string()` and chains normally — verified by direct runtime test. However, `.optional()` in Zod v4 wraps the inner type, making `.regex()` unavailable after calling `.optional()`. The correct order when a field is optional but has a regex constraint is `z.string().regex(...).optional()`, not `z.string().optional().regex(...)`. All three validated fields (azureblob `account`, s3 `region`, gcs `project_number`) are `required: true`, so the simpler pattern applies: `z.string().min(1, ...).regex(regex, message)`.
|
||||
|
||||
The tooltip feature requires zero new dependencies. React `useState` per field is the right tool. The CSS toggle pattern already established in `AzureAuthToggle` and `SftpAuthToggle` (div.block / div.hidden) is the exact pattern to reuse for tooltip panel visibility. All existing tests pass (147/147); new tests must be added for VALID-01 regex rejection and UX-01 tooltip toggle behavior.
|
||||
|
||||
**Primary recommendation:** Implement as two sequential plans — Plan 01 adds validation (registry + schema + tests), Plan 02 adds tooltips (FieldDef extension + FieldRenderer + PasswordField + SftpAuthToggle + tests).
|
||||
|
||||
---
|
||||
|
||||
## Standard Stack
|
||||
|
||||
### Core (already installed — no new dependencies)
|
||||
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
|---------|---------|---------|--------------|
|
||||
| zod | 4.3.6 | Schema validation | Already in use — `buildZodSchema()` already chains `.min(1)`, just extend with `.regex()` |
|
||||
| react-hook-form | 7.72.0 | Form state + validation trigger | Already wired — `mode: 'onSubmit', reValidateMode: 'onChange'` means errors appear after first submit then live |
|
||||
| @hookform/resolvers | 5.2.2 | zodResolver bridges Zod into RHF | Already wired in RemoteConfigStep |
|
||||
| react | 18.3.1 | `useState` for tooltip toggle | useState already used in PasswordField for show/hide |
|
||||
| tailwindcss | 4.2.2 | Styling | Inline tooltip panel follows existing helpText style (`text-xs`) |
|
||||
|
||||
**No new npm packages needed for this phase.**
|
||||
|
||||
### Alternatives Considered
|
||||
|
||||
| Instead of | Could Use | Tradeoff |
|
||||
|------------|-----------|----------|
|
||||
| Local `useState` toggle | Global tooltip store | Local is correct here — each field is independent, no cross-field coordination |
|
||||
| CSS div.hidden / div.block pattern | `{showTooltip && <div>...}` conditional render | Both work; CSS-hidden is the established project convention for toggles (AzureAuthToggle, SftpAuthToggle) — use CSS-hidden for consistency |
|
||||
| Text character ⓘ | Heroicons InformationCircleIcon | No new dependency needed; text or inline SVG both work; Claude's discretion |
|
||||
|
||||
---
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Recommended File Modification Order
|
||||
|
||||
```
|
||||
src/schemas/registry.ts # Step 1: extend FieldDef interface, add validate + tooltipText fields
|
||||
src/schemas/index.ts # Step 2: extend buildZodSchema() to read field.validate
|
||||
src/components/ui/FieldRenderer.tsx # Step 3: add ⓘ icon + tooltip panel for text/select renderers
|
||||
src/components/ui/PasswordField.tsx # Step 3: add ⓘ icon + tooltip panel for password renderer
|
||||
src/components/wizard/SftpAuthToggle.tsx # Step 4: add ⓘ above the tab toggle
|
||||
src/components/wizard/RemoteConfigStep.test.tsx # Tests: VALID-01 + UX-01 coverage
|
||||
```
|
||||
|
||||
### Pattern 1: FieldDef Interface Extension
|
||||
|
||||
**What:** Add two optional properties to the existing `FieldDef` interface.
|
||||
**When to use:** Always — this is the single source of truth for both validate and tooltipText.
|
||||
|
||||
```typescript
|
||||
// src/schemas/registry.ts — add to FieldDef interface
|
||||
export interface FieldDef {
|
||||
key: string;
|
||||
label: string;
|
||||
inputType: 'text' | 'password' | 'select' | 'toggle';
|
||||
required: boolean;
|
||||
placeholder?: string;
|
||||
helpText?: string;
|
||||
options?: { value: string; label: string }[];
|
||||
validate?: { regex: RegExp; message: string }; // NEW: format validation rule
|
||||
tooltipText?: string; // NEW: longer plain-language explanation
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: buildZodSchema() Extension
|
||||
|
||||
**What:** After the existing required/optional branch, chain `.regex()` if `field.validate` is present.
|
||||
**Critical detail:** For required fields, chain `.regex()` AFTER `.min(1)`. For optional fields, chain `.regex()` BEFORE `.optional()` — though all three validated fields are required, so the optional case does not apply in this phase.
|
||||
|
||||
```typescript
|
||||
// src/schemas/index.ts — updated loop body
|
||||
for (const field of fields) {
|
||||
let schema: z.ZodTypeAny = field.required
|
||||
? z.string().min(1, `${field.label} is required`)
|
||||
: z.string();
|
||||
|
||||
if (field.validate) {
|
||||
schema = (schema as z.ZodString).regex(field.validate.regex, field.validate.message);
|
||||
}
|
||||
|
||||
if (!field.required) {
|
||||
schema = (schema as z.ZodString).optional();
|
||||
}
|
||||
|
||||
shape[field.key] = schema;
|
||||
}
|
||||
```
|
||||
|
||||
**Zod v4 verification:** `z.string().min(1, ...).regex(/pattern/, 'message')` — confirmed working via runtime test. Regex fires after min(1), so empty string shows "required" error not "invalid format".
|
||||
|
||||
### Pattern 3: Tooltip Toggle in FieldRenderer
|
||||
|
||||
**What:** Add local `useState` for tooltip visibility; conditionally render the ⓘ button and panel.
|
||||
**When to use:** When `field.tooltipText` is present — guard with `field.tooltipText &&`.
|
||||
|
||||
```typescript
|
||||
// src/components/ui/FieldRenderer.tsx — text case (same pattern for select)
|
||||
import { useState } from 'react';
|
||||
|
||||
// Inside the component function:
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
// Label with optional ⓘ icon:
|
||||
<label htmlFor={field.key} className="text-sm font-medium text-gray-700 flex items-center gap-1">
|
||||
{field.label}
|
||||
{field.required && <span className="ml-1 text-red-500">*</span>}
|
||||
{field.tooltipText && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowTooltip(v => !v)}
|
||||
aria-label={`More info about ${field.label}`}
|
||||
className="ml-1 text-blue-500 hover:text-blue-700 text-xs"
|
||||
>
|
||||
ⓘ
|
||||
</button>
|
||||
)}
|
||||
</label>
|
||||
|
||||
// Tooltip panel (below input, above helpText/error):
|
||||
{field.tooltipText && showTooltip && (
|
||||
<p className="text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1.5">
|
||||
{field.tooltipText}
|
||||
</p>
|
||||
)}
|
||||
```
|
||||
|
||||
### Pattern 4: PasswordField Tooltip
|
||||
|
||||
**What:** PasswordField currently does not receive `field: FieldDef` — it receives individual props. Two options for adding tooltipText support:
|
||||
|
||||
**Option A (minimal change):** Add `tooltipText?: string` prop to PasswordFieldProps, same `useState` pattern inside.
|
||||
**Option B (structural change):** Pass the full `FieldDef` to PasswordField so it receives all future FieldDef additions automatically.
|
||||
|
||||
**Recommendation:** Option A (minimal). The existing interface is stable and PasswordField is already used by both FieldRenderer and the auth toggle components — changing its interface to accept full FieldDef would require updating all call sites. Adding a single optional prop is lower risk.
|
||||
|
||||
```typescript
|
||||
// Updated PasswordFieldProps:
|
||||
interface PasswordFieldProps {
|
||||
id: string;
|
||||
label: string;
|
||||
error?: FieldError;
|
||||
registration: UseFormRegisterReturn;
|
||||
placeholder?: string;
|
||||
helpText?: string;
|
||||
tooltipText?: string; // NEW
|
||||
}
|
||||
```
|
||||
|
||||
FieldRenderer passes `tooltipText={field.tooltipText}` when calling PasswordField. AzureAuthToggle call sites do not need tooltipText (azureblob key and sas_url get their tooltip via FieldRenderer in the registry loop — wait, see Pitfall 1 below).
|
||||
|
||||
### Pattern 5: SftpAuthToggle Auth-Method Tooltip
|
||||
|
||||
**What:** The auth-method tooltip is not field-level — it explains the Password vs Private Key choice. A standalone `useState` for one tooltip, placed above the segmented control.
|
||||
|
||||
```typescript
|
||||
// SftpAuthToggle.tsx addition:
|
||||
const [showAuthTip, setShowAuthTip] = useState(false);
|
||||
|
||||
// Before the segmented control div:
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm font-medium text-gray-700">Authentication Method</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAuthTip(v => !v)}
|
||||
aria-label="More info about authentication methods"
|
||||
className="text-blue-500 hover:text-blue-700 text-xs"
|
||||
>
|
||||
ⓘ
|
||||
</button>
|
||||
</div>
|
||||
{showAuthTip && (
|
||||
<p className="text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1.5">
|
||||
Password auth sends your password directly to the SFTP server.
|
||||
Key-based auth uses a private key file (more secure — the server only stores your public key).
|
||||
Paste the private key PEM content if using key-based auth.
|
||||
</p>
|
||||
)}
|
||||
```
|
||||
|
||||
### Anti-Patterns to Avoid
|
||||
|
||||
- **Calling `.optional()` then `.regex()` in Zod v4:** `z.string().optional()` returns `ZodOptional<ZodString>` which has no `.regex()` method. Order must be `.regex().optional()` for optional+validated fields (not applicable here since all validated fields are required, but important to know).
|
||||
- **Adding tooltip state to a parent component:** Each field manages its own tooltip state with `useState`. No lifting state up, no global tooltip context.
|
||||
- **Rendering the ⓘ button when `tooltipText` is undefined:** Guard with `field.tooltipText &&` to avoid rendering an empty button for most fields.
|
||||
- **Passing `tooltipText` to PasswordField from AzureAuthToggle for azureblob sas_url and key fields:** These fields are rendered inside AzureAuthToggle which calls PasswordField directly — not via FieldRenderer's registry loop. The `tooltipText` for azureblob.key and azureblob.sas_url must be passed explicitly through AzureAuthToggle (see Pitfall 1).
|
||||
|
||||
---
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| Regex validation error display | Custom error state in component | Zod `.regex()` + zodResolver | RHF already captures Zod errors and makes them available as `errors[field.key]` — FieldRenderer already renders them |
|
||||
| Tooltip positioning/overflow | CSS `position: absolute` tooltip popover | Inline expand (push content down) | Project decision: inline reveal, pushes content down — simpler, no overflow issues, no z-index conflicts |
|
||||
| Validation trigger timing | Custom onChange handlers | RHF `reValidateMode: 'onChange'` | Already configured in RemoteConfigStep — regex errors appear live after first submit attempt |
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: AzureAuthToggle bypasses FieldRenderer for sas_url and key
|
||||
|
||||
**What goes wrong:** `azureblob.sas_url` and `azureblob.key` are not rendered through the registry loop in RemoteConfigStep — they are rendered directly by `AzureAuthToggle` which calls `PasswordField` with hardcoded props. Adding `tooltipText` to the registry entries for these fields is not enough to make the tooltip appear automatically.
|
||||
|
||||
**Why it happens:** RemoteConfigStep uses a three-branch ternary. The `azureblob` branch renders the account field via FieldRenderer but delegates key+sas_url to `AzureAuthToggle`. AzureAuthToggle has hardcoded `helpText` strings and doesn't read from the registry.
|
||||
|
||||
**How to avoid:** Two options — (A) look up the registry entry inside AzureAuthToggle and pass `field.tooltipText` to PasswordField, or (B) hardcode the `tooltipText` prop in AzureAuthToggle's PasswordField calls. Option A is cleaner. If using Option A: `BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'sas_url')?.tooltipText`.
|
||||
|
||||
**Warning signs:** Registry has `tooltipText` on sas_url and key, but no tooltip appears in the Azure form. Test will catch this if asserting tooltip button presence.
|
||||
|
||||
### Pitfall 2: PasswordField used by both FieldRenderer and auth toggles — prop update needed in all call sites
|
||||
|
||||
**What goes wrong:** PasswordField is called in three places: FieldRenderer, AzureAuthToggle, and SftpAuthToggle. Adding `tooltipText?` to PasswordFieldProps is safe (optional prop), but only FieldRenderer and AzureAuthToggle need it populated. SftpAuthToggle's PasswordField calls (for `pass` and `key_pem`) do not need tooltipText — the auth method tooltip is above the toggle, not on each individual field.
|
||||
|
||||
**Why it happens:** PasswordField is a shared component used in multiple contexts.
|
||||
|
||||
**How to avoid:** Simply don't pass `tooltipText` in SftpAuthToggle's PasswordField calls — the prop is optional.
|
||||
|
||||
### Pitfall 3: Zod v4 `.regex()` returns a new ZodString — type must be cast for chaining
|
||||
|
||||
**What goes wrong:** In `buildZodSchema()`, the `schema` variable is typed as `z.ZodTypeAny`. After calling `.min(1, ...)` on a `z.string()`, the result type is still `ZodString`, but TypeScript may not know this through a `ZodTypeAny` variable.
|
||||
|
||||
**Why it happens:** `ZodTypeAny` is a broad union type. The `.regex()` method is only on `ZodString`.
|
||||
|
||||
**How to avoid:** Cast to `z.ZodString` before calling `.regex()`: `(schema as z.ZodString).regex(...)`. Alternatively, refactor the loop to keep a `ZodString` variable explicitly. Confirmed working via runtime test.
|
||||
|
||||
### Pitfall 4: Test assertions for tooltip — input type='password' accessibility
|
||||
|
||||
**What goes wrong:** When testing tooltip button presence for password fields (sas_url, key, onedrive token), the label must match what `getByLabelText` expects. The ⓘ button is inline with the label text, not the label itself.
|
||||
|
||||
**Why it happens:** If the ⓘ is a `<button>` inside the `<label>` element, `getByLabelText` behavior may differ depending on the DOM structure. The ⓘ should be OUTSIDE the `<label>` element to avoid accessibility issues.
|
||||
|
||||
**How to avoid:** Keep the ⓘ button as a sibling element after the label (wrapped in a flex container), not inside the `<label>` tag. Test with `screen.getByRole('button', { name: /more info about/i })` or `getByLabelText` on the input separately.
|
||||
|
||||
### Pitfall 5: S3 region regex edge cases
|
||||
|
||||
**What goes wrong:** The decided regex `/^[a-z][a-z0-9-]+[a-z0-9]$/` requires at least 3 characters (start + middle + end). This is correct for all current AWS regions. However, future AWS regions could theoretically be 2-character — though this has never happened.
|
||||
|
||||
**Why it happens:** The `+` quantifier on the middle group requires at least one character between start and end.
|
||||
|
||||
**How to avoid:** The decided regex is acceptable for the current AWS region list (verified by cross-checking against known regions like `us-east-1`, `eu-west-1`, `ap-southeast-2`). Document the regex choice in registry helpText or a comment.
|
||||
|
||||
---
|
||||
|
||||
## Code Examples
|
||||
|
||||
Verified patterns from codebase + Zod v4 runtime tests:
|
||||
|
||||
### Zod v4 regex chaining (verified 2026-03-31)
|
||||
|
||||
```typescript
|
||||
// Required field with regex — order: min(1) then regex
|
||||
z.string().min(1, 'Storage Account Name is required')
|
||||
.regex(/^[a-z0-9]{3,24}$/, 'Must be 3–24 lowercase alphanumeric characters')
|
||||
|
||||
// Runtime verified:
|
||||
// .safeParse('abc123') → { success: true }
|
||||
// .safeParse('ABC') → { success: false, error.issues[0].message: 'Must be 3–24...' }
|
||||
// .safeParse('') → { success: false, error.issues[0].message: 'Storage Account Name is required' }
|
||||
```
|
||||
|
||||
### buildZodSchema() extended loop body
|
||||
|
||||
```typescript
|
||||
function buildZodSchema(backendType: BackendType): z.ZodObject<Record<string, z.ZodTypeAny>> {
|
||||
const fields = BACKEND_REGISTRY[backendType].fields;
|
||||
const shape: Record<string, z.ZodTypeAny> = {};
|
||||
for (const field of fields) {
|
||||
let schema: z.ZodTypeAny = field.required
|
||||
? z.string().min(1, `${field.label} is required`)
|
||||
: z.string();
|
||||
|
||||
if (field.validate) {
|
||||
schema = (schema as z.ZodString).regex(field.validate.regex, field.validate.message);
|
||||
}
|
||||
|
||||
if (!field.required) {
|
||||
schema = (schema as z.ZodString).optional();
|
||||
}
|
||||
|
||||
shape[field.key] = schema;
|
||||
}
|
||||
return z.object(shape);
|
||||
}
|
||||
```
|
||||
|
||||
### FieldRenderer useState tooltip pattern
|
||||
|
||||
```typescript
|
||||
import { useState } from 'react';
|
||||
|
||||
export function FieldRenderer({ field, register, error }: FieldRendererProps) {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
// ...
|
||||
// Label row (flex, items-center):
|
||||
// <label> text + required star + ⓘ button (if field.tooltipText)
|
||||
// <input>
|
||||
// tooltip panel (if field.tooltipText && showTooltip)
|
||||
// helpText (if !error && !showTooltip, or always — Claude's discretion)
|
||||
// error (if error)
|
||||
}
|
||||
```
|
||||
|
||||
### Registry entry with both validate and tooltipText
|
||||
|
||||
```typescript
|
||||
// azureblob account field — validate only
|
||||
{
|
||||
key: 'account',
|
||||
label: 'Storage Account Name',
|
||||
inputType: 'text',
|
||||
required: true,
|
||||
placeholder: 'mystorageaccount',
|
||||
helpText: 'The storage account name (not the full URL)',
|
||||
validate: {
|
||||
regex: /^[a-z0-9]{3,24}$/,
|
||||
message: 'Must be 3–24 lowercase alphanumeric characters (no hyphens or uppercase)',
|
||||
},
|
||||
}
|
||||
|
||||
// azureblob sas_url field — tooltipText only
|
||||
{
|
||||
key: 'sas_url',
|
||||
label: 'SAS URL',
|
||||
inputType: 'password',
|
||||
required: false,
|
||||
placeholder: 'https://mystorageaccount.blob.core.windows.net/?sv=...',
|
||||
helpText: 'Full SAS URL including account and container.',
|
||||
tooltipText: 'A SAS URL (Shared Access Signature) bundles the storage endpoint with a time-limited, scope-limited token. It grants access only to the containers specified and expires automatically. Use this if you want limited-access credentials. If you have the full account key instead, switch to Access Key.',
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State of the Art
|
||||
|
||||
| Old Approach | Current Approach | When Changed | Impact |
|
||||
|--------------|------------------|--------------|--------|
|
||||
| Hand-written Zod schemas per backend | `buildZodSchema()` from registry | Phase 5 (v1.1) | Adding a registry field automatically adds its Zod validation |
|
||||
| Hardcoded field lists in components | Registry-driven `fields.map()` loop | Phase 5-6 | New field with `validate` property gets regex validation for free |
|
||||
|
||||
**Deprecated/outdated:**
|
||||
- None for this phase — patterns are all already established.
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **helpText visibility when tooltip is showing**
|
||||
- What we know: helpText is the brief always-visible hint; tooltipText is the expanded explanation
|
||||
- What's unclear: Should helpText hide when the tooltip panel is open to reduce visual clutter, or always show?
|
||||
- Recommendation: Keep helpText always visible (it's brief). Show tooltip panel below it. Claude's discretion on layout.
|
||||
|
||||
2. **AzureAuthToggle registry lookup vs hardcoded tooltipText**
|
||||
- What we know: AzureAuthToggle bypasses FieldRenderer, so registry tooltipText won't appear automatically
|
||||
- What's unclear: Whether to read from registry inside AzureAuthToggle or hardcode the tooltip text strings
|
||||
- Recommendation: Read from registry (`BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'sas_url')?.tooltipText`) to keep registry as single source of truth. Hardcoding is acceptable if simpler.
|
||||
|
||||
---
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
### Test Framework
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Framework | Vitest 4.1.1 + @testing-library/react 16.3.2 |
|
||||
| Config file | `vitest.config.ts` (environment: node; test files use `@vitest-environment jsdom` pragma) |
|
||||
| Quick run command | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` |
|
||||
| Full suite command | `npx vitest run` |
|
||||
|
||||
### Phase Requirements → Test Map
|
||||
|
||||
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
||||
|--------|----------|-----------|-------------------|-------------|
|
||||
| VALID-01 | Azure account name rejects non-lowercase, non-alphanumeric, wrong length | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
|
||||
| VALID-01 | Azure account name accepts valid 3-24 char lowercase alphanumeric | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
|
||||
| VALID-01 | S3 region rejects invalid format (e.g., 'us_east_1', uppercase) | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
|
||||
| VALID-01 | S3 region accepts valid format (e.g., 'us-east-1') | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
|
||||
| VALID-01 | GCS project_number rejects non-digits | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
|
||||
| VALID-01 | GCS project_number accepts digits | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
|
||||
| UX-01 | ⓘ button present on azureblob sas_url field | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
|
||||
| UX-01 | Clicking ⓘ on sas_url shows tooltip panel | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
|
||||
| UX-01 | Clicking ⓘ again hides tooltip panel | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
|
||||
| UX-01 | ⓘ button present on SFTP auth method section | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
|
||||
| UX-01 | ⓘ button present on OneDrive token field | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
|
||||
|
||||
### Sampling Rate
|
||||
- **Per task commit:** `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx`
|
||||
- **Per wave merge:** `npx vitest run`
|
||||
- **Phase gate:** Full suite green (currently 147 tests) before `/gsd:verify-work`
|
||||
|
||||
### Wave 0 Gaps
|
||||
- [ ] VALID-01 test stubs in `src/components/wizard/RemoteConfigStep.test.tsx` — covers regex rejection and acceptance for all 3 fields
|
||||
- [ ] UX-01 test stubs in `src/components/wizard/RemoteConfigStep.test.tsx` — covers ⓘ button presence and toggle behavior for all 4 tooltip fields
|
||||
|
||||
*(Note: test FILE exists — new describes/its are added to the existing file)*
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
- Direct Zod v4.3.6 runtime execution — `.regex()` chaining verified, `.optional().regex()` order issue verified
|
||||
- Codebase read: `src/schemas/registry.ts`, `src/schemas/index.ts`, `src/components/ui/FieldRenderer.tsx`, `src/components/ui/PasswordField.tsx`, `src/components/wizard/AzureAuthToggle.tsx`, `src/components/wizard/SftpAuthToggle.tsx`, `src/components/wizard/RemoteConfigStep.tsx`, `src/components/wizard/RemoteConfigStep.test.tsx`
|
||||
- Test suite run: `npx vitest run` — 147/147 pass as baseline
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
- 07-CONTEXT.md — user locked decisions, regexes, field lists
|
||||
- REQUIREMENTS.md — VALID-01, UX-01 success criteria
|
||||
|
||||
### Tertiary (LOW confidence)
|
||||
- None — all findings are from direct codebase inspection and runtime verification.
|
||||
|
||||
---
|
||||
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
- Standard stack: HIGH — all libraries already installed and in use; no new dependencies
|
||||
- Architecture: HIGH — patterns directly observed in existing code; Zod v4 regex chaining runtime-verified
|
||||
- Pitfalls: HIGH — AzureAuthToggle bypass confirmed by reading RemoteConfigStep; Zod optional+regex order confirmed by runtime test
|
||||
|
||||
**Research date:** 2026-03-31
|
||||
**Valid until:** 2026-04-30 (stable codebase, no moving targets)
|
||||
Reference in New Issue
Block a user