diff --git a/build-progress.txt b/build-progress.txt index 538f30f6..e94d842b 100644 --- a/build-progress.txt +++ b/build-progress.txt @@ -10786,3 +10786,9 @@ Blockers: full make test Cargo passed but broad web Vitest still has unrelated u Status: QA PASS. Fresh controller recovery accepted for Network/Forks and ready for main merge. Evidence: make doctor healthy; make check passed; DB-backed package_detail_contract 4/4, projects_list_contract 1/1, and repository_network_contract 1/1 passed against localhost:55433; focused Network/Forks/API docs Vitest passed 9/9; focused system-Chrome repository-network Playwright passed setup + flow 2/2 in 21.0s. Fixes accepted: package settings camelCase serde IDs, project copy FOR UPDATE OF projects + workflow_key clone + org membership/base-role guard, corrected projects default-open expectation, repository_network_contract rate-limit identity isolation, and repository-network E2E now uses shared auth plus container-backed psql fallback instead of host psql. + +2026-05-24 - ai-001 QA/fix final lane +- make doctor passed after restarting postgres-test; host localhost:55433 became reachable. +- Fixed /api/ai/* routes to require authenticated users before AI content/cache access, aligning per-user settings/rate-limit/privacy requirements. +- Added DB-backed ai-001 contract coverage for auth, opt-in, provider-missing, cache hit/miss, PR files/reviewers/inline seed, and changelog previousTag/targetTag cache key. Focused AI API tests passed (3/3). +- Focused AI UI unit tests passed (3 files, 61 tests). make check passed. make test remains red from unrelated web unit timeouts; make test-e2e is blocked by missing/unsupported Playwright Chromium on ubuntu26.04-x64, so ai-001 qa_pass remains false. diff --git a/crates/api/src/routes/ai.rs b/crates/api/src/routes/ai.rs index e02a8dc6..a1bc4564 100644 --- a/crates/api/src/routes/ai.rs +++ b/crates/api/src/routes/ai.rs @@ -45,12 +45,12 @@ async fn repo_summary( Query(query): Query, ) -> Result, (StatusCode, Json)> { let pool = state.db.as_ref().ok_or_else(database_unavailable)?; - let actor = AuthenticatedUser::optional_from_headers(&state, &headers).await?; + let actor = AuthenticatedUser::from_headers(&state, &headers).await?.0; let summary = repository_ai_summary( pool, &owner, &repo, - actor.as_ref().map(|user| user.id), + Some(actor.id), query.regenerate.unwrap_or(false), ) .await @@ -64,16 +64,10 @@ async fn regenerate_repo_summary( Path((owner, repo)): Path<(String, String)>, ) -> Result, (StatusCode, Json)> { let pool = state.db.as_ref().ok_or_else(database_unavailable)?; - let actor = AuthenticatedUser::optional_from_headers(&state, &headers).await?; - let summary = repository_ai_summary( - pool, - &owner, - &repo, - actor.as_ref().map(|user| user.id), - true, - ) - .await - .map_err(map_ai_error)?; + let actor = AuthenticatedUser::from_headers(&state, &headers).await?.0; + let summary = repository_ai_summary(pool, &owner, &repo, Some(actor.id), true) + .await + .map_err(map_ai_error)?; Ok(Json(json!(summary))) } @@ -84,13 +78,13 @@ async fn pr_summary( Query(query): Query, ) -> Result, (StatusCode, Json)> { let pool = state.db.as_ref().ok_or_else(database_unavailable)?; - let actor = AuthenticatedUser::optional_from_headers(&state, &headers).await?; + let actor = AuthenticatedUser::from_headers(&state, &headers).await?.0; let summary = pull_request_ai_summary( pool, &owner, &repo, number, - actor.as_ref().map(|user| user.id), + Some(actor.id), query.regenerate.unwrap_or(false), ) .await @@ -104,17 +98,10 @@ async fn regenerate_pr_summary( Path((owner, repo, number)): Path<(String, String, i64)>, ) -> Result, (StatusCode, Json)> { let pool = state.db.as_ref().ok_or_else(database_unavailable)?; - let actor = AuthenticatedUser::optional_from_headers(&state, &headers).await?; - let summary = pull_request_ai_summary( - pool, - &owner, - &repo, - number, - actor.as_ref().map(|user| user.id), - true, - ) - .await - .map_err(map_ai_error)?; + let actor = AuthenticatedUser::from_headers(&state, &headers).await?.0; + let summary = pull_request_ai_summary(pool, &owner, &repo, number, Some(actor.id), true) + .await + .map_err(map_ai_error)?; Ok(Json(json!(summary))) } @@ -126,12 +113,12 @@ async fn changelog( RestJson(request): RestJson, ) -> Result, (StatusCode, Json)> { let pool = state.db.as_ref().ok_or_else(database_unavailable)?; - let actor = AuthenticatedUser::optional_from_headers(&state, &headers).await?; + let actor = AuthenticatedUser::from_headers(&state, &headers).await?.0; let changelog = ai_changelog( pool, &owner, &repo, - actor.as_ref().map(|user| user.id), + Some(actor.id), request, query.regenerate.unwrap_or(false), ) diff --git a/crates/api/tests/api_ai_surfaces_contract.rs b/crates/api/tests/api_ai_surfaces_contract.rs index 2eabf2f9..666ff149 100644 --- a/crates/api/tests/api_ai_surfaces_contract.rs +++ b/crates/api/tests/api_ai_surfaces_contract.rs @@ -23,6 +23,7 @@ use uuid::Uuid; static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("./migrations"); async fn database_pool() -> Option { + let _ = dotenvy::from_filename(".env.test"); let database_url = std::env::var("TEST_DATABASE_URL") .or_else(|_| std::env::var("DATABASE_URL")) .ok() @@ -250,7 +251,7 @@ async fn repository_ai_summary_respects_repository_and_user_opt_in() { let (status, body) = send_json( app.clone(), &format!("/api/ai/repos/{owner_login}/{public_repo}/summary"), - None, + Some(&reader_cookie), ) .await; assert_eq!(status, StatusCode::OK); @@ -384,3 +385,191 @@ async fn ai_changelog_uses_previous_and_target_tag_range_for_cache_key() { "### Added\n- Generated from only the selected tag range." ); } + +async fn seed_pull_request(pool: &PgPool, repository_id: Uuid, author_id: Uuid) -> (Uuid, i64) { + let issue_id: Uuid = sqlx::query_scalar( + r#" + INSERT INTO issues (repository_id, number, title, body, state, author_user_id) + VALUES ($1, 42, 'Improve AI surfaces', 'body', 'open', $2) + RETURNING id + "#, + ) + .bind(repository_id) + .bind(author_id) + .fetch_one(pool) + .await + .expect("issue should persist"); + let pull_request_id: Uuid = sqlx::query_scalar( + r#" + INSERT INTO pull_requests (repository_id, issue_id, number, title, body, author_user_id, head_ref, base_ref, head_repository_id, base_repository_id) + VALUES ($1, $2, 42, 'Improve AI surfaces', 'Adds AI summary UI', $3, 'ai', 'main', $1, $1) + RETURNING id + "#, + ) + .bind(repository_id) + .bind(issue_id) + .bind(author_id) + .fetch_one(pool) + .await + .expect("pull request should persist"); + let (commit_id, _) = seed_commit(pool, repository_id, author_id, "Wire PR AI summary", 1).await; + sqlx::query("INSERT INTO pull_request_commits (pull_request_id, commit_id, position) VALUES ($1, $2, 0)") + .bind(pull_request_id) + .bind(commit_id) + .execute(pool) + .await + .expect("pull commit should persist"); + sqlx::query( + r#" + INSERT INTO pull_request_files (pull_request_id, path, status, additions, deletions, byte_size) + VALUES ($1, 'web/src/components/AiSummary.tsx', 'added', 80, 3, 4096) + "#, + ) + .bind(pull_request_id) + .execute(pool) + .await + .expect("pull file should persist"); + (pull_request_id, 42) +} + +#[tokio::test] +async fn ai_endpoints_require_auth_and_return_cached_shapes_without_provider() { + let Some(pool) = database_pool().await else { + panic!("ai-001 contract requires TEST_DATABASE_URL/DATABASE_URL and reachable Postgres"); + }; + + std::env::remove_var("OPENAI_API_KEY"); + let config = app_config(); + let owner = create_user(&pool, "ai-shape-owner").await; + let owner_login = owner.username.as_deref().unwrap_or(&owner.email); + let owner_cookie = cookie_header(&pool, &config, &owner).await; + let repo_name = create_repo(&pool, &owner, RepositoryVisibility::Public, true).await; + let repository_id = sqlx::query_scalar::<_, Uuid>( + "SELECT id FROM repositories WHERE owner_user_id = $1 AND name = $2", + ) + .bind(owner.id) + .bind(&repo_name) + .fetch_one(&pool) + .await + .expect("repository should exist"); + + let (repo_commit_id, _) = seed_commit( + &pool, + repository_id, + owner.id, + "Summarize repository with AI", + 1, + ) + .await; + sqlx::query("INSERT INTO repository_files (repository_id, commit_id, oid, path, content, byte_size) VALUES ($1, $2, '1111111111111111111111111111111111111111', 'README.md', 'AI repo readme', 14)") + .bind(repository_id) + .bind(repo_commit_id) + .execute(&pool) + .await + .expect("repository file should persist"); + let repo_context = "Repository: ".to_owned() + owner_login + "/" + &repo_name + "\nDescription: AI surface contract repository\nFiles:\n## README.md\nAI repo readme\nRecent commits:\nSummarize repository with AI"; + let repo_hash = hash_content(&repo_context); + sqlx::query( + r#" + INSERT INTO ai_outputs (kind, scope_type, scope_id, content_hash, prompt_version, model, output, created_by_user_id) + VALUES ('repo_summary', 'repository', $1, $2, 'ai-001-v1', 'gpt-4o-mini', '- Cached repository summary', $3) + "#, + ) + .bind(repository_id) + .bind(repo_hash) + .bind(owner.id) + .execute(&pool) + .await + .expect("repo cache should persist"); + + let (pull_request_id, pr_number) = seed_pull_request(&pool, repository_id, owner.id).await; + let pr_context = "Title: Improve AI surfaces\nBody: Adds AI summary UI\nBase: main\nHead: ai\nFiles:\n- added web/src/components/AiSummary.tsx +80 -3\nCommits:\nWire PR AI summary"; + let pr_hash = hash_content(pr_context); + sqlx::query( + r#" + INSERT INTO ai_outputs (kind, scope_type, scope_id, content_hash, prompt_version, model, output, created_by_user_id) + VALUES ('pr_summary', 'pull_request', $1, $2, 'ai-001-v1', 'gpt-4o', 'TL;DR cached PR summary', $3) + "#, + ) + .bind(pull_request_id) + .bind(pr_hash) + .bind(owner.id) + .execute(&pool) + .await + .expect("pr cache should persist"); + + let (previous_commit_id, _) = + seed_commit(&pool, repository_id, owner.id, "Previous release", 4).await; + let (target_commit_id, target_oid) = + seed_commit(&pool, repository_id, owner.id, "Ship AI changelog", 2).await; + seed_release(&pool, repository_id, owner.id, "v1.0.0", previous_commit_id).await; + let release_id = seed_release(&pool, repository_id, owner.id, "v1.1.0", target_commit_id).await; + let changelog_context = format!("{target_oid} Ship AI changelog"); + let changelog_hash = hash_content(&format!("{}:{}:{changelog_context}", "v1.0.0", "v1.1.0")); + sqlx::query( + r#" + INSERT INTO ai_outputs (kind, scope_type, scope_id, content_hash, prompt_version, model, output, created_by_user_id) + VALUES ('changelog', 'release', $1, $2, 'ai-001-v1', 'gpt-4o', '### Added\n- Cached changelog', $3) + "#, + ) + .bind(release_id) + .bind(changelog_hash) + .bind(owner.id) + .execute(&pool) + .await + .expect("changelog cache should persist"); + + let app = opengithub_api::build_app_with_config(Some(pool.clone()), config); + let (status, body) = send_json( + app.clone(), + &format!("/api/ai/repos/{owner_login}/{repo_name}/summary"), + None, + ) + .await; + assert_eq!(status, StatusCode::UNAUTHORIZED); + assert_eq!(body["error"]["code"], "not_authenticated"); + + let (status, body) = send_json( + app.clone(), + &format!("/api/ai/repos/{owner_login}/{repo_name}/summary"), + Some(&owner_cookie), + ) + .await; + assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE); + assert_eq!(body["error"]["code"], "ai_provider_not_configured"); + + let (status, body) = post_json( + app.clone(), + &format!("/api/ai/repos/{owner_login}/{repo_name}/summary"), + Some(&owner_cookie), + json!({}), + ) + .await; + assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE); + assert_eq!(body["error"]["code"], "ai_provider_not_configured"); + + let (status, body) = send_json( + app.clone(), + &format!("/api/ai/repos/{owner_login}/{repo_name}/pulls/{pr_number}/summary"), + Some(&owner_cookie), + ) + .await; + assert_eq!(status, StatusCode::OK); + assert_eq!(body["output"]["cached"], true); + assert_eq!( + body["filesOfInterest"][0]["path"], + "web/src/components/AiSummary.tsx" + ); + assert!(body["inlineCommentSeed"].as_str().is_some()); + + let (status, body) = post_json( + app, + &format!("/api/ai/repos/{owner_login}/{repo_name}/releases/changelog"), + Some(&owner_cookie), + json!({ "previousTag": "v1.0.0", "targetTag": "v1.1.0" }), + ) + .await; + assert_eq!(status, StatusCode::OK); + assert_eq!(body["output"]["cached"], true); + assert_eq!(body["output"]["output"], "### Added\\n- Cached changelog"); +} diff --git a/qa-hints.json b/qa-hints.json index 91907b17..4795e316 100644 --- a/qa-hints.json +++ b/qa-hints.json @@ -8068,16 +8068,16 @@ { "feature_id": "ai-001", "tests_written": [ + "DB-backed AI API contract for auth, opt-in, cache hit/miss/provider-missing, PR summary, changelog tag range", "repository AI summary card renders cached output and regenerate POST action", - "pull request AI brief renders TL;DR, files of interest, suggested reviewers, and regenerate POST action", - "release detail renders AI changelog output and generate changelog POST action", - "API docs list repository summary, PR summary, and release changelog AI endpoints" + "pull request AI brief renders TL;DR, files of interest, suggested reviewers, author-only inline seed, and regenerate POST action", + "release detail renders AI changelog output and generate changelog POST action" ], "needs_deeper_qa": [ - "Live OpenAI provider behavior with OPENAI_API_KEY, prompt quality, and provider failure handling", - "Private repository opt-in enforcement against real private repositories and user AI setting toggles", - "Rate-limit bucket behavior for repeated AI generation under real signed sessions", - "Full signed-session E2E after local TEST_DATABASE_URL credentials are healthy" + "Run full make test after unrelated api-docs and repository-code-scanning-alerts-page vitest timeouts are fixed or deflaked", + "Run Playwright E2E and axe/a11y scans once a supported Chromium browser is available for ubuntu26.04-x64 or the runner OS is supported", + "Live OpenAI provider behavior with a real OPENAI_API_KEY and prompt quality review", + "Repeated signed-session rate-limit exhaustion probe through a browser/API scenario" ] }, { diff --git a/qa-report-summary.json b/qa-report-summary.json index 38a50f82..5d8f05bc 100644 --- a/qa-report-summary.json +++ b/qa-report-summary.json @@ -3763,42 +3763,53 @@ "description": "AI surface \u2014 repository auto-summary card, AI-generated PR summary + suggested r", "category": "feature", "qa_pass": false, - "attempts": 2, + "attempts": 4, "exhausted": false, "sub_phases": { "functional": { "status": "partial", - "notes": "Repository, PR, and release AI UI unit coverage passed; PR inline-comment seed author-only unit coverage passed; focused AI Rust contract compiled and ran but DB-backed assertions self-skipped because no TEST_DATABASE_URL/DATABASE_URL was available. Live no-DB API probes returned safe JSON 503 envelopes with rate-limit headers. make test-e2e timed out in db-wait-test before Playwright started; Ever CLI returned a non-JSON HTML parse error, so signed-session browser QA and live OpenAI provider behavior remain unverified. Final make check && make test passed after the fix." + "notes": "Focused React UI coverage passed for repository summary card/regenerate, PR AI tab content/author-only inline seed rendering, and release changelog edit-before-publish form: cd web && npx vitest run tests/repository-code-overview.test.tsx tests/repository-pull-request-detail.test.tsx tests/repository-releases-page.test.tsx (61 passed). Full make test is red due unrelated web unit timeouts in api-docs and repository-code-scanning-alerts-page. Browser E2E is blocked before app tests because Playwright Chromium is unsupported/missing on ubuntu26.04-x64." }, "api_contract": { - "status": "partial", + "status": "pass", "endpoints_tested": [ "GET /api/ai/repos/:owner/:repo/summary", "POST /api/ai/repos/:owner/:repo/summary", "GET /api/ai/repos/:owner/:repo/pulls/:number/summary", + "POST /api/ai/repos/:owner/:repo/pulls/:number/summary", "POST /api/ai/repos/:owner/:repo/releases/changelog" ], - "notes": "Degraded live curl probes verified application/json database_unavailable envelopes and X-RateLimit/X-GitHub-Api-Version headers for AI endpoints without a DB pool. Focused contract test still self-skips DB-backed repository/user opt-in assertions without TEST_DATABASE_URL/DATABASE_URL, so provider-backed happy paths, private repo opt-in against real rows, and repeated signed rate-limit bucket behavior remain unverified." + "notes": "Added and ran DB-backed ai-001 contract coverage: auth required, repository/user/private opt-in, missing OPENAI_API_KEY provider failure, PR cached summary shape with files-of-interest and author-only inline seed, changelog cache hit, and previousTag/targetTag range cache key. Focused command passed: set -a; . ./.env.test; set +a; ./hack/cargo_locked.sh test -p opengithub-api --test api_ai_surfaces_contract -- --nocapture (3 passed)." }, "security": { "status": "partial", "checks": [ - "auth_bypass", - "input_sanitization", - "cors", - "data_exposure", + "auth_enforced", "private_repo_opt_in", - "author_only_inline_seed" - ], - "notes": "Fixed a major PR privacy issue: inline-comment seed is now returned only when the authenticated actor is the PR author. Anonymous SQL/XSS-shaped live probes returned generic 503 database_unavailable envelopes without reflecting payloads or leaking stack traces. Hostile-origin OPTIONS returned 405 without Access-Control-Allow-Origin. DB-backed private repository and signed-session curl probes remain blocked by unavailable local test Postgres." - }, - "accessibility": { - "status": "skip", - "violations": [], - "notes": "not applicable for this feature category; QA_MODULES included base, api, security, footer only" - } - }, - "overall_status": "partial" + "user_opt_in", + "missing_provider_no_secret_leak", + "author_only_inline_seed", + "changelog_range", + "no_stack_trace_error_envelopes" + ], + "notes": "Changed /api/ai/* routes to require authenticated session/PAT before any repository content can be summarized. Contract tests verify anonymous 401 JSON, provider-missing generic ai_provider_not_configured without key leakage, private repository opt-in gate, and author-only inline-comment seed. Full browser security/E2E remains blocked by Playwright browser install failure." + }, + "accessibility": { + "status": "blocked", + "notes": "AI surface component/unit rendering passed, but required axe/Playwright browser a11y scan could not run because npx playwright install chromium fails with: Playwright does not support chromium on ubuntu26.04-x64." + } + }, + "overall_status": "partial", + "status": "blocked", + "latest_evidence": [ + "make doctor passed after clean postgres-test restart; host TCP localhost:55433 became reachable.", + "make check passed (Cargo check, web typecheck, clippy, Biome).", + "Cargo portion of make test passed; web vitest full suite failed in unrelated api-docs and repository-code-scanning-alerts-page timeouts (111 files passed, 2 failed).", + "Focused AI UI vitest passed: 3 files, 61 tests.", + "Focused AI API contract passed: 3 tests.", + "make test-e2e failed in auth.setup before app tests: missing Chromium executable; installing with npx playwright install chromium failed because ubuntu26.04-x64 is unsupported." + ], + "blocker": "Full QA cannot pass: unrelated full web unit timeouts plus Playwright Chromium unsupported on ubuntu26.04-x64 block E2E/a11y." }, { "feature_id": "insights-002", diff --git a/qa-report.json b/qa-report.json index 3430d8e2..45fe3864 100644 --- a/qa-report.json +++ b/qa-report.json @@ -2242,15 +2242,16 @@ }, { "feature_id": "ai-001", - "attempt": 3, - "status": "partial", + "attempt": 4, + "status": "blocked", + "qa_pass": false, "sub_phases": { "functional": { "status": "partial", - "notes": "Repository, PR, and release AI UI unit coverage passed; added and ran focused AI changelog tag-range contract coverage, but DB-backed assertions self-skipped because the local test Postgres service on localhost:55433 did not become ready. make test-e2e timed out in db-wait-test before Playwright started, and Ever CLI still returned a non-JSON HTML parse error. Live OpenAI/provider, signed-session E2E, private-repo opt-in, and real rate-limit bucket behavior remain unverified in this environment." + "notes": "Focused React UI coverage passed for repository summary card/regenerate, PR AI tab content/author-only inline seed rendering, and release changelog edit-before-publish form: cd web && npx vitest run tests/repository-code-overview.test.tsx tests/repository-pull-request-detail.test.tsx tests/repository-releases-page.test.tsx (61 passed). Full make test is red due unrelated web unit timeouts in api-docs and repository-code-scanning-alerts-page. Browser E2E is blocked before app tests because Playwright Chromium is unsupported/missing on ubuntu26.04-x64." }, "api_contract": { - "status": "partial", + "status": "pass", "endpoints_tested": [ "GET /api/ai/repos/:owner/:repo/summary", "POST /api/ai/repos/:owner/:repo/summary", @@ -2258,56 +2259,48 @@ "POST /api/ai/repos/:owner/:repo/pulls/:number/summary", "POST /api/ai/repos/:owner/:repo/releases/changelog" ], - "notes": "Code inspection found and fixed the changelog compare-range contract. Live degraded curl probes verified application/json database_unavailable envelopes and rate-limit/version headers without a DB pool. Focused AI integration tests compile and run, but DB-backed provider/cache/private-repo/session assertions self-skip without a reachable TEST_DATABASE_URL/DATABASE_URL." + "notes": "Added and ran DB-backed ai-001 contract coverage: auth required, repository/user/private opt-in, missing OPENAI_API_KEY provider failure, PR cached summary shape with files-of-interest and author-only inline seed, changelog cache hit, and previousTag/targetTag range cache key. Focused command passed: set -a; . ./.env.test; set +a; ./hack/cargo_locked.sh test -p opengithub-api --test api_ai_surfaces_contract -- --nocapture (3 passed)." }, "security": { "status": "partial", "checks": [ - "auth_bypass", - "input_sanitization", - "cors", - "data_exposure", + "auth_enforced", "private_repo_opt_in", + "user_opt_in", + "missing_provider_no_secret_leak", "author_only_inline_seed", - "changelog_range" + "changelog_range", + "no_stack_trace_error_envelopes" ], - "notes": "Anonymous and injection-shaped live probes returned generic 503 database_unavailable JSON without reflecting payloads or leaking stack traces; hostile-origin OPTIONS returned 405 without Access-Control-Allow-Origin. The changelog fix prevents sending commits outside the requested tag range to OpenAI. DB-backed signed-session, private repository content, and repeated rate-limit bucket probes remain blocked by the unavailable test database." + "notes": "Changed /api/ai/* routes to require authenticated session/PAT before any repository content can be summarized. Contract tests verify anonymous 401 JSON, provider-missing generic ai_provider_not_configured without key leakage, private repository opt-in gate, and author-only inline-comment seed. Full browser security/E2E remains blocked by Playwright browser install failure." }, "accessibility": { - "status": "skip", - "violations": [], - "notes": "not applicable for this feature category; QA_MODULES included base, api, security, footer only" + "status": "blocked", + "notes": "AI surface component/unit rendering passed, but required axe/Playwright browser a11y scan could not run because npx playwright install chromium fails with: Playwright does not support chromium on ubuntu26.04-x64." } }, - "tested_steps": [ - "Read qa-report.json attempt history, qa-hints.json, BUILD_GUIDE.md, ralph-config.json, web/AGENTS.md, AI route/domain code, AI UI components, and focused AI tests.", - "Attempted timeout 120 make db-up-test; it hung until timeout and exited Error 130, so localhost:55433 remained unavailable for DB-backed QA.", - "Ran mandatory Editorial banned-token scan with ripgrep -e syntax: no banned GitHub hex, Primer, or Octicon hits in web/src outside og CSS.", - "Ran cd web && npx vitest run tests/repository-code-overview.test.tsx tests/repository-pull-request-detail.test.tsx tests/repository-releases-page.test.tsx: 3 files passed, 59 tests passed.", - "Reviewed crates/api/src/domain/ai.rs and found release_commit_context ignored previousTag and targetTag, so AI changelog prompts could include commits outside the selected compare range.", - "Fixed AI changelog context to resolve previous and target tag commit timestamps from releases/repository_git_refs and include only commits after previousTag and up through targetTag.", - "Added api_ai_surfaces_contract coverage for the tag-range cache key so a healthy DB verifies only the selected range hits the cached changelog output instead of calling OpenAI.", - "Ran cargo fmt --all --check: passed after formatting.", - "Ran cargo test -p opengithub-api --test api_ai_surfaces_contract -- --nocapture with .env.test loaded: 2 passed with DB-backed scenarios self-skipping because TEST_DATABASE_URL/DATABASE_URL were unreachable.", - "Ran cargo test -p opengithub-api domain::ai::tests::pr_inline_comment_seed_is_author_only -- --nocapture: passed.", - "Started the Rust API without a DB pool and live-probed AI summary and changelog endpoints; each returned JSON 503 database_unavailable with X-RateLimit and X-GitHub-Api-Version headers.", - "Sent SQL-like and XSS-like AI probes; responses stayed generic JSON 503 without payload reflection, SQL errors, stack traces, or environment values.", - "Probed OPTIONS /api/ai/repos/mona/octo-app/summary with Origin https://evil.com; response was 405 and did not emit Access-Control-Allow-Origin.", - "Attempted timeout 180 make test-e2e; it timed out in db-wait-test before Playwright started.", - "Started Next dev and attempted Ever CLI against http://localhost:3015; ever start returned Unexpected token HTML/non-JSON, so Ever manual verification remained unavailable.", - "Curl-rendered /mona/octo-app through Next; degraded repo page rendered the Editorial shell with var(--bg) and Fraunces/Inter/JetBrains font classes, but no DB-backed AI content could load." + "evidence": [ + "make doctor passed after clean postgres-test restart; host TCP localhost:55433 became reachable.", + "make check passed (Cargo check, web typecheck, clippy, Biome).", + "Cargo portion of make test passed; web vitest full suite failed in unrelated api-docs and repository-code-scanning-alerts-page timeouts (111 files passed, 2 failed).", + "Focused AI UI vitest passed: 3 files, 61 tests.", + "Focused AI API contract passed: 3 tests.", + "make test-e2e failed in auth.setup before app tests: missing Chromium executable; installing with npx playwright install chromium failed because ubuntu26.04-x64 is unsupported." ], - "bugs_found": [ + "bugs_fixed": [ { "severity": "major", - "phase": "functional", - "description": "AI changelog generation ignored the selected previousTag and targetTag compare range, so the OpenAI prompt could include unrelated commits outside the release range.", - "expected": "Generate changelog with AI sends only the commit log between previousTag and targetTag, matching the PRD behavior \"Compare from \".", - "actual": "release_commit_context ignored previousTag and selected the latest 60 repository commits regardless of the requested target release range.", - "reproduction": "Before this fix, create releases v1.0.0 and v1.1.0 with later unreleased commits, then POST /api/ai/repos/:owner/:repo/releases/changelog with {\"previousTag\":\"v1.0.0\",\"targetTag\":\"v1.1.0\"}; the context builder used recent_commits(repository_id, 60), including commits outside v1.0.0..v1.1.0." + "description": "AI REST endpoints accepted anonymous requests for public repositories even though ai-001 requires per-user rate limiting/settings and security QA requires auth enforcement.", + "fix": "Changed repo summary, PR summary, PR regenerate, and changelog routes to require AuthenticatedUser::from_headers before content generation/cache access." + }, + { + "severity": "test", + "description": "ai-001 DB-backed contract tests could silently self-skip when make test did not export .env.test.", + "fix": "Focused ai-001 contract now loads .env.test and the new comprehensive scenario panics if DB is unavailable, preventing false green coverage." } ], - "fix_description": "Fixed AI changelog context to honor previousTag/targetTag bounds and added focused DB-backed contract coverage. Full signed-session, private-repo opt-in, rate-limit bucket, and live OpenAI provider verification remain blocked by unavailable local test Postgres, so prd.json keeps ai-001.qa_pass=false." + "fix_description": "Auth-enforced AI API routes and added DB-backed ai-001 contract coverage. qa_pass remains false because full web unit suite and Playwright E2E/a11y gates are blocked/red outside the target feature.", + "timestamp": "2026-05-24" }, { "feature_id": "dx-001",