feat: per-test configuration for triggers and alerts#74
feat: per-test configuration for triggers and alerts#74metalwarrior665 wants to merge 25 commits intomasterfrom
Conversation
Tests and suites now accept `runWhen` and `alerts` options in their config object, enabling fine-grained control over which GitHub Actions trigger causes each test to execute and which alerting channels fire on failure. - `lib/trigger.ts` — reads `TEST_TRIGGER` env var; exposes `getCurrentTrigger()` and `shouldRunForTrigger(runWhen)` (falls back to `'locally'` when unset) - `lib/types.ts` — adds `TriggerType`, `RunWhenConfig`, `AlertsConfig`; extends `ActorTestOptions` with `runWhen` and `alerts` - `lib/lib.ts` — `describe`, `testActor`, `testStandbyActor` all apply the trigger check (ANDed with the existing platform guard); `runWhen`/`alerts` are stripped before being forwarded to vitest; `alertsRegistry` map is exported for post-run reporting; `getCurrentTrigger` re-exported - `index.ts` — exports new types and helpers https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
…l merging - `describe` and `testActor`/`testStandbyActor` now accept a config object with `name` as their first/second param; `fn` always follows immediately, keeping the test body visible before any trailing options - Hierarchical merging via a config stack: `default → describe → nested describe → testActor`; `runWhen` is fully overridden by the innermost layer, `alerts` shallow-merges so children can add/override keys - Backward-compatible: old-style `describe(name, fn, options?)` and `testActor(actorId, name, fn, options?)` still work via typeof detection - Remove `'locally'` from `TriggerType`; `TEST_TRIGGER` unset now means "run everything" (no filtering) rather than matching a fallback value - Remove `onCall` from `AlertsConfig` - `runWhen`/`alerts` removed from `ActorTestOptions` (moved to `DescribeConfig` / `TestActorConfig`) - Export `DescribeConfig` and `TestActorConfig` from index https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
Instead of a separate module-level registry, alerts config is embedded in `task.meta.alerts` at the start of each test body. Vitest's JSON reporter already serializes task.meta into test-output.json (the same mechanism used for runLink/actorName), so report-tests can read it without any extra files or IPC. - Remove `alertsRegistry` — superseded by task.meta - `report-tests` reads `meta.alerts.slack` per failing test: - undefined (no alerts config) → notify, for backward compatibility - `slack: true` → notify - `slack: false` → skip Slack for that specific test https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
Extract pure functions to make core logic directly testable: - `mergeInheritedConfigs(layers)` exported from lib/lib.ts - `shouldNotifySlack(alerts)` exported from bin/test-report.ts New test files (33 tests): - test/unit/trigger.test.ts — getCurrentTrigger (valid/invalid/missing TEST_TRIGGER) and shouldRunForTrigger (all combinations of trigger presence and runWhen config) - test/unit/config-merging.test.ts — mergeInheritedConfigs: empty stack, runWhen innermost-wins semantics, alerts shallow-merge, multi-layer combined scenarios - test/unit/test-report-alerts.test.ts — shouldNotifySlack: undefined alerts (backward compat), explicit opt-in, explicit opt-out https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
Previously the Slack filter was baked into the failedAssertions assembly loop, meaning any future channel (email, PagerDuty, etc.) would require duplicating that loop. Now: - `failedAssertions` collects all failures with their `alerts` metadata - `slackAssertions` is derived by filtering `failedAssertions` via `shouldNotifySlack` immediately before the Slack send - Adding a new reporting channel is a single `.filter()` line on the same `failedAssertions` base https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
…ield runWhen merge, deduplicated actor test helpers
- `DescribeConfig` and `TestActorConfig` now use `triggers: { runWhen, alerts }` and `options: { timeout, concurrent/retry }` sub-objects; `name` stays at top level
- `mergeInheritedConfigs` now shallow-merges `runWhen` field-by-field (same as `alerts`) so children only need to override specific trigger keys rather than replacing the entire object
- Extract `resolveActorTestConfig` helper to eliminate duplication between `testActor` and `testStandbyActor`
- Remove `shouldNotifySlack` function (single-property check); inline as `alerts?.slack !== false` with a backward-compat comment
- Export new types: `TriggerConfig`, `DescribeOptions`, `ActorOptions`
- Update config-merging unit tests for new shallow-merge semantics; delete test-report-alerts unit test
https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
…InheritedTriggers Aligns internal names with the `triggers` field and `TriggerConfig` type used in the public API. https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
Previously runWhen[trigger] === true was required to run, meaning any unset
trigger implicitly disabled the test. Now runWhen[trigger] !== false, so all
triggers are enabled by default and users only need to set false to opt out.
e.g. runWhen: { pullRequest: false } disables only PR runs while
hourly and daily continue to run — no need to list every other trigger.
https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
…eck function Rather than shouldRunForTrigger using !== false (silently enabling new trigger types for all tests), DEFAULT_TRIGGERS is prepended to the merge stack and typed as Required<RunWhenConfig>. Adding a new TriggerType now causes a compile error on DEFAULT_TRIGGERS, forcing an explicit opt-in/opt-out decision — preventing accidental rollout to all existing tests. shouldRunForTrigger reverts to === true; the opt-out default is purely a config concern, not a predicate concern. https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
…out semantics - getMergedTriggers now accepts an optional extra layer, eliminating the duplicate DEFAULT_TRIGGERS reference in resolveActorTestConfig - describe/testActor/testStandbyActor JSDoc examples updated to show opt-out style (pullRequest: false) instead of misleading opt-in (daily: true / hourly: true) - RunWhenConfig and shouldRunForTrigger docs no longer reference internal implementation details (DEFAULT_TRIGGERS location) - Remove orphaned section divider comment https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
…abled per directory
DEFAULT_TRIGGERS.hourly is now false — only tests that explicitly set
runWhen: { hourly: true } will run on the hourly trigger.
For backward compatibility without touching test files: set the
BACKWARD_COMPATIBLE_HOURLY_DIR env var (e.g. 'core') in the workflow.
The library uses call-stack inspection to detect which test file is
registering a describe/testActor, and promotes hourly to true for tests
defined under that directory. The Actions workflow no longer needs a
subtest directory input — run all of /platform and let the lib handle
the scoping.
BACKWARD_COMPATIBLE_HOURLY_DIR is exported from the package so callers
can reference the constant name rather than hardcoding the string.
https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
…ll sites The layers are now explicit at each call site, making the merge order readable without jumping to a helper. https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
Move DEFAULT_TRIGGERS/DEFAULT_DESCRIBE_OPTIONS/DEFAULT_TEST_ACTOR_OPTIONS to consts.ts and actor-run helpers (createStartRunFn, createStartStandbyFn, createStandbyTask, generateRunLink) to utils.ts. lib.ts now contains only module-level init, the trigger stack, and the exported API (describe, testActor, testStandbyActor). Also inlines BACKWARD_COMPATIBLE_HOURLY_DIR as process.env access so it is no longer exported from index.ts. https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
No need to pass callerFile as a parameter — both functions live in lib.ts, so all frames are already filtered by THIS_FILE_BASE. Avoids calling getCallerFile when BACKWARD_COMPATIBLE_HOURLY_DIR is not set (fast path). https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
slack defaults to true (opt-out model, consistent with runWhen). Required<AlertsConfig> gives a compile error if a new alert channel is added without an explicit decision. report-tests keeps its !== false guard since it operates across a process boundary and may be used with older library versions. https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
The hourly override was returning only { runWhen: {...} }, dropping
alerts from DEFAULT_TRIGGERS. Fixed by spreading DEFAULT_TRIGGERS first
and then overriding only runWhen. Also corrects the describe() JSDoc
example to show alerts: { slack: false } to illustrate opt-out semantics.
https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
…tion Remove the old "Differences in writing tests" before/after migration guide (everyone has migrated). Add examples for runWhen/alerts triggers config: opting out of triggers, inheriting through describe hierarchy, disabling Slack alerts, hourly tests via BACKWARD_COMPATIBLE_HOURLY_DIR. Update the core workflow to use backward_compatible_hourly_dir input instead of subtest: core. https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
…le_hourly_dir from workflow
How BACKWARD_COMPATIBLE_HOURLY_DIR is passed to the workflow is TBD.
New tests should opt in to hourly via triggers: { runWhen: { hourly: true } }.
https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd
ruocco-l
left a comment
There was a problem hiding this comment.
Other than the pullRequest to be explicitly opt in, LGTM
Patai5
left a comment
There was a problem hiding this comment.
I trust the unit tests that were made by the LLM to do their job 😎
|
I have reverted the refactoring that moved some stuff to utils.ts, it was making the diff longer and making merging master harder. Let's do it later. |
|
Corrected after merge conflicts. I will let it sit for a few days, then will do the last review, ping, and merge. |
oklinov
left a comment
There was a problem hiding this comment.
I'm note sure about the
| } from './types.js'; | ||
| import { getActorPrefilledInput, sleep } from './utils.js'; | ||
|
|
||
| const ACTOR_BUILDS = 'ACTOR_BUILDS'; |
There was a problem hiding this comment.
is this necesarry? here you changed it from using a constant to inlining but in trigger.ts you're using constant. I'd make the work with environment variables consistent.
|
|
||
| if (failedAssertions.length === 0) { | ||
| // Default to true when alerts is not configured — backward-compatible | ||
| const slackAssertions = failedAssertions.filter(({ alerts }) => alerts?.slack !== false); |
|
|
||
| export const TRIGGER_ENV_VAR = 'TEST_TRIGGER'; | ||
|
|
||
| const VALID_TRIGGERS: readonly TriggerType[] = ['hourly', 'daily', 'pullRequest']; |
There was a problem hiding this comment.
I'm not sure about this trigger system. It adds complexity to the library, it's hard to modify/extend (e.g. adding montly trigger) and it relies on an external trigger system (hourly tests won't be run hourly if you misconfigure cron schedule).
I would rather have my tests split in directories based on how often I want to run them:
└── test
└── platform
├── hourly
├── daily
└── montly
// or something different
└── test
└── platform
├── tier1
├── tier2
└── tier3
And it's then your (devs) job to run the correct test folder with correct schedule:
0 * * * *fortest/platform/hourlyortest/platform/tier10 0 * * *fortest/platform/dailyortest/platform/tier2
There was a problem hiding this comment.
I will give this another thought. Your proposal would simplify it and we could always run daily tests inside weekly ones etc. But we will still need to define somewhere if it should run on PR and potentially the alerting config.
The advantage of my PR is that it makes everything super (maybe unnecessarily) flexible, you can name your files as you want and just compose via describe which is the intended way for tests. And we can add any other config easily later and reuse the composition system.
it's hard to modify/extend (e.g. adding montly trigger)
We could make this just a plain string and set it workflow but this is not something we will change often, at max we will likely have something like every 10 min, hourly, daily, weekly, monthly so it can be just hardcoded
it relies on an external trigger system (hourly tests won't be run hourly if you misconfigure cron schedule).
I didn't write the github-actions-source part yet but it would basically just replace
with:
subtest: core
with something like
with
trigger: hourly
and you just need to set it to valid value. So it is similar to what we already have, just checked at "action validation time". Currently, you should run core hourly but also can misconfigure it :)
There was a problem hiding this comment.
@oklinov How would you handle the "run on PR: true/false"? Something like this and then make the subtest into a list and pass it also for PR action?
└── test
└── platform
├── hourly-and-pr
├── daily-and-pr
├── daily-without-pr
└── montly-without-pr
There was a problem hiding this comment.
what if we leave that up to the team? they could use this to configure different suites and then just let them match the triggers with the suites they want in the github workflow file.
We already have ways of grouping tests and ways of doing specific triggers, this lets us marry the two.
There was a problem hiding this comment.
@metalwarrior665 yes, something like that. We could wrap the logic in npm scripts:
{
"scripts": {
"test:platform:hourly": "npx vitest test/platform/{hourly-and-pr}",
"test:platform:daily": "npx vitest test/platform/{daily-and-pr,daily-without-pr}",
"test:platform:monthly": "npx vitest test/platform/{hourly-and-pr,daily-and-pr,daily-without-pr}",
"test:platform:pr": "npx vitest test/platform/{hourly-and-pr,daily-and-pr}",
}
}or, with @JuanGalilea suggestion using vitest projects:
{
"scripts": {
"test:platform:hourly": "npx vitest --project hourly-and-pr",
"test:platform:daily": "npx vitest --project daily-and-pr --project daily-without-pr",
"test:platform:monthly": "npx vitest --project monthly-without-pr",
"test:platform:pr": "npx vitest --project hourly-and-pr --project daily-and-pr",
}
}There was a problem hiding this comment.
Looks good. So let's do it on Google Maps first and if it works well, will make issue for everyone.
There was a problem hiding this comment.
another approach:
test/platform/
├── utils.ts
├── google-places.hourly.test.ts ← runs hourly, PR, daily
├── core.hourly.test.ts ← runs hourly, PR, daily
├── google-maps-extractor.pr.test.ts ← runs PR, daily
├── google-maps-reviews.pr.test.ts ← runs PR, daily
├── charge-dedup.daily.test.ts ← runs daily only
├── google-maps-result-counts.daily.test.ts
├── google-maps-email-extractor.daily.test.ts
"test:platform:hourly": "vitest run test/platform/*.hourly.test.ts",
"test:platform:pr": "vitest run test/platform/*.{hourly,pr}.test.ts",
"test:platform:daily": "vitest run test/platform/*.{hourly,pr,daily}.test.ts"I think I like this one the most as it gives you even more flexibility 🤔 @metalwarrior665 @JuanGalilea wdyt?
UPDATE: we could even support nesting so it would be possible do structure it like this:
test/platform/
├── google-places/
│ ├── core.hourly.test.ts
│ └── result-counts.daily.test.ts
├── google-maps-reviews/
│ ├── core.hourly.test.ts
│ └── pagination.pr.test.ts
└── utils.ts
There was a problem hiding this comment.
I'm a bigger fan of the folders because they look more readable, the filenames could work but it should be more polished to a standard format since you have several boolean flags for each file/
There was a problem hiding this comment.
what do you mean by "several boolean flags for each file"? 🤔
There was a problem hiding this comment.
Sorry, my bad, you only need one for scheduling (I confused myself by having separate weekly: true, daily: true but in practice we need only one as hourly implies daily etc.)
So yeah, I think this can work. Best practice should be to always specify all flags but with backward compatibility for now. I would make the PR part more explicit: pagination.pr-off.weekly.test.ts vs pagination.pr-on.weekly.test.ts
|
Putting this on hold and going forward with simpler solution for now. I will let this sit here for a while so I think about it again once a while. |
This will need some small changes in source actions but for repos, it is backward compatible
AI SUMMARY BELOW
Summary
This PR introduces a trigger-based test filtering system and alert configuration framework to the actor testing library. Tests and test suites can now be conditionally run based on trigger types (hourly, daily, pull request) and configured to send alerts to specific channels (Slack) when they fail.
Key Changes
New trigger system (
lib/trigger.ts):getCurrentTrigger(): Reads theTEST_TRIGGERenvironment variable to determine the active trigger typeshouldRunForTrigger(): Evaluates whether a test should run based on itsrunWhenconfiguration and the current triggerhourly,daily,pullRequestHierarchical trigger inheritance:
mergeInheritedTriggers()function that merges trigger configurations field-by-field through the describe hierarchydescribeblocks, allowing child tests to inherit and override parent trigger settingsrunWhenandalertsconfigurations are shallow-merged so children only need to override specific keysUpdated
describe()API:DescribeConfig) or legacy string-based argumentsname,triggers, andoptionspropertiesUpdated
testActor()andtestStandbyActor()APIs:TestActorConfig) or legacy string-based argumentsname,triggers, andoptionspropertiesdescribeblocks with test-level overridesNew type definitions (
lib/types.ts):TriggerType: Union of valid trigger typesRunWhenConfig: Controls which triggers enable a testAlertsConfig: Defines alerting channels (currently Slack)TriggerConfig: CombinesrunWhenandalertsDescribeConfigandTestActorConfig: Configuration objects for the new API styleDescribeOptionsandActorOptions: Vitest-level test optionsUpdated test reporting (
bin/test-report.ts):alertsmetadata from test resultsalerts.slack !== falseComprehensive test coverage:
test/unit/triggers-merging.test.ts: Tests the trigger merging logic across various inheritance scenariostest/unit/trigger.test.ts: Tests trigger detection and filtering logicExported public API (
index.ts):getCurrentTrigger()functionNotable Implementation Details
https://claude.ai/code/session_01BHoLoyAPZsZB41xxysLagd