Merge pull request #2641 from Molecule-AI/staging
staging → main: auto-promote 2e96860
This commit is contained in:
commit
bae2727074
@ -36,11 +36,6 @@ export function SearchDialog() {
|
|||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
// Reset focused index when query changes
|
|
||||||
useEffect(() => {
|
|
||||||
setFocusedIndex(-1);
|
|
||||||
}, [query]);
|
|
||||||
|
|
||||||
const filtered = nodes.filter((n) => {
|
const filtered = nodes.filter((n) => {
|
||||||
if (!query) return true;
|
if (!query) return true;
|
||||||
const q = query.toLowerCase();
|
const q = query.toLowerCase();
|
||||||
@ -51,6 +46,18 @@ export function SearchDialog() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto-highlight the first match while the user is typing, so Enter
|
||||||
|
// selects something instead of being a no-op. With an empty query we
|
||||||
|
// keep -1 so opening the dialog (which shows ALL workspaces) doesn't
|
||||||
|
// visually pin one row arbitrarily — only commit a highlight once the
|
||||||
|
// user has narrowed the list.
|
||||||
|
useEffect(() => {
|
||||||
|
setFocusedIndex(query && filtered.length > 0 ? 0 : -1);
|
||||||
|
// Re-running on filtered.length keeps the highlight pinned to the
|
||||||
|
// first row while the result set shrinks/grows; the effect handler
|
||||||
|
// above already short-circuits to -1 when results disappear.
|
||||||
|
}, [query, filtered.length]);
|
||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(nodeId: string) => {
|
(nodeId: string) => {
|
||||||
selectNode(nodeId);
|
selectNode(nodeId);
|
||||||
@ -113,7 +120,7 @@ export function SearchDialog() {
|
|||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
onKeyDown={handleInputKeyDown}
|
onKeyDown={handleInputKeyDown}
|
||||||
placeholder="Search workspaces..."
|
placeholder="Search workspaces..."
|
||||||
className="flex-1 bg-transparent text-sm text-ink placeholder-zinc-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus:outline-none rounded"
|
className="flex-1 bg-transparent text-sm text-ink placeholder-ink-soft focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent rounded"
|
||||||
/>
|
/>
|
||||||
<kbd className="text-[9px] text-ink-mid bg-surface-card/60 px-1.5 py-0.5 rounded border border-line/40">ESC</kbd>
|
<kbd className="text-[9px] text-ink-mid bg-surface-card/60 px-1.5 py-0.5 rounded border border-line/40">ESC</kbd>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -155,18 +155,31 @@ describe("SearchDialog — keyboard accessibility", () => {
|
|||||||
expect(selectNode).not.toHaveBeenCalled();
|
expect(selectNode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("typing a new query resets focusedIndex to -1", () => {
|
it("typing a query that matches auto-highlights the first result", () => {
|
||||||
|
// Replaces the older "resets to -1" assertion. New behavior: a query
|
||||||
|
// with at least one match pins the highlight to row 0 so Enter picks
|
||||||
|
// a result instead of being a no-op. Empty-query case is covered by
|
||||||
|
// "Enter at focusedIndex=-1 does not select anything" above.
|
||||||
|
render(<SearchDialog />);
|
||||||
|
const input = screen.getByRole("combobox");
|
||||||
|
fireEvent.change(input, { target: { value: "Alpha" } });
|
||||||
|
const options = screen.getAllByRole("option");
|
||||||
|
expect(options[0].getAttribute("aria-selected")).toBe("true");
|
||||||
|
// Enter on the auto-highlighted match should select it without
|
||||||
|
// needing a manual ArrowDown first.
|
||||||
|
fireEvent.keyDown(input, { key: "Enter" });
|
||||||
|
expect(selectNode).toHaveBeenCalledWith("ws-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("typing a query that matches NOTHING resets focusedIndex to -1", () => {
|
||||||
render(<SearchDialog />);
|
render(<SearchDialog />);
|
||||||
const input = screen.getByRole("combobox");
|
const input = screen.getByRole("combobox");
|
||||||
fireEvent.keyDown(input, { key: "ArrowDown" }); // focusedIndex → 0
|
fireEvent.keyDown(input, { key: "ArrowDown" }); // focusedIndex → 0
|
||||||
// Verify selection before reset
|
fireEvent.change(input, { target: { value: "zzz-no-match" } });
|
||||||
expect(screen.getAllByRole("option")[0].getAttribute("aria-selected")).toBe("true");
|
// No options remain, so nothing to assert on aria-selected directly —
|
||||||
// Change query — triggers the useEffect that resets focusedIndex
|
// the empty-state message takes over. But Enter should be a no-op.
|
||||||
fireEvent.change(input, { target: { value: "Alpha" } });
|
fireEvent.keyDown(input, { key: "Enter" });
|
||||||
// After reset all options must have aria-selected="false"
|
expect(selectNode).not.toHaveBeenCalled();
|
||||||
screen.getAllByRole("option").forEach((opt) => {
|
|
||||||
expect(opt.getAttribute("aria-selected")).toBe("false");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("aria-activedescendant matches the focused option's id", () => {
|
it("aria-activedescendant matches the focused option's id", () => {
|
||||||
|
|||||||
@ -517,6 +517,7 @@ fi
|
|||||||
# "Encrypted content is not supported" → hermes codex_responses API misroute (#14)
|
# "Encrypted content is not supported" → hermes codex_responses API misroute (#14)
|
||||||
# "Unknown provider" → bridge misconfigured PROVIDER= (regression of #13 fix)
|
# "Unknown provider" → bridge misconfigured PROVIDER= (regression of #13 fix)
|
||||||
# "hermes-agent unreachable" → gateway process died
|
# "hermes-agent unreachable" → gateway process died
|
||||||
|
# "exceeded your current quota" → MOLECULE_STAGING_OPENAI_KEY billing (NOT a platform regression — #2578)
|
||||||
#
|
#
|
||||||
# Fail LOUD with the specific pattern so CI log + alert channel makes the
|
# Fail LOUD with the specific pattern so CI log + alert channel makes the
|
||||||
# regression unambiguous.
|
# regression unambiguous.
|
||||||
@ -542,6 +543,16 @@ fi
|
|||||||
if echo "$AGENT_TEXT" | grep -qF "Invalid API key"; then
|
if echo "$AGENT_TEXT" | grep -qF "Invalid API key"; then
|
||||||
fail "A2A — REGRESSION: tenant auth chain returned 'Invalid API key'. Likely CP boot-event 401 race (CP #238) or stale OPENAI_API_KEY in the runtime env. Raw: $AGENT_TEXT"
|
fail "A2A — REGRESSION: tenant auth chain returned 'Invalid API key'. Likely CP boot-event 401 race (CP #238) or stale OPENAI_API_KEY in the runtime env. Raw: $AGENT_TEXT"
|
||||||
fi
|
fi
|
||||||
|
# Provider quota exhausted — distinguish from a platform regression so
|
||||||
|
# the canary alert names the operator action directly instead of falling
|
||||||
|
# through to the generic "error-shaped response" message. Steps 0-7 having
|
||||||
|
# passed means the platform itself is healthy (CP up, tenant provisioned,
|
||||||
|
# workspace online, A2A delivery end-to-end). When the agent comes back
|
||||||
|
# with a provider-side 429, that is a billing event on the configured
|
||||||
|
# OpenAI key, not a platform regression. Tracked in #2578.
|
||||||
|
if echo "$AGENT_TEXT" | grep -qiE "exceeded your current quota|insufficient_quota"; then
|
||||||
|
fail "A2A — PROVIDER QUOTA EXHAUSTED (NOT a platform regression). Operator action: top up MOLECULE_STAGING_OPENAI_KEY billing or rotate to a higher-quota org at Settings → Secrets and Variables → Actions. Tracked in #2578. Raw: $AGENT_TEXT"
|
||||||
|
fi
|
||||||
# Generic catch-all — falls through if none of the known regressions hit.
|
# Generic catch-all — falls through if none of the known regressions hit.
|
||||||
if echo "$AGENT_TEXT" | grep -qiE "error|exception"; then
|
if echo "$AGENT_TEXT" | grep -qiE "error|exception"; then
|
||||||
fail "A2A returned an error-shaped response: $AGENT_TEXT"
|
fail "A2A returned an error-shaped response: $AGENT_TEXT"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user