Skip to content
Merged
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
42 changes: 22 additions & 20 deletions .bb/skills/verify-bb/features/projects-environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,32 @@ command’s `--help` before mutation. Use fresh browser snapshots for controls.

## Source

- `apps/app/src/views/ProjectSettingsView.tsx`
- `apps/app/src/views/ProjectDetailSettingsView.tsx`
- `apps/app/src/components/project/ProjectActionsMenu.tsx`
- `apps/cli/src/commands/project.ts`
- `apps/cli/src/commands/environment.ts`

Legacy `/projects/:projectId/settings` bookmarks redirect with history replacement to `/settings/projects/:projectId`, preserving query and hash. Check the sidebar, project actions, header, and machine checkout links against the same detail page.

## Feature recipes

| Feature | Drive | Observable success |
| --- | --- | --- |
| Create and rename projects | Run the local-project recipe, then rename through the project actions menu and reload; compare project show/update. | Project identity remains stable while its name changes. |
| Multiple sources and default source | Add a synthetic source on a second disposable host, change its path/default flag, then remove it with project source operations. Each host permits one source and the source host is immutable. | Sources persist, the intended default is selected, and the remaining source is still usable. |
| Git remote projects | Create a local-path project, then clone a disposable remote onto a second host using project source add --clone and the UI source controls. | Clone/provisioning uses the requested remote and branch; invalid remote errors do not create a usable fake checkout. |
| Recent repository import | On a disposable host home with synthetic recent repos, run the offered import action and inspect project list. | Only discovered candidates are imported; duplicates and missing paths are handled. Do not use real recent repos as fixtures. |
| Local versus managed worktree | Create one thread with Work locally and one with a new worktree and selected base branch. | Environment path, branch, and lifecycle match the selection; edits in the managed worktree do not affect the original checkout. |
| Reuse and switch environments | Select an existing environment for another thread; use environment update for display name/merge-base changes; test path switching separately through the thread environment-directory action after reading help. | Both thread details identify the intended environment; invalid paths fail without silently changing scope. |
| Environment status and branch discovery | Compare Info panel with environment show/status/branches and project branches for the same source. | Branch, dirty state, host, and path agree; disconnected or missing workspaces show an actionable error. |
| Diff views and selected patches | Open Diff with tracked edits, additions, renames, and deletions; use environment diff/diff-files/diff-file/diff-patch. | File lists, old/new contents, line numbers, and selected patches match git diff including untracked changes as supported. |
| Commit | Prepare a fixture containing only changes intended for a commit; invoke the UI/CLI Commit action and inspect git show and the clean diff. | The action stages all workspace changes with git add -A; the resulting commit contains the fixture changes. |
| Pull requests | With a disposable authenticated remote PR, inspect environment pull-request show; exercise ready, draft, and merge only in that test repo. | Forge state agrees with UI/CLI; missing auth/checks/conflicts produce explicit failures. Never run this on a user PR for documentation. |
| Archive environment threads | Create threads in two managed worktree environments and invoke environment archive-threads for one; try a local environment separately. | Only the selected managed environment’s active threads are archived; local environments are rejected with HTTP409. |
| Project attachments and history | Upload/download a synthetic file with project attachment; compare bytes; inspect project history and workspace file/path/content commands. | Returned content and history belong to the chosen project/host; missing files report failure. |
| Execution defaults | Set project defaults for environment/provider/model/permissions, open a new root draft and override one choice before sending. | Resolved defaults populate once, explicit draft choices win, and thread details reflect the actual execution options. |
| Clone destination and folder discovery | Browse an empty test host directory and inspect suggested clone path, path existence and invalid destination feedback. | Folder and clone suggestions target the chosen host; existing paths are not overwritten by a failed clone. |
| Delete project | Delete a disposable project through its confirmation flow, then inspect projects and its threads. | Deletion scope matches the confirmation; cancel leaves all state intact. |
| Feature | Drive | Observable success |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Create and rename projects | Run the local-project recipe, then rename through the project actions menu and reload; compare project show/update. | Project identity remains stable while its name changes. |
| Multiple sources and default source | Add a synthetic source on a second disposable host, change its path/default flag, then remove it with project source operations. Each host permits one source and the source host is immutable. | Sources persist, the intended default is selected, and the remaining source is still usable. |
| Git remote projects | Create a local-path project, then clone a disposable remote onto a second host using project source add --clone and the UI source controls. | Clone/provisioning uses the requested remote and branch; invalid remote errors do not create a usable fake checkout. |
| Recent repository import | On a disposable host home with synthetic recent repos, run the offered import action and inspect project list. | Only discovered candidates are imported; duplicates and missing paths are handled. Do not use real recent repos as fixtures. |
| Local versus managed worktree | Create one thread with Work locally and one with a new worktree and selected base branch. | Environment path, branch, and lifecycle match the selection; edits in the managed worktree do not affect the original checkout. |
| Reuse and switch environments | Select an existing environment for another thread; use environment update for display name/merge-base changes; test path switching separately through the thread environment-directory action after reading help. | Both thread details identify the intended environment; invalid paths fail without silently changing scope. |
| Environment status and branch discovery | Compare Info panel with environment show/status/branches and project branches for the same source. | Branch, dirty state, host, and path agree; disconnected or missing workspaces show an actionable error. |
| Diff views and selected patches | Open Diff with tracked edits, additions, renames, and deletions; use environment diff/diff-files/diff-file/diff-patch. | File lists, old/new contents, line numbers, and selected patches match git diff including untracked changes as supported. |
| Commit | Prepare a fixture containing only changes intended for a commit; invoke the UI/CLI Commit action and inspect git show and the clean diff. | The action stages all workspace changes with git add -A; the resulting commit contains the fixture changes. |
| Pull requests | With a disposable authenticated remote PR, inspect environment pull-request show; exercise ready, draft, and merge only in that test repo. | Forge state agrees with UI/CLI; missing auth/checks/conflicts produce explicit failures. Never run this on a user PR for documentation. |
| Archive environment threads | Create threads in two managed worktree environments and invoke environment archive-threads for one; try a local environment separately. | Only the selected managed environment’s active threads are archived; local environments are rejected with HTTP409. |
| Project attachments and history | Upload/download a synthetic file with project attachment; compare bytes; inspect project history and workspace file/path/content commands. | Returned content and history belong to the chosen project/host; missing files report failure. |
| Execution defaults | Set project defaults for environment/provider/model/permissions, open a new root draft and override one choice before sending. | Resolved defaults populate once, explicit draft choices win, and thread details reflect the actual execution options. |
| Clone destination and folder discovery | Browse an empty test host directory and inspect suggested clone path, path existence and invalid destination feedback. | Folder and clone suggestions target the chosen host; existing paths are not overwritten by a failed clone. |
| Delete project | Delete a disposable project through its confirmation flow, then inspect projects and its threads. | Deletion scope matches the confirmation; cancel leaves all state intact. |

## Evidence and cleanup

Expand All @@ -49,8 +51,8 @@ recipe. External writes require a disposable test target and task authorization.

## Maintenance notes

- Create a local project, then use its project actions menu → Rename and reload; compare source project show/update. Project settings contains source controls, not Rename. Source: `apps/app/src/components/project/ProjectActionsMenu.tsx:87; apps/cli/src/commands/project.ts:530`.
- Use two disposable hosts: each project permits one source per host. Select host when adding; update path/default with source operations. Move between hosts by adding/removing sources, not by changing a source host. Source: `apps/cli/src/commands/project.ts:574; apps/app/src/views/ProjectSettingsView.tsx:173`.
- Create a local project, then use its project actions menu → Rename and reload; compare source project show/update. Project settings opens Settings → Projects → project detail, including rename, checkouts, thread defaults, project information, and deletion. Source: `apps/app/src/components/project/ProjectActionsMenu.tsx:87; apps/cli/src/commands/project.ts:530`.
- Use two disposable hosts: each project permits one source per host. Select host when adding; update path/default with source operations. Move between hosts by adding/removing sources, not by changing a source host. Source: `apps/cli/src/commands/project.ts:574; apps/app/src/views/ProjectDetailSettingsView.tsx`.
- Create a project from a local path, then use project source add --clone --remote-url <disposable-remote> --target-path <fresh-path> on a second host. project create does not accept a remote URL. Source: `apps/cli/src/commands/project.ts:487; apps/cli/src/commands/project.ts:574`.
- Await provisioning with thread show; its JSON wraps thread and environment. Initial spawn may have environmentId null. Source: `apps/cli/src/commands/thread/spawn.ts; apps/cli/src/commands/environment.ts:301`.
- Reuse an environment with thread spawn --environment. environment update supports display name and merge-base override only. Test path switching with the supported thread environment-directory action separately. Source: `apps/cli/src/commands/environment.ts:579; apps/cli/src/commands/thread/spawn.ts`.
Expand Down
86 changes: 86 additions & 0 deletions apps/app/src/App.project-settings-routes.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// @vitest-environment jsdom

import type { ReactNode } from "react";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
MemoryRouter,
useLocation,
useNavigate,
useParams,
} from "react-router-dom";
import { AppRoutes } from "./App";

vi.mock("./components/layout/AppLayout", () => ({
AppLayout: ({ children }: { children: ReactNode }) => <>{children}</>,
}));
vi.mock("./views/ProjectDetailSettingsView", () => ({
ProjectDetailSettingsView: () => {
const { projectId } = useParams();
return <h1>Project detail: {projectId}</h1>;
},
}));
vi.mock("./views/SettingsView", () => ({
SettingsView: () => <h1>Settings</h1>,
}));

function NavigationProbe() {
const location = useLocation();
const navigate = useNavigate();
return (
<>
<output>
{location.pathname}
{location.search}
{location.hash}
</output>
<button onClick={() => navigate(-1)}>Back</button>
</>
);
}

function renderRoute(path: string) {
render(
<MemoryRouter initialEntries={["/settings", path]} initialIndex={1}>
<NavigationProbe />
<AppRoutes />
</MemoryRouter>,
);
}

afterEach(cleanup);

describe("project settings routes", () => {
it.each([
"proj_example",
"proj_missing",
"proj_personal",
"project with spaces",
])("opens the existing detail for legacy project %s", async (projectId) => {
renderRoute(
`/projects/${encodeURIComponent(projectId)}/settings?from=bookmark#checkouts`,
);
expect(
await screen.findByRole("heading", {
name: `Project detail: ${projectId}`,
}),
).toBeTruthy();
expect(screen.getByRole("status").textContent).toBe(
`/settings/projects/${encodeURIComponent(projectId)}?from=bookmark#checkouts`,
);
fireEvent.click(screen.getByRole("button", { name: "Back" }));
expect(
await screen.findByRole("heading", { name: "Settings" }),
).toBeTruthy();
expect(screen.getByRole("status").textContent).toBe("/settings");
});

it("keeps the Settings detail destination directly accessible", async () => {
renderRoute("/settings/projects/proj_example");
expect(
await screen.findByRole("heading", {
name: "Project detail: proj_example",
}),
).toBeTruthy();
});
});
31 changes: 22 additions & 9 deletions apps/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
LEGACY_TOOLS_SPLAT_ROUTE_PATH,
PROJECT_ARCHIVED_ROUTE_PATH,
PROJECTLESS_ARCHIVED_ROUTE_PATH,
PROJECT_SETTINGS_ROUTE_PATH,
LEGACY_PROJECT_SETTINGS_ROUTE_PATH,
SETTINGS_PLUGIN_ROUTE_PATH,
SETTINGS_PLUGINS_ROUTE_PATH,
SETTINGS_MACHINE_ROUTE_PATH,
Expand All @@ -53,6 +53,7 @@ import {
getAutomationEditRoutePath,
getAutomationsRoutePath,
getSettingsRoutePath,
getSettingsProjectRoutePath,
getSkillDetailRoutePath,
} from "./lib/route-paths";
import { AppCommandProvider } from "./components/commands/AppCommandProvider";
Expand All @@ -79,15 +80,27 @@ const MachineSettingsView = lazy(() =>
default: m.MachineSettingsView,
})),
);
const ProjectSettingsView = lazy(() =>
import("./views/ProjectSettingsView").then((m) => ({
default: m.ProjectSettingsView,
})),
);
const splitWorkspaceRouteModule = import("./views/SplitWorkspaceRoute");
splitWorkspaceRouteModule.catch(() => {});
const SplitWorkspaceRoute = lazy(() => splitWorkspaceRouteModule);

function LegacyProjectSettingsRedirect() {
const { projectId } = useParams<{ projectId: string }>();
const { search, hash } = useLocation();
return (
<Navigate
to={{
pathname: projectId
? getSettingsProjectRoutePath(projectId)
: getSettingsRoutePath("projects"),
search,
hash,
}}
replace
/>
);
}

export function LegacyAutomationDetailRedirect() {
const location = useLocation();
const { projectId, automationId } = useParams<{
Expand Down Expand Up @@ -208,7 +221,7 @@ export function HashNavigationScroll() {
return null;
}

function AppRoutes() {
export function AppRoutes() {
return (
<AppLayout>
<Suspense fallback={null}>
Expand All @@ -232,8 +245,8 @@ function AppRoutes() {
element={<ProjectDetailSettingsView />}
/>
<Route
path={PROJECT_SETTINGS_ROUTE_PATH}
element={<ProjectSettingsView />}
path={LEGACY_PROJECT_SETTINGS_ROUTE_PATH}
element={<LegacyProjectSettingsRedirect />}
/>
<Route
path={PROJECT_ARCHIVED_ROUTE_PATH}
Expand Down
Loading
Loading