diff --git a/.github/workflows/backend-pr-ci.yml b/.github/workflows/backend-pr-ci.yml new file mode 100644 index 0000000..9e5bf95 --- /dev/null +++ b/.github/workflows/backend-pr-ci.yml @@ -0,0 +1,101 @@ +name: backend-pr-ci + +on: + pull_request: + paths: + - "backend/**" + - ".github/workflows/backend-pr-ci.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: backend-pr-ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + GO_VERSION: "1.24" + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + timeout-minutes: 10 + + defaults: + run: + working-directory: backend + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: backend/go.sum + + - name: Install golangci-lint + run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest + + - name: Run golangci-lint + run: golangci-lint run --timeout=5m + + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 10 + + defaults: + run: + working-directory: backend + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: backend/go.sum + + - name: Build WASM + run: GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o bdd.wasm . + + test: + name: Tests + runs-on: ubuntu-latest + timeout-minutes: 15 + + defaults: + run: + working-directory: backend + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: backend/go.sum + + - name: Run tests (race detector) + run: go test -race -timeout 60s -coverprofile=coverage.out $(go list ./...) + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: backend/coverage.out + retention-days: 7 diff --git a/.github/workflows/frontend-pr-ci.yml b/.github/workflows/frontend-pr-ci.yml new file mode 100644 index 0000000..8798bad --- /dev/null +++ b/.github/workflows/frontend-pr-ci.yml @@ -0,0 +1,53 @@ +name: frontend-pr-ci + +on: + pull_request: + paths: + - "frontend/**" + - ".github/workflows/frontend-pr-ci.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: frontend-pr-ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + web-quality: + name: Typecheck and build frontend + runs-on: ubuntu-latest + timeout-minutes: 20 + + defaults: + run: + working-directory: frontend + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.2.23" + + - name: Cache Bun packages + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('frontend/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Typecheck + run: bun run typecheck + + - name: Build production bundle + run: bun run build diff --git a/.github/workflows/mcp-pr-ci.yml b/.github/workflows/mcp-pr-ci.yml new file mode 100644 index 0000000..1429198 --- /dev/null +++ b/.github/workflows/mcp-pr-ci.yml @@ -0,0 +1,53 @@ +name: mcp-pr-ci + +on: + pull_request: + paths: + - "mcp/**" + - ".github/workflows/mcp-pr-ci.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: mcp-pr-ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + mcp-quality: + name: Typecheck and build MCP server + runs-on: ubuntu-latest + timeout-minutes: 10 + + defaults: + run: + working-directory: mcp + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.2.23" + + - name: Cache Bun packages + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-mcp-${{ hashFiles('mcp/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun-mcp- + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Typecheck + run: bun run typecheck + + - name: Build + run: bun run build diff --git a/backend/go.mod b/backend/go.mod index 6d09bde..805bd6b 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -2,6 +2,6 @@ module github.com/Paca-AI/first-party/bdd go 1.24 -require github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.3 +require github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.5 require github.com/google/uuid v1.6.0 diff --git a/backend/go.sum b/backend/go.sum index 664e182..344bb56 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,4 +1,4 @@ -github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.3 h1:2MFHmdVIKVo1/Cyo7GL3S/LsAFxpsqhJSAbTao+xNEw= -github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.3/go.mod h1:5WeC6cSEf2wM1ovICZbDaVky9oi5id/Qpdfc5LDAQnw= +github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.5 h1:xrEPnJLM2sdwtY/wRaGOdfeDPv5zPUpR8QZJ/w2nQVU= +github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.5/go.mod h1:5WeC6cSEf2wM1ovICZbDaVky9oi5id/Qpdfc5LDAQnw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/backend/scenarios.go b/backend/scenarios.go index ef5f724..a62f7ce 100644 --- a/backend/scenarios.go +++ b/backend/scenarios.go @@ -110,6 +110,8 @@ func (p *bddPlugin) createScenario(req *plugin.Request, res *plugin.Response) { CreatedAt: now, UpdatedAt: now, } + plugin.RecordActivity(taskID, projectID, req.Caller.UserID, "task.bdd_scenario.created", + map[string]any{"title": b.Title}) created(res, scenario) } @@ -225,6 +227,22 @@ func (p *bddPlugin) updateScenario(req *plugin.Request, res *plugin.Response) { res.Error(404, "bdd scenario not found") return } + // Collect which fields changed for the activity record. + var changedFields []string + if b.Title != nil && *b.Title != sc.str("title") { + changedFields = append(changedFields, "title") + } + if b.Given != nil && *b.Given != sc.str("given_text") { + changedFields = append(changedFields, "given") + } + if b.When != nil && *b.When != sc.str("when_text") { + changedFields = append(changedFields, "when") + } + if b.Then != nil && *b.Then != sc.str("then_text") { + changedFields = append(changedFields, "then") + } + plugin.RecordActivity(taskID, projectID, req.Caller.UserID, "task.bdd_scenario.updated", + map[string]any{"title": updTitle, "changes": changedFields}) ok(res, bddScenario{ ID: scenarioID, TaskID: taskID, @@ -247,6 +265,23 @@ func (p *bddPlugin) deleteScenario(req *plugin.Request, res *plugin.Response) { return } + // Fetch title before deletion for the activity record. + titleResult, err := p.db.Query( + `SELECT title FROM bdd_scenarios WHERE id = $1 AND task_id = $2`, + scenarioID, taskID, + ) + if err != nil { + p.log.Error("deleteScenario title fetch: " + err.Error()) + res.Error(500, "failed to delete bdd scenario") + return + } + if len(titleResult.Rows) == 0 { + res.Error(404, "bdd scenario not found") + return + } + scenarioTitleSC := newRowScanner(titleResult.Columns, titleResult.Rows[0]) + scenarioTitle := scenarioTitleSC.str("title") + affected, err := p.db.Exec( `DELETE FROM bdd_scenarios WHERE id = $1 AND task_id = $2`, scenarioID, taskID, @@ -260,6 +295,8 @@ func (p *bddPlugin) deleteScenario(req *plugin.Request, res *plugin.Response) { res.Error(404, "bdd scenario not found") return } + plugin.RecordActivity(taskID, projectID, req.Caller.UserID, "task.bdd_scenario.deleted", + map[string]any{"title": scenarioTitle}) res.NoContent() } diff --git a/frontend/src/BDDScenarioCard.tsx b/frontend/src/BDDScenarioCard.tsx deleted file mode 100644 index e095e1e..0000000 --- a/frontend/src/BDDScenarioCard.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { ChevronDown, ChevronUp, Pencil, Trash2, X, Check } from "lucide-react"; -import { useState } from "react"; -import type { BDDScenario } from "./types"; - -function cn(...classes: (string | boolean | undefined | null)[]): string { - return classes.filter(Boolean).join(" "); -} - -interface BDDScenarioCardProps { - scenario: BDDScenario; - canEdit: boolean; - onUpdate: ( - id: string, - patch: { title?: string; given?: string; when?: string; then?: string }, - ) => void; - onDelete: (id: string) => void; -} - -export function BDDScenarioCard({ - scenario, - canEdit, - onUpdate, - onDelete, -}: BDDScenarioCardProps) { - const [expanded, setExpanded] = useState(false); - const [editing, setEditing] = useState(false); - const [draft, setDraft] = useState({ - title: scenario.title, - given: scenario.given, - when: scenario.when, - then: scenario.then, - }); - - const startEdit = () => { - setDraft({ - title: scenario.title, - given: scenario.given, - when: scenario.when, - then: scenario.then, - }); - setEditing(true); - setExpanded(true); - }; - - const cancelEdit = () => { - setEditing(false); - setDraft({ - title: scenario.title, - given: scenario.given, - when: scenario.when, - then: scenario.then, - }); - }; - - const submitEdit = () => { - if (!draft.title.trim()) return; - onUpdate(scenario.id, { - title: draft.title.trim(), - given: draft.given, - when: draft.when, - then: draft.then, - }); - setEditing(false); - }; - - const hasContent = scenario.given || scenario.when || scenario.then; - - return ( -
- {/* Header */} -
- - - {canEdit && ( -
- {editing ? ( - <> - - - - ) : ( - <> - - - - )} -
- )} -
- - {/* Body */} - {(expanded || editing) && ( -
- {(["given", "when", "then"] as const).map((field) => ( -
- - {field} - - {editing ? ( -