From 20067bfa74fe24cebf55fd581256edf7ebf156c7 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 17 Jun 2026 18:42:45 +0000 Subject: [PATCH] fix(harness): surface managed-memory heads-up on dev deploy + validate session storage path in TUI Two bugs found in the gated managed-memory / harness TUI flows. 1. Managed-memory heads-up missing on `agentcore dev` deploys. The dev deploy hook (useDevDeploy) called handleDeploy without an onNotice callback, so the "this harness provisions a dedicated AgentCore Memory resource (3-5 min)..." heads-up that the regular deploy path shows never reached the dev UI. Wire onNotice through useDevDeploy and render it in DevScreen's deploying view as a plain dim "Note:" (matching the deploy-screen convention). 2. Session-storage mount path not validated in the add-harness wizard. The step only checked value.startsWith('/'), so a nested path like /mnt/data/workplace/ passed Enter and failed later at schema-write/deploy. The API constraint is exactly one segment under /mnt (smithy MountPath pattern ^/mnt/[a-zA-Z0-9._-]+/?$, length 6-200; mirrored in the Harness CFN resource schema). Use the existing validateBYOMountPath (already used by the EFS/S3 mount-path steps and matching that pattern) so the step shows a red error and blocks Enter on invalid input. Adds useDevDeploy tests for the onNotice -> managedMemoryNotice wiring (set and unset cases). The mount-path validator already has direct coverage incl. the nested-path rejection (/mnt/foo/bar). --- .../tui/hooks/__tests__/useDevDeploy.test.tsx | 30 +++++++++++++++++-- src/cli/tui/hooks/useDevDeploy.ts | 12 ++++++-- src/cli/tui/screens/dev/DevScreen.tsx | 6 ++++ .../tui/screens/harness/AddHarnessScreen.tsx | 5 +++- 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/cli/tui/hooks/__tests__/useDevDeploy.test.tsx b/src/cli/tui/hooks/__tests__/useDevDeploy.test.tsx index 690055c55..d4130ccd0 100644 --- a/src/cli/tui/hooks/__tests__/useDevDeploy.test.tsx +++ b/src/cli/tui/hooks/__tests__/useDevDeploy.test.tsx @@ -36,10 +36,11 @@ vi.mock('../../../operations/deploy/change-detection', () => ({ })); function Harness({ skip }: { skip?: boolean }) { - const { steps, isComplete, error } = useDevDeploy({ skip }); + const { steps, isComplete, error, managedMemoryNotice } = useDevDeploy({ skip }); return ( - steps:{steps.length} isComplete:{String(isComplete)} error:{error ?? 'null'} + steps:{steps.length} isComplete:{String(isComplete)} error:{error ?? 'null'} notice: + {managedMemoryNotice ?? 'null'} ); } @@ -108,6 +109,31 @@ describe('useDevDeploy', () => { }); }); + it('surfaces the managed-memory heads-up from the onNotice callback', async () => { + mockHandleDeploy.mockImplementation((opts: { onNotice?: (message: string) => void }) => { + opts.onNotice?.('Managed memory: this harness automatically provisions a dedicated AgentCore Memory resource'); + return Promise.resolve({ success: true }); + }); + + const { lastFrame } = render(); + + await vi.waitFor(() => { + expect(lastFrame()).toContain('notice:Managed memory:'); + expect(lastFrame()).toContain('isComplete:true'); + }); + }); + + it('leaves the managed-memory heads-up null when onNotice is not called', async () => { + mockHandleDeploy.mockResolvedValue({ success: true }); + + const { lastFrame } = render(); + + await vi.waitFor(() => { + expect(lastFrame()).toContain('isComplete:true'); + }); + expect(lastFrame()).toContain('notice:null'); + }); + it('populates steps from onProgress callback', async () => { mockHandleDeploy.mockImplementation((opts: { onProgress?: (step: string, status: string) => void }) => { opts.onProgress?.('Validate project', 'start'); diff --git a/src/cli/tui/hooks/useDevDeploy.ts b/src/cli/tui/hooks/useDevDeploy.ts index a570676f4..d6615f8ee 100644 --- a/src/cli/tui/hooks/useDevDeploy.ts +++ b/src/cli/tui/hooks/useDevDeploy.ts @@ -18,6 +18,8 @@ export interface UseDevDeployResult { isComplete: boolean; error: string | undefined; logPath: string | undefined; + /** Managed-memory heads-up surfaced by handleDeploy (null when not applicable) */ + managedMemoryNotice: string | null; } export function useDevDeploy({ skip, ready = true }: UseDevDeployOptions = {}): UseDevDeployResult { @@ -26,6 +28,7 @@ export function useDevDeploy({ skip, ready = true }: UseDevDeployOptions = {}): const [deployDone, setDeployDone] = useState(false); const [error, setError] = useState(); const [logPath, setLogPath] = useState(); + const [managedMemoryNotice, setManagedMemoryNotice] = useState(null); const hasStarted = useRef(false); const onProgress = useCallback((stepName: string, status: 'start' | 'success' | 'error') => { @@ -41,6 +44,10 @@ export function useDevDeploy({ skip, ready = true }: UseDevDeployOptions = {}): setDeployMessages(prev => [...prev, msg]); }, []); + const onNotice = useCallback((message: string) => { + setManagedMemoryNotice(message); + }, []); + useEffect(() => { if (skip || !ready || hasStarted.current) return; hasStarted.current = true; @@ -78,6 +85,7 @@ export function useDevDeploy({ skip, ready = true }: UseDevDeployOptions = {}): verbose: true, onProgress, onDeployMessage, + onNotice, }); if (result.logPath) { @@ -95,10 +103,10 @@ export function useDevDeploy({ skip, ready = true }: UseDevDeployOptions = {}): }; void run(); - }, [skip, ready, onProgress, onDeployMessage]); + }, [skip, ready, onProgress, onDeployMessage, onNotice]); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- skip is boolean, not nullable; || is the correct operator here const isComplete = skip || deployDone; - return { steps, deployMessages, isComplete, error, logPath }; + return { steps, deployMessages, isComplete, error, logPath, managedMemoryNotice }; } diff --git a/src/cli/tui/screens/dev/DevScreen.tsx b/src/cli/tui/screens/dev/DevScreen.tsx index aa3abd89d..e52280b53 100644 --- a/src/cli/tui/screens/dev/DevScreen.tsx +++ b/src/cli/tui/screens/dev/DevScreen.tsx @@ -245,6 +245,7 @@ export function DevScreen(props: DevScreenProps) { isComplete: deployComplete, error: deployError, logPath: deployLogPath, + managedMemoryNotice, } = useDevDeploy({ skip: props.skipDeploy, ready: mode === 'deploying' }); const hasTransitionedFromDeployRef = useRef(false); @@ -527,6 +528,11 @@ export function DevScreen(props: DevScreenProps) { + {managedMemoryNotice && !deployComplete && ( + + Note: {managedMemoryNotice} + + )} {hasStartedCfn && ( diff --git a/src/cli/tui/screens/harness/AddHarnessScreen.tsx b/src/cli/tui/screens/harness/AddHarnessScreen.tsx index 3d94b85cb..60941f5f8 100644 --- a/src/cli/tui/screens/harness/AddHarnessScreen.tsx +++ b/src/cli/tui/screens/harness/AddHarnessScreen.tsx @@ -1423,7 +1423,10 @@ export function AddHarnessScreen({ initialValue="/mnt/data/" onSubmit={wizard.setSessionStoragePath} onCancel={() => wizard.goBack()} - customValidation={value => (value.startsWith('/') ? true : 'Must be an absolute path')} + customValidation={value => { + const r = validateBYOMountPath(value); + return r === true ? true : r; + }} /> )}