ci: run only the integration suites a pull request can affect [APPS-37918] - #775
Sarath1018 wants to merge 5 commits into
Conversation
1df05a8 to
2630b5f
Compare
|
✅ No issues found. Checked for bugs and CLAUDE.md compliance. |
PR coverage runs used to execute every integration suite for every change, so a docs- or packages-only PR spent ~17 minutes per leg creating Data Fabric entities and Maestro instances it could not influence, and every platform hiccup in ~3,000 requests failed the PR. A new `scope` job classifies the files changed against the base branch (scripts/integration-scope.mjs). Suites are the folders under tests/integration/shared; a change under src/services/<name>, src/models/<name> or tests/integration/shared/<name> runs that suite, docs, samples, packages and unit tests run nothing, and anything else (core, utils, the harness, config, workflows, a domain with no suite folder) runs everything. The cross-cutting smoke, http and auth-errors suites run whenever any suite runs. The `integration` job receives the resulting vitest path filters and is skipped only when the resolver explicitly says nothing is affected; a missing output still runs. The `ci:full-integration` label forces a full run; weekly-coverage.yml keeps running everything. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2630b5f to
4189c2b
Compare
|
✅ No issues found. Checked for bugs and CLAUDE.md compliance. |
|
they also depend on orchestrator processes? (process.start())? |
| } | ||
|
|
||
| /** One path → { kind: 'ignore' | 'always-on' | 'domain' | 'all' }. */ | ||
| export function classify(file, domains) { |
There was a problem hiding this comment.
ques - a version bump PR would run all tests right?
other than this, the weekly coverage would run all tests every week?
There was a problem hiding this comment.
Yes, should we not run for version bump?
There was a problem hiding this comment.
IMO we should run
| ]); | ||
|
|
||
| /** PR label that forces the full run. */ | ||
| export const FULL_RUN_LABEL = 'ci:full-integration'; |
There was a problem hiding this comment.
what's this used for? I think the check is hardcoding this string.
There was a problem hiding this comment.
Its ununsed, now the workflow passes the PR labels through (--labels "$PR_LABELS", joined from github.event.pull_request.labels.*.name) and the script does the check, so the constant is the single source of truth
| if (IGNORED_PATTERNS.some(pattern => pattern.test(file))) return { kind: 'ignore' }; | ||
| const domain = file.match(DOMAIN_PATH)?.[1]; | ||
| if (domain && domains.includes(domain)) return { kind: 'domain', domain }; | ||
| if (domain && ALWAYS_ON.includes(`${SHARED}/${domain}`)) return { kind: 'always-on' }; |
There was a problem hiding this comment.
editing a file in http/ runs only the always-on suites, but editing smoke.integration.test.ts or auth-errors.integration.test.ts runs everything.
ideally the behavior should be same for all 3?
They should run all tests but ig the source they cover already triggers "all", so a test file edit alone then would only affect that file?
There was a problem hiding this comment.
source under src/core/ → everything, since every service goes through it
a unit test under tests/unit/ → nothing, it cannot affect an integration suite
one of the three always-on integration tests → always-on suites only, the edit can only change that test
| - **The `requirement` argument is `'both'` unless the service rejects one of the credentials.** `'both'` runs the suite once per configured credential — under the PAT *and* under the user token in CI — because the two cover different things: the PAT exercises the external-application OAuth scope model, the user token exercises the general API surface. Use `'user'` only for services that reject PAT and client-credentials tokens outright, and `'pat'` only for something specific to the external-application identity. | ||
| - **Always `throw new Error()` when test preconditions are not met** — whether it's missing config (e.g., no `folderId`) or missing test data (e.g., no running jobs). Never use `console.warn()` + `return` to silently skip — silent skips hide unrunnable tests and make CI green when tests aren't actually exercised. | ||
| - **When a service rejects PAT auth, run it under a user token — do not `describe.skip` it.** `insightsrtm_` endpoints (Agents, Agent Memory, Agent Traces, Governance) and the notification service return 401 for PAT and client-credentials tokens regardless of scopes. Those suites are declared with `describeIntegration(name, 'user', modes, body)`, which states the requirement once and derives the credential, the host and the collection-time skip guard from it, so they run wherever `UIPATH_USER_TOKEN` is configured and are reported as skipped where it isn't. Declaring the guard separately from the requirement lets the two disagree — use the helper. See "Authentication modes" in `tests/integration/README.md`. Do **not** use `describe.skip` for missing test data, missing config, or flakiness — those require a `beforeAll` guard or a `throw`. Equivalently, **NEVER** exclude integration test files via `vitest.integration.config.ts` using env vars or file exclusion patterns — that is functionally equivalent to `describe.skip` across an entire file and has the same problem: tests appear to pass but are never actually exercised. Guard with `beforeAll` + `throw` inside the test file instead. | ||
| - **Scoping a pull-request run to the suites its changed files can affect (the `scope` job, `scripts/integration-scope.mjs`) is not such an exclusion**: no suite is disabled, unrecognised paths run everything, the `ci:full-integration` label forces the full run, and `weekly-coverage.yml` still runs everything. Name a new service's suite folder after its `src/services/` folder. |
There was a problem hiding this comment.
why is this needed for the agent?
There was a problem hiding this comment.
better we update the integration readme with this.
| const domain = file.match(DOMAIN_PATH)?.[1]; | ||
| if (domain && domains.includes(domain)) return { kind: 'domain', domain }; | ||
| if (domain && ALWAYS_ON.includes(`${SHARED}/${domain}`)) return { kind: 'always-on' }; | ||
| return { kind: 'all', reason: `${file} is outside the per-domain folders` }; |
There was a problem hiding this comment.
could is outside the per-domain folders be misleading for something like src/services/integration-service/connections.ts, as the domain is set but there is no test suite.
There was a problem hiding this comment.
true, now If there is no test suite it will simply don't run any
We will not run cross dependant test cases, thats the limitation as it is hard to maintain the dependancy graph. We will have a daily run that posts to a slack channel so any cross dependent failures will be caught |
- Rename the `scope` job to `integration-scope`, matching the script.
- Treat all three always-on entries alike: a change to the smoke test,
`shared/http/` or `auth-errors` runs only the always-on suites. Other
loose test files still run everything.
- Make the full-run reason name the missing suite folder for a domain
that has none, instead of calling the path "outside the per-domain
folders".
- Move the full-run label check into the script (`--labels`); the
workflow passes the PR's labels through and `FULL_RUN_LABEL` is the
only place the name lives.
- Document the scoping in tests/integration/README.md ("Which suites run
on a pull request") instead of an agent_docs/rules.md bullet; keep only
the suite-folder naming clause there.
- Unit tests for the always-on paths, both reason messages, a
version-bump change set and the label path.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
✅ No issues found. Checked for bugs and CLAUDE.md compliance. |
The integration-test scoping is documented in tests/integration/README.md only; the suite-folder naming clause goes too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
d0ff65f to
49cf7a8
Compare
|
✅ No issues found. Checked for bugs and CLAUDE.md compliance. |
A change under src/services/<name> or src/models/<name> where the service folder exists but tests/integration/shared/<name> does not (integration-service today) has no suite to run, so it no longer falls back to the full run. Only real service folders qualify; src/models folders that are not a service (common, document-understanding) are shared code and still run everything. The former safety net for a mis-named suite folder — fall back to the full run — is replaced by a unit test asserting every suite folder is named after a src/services folder. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| expect(scope).toMatchObject({ run: true, all: true, paths: [] }); | ||
| expect(scope.reasons[0]).toContain(FULL_RUN_LABEL); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
The resolveArgs describe block has no test for the git diff failure path — lines 107–110 in the script, whose comment explicitly says "A diff problem must never skip the run." That's the most safety-critical branch in the whole CLI, yet a regression there (e.g., accidentally re-throwing or calling process.exit) would be invisible.
The branch is testable by mocking node:child_process with vi.mock/vi.hoisted (ESM pattern the project already uses elsewhere):
import { vi, describe, it, expect, beforeEach } from 'vitest';
const { mockExecFileSync } = vi.hoisted(() => ({ mockExecFileSync: vi.fn() }));
vi.mock('node:child_process', () => ({ execFileSync: mockExecFileSync }));
describe('integration-scope resolveArgs — git diff failure', () => {
beforeEach(() => mockExecFileSync.mockReset());
it('falls back to the full run when git diff throws', () => {
mockExecFileSync.mockImplementation(() => { throw new Error('fatal: bad object origin/main'); });
const { scope } = resolveArgs(['--base', 'origin/main'])!;
expect(scope).toMatchObject({ run: true, all: true, paths: [] });
expect(scope!.reasons[0]).toContain('could not diff against origin/main');
});
});
Review findingsNew inline comment at tests/unit/scripts/integration-scope.test.ts line 136: the resolveArgs git diff failure catch block (script lines 107-110, comment: 'A diff problem must never skip the run') has no test. Suggested a vi.hoisted + vi.mock pattern to cover it. |
A base ref git does not know must fall back to the full run, never throw or exit. Also covers the --base success path and --files parsing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Created 3 throwaway PRs covering 3 scenarios
|
|
✅ No issues found. Checked for bugs and CLAUDE.md compliance. |
Summary
This adds an
integration-scopejob tocoverage.ymlthat decides which suites a PR needs from the files it changes (scripts/integration-scope.mjs, ~120 lines, no dependencies). The rule is name-based: suites are the folders undertests/integration/shared/, and a domain's sources live under the same name insrc/services/andsrc/models/.src/services/<name>/**,src/models/<name>/**,tests/integration/shared/<name>/**(suite folder exists)tests/integration/shared/<name>plus the always-on suitessrc/services/<name>/**,src/models/<name>/**wheresrc/services/<name>/exists but no suite folder does (integration-service)shared/smoke.integration.test.ts,shared/http/,auth-errors.integration.test.tsdocs/**,samples/**,packages/**,tests/unit/**,tests/utils/mocks/**, Markdown, lint/build configsrc/core,src/utils,src/services/base.ts,src/modelsfolders that are not a service (common,document-understanding), the test harness, config, workflows,package.jsonThe
integrationjob is skipped only when the resolver explicitly reports nothing affected (run_integration=false); a missing output, a failed diff or an unrecognised path all run the full suite.sonarandsummaryhandle the skipped case;test-and-buildpasses because a skipped job does not fail the called workflow. A version bump editspackage.json, so it runs everything;weekly-coverage.ymlnever passesscope_to_changes, so it always runs everything.Deliberately not modelled (kept simple on request): cross-domain dependencies. Two exist today, both into
maestro: its case instances wrap the Action CenterTaskService(source import), and its process-instance and case-instance suites seed data through Orchestratorprocesses.start()(test-side). So anaction-center-only ororchestrator-only change does not runmaestro.processes.start()itself is covered by the orchestrator suite, and the weekly run covers the rest. Adding a mapping later is a one-line table.Escape hatch: the
ci:full-integrationlabel forces a full run. The label name lives only inFULL_RUN_LABELin the script; the workflow passes the PR's labels through--labelsand the script decides. Labels are read from the event that started the run, so push a commit after labelling. Scheduled/manual runs are always full.Demo PRs
A demo PR into
mainwould always resolve to the full run, because its diff would include this PR's own workflow and script changes. The demos are therefore stacked on a throwaway base,demo/integration-scope-base: this branch plus one line sopr-checksruns for pull requests into any branch. None of them is for merge.src/services/action-center/tasks.ts) →scope=action-centersrc/services/demo-service/,tests/integration/shared/demo-service/) →scope=demo-servicesrc/core/→scope=allscope=all(workflow and script changed)Dry run against recent PRs
#720(coded-action-app docs/types) → nothing ·#709(sample) → nothing ·#745(maestro tests) → maestro only ·#736,#747,#734,#663(config / core / workflow changes) → everything.Docs
tests/integration/README.md→ "Which suites run on a pull request": the rule table, the label escape hatch, what a new suite folder must be called, and the local dry-run command.Notes
tests/unit/scripts/integration-scope.test.ts(behavioural cases, the label path, the suite-folder discovery, and a guard that every suite folder is named after asrc/services/folder — a mis-named suite failsnpm run test:unitinstead of never running on a PR).Test plan
npm run test:unit,npm run lintnode scripts/integration-scope.mjs --base origin/mainon this branch → full run (workflow + script changed)coverage / integration-scopeon this PR resolvesscope=all; the integration legs run the full suiteaction-center,demo-serviceandall🤖 Generated with Claude Code