Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions build-progress.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 - search-007 QA final lane
- Fixed /api/repos/:owner/:repo/find to honor the PRD path-list contract: q is ignored for /find, default pageSize is 10000, and repository_ref_files is refreshed while legacy /file-finder filtering remains intact.
- Updated the dedicated /<owner>/<repo>/find/<ref> page fetch to use the /find path-list contract for client-side fuzzy scoring.
- Added focused DB-backed API/security coverage and focused Playwright UI/a11y/keyboard coverage; make check, focused Rust contract, and focused Playwright with /snap/bin/chromium passed.
- QA remains blocked (qa_pass=false): full make test fails in unrelated web unit-test timeouts, and default make test-e2e cannot provision bundled Chromium on ubuntu26.04-x64 without executable override.
2 changes: 1 addition & 1 deletion crates/api/src/domain/repositories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3053,7 +3053,7 @@ pub async fn repository_file_finder_for_actor_by_owner_name(
let resolved_ref = resolve_repository_ref(pool, &repository, query.ref_name).await?;
let normalized_query = query.query.unwrap_or("").trim().to_lowercase();
let page = query.page.max(1);
let page_size = query.page_size.clamp(1, 100);
let page_size = query.page_size.clamp(1, if query.query.is_none() { 10_000 } else { 100 });
let files = list_repository_files_for_resolved_ref(pool, repository.id, &resolved_ref).await?;
refresh_repository_ref_files_cache(pool, repository.id, &resolved_ref, &files).await?;
let mut items = files
Expand Down
15 changes: 11 additions & 4 deletions crates/api/src/routes/repositories.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use axum::{
body::Bytes,
extract::{Path, Query, State},
http::{header, HeaderMap, HeaderValue, StatusCode},
extract::{MatchedPath, Path, Query, State},
http::{header, HeaderMap, HeaderValue, StatusCode, Uri},
response::{IntoResponse, Response},
routing::{delete, get, patch, post, put},
Json, Router,
Expand Down Expand Up @@ -4435,19 +4435,26 @@ async fn file_finder(
headers: HeaderMap,
Path((owner, repo)): Path<(String, String)>,
Query(query): Query<FileFinderQuery>,
uri: Uri,
matched_path: MatchedPath,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorEnvelope>)> {
let actor = AuthenticatedUser::from_headers(&state, &headers).await?;
let pool = state.db.as_ref().ok_or_else(database_unavailable)?;
let is_path_list_contract = uri.path().ends_with("/find") || matched_path.as_str().contains("/:owner/:repo/find");
let envelope = repository_file_finder_for_actor_by_owner_name(
pool,
actor.0.id,
&owner,
&repo,
RepositoryFileFinderQuery {
ref_name: query.ref_name.as_deref(),
query: query.q.as_deref(),
// The /find contract returns the cached full path list; filtering is intentionally client-side.
query: if is_path_list_contract { None } else { query.q.as_deref() },
page: query.page.unwrap_or(1).max(1),
page_size: query.page_size.unwrap_or(20).clamp(1, 100),
page_size: query
.page_size
.unwrap_or(if is_path_list_contract { 10_000 } else { 20 })
.clamp(1, if is_path_list_contract { 10_000 } else { 100 }),
},
)
.await
Expand Down
51 changes: 51 additions & 0 deletions crates/api/tests/repository_tree_navigation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,57 @@ async fn repository_tree_contract_resolves_branches_tags_and_recovery_links() {
assert_eq!(finder_page_body["total"], 105);
assert_eq!(finder_page_body["items"][0]["path"], "docs/example-040.md");


let (find_status, find_body) = send_json(
app.clone(),
&format!("{base}/find?ref={encoded_feature}&q=guide"),
Some(&owner_cookie),
)
.await;
assert_eq!(find_status, StatusCode::OK);
assert_eq!(find_body["resolvedRef"]["shortName"], "feature/tree-nav");
assert_eq!(find_body["page"], 1);
assert_eq!(find_body["pageSize"], 10000);
assert_eq!(find_body["total"], 107);
assert!(find_body["items"]
.as_array()
.expect("find items should be an array")
.iter()
.any(|item| item["path"] == "docs/guide.md"));
assert_eq!(
find_body["items"]
.as_array()
.expect("find items should be an array")
.len(),
107,
"/find should return the full cached path list for client-side fuzzy scoring"
);

let cached_paths: serde_json::Value = sqlx::query_scalar(
"SELECT paths FROM repository_ref_files WHERE repository_id = $1 AND ref = $2",
)
.bind(repository.id)
.bind("feature/tree-nav")
.fetch_one(&pool)
.await
.expect("finder should refresh repository_ref_files");
assert!(cached_paths
.as_array()
.expect("cached paths should be an array")
.iter()
.any(|path| path == "docs/guide.md"));

let (find_unauth_status, find_unauth_body) = send_json(
app.clone(),
&format!("{base}/find?ref={encoded_feature}"),
None,
)
.await;
assert_eq!(find_unauth_status, StatusCode::UNAUTHORIZED);
assert_eq!(find_unauth_body["error"]["code"], "not_authenticated");
assert!(!find_unauth_body.to_string().contains("docs/guide.md"));
assert!(!find_unauth_body.to_string().to_lowercase().contains("stack"));

let (bad_path_status, bad_path_body) = send_json(
app.clone(),
&format!("{base}/contents/%2E%2E/secrets?ref={encoded_feature}"),
Expand Down
110 changes: 93 additions & 17 deletions hack/cleanup_worktree.sh
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
#!/usr/bin/env bash
# cleanup_worktree.sh — Remove an opengithub git worktree and its branch.
# cleanup_worktree.sh — Remove opengithub git worktrees and their branches.
#
# Usage: ./hack/cleanup_worktree.sh [worktree_name]
# Usage: ./hack/cleanup_worktree.sh [--yes] <name> [<name> ...]
# - no args: lists worktrees under $HOME/wt/opengithub
# - one arg: removes the named worktree (and prompts to delete its branch)
# - one or more names: removes each in turn
# - --yes: skip the "delete branch?" prompt and delete it

set -euo pipefail

REPO_BASE_NAME="$(basename "$(git rev-parse --show-toplevel)")"
WORKTREE_BASE_DIR="${OPENGITHUB_WORKTREE_BASE:-$HOME/wt/${REPO_BASE_NAME}}"
CURRENT_TOPLEVEL="$(git rev-parse --show-toplevel)"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

ASSUME_YES=0

list_worktrees() {
echo -e "${YELLOW}Worktrees under ${WORKTREE_BASE_DIR}:${NC}"
git worktree list | grep -E "^${WORKTREE_BASE_DIR}" || {
Expand All @@ -23,20 +27,65 @@ list_worktrees() {
}
}

confirm() {
local prompt="$1"
if [ "$ASSUME_YES" = "1" ]; then
return 0
fi
read -p "$prompt (y/N) " -n 1 -r
echo ""
[[ $REPLY =~ ^[Yy]$ ]]
}

cleanup_worktree() {
local name="$1"
local path="${WORKTREE_BASE_DIR}/${name}"

if ! git worktree list | grep -q "$path"; then
# Robust lookup: directory exists AND git knows it as a worktree (exact match).
if [ ! -d "$path" ] || ! git worktree list --porcelain | grep -qxF "worktree $path"; then
echo -e "${RED}Error: no worktree at $path${NC}"
echo ""
list_worktrees || true
exit 1
fi

echo -e "${YELLOW}Removing worktree: $path${NC}"
# Refuse to remove the worktree we're currently inside.
if [ "$CURRENT_TOPLEVEL" = "$path" ]; then
echo -e "${RED}Error: refusing to remove the current worktree ($path)${NC}"
echo " cd elsewhere first, then re-run."
exit 1
fi

# Detect the actual branch checked out in that worktree (don't assume it == $name).
local branch
branch="$(git -C "$path" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "$name")"

echo -e "${YELLOW}Removing worktree: $path${NC} ${YELLOW}(branch: ${branch})${NC}"

# Drop scratch caches before git removes the dir (faster, gives clear feedback)
# Warn on uncommitted changes.
if [ -n "$(git -C "$path" status --porcelain 2>/dev/null || true)" ]; then
echo -e "${YELLOW} ! $path has uncommitted changes${NC}"
if ! confirm " Remove anyway?"; then
echo " aborted."
exit 1
fi
fi

# Warn on unpushed commits (only if an upstream is configured).
if git -C "$path" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
local unpushed
unpushed="$(git -C "$path" log --oneline '@{u}..HEAD' 2>/dev/null || true)"
if [ -n "$unpushed" ]; then
echo -e "${YELLOW} ! $path has unpushed commits:${NC}"
echo "$unpushed" | sed 's/^/ /'
if ! confirm " Remove anyway?"; then
echo " aborted."
exit 1
fi
fi
fi

# Drop scratch caches before git removes the dir (faster, gives clear feedback).
if [ -d "$path/.scratch" ]; then
echo " → removing .scratch/ (cargo-target, tmp)"
rm -rf "$path/.scratch"
Expand All @@ -52,28 +101,55 @@ cleanup_worktree() {
fi

echo ""
read -p "Delete branch '${name}'? (y/N) " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
if git branch -D "$name" 2>/dev/null; then
echo -e "${GREEN} ✓ branch deleted${NC}"
if confirm "Delete branch '${branch}'?"; then
# Try safe delete first; if it refuses (unmerged), confirm before force.
if git branch -d "$branch" 2>/dev/null; then
echo -e "${GREEN} ✓ branch '${branch}' deleted${NC}"
else
echo -e "${YELLOW} ! branch '${name}' did not exist or could not be deleted${NC}"
echo -e "${YELLOW} ! branch '${branch}' is unmerged or doesn't exist${NC}"
if confirm " Force-delete '${branch}' anyway?"; then
if git branch -D "$branch" 2>/dev/null; then
echo -e "${GREEN} ✓ branch '${branch}' force-deleted${NC}"
else
echo -e "${YELLOW} ! could not delete '${branch}' (may not exist)${NC}"
fi
else
echo " branch '${branch}' kept"
fi
fi
else
echo " branch '${name}' kept"
echo " branch '${branch}' kept"
fi

git worktree prune
echo ""
echo -e "${GREEN}✅ Cleanup complete${NC}"
echo -e "${GREEN}✅ Cleanup complete for ${name}${NC}"
}

if [ $# -eq 0 ]; then
# Parse flags.
names=()
for arg in "$@"; do
case "$arg" in
-y|--yes) ASSUME_YES=1 ;;
-h|--help)
sed -n '2,7p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
-*)
echo -e "${RED}Unknown flag: $arg${NC}" >&2
exit 2
;;
*) names+=("$arg") ;;
esac
done

if [ "${#names[@]}" -eq 0 ]; then
list_worktrees || exit 1
echo ""
echo "Usage: $0 <worktree_name>"
echo "Usage: $0 [--yes] <worktree_name> [<worktree_name> ...]"
exit 0
fi

cleanup_worktree "$1"
for name in "${names[@]}"; do
cleanup_worktree "$name"
done
3 changes: 2 additions & 1 deletion prd.json
Original file line number Diff line number Diff line change
Expand Up @@ -2171,7 +2171,8 @@
"repocode-002",
"search-005"
],
"build_pass": true
"build_pass": true,
"qa_pass": false
},
{
"id": "security-002",
Expand Down
11 changes: 6 additions & 5 deletions qa-hints.json
Original file line number Diff line number Diff line change
Expand Up @@ -7892,13 +7892,14 @@
"Extended /docs/api with the file finder API contract and cache semantics.",
"Added focused Vitest coverage for fuzzy filtering, highlighted/concrete result links, keyboard open, empty state, and Escape clearing.",
"Browser smoke passed on /mona/octo-app/find/main with a local API-compatible stub: filtered to src/app/page.tsx, Enter navigated to the blob route, verified empty state, checked zero href=\"#\", checked no horizontal overflow, and saved ralph/screenshots/build/search-007-file-finder.jpg.",
"Verification passed: cargo check -p opengithub-api --tests, focused Vitest, web TypeScript, focused Biome, full make check, full make test with Cargo tests plus 631 web tests, and mandatory Editorial banned-value scan."
"Verification passed: cargo check -p opengithub-api --tests, focused Vitest, web TypeScript, focused Biome, full make check, full make test with Cargo tests plus 631 web tests, and mandatory Editorial banned-value scan.",
"QA final lane added DB-backed /find contract assertions for full path-list/no server q filtering, repository_ref_files cache refresh, unauthenticated 401 shape, and no path/stack leak.",
"QA final lane added Playwright repository-file-finder.spec.ts covering t shortcut, labeled focused combobox, empty cached list, local fuzzy filtering, keyboard navigation, Enter open, no-match state, and dead-link check."
],
"needs_deeper_qa": [
"Run full signed-session Playwright after the local TEST_DATABASE_URL path is healthy; bounded timeout 120 make test-e2e terminated with no Playwright detail.",
"Run DB-backed API assertions after migrations against a credentialed Postgres to verify repository_ref_files rows are created/updated for branch and tag refs.",
"Probe very large repositories beyond the current 100-item page fetch; the UI scores the fetched cached list locally, so a later backend/page-size contract may be needed for huge path lists.",
"Verify keyboard-only behavior on mobile/desktop with long paths, duplicate filenames in different folders, refs containing slashes, binary files, and private repository permission boundaries."
"Unblock full make test web unit-suite timeouts in unrelated repository-code-overview and repository-dependency-graph-page tests before setting qa_pass true.",
"Unblock default make test-e2e browser provisioning on ubuntu26.04-x64 or configure the committed test runner to use /snap/bin/chromium; focused E2E passed with PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH.",
"After full gates are green, rerun full make test-e2e in addition to the focused repository-file-finder spec."
]
},
{
Expand Down
25 changes: 23 additions & 2 deletions qa-report-summary.json
Original file line number Diff line number Diff line change
Expand Up @@ -3407,9 +3407,30 @@
"description": "File finder widget \u2014 keyboard-driven `t` shortcut on a repo to fuzzy-find a file",
"category": "feature",
"qa_pass": false,
"attempts": 0,
"attempts": 1,
"exhausted": false,
"sub_phases": {}
"sub_phases": {
"functional": {
"status": "pass",
"notes": "Focused Playwright passed via system Chromium: repository-file-finder.spec.ts covered t shortcut, /find/main page, focused labeled combobox, empty cached list display, local fuzzy filtering, ArrowUp/ArrowDown, Enter-to-open README.md, no-match empty state, and no dead href controls."
},
"api_contract": {
"status": "pass",
"notes": "DB-backed Rust contract test repository_tree_navigation passed for GET /api/repos/:owner/:repo/find?ref=... returning the full 107-path list despite q=guide, pageSize 10000, repository_ref_files refresh, legacy /file-finder server filtering preserved, and unauthenticated 401 JSON shape."
},
"security": {
"status": "pass",
"notes": "Focused API test verified private path-list endpoint requires auth and unauthenticated response contains no repository path data or stack trace."
},
"accessibility": {
"status": "pass",
"notes": "Focused browser test verified keyboard-only t shortcut, focus lands on named combobox, listbox/option semantics are usable, Arrow navigation and Enter activation work."
},
"regression": {
"status": "blocked",
"notes": "Required full make test is red in unrelated pre-existing web unit tests: repository-code-overview.test.tsx large-directory paging and repository-dependency-graph-page.test.tsx dependency page timed out at 5000ms. make test-e2e without executable override is blocked because Playwright does not support installing Chromium on ubuntu26.04-x64; focused E2E passed with PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/snap/bin/chromium."
}
}
},
{
"feature_id": "security-002",
Expand Down
Loading