Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1080a3fb7a | |||
| 28a55e752e | |||
| 41bb9e48d9 | |||
| e8c78d6a20 | |||
| 8bd3585f55 | |||
| a507d5d19f | |||
| 7f90630f98 | |||
| 303cc4623e | |||
| 1688c1a991 |
@@ -220,12 +220,14 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${MOLECULE_GITEA_TOKEN}" ]; then
|
||||
echo "::error::AUTO_SYNC_TOKEN secret is empty — register the devops-engineer persona PAT in repo Actions secrets"
|
||||
exit 1
|
||||
echo "::warning::AUTO_SYNC_TOKEN not set — using anonymous clone (repos are public per manifest.json OSS contract)"
|
||||
fi
|
||||
mkdir -p .tenant-bundle-deps
|
||||
# Strip JSON5 comments before jq parsing — Integration Tester appends
|
||||
# `// Triggered by ...` which breaks `jq` in clone-manifest.sh.
|
||||
sed '/^[[:space:]]*\/\//d' manifest.json > .manifest-stripped.json
|
||||
bash scripts/clone-manifest.sh \
|
||||
manifest.json \
|
||||
.manifest-stripped.json \
|
||||
.tenant-bundle-deps/workspace-configs-templates \
|
||||
.tenant-bundle-deps/org-templates \
|
||||
.tenant-bundle-deps/plugins
|
||||
|
||||
@@ -54,7 +54,11 @@ env:
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build & push canvas image
|
||||
runs-on: ubuntu-latest
|
||||
# NOTE: infra-sre must register a `docker` label on every act-runner that
|
||||
# mounts /var/run/docker.sock (group=docker, socket perms 660+). Jobs without
|
||||
# the `docker` label land on runners that lack the socket and fail here.
|
||||
# See issue #576.
|
||||
runs-on: [ubuntu-latest, docker]
|
||||
# Phase 3 (RFC #219 §1): surface broken workflows without blocking.
|
||||
continue-on-error: true
|
||||
steps:
|
||||
@@ -79,8 +83,10 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "::group::Docker daemon health check"
|
||||
echo "Runner: ${HOSTNAME:-unknown}"
|
||||
docker info 2>&1 | head -5 || {
|
||||
echo "::error::Docker daemon is not accessible at /var/run/docker.sock"
|
||||
echo "::error::Runner: ${HOSTNAME:-unknown}"
|
||||
echo "::error::Check: (1) daemon running, (2) runner user in docker group, (3) sock perms 660+"
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -52,7 +52,12 @@ env:
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
# NOTE: infra-sre must register a `docker` label on every act-runner that
|
||||
# mounts /var/run/docker.sock (group=docker, socket perms 660+). Jobs without
|
||||
# the `docker` label land on runners that lack the socket and fail here.
|
||||
# molecule-runner-1 (no socket) vs molecule-runner-4 (socket) — coin-flip
|
||||
# without this label gate. See issue #576.
|
||||
runs-on: [ubuntu-latest, docker]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -68,8 +73,10 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "::group::Docker daemon health check"
|
||||
echo "Runner: ${HOSTNAME:-unknown}"
|
||||
docker info 2>&1 | head -5 || {
|
||||
echo "::error::Docker daemon is not accessible at /var/run/docker.sock"
|
||||
echo "::error::Runner: ${HOSTNAME:-unknown}"
|
||||
echo "::error::Check: (1) daemon is running, (2) runner user is in docker group, (3) sock permissions are 660+"
|
||||
exit 1
|
||||
}
|
||||
@@ -96,8 +103,11 @@ jobs:
|
||||
# 2026-05-08 migration). The token is only needed for private repos.
|
||||
# Do NOT require it — a missing secret would fail the build unnecessarily.
|
||||
mkdir -p .tenant-bundle-deps
|
||||
# Strip JSON5 comments before jq parsing — Integration Tester appends
|
||||
# `// Triggered by ...` which breaks `jq` in clone-manifest.sh.
|
||||
sed '/^[[:space:]]*\/\//d' manifest.json > .manifest-stripped.json
|
||||
bash scripts/clone-manifest.sh \
|
||||
manifest.json \
|
||||
.manifest-stripped.json \
|
||||
.tenant-bundle-deps/workspace-configs-templates \
|
||||
.tenant-bundle-deps/org-templates \
|
||||
.tenant-bundle-deps/plugins
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* AttachmentLightbox — shared fullscreen modal for image/PDF/preview.
|
||||
*
|
||||
* Per RFC #2991 Phase 2, AttachmentLightbox owns:
|
||||
* - Backdrop + centered viewport
|
||||
* - Esc to close
|
||||
* - Click-outside to close (stopPropagation on content)
|
||||
* - Focus trap: focus enters close button on open, restores on close
|
||||
* - prefers-reduced-motion respect
|
||||
*
|
||||
* NOTE: No @testing-library/jest-dom import — use textContent / className /
|
||||
* getAttribute / checked / value checks to avoid jest-dom dependency errors.
|
||||
*
|
||||
* Covers:
|
||||
* - Does not render when open=false
|
||||
* - Renders dialog with role=dialog and aria-modal
|
||||
* - Renders with provided aria-label
|
||||
* - Close button has aria-label="Close preview"
|
||||
* - Clicking backdrop (outside content) calls onClose
|
||||
* - Clicking content does NOT call onClose (stopPropagation)
|
||||
* - Escape key calls onClose
|
||||
* - Focus moves to close button when opened
|
||||
* - Focus restores to previous element when closed
|
||||
* - Reduced motion: motion-reduce class on backdrop
|
||||
* - Renders children inside the modal
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
|
||||
import { AttachmentLightbox } from "../AttachmentLightbox";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Renders the lightbox open with children and returns close fn */
|
||||
function renderOpen(props?: Partial<React.ComponentProps<typeof AttachmentLightbox>>) {
|
||||
const onClose = vi.fn();
|
||||
const result = render(
|
||||
<AttachmentLightbox
|
||||
open={true}
|
||||
onClose={onClose}
|
||||
ariaLabel="Image preview"
|
||||
{...props}
|
||||
>
|
||||
<img alt="test" src="data:image/png;base64,iVBORw0KGgo=" />
|
||||
</AttachmentLightbox>,
|
||||
);
|
||||
return { ...result, onClose };
|
||||
}
|
||||
|
||||
// ─── Render States ────────────────────────────────────────────────────────────
|
||||
|
||||
describe("AttachmentLightbox — render", () => {
|
||||
it("does not render when open=false", () => {
|
||||
const { container } = render(
|
||||
<AttachmentLightbox
|
||||
open={false}
|
||||
onClose={vi.fn()}
|
||||
ariaLabel="Preview"
|
||||
>
|
||||
<div>content</div>
|
||||
</AttachmentLightbox>,
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("renders dialog with role=dialog and aria-modal", () => {
|
||||
renderOpen();
|
||||
const dialog = document.querySelector('[role="dialog"]');
|
||||
expect(dialog).toBeTruthy();
|
||||
expect(dialog?.getAttribute("aria-modal")).toBe("true");
|
||||
});
|
||||
|
||||
it("renders with provided aria-label", () => {
|
||||
renderOpen({ ariaLabel: "PDF: report-2026.pdf" });
|
||||
const dialog = document.querySelector('[role="dialog"]');
|
||||
expect(dialog?.getAttribute("aria-label")).toBe("PDF: report-2026.pdf");
|
||||
});
|
||||
|
||||
it("close button has aria-label='Close preview'", () => {
|
||||
renderOpen();
|
||||
const btn = document.querySelector('[aria-label="Close preview"]');
|
||||
expect(btn).toBeTruthy();
|
||||
expect(btn?.tagName).toBe("BUTTON");
|
||||
});
|
||||
|
||||
it("renders children inside the modal", () => {
|
||||
renderOpen({ ariaLabel: "Preview" });
|
||||
// Children are inside the dialog
|
||||
const dialog = document.querySelector('[role="dialog"]');
|
||||
expect(dialog?.querySelector("img")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("applies reduced-motion class on backdrop", () => {
|
||||
renderOpen();
|
||||
// The div[role="dialog"] IS the fixed backdrop — contains motion-reduce:transition-none
|
||||
const dialog = document.querySelector('[role="dialog"]') as HTMLElement;
|
||||
expect(dialog?.className).toContain("motion-reduce");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Interaction ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe("AttachmentLightbox — interaction", () => {
|
||||
it("calls onClose when close button is clicked", () => {
|
||||
const onClose = vi.fn();
|
||||
renderOpen({ onClose });
|
||||
const btn = document.querySelector('[aria-label="Close preview"]') as HTMLButtonElement;
|
||||
btn.click();
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onClose when backdrop (outside content) is clicked", () => {
|
||||
const onClose = vi.fn();
|
||||
renderOpen({ onClose });
|
||||
// The div[role="dialog"] IS the backdrop (fixed inset-0).
|
||||
// Click on it — e.target === e.currentTarget triggers onBackdropClick.
|
||||
const backdrop = document.querySelector('[role="dialog"]') as HTMLElement;
|
||||
fireEvent.click(backdrop, { target: backdrop });
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does NOT call onClose when content (inside modal) is clicked", () => {
|
||||
const onClose = vi.fn();
|
||||
renderOpen({ onClose });
|
||||
// Click on the img inside the modal
|
||||
const img = document.querySelector("img") as HTMLElement;
|
||||
fireEvent.click(img);
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls onClose on Escape key", () => {
|
||||
const onClose = vi.fn();
|
||||
renderOpen({ onClose });
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not call onClose for other keys", () => {
|
||||
const onClose = vi.fn();
|
||||
renderOpen({ onClose });
|
||||
fireEvent.keyDown(document, { key: "Enter" });
|
||||
fireEvent.keyDown(document, { key: "Tab" });
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Focus Management ────────────────────────────────────────────────────────
|
||||
|
||||
describe("AttachmentLightbox — focus management", () => {
|
||||
it("moves focus to close button when opened", () => {
|
||||
renderOpen();
|
||||
const btn = document.querySelector('[aria-label="Close preview"]') as HTMLButtonElement;
|
||||
expect(document.activeElement).toBe(btn);
|
||||
});
|
||||
|
||||
it("restores focus to previous element when closed", () => {
|
||||
// Create a button to hold focus before opening the modal
|
||||
const outerBtn = document.createElement("button");
|
||||
outerBtn.textContent = "Open modal";
|
||||
document.body.appendChild(outerBtn);
|
||||
outerBtn.focus();
|
||||
expect(document.activeElement).toBe(outerBtn);
|
||||
|
||||
const onClose = vi.fn();
|
||||
const { rerender } = render(
|
||||
<AttachmentLightbox
|
||||
open={false}
|
||||
onClose={onClose}
|
||||
ariaLabel="Preview"
|
||||
>
|
||||
<div>content</div>
|
||||
</AttachmentLightbox>,
|
||||
);
|
||||
|
||||
// Open the modal
|
||||
rerender(
|
||||
<AttachmentLightbox
|
||||
open={true}
|
||||
onClose={onClose}
|
||||
ariaLabel="Preview"
|
||||
>
|
||||
<div>content</div>
|
||||
</AttachmentLightbox>,
|
||||
);
|
||||
|
||||
// Focus should now be on close button
|
||||
const btn = document.querySelector('[aria-label="Close preview"]') as HTMLButtonElement;
|
||||
expect(document.activeElement).toBe(btn);
|
||||
|
||||
// Close the modal
|
||||
rerender(
|
||||
<AttachmentLightbox
|
||||
open={false}
|
||||
onClose={onClose}
|
||||
ariaLabel="Preview"
|
||||
>
|
||||
<div>content</div>
|
||||
</AttachmentLightbox>,
|
||||
);
|
||||
|
||||
// Focus should be restored to outerBtn
|
||||
expect(document.activeElement).toBe(outerBtn);
|
||||
|
||||
document.body.removeChild(outerBtn);
|
||||
});
|
||||
});
|
||||
@@ -763,6 +763,7 @@ def test_sanitize_agent_error_stderr_and_exc():
|
||||
out = sanitize_agent_error(exc=err, stderr="rate limit exceeded")
|
||||
assert "ValueError" in out # exc class IS the tag when stderr is provided
|
||||
assert "rate limit exceeded" in out
|
||||
assert "workspace logs" not in out # stderr form, not the generic form
|
||||
|
||||
|
||||
def test_sanitize_agent_error_stderr_empty_string():
|
||||
|
||||
Reference in New Issue
Block a user