Skip to content

Commit 5ec13f6

Browse files
authored
Chore: Frontend Quality Refactor & Test Suite DRY-ing (#8)
* chore(docs): update devops-agent.md to include CI/CD workflow review and deployment roadmap * chore(fe): replace window with globalThis for better compatibility * chore(docker): update production image to use specific SHA for consistency * chore(tests): improve credential verification in BFF integration tests to avoid dead stores variables requestMadeWithCredentials * feat(agent): add git-worktree-manager skill Adds a new agent skill for managing git worktrees safely. This enables parallel development workflows by allowing the agent to create, manage, and remove sibling worktrees for isolated tasks. * refactor(frontend): use Readonly type for AuthProvider props Updates the AuthProvider component to use Readonly<AuthProviderProps>. This enforces immutability for the component props, aligning with React best practices and improving type safety.
1 parent b8dcba8 commit 5ec13f6

10 files changed

Lines changed: 87 additions & 37 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
name: git-worktree-manager
3+
description: Manages the lifecycle of Git worktrees for parallel development tasks. Handles creation, safe removal, and branch management to keep the main working directory clean.
4+
---
5+
6+
# Git Worktree Manager Skill
7+
8+
This skill enables the agent to safely create, manage, and destroy Git worktrees. This allows for parallel development (e.g., fixing a bug while in the middle of a feature) without disrupting the main working directory.
9+
10+
## 1. Directory Strategy
11+
To prevent path resolution issues and nesting conflicts, all worktrees MUST be created as **siblings** to the current project root.
12+
13+
* **Current Root**: `../project-name`
14+
* **Worktree Root**: `../project-name-<feature>`
15+
16+
**Example:**
17+
If working in `P:/dev/my-app`, a worktree for a "auth-fix" should be at `P:/dev/my-app-auth-fix`.
18+
19+
## 2. Worktree Protocols
20+
21+
### <PROTOCOL:WORKTREE_CREATE>
22+
**Trigger**: When the user needs to switch contexts (e.g., "fix this bug" while changes are unstaged) or explicitly requests a clean environment.
23+
24+
1. **Analyze Request**: Determine the target branch name and the purpose.
25+
2. **Determine Path**: Construct the sibling path `../<current_folder_name>-<short-description>`.
26+
3. **Execute Creation**:
27+
* *New Branch*: `git worktree add ../<path> -b <branch-name>`
28+
* *Existing Branch*: `git worktree add ../<path> <branch-name>`
29+
4. **Confirm**: Report the location of the new worktree and remind the user that dependencies (like `node_modules`) may need to be re-installed there.
30+
31+
### <PROTOCOL:WORKTREE_REMOVE>
32+
**Trigger**: When the task in the worktree is complete and changes are committed.
33+
34+
1. **Verify State**: Ensure changes in the worktree are committed or stashable.
35+
2. **Execute Removal**:
36+
* Attempt standard removal: `git worktree remove <path>`
37+
* *Handle Untracked Files*: If the command fails because of untracked files (e.g., `node_modules`, `target/`), and the work is definitely saved/committed, use force: `git worktree remove --force <path>`.
38+
3. **Prune**: Run `git worktree prune` to clean up references.
39+
40+
## 3. Operational Guidelines
41+
42+
* **Dependency Awareness**: A new worktree is a fresh checkout. It does **not** share `node_modules`, `target`, or `venv` folders. You MUST check for their existence and run install commands (`npm install`, `./mvnw compile`) before running tests in the new worktree.
43+
* **Path Context**: When operating in a worktree, always use absolute paths or relative paths carefully calculated from the worktree root.
44+
* **Shell Context**: When running shell commands, explicitly `cd` into the worktree directory: `cd ../<worktree-path>; <command>`.

backend/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ ENTRYPOINT ["java","-jar","/app/app.jar"]
5858
# This drastically reduces CVE count and eliminates the shell/package manager.
5959
# This is the default stage when building without --target flag
6060
# Usage locally: docker build -t demo-backend:latest .
61-
FROM gcr.io/distroless/java25-debian13:latest AS production
61+
FROM gcr.io/distroless/java25-debian13@sha256:cc3bb44755599d4c25c26c43b05761eeb1da2e779172cee258c2202ca071abfa AS production
6262

6363
# --- SECURITY & UTILITIES (AS NON-ROOT BY DEFAULT) ---
6464
# NOTE: No need to install wget or create a user. Distroless runs as 'nonroot'.

frontend/e2e/bff-integration.spec.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,6 @@ test.describe("BFF Integration - Credentials Configuration", () => {
399399
},
400400
]);
401401

402-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
403402
let requestMadeWithCredentials = false;
404403

405404
await page.route("**/api/v1/greetings**", (route) => {
@@ -423,10 +422,8 @@ test.describe("BFF Integration - Credentials Configuration", () => {
423422
await page.goto("/");
424423
await page.waitForTimeout(1000);
425424

426-
// Note: Playwright route interception may not show cookies in all cases,
427-
// but the application is configured with credentials: "include"
428-
// This is verified in unit tests (config.test.ts)
429-
expect(true).toBe(true); // Placeholder assertion - see config.test.ts for full verification
425+
// Verify that the request actually included the credentials
426+
expect(requestMadeWithCredentials).toBe(true);
430427
});
431428
});
432429

frontend/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export default function App() {
9696

9797
const handleDelete = useCallback(
9898
async (id: string) => {
99-
if (window.confirm("Are you sure you want to delete this greeting?")) {
99+
if (globalThis.confirm("Are you sure you want to delete this greeting?")) {
100100
const success = await deleteGreeting(id);
101101
if (success) {
102102
await refreshList();

frontend/src/api/config.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ describe("api/config initApiClient", () => {
183183

184184
it("dispatches session-expired event on 401 response", async () => {
185185
const eventListener = vi.fn();
186-
window.addEventListener("auth:session-expired", eventListener);
186+
globalThis.addEventListener("auth:session-expired", eventListener);
187187

188188
const fetchStub = vi.fn(
189189
async () =>
@@ -203,6 +203,6 @@ describe("api/config initApiClient", () => {
203203

204204
expect(eventListener).toHaveBeenCalled();
205205

206-
window.removeEventListener("auth:session-expired", eventListener);
206+
globalThis.removeEventListener("auth:session-expired", eventListener);
207207
});
208208
});

frontend/src/api/config.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ export function getApiBasePath(): string {
3737

3838
// Ensure absolute URL for environments where `Request` requires it (e.g., Vitest/node).
3939
// In the browser, same-origin absolute URLs work with Vite proxy and production nginx.
40-
const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost";
40+
const origin =
41+
typeof globalThis !== "undefined" && globalThis.location
42+
? globalThis.location.origin
43+
: "http://localhost";
4144
return new URL("/api", origin).toString();
4245
}
4346

@@ -108,8 +111,12 @@ export function initApiClient(options: InitApiClientOptions = {}): void {
108111
});
109112

110113
client.interceptors.response.use((response) => {
111-
if (response.status === 401 && typeof window !== "undefined") {
112-
window.dispatchEvent(new CustomEvent("auth:session-expired"));
114+
if (
115+
response.status === 401 &&
116+
typeof globalThis !== "undefined" &&
117+
globalThis.dispatchEvent
118+
) {
119+
globalThis.dispatchEvent(new CustomEvent("auth:session-expired"));
113120
}
114121
return response;
115122
});

frontend/src/features/auth/AuthProvider.test.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ describe("AuthProvider", () => {
130130

131131
await waitFor(() => expect(screen.getByTestId("status").textContent).toBe("authenticated"));
132132

133-
window.dispatchEvent(new CustomEvent("auth:session-expired"));
133+
globalThis.dispatchEvent(new CustomEvent("auth:session-expired"));
134134

135135
await waitFor(() => expect(screen.getByTestId("status").textContent).toBe("anonymous"));
136136
expect(screen.getByTestId("username").textContent).toBe("");
@@ -139,7 +139,7 @@ describe("AuthProvider", () => {
139139

140140
describe("AuthProvider logout", () => {
141141
const originalFetch = global.fetch;
142-
const originalLocation = window.location;
142+
const originalLocation = globalThis.location;
143143
let cookieValue = "";
144144

145145
beforeEach(() => {
@@ -154,7 +154,7 @@ describe("AuthProvider logout", () => {
154154
configurable: true,
155155
});
156156
// Mock window.location for redirect testing
157-
Object.defineProperty(window, "location", {
157+
Object.defineProperty(globalThis, "location", {
158158
value: {
159159
href: "http://localhost:3000",
160160
origin: "http://localhost:3000",
@@ -167,7 +167,7 @@ describe("AuthProvider logout", () => {
167167

168168
afterEach(() => {
169169
global.fetch = originalFetch;
170-
Object.defineProperty(window, "location", {
170+
Object.defineProperty(globalThis, "location", {
171171
value: originalLocation,
172172
writable: true,
173173
});
@@ -292,7 +292,7 @@ describe("AuthProvider logout", () => {
292292
await user.click(screen.getByTestId("logout-btn"));
293293

294294
await waitFor(() => {
295-
expect(window.location.href).toBe(keycloakLogoutUrl);
295+
expect(globalThis.location.href).toBe(keycloakLogoutUrl);
296296
});
297297
});
298298

@@ -320,7 +320,7 @@ describe("AuthProvider logout", () => {
320320
await user.click(screen.getByTestId("logout-btn"));
321321

322322
await waitFor(() => {
323-
expect(window.location.reload).toHaveBeenCalled();
323+
expect(globalThis.location.reload).toHaveBeenCalled();
324324
});
325325
});
326326

@@ -348,7 +348,7 @@ describe("AuthProvider logout", () => {
348348

349349
await waitFor(() => {
350350
expect(consoleErrorSpy).toHaveBeenCalledWith("Logout failed:", expect.any(Error));
351-
expect(window.location.href).toBe("/");
351+
expect(globalThis.location.href).toBe("/");
352352
});
353353

354354
consoleErrorSpy.mockRestore();

frontend/src/features/auth/AuthProvider.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { AuthContext } from "./hooks";
77
import type { AuthContextValue, AuthProviderProps, AuthStatus } from "./types";
88
import { resolveAuthMode, resolveLoginUri } from "./utils";
99

10-
export function AuthProvider({ children, mode, mockUser }: AuthProviderProps) {
10+
export function AuthProvider({ children, mode, mockUser }: Readonly<AuthProviderProps>) {
1111
const resolvedMode = resolveAuthMode(mode);
1212

1313
const [status, setStatus] = useState<AuthStatus>("loading");
@@ -71,7 +71,7 @@ export function AuthProvider({ children, mode, mockUser }: AuthProviderProps) {
7171
}, [load]);
7272

7373
useEffect(() => {
74-
if (resolvedMode === "mock" || typeof window === "undefined") {
74+
if (resolvedMode === "mock" || typeof globalThis === "undefined") {
7575
return;
7676
}
7777

@@ -81,9 +81,9 @@ export function AuthProvider({ children, mode, mockUser }: AuthProviderProps) {
8181
setError(null);
8282
};
8383

84-
window.addEventListener("auth:session-expired", onSessionExpired);
84+
globalThis.addEventListener("auth:session-expired", onSessionExpired);
8585
return () => {
86-
window.removeEventListener("auth:session-expired", onSessionExpired);
86+
globalThis.removeEventListener("auth:session-expired", onSessionExpired);
8787
};
8888
}, [resolvedMode]);
8989

@@ -107,7 +107,7 @@ export function AuthProvider({ children, mode, mockUser }: AuthProviderProps) {
107107
}
108108

109109
const loginUri = await resolveLoginUri();
110-
window.location.assign(loginUri);
110+
globalThis.location.assign(loginUri);
111111
}, [mockUser, resolvedMode, user]);
112112

113113
const logout = useCallback(async () => {
@@ -127,7 +127,7 @@ export function AuthProvider({ children, mode, mockUser }: AuthProviderProps) {
127127
?.substring("XSRF-TOKEN=".length);
128128

129129
const headers: Record<string, string> = {
130-
"X-POST-LOGOUT-SUCCESS-URI": window.location.origin,
130+
"X-POST-LOGOUT-SUCCESS-URI": globalThis.location.origin,
131131
};
132132

133133
if (csrfToken) {
@@ -142,14 +142,14 @@ export function AuthProvider({ children, mode, mockUser }: AuthProviderProps) {
142142

143143
const logoutUri = response.headers.get("Location");
144144
if (logoutUri) {
145-
window.location.href = logoutUri;
145+
globalThis.location.href = logoutUri;
146146
} else {
147-
window.location.reload();
147+
globalThis.location.reload();
148148
}
149149
} catch (error) {
150150
// eslint-disable-next-line no-console
151151
console.error("Logout failed:", error);
152-
window.location.href = "/";
152+
globalThis.location.href = "/";
153153
}
154154
}, [resolvedMode]);
155155

website/docs/agent/prompts/devops-agent.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,18 @@ Analyze
2121
- `*.config` in the frontend
2222
- all nginx files (incl for the ./webiste) are configured correctly
2323
- `template-realm.json` for Keycloak setup and user creation
24+
- all `.github/workflows/*.yml` for CI/CD pipelines
2425

2526
We need to make sure every variable is injected properly and all ports, hostnames are set up properly.
27+
I need a clear roadmap to move from local development to a infomaniak jelastic deployment. What do i need to implement ? configure etc... to be able to create then a complete CI/CD pipeline to deploy the app to jelastic from github actions.
28+
29+
# Output Format
30+
Please provide the review in this Markdown format:
31+
1. **Jelastic Compatibility Risks:** (e.g., Hardcoded memory settings, localhost usage).
32+
2. **Dockerfile Optimization:** (e.g Layer caching, distroless suggestions).
33+
3. **Network & SSL Config:** (e.gProxy settings for OAuth2).
34+
4. **Ready-to-Deploy Fixes:** specific code blocks to update `compose.yaml` or `Dockerfile`.
35+
5. **Manual tests procedure on local docker-compose:** (e.g curl commands to verify service connectivity).
2636

2737
## 1. Jelastic & Docker Compatibility (Critical)
2838
* **Base Images:** Verify we are using valid, public Docker Hub images for Java 25 (e.g., `eclipse-temurin:25` or `openjdk:25-slim`). Jelastic requires the image to be pullable from a registry if not built locally.
@@ -51,11 +61,3 @@ We need to make sure every variable is injected properly and all ports, hostname
5161
## 4. Compose to Jelastic Translation
5262
* **Volume Mounts:** Check for local volume binds (e.g., `./config:/app/config`). These will break on Jelastic unless you are using specific "Add-on" storage configurations. Suggest converting these to Environment Variables or ConfigMaps where possible.
5363
* **Restart Policy:** Ensure `restart: always` or `unless-stopped` is defined.
54-
55-
# Output Format
56-
Please provide the review in this Markdown format:
57-
1. **Jelastic Compatibility Risks:** (e.g., Hardcoded memory settings, localhost usage).
58-
2. **Dockerfile Optimization:** (e.g Layer caching, distroless suggestions).
59-
3. **Network & SSL Config:** (e.gProxy settings for OAuth2).
60-
4. **Ready-to-Deploy Fixes:** specific code blocks to update `compose.yaml` or `Dockerfile`.
61-
5. **Manual tests procedure on local docker-compose:** (e.g curl commands to verify service connectivity).

website/docs/agent/prompts/fe-agent.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Role & Objective
2-
Act as a Senior Frontend Security Architect specialized in React 19, TypeScript 5.9, and OpenAPI-driven development. Your goal is to perform a security and code review of the React frontend.
2+
Act as a Senior Frontend Developer specialized in React 19, TypeScript 5.9, and OpenAPI-driven development. Your goal is to perform a security and code review of the React frontend.
33

44
# Project Context
55
* **Architecture:** BFF Pattern (Cookie-based).

0 commit comments

Comments
 (0)