diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md
index 69add25c..70954c3b 100644
--- a/docs/USER_GUIDE.md
+++ b/docs/USER_GUIDE.md
@@ -287,11 +287,12 @@ Unlike the Pull Requests tab (which groups by repo), the Dependencies tab groups
| Group | Criteria |
|-------|----------|
-| **Needs Review** | CI passing (all checks green), PR not yet approved — these are ready to merge |
-| **Waiting** | CI pending, checks still running, or PR is a draft — not yet actionable |
+| **Mergeable** | CI passing (all checks green), not yet approved — awaiting review |
+| **Pending Rebase** | PR has the configured rebase label — waiting for the bot to rebase |
+| **Needs Action** | CI pending, checks still running, or PR is a draft — not yet actionable |
| **Stale** | PR has been open more than 14 days without merging — may need a rebase or manual review |
-Within each group, PRs are sorted by updated date (most recent first).
+Within each group, PRs are sorted by repository name, then update category (maintenance, pin, patch, minor, major), then update date.
### Abandoned Dependencies
diff --git a/src/app/components/dashboard/DashboardPage.tsx b/src/app/components/dashboard/DashboardPage.tsx
index caea9250..8356bb86 100644
--- a/src/app/components/dashboard/DashboardPage.tsx
+++ b/src/app/components/dashboard/DashboardPage.tsx
@@ -187,6 +187,7 @@ onAuthCleared(() => {
setDepMeta(new Map());
localStorage.removeItem?.(DEP_META_STORAGE_KEY);
_fetchingDashboardBodies = false;
+ _fetchingDepBodies = false;
resetAbandonedPatternCache();
const coord = _coordinator();
if (coord) {
@@ -1278,10 +1279,10 @@ export default function DashboardPage() {
isRefreshing={_coordinator()?.isRefreshing() ?? dashboardData.loading}
lastRefreshedAt={_coordinator()?.lastRefreshAt() ?? dashboardData.lastRefreshedAt}
onRefresh={() => _coordinator()?.manualRefresh()}
- sortOptions={globalSortOptions}
- sortValue={viewState.globalSort.field}
- sortDirection={viewState.globalSort.direction}
- onSortChange={(field, dir) => setSortPreference(field, dir)}
+ sortOptions={activeTab() === "dependencies" ? undefined : globalSortOptions}
+ sortValue={activeTab() === "dependencies" ? undefined : viewState.globalSort.field}
+ sortDirection={activeTab() === "dependencies" ? undefined : viewState.globalSort.direction}
+ onSortChange={activeTab() === "dependencies" ? undefined : (field, dir) => setSortPreference(field, dir)}
hideOrgRepo={!isBuiltinTab(activeTab()) || activeTab() === "dependencies"}
/>
@@ -1331,6 +1332,20 @@ export default function DashboardPage() {
onRefresh={() => _coordinator()?.manualRefresh()}
/>
+
+ _coordinator()?.manualRefresh()}
+ />
+
{/* TrackedTab intentionally receives unfiltered dashboardData — it bypasses exclusivity */}
{
- const { field, direction } = viewState.globalSort;
const items = [...classifiedPRs()];
- const dir = direction === "asc" ? 1 : -1;
-
items.sort((a, b) => {
- // User-selected sort as primary
- let cmp = 0;
- switch (field) {
- case "repo": cmp = a.pr.repoFullName.localeCompare(b.pr.repoFullName); break;
- case "title": cmp = a.pr.title.localeCompare(b.pr.title); break;
- case "author": cmp = a.pr.userLogin.localeCompare(b.pr.userLogin); break;
- case "comments": cmp = a.pr.comments - b.pr.comments; break;
- case "checkStatus": cmp = (a.pr.checkStatus ?? "").localeCompare(b.pr.checkStatus ?? ""); break;
- case "reviewDecision": cmp = (a.pr.reviewDecision ?? "").localeCompare(b.pr.reviewDecision ?? ""); break;
- case "size": cmp = (a.pr.additions + a.pr.deletions) - (b.pr.additions + b.pr.deletions); break;
- case "createdAt": cmp = a.pr.createdAt.localeCompare(b.pr.createdAt); break;
- case "updatedAt":
- default:
- cmp = a.pr.updatedAt.localeCompare(b.pr.updatedAt);
- break;
- }
- if (cmp !== 0) return cmp * dir;
-
- // Category as secondary (safest → least safe within each group)
+ const repoCmp = a.pr.repoFullName.localeCompare(b.pr.repoFullName);
+ if (repoCmp !== 0) return repoCmp;
+
const catCmp = CATEGORY_SORT_ORDER[a.category] - CATEGORY_SORT_ORDER[b.category];
if (catCmp !== 0) return catCmp;
- return a.pr.repoFullName.localeCompare(b.pr.repoFullName);
+ return a.pr.updatedAt.localeCompare(b.pr.updatedAt);
});
-
return items;
});
diff --git a/src/app/components/dashboard/ItemRow.tsx b/src/app/components/dashboard/ItemRow.tsx
index 21eb217c..07f8ccbf 100644
--- a/src/app/components/dashboard/ItemRow.tsx
+++ b/src/app/components/dashboard/ItemRow.tsx
@@ -117,22 +117,23 @@ export default function ItemRow(props: ItemRowProps) {
when={isCompact()}
fallback={
- {props.repo}
+ {props.subtleRepo ? repoShortName() : props.repo}
}
>
{repoShortName()}
diff --git a/src/app/lib/dependency-detection.ts b/src/app/lib/dependency-detection.ts
index b3cb597e..171e8f9d 100644
--- a/src/app/lib/dependency-detection.ts
+++ b/src/app/lib/dependency-detection.ts
@@ -18,7 +18,7 @@ export const DEP_BRANCH_PREFIXES = [
"pyup-update-",
];
-export const DEP_TITLE_PATTERN = /^(Bump |Update dependency |chore\(deps|fix\(deps|build\(deps|\[Snyk\])/i;
+export const DEP_TITLE_PATTERN = /^(Bump |Update dependency |\[Snyk\]|(?:chore|fix|build)\(deps[^)]*\):\s*(?:update |pin |lock file |bump ))/i;
export const DEP_TOOL_LABEL_NAMES = new Set([
"dependencies",
@@ -72,8 +72,14 @@ export function isDependencyPr(pr: PullRequest, trackedBotLogins: Set):
return false;
}
+const VERSION_SPECIFIER_RE = /^[><=!~^]+/;
+
+function stripVersionSpecifier(v: string): string {
+ return v.replace(VERSION_SPECIFIER_RE, "");
+}
+
function parseSemver(v: string): [number, number, number] | null {
- const cleaned = v.replace(/^v/, "");
+ const cleaned = stripVersionSpecifier(v).replace(/^v/, "");
const parts = cleaned.split(".");
if (parts.length < 2) return null;
const nums = parts.slice(0, 3).map(Number);
@@ -122,8 +128,8 @@ export function extractVersionInfo(title: string): VersionInfo | null {
// "update X requirement from >=A to >=B" (Dependabot Python)
const reqMatch = /^update\s+(.+?)\s+requirement\s+from\s+([^\s]+)\s+to\s+([^\s]+)/i.exec(body);
if (reqMatch) {
- const from = reqMatch[2]!.replace(/^[><=!~^]+/, "");
- const to = reqMatch[3]!.replace(/^[><=!~^]+/, "");
+ const from = stripVersionSpecifier(reqMatch[2]!);
+ const to = stripVersionSpecifier(reqMatch[3]!);
return { packageName: reqMatch[1]!, from, to, updateType: semverUpdateType(from, to) ?? undefined };
}
@@ -197,13 +203,13 @@ export function parseRenovateBody(body: string): VersionInfo | null {
if (changeIdx >= 0 && changeIdx < cells.length) {
const m = VERSION_ARROW_RE.exec(cells[changeIdx]);
- if (m) { result.from = m[1]; result.to = m[2]; }
+ if (m) { result.from = stripVersionSpecifier(m[1]); result.to = stripVersionSpecifier(m[2]); }
}
if (!result.from) {
for (const cell of cells) {
const m = VERSION_ARROW_RE.exec(cell);
- if (m) { result.from = m[1]; result.to = m[2]; break; }
+ if (m) { result.from = stripVersionSpecifier(m[1]); result.to = stripVersionSpecifier(m[2]); break; }
}
}
diff --git a/src/app/services/api.ts b/src/app/services/api.ts
index 281e96ed..517e6a5f 100644
--- a/src/app/services/api.ts
+++ b/src/app/services/api.ts
@@ -922,7 +922,7 @@ async function graphqlLightCombinedSearch(
return {
issues: [],
pullRequests: [],
- errors: [{ repo: "search", statusCode: null, message: "Invalid userLogin", retryable: false }],
+ errors: [{ repo: "search", statusCode: null, message: `Invalid userLogin: "${userLogin}"`, retryable: false }],
};
}
diff --git a/src/app/stores/config.ts b/src/app/stores/config.ts
index fed62eb1..8fde1ee9 100644
--- a/src/app/stores/config.ts
+++ b/src/app/stores/config.ts
@@ -38,6 +38,17 @@ export function loadConfig(): Config {
(parsed as Record).theme = "auto";
}
}
+ // Migrate tracked user logins: strip [bot] suffix from stored logins.
+ // handleTrackBot now stores base names only, but pre-migration data may
+ // have "renovate[bot]" which causes a doubled "renovate[bot][bot]" variant.
+ if (parsed && typeof parsed === "object" && Array.isArray((parsed as Record).trackedUsers)) {
+ const users = (parsed as Record).trackedUsers as { login?: string }[];
+ for (const u of users) {
+ if (typeof u.login === "string" && /\[bot\]$/i.test(u.login)) {
+ u.login = u.login.replace(/\[bot\]$/i, "");
+ }
+ }
+ }
const result = ConfigSchema.safeParse(parsed);
if (result.success) {
const data = result.data;
diff --git a/tests/components/IssuesTab.test.tsx b/tests/components/IssuesTab.test.tsx
index c9515c7e..ec826bf5 100644
--- a/tests/components/IssuesTab.test.tsx
+++ b/tests/components/IssuesTab.test.tsx
@@ -573,3 +573,56 @@ describe("IssuesTab", () => {
expect(link.getAttribute("rel")).toBe("noopener noreferrer");
});
});
+
+describe("IssuesTab — hideDepDashboard + dependencies.enabled interaction", () => {
+ it("shows Dependency Dashboard in custom tab even when hideDepDashboard=true and deps disabled", () => {
+ viewStore.updateViewState({ hideDepDashboard: true });
+ updateConfig({ dependencies: { enabled: false, rebaseLabel: "rebase" } });
+ const issues = [
+ makeIssue({ id: 1, title: "Dependency Dashboard", repoFullName: "org/repo", userLogin: "me" }),
+ ];
+ setAllExpanded("my-custom-tab", ["org/repo"], true);
+ render(() => );
+ screen.getByText("Dependency Dashboard");
+ });
+});
+
+describe("IssuesTab — hideDepDashboard + dependencies.enabled", () => {
+ it("hides Dependency Dashboard issue when hideDepDashboard=true and dependencies.enabled=false", () => {
+ viewStore.updateViewState({ hideDepDashboard: true });
+ updateConfig({ dependencies: { enabled: false, rebaseLabel: "rebase" } });
+ const issues = [
+ makeIssue({ id: 1, title: "Dependency Dashboard", repoFullName: "org/repo" }),
+ makeIssue({ id: 2, title: "Regular issue", repoFullName: "org/repo" }),
+ ];
+ setAllExpanded("issues", ["org/repo"], true);
+ render(() => );
+ expect(screen.queryByText("Dependency Dashboard")).toBeNull();
+ screen.getByText("Regular issue");
+ });
+
+ it("shows Dependency Dashboard issue when hideDepDashboard=true but dependencies.enabled=true", () => {
+ viewStore.updateViewState({ hideDepDashboard: true });
+ updateConfig({ dependencies: { enabled: true, rebaseLabel: "rebase" } });
+ const issues = [
+ makeIssue({ id: 1, title: "Dependency Dashboard", repoFullName: "org/repo" }),
+ makeIssue({ id: 2, title: "Regular issue", repoFullName: "org/repo" }),
+ ];
+ setAllExpanded("issues", ["org/repo"], true);
+ render(() => );
+ screen.getByText("Dependency Dashboard");
+ screen.getByText("Regular issue");
+ });
+
+ it("shows Dependency Dashboard issue when hideDepDashboard=false regardless of dependencies.enabled", () => {
+ viewStore.updateViewState({ hideDepDashboard: false });
+ updateConfig({ dependencies: { enabled: false, rebaseLabel: "rebase" } });
+ const issues = [
+ makeIssue({ id: 1, title: "Dependency Dashboard", repoFullName: "org/repo" }),
+ ];
+ setAllExpanded("issues", ["org/repo"], true);
+ render(() => );
+ screen.getByText("Dependency Dashboard");
+ });
+
+});
diff --git a/tests/components/dashboard/DependenciesTab.test.tsx b/tests/components/dashboard/DependenciesTab.test.tsx
index 897d6bdc..ea47e4b8 100644
--- a/tests/components/dashboard/DependenciesTab.test.tsx
+++ b/tests/components/dashboard/DependenciesTab.test.tsx
@@ -528,6 +528,148 @@ describe("DependenciesTab — ignore button", () => {
expect(screen.getByLabelText(/ignored items/i)).toBeDefined();
});
+
+ it("ignored dep PRs appear in the IgnoreBadge", () => {
+ const pr = makeMergeablePR({ id: 5003, title: "chore(deps): update dependency chalk to v6" });
+ renderTab({ pullRequests: [pr] });
+
+ fireEvent.click(screen.getByRole("button", { name: /^Ignore #/ }));
+
+ expect(screen.getByLabelText(/ignored items/i)).toBeDefined();
+ });
+});
+
+// ── Track button ──────────────────────────────────────────────────────────────
+
+describe("DependenciesTab — track button", () => {
+ it("track button is not rendered when enableTracking is false", () => {
+ updateConfig({ enableTracking: false });
+ const pr = makeMergeablePR({ title: "chore(deps): update dependency lodash to v5" });
+ renderTab({ pullRequests: [pr] });
+
+ expect(screen.queryByRole("button", { name: /^Pin #/ })).toBeNull();
+ });
+
+ it("track button renders when enableTracking is true", () => {
+ updateConfig({ enableTracking: true });
+ const pr = makeMergeablePR({ title: "chore(deps): update dependency lodash to v5" });
+ renderTab({ pullRequests: [pr] });
+
+ expect(screen.getByRole("button", { name: /^Pin #/ })).toBeDefined();
+ });
+
+ it("clicking track button adds the PR to trackedItems", () => {
+ updateConfig({ enableTracking: true });
+ const pr = makeMergeablePR({ id: 6001, title: "Bump react from 17.0.0 to 18.0.0" });
+ renderTab({ pullRequests: [pr] });
+
+ fireEvent.click(screen.getByRole("button", { name: /^Pin #/ }));
+
+ expect(viewState.trackedItems.some((t) => t.id === 6001 && t.type === "pullRequest")).toBe(true);
+ });
+
+ it("clicking track button a second time removes the PR from trackedItems (toggle)", () => {
+ updateConfig({ enableTracking: true });
+ const pr = makeMergeablePR({ id: 6002, title: "Bump typescript from 4.0.0 to 5.0.0" });
+ renderTab({ pullRequests: [pr] });
+
+ fireEvent.click(screen.getByRole("button", { name: /^Pin #/ }));
+ expect(viewState.trackedItems.some((t) => t.id === 6002)).toBe(true);
+
+ fireEvent.click(screen.getByRole("button", { name: /^Unpin #/ }));
+ expect(viewState.trackedItems.some((t) => t.id === 6002)).toBe(false);
+ });
+});
+
+// ── Unknown bot detection ────────────────────────────────────────────────────
+
+describe("DependenciesTab — unknown bot banner", () => {
+ it("shows banner for unknown bot authors", () => {
+ const pr = makeMergeablePR({
+ userLogin: "custom-dep-bot",
+ userAvatarUrl: "https://avatars.githubusercontent.com/u/12345",
+ });
+ renderTab({ pullRequests: [pr] });
+ expect(screen.getByRole("button", { name: "Track bot" })).toBeDefined();
+ expect(screen.getByRole("button", { name: "Dismiss" })).toBeDefined();
+ });
+
+ it("does not show banner for known dep bots", () => {
+ const pr = makeMergeablePR({ userLogin: "renovate[bot]" });
+ renderTab({ pullRequests: [pr] });
+ expect(screen.queryByRole("button", { name: "Track bot" })).toBeNull();
+ });
+
+ it("does not show banner for known bots without [bot] suffix", () => {
+ const pr = makeMergeablePR({ userLogin: "dependabot" });
+ renderTab({ pullRequests: [pr] });
+ expect(screen.queryByRole("button", { name: "Track bot" })).toBeNull();
+ });
+
+ it("does not show banner for the authenticated user", () => {
+ const pr = makeMergeablePR({ userLogin: "testuser" });
+ renderTab({ pullRequests: [pr], userLogin: "testuser" });
+ expect(screen.queryByRole("button", { name: "Track bot" })).toBeNull();
+ });
+
+ it("dismiss button hides the banner for the session", () => {
+ const pr = makeMergeablePR({
+ userLogin: "custom-dep-bot",
+ userAvatarUrl: "https://avatars.githubusercontent.com/u/12345",
+ });
+ renderTab({ pullRequests: [pr] });
+ expect(screen.getByRole("button", { name: "Dismiss" })).toBeDefined();
+
+ fireEvent.click(screen.getByRole("button", { name: "Dismiss" }));
+ expect(screen.queryByRole("button", { name: "Track bot" })).toBeNull();
+ });
+
+ it("track button adds bot to config.trackedUsers", () => {
+ const pr = makeMergeablePR({
+ userLogin: "custom-dep-bot",
+ userAvatarUrl: "https://avatars.githubusercontent.com/u/12345",
+ });
+ renderTab({ pullRequests: [pr] });
+ fireEvent.click(screen.getByRole("button", { name: "Track bot" }));
+
+ expect(config.trackedUsers.some((u) => u.login === "custom-dep-bot" && u.type === "bot")).toBe(true);
+ });
+});
+
+// ── Ignore button ─────────────────────────────────────────────────────────────
+
+describe("DependenciesTab — ignore button", () => {
+ it("clicking the ignore button hides the PR from the list", () => {
+ const pr = makeMergeablePR({ title: "chore(deps): update dependency lodash to v5" });
+ renderTab({ pullRequests: [pr] });
+ expect(screen.getByText("lodash → v5")).toBeDefined();
+
+ const ignoreBtn = screen.getByRole("button", { name: /^Ignore #/ });
+ fireEvent.click(ignoreBtn);
+
+ expect(screen.queryByText("lodash → v5")).toBeNull();
+ });
+
+ it("ignore button adds item to ignoredItems in viewState", () => {
+ const pr = makeMergeablePR({ id: 5001, title: "chore(deps): update dependency react to v19" });
+ renderTab({ pullRequests: [pr] });
+
+ const ignoreBtn = screen.getByRole("button", { name: /^Ignore #/ });
+ fireEvent.click(ignoreBtn);
+
+ expect(viewState.ignoredItems.some((i) => i.id === 5001 && i.type === "pullRequest")).toBe(true);
+ });
+
+ it("ignored PR is not rendered even when re-renderTab is called", () => {
+ const pr = makeMergeablePR({ id: 5002, title: "Bump axios from 0.27.2 to 1.0.0" });
+ const { unmount } = renderTab({ pullRequests: [pr] });
+
+ fireEvent.click(screen.getByRole("button", { name: /^Ignore #/ }));
+ unmount();
+
+ renderTab({ pullRequests: [pr] });
+ expect(screen.queryByText(/axios/)).toBeNull();
+ });
});
// ── Track button ──────────────────────────────────────────────────────────────
diff --git a/tests/components/dashboard/PullRequestsTab.test.tsx b/tests/components/dashboard/PullRequestsTab.test.tsx
index 0bf0bf9c..27101128 100644
--- a/tests/components/dashboard/PullRequestsTab.test.tsx
+++ b/tests/components/dashboard/PullRequestsTab.test.tsx
@@ -31,7 +31,7 @@ vi.mock("../../../src/app/lib/url", () => ({
import { produce } from "solid-js/store";
import PullRequestsTab from "../../../src/app/components/dashboard/PullRequestsTab";
-import { viewState, setViewState, setTabFilter, setAllExpanded, resetViewState, updateViewState, setCustomTabFilter } from "../../../src/app/stores/view";
+import { viewState, setViewState, setTabFilter, setAllExpanded, resetViewState, updateViewState, setCustomTabFilter, ignoreItem } from "../../../src/app/stores/view";
import type { TrackedUser } from "../../../src/app/stores/config";
import { updateConfig, resetConfig } from "../../../src/app/stores/config";
@@ -869,3 +869,34 @@ describe("PullRequestsTab — customTabId filter preset", () => {
screen.getByText("My PR");
});
});
+
+// ── PullRequestsTab — depPrIds ignored-item filtering ───────────────────────
+
+describe("PullRequestsTab — depPrIds ignored-item filtering", () => {
+ it("excludes dep PR IDs from ignored items when depPrIds is provided", () => {
+ ignoreItem({ id: 100, type: "pullRequest", repo: "owner/repo", title: "Dep PR", ignoredAt: Date.now() });
+ ignoreItem({ id: 200, type: "pullRequest", repo: "owner/repo", title: "Normal PR", ignoredAt: Date.now() });
+
+ render(() => );
+
+ screen.getByRole("button", { name: /1 ignored/i });
+ expect(screen.queryByRole("button", { name: /2 ignored/i })).toBeNull();
+ });
+
+ it("includes all ignored PRs when depPrIds is undefined", () => {
+ ignoreItem({ id: 100, type: "pullRequest", repo: "owner/repo", title: "PR A", ignoredAt: Date.now() });
+ ignoreItem({ id: 200, type: "pullRequest", repo: "owner/repo", title: "PR B", ignoredAt: Date.now() });
+
+ render(() => );
+
+ screen.getByRole("button", { name: /2 ignored/i });
+ });
+
+ it("shows no badge when all ignored PRs belong to depPrIds", () => {
+ ignoreItem({ id: 100, type: "pullRequest", repo: "owner/repo", title: "Dep only", ignoredAt: Date.now() });
+
+ render(() => );
+
+ expect(screen.queryByRole("button", { name: /ignored/i })).toBeNull();
+ });
+});
diff --git a/tests/components/layout/TabBar.test.tsx b/tests/components/layout/TabBar.test.tsx
index 2f86528d..366b13a4 100644
--- a/tests/components/layout/TabBar.test.tsx
+++ b/tests/components/layout/TabBar.test.tsx
@@ -371,4 +371,64 @@ describe("TabBar", () => {
await user.click(depTab);
expect(onTabChange).toHaveBeenCalledWith("dependencies");
});
+
+ // ── Dependencies tab ─────────────────────────────────────────────────────────
+
+ it("does not render Dependencies tab when enableDependencies is false", () => {
+ const onTabChange = vi.fn();
+ render(() => (
+
+ ));
+ expect(screen.queryByRole("tab", { name: /Dependencies/i })).toBeNull();
+ });
+
+ it("does not render Dependencies tab when enableDependencies is undefined", () => {
+ const onTabChange = vi.fn();
+ render(() => (
+
+ ));
+ expect(screen.queryByRole("tab", { name: /Dependencies/i })).toBeNull();
+ });
+
+ it("renders Dependencies tab when enableDependencies is true", () => {
+ const onTabChange = vi.fn();
+ render(() => (
+
+ ));
+ screen.getByRole("tab", { name: /Dependencies/i });
+ });
+
+ it("renders Dependencies tab between Pull Requests and Actions", () => {
+ const onTabChange = vi.fn();
+ render(() => (
+
+ ));
+ const tabs = screen.getAllByRole("tab");
+ const names = tabs.map((t) => t.textContent?.trim() ?? "");
+ const prIdx = names.findIndex((n) => /Pull Requests/i.test(n));
+ const depIdx = names.findIndex((n) => /Dependencies/i.test(n));
+ const actionsIdx = names.findIndex((n) => /^Actions/i.test(n));
+ expect(depIdx).toBeGreaterThan(prIdx);
+ expect(depIdx).toBeLessThan(actionsIdx);
+ });
+
+ it("shows dependencies count badge when enableDependencies is true and count provided", () => {
+ const onTabChange = vi.fn();
+ const counts: TabCounts = { dependencies: 8 };
+ render(() => (
+
+ ));
+ screen.getByText("8");
+ });
+
+ it("calls onTabChange with 'dependencies' when Dependencies tab clicked", async () => {
+ const user = userEvent.setup();
+ const onTabChange = vi.fn();
+ render(() => (
+
+ ));
+ const depTab = screen.getByRole("tab", { name: /Dependencies/i });
+ await user.click(depTab);
+ expect(onTabChange).toHaveBeenCalledWith("dependencies");
+ });
});
diff --git a/tests/lib/dependency-detection.test.ts b/tests/lib/dependency-detection.test.ts
index ba7180aa..8fb4b6bc 100644
--- a/tests/lib/dependency-detection.test.ts
+++ b/tests/lib/dependency-detection.test.ts
@@ -91,6 +91,21 @@ describe("isDependencyPr", () => {
expect(isDependencyPr(pr, NO_TRACKED_BOTS)).toBe(false);
});
+ it("returns false for human fix(deps) PR that is not a bot update", () => {
+ const pr = makePullRequest({
+ userLogin: "octocat",
+ headRef: "fix/deps-tab-fixes",
+ title: "fix(deps): post-deploy dependency tab fixes",
+ labels: [],
+ });
+ expect(isDependencyPr(pr, NO_TRACKED_BOTS)).toBe(false);
+ });
+
+ it("returns true for bot fix(deps) PR with update action", () => {
+ const pr = makePullRequest({ title: "fix(deps): update all non-major dependencies" });
+ expect(isDependencyPr(pr, NO_TRACKED_BOTS)).toBe(true);
+ });
+
it("returns false for regular PR even with tracked users of type user", () => {
const pr = makePullRequest({ userLogin: "alice" });
const tracked = new Set(["bob"]);
@@ -180,6 +195,11 @@ describe("extractVersionInfo", () => {
expect(result).toEqual({ packageName: "mypy", from: "1.15.0", to: "1.20.1", updateType: "minor" });
});
+ it("handles pinned == pip requirement specifier", () => {
+ const result = extractVersionInfo("update boto3 requirement from ==1.26.0 to ==1.34.0");
+ expect(result).toEqual({ packageName: "boto3", from: "1.26.0", to: "1.34.0", updateType: "minor" });
+ });
+
it("returns null for unrecognized title format", () => {
expect(extractVersionInfo("Fix a bug in auth flow")).toBeNull();
});
@@ -531,6 +551,20 @@ describe("parseRenovateBody", () => {
});
});
+ it("strips Python version specifiers (==, >=, ~=) from versions", () => {
+ const body = [
+ "| Package | Change | [Age](url) | [Confidence](url) |",
+ "|---|---|---|---|",
+ "| [stamina](url) ([changelog](url2)) | `==25.2.0` → `==26.1.0` |  |  |",
+ ].join("\n");
+ expect(parseRenovateBody(body)).toEqual({
+ packageName: "stamina",
+ updateType: "major",
+ from: "25.2.0",
+ to: "26.1.0",
+ });
+ });
+
it("parses digest update", () => {
const body = [
"| Package | Update | Change |",