tests #264
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
@ci-bot can you propose a testing strategy for the program? ideally i would like it to be as close as possible to the real thing, so i guess a forgejo instance running in actions/devcontainer.
For the js side i prefer the vitest library.
For now report only the plan and some example tests, no implementation
Testing strategy for forgejo-agent
Goal
Test the program as close as possible to how it runs in production: a real Forgejo instance, the real binary, real git operations, real API calls. The only non-deterministic part is the LLM, so we isolate it with a scripted session for CI and keep a real-LLM smoke test for on-demand runs.
Layered approach
node:httpLayer 0 — Tooling
vitest+@vitest/coverage-v8as devDependencies.test(vitest run),test:unit,test:integration,test:e2e,test:coverage.vitest.config.ts: node environment,setupFilesthat set the env varssrc/forgejo/fetch.tsreads at import time (FORGEJO_API_URL,CTX_AUTH_TOKEN,CTX_AUTH_USERNAME) — otherwise importing the client throws.Layer 1 — Unit tests
src/prompt.test.ts—buildPromptfor the 4 event types; assert branch instructions, tool instructions, issue/PR content, comment ordering, empty body.src/schemas.test.ts— TypeBox validation; contract tests that validate every file inexamples/event_payloads/against the schemas (keeps fixtures in sync with the Forgejo version).src/context.test.ts— env parsing; missing/invalid env throws; repository name pattern; forgejo client mocked.src/tools.test.ts— default repository/issueId fallback;create-pr-reviewmapsside/line→old_position/new_position; parameter schema validation.src/git.ts—getLatestCommitId;checkoutRepositoryagainst a local bare repo.Layer 2 — Integration tests
node:http) mimicking the Forgejo REST API, serving the real payloads fromexamples/event_payloads/.>= 400.getEventContextend-to-end: repository + issue + PR + comments.Layer 3 — E2E with a real Forgejo (the centerpiece)
Instance —
docker-compose.test.yaml:Provisioning —
scripts/e2e-setup.sh:/api/v1/version;forgejo admin user create ...);forgejo admin user generate-access-token);POST /user/reposwithauto_init: true);Deterministic agent session — the only non-deterministic part is the LLM. Add a small test hook in
main.ts: whenCTX_TEST_SCRIPTis set, replace the Pi session with a scripted one that replays a fixed sequence of tool calls (e.g. "callcreate-issue-commentwith body X"). This exercises everything else for real: env parsing, API reads, git clone/checkout, prompt building, tool execution, API writes.Test — run the built binary in a fresh temp dir with env vars pointing at the test instance, then assert the outcome via the API (comment exists, issue closed, PR created, review submitted).
Devcontainer — add the same forgejo service to
.devcontainer/docker-compose.yamlsopnpm test:e2eworks locally withdocker compose up.Layer 4 — Smoke tests (real LLM)
CI integration
Alternative: use the
services:keyword (act_runner supports it like GitHub Actions), but then provisioning can't usedocker exec— you'd need a custom image that provisions on first boot, or a pre-seeded data volume. The compose approach above is simpler and identical to the devcontainer experience.Example tests
1.
src/prompt.test.ts— pure, no mocks2.
src/schemas.test.ts— contract tests against the real payloads3.
src/forgejo/index.test.ts— client with stubbed fetch4.
src/tools.test.ts— review comment position mapping5. E2E sketch — real Forgejo, scripted session
Open questions
15.0.3(the version the payloads were saved on), or track production?services:keyword (needs a provisioning image)?src/?CTX_TEST_SCRIPTscripted-session hook tomain.ts? It's the key to deterministic E2E.Testing strategy for forgejo-agent
I went through the current codebase (
src/context.ts,src/prompt.ts,src/schemas.ts,src/forgejo/*,src/tools.ts,src/git.ts,src/main.ts,examples/event_payloads/) and the plan below is verified against the real code — function names, env vars, payload shapes, and the binary name (dist/forgejo-agent, fromsea.json) all match.Goal
Test the program as close as possible to production: a real Forgejo instance, the real binary, real git operations, real API calls. The only non-deterministic part is the LLM, so we isolate it with a scripted session for CI and keep a real-LLM smoke test for on-demand/nightly runs.
Layered approach
node:httpLayer 0 — Tooling
vitest+@vitest/coverage-v8as devDependencies.test(vitest run),test:unit,test:integration,test:e2e,test:coverage.vitest.config.ts: node environment,setupFilesthat set the env varssrc/forgejo/fetch.tsreads at import time —FORGEJO_API_URL,CTX_AUTH_TOKEN,CTX_AUTH_USERNAME(verified:fetch.tsevaluatesprocess.env.FORGEJO_API_URLandgetAuthContext()at module load, so importing the client without them throws).src/context.tsadditionally readsFORGEJO_REPOSITORY,CTX_ISSUE_NUMBER,CTX_EVENT_NAMEinside functions.Layer 1 — Unit tests
src/prompt.test.ts—buildPromptfor the 4 event types; assert branch instructions, tool instructions, issue/PR content, comment ordering, empty body.src/schemas.test.ts— TypeBox validation; contract tests that validate every file inexamples/event_payloads/against the schemas (keeps fixtures in sync with the Forgejo version). Verified:issue_opened.jsonandpull_request_opened.jsoncontain all fields required byissueSchema/pullRequestSchema(number,user.username,title,body,state,pull_request,head.label,base.label).src/context.test.ts— env parsing; missing/invalid env throws; repository name pattern; forgejo client mocked.src/tools.test.ts— default repository/issueId fallback;create-pr-reviewmapsside/line→old_position/new_position(verified intools.ts:new_position: c.side === "HEAD" ? c.line : 0,old_position: c.side === "BASE" ? c.line : 0); parameter schema validation.src/git.ts—getLatestCommitId;checkoutRepositoryagainst a local bare repo.Layer 2 — Integration tests
node:http) mimicking the Forgejo REST API, serving the real payloads fromexamples/event_payloads/.apiUrl + /repos/{owner}/{repo}/issues/{id}), method,Authorization: Bearer <token>header, content-type, response schema validation, error on>= 400(verified: throwsfetch failed: <body>).getEventContextend-to-end: repository + issue + PR + comments.Layer 3 — E2E with a real Forgejo (the centerpiece)
Instance —
docker-compose.test.yaml:Provisioning —
scripts/e2e-setup.sh:/api/v1/version;forgejo admin user create ...);forgejo admin user generate-access-token);POST /user/reposwithauto_init: true);Deterministic agent session — the only non-deterministic part is the LLM. Add a small test hook in
main.ts: whenCTX_TEST_SCRIPTis set, replace the Pi session with a scripted one that replays a fixed sequence of tool calls (e.g. "callcreate-issue-commentwith body X"). This exercises everything else for real: env parsing, API reads, git clone/checkout, prompt building, tool execution, API writes.Test — run the built binary in a fresh temp dir with env vars pointing at the test instance, then assert the outcome via the API (comment exists, issue closed, PR created, review submitted).
Devcontainer — add the same forgejo service to
.devcontainer/docker-compose.yaml(currently only has theworkspaceservice) sopnpm test:e2eworks locally withdocker compose up.Layer 4 — Smoke tests (real LLM)
CI integration
This mirrors the existing workflows (same clone pattern, same
ci-pnpmimage,srv-homealready runs the agent job). Alternative: theservices:keyword (act_runner supports it), but then provisioning can't usedocker exec— you'd need a custom image that provisions on first boot, or a pre-seeded data volume. The compose approach is simpler and identical to the devcontainer experience.Example tests
1.
src/prompt.test.ts— pure, no mocks2.
src/schemas.test.ts— contract tests against the real payloads3.
src/forgejo/index.test.ts— client with stubbed fetch4.
src/tools.test.ts— review comment position mapping5. E2E sketch — real Forgejo, scripted session
Open questions — my recommendations
services:keyword since provisioning needsdocker exec.src/is a reasonable starting point; adjust after the first run.CTX_TEST_SCRIPThook — yes, add it. It's the key to deterministic E2E; keep it small (a few lines inmain.ts) and documented.No implementation yet, as requested — this is the plan + examples. Happy to turn any layer into a PR.
Testing strategy — verified against the code
I re-checked the plan against current
main(v1.9.12). Everything holds; here are the concrete verifications plus a few implementation subtleties worth knowing before we build it.Verified against the code
fetch.tsreadsFORGEJO_API_URL+CTX_AUTH_TOKEN/CTX_AUTH_USERNAMEat import timeapiUrlandauthContextare module-level constants insrc/forgejo/fetch.ts; importing the client without them throwscontext.tsreadsFORGEJO_REPOSITORY,CTX_ISSUE_NUMBER,CTX_EVENT_NAMEgetEventContext()create-pr-reviewmapsside/line→old_position/new_positionsrc/tools.ts:new_position: c.side === "HEAD" ? c.line : 0,old_position: c.side === "BASE" ? c.line : 0dist/forgejo-agentsea.json→output: dist/forgejo-agent>= 400fetch.tsthrowsfetch failed: <body>issue_opened.json/pull_request_opened.jsoncontain all required fields (number,user.username,title,body,state,pull_request,head.label,base.label)examples/event_payloads/README.md: "Saved on Forgejo version 15.0.3".devcontaineronly hasworkspacesrv-home+ci-pnpmimageSubtleties to bake into the implementation
fetch.tsevaluates env at module load,test/setup.tsmust set the three vars before any import of the client. For the Layer-2 mock server,apiUrlis a module constant: start thenode:httpserver in a global setup (orbeforeAll+ dynamicimport()) so the URL is known before the client module loads.Type.ObjectwithoutadditionalProperties: falseaccepts the extra fields in the real payloads, so the contract tests pass today. If we want strict drift detection, addadditionalProperties: falseto the schemas (or snapshot the parsed shape).git.tsalso importsapiUrl— unit tests oftools.tsmust mock../src/git.ts(as the example does) or set the env vars, otherwise thegetLatestCommitIdmodule chain pulls infetch.ts.checkoutRepositorybuilds the remote URL fromapiUrlwith the token as password, so the E2E binary needsFORGEJO_API_URLpointing at the test instance (the sketch already does this) and the bot token needs repo read/write.Recommendations on the open questions
services:since provisioning needsdocker exec.src/as a starting point.CTX_TEST_SCRIPThook — yes; a few lines inmain.ts, documented.Next step: I can turn Layer 1 (vitest config + unit tests) into a PR first — it's self-contained and unblocks the rest. Just say the word.
@ci-bot let's start gradually adding unit tests
Done — first batch of unit tests is in PR #265 (
test: add unit tests with vitest).What's in it
vitest+@vitest/coverage-v8devDeps,test/test:unit/test:coveragescripts,vitest.config.ts, andtest/setup.tsthat sets the env varssrc/forgejo/fetch.tsreads at import time.prompt.ts— all 4 event types, branch/tool instructions, empty body, comment orderingschemas.ts— event names + contract tests against every file inexamples/event_payloads/context.ts— auth env parsing,getEventContext(issue vs PR, comments, invalid env)tools.ts— all 5 tools, default fallbacks, and thecreate-pr-reviewposition mappinggit.ts—getLatestCommitIdFindings worth knowing
CTX_ISSUE_NUMBERis not validated —Number(Value.Parse(Type.String(), ...))turns"not-a-number"intoNaNwithout throwing; the failure surfaces later at the API call. Minor robustness gap, left as-is for now — happy to fix in a follow-up.@earendil-works/pi-serveris missing —pi-coding-agent's index re-exportsmain, which loads an experimental server module importing@earendil-works/pi-server(not installed; the rolldown build tree-shakes it). Tests mock the package to avoid that graph. If you ever import the package in a non-bundled context, this will bite.checkoutRepositorydeferred — it needs a real git remote, so it fits the integration/E2E layers rather than unit.Checks
pnpm check/lint:check/format:check✅,pnpm test30/30 ✅. Coverage onsrc/(excludingsrc/forgejo/, which is Layer 2 territory): ~88% statements.Next natural step: Layer 2 (integration tests with a mock Forgejo HTTP server) or wiring
pnpm testinto CI. Just say the word.@ci-bot ok, unit tests merged, now you can continue. with integration tests. It was decided that payloads in the repo are only to be used as a reference and that for tests we use the actual forgejo instance to guarantee that the content is always synced
Integration tests are in PR #266 (
test(integration): add integration tests against a real forgejo instance), following the decision thatexamples/event_payloads/are reference-only: tests hit a real Forgejo instance so the schemas are always validated against live payloads.What's in it — 16 tests, all green
test/integration/forgejo.test.ts— the client: repositories, issues, comments, PRs, reviews, inline review comments. Every call goes throughforgejoFetch, which validates the response against the TypeBox schemas, so a passing suite is the "content always synced" guarantee (no fixture drift possible).test/integration/context.test.ts—getEventContextfor issue + PR events against live data.test/integration/tools.test.ts— all five tools run for real (close-issue, create-issue, create-issue-comment, create-pr, create-pr-review). The review test runs from a checkout of the PR head branch sogetLatestCommitId()resolves a real commit, and the inline comment lands on a known diff position.Infrastructure
docker-compose.test.yaml— pinned Forgejo 15.0.3 (the version the payloads were captured on), healthcheck via/api/v1/version.scripts/integration-setup.sh(POSIX sh) — creates users + access tokens via the Forgejo CLI in the container, deletes/recreates the test repo on every run (fully deterministic), pushes a change branch with a known diff, seeds an issue + comment, writestest/integration/credentials.json(gitignored).vitest.integration.config.ts+pnpm test:integration— separate config so the defaultpnpm teststays fast and instance-free; files run sequentially (tests share the instance)..devcontainer/docker-compose.yamlso tests run from the workspace container too.test/README.mdwith the full run flow:up -d --wait→./scripts/integration-setup.sh→pnpm test:integration.Validation
Ran the whole cycle against a live Forgejo 15.0.3 instance:
test:integration16/16 ✅ (repeat runs too), unitpnpm test23/23 ✅,check/lint:check/format:check✅, yamllint + prettier ✅ (had to add*.shto.prettierignore— prettier has no shell parser).Two quirks worth knowing:
defineToolfrom pi-coding-agent is mocked (it's just a metadata factory; the package itself can't be imported outside the bundle — missing@earendil-works/pi-server).⚠️ Transparency note: I accidentally pushed the commit straight to
mainfirst; sincemainis protected against force-pushes, I reverted it there (420a634) and re-applied the changes on the PR branch — the PR diff is exactly the intended change,mainis back to its pre-PR content.Next steps when you're ready: wire
pnpm test:integrationinto CI (needs a docker-capable runner — I left the workflow out of this PR on purpose), then Layer 3 (E2E with theCTX_TEST_SCRIPTscripted-session hook) and Layer 4 (real-LLM smoke).