Skip to content
Closed
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

Large diffs are not rendered by default.

52 changes: 26 additions & 26 deletions .agent/specs/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -319,28 +319,6 @@
"phaseCount": 5,
"taskCount": 15
},
"2511281500": {
"folder": "2511281500-preview-config-ui",
"path": "todo/2511281500-preview-config-ui/spec.md",
"spec_type": "feature",
"status": "completed",
"created": "2025-11-28T15:00:00Z",
"updated": "2025-11-28T17:30:00Z",
"totalComplexity": 38,
"phaseCount": 3,
"taskCount": 9
},
"2511282130": {
"folder": "2511282130-server-start-consolidation",
"path": "todo/2511282130-server-start-consolidation/spec.md",
"spec_type": "issue",
"status": "completed",
"created": "2025-11-28T21:30:00Z",
"updated": "2025-11-28T22:10:00Z",
"totalComplexity": 47,
"phaseCount": 1,
"taskCount": 9
},
"2511281459": {
"folder": "2511281459-inngest-config-consolidation",
"path": "todo/2511281459-inngest-config-consolidation/spec.md",
Expand All @@ -352,6 +330,17 @@
"phaseCount": 3,
"taskCount": 8
},
"2511281500": {
"folder": "2511281500-preview-config-ui",
"path": "todo/2511281500-preview-config-ui/spec.md",
"spec_type": "feature",
"status": "completed",
"created": "2025-11-28T15:00:00Z",
"updated": "2025-11-28T17:30:00Z",
"totalComplexity": 38,
"phaseCount": 3,
"taskCount": 9
},
"2511282026": {
"folder": "2511282026-websocket-reconnect-ux",
"path": "done/2511282026-websocket-reconnect-ux/spec.md",
Expand All @@ -363,6 +352,17 @@
"phaseCount": 1,
"taskCount": 9
},
"2511282130": {
"folder": "2511282130-server-start-consolidation",
"path": "todo/2511282130-server-start-consolidation/spec.md",
"spec_type": "issue",
"status": "completed",
"created": "2025-11-28T21:30:00Z",
"updated": "2025-11-28T22:10:00Z",
"totalComplexity": 47,
"phaseCount": 1,
"taskCount": 9
},
"2511290735": {
"folder": "2511290735-session-followup-action",
"path": "todo/2511290735-session-followup-action/spec.md",
Expand All @@ -387,11 +387,11 @@
},
"2511291528": {
"folder": "2511291528-e2e-test-suite",
"path": "todo/2511291528-e2e-test-suite/spec.md",
"path": "done/2511291528-e2e-test-suite/spec.md",
"spec_type": "feature",
"status": "draft",
"status": "completed",
"created": "2025-11-29T22:28:43Z",
"updated": "2025-11-29T22:28:43Z",
"updated": "2025-11-30T06:54:00.000Z",
"totalComplexity": 147,
"phaseCount": 6,
"taskCount": 31
Expand All @@ -408,4 +408,4 @@
"taskCount": 19
}
}
}
}
2 changes: 1 addition & 1 deletion .claude/commands/refresh-spec-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Scans spec folders and reconciles with index.json, removing orphaned entries, ad

## Instructions

- Scan all spec folders in backlog/todo/in-progress/done
- Scan all spec folders in backlog/todo/done
- Compare to index.json entries
- Remove entries for deleted folders
- Add entries for untracked folders
Expand Down
23 changes: 23 additions & 0 deletions apps/app/e2e/fixtures/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ import { fileURLToPath } from "node:url";
import {
seedProject,
seedSession,
seedTestProject,
seedWorkflowDefinition,
seedSpecFile,
seedFileChange,
type SeedProjectOptions,
type SeedSessionOptions,
type SeedTestProjectOptions,
type SeedWorkflowDefinitionOptions,
type SeedSpecFileOptions,
} from "../utils/seed-database";

const __filename = fileURLToPath(import.meta.url);
Expand Down Expand Up @@ -40,6 +47,10 @@ export interface DatabaseFixtures {
seedProjects: (options: SeedProjectOptions[]) => Promise<Awaited<ReturnType<typeof seedProject>>[]>;
seedSession: (options: SeedSessionOptions) => ReturnType<typeof seedSession>;
seedSessions: (options: SeedSessionOptions[]) => Promise<Awaited<ReturnType<typeof seedSession>>[]>;
seedTestProject: (options?: SeedTestProjectOptions) => ReturnType<typeof seedTestProject>;
seedWorkflowDefinition: (options: SeedWorkflowDefinitionOptions) => ReturnType<typeof seedWorkflowDefinition>;
seedSpecFile: (projectPath: string, options?: SeedSpecFileOptions) => ReturnType<typeof seedSpecFile>;
seedFileChange: (projectPath: string, filename: string, content: string) => ReturnType<typeof seedFileChange>;
};
}

Expand Down Expand Up @@ -74,6 +85,18 @@ export const test = base.extend<DatabaseFixtures>({
seedSessions: async (options: SeedSessionOptions[]) => {
return Promise.all(options.map((opt) => seedSession(prisma, opt)));
},
seedTestProject: (options?: SeedTestProjectOptions) => {
return seedTestProject(prisma, options);
},
seedWorkflowDefinition: (options: SeedWorkflowDefinitionOptions) => {
return seedWorkflowDefinition(prisma, options);
},
seedSpecFile: (projectPath: string, options?: SeedSpecFileOptions) => {
return seedSpecFile(projectPath, options);
},
seedFileChange: (projectPath: string, filename: string, content: string) => {
return seedFileChange(projectPath, filename, content);
},
};

// eslint-disable-next-line react-hooks/rules-of-hooks
Expand Down
1 change: 1 addition & 0 deletions apps/app/e2e/fixtures/test-project
Submodule test-project added at 72256c
7 changes: 5 additions & 2 deletions apps/app/e2e/pages/DashboardPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,13 @@ export class DashboardPage extends BasePage {

/**
* Complete logout flow via UI
* Opens user menu and clicks logout
* Opens user menu, clicks logout, and waits for confirmation
*/
async logout() {
await this.openUserMenu();
await this.clickLogout();
// Wait for logout success toast to confirm action completed
await this.page.locator('text="Logged out successfully"').waitFor({ timeout: 5000 });
}

/**
Expand All @@ -84,10 +86,11 @@ export class DashboardPage extends BasePage {
}

/**
* Assert redirected to login page
* Assert redirected to login page and wait for load
*/
async expectRedirectedToLogin() {
await this.page.waitForURL(/\/login/, { timeout: 10000 });
await this.page.waitForLoadState("networkidle");
expect(this.page.url()).toContain("/login");
}

Expand Down
115 changes: 115 additions & 0 deletions apps/app/e2e/pages/GitPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import type { Page, Locator } from "@playwright/test";
import { expect } from "@playwright/test";
import { BasePage } from "./BasePage";

/**
* Git Page Object
*
* Git operations page with Changes, History, and Branches tabs
*/
export class GitPage extends BasePage {
constructor(page: Page) {
super(page);
}

async goto(projectId: string) {
await this.page.goto(`/projects/${projectId}/git`);
}

/**
* Click tab (Changes, History, Branches)
*/
async clickTab(tabName: "Changes" | "History" | "Branches") {
await this.page.locator(`button:has-text("${tabName}"), [role="tab"]:has-text("${tabName}")`).first().click();
}

// ========================================
// Changes Tab
// ========================================

/**
* Get unstaged files list
*/
getUnstagedFiles(): Locator {
return this.getByTestId("unstaged-file");
}

/**
* Stage a file
*/
async stageFile(filename: string) {
const file = this.getUnstagedFiles().filter({ hasText: filename });
await file.locator('button:has-text("Stage"), [data-testid="stage-button"]').first().click();
}

/**
* Fill commit message
*/
async fillCommitMessage(message: string) {
await this.getByTestId("commit-message-input").fill(message);
}

/**
* Click commit button
*/
async clickCommit() {
await this.getByTestId("commit-button").click();
}

// ========================================
// History Tab
// ========================================

/**
* Get commit card
*/
getCommitCard(message: string): Locator {
return this.getByTestId("commit-card").filter({ hasText: message });
}

/**
* Assert commit visible in history
*/
async expectCommitVisible(message: string) {
await expect(this.getCommitCard(message)).toBeVisible({ timeout: 10000 });
}

// ========================================
// Branches Tab
// ========================================

/**
* Click Create Branch button
*/
async clickCreateBranch() {
await this.page.locator('button:has-text("Create Branch"), [data-testid="create-branch-button"]').first().click();
}

/**
* Fill branch name
*/
async fillBranchName(name: string) {
await this.getByTestId("branch-name-input").fill(name);
}

/**
* Submit branch creation
*/
async submitBranch() {
await this.getByTestId("branch-submit-button").click();
}

/**
* Get current branch badge
*/
getCurrentBranch(): Locator {
return this.getByTestId("current-branch-badge");
}

/**
* Assert current branch
*/
async expectCurrentBranch(branchName: string) {
await expect(this.getCurrentBranch()).toContainText(branchName);
}
}
62 changes: 62 additions & 0 deletions apps/app/e2e/pages/NewWorkflowRunPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { Page } from "@playwright/test";
import { BasePage } from "./BasePage";

/**
* New Workflow Run Page Object
*
* Form for creating a new workflow run
*/
export class NewWorkflowRunPage extends BasePage {
constructor(page: Page) {
super(page);
}

async goto(projectId: string) {
await this.page.goto(`/projects/${projectId}/workflows/new`);
}

/**
* Select workflow definition
*/
async selectWorkflowDefinition(identifier: string) {
await this.getByTestId("workflow-definition-select").click();
await this.page.locator(`[data-testid="workflow-option-${identifier}"], text="${identifier}"`).first().click();
}

/**
* Fill run name
*/
async fillRunName(name: string) {
await this.getByTestId("workflow-run-name-input").fill(name);
}

/**
* Attach spec file
*/
async attachSpecFile(specFile: string) {
await this.getByTestId("spec-file-input").fill(specFile);
}

/**
* Submit form
*/
async submitForm() {
await this.getByTestId("workflow-run-submit").click();
}

/**
* Create workflow run (full flow)
*/
async createWorkflowRun(options: {
workflowId: string;
runName: string;
specFile?: string;
}) {
await this.selectWorkflowDefinition(options.workflowId);
await this.fillRunName(options.runName);
if (options.specFile) {
await this.attachSpecFile(options.specFile);
}
await this.submitForm();
}
}
Loading