Merge origin/main into feature/shared-packages

Resolved conflicts:
- list.ts: Keep @automaker/git-utils import, add worktree-metadata import
- feature-loader.ts: Use Feature type from @automaker/types
- automaker-paths.test.ts: Import from @automaker/platform
- kanban-card.tsx: Accept deletion (split into components/)
- subprocess.test.ts: Keep libs/platform location

Added missing exports to @automaker/platform:
- getGlobalSettingsPath, getCredentialsPath, getProjectSettingsPath, ensureDataDir

Added title and titleGenerating fields to @automaker/types Feature interface.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kacper
2025-12-20 22:20:17 +01:00
108 changed files with 10834 additions and 3489 deletions

View File

@@ -53,13 +53,14 @@ test.describe("Spec Editor Persistence", () => {
// Step 6: Wait for CodeMirror to initialize (it has a .cm-content element)
await specEditor.locator(".cm-content").waitFor({ state: "visible", timeout: 10000 });
// Small delay to ensure editor is fully initialized
await page.waitForTimeout(500);
// Step 7: Modify the editor content to "hello world"
await setEditorContent(page, "hello world");
// Step 8: Click the save button
// Verify content was set before saving
const contentBeforeSave = await getEditorContent(page);
expect(contentBeforeSave.trim()).toBe("hello world");
// Step 8: Click the save button and wait for save to complete
await clickSaveButton(page);
// Step 9: Refresh the page
@@ -77,8 +78,43 @@ test.describe("Spec Editor Persistence", () => {
const specEditorAfterReload = await getByTestId(page, "spec-editor");
await specEditorAfterReload.locator(".cm-content").waitFor({ state: "visible", timeout: 10000 });
// Small delay to ensure editor content is loaded
await page.waitForTimeout(500);
// Wait for CodeMirror content to update with the loaded spec
// The spec might need time to load into the editor after page reload
let contentMatches = false;
let attempts = 0;
const maxAttempts = 30; // Try for up to 30 seconds with 1-second intervals
while (!contentMatches && attempts < maxAttempts) {
try {
const contentElement = page.locator('[data-testid="spec-editor"] .cm-content');
const text = await contentElement.textContent();
if (text && text.trim() === "hello world") {
contentMatches = true;
break;
}
} catch (e) {
// Element might not be ready yet, continue
}
if (!contentMatches) {
await page.waitForTimeout(1000);
attempts++;
}
}
// If we didn't get the right content with our polling, use the fallback
if (!contentMatches) {
await page.waitForFunction(
(expectedContent) => {
const contentElement = document.querySelector('[data-testid="spec-editor"] .cm-content');
if (!contentElement) return false;
const text = (contentElement.textContent || "").trim();
return text === expectedContent;
},
"hello world",
{ timeout: 10000 }
);
}
// Step 11: Verify the content was persisted
const persistedContent = await getEditorContent(page);

View File

@@ -57,3 +57,22 @@ export async function getCategoryOption(
.replace(/\s+/g, "-")}`;
return page.locator(`[data-testid="${optionTestId}"]`);
}
/**
* Click the "Create new" option for a category that doesn't exist
*/
export async function clickCreateNewCategoryOption(
page: Page
): Promise<void> {
const option = page.locator('[data-testid="category-option-create-new"]');
await option.click();
}
/**
* Get the "Create new" option element for categories
*/
export async function getCreateNewCategoryOption(
page: Page
): Promise<Locator> {
return page.locator('[data-testid="category-option-create-new"]');
}

View File

@@ -60,12 +60,16 @@ export async function navigateToSpecEditor(page: Page): Promise<void> {
/**
* Get the CodeMirror editor content
* Waits for CodeMirror to be ready and returns the content
*/
export async function getEditorContent(page: Page): Promise<string> {
// CodeMirror uses a contenteditable div with class .cm-content
const content = await page
.locator('[data-testid="spec-editor"] .cm-content')
.textContent();
// Wait for it to be visible and then read its textContent
const contentElement = page.locator('[data-testid="spec-editor"] .cm-content');
await contentElement.waitFor({ state: "visible", timeout: 10000 });
// Read the content - CodeMirror should have updated its DOM by now
const content = await contentElement.textContent();
return content || "";
}

View File

@@ -850,6 +850,58 @@ test.describe("Worktree Integration Tests", () => {
expect(featureData.status).toBe("backlog");
});
test("should auto-select worktree after creating feature with new branch", async ({
page,
}) => {
await setupProjectWithPath(page, testRepo.path);
await page.goto("/");
await waitForNetworkIdle(page);
await waitForBoardView(page);
// Use a branch name that doesn't exist yet
const branchName = "feature/auto-select-worktree";
// Verify branch does NOT exist before we create the feature
const branchesBefore = await listBranches(testRepo.path);
expect(branchesBefore).not.toContain(branchName);
// Click add feature button
await clickAddFeature(page);
// Fill in the feature details with the new branch
await fillAddFeatureDialog(page, "Feature with auto-select worktree", {
branch: branchName,
category: "Testing",
});
// Confirm
await confirmAddFeature(page);
// Wait for feature to be saved and worktree to be created
// Also wait for the worktree to appear in the UI and be auto-selected
await page.waitForTimeout(2000);
// Wait for the worktree button to appear in the UI
// Worktree buttons are actual <button> elements (not divs with role="button" like kanban cards)
// and have a title attribute like "Click to switch to this worktree's branch"
const worktreeButton = page
.locator('button[title*="worktree"], button[title*="branch"]')
.filter({ hasText: new RegExp(branchName.replace("/", "\\/"), "i") })
.first();
await expect(worktreeButton).toBeVisible({ timeout: 10000 });
// Verify the worktree is auto-selected by checking if the feature is visible
// Features are filtered by the selected worktree, so if the feature is visible,
// it means the worktree was auto-selected after creation
const featureText = page.getByText("Feature with auto-select worktree");
await expect(featureText).toBeVisible({ timeout: 10000 });
// Additional verification: Check that the button has the selected styling
// Selected worktree buttons have variant="default" which applies bg-primary class
// We verify this by checking the button has the primary background styling
await expect(worktreeButton).toHaveClass(/bg-primary/, { timeout: 5000 });
});
test("should reset feature branch and worktree when worktree is deleted", async ({
page,
}) => {
@@ -1217,7 +1269,11 @@ test.describe("Worktree Integration Tests", () => {
// Worktree Feature Flag Disabled
// ==========================================================================
test("should not show worktree panel when useWorktrees is disabled", async ({
// Skip: This test is flaky because the WorktreePanel component always renders
// the "Branch:" label and switch branch button, even when useWorktrees is disabled.
// The component only conditionally hides the "Worktrees:" section, not the entire panel.
// The test expectations don't match the current implementation.
test.skip("should not show worktree panel when useWorktrees is disabled", async ({
page,
}) => {
// Use the setup function that disables worktrees
@@ -1235,7 +1291,12 @@ test.describe("Worktree Integration Tests", () => {
await expect(branchSwitchButton).not.toBeVisible();
});
test("should allow creating and moving features when worktrees are disabled", async ({
// Skip: The WorktreePanel component always renders the "Branch:" label
// and main worktree tab, regardless of useWorktrees setting.
// It only conditionally hides the "Worktrees:" section.
// This test is unreliable because it tests implementation details that
// don't match the current component behavior.
test.skip("should allow creating and moving features when worktrees are disabled", async ({
page,
}) => {
// Use the setup function that disables worktrees
@@ -2615,4 +2676,248 @@ test.describe("Worktree Integration Tests", () => {
// worktreePath should not exist in the feature data (worktrees are created at execution time)
expect(featureData.worktreePath).toBeUndefined();
});
// ==========================================================================
// PR URL Tracking Tests
// ==========================================================================
test("feature should support prUrl field for tracking pull request URLs", async ({
page,
}) => {
await setupProjectWithPath(page, testRepo.path);
await page.goto("/");
await waitForNetworkIdle(page);
await waitForBoardView(page);
// Create a feature
await clickAddFeature(page);
await fillAddFeatureDialog(page, "Feature for PR URL test", {
category: "Testing",
});
await confirmAddFeature(page);
await page.waitForTimeout(1000);
// Verify feature was created
const featuresDir = path.join(testRepo.path, ".automaker", "features");
const featureDirs = fs.readdirSync(featuresDir);
const featureDir = featureDirs.find((dir) => {
const featureFilePath = path.join(featuresDir, dir, "feature.json");
if (fs.existsSync(featureFilePath)) {
const data = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
return data.description === "Feature for PR URL test";
}
return false;
});
expect(featureDir).toBeDefined();
// Manually update the feature.json file to add prUrl (simulating what happens after PR creation)
const featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
const featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
featureData.prUrl = "https://github.com/test/repo/pull/123";
fs.writeFileSync(featureFilePath, JSON.stringify(featureData, null, 2));
// Reload the page to pick up the change
await page.reload();
await waitForNetworkIdle(page);
await waitForBoardView(page);
await page.waitForTimeout(1000);
// Verify the PR URL link is displayed on the card
const prUrlLink = page.locator(`[data-testid="pr-url-${featureData.id}"]`);
await expect(prUrlLink).toBeVisible({ timeout: 5000 });
await expect(prUrlLink).toHaveText(/Pull Request/);
await expect(prUrlLink).toHaveAttribute(
"href",
"https://github.com/test/repo/pull/123"
);
});
test("prUrl should persist when updating feature", async ({ page }) => {
await setupProjectWithPath(page, testRepo.path);
await page.goto("/");
await waitForNetworkIdle(page);
await waitForBoardView(page);
// Create a feature
await clickAddFeature(page);
await fillAddFeatureDialog(page, "Feature with PR URL persistence", {
category: "Testing",
});
await confirmAddFeature(page);
await page.waitForTimeout(1000);
// Find the feature file
const featuresDir = path.join(testRepo.path, ".automaker", "features");
const featureDirs = fs.readdirSync(featuresDir);
const featureDir = featureDirs.find((dir) => {
const featureFilePath = path.join(featuresDir, dir, "feature.json");
if (fs.existsSync(featureFilePath)) {
const data = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
return data.description === "Feature with PR URL persistence";
}
return false;
});
expect(featureDir).toBeDefined();
// Add prUrl to the feature
const featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
let featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
const originalPrUrl = "https://github.com/test/repo/pull/456";
featureData.prUrl = originalPrUrl;
fs.writeFileSync(featureFilePath, JSON.stringify(featureData, null, 2));
// Reload the page
await page.reload();
await waitForNetworkIdle(page);
await waitForBoardView(page);
await page.waitForTimeout(1000);
// Open edit dialog by double-clicking the feature card
const featureCard = page.getByText("Feature with PR URL persistence");
await featureCard.dblclick();
await page.waitForTimeout(500);
// Wait for edit dialog to open
const editDialog = page.locator('[data-testid="edit-feature-dialog"]');
await expect(editDialog).toBeVisible({ timeout: 5000 });
// Update the description - wait for the textarea to be visible
const descInput = page.locator(
'[data-testid="feature-description-input"]'
);
await expect(descInput).toBeVisible({ timeout: 5000 });
await descInput.fill("Feature with PR URL persistence - updated");
// Save the feature
const saveButton = page.locator('[data-testid="confirm-edit-feature"]');
await saveButton.click();
await page.waitForTimeout(1000);
// Verify prUrl was preserved
featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
expect(featureData.prUrl).toBe(originalPrUrl);
expect(featureData.description).toBe(
"Feature with PR URL persistence - updated"
);
});
test("feature in waiting_approval with prUrl should show Verify button instead of Commit", async ({
page,
}) => {
await setupProjectWithPath(page, testRepo.path);
await page.goto("/");
await waitForNetworkIdle(page);
await waitForBoardView(page);
// Create a feature
await clickAddFeature(page);
await fillAddFeatureDialog(page, "Feature with PR for verify test", {
category: "Testing",
});
await confirmAddFeature(page);
await page.waitForTimeout(1000);
// Find the feature file
const featuresDir = path.join(testRepo.path, ".automaker", "features");
const featureDirs = fs.readdirSync(featuresDir);
const featureDir = featureDirs.find((dir) => {
const featureFilePath = path.join(featuresDir, dir, "feature.json");
if (fs.existsSync(featureFilePath)) {
const data = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
return data.description === "Feature with PR for verify test";
}
return false;
});
expect(featureDir).toBeDefined();
// Update the feature to waiting_approval status with a prUrl
const featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
let featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
featureData.status = "waiting_approval";
featureData.prUrl = "https://github.com/test/repo/pull/789";
fs.writeFileSync(featureFilePath, JSON.stringify(featureData, null, 2));
// Reload the page to pick up the changes
await page.reload();
await waitForNetworkIdle(page);
await waitForBoardView(page);
await page.waitForTimeout(1000);
// Verify the feature card is in the waiting_approval column
const waitingApprovalColumn = page.locator(
'[data-testid="kanban-column-waiting_approval"]'
);
const featureCard = waitingApprovalColumn.locator(
`[data-testid="kanban-card-${featureData.id}"]`
);
await expect(featureCard).toBeVisible({ timeout: 5000 });
// Verify the Verify button is visible (not Commit button)
const verifyButton = page.locator(`[data-testid="verify-${featureData.id}"]`);
await expect(verifyButton).toBeVisible({ timeout: 5000 });
// Verify the Commit button is NOT visible
const commitButton = page.locator(`[data-testid="commit-${featureData.id}"]`);
await expect(commitButton).not.toBeVisible({ timeout: 2000 });
});
test("feature in waiting_approval without prUrl should show Mark as Verified button", async ({
page,
}) => {
await setupProjectWithPath(page, testRepo.path);
await page.goto("/");
await waitForNetworkIdle(page);
await waitForBoardView(page);
// Create a feature
await clickAddFeature(page);
await fillAddFeatureDialog(page, "Feature without PR for mark as verified test", {
category: "Testing",
});
await confirmAddFeature(page);
await page.waitForTimeout(1000);
// Find the feature file
const featuresDir = path.join(testRepo.path, ".automaker", "features");
const featureDirs = fs.readdirSync(featuresDir);
const featureDir = featureDirs.find((dir) => {
const featureFilePath = path.join(featuresDir, dir, "feature.json");
if (fs.existsSync(featureFilePath)) {
const data = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
return data.description === "Feature without PR for mark as verified test";
}
return false;
});
expect(featureDir).toBeDefined();
// Update the feature to waiting_approval status WITHOUT prUrl
const featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
let featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
featureData.status = "waiting_approval";
// Explicitly do NOT set prUrl
fs.writeFileSync(featureFilePath, JSON.stringify(featureData, null, 2));
// Reload the page to pick up the changes
await page.reload();
await waitForNetworkIdle(page);
await waitForBoardView(page);
await page.waitForTimeout(1000);
// Verify the feature card is in the waiting_approval column
const waitingApprovalColumn = page.locator(
'[data-testid="kanban-column-waiting_approval"]'
);
const featureCard = waitingApprovalColumn.locator(
`[data-testid="kanban-card-${featureData.id}"]`
);
await expect(featureCard).toBeVisible({ timeout: 5000 });
// Verify the Mark as Verified button is visible
const markAsVerifiedButton = page.locator(`[data-testid="mark-as-verified-${featureData.id}"]`);
await expect(markAsVerifiedButton).toBeVisible({ timeout: 5000 });
// Verify the Verify button is NOT visible
const verifyButton = page.locator(`[data-testid="verify-${featureData.id}"]`);
await expect(verifyButton).not.toBeVisible({ timeout: 2000 });
});
});