Context
Operator-flagged friction (PR #12976): a PR changing ONLY a unit test + its fixture (test/playwright/unit/apps/portal/view/news/discussions/Component.spec.mjs + a .md fixture) triggered the integration CI suite — which makes no sense for a unit-test-only change.
The Problem
.github/workflows/test.yml's scope-classifier (the changes job) computes run_integration as:
const isDocsOnlyPath = file => file.endsWith('.md') || file.startsWith('learn/') || file.startsWith('resources/content/');
const runIntegration = normalizedFiles.some(file => !isDocsOnlyPath(file));Integration runs for any changed file that isn't docs-only. The default is "run integration UNLESS docs-only" — so every non-docs path-type (a unit-test .mjs, a config, a script) defaults to triggering integration. There is no per-suite path-scoping.
The Fix
Flip run_integration to a whitelist (trigger only on integration-relevant paths):
const runIntegration = normalizedFiles.some(file =>
file.startsWith('src/') ||
file.startsWith('ai/') ||
file.startsWith('test/playwright/integration/') ||
file.startsWith('test/playwright/util/') ||
file === 'test/playwright/fixtures.mjs' ||
file === 'test/playwright/setup.mjs' ||
file === 'test/playwright/playwright.config.integration.mjs' ||
file === 'package.json' || file === 'package-lock.json' ||
file === '.github/workflows/test.yml'
);The Architectural Reality (why the whitelist must be COMPLETE)
The current blacklist over-triggers — wasteful but safe (never misses). A whitelist's failure mode is the opposite and worse: skip-when-it-was-needed → a real integration regression merges green. So the whitelist must be provably complete. Membership is derived from what the integration suite actually exercises (verified): Agent-OS/cloud integration (KB tenant-isolation, MCP transport, OIDC, heartbeat, backup/restore, multi-tenant ingestion, daemon safety) heavily under ai/, AND it runs the engine — BackupRestoreWipe.integration.spec.mjs imports src/Neo.mjs + src/core/_export.mjs.
Avoided Traps
- Do NOT use a tight
src/+ai/-only whitelist — it would silently SKIP integration on dep bumps (package.json/lock), the integration tests themselves, shared test infra, and CI-logic changes → false-negative CI gaps.
ai/deploy/docker-compose.test.yml (the integration Chroma stack) is already under ai/.
- Preserve the present-but-skipped check pattern (the
test matrix job always runs; only inner steps gate) so protected-branch required-checks aren't blocked by a missing check.
Out of Scope (follow-up)
The symmetric rewrite of run_unit into its own per-suite whitelist is the broader root-fix — deferred to bound this change to the operator-reported integration friction. (run_unit over-triggers too, but unit is cheap + over-running is safe.)
Acceptance Criteria
Release classification: post-v13 (CI-efficiency; not release-blocking — v13 already shipped).
Context
Operator-flagged friction (PR #12976): a PR changing ONLY a unit test + its fixture (
test/playwright/unit/apps/portal/view/news/discussions/Component.spec.mjs+ a.mdfixture) triggered the integration CI suite — which makes no sense for a unit-test-only change.The Problem
.github/workflows/test.yml's scope-classifier (thechangesjob) computesrun_integrationas:const isDocsOnlyPath = file => file.endsWith('.md') || file.startsWith('learn/') || file.startsWith('resources/content/'); const runIntegration = normalizedFiles.some(file => !isDocsOnlyPath(file));Integration runs for any changed file that isn't docs-only. The default is "run integration UNLESS docs-only" — so every non-docs path-type (a unit-test
.mjs, a config, a script) defaults to triggering integration. There is no per-suite path-scoping.The Fix
Flip
run_integrationto a whitelist (trigger only on integration-relevant paths):const runIntegration = normalizedFiles.some(file => file.startsWith('src/') || // engine the Agent-OS stack runs on file.startsWith('ai/') || // Agent OS — what integration exercises (incl. ai/deploy/docker-compose.test.yml) file.startsWith('test/playwright/integration/') || // the integration specs + fixtures themselves file.startsWith('test/playwright/util/') || // shared test infra used by integration file === 'test/playwright/fixtures.mjs' || file === 'test/playwright/setup.mjs' || file === 'test/playwright/playwright.config.integration.mjs' || file === 'package.json' || file === 'package-lock.json' || // dep changes can break the integration stack file === '.github/workflows/test.yml' // CI-logic changes run the full suite to validate );The Architectural Reality (why the whitelist must be COMPLETE)
The current blacklist over-triggers — wasteful but safe (never misses). A whitelist's failure mode is the opposite and worse: skip-when-it-was-needed → a real integration regression merges green. So the whitelist must be provably complete. Membership is derived from what the integration suite actually exercises (verified): Agent-OS/cloud integration (KB tenant-isolation, MCP transport, OIDC, heartbeat, backup/restore, multi-tenant ingestion, daemon safety) heavily under
ai/, AND it runs the engine —BackupRestoreWipe.integration.spec.mjsimportssrc/Neo.mjs+src/core/_export.mjs.Avoided Traps
src/+ai/-only whitelist — it would silently SKIP integration on dep bumps (package.json/lock), the integration tests themselves, shared test infra, and CI-logic changes → false-negative CI gaps.ai/deploy/docker-compose.test.yml(the integration Chroma stack) is already underai/.testmatrix job always runs; only inner steps gate) so protected-branch required-checks aren't blocked by a missing check.Out of Scope (follow-up)
The symmetric rewrite of
run_unitinto its own per-suite whitelist is the broader root-fix — deferred to bound this change to the operator-reported integration friction. (run_unitover-triggers too, but unit is cheap + over-running is safe.)Acceptance Criteria
run_integrationuses the whitelist; a unit-test-only PR (the #12976 shape) does NOT trigger integration.src/orai/change DOES trigger integration.package.json/lock), an integration-spec change, a shared-infra change, or atest.ymlchange DOES trigger integration (no false-negative).Release classification: post-v13 (CI-efficiency; not release-blocking — v13 already shipped).