diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 1c1dbe6..b996111 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -35,7 +35,13 @@ RUN --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ php \ php-cli \ # includes certutil for dev cert verification - libnss3-tools + libnss3-tools \ + # Playwright system dependencies (headless Chromium, English-only) + libasound2t64 libatk-bridge2.0-0t64 libatk1.0-0t64 libatspi2.0-0t64 \ + libcairo2 libcups2t64 libdbus-1-3 libdrm2 libgbm1 libglib2.0-0t64 \ + libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcb1 libxcomposite1 \ + libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 \ + libfontconfig1 libfreetype6 fonts-liberation fonts-freefont-ttf # Install Vale ARG VALE_VERSION=3.12.0 RUN set -eux; \ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5c96ab9..5d66cf0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -20,6 +20,9 @@ "kubectl": "latest", "helm": "latest" }, + "ghcr.io/devcontainers/features/dotnet:2": {}, + "ghcr.io/devcontainers/features/node:1": {}, + "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {}, "ghcr.io/devcontainers-extra/features/markdownlint-cli2:1": {}, "ghcr.io/devcontainers/features/github-cli:1": {}, "./features/aspire": { @@ -27,7 +30,12 @@ }, "ghcr.io/devcontainers/features/go:1": {}, "ghcr.io/devcontainers/features/terraform:1": {}, - "ghcr.io/devcontainers-extra/features/go-task:1": {} + "ghcr.io/devcontainers-extra/features/go-task:1": {}, + "ghcr.io/devcontainers/features/desktop-lite:1": { + "password": "crucible", + "webPort": "6080", + "vncPort": "5901" + } }, "containerEnv": { "NUGET_PACKAGES": "/home/vscode/.nuget/packages", // this might not be needed. will try removing later. @@ -59,7 +67,8 @@ "postStartCommand": "bash -l .devcontainer/poststart.sh", "forwardPorts": [ 9003, // Xdebug - 443 // Minikube ingress + 443, // Minikube ingress + 6080 // noVNC (Playwright headed browser) ], "customizations": { "vscode": { @@ -76,6 +85,7 @@ "shd101wyy.markdown-preview-enhanced", "pflannery.vscode-versionlens", "Anthropic.claude-code", + "ms-playwright.playwright", "AmazonWebServices.aws-toolkit-vscode", "bmewburn.vscode-intelephense-client", "xdebug.php-debug" @@ -93,7 +103,13 @@ // Enable when working on Moodle/PHP: Extensions > Intelephense > Enable (Workspace) "intelephense.enable": false, // Disable CloudFormation language server to save ~163MB memory (not used in this project) - "aws.cloudFormation.enable": false + "aws.cloudFormation.enable": false, + // Point Playwright extension to the test project + "playwright.reuseBrowser": true, + "playwright.env": {}, + "playwright.configs": [ + "/mnt/data/crucible/crucible-tests/playwright.config.ts" + ] } } } diff --git a/.devcontainer/postcreate.sh b/.devcontainer/postcreate.sh index 829eefa..c5c59b0 100755 --- a/.devcontainer/postcreate.sh +++ b/.devcontainer/postcreate.sh @@ -22,7 +22,31 @@ DOTNET_EF_PID=$! (npm config -g set fund false && npm install -g @angular/cli@latest) & ANGULAR_PID=$! -wait $DOTNET_EF_PID $ANGULAR_PID +# Initialize Playwright test agents in the dev container +PLAYWRIGHT_TESTING_DIR="/mnt/data/crucible/libraries/crucible-tests" +if [ -d "$PLAYWRIGHT_TESTING_DIR" ]; then + ( + cd "$PLAYWRIGHT_TESTING_DIR" || exit 1 + + echo "Installing Playwright dependencies..." + npm install + + echo "Installing Playwright browser binaries..." + npx playwright install chromium + npx playwright install firefox + + echo "Initializing Playwright test agents..." + TMPDIR=$(mktemp -d) + cd "$TMPDIR" + npx --prefix "$PLAYWRIGHT_TESTING_DIR" playwright init-agents --loop=claude --config "$PLAYWRIGHT_TESTING_DIR/playwright.config.ts" + mkdir -p /workspaces/crucible-development/.claude/agents + cp "$TMPDIR/.claude/agents/"*.md /workspaces/crucible-development/.claude/agents/ + rm -rf "$TMPDIR" + ) & + PLAYWRIGHT_AGENTS_PID=$! +fi + +wait $DOTNET_EF_PID $ANGULAR_PID ${PLAYWRIGHT_AGENTS_PID:-} echo "Tool installs complete." # Generate dotnet dev-cert. Needed if not using aspire extension launch profiles diff --git a/.gitignore b/.gitignore index 5c83c6e..fc8caef 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,9 @@ helm-charts/crucible/files/*.crt # playwright mcp logs .playwright-mcp/ +# Generated Playwright test agents +.claude/agents/ + # Local repository configurations (private/internal/personal repos, local overrides) scripts/repos.local.json diff --git a/.mcp.json b/.mcp.json index 6983627..d858c08 100644 --- a/.mcp.json +++ b/.mcp.json @@ -3,8 +3,8 @@ "aspire": { "command": "aspire", "args": [ - "mcp", - "start" + "agent", + "mcp" ] }, "playwright": { @@ -13,6 +13,15 @@ "-y", "@playwright/mcp@latest" ] + }, + "playwright-test": { + "command": "/mnt/data/crucible/crucible-tests/node_modules/.bin/playwright", + "args": [ + "run-test-mcp-server", + "--config", + "/mnt/data/crucible/crucible-tests/playwright.config.ts" + ], + "cwd": "/mnt/data/crucible/crucible-tests" } } -} \ No newline at end of file +} diff --git a/CLAUDE.md b/CLAUDE.md index e6b0fdc..babb911 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,6 +83,9 @@ IMPORTANT! Aspire is designed to capture rich logs and telemetry for all resourc ## Playwright MCP server The playwright MCP server has also been configured in this repository and you should use it to perform functional investigations of the resources defined in the app model as you work on the codebase. To get endpoints that can be used for navigation using the playwright MCP server use the list resources tool. +## Playwright Test Suite +End-to-end Playwright tests live at `/mnt/data/crucible/crucible-tests/`. This repo contains test plans and spec files for all 11 Crucible applications. See the `README.md` in that directory for full details on test conventions, fixtures, and how to write/run tests. When asked to create, update, or run Playwright tests, work in that directory. + ## Aspire workload IMPORTANT! The aspire workload is obsolete. You should never attempt to install or use the Aspire workload. diff --git a/Crucible.AppHost/resources/crucible-realm.json b/Crucible.AppHost/resources/crucible-realm.json index 11c795d..9bbe211 100644 --- a/Crucible.AppHost/resources/crucible-realm.json +++ b/Crucible.AppHost/resources/crucible-realm.json @@ -96,9 +96,15 @@ "description": "${role_default-roles}", "composite": true, "composites": { - "realm": ["offline_access", "uma_authorization"], + "realm": [ + "offline_access", + "uma_authorization" + ], "client": { - "account": ["view-profile", "manage-account"] + "account": [ + "view-profile", + "manage-account" + ] } }, "clientRole": false, @@ -174,7 +180,9 @@ "composite": true, "composites": { "client": { - "realm-management": ["query-clients"] + "realm-management": [ + "query-clients" + ] } }, "clientRole": true, @@ -275,7 +283,10 @@ "composite": true, "composites": { "client": { - "realm-management": ["query-users", "query-groups"] + "realm-management": [ + "query-users", + "query-groups" + ] } }, "clientRole": true, @@ -395,7 +406,9 @@ "composite": true, "composites": { "client": { - "account": ["view-consent"] + "account": [ + "view-consent" + ] } }, "clientRole": true, @@ -445,7 +458,9 @@ "composite": true, "composites": { "client": { - "account": ["manage-account-links"] + "account": [ + "manage-account-links" + ] } }, "clientRole": true, @@ -461,7 +476,9 @@ "name": "Administrators", "path": "/Administrators", "attributes": {}, - "realmRoles": ["Administrator"], + "realmRoles": [ + "Administrator" + ], "clientRoles": {}, "subGroups": [] }, @@ -483,7 +500,9 @@ "clientRole": false, "containerId": "fcd98ec3-93e0-416c-aa5c-e41e7b8c6b02" }, - "requiredCredentials": ["password"], + "requiredCredentials": [ + "password" + ], "otpPolicyType": "totp", "otpPolicyAlgorithm": "HmacSHA1", "otpPolicyInitialCounter": 0, @@ -497,7 +516,9 @@ "totpAppFreeOTPName" ], "webAuthnPolicyRpEntityName": "keycloak", - "webAuthnPolicySignatureAlgorithms": ["ES256"], + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], "webAuthnPolicyRpId": "", "webAuthnPolicyAttestationConveyancePreference": "not specified", "webAuthnPolicyAuthenticatorAttachment": "not specified", @@ -507,7 +528,9 @@ "webAuthnPolicyAvoidSameAuthenticatorRegister": false, "webAuthnPolicyAcceptableAaguids": [], "webAuthnPolicyPasswordlessRpEntityName": "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"], + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], "webAuthnPolicyPasswordlessRpId": "", "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", @@ -519,14 +542,19 @@ "scopeMappings": [ { "clientScope": "offline_access", - "roles": ["offline_access"] + "roles": [ + "offline_access" + ] } ], "clientScopeMappings": { "account": [ { "client": "account-console", - "roles": ["manage-account", "view-groups"] + "roles": [ + "manage-account", + "view-groups" + ] } ] }, @@ -541,7 +569,9 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["/realms/crucible/account/*"], + "redirectUris": [ + "/realms/crucible/account/*" + ], "webOrigins": [], "notBefore": 0, "bearerOnly": false, @@ -583,7 +613,9 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["/realms/crucible/account/*"], + "redirectUris": [ + "/realms/crucible/account/*" + ], "webOrigins": [], "notBefore": 0, "bearerOnly": false, @@ -677,8 +709,12 @@ "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "secret": "gn3D1s0UKCeqUB5ZjtN0aZsStiJjecRW", - "redirectUris": ["/*"], - "webOrigins": ["/*"], + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -738,8 +774,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["http://localhost:4402/*"], - "webOrigins": ["http://localhost:4402"], + "redirectUris": [ + "http://localhost:4402/*" + ], + "webOrigins": [ + "http://localhost:4402" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -791,8 +831,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["http://localhost:4403/*"], - "webOrigins": ["http://localhost:4403"], + "redirectUris": [ + "http://localhost:4403/*" + ], + "webOrigins": [ + "http://localhost:4403" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -882,8 +926,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["/*"], - "webOrigins": ["/*"], + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -932,8 +980,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["http://localhost:4309/*"], - "webOrigins": ["http://localhost:4309"], + "redirectUris": [ + "http://localhost:4309/*" + ], + "webOrigins": [ + "http://localhost:4309" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -981,8 +1033,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["http://localhost:4310/auth-callback*"], - "webOrigins": ["http://localhost:4310"], + "redirectUris": [ + "http://localhost:4310/auth-callback*" + ], + "webOrigins": [ + "http://localhost:4310" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -1031,8 +1087,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["/*"], - "webOrigins": ["/*"], + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -1085,7 +1145,10 @@ "http://localhost:5006/api/oauth2-redirect.html", "http://localhost:5002/api/oauth2-redirect.html" ], - "webOrigins": ["http://localhost:5006", "http://localhost:5002"], + "webOrigins": [ + "http://localhost:5006", + "http://localhost:5002" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -1141,7 +1204,9 @@ "http://localhost:4202/oidc", "http://localhost:4202/oidc-silent.html" ], - "webOrigins": ["http://localhost:4202"], + "webOrigins": [ + "http://localhost:4202" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -1194,8 +1259,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["http://localhost:4300/*"], - "webOrigins": ["http://localhost:4300"], + "redirectUris": [ + "http://localhost:4300/*" + ], + "webOrigins": [ + "http://localhost:4300" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -1305,8 +1374,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["/*"], - "webOrigins": ["/*"], + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -1354,7 +1427,9 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["http://localhost:4302/*"], + "redirectUris": [ + "http://localhost:4302/*" + ], "webOrigins": [ "http://localhost:4302", "http://localhost:4302/healthcheck-ui" @@ -1413,8 +1488,13 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["", "http://localhost:4305/auth-callback*"], - "webOrigins": ["http://localhost:4305"], + "redirectUris": [ + "", + "http://localhost:4305/auth-callback*" + ], + "webOrigins": [ + "http://localhost:4305" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -1537,8 +1617,12 @@ "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "secret": "KAVqRPFpIYsIL3xsK0h8wscDc1W5q50F", - "redirectUris": ["/*"], - "webOrigins": ["/*"], + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -1670,8 +1754,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["/admin/crucible/console/*"], - "webOrigins": ["+"], + "redirectUris": [ + "/admin/crucible/console/*" + ], + "webOrigins": [ + "+" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -1798,7 +1886,10 @@ "http://localhost:4201/assets/oidc-silent.html", "http://localhost:5004/topo/oidc" ], - "webOrigins": ["http://localhost:4201", "http://localhost:5004"], + "webOrigins": [ + "http://localhost:4201", + "http://localhost:5004" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -1846,8 +1937,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["http://localhost:5097/*"], - "webOrigins": ["http://localhost:5097"], + "redirectUris": [ + "http://localhost:5097/*" + ], + "webOrigins": [ + "http://localhost:5097" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -1867,7 +1962,12 @@ "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": true, "nodeReRegistrationTimeout": -1, - "defaultClientScopes": ["xview2", "web-origins", "acr", "roles"], + "defaultClientScopes": [ + "xview2", + "web-origins", + "acr", + "roles" + ], "optionalClientScopes": [ "address", "phone", @@ -1893,7 +1993,9 @@ "http://localhost:4200/auth-callback-silent.html", "http://localhost:4200/auth-callback" ], - "webOrigins": ["http://localhost:4200"], + "webOrigins": [ + "http://localhost:4200" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -1940,8 +2042,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["http://localhost:4724/api/oauth2-redirect.html"], - "webOrigins": ["http://localhost:4724"], + "redirectUris": [ + "http://localhost:4724/api/oauth2-redirect.html" + ], + "webOrigins": [ + "http://localhost:4724" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -1995,8 +2101,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["http://localhost:4724/*"], - "webOrigins": ["*"], + "redirectUris": [ + "http://localhost:4724/*" + ], + "webOrigins": [ + "*" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -2054,7 +2164,9 @@ "http://localhost:4725/auth-callback", "http://localhost:4725/auth-callback-silent.html" ], - "webOrigins": ["http://localhost:4725"], + "webOrigins": [ + "http://localhost:4725" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -2141,8 +2253,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["http://localhost:4720/api/oauth2-redirect.html"], - "webOrigins": ["http://localhost:4720"], + "redirectUris": [ + "http://localhost:4720/api/oauth2-redirect.html" + ], + "webOrigins": [ + "http://localhost:4720" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -2192,8 +2308,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["http://localhost:4720/*"], - "webOrigins": ["*"], + "redirectUris": [ + "http://localhost:4720/*" + ], + "webOrigins": [ + "*" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -2247,7 +2367,9 @@ "http://localhost:4721/auth-callback", "http://localhost:4721/auth-callback-silent.html" ], - "webOrigins": ["http://localhost:4721"], + "webOrigins": [ + "http://localhost:4721" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -2297,8 +2419,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["http://localhost:4722/api/oauth2-redirect.html"], - "webOrigins": ["http://localhost:4722"], + "redirectUris": [ + "http://localhost:4722/api/oauth2-redirect.html" + ], + "webOrigins": [ + "http://localhost:4722" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -2348,8 +2474,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["http://localhost:4722/*"], - "webOrigins": ["*"], + "redirectUris": [ + "http://localhost:4722/*" + ], + "webOrigins": [ + "*" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -2401,7 +2531,9 @@ "http://localhost:4723/auth-callback-silent.html", "http://localhost:4723/auth-callback" ], - "webOrigins": ["http://localhost:4723"], + "webOrigins": [ + "http://localhost:4723" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -2451,8 +2583,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["http://localhost:4400/api/oauth2-redirect.html"], - "webOrigins": ["http://localhost:4400"], + "redirectUris": [ + "http://localhost:4400/api/oauth2-redirect.html" + ], + "webOrigins": [ + "http://localhost:4400" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -2505,8 +2641,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["http://localhost:4400/*"], - "webOrigins": ["*"], + "redirectUris": [ + "http://localhost:4400/*" + ], + "webOrigins": [ + "*" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -2562,7 +2702,9 @@ "http://localhost:4401/auth-callback-silent.html", "http://localhost:4401/auth-callback" ], - "webOrigins": ["http://localhost:4401"], + "webOrigins": [ + "http://localhost:4401" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -2615,8 +2757,12 @@ "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "secret": "super-safe-secret", - "redirectUris": ["http://localhost/admin/oauth2callback.php"], - "webOrigins": ["http://localhost"], + "redirectUris": [ + "http://localhost/admin/oauth2callback.php" + ], + "webOrigins": [ + "http://localhost" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -2682,8 +2828,12 @@ "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "secret": "superset-client-secret", - "redirectUris": ["http://localhost:8088/*"], - "webOrigins": ["http://localhost:8088"], + "redirectUris": [ + "http://localhost:8088/*" + ], + "webOrigins": [ + "http://localhost:8088" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -3581,7 +3731,9 @@ }, "smtpServer": {}, "eventsEnabled": false, - "eventsListeners": ["jboss-logging"], + "eventsListeners": [ + "jboss-logging" + ], "enabledEventTypes": [], "adminEventsEnabled": false, "adminEventsDetailsEnabled": false, @@ -3623,7 +3775,9 @@ "subType": "authenticated", "subComponents": {}, "config": { - "allow-default-scopes": ["true"] + "allow-default-scopes": [ + "true" + ] } }, { @@ -3633,7 +3787,9 @@ "subType": "anonymous", "subComponents": {}, "config": { - "max-clients": ["200"] + "max-clients": [ + "200" + ] } }, { @@ -3651,7 +3807,9 @@ "subType": "anonymous", "subComponents": {}, "config": { - "allow-default-scopes": ["true"] + "allow-default-scopes": [ + "true" + ] } }, { @@ -3680,8 +3838,12 @@ "subType": "anonymous", "subComponents": {}, "config": { - "host-sending-registration-request-must-match": ["true"], - "client-uris-must-match": ["true"] + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] } } ], @@ -3695,12 +3857,18 @@ "privateKey": [ "MIIEpQIBAAKCAQEAu4+F+guhmMR46OIlH6mWrKLTTZZ3z4EAKx28ulfWh2FNNz8eOwNYrGKmqSGVk6oXaZMYrDjk7QU4cvtUxJFJVMxFgNKqYZ/O57af7Jya5a20r8t2O+xDP++e6poTGalVhIbf+zvUEnIM1pLx4RvDawyyp3Tigq7ibCcJvC48aT/5KvMcmwHAqKGK0LfSxoK+3NusTk+B1LVs6AnegjL3ARS27czrrCNeAHkfQuyjwnt4PzxGoW/cYQ0FKqfl561wXHSx5j5hiscQqS/T1IWanq1jFM8ETCgldpT9+og0wwGRE6w0m2Z1pKek/W+q5Z7Fg0mjpzOH9vpBHwiyJ3PAlQIDAQABAoIBAAS4UamPed9XkGhh6oe/s45AIbup3fV/nFK5cpqo47nv8arCgJ8BEE30RJfsg7Bd3y11uXD6FI6/ayJ/nyw8MMF8y4H4qzd+N01++8rPTRmbE7k50E2lPjMBc3kZbAIhEAkgAMp4gLd/HcIXnBUrZvFOdj9/EkUyI/oPSHAvJK+MVMpNizq3x/bOYOPoPq7mIWyhaCRFosYS5ds7P455Cwp0gh3Q/rYo1dUOdh9vdNAzwu7kR3FtfFTVOStJ1W8d/e1m4vcW/gm1IklxEGB34XfjQ+MKM2b32bIub/SN13L4V01al+pcaum2j08qc1mCD3U4zBD2VvjOrPctqTTadfkCgYEA6dyPLj/hpMbBwH09u6YD2UN+1/WAf5+lbMiZ3IjuW5Sdmf5kaoL/t9lmOAb69YituqRcSDJpJrA74J0mMXQvInD5fH1sTZ+88F1y7Zo+gjSmvBn1GnXn5VfMyzrwf5YLGsf9RETg/4cOzeIg4E1pHue95mzob8xky2PKGOu8/u0CgYEAzVDmUAcSMDPu+LtgitKaZgsqpodVCSjjxeT67PIS0Uk45YxSCCQgRjQRIRIFloeizPyrta4You/eEJDrza6u67g1ksd5T0xgPIjXRsVRFAa5qIYIc0KEma2umqvBdkMnPLeAVJRnOmmVHIiVJLUYR8MMkUxkLEiYrVzFss3Ka0kCgYEA0XUUYK4ioXzLSGZkBk+5Hr0PPMnMH4KTnY1GEXorUqcXSTfKJIPUGYyDuya1W3jhcUuIw7ky6M3rs0/NR3nyRXy+V7vWZuftR7PLHfiKiAA0XkE5gEueOZGcAWJ2yS9QHtqEgsLWasdCgTBJldx/jIivU1S0En4UwP5NomhxzDkCgYEAoPe9KZ7xlpMQ1zdosE3/OOOmU5skgyLouL0WMXB0alrC3c0Of017dC7cAxZzBRpf++BY6v6MWCpA6rID/WTnxOzOK75yEEar6KnMRbLrJw1Cv6ods+fBuA6gJqlj6skpWQPw+97Bs90VR6KZc4b7ez+jecLvgnyEHt7uLIoFGekCgYEA0/Op9ts9VxYDQKcQ90Dqpayck16vAfNQ31FgTv/LubdZ1S9JBRnCgz7HQI38oaBRfB8in0NLIT/F/IG8lSKvQDmmvjTJIGJngsj6xQ6KcxcPDrrSOVWOx3seXtvvcuSof+C+d1kNU8K3n+ArctHFT5BZxQo84kuR6HaWUhAED6s=" ], - "keyUse": ["ENC"], + "keyUse": [ + "ENC" + ], "certificate": [ "MIICnzCCAYcCBgGJ4F1P2DANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhjcnVjaWJsZTAeFw0yMzA4MTAxNjUwNTlaFw0zMzA4MTAxNjUyMzlaMBMxETAPBgNVBAMMCGNydWNpYmxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu4+F+guhmMR46OIlH6mWrKLTTZZ3z4EAKx28ulfWh2FNNz8eOwNYrGKmqSGVk6oXaZMYrDjk7QU4cvtUxJFJVMxFgNKqYZ/O57af7Jya5a20r8t2O+xDP++e6poTGalVhIbf+zvUEnIM1pLx4RvDawyyp3Tigq7ibCcJvC48aT/5KvMcmwHAqKGK0LfSxoK+3NusTk+B1LVs6AnegjL3ARS27czrrCNeAHkfQuyjwnt4PzxGoW/cYQ0FKqfl561wXHSx5j5hiscQqS/T1IWanq1jFM8ETCgldpT9+og0wwGRE6w0m2Z1pKek/W+q5Z7Fg0mjpzOH9vpBHwiyJ3PAlQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCKuDB2NHDzQkjc1nJw4b48fwYW24fMiDknfx+4L82hNytmAOmvUAJ3pfzxY5JybVckRbi1Z0MWDwx9/qybGQSUC2d/6HXlVFZT7iXnM+5rH4QL/LGaFdSHA/PYn12WUywt9urzE8Vx4Dm4iJB//7KwnAQGfWaVhzKMORpZwUCrrJxna5LAZg+Sa1Bt4T5zBOsw2h+ycGa75pKqJ2mm718jzK4evZVSlC1i6Es9FE2+aKLcwH9XC5U/DkV4mfEIVCm4JJ7xceOz+LafyS/yb6a/h28Pk303eK6ORdk8hRwLeOpjyusxvNCMn2z4dBAKFFcJz0QGj7rif2aRI1f2H0xf" ], - "priority": ["100"], - "algorithm": ["RSA-OAEP"] + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] } }, { @@ -3712,11 +3880,15 @@ "privateKey": [ "MIIEpAIBAAKCAQEA0v2rexBsiISp9Nou1PYTgESRU9wqmeOV2gb+Eg71jrOX2IcJO0VN232x21IVtWOxV4pKz12QtJUf37+M0m2pzVPM2hfx6dNu89znpOP5cOMLfuVxZzAapkQwrKzitMbLwTA5QxAsm6BFe586kVFbvd5E+DItFJOoWFL9eaRZEVUB7sO3KN//UJYd29zXEfK53UGWqOHhaLZD/oIh/8y5Sc5T7wPZaZKhtVMB62S8XpVbmTsM5fchIWe01rsdrm4W0+LqNgBB2TCCcmoTtbAbu+Ms16dGZkRLi5hXSQOG/uC/nsGHzGXwboyI6YoiTYYPxCTD0JilfflOMIFzQOls8wIDAQABAoIBADvkC8gcCmLaHZBGWqLS0whG7mW6ilEye/fuojIoEuYV7pVlB2bOrmIOlOznqQfbK5zowYGJUqkf53NQ4T1eKaQCFJUX1PCFtun8G7j/iW/U0w+LO5yJ2Ba4JoTtclDUWUQWVzrFHZRzcyW7NhnH2V3U+Qjm3UsI+vFPZWohD7NORRI7F6tk9ijKxQ+cqEu6AWXKTErjup2PBzYMgTtaITAvoZp6WGDKfvKIMinx506CUhGGi8+AIf3WFCK5LgNgajSL0PY2rSLJxQN2jtsuGutiwxuo8HvS36CN7nTatU4bzqHk0fRtVippvKSesrN39RvOZkwOk3R5189dVs9Ba8kCgYEA6rdEP2RqN/hx093sMUMAKFemzRuYjeCYnVo/d8IGOZNg8dH8bd3TEcCRsT4tCWQRaKIdw6yfCOlpPHSKjLMSGY/GcGejASIwFnR/VljBO2Oc1NzARleNW9xrFTRzFdJ1tUIbsZFPUbZzphJA8CUlLDN1KS7zds9ESY1e2iMvIoUCgYEA5h+lwMAPwlavHT+8fnIt0pLxhe06t/wSoaGp18BrYN6Ot0CoUXrylCqn5wCVBjbrJnnEy/f+LAkKLnJ+DzS10IdjuXOLkXka36bXgsj8m9IpjGQJlIfSeuMMhSDBIIkHk/Nhyiiod47iOcsPrvEzX2RTmFc9dPzhaGrA3Q+T9xcCgYBKrSI+lQEia54zjTDff3SmYTTFnxkLUsDbl2IIBSgb22MFrQyGHARSapUv8hs8GKVdR+72WY6DtFdyD5YhK7v/e/nju2VmL+1ix4/X9gcMkSXNp6pY9vQXnOpI5dYTxFEE6VKMTTISSl1DIh2dCgRoqrqE7tYxH8KXMC5UcstFTQKBgQDCVDFV3xqvwyHnsj4MDYGSlBIvRZDTc6OMKIfTsSM8T1T3fTtma0vUQV0+Xqh58gCLokLE9+wE5bFaXccEMj/jE6HsJp3SwBEokqzlPbLMJyJ88rGAY14j9f5JocpVHkJu9xU6cTEqnVd+9HKPChKW2JMbT3iPhUkYMyYgx5ntCwKBgQC37H1HbnTAzlTt6YppWak7dxRgaluB6iniT1cYDSsGNpZn2mlfbc3QQqRqU2eMU03RkcOFn0gTaRxvuMNKPEJpqnC5dTHPn6c2b9I+7cUug3BxpQdI5mWQGJFl8AjbyLVCJ/Q5X8saAdu3p4XTmvfvzIayPnF9iTw1D8UufudeBA==" ], - "keyUse": ["SIG"], + "keyUse": [ + "SIG" + ], "certificate": [ "MIICnzCCAYcCBgGJ4F1PTzANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhjcnVjaWJsZTAeFw0yMzA4MTAxNjUwNThaFw0zMzA4MTAxNjUyMzhaMBMxETAPBgNVBAMMCGNydWNpYmxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0v2rexBsiISp9Nou1PYTgESRU9wqmeOV2gb+Eg71jrOX2IcJO0VN232x21IVtWOxV4pKz12QtJUf37+M0m2pzVPM2hfx6dNu89znpOP5cOMLfuVxZzAapkQwrKzitMbLwTA5QxAsm6BFe586kVFbvd5E+DItFJOoWFL9eaRZEVUB7sO3KN//UJYd29zXEfK53UGWqOHhaLZD/oIh/8y5Sc5T7wPZaZKhtVMB62S8XpVbmTsM5fchIWe01rsdrm4W0+LqNgBB2TCCcmoTtbAbu+Ms16dGZkRLi5hXSQOG/uC/nsGHzGXwboyI6YoiTYYPxCTD0JilfflOMIFzQOls8wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAaCb8NDHYcHsRc0lec3Tme3sbIqtM09Ht13TOh2+KZ1utt0XZk+gEPALH3vdCgUVErYMmgYImDzJ8b+tU+x4WnSc/QxV99381ljJ/+RAm7gyrqSidTyarBfcY5cH57QYiW8/iZNcDT99dDO5hZIuuyG8VESVEGLAUPYVQSN8KIWvEZLVcRTL9KdakaQSXDiBYnhtumRVYD2rLMwt0jh1Woi+anTRH3VmE3As6HZ8xiX8FXchdO9Pszu61zjxe9hvNJVs8U2VC2120Q31RGCvFXBnfnfj4wa3Z37nEIhqlW8xSnzuVE/E3TLWRgEn80Q2z0u3tXOYOaBnr5f397hwMe" ], - "priority": ["100"] + "priority": [ + "100" + ] } }, { @@ -3725,9 +3897,15 @@ "providerId": "aes-generated", "subComponents": {}, "config": { - "kid": ["9629dd0b-deb7-4100-b1cb-edc386c8c8aa"], - "secret": ["CXJ2dbXimkxrm0kCMharwA"], - "priority": ["100"] + "kid": [ + "9629dd0b-deb7-4100-b1cb-edc386c8c8aa" + ], + "secret": [ + "CXJ2dbXimkxrm0kCMharwA" + ], + "priority": [ + "100" + ] } }, { @@ -3736,12 +3914,18 @@ "providerId": "hmac-generated", "subComponents": {}, "config": { - "kid": ["2fc08b5a-5806-4354-8266-bd36550fd82e"], + "kid": [ + "2fc08b5a-5806-4354-8266-bd36550fd82e" + ], "secret": [ "UEXn-42kJ86OwlSQKxCv0iwt0n3aAw8DaEvYZSyiikCAMxa2HiRqYSGTSkkn5WjiGrzHQUkHLYV19RH3hjDhJA" ], - "priority": ["100"], - "algorithm": ["HS256"] + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] } } ] @@ -4404,7 +4588,9 @@ "firstName": "Admin", "lastName": "User", "attributes": { - "terms_and_conditions": ["1697642726"] + "terms_and_conditions": [ + "1697642726" + ] }, "credentials": [ { @@ -4418,11 +4604,18 @@ ], "disableableCredentialTypes": [], "requiredActions": [], - "realmRoles": ["Administrator", "default-roles-crucible"], + "realmRoles": [ + "Administrator", + "default-roles-crucible" + ], "clientConsents": [ { "clientId": "player.ui", - "grantedClientScopes": ["email", "profile", "roles"], + "grantedClientScopes": [ + "email", + "profile", + "roles" + ], "createdDate": 1693252224685, "lastUpdatedDate": 1693252224733 } @@ -4440,7 +4633,9 @@ "firstName": "OG", "lastName": "Admin", "attributes": { - "terms_and_conditions": ["1730729259"] + "terms_and_conditions": [ + "1730729259" + ] }, "credentials": [ { @@ -4454,7 +4649,9 @@ ], "disableableCredentialTypes": [], "requiredActions": [], - "realmRoles": ["default-roles-crucible"], + "realmRoles": [ + "default-roles-crucible" + ], "notBefore": 0, "groups": [] }, @@ -4468,7 +4665,9 @@ "firstName": "Content", "lastName": "Developer", "attributes": { - "terms_and_conditions": ["1733933674"] + "terms_and_conditions": [ + "1733933674" + ] }, "credentials": [ { @@ -4482,7 +4681,10 @@ ], "disableableCredentialTypes": [], "requiredActions": [], - "realmRoles": ["Content Developer", "default-roles-crucible"], + "realmRoles": [ + "Content Developer", + "default-roles-crucible" + ], "notBefore": 0, "groups": [] }, @@ -4496,7 +4698,9 @@ "firstName": "Crucible", "lastName": "Admin", "attributes": { - "terms_and_conditions": ["1705939925"] + "terms_and_conditions": [ + "1705939925" + ] }, "credentials": [ { @@ -4510,7 +4714,9 @@ ], "disableableCredentialTypes": [], "requiredActions": [], - "realmRoles": ["default-roles-crucible"], + "realmRoles": [ + "default-roles-crucible" + ], "notBefore": 0, "groups": [] }, @@ -4524,7 +4730,9 @@ "firstName": "Demo", "lastName": "User", "attributes": { - "terms_and_conditions": ["1742337017"] + "terms_and_conditions": [ + "1742337017" + ] }, "credentials": [ { @@ -4538,9 +4746,13 @@ ], "disableableCredentialTypes": [], "requiredActions": [], - "realmRoles": ["default-roles-crucible"], + "realmRoles": [ + "default-roles-crucible" + ], "notBefore": 0, - "groups": ["/White Cell"] + "groups": [ + "/White Cell" + ] }, { "id": "987b237f-764e-460f-a80d-b761321d9e98", @@ -4552,7 +4764,9 @@ "firstName": "Rangetech", "lastName": "Admin", "attributes": { - "terms_and_conditions": ["1733932342"] + "terms_and_conditions": [ + "1733932342" + ] }, "credentials": [ { @@ -4566,7 +4780,9 @@ ], "disableableCredentialTypes": [], "requiredActions": [], - "realmRoles": ["default-roles-crucible"], + "realmRoles": [ + "default-roles-crucible" + ], "notBefore": 0, "groups": [] }, @@ -4580,7 +4796,9 @@ "firstName": "Read", "lastName": "Only", "attributes": { - "terms_and_conditions": ["1733933101"] + "terms_and_conditions": [ + "1733933101" + ] }, "credentials": [ { @@ -4594,7 +4812,9 @@ ], "disableableCredentialTypes": [], "requiredActions": [], - "realmRoles": ["default-roles-crucible"], + "realmRoles": [ + "default-roles-crucible" + ], "notBefore": 0, "groups": [] }, @@ -4608,8 +4828,12 @@ "serviceAccountClientId": "player.vm.webhooks", "credentials": [], "disableableCredentialTypes": [], - "requiredActions": ["TERMS_AND_CONDITIONS"], - "realmRoles": ["default-roles-crucible"], + "requiredActions": [ + "TERMS_AND_CONDITIONS" + ], + "realmRoles": [ + "default-roles-crucible" + ], "notBefore": 0, "groups": [] }, @@ -4623,21 +4847,25 @@ "firstName": "User1", "lastName": "User", "attributes": { - "terms_and_conditions": ["1703175332"] + "terms_and_conditions": [ + "1703175332" + ] }, "credentials": [ { - "id": "1c0b51a9-032b-45e9-bf6f-f75dc00da35f", + "id": "cbb61d2d-8b35-48f1-b916-c9ea4991ba8f", "type": "password", "userLabel": "My password", - "createdDate": 1697743646353, - "secretData": "{\"value\":\"xCdS3sYG25gkMyZ7H6kc63tsRHVoqkBHG7tCVj8IQqg=\",\"salt\":\"2ElAs9aJaR5c6UOwZQVEiw==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + "createdDate": 1776091609463, + "secretData": "{\"value\": \"KhQBwFavoB+5L2NLJmNkop95qBOTxzWnlfPHJv4zmx4=\", \"salt\": \"3xXxf8thkK4kEAVnbNVEzg==\", \"additionalParameters\": {}}", + "credentialData": "{\"hashIterations\": 27500, \"algorithm\": \"pbkdf2-sha256\", \"additionalParameters\": {}}" } ], "disableableCredentialTypes": [], "requiredActions": [], - "realmRoles": ["Administrator", "default-roles-crucible"], + "realmRoles": [ + "default-roles-crucible" + ], "notBefore": 0, "groups": [] }, @@ -4650,7 +4878,9 @@ "emailVerified": false, "firstName": "user2", "attributes": { - "terms_and_conditions": ["1710960650"] + "terms_and_conditions": [ + "1710960650" + ] }, "credentials": [ { @@ -4664,9 +4894,11 @@ ], "disableableCredentialTypes": [], "requiredActions": [], - "realmRoles": ["default-roles-crucible"], + "realmRoles": [ + "default-roles-crucible" + ], "notBefore": 0, "groups": [] } ] -} +} \ No newline at end of file diff --git a/README.md b/README.md index 18ba766..477777f 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Development Environment for [Crucible](https://github.com/cmu-sei/crucible) - a - [Launch Profiles](#launch-profiles) - [Default Credentials](#default-credentials) - [Claude Code](#claude-code) +- [Playwright Testing](#playwright-testing) - [GitHub CLI](#github-cli) - [Memory Optimization](#memory-optimization) - [Intelephense PHP Extension](#intelephense-php-extension) @@ -144,6 +145,80 @@ The config file is mounted to `/home/vscode/.aws/config` inside the container an Once the container is running with valid credentials, run `claude` in the terminal to start Claude Code. +## Playwright Testing + +The dev container includes [Playwright](https://playwright.dev/) for end-to-end testing of Crucible applications. Dependencies (Node.js packages and Chromium browser) are installed automatically during container creation. + +The test suite lives in `/mnt/data/crucible/crucible-tests/` and covers all 11 Crucible applications. Each app has a test plan and organized spec files. See the [crucible-tests README](https://github.com/cmu-sei/crucible-tests) for full documentation. + +### VS Code Playwright Extension + +The [Playwright Test for VS Code](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) extension is pre-installed and configured to use the Crucible test suite. Open the **Testing** panel in VS Code to browse, run, and debug tests visually. + +### Claude Code Playwright Test Agents + +The dev container automatically initializes three [Playwright test agents](https://playwright.dev/docs/test-agents) for use with Claude Code during container creation. These agents allow Claude Code to plan, generate, and fix Playwright tests interactively using a real browser. + +| Agent | Purpose | +|-------|---------| +| **playwright-test-planner** | Navigates a running application in a browser, explores the UI, and produces a comprehensive test plan (saved as a markdown file) | +| **playwright-test-generator** | Takes a test plan and generates `.spec.ts` files by executing each step in a real browser, then recording the actions | +| **playwright-test-healer** | Runs failing tests, debugs them in a live browser, identifies root causes, and fixes the test code | + +To use the agents, start Claude Code in the terminal and ask it to plan, generate, or fix tests. Claude Code will automatically delegate to the appropriate agent. For example: + +- *"Create a test plan for the Player application"* — invokes the **planner** to explore the Player UI and produce a test plan +- *"Generate tests for the Blueprint authentication section"* — invokes the **generator** to create spec files from the test plan +- *"Fix the failing Blueprint tests"* — invokes the **healer** to debug and repair broken tests + +The agents require Crucible services to be running since they interact with the applications through a real browser. + +### Running Tests from the Terminal + +Start the Crucible services first (via a VS Code launch profile or `aspire run`), then: + +```bash +cd /mnt/data/crucible/libraries/crucible-tests + +# Run tests for a specific application +./run-tests.sh topomojo +./run-tests.sh blueprint +./run-tests.sh player + +# Run all tests +./run-tests.sh all + +# Smoke tests (login/home) for a specific app or all apps +./run-tests.sh quick --app cite +./run-tests.sh quick + +# Interactive UI mode +./run-tests.sh ui gameboard + +# Headed mode (see browser) +./run-tests.sh headed caster + +# Filter tests by pattern +./run-tests.sh alloy --filter login + +# Skip health checks +./run-tests.sh topomojo --no-check + +# View test report +./run-tests.sh report +``` + +The script automatically checks that Keycloak and the target application are reachable before running tests. Use `--no-check` to skip these checks. + +### Configuring Service URLs + +All service URLs used by the test suite are defined in a single file: + +``` +/mnt/data/crucible/crucible-tests/.env +``` + +Edit this file to change ports or hostnames for your environment. The `.env` file is loaded by both the shell scripts (`run-tests.sh`, `setup.sh`) and the Playwright TypeScript configuration. If the file is missing, all URLs fall back to their default `localhost` values. ## GitHub CLI The dev container includes the [GitHub CLI](https://cli.github.com/) (`gh`). The GitHub CLI's authentication is reused by the GitHub MCP server for agentic development. diff --git a/aspire.config.json b/aspire.config.json new file mode 100644 index 0000000..21dce60 --- /dev/null +++ b/aspire.config.json @@ -0,0 +1,5 @@ +{ + "appHost": { + "path": "Crucible.AppHost/Crucible.AppHost.csproj" + } +} \ No newline at end of file diff --git a/scripts/repos.json b/scripts/repos.json index 0006748..523ea82 100644 --- a/scripts/repos.json +++ b/scripts/repos.json @@ -44,6 +44,10 @@ { "name": "Crucible-Github-Actions", "url": "https://github.com/cmu-sei/Crucible-Github-Actions" + }, + { + "name": "crucible-tests", + "url": "https://github.com/cmu-sei/crucible-tests.git" } ] },