Files
openobserve/.github/workflows/generate-unit-tests.yml
T

307 lines
12 KiB
YAML

name: Generate Unit Tests (reusable)
# Reusable workflow — called from unit-tests.yml.
# Must receive caller context as inputs because inside a workflow_call the
# `github.event_name` is always "workflow_call", not "pull_request"/"push".
on:
workflow_call:
inputs:
event_name:
description: 'Triggering event (pull_request or push)'
required: true
type: string
base_ref:
description: 'Base branch for comparison (e.g. main)'
required: false
type: string
default: 'main'
secrets:
ANTHROPIC_API_KEY:
required: true
GITHUB_TOKEN:
required: false
permissions: {}
jobs:
generate:
runs-on:
labels: ubicloud-standard-8
permissions:
contents: write
id-token: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
# Full history required to diff against PR base and check git log timestamps.
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Get changed web source files
id: changed_files
run: |
# For PRs: compare the full PR branch against its base branch.
# Handles rebase / force-push correctly — HEAD~1 would be wrong after a rebase.
# For pushes to main: compare only the latest commit.
if [ "${{ inputs.event_name }}" = "pull_request" ]; then
BASE_REF="origin/${{ inputs.base_ref }}"
else
BASE_REF="HEAD~1"
fi
echo "Using base ref: $BASE_REF"
git fetch origin "${{ inputs.base_ref }}" --depth=1 2>/dev/null || true
CHANGED=$(git diff --name-only "$BASE_REF" HEAD -- 'web/src/**' \
| grep -E '\.(ts|vue|js)$' \
| grep -v '\.spec\.' \
| grep -v '__tests__' \
| grep -v '/test/' \
| grep -v '\.d\.ts$' \
|| true)
echo "changed_files<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGED" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
if [ -n "$CHANGED" ]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "Changed source files requiring test review:"
echo "$CHANGED"
else
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "No source files changed in web/src/ — skipping test generation"
fi
# Expose resolved base ref for use in the Claude prompt
echo "base_ref=$BASE_REF" >> $GITHUB_OUTPUT
- name: Setup Node.js
if: steps.changed_files.outputs.has_changes == 'true'
uses: actions/setup-node@v5
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- name: Install dependencies
if: steps.changed_files.outputs.has_changes == 'true'
working-directory: web
run: npm ci
- name: Configure git
if: steps.changed_files.outputs.has_changes == 'true'
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
- name: Generate/Update Unit Tests with Claude
if: steps.changed_files.outputs.has_changes == 'true'
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
track_progress: true
claude_args: |
--allowedTools "Read,Write,Edit,Glob,Grep,Bash"
prompt: |
You are a senior frontend engineer maintaining unit tests for the OpenObserve web application.
## Event Context
- Event type: ${{ inputs.event_name }}
- Base ref for diff: ${{ steps.changed_files.outputs.base_ref }}
## Changed Source Files
The following source files changed and need their unit tests reviewed:
```
${{ steps.changed_files.outputs.changed_files }}
```
---
## For EACH file above, execute this exact sequence:
---
### PHASE 1 — Assess (decide what action is needed)
**1a. Read the source file completely.**
**1b. Get the full diff of what changed in this file:**
```bash
git diff ${{ steps.changed_files.outputs.base_ref }} HEAD -- <file_path>
```
**1c. Find the spec file** (same path, `.spec.ts` extension):
- `web/src/components/Foo.vue` → `web/src/components/Foo.spec.ts`
- `web/src/utils/helper.ts` → `web/src/utils/helper.spec.ts`
- `web/src/composables/useFoo.ts` → `web/src/composables/useFoo.spec.ts`
**1d. If spec file exists — check if it is stale:**
```bash
# When was the test file last updated?
git log --follow -1 --format="%H %ar %s" -- <spec_file_path>
# When was the source file last updated?
git log --follow -1 --format="%H %ar %s" -- <source_file_path>
```
**1e. Make ONE of these four decisions:**
| Decision | Condition |
|----------|-----------|
| **CREATE** | No spec file exists |
| **UPDATE** | Spec file exists BUT source changed after last spec update, OR diff reveals new/removed/changed logic not reflected in tests |
| **CLEANUP-ONLY** | Spec file exists, recently updated, but diff shows some tests now reference removed functionality |
| **SKIP** | Spec file exists, updated after source, diff shows no logic changes — tests already in sync |
If **SKIP**: print `[SKIP] <file> — tests already up-to-date` and move to the next file.
---
### PHASE 2 — Execute the decision
#### For CREATE or UPDATE:
Read these real examples to learn the exact coding conventions:
```bash
cat web/src/components/iam/organizations/AppOrganizations.spec.ts
cat web/src/components/ConfirmDialog.spec.ts
```
Then write/update the spec file with this structure:
```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { mount, VueWrapper } from "@vue/test-utils";
import { createStore } from "vuex";
import ComponentName from "./ComponentName.vue";
// Mocks MUST be at top level (hoisted by vitest)
vi.mock("@/aws-exports", () => ({
default: { isEnterprise: "false", isCloud: "false" },
}));
vi.mock("@/services/someService", () => ({
default: {
getItems: vi.fn().mockResolvedValue({ data: [] }),
},
}));
describe("ComponentName.vue", () => {
let wrapper: VueWrapper<any>;
const createMockStore = (overrides: Record<string, any> = {}) =>
createStore({
state: {
selectedOrganization: { identifier: "default", label: "Default", id: 1 },
zoConfig: { version: "v0.2.0", ...overrides.zoConfig },
...overrides.state,
},
mutations: {
setOrg(state: any, org: any) { state.selectedOrganization = org; },
...overrides.mutations,
},
actions: { ...overrides.actions },
});
beforeEach(() => { vi.clearAllMocks(); });
afterEach(() => { if (wrapper) wrapper.unmount(); });
describe("Initialization", () => {
it("should mount successfully", () => {
wrapper = mount(ComponentName, { global: { plugins: [createMockStore()] } });
expect(wrapper.exists()).toBe(true);
});
});
describe("Core Functionality", () => { /* ... */ });
describe("Error Handling", () => { /* ... */ });
describe("Edge Cases", () => { /* ... */ });
});
```
**Test every code path introduced by the diff:**
- ✅ New props/emits added → test them
- ✅ New methods/computed → test each branch
- ✅ New API calls → test success + failure
- ✅ New conditional rendering → test each condition
- ✅ Changed watcher logic → test old + new trigger conditions
- ✅ Removed functionality → remove the corresponding tests
**Technical rules:**
- `vi.mock()` calls must be at module top level (before `describe`)
- Always `vi.clearAllMocks()` in `beforeEach`
- Always `wrapper.unmount()` in `afterEach`
- Use `await wrapper.vm.$nextTick()` after state mutations before DOM assertions
- Use `await flushPromises()` (from `@vue/test-utils`) after async operations
- Mock child components that have external deps: `{ name: "Foo", template: "<div/>" }`
#### For CLEANUP-ONLY:
- Remove tests that reference functions/props/events no longer in the source
- Verify each removed test actually has no corresponding source construct
- Do NOT remove tests for things that still exist
---
### PHASE 3 — Self-healing test verification (max 3 attempts)
After writing/updating the spec file, run it:
```bash
cd web && npx vitest run --reporter=verbose --environment jsdom <path-to-spec-file> 2>&1
```
**Attempt loop (repeat up to 3 times):**
- **If all tests pass** → proceed to Phase 4
- **If tests fail:**
1. Read the full error output carefully
2. Identify root cause: wrong mock, missing await, wrong assertion, import error
3. Fix the specific failing test(s) — do NOT delete tests to make them pass
4. Re-run the spec file
5. If still failing after 3 attempts → print `[WARN] Could not fully heal <spec-file> — leaving with best-effort fixes` and proceed
**Never commit a spec file that introduces new test failures.**
---
### PHASE 4 — Commit only if something actually changed
After all files have been processed:
```bash
# Check what changed
git status --short
# Stage only spec files
find web/src -name "*.spec.ts" -exec git add {} \;
# Only commit if there are staged changes
git diff --cached --quiet || git commit -m "test: auto-update unit tests for recent changes"
# Push to current branch
git push origin HEAD
```
---
## ABSOLUTE RULES
1. **DO NOT skip any file in the list** — assess every one, even if the decision is SKIP
2. **DO NOT write hollow tests** — every `it()` must have real assertions
3. **DO NOT delete tests just to fix failures** — fix the root cause
4. **DO NOT commit if tests fail** — only push when the spec file passes
5. **ONE commit at the end** — not one commit per file
6. **DO NOT modify source files** — only touch `*.spec.ts` files
7. **Run this workflow once** — commit with the exact message above so the bot-actor guard prevents re-triggering
## Environment
- Setup file: `web/src/test/unit/helpers/setupTests.ts` (auto-loaded)
- Vitest config: `web/vitest.config.ts`
- Test environment: jsdom
- Coverage thresholds: 33% lines/functions/statements, 40% branches
- Stack: Vue 3 + Vuex 4 + Vue Router 4 + vue-i18n 9 + Quasar 2 + Vitest + @vue/test-utils 2
- All `npm` commands must be run from the `web/` directory