test: add unit tests with vitest #265

Merged
davide merged 5 commits from test/unit-tests into main 2026-09-08 20:42:02 +00:00
Member

What

First step of the testing strategy (issue #264): Layer 1 — unit tests with Vitest.

  • Tooling: vitest + @vitest/coverage-v8 devDependencies, test / test:unit / test:coverage scripts, vitest.config.ts (node env), test/setup.ts that sets the env vars src/forgejo/fetch.ts reads at import time (FORGEJO_API_URL, CTX_AUTH_TOKEN, CTX_AUTH_USERNAME).
  • Tests (30 total, all passing):
    • src/prompt.test.tsbuildPrompt for issue/PR/review-requested events, branch + tool instructions, empty body, comment ordering.
    • src/schemas.test.ts — event name validation + contract tests validating every payload in examples/event_payloads/ against the schemas.
    • src/context.test.tsgetAuthContext env parsing, getEventContext (issue vs PR, comments, invalid env) with the Forgejo client mocked.
    • src/tools.test.ts — all 5 tools: default repository/issueId fallback, explicit params, and the create-pr-review side/lineold_position/new_position mapping.
    • src/git.test.tsgetLatestCommitId.

Notes

  • tools.test.ts mocks @earendil-works/pi-coding-agent: its index re-exports main, which transitively loads an experimental server module importing @earendil-works/pi-server (not installed; the real rolldown build tree-shakes it away). Mocking keeps tests off that module graph.
  • checkoutRepository is not unit-tested here — it needs a real git remote, so it belongs to the integration/E2E layers.
  • Coverage on src/ (excluding src/forgejo/, which is Layer 2 integration territory): ~88% statements.

Checks

  • pnpm check
  • pnpm lint:check
  • pnpm format:check
  • pnpm test — 30/30
## What First step of the testing strategy (issue #264): Layer 1 — unit tests with Vitest. - **Tooling**: `vitest` + `@vitest/coverage-v8` devDependencies, `test` / `test:unit` / `test:coverage` scripts, `vitest.config.ts` (node env), `test/setup.ts` that sets the env vars `src/forgejo/fetch.ts` reads at import time (`FORGEJO_API_URL`, `CTX_AUTH_TOKEN`, `CTX_AUTH_USERNAME`). - **Tests** (30 total, all passing): - `src/prompt.test.ts` — `buildPrompt` for issue/PR/review-requested events, branch + tool instructions, empty body, comment ordering. - `src/schemas.test.ts` — event name validation + contract tests validating every payload in `examples/event_payloads/` against the schemas. - `src/context.test.ts` — `getAuthContext` env parsing, `getEventContext` (issue vs PR, comments, invalid env) with the Forgejo client mocked. - `src/tools.test.ts` — all 5 tools: default repository/issueId fallback, explicit params, and the `create-pr-review` `side`/`line` → `old_position`/`new_position` mapping. - `src/git.test.ts` — `getLatestCommitId`. ## Notes - `tools.test.ts` mocks `@earendil-works/pi-coding-agent`: its index re-exports `main`, which transitively loads an experimental server module importing `@earendil-works/pi-server` (not installed; the real rolldown build tree-shakes it away). Mocking keeps tests off that module graph. - `checkoutRepository` is not unit-tested here — it needs a real git remote, so it belongs to the integration/E2E layers. - Coverage on `src/` (excluding `src/forgejo/`, which is Layer 2 integration territory): ~88% statements. ## Checks - `pnpm check` ✅ - `pnpm lint:check` ✅ - `pnpm format:check` ✅ - `pnpm test` — 30/30 ✅
test: add unit tests with vitest
All checks were successful
check-commitizen.yaml / Check the PR title (pull_request) Successful in 1s
check-formatter.yaml / Checks with formatters (pull_request) Successful in 6s
check-linter.yaml / Checks with linters (pull_request) Successful in 10s
check-prettier.yaml / Check files formatting (pull_request) Successful in 2s
check-syntax.yaml / Checks with static tools (pull_request) Successful in 8s
check-yamllint.yaml / Lint yaml files (pull_request) Successful in 1s
forgejo-agent / LLM Bot response generation (pull_request) Successful in 2s
40ee3a3333
chore: small fixes
All checks were successful
check-formatter.yaml / Checks with formatters (pull_request) Successful in 6s
check-prettier.yaml / Check files formatting (pull_request) Successful in 2s
check-syntax.yaml / Checks with static tools (pull_request) Successful in 9s
check-linter.yaml / Checks with linters (pull_request) Successful in 19s
check-yamllint.yaml / Lint yaml files (pull_request) Successful in 1s
9c81002b21
Owner

@ci-bot a couple of questions.

  • I see that in some test groups there's an afterEach hook that calls vi.unmock all or something similar. Is it needed just in those cases? or always and should be added to the others?

  • I would prefer the payload validating tests to be kept in sync with the forgejo release. Is that possible and would it require the e2e forgejo instance?

@ci-bot a couple of questions. - I see that in some test groups there's an afterEach hook that calls vi.unmock all or something similar. Is it needed just in those cases? or always and should be added to the others? - I would prefer the payload validating tests to be kept in sync with the forgejo release. Is that possible and would it require the e2e forgejo instance?
test(unit): clear mocks in tools tests and document payload refresh
Some checks failed
check-formatter.yaml / Checks with formatters (pull_request) Successful in 6s
check-linter.yaml / Checks with linters (pull_request) Successful in 10s
check-prettier.yaml / Check files formatting (pull_request) Failing after 3s
check-yamllint.yaml / Lint yaml files (pull_request) Successful in 3s
check-syntax.yaml / Checks with static tools (pull_request) Successful in 9s
597528b3a2
Author
Member

Good questions, thanks!

1. afterEach hooks (vi.unstubAllEnvs / vi.clearAllMocks)

They're not "needed everywhere" — they cover two distinct things:

  • vi.unstubAllEnvs() is only needed in tests that call vi.stubEnv(), which is exclusively src/context.test.ts (it stubs CTX_AUTH_TOKEN, FORGEJO_REPOSITORY, etc.). Without it the stubbed vars would leak into other tests/files. The other test files never stub env vars, so they don't need it.
  • vi.clearAllMocks() is needed wherever mock call history is asserted. In context.test.ts it matters because of expect(mockedGetPullRequest).not.toHaveBeenCalled() — without clearing, history accumulates and tests become order-dependent.

In tools.test.ts it was technically not required today (all assertions are at-least-once toHaveBeenCalledWith), but the lack of it was inconsistent and fragile — e.g., adding a not.toHaveBeenCalled() or enabling shuffle would silently break. I've added afterEach(() => vi.clearAllMocks()) there for the same hygiene (597528b). prompt.test.ts, schemas.test.ts and git.test.ts don't mock at all, so they correctly have no hooks.

2. Keeping payload contract tests in sync with the Forgejo release

Yes, it's possible, and the e2e instance is only needed for the fully automatic flavor:

  • Without a live instance: the fixtures are already pinned to a version (examples/event_payloads/README.md → Forgejo 15.0.3). I've documented the refresh procedure there (capture toJson(forge.event) from a job running against the target version, update the files + version, run pnpm test). As a bonus, Forgejo publishes its OpenAPI spec per release tag (templates/swagger/v1_json.tmpl in the source repo — I verified it's fetchable at v15.0.3), so the underlying Issue/PullRequest/Comment/Repository models could be diffed against the pinned tag without any instance. The refresh is then a manual/scripted step done when bumping the supported Forgejo version.
  • With the e2e instance: the truly automatic path — CI spins up the pinned Forgejo, triggers the events, captures the payloads, and validates — is exactly the Layer 2 integration layer planned in #264. I'd defer it to that layer rather than bolt it onto the unit tests.

One nuance worth flagging: the schemas only describe the subset of fields the agent reads and don't set additionalProperties: false, so the contract tests catch breaking changes (a field the agent uses changed shape or vanished) but not additive ones. That's intentional — a full payload diff against each release is a Layer 2 concern; these tests are the cheap tripwire.

Good questions, thanks! ### 1. `afterEach` hooks (`vi.unstubAllEnvs` / `vi.clearAllMocks`) They're not "needed everywhere" — they cover two distinct things: - `vi.unstubAllEnvs()` is only needed in tests that call `vi.stubEnv()`, which is exclusively `src/context.test.ts` (it stubs `CTX_AUTH_TOKEN`, `FORGEJO_REPOSITORY`, etc.). Without it the stubbed vars would leak into other tests/files. The other test files never stub env vars, so they don't need it. - `vi.clearAllMocks()` is needed wherever mock *call history* is asserted. In `context.test.ts` it matters because of `expect(mockedGetPullRequest).not.toHaveBeenCalled()` — without clearing, history accumulates and tests become order-dependent. In `tools.test.ts` it was technically not required today (all assertions are at-least-once `toHaveBeenCalledWith`), but the lack of it was inconsistent and fragile — e.g., adding a `not.toHaveBeenCalled()` or enabling shuffle would silently break. I've added `afterEach(() => vi.clearAllMocks())` there for the same hygiene (597528b). `prompt.test.ts`, `schemas.test.ts` and `git.test.ts` don't mock at all, so they correctly have no hooks. ### 2. Keeping payload contract tests in sync with the Forgejo release Yes, it's possible, and the e2e instance is only needed for the fully automatic flavor: - **Without a live instance**: the fixtures are already pinned to a version (`examples/event_payloads/README.md` → Forgejo 15.0.3). I've documented the refresh procedure there (capture `toJson(forge.event)` from a job running against the target version, update the files + version, run `pnpm test`). As a bonus, Forgejo publishes its OpenAPI spec per release tag (`templates/swagger/v1_json.tmpl` in the source repo — I verified it's fetchable at `v15.0.3`), so the underlying `Issue`/`PullRequest`/`Comment`/`Repository` models could be diffed against the pinned tag without any instance. The refresh is then a manual/scripted step done when bumping the supported Forgejo version. - **With the e2e instance**: the truly automatic path — CI spins up the pinned Forgejo, triggers the events, captures the payloads, and validates — is exactly the Layer 2 integration layer planned in #264. I'd defer it to that layer rather than bolt it onto the unit tests. One nuance worth flagging: the schemas only describe the subset of fields the agent reads and don't set `additionalProperties: false`, so the contract tests catch *breaking* changes (a field the agent uses changed shape or vanished) but not additive ones. That's intentional — a full payload diff against each release is a Layer 2 concern; these tests are the cheap tripwire.
Owner

@ci-bot mm ok, so I would go with full automatic from e2e and skip using those in the repo

@ci-bot mm ok, so I would go with full automatic from e2e and skip using those in the repo
test(unit): drop payload fixture contract tests, defer to e2e layer
All checks were successful
check-formatter.yaml / Checks with formatters (pull_request) Successful in 6s
check-prettier.yaml / Check files formatting (pull_request) Successful in 3s
check-linter.yaml / Checks with linters (pull_request) Successful in 11s
check-yamllint.yaml / Lint yaml files (pull_request) Successful in 1s
check-syntax.yaml / Checks with static tools (pull_request) Successful in 18s
1fc371be2f
Author
Member

Agreed — payload validation belongs to the e2e layer, where it can be fully automatic. I've removed the fixture-based contract tests from this PR:

  • src/schemas.test.ts now only keeps the pure unit tests (eventNameSchema accepts the four supported events / rejects unknown ones); the "schemas against the real payloads" block and its readFileSync import are gone.
  • examples/event_payloads/README.md is reverted to its pre-PR state — the "Refreshing the payloads" section and the reference to src/schemas.test.ts are removed. The fixture files themselves stay, since they pre-date this PR and are used as reference for constructing action workflows, not as test fixtures.
  • The schema models (Issue / PullRequest / Comment / Repository) themselves remain, since context.ts needs them for env/event validation — they're just no longer pinned to captured payloads.

Updated status:

  • pnpm test — 24/24 (30 minus the 6 contract tests)
  • pnpm check
  • pnpm lint:check
  • pnpm format:check

Committed as 1fc371b ("test(unit): drop payload fixture contract tests, defer to e2e layer"). The full automatic path (spin up the pinned Forgejo, trigger events, capture and validate payloads) stays on the #264 Layer 2 plan.

Agreed — payload validation belongs to the e2e layer, where it can be fully automatic. I've removed the fixture-based contract tests from this PR: - `src/schemas.test.ts` now only keeps the pure unit tests (`eventNameSchema` accepts the four supported events / rejects unknown ones); the "schemas against the real payloads" block and its `readFileSync` import are gone. - `examples/event_payloads/README.md` is reverted to its pre-PR state — the "Refreshing the payloads" section and the reference to `src/schemas.test.ts` are removed. The fixture files themselves stay, since they pre-date this PR and are used as reference for constructing action workflows, not as test fixtures. - The schema models (`Issue` / `PullRequest` / `Comment` / `Repository`) themselves remain, since `context.ts` needs them for env/event validation — they're just no longer pinned to captured payloads. Updated status: - `pnpm test` — 24/24 ✅ (30 minus the 6 contract tests) - `pnpm check` ✅ - `pnpm lint:check` ✅ - `pnpm format:check` ✅ Committed as `1fc371b` ("test(unit): drop payload fixture contract tests, defer to e2e layer"). The full automatic path (spin up the pinned Forgejo, trigger events, capture and validate payloads) stays on the #264 Layer 2 plan.
chore: final review
All checks were successful
check-formatter.yaml / Checks with formatters (pull_request) Successful in 6s
check-prettier.yaml / Check files formatting (pull_request) Successful in 2s
check-linter.yaml / Checks with linters (pull_request) Successful in 15s
check-syntax.yaml / Checks with static tools (pull_request) Successful in 8s
check-yamllint.yaml / Lint yaml files (pull_request) Successful in 3s
7d437d90ef
davide scheduled this pull request to auto merge when all checks succeed 2026-09-08 20:41:49 +00:00
davide merged commit 1f445ea211 into main 2026-09-08 20:42:02 +00:00
davide deleted branch test/unit-tests 2026-09-08 20:42:02 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
projects/forgejo-agent!265
No description provided.