feat(skip): add #[Skip] attribute to skip tests declaratively - #314
Conversation
…back Review feedback on php-testo#314: the attribute now implements Interceptable and declares #[FallbackInterceptor(SkipInterceptor::class)], mirroring Retry. The interface alone would break class-level usage (an Interceptable without a fallback alias makes the attributes interceptor throw at pipeline build), so the pair goes together. The TestPlugin registration stays: the case-level fallback path only reads class attributes, so a method-level #[Skip] still needs the registered instance; the duplicate spawn for class-level cases is collapsed by the sorter's conflict policy. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
|
Found some bugs, fixing them right now. Marked this PR as draft until they're fixed. |
…rviving tests LifecycleInterceptor::collectCaseFunctions() resolved a function-based case's source file by scanning the case's remaining tests for the first ReflectionFunction. Any outer case interceptor that prunes tests before the lifecycle interceptor runs (it is the innermost, order PHP_INT_MAX) could therefore empty the scan: with #[Skip] (PR php-testo#314) parking every #[Test] function of a file, the path came back null and the #[BeforeClass]/#[AfterClass] hooks were silently dropped, breaking the documented Skip contract that "BeforeClass/AfterClass hooks still run - also when every test of the case is skipped". Class-based cases were unaffected (hooks come from CaseDefinition::$reflection). The path now comes from CaseDefinition::$file, which every locator fills from the tokenized file the case was discovered in, so hook discovery no longer depends on who pruned the test set or when (the same hole existed for a fully filtered-out case). Motive check: the surviving-test scan (da29d6a, 2026-07-01) predates CaseDefinition::$file (f17f4ed, 2026-08-02); it was the only way to get the path at the time and was simply never migrated - no semantic reason to keep it. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
0c06301 to
4664f13
Compare
…back Review feedback on php-testo#314: the attribute now implements Interceptable and declares #[FallbackInterceptor(SkipInterceptor::class)], mirroring Retry. The interface alone would break class-level usage (an Interceptable without a fallback alias makes the attributes interceptor throw at pipeline build), so the pair goes together. The TestPlugin registration stays: the case-level fallback path only reads class attributes, so a method-level #[Skip] still needs the registered instance; the duplicate spawn for class-level cases is collapsed by the sorter's conflict policy. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
4664f13 to
6059ac0
Compare
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
A plain marker attribute (method, class, or free function) plus a registered
case-level TestCaseRunInterceptor: parked tests are filtered out of the case
before any lifecycle hooks and reported back as synthetic Skipped results with
a composed reason ("{testId} is skipped via #[Skip] ==> {reason}"), delivered
through a batch-runner wrapper so reporters render them inside the case block
with no reporter changes.
Assisted-By: Claude Fable 5 <noreply@anthropic.com>
Unit: interceptor filtration before $next, reason composition, synthetic result shape, origin attribute, batch-runner wrapping, pipeline events. Feature: full v1 contract over a stub catalog - method/class/function targets, inheritance from parent and trait, lifecycle hook contract, instantiation rules, DataProvider/Retry/Repeat/fiber composition, summary arithmetic and the success of an only-parked run. Plus one narrow JUnitWriter case pinning that a Skipped result renders its failure message. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
…y-tests write-tests gains a "Parking a test" section with the skip-tool comparison table and the runtime contract; plugin-author points to SkipInterceptor as the canonical shipped "return, do not throw" example; flaky-tests adds the parking branch to the decision flow. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
feat bumps testo/test 0.1.6 -> 0.2.0 on release: widen the root require to "0.1.6 - 1" so composer resolves both before and after the release, and move the path-repository version to 0.2.x-dev per docs/spec/plugin-creation.md. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
…back Review feedback on php-testo#314: the attribute now implements Interceptable and declares #[FallbackInterceptor(SkipInterceptor::class)], mirroring Retry. The interface alone would break class-level usage (an Interceptable without a fallback alias makes the attributes interceptor throw at pipeline build), so the pair goes together. The TestPlugin registration stays: the case-level fallback path only reads class attributes, so a method-level #[Skip] still needs the registered instance; the duplicate spawn for class-level cases is collapsed by the sorter's conflict policy. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
Class-level #[Skip] spawns a duplicate interceptor instance through the fallback alias; ConflictPolicy::First (previously implicit) collapses it onto the instance registered by TestPlugin. Spell the policy out and explain why, mirroring DataProviderInterceptor. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
The fallback interceptor is spawned from class attributes only (AttributesInterceptor::runTestCase), so without TestPlugin it rescues a class-level #[Skip] alone; a method- or function-level #[Skip] is inert in that setup. The previous wording implied full standalone coverage. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
…ameter The instance is built both by the container (TestPlugin registration, where an attribute cannot be resolved) and by the injector on a fallback spawn (where the attribute is passed and ignored); attributes are looked up per case in findParked(). A Skip parameter would break the container path at pipeline construction. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
test(test): pin the declared testType of SkipInterceptor SkipInterceptor declares testType: TestType::Test, so the type filter drops it for #[Bench] and #[TestInline] cases and the attribute has no effect there. Add the point to the Skip runtime contract and the write-tests skill, and pin the declaration with a unit test. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
TerminalLogger reads the description from the result attributes, where the regular test path stamps it; the synthetic result built for a parked test lacked it, so the PHPDoc description was not rendered. Stamp TestDefinition::getDescription() the way TestRunner does. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
…tPlugin Every existing feature test runs through suite defaults where TestPlugin is registered, so the FallbackInterceptor declaration itself was never exercised. Run a convention-discovered catalog with SuitePlugins::without(TestPlugin::class) and assert the class-level #[Skip] still parks every test, with exactly one result per test. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
…s live The dedup invariant (registered interceptor + fallback spawn collapse into one delivery) was held only by the arithmetic of the summary tests. Name it: a full-application run of a class-level parked catalog yields exactly one result per test in its CaseResult. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
Integration proof of the #[Skip] contract documented on the attribute: BeforeClass/AfterClass hooks still run when every test of the case is parked. Drives the real pipeline over a function-based and a class-based catalog and checks the hooks fire exactly once while the per-test hooks stay silent. Complements the Skip-independent unit regression test that landed with the lifecycle fix: that one pins hook discovery on a pruned case in isolation, this one pins the contract with the actual #[Skip] interceptor doing the pruning. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
… neighbor Audit follow-up. namedReasonArgument asserted a PHP language guarantee (named argument binding to the single promoted parameter) and could not fail while its positional twin passes, so it is removed. enabled_function in skip_functions.php was asserted by nothing: TestRunner::runTest() returns only the requested result, so a throwing neighbor would go unnoticed. The new feature test runs it and expects Passed, closing the one untested cell of the semantics matrix: an enabled function of a partially parked file survives the wrapped batch runner. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
…n order The two function-based tests resolved their targets only because an earlier test's inner catalog run happened to include skip_functions.php: functions are not autoloadable, and TestRunner::runTest() throws "Invalid test function provided." when the name does not exist yet. Running either test in isolation (--filter) errored before any assertion. Load the stub in the constructor, the same way the other function-based feature tests already do. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
The docs no longer promise the skip reason in every report, and the construction, fiber and provider checks now assert per-run evidence. Assisted-By: Claude Fable 5.1 <noreply@anthropic.com>
Assisted-By: GPT-5.6 Terra
php-testo#318 removed TestDefinitions::undefine() and gave TestDefinition an `active` flag. The interceptor now flips that flag the way the filter plugin does, walks the case's active tests only, and the docblocks describe the mechanic that is left instead of the one that is gone. Three unit tests pin deactivation-not-discard, an inert #[Skip] on a non-test member, and no Skipped report for an already-filtered test; a fourth pins the declared order and conflict policy. Assisted-By: Claude Fable 5.1 <noreply@anthropic.com>
6379b2a to
58bd873
Compare
|
@roxblnfk sorry for the pause. PR is ready for review, rebased onto the current 1.x. |
…case pipeline The case pipeline is built from class attributes alone, so an attribute that has to act on the whole case from a single test (take it out before the class-level hooks) could only be wired by a plugin. Scanning every `Interceptable` on the tests would make a method-level `#[RunInFiber]` install a fiber batch runner for the entire case, hence an explicit opt-in interface. Test-level attributes are not stamped on `CaseInfo::$attributes`. Assisted-By: Claude Fable 5.1
04d7d87 to
bda23e9
Compare
`#[Skip]` is a one-attribute plugin of the same shape as `testo/retry` and `testo/repeat`, and the plugin naming rule puts the top-level class `\Testo\Skip` in a package of the same short name. The manifest starts at 0.0.0 so the first release is 0.1.0. Assisted-By: Claude Fable 5.1
… the `#[Skip]` docblocks Assisted-By: Claude Fable 5.1
…stPlugin` docs(skills): point `#[Skip]` guidance at the self-wiring attribute and document `CaseInterceptable` The attribute is wired by its `#[FallbackInterceptor]` alone: as a `CaseInterceptable` it reaches the case pipeline from a method or function too, so no plugin registers anything and the attribute works in any suite. `ConflictPolicy::First` now collapses the instances spawned per `#[Skip]` occurrence. Assisted-By: Claude Fable 5.1
bda23e9 to
b9fd5d6
Compare
…attribute # Conflicts: # composer.json
…in convention The root requirement takes the shape every freshly added plugin uses, and the plugin pins the core release that ships `CaseInterceptable` with an explicit patch component, like its neighbours. Assisted-By: Claude Opus 5
CI pins `COMPOSER_ROOT_VERSION` to `resources/version.json`, so the root package is the released core (0.10.47) and a constraint on the unreleased 0.11.0 makes the whole install unresolvable. Every other plugin names an already-released core the same way; bump this one once 0.11.0, which ships `CaseInterceptable`, is out. Assisted-By: Claude Opus 5
…stating it The case-level scan walks the active tests, and that is the load-bearing part: an attribute on a test the filter dropped must not shape the case. The stale "merge and instantiate" line went with it — instantiation now happens a few lines above. Assisted-By: Claude Opus 5
cb512e4 to
44159a6
Compare
refactor(core)!: drop `CaseInterceptable` A skipped test is a fact about the definition, not about the run: with `TestDefinition::$skipped` a locator can state it ahead of the run, and the lifecycle plugin and the test runner read it instead of a case interceptor having to deactivate tests and hand back synthetic results. A skipped test stays active, so the case keeps it and reports it, and the runner returns the Skipped result itself when no interceptor did. `CaseInterceptable` existed only to get a method-level `#[Skip]` onto the case pipeline; with the flag on the definition nothing reaches the case pipeline from a test, and the case pipeline reads class attributes alone again. Assisted-By: Claude Fable 5.1
…est to run A skipped test has no body to set up, so the per-test hooks stay silent for it, and a case whose active tests are all skipped, or that an outer interceptor left without a test, gets no class-level hooks either: there is nothing they would prepare for. Previously the class hooks fired for such a case because hook discovery only looked at the non-tests. Assisted-By: Claude Fable 5.1
… per-test pipeline docs(skills): describe the skip flag instead of `CaseInterceptable` `SkipPlugin`, part of the default suite plugins, sets `TestDefinition::$skipped` from a case locator, which is what keeps the lifecycle hooks away from the skipped tests and from a fully skipped case. `SkipInterceptor` becomes a per-test interceptor spawned by the attribute, ordered outer to the fiber wrap and the data provider, and returns the Skipped result with the reason; the batch-runner wrap and the hand-dispatched events are gone with it. The Skipped result does not depend on the plugin: without it only the hooks are left unaware of the skip. Assisted-By: Claude Fable 5.1
…what reads them docs(core): cut the skipped-test comment back to its placement facts docs(lifecycle): drop the comment restating the skipped flag `SkipInterceptor` claimed it never throws because a throw is a runtime skip that reports differently. The real constraint is that a throw from an interceptor leaves the pipeline and lands as `Aborted`, so the result has to be returned. The rest is scope: a comment holds what its own lines cannot. What the flag makes lifecycle hooks and the runner do belongs at those two sites, where it already is, so `SkipLocatorInterceptor` keeps only the split against `SkipInterceptor` — flag here, reason there. In its place that docblock now separates the two type mechanisms: `testType` drops the interceptor from a run filtered to other types, while a located file still yields cases of every type, which is why the per-case check must stay. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Lifecycle contract, inherited-reason selection, and split-package compatibility issues remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds a declarative #[Skip] attribute that reports tests as skipped without executing their bodies.
Changes:
- Adds the
testo/skipplugin and core skipped-definition support. - Integrates lifecycle handling and comprehensive tests.
- Updates documentation, dependencies, and release configuration.
File summaries
| File | Description |
|---|---|
| tools/phpunit/phpunit.xml | Includes skip plugin coverage. |
| tools/phpunit/infection.phpunit.json | Adds skip mutation scope. |
| tests/Output/Unit/JUnit/JUnitWriterTest.php | Tests skipped-reason XML output. |
| tests/Core/Definition/TestDefinitionsTest.php | Tests skipped filtering. |
| tests/Application/Stub/Skipped/SkippedByLocator.php | Adds skipped-definition fixture. |
| tests/Application/Stub/Skipped/FlagSkippedPlugin.php | Adds locator flagging fixture. |
| tests/Application/Feature/Runner/SkippedDefinitionTest.php | Tests core skipped execution. |
| testo.php | Registers skip test suites. |
| skills/testo-write-tests/SKILL.md | Documents #[Skip] usage. |
| skills/testo-plugin-author/SKILL.md | Documents skip integration. |
| skills/testo-flaky-tests/SKILL.md | Documents skipping flaky tests. |
| resources/version.json | Versions the skip plugin. |
| plugin/skip/tests/Unit/SkipPluginTest.php | Tests plugin registration. |
| plugin/skip/tests/Unit/SkipAttributeTest.php | Tests attribute metadata. |
| plugin/skip/tests/Unit/Internal/SkipLocatorInterceptorTest.php | Tests definition flagging. |
| plugin/skip/tests/Unit/Internal/SkipInterceptorTest.php | Tests skipped-result creation. |
| plugin/skip/tests/Unit/Fixture/SkipMixedMethodsFixture.php | Provides mixed-method fixtures. |
| plugin/skip/tests/Unit/Fixture/SkipClassLevelFixture.php | Provides class-level fixtures. |
| plugin/skip/tests/suites.php | Configures skip test suites. |
| plugin/skip/tests/Stub/SkipSummary/OnlySkipped/OnlySkippedStub.php | Covers all-skipped summaries. |
| plugin/skip/tests/Stub/SkipSummary/Mixed/MixedStub.php | Covers mixed summaries. |
| plugin/skip/tests/Stub/SkipStandalone/StandaloneSkippedTest.php | Covers standalone discovery. |
| plugin/skip/tests/Stub/Skip/SkipWithRetryStub.php | Covers retry composition. |
| plugin/skip/tests/Stub/Skip/SkipWithRepeatStub.php | Covers repeat composition. |
| plugin/skip/tests/Stub/Skip/SkipWithHooksStub.php | Covers lifecycle composition. |
| plugin/skip/tests/Stub/Skip/SkipWithDataProviderStub.php | Covers provider suppression. |
| plugin/skip/tests/Stub/Skip/SkipTraitStub.php | Covers trait inheritance. |
| plugin/skip/tests/Stub/Skip/SkipParentStub.php | Defines inherited class skip. |
| plugin/skip/tests/Stub/Skip/SkipOverridingMethodStub.php | Covers method inheritance. |
| plugin/skip/tests/Stub/Skip/SkipOverriddenMethodParentStub.php | Defines inherited method skip. |
| plugin/skip/tests/Stub/Skip/SkipNonStaticHookStub.php | Covers non-static hooks. |
| plugin/skip/tests/Stub/Skip/SkipMethodStub.php | Covers method-level skips. |
| plugin/skip/tests/Stub/Skip/SkipMarkerTrait.php | Defines trait skip marker. |
| plugin/skip/tests/Stub/Skip/SkipInFiberStub.php | Covers fiber composition. |
| plugin/skip/tests/Stub/Skip/SkipConstructorSpyStub.php | Checks construction suppression. |
| plugin/skip/tests/Stub/Skip/SkipClassLevelStub.php | Covers class-level skipping. |
| plugin/skip/tests/Stub/Skip/SkipClassAndMethodStub.php | Covers reason precedence. |
| plugin/skip/tests/Stub/Skip/SkipChildStub.php | Covers parent inheritance. |
| plugin/skip/tests/Stub/Skip/skip_functions.php | Covers function-level skipping. |
| plugin/skip/tests/Stub/PipelineEntrySpyPlugin.php | Tracks pipeline entry. |
| plugin/skip/tests/Feature/SkipSummaryTest.php | Tests summary behavior. |
| plugin/skip/tests/Feature/SkipFeatureTest.php | Tests end-to-end semantics. |
| plugin/skip/tests/Feature/SkipFallbackStandaloneTest.php | Tests fallback interception. |
| plugin/skip/src/SkipPlugin.php | Registers skip location handling. |
| plugin/skip/src/Internal/SkipLocatorInterceptor.php | Flags skipped definitions. |
| plugin/skip/src/Internal/SkipInterceptor.php | Produces skipped results. |
| plugin/skip/Skip.php | Defines the public attribute. |
| plugin/skip/README.md | Documents the package. |
| plugin/skip/composer.json | Defines package metadata. |
| plugin/skip/.github/workflows/close-prs.yml | Configures mirror PR handling. |
| plugin/lifecycle/tests/Unit/Internal/LifecycleInterceptorTest.php | Tests skipped lifecycle behavior. |
| plugin/lifecycle/tests/Stub/FullySkipped/FullySkippedClassStub.php | Adds skipped-class fixture. |
| plugin/lifecycle/tests/Stub/FullySkipped/fully_skipped_functions.php | Adds skipped-function fixtures. |
| plugin/lifecycle/tests/Feature/FullySkippedCaseFeatureTest.php | Tests fully skipped cases. |
| plugin/lifecycle/src/Internal/LifecycleInterceptor.php | Integrates skipped definitions. |
| core/Pipeline/Internal/AttributesInterceptor.php | Removes a redundant comment. |
| core/Core/Definition/TestDefinitions.php | Adds skipped-state filtering. |
| core/Core/Definition/TestDefinition.php | Adds the skipped flag. |
| core/Application/Internal/Runner/TestRunner.php | Handles flagged skipped tests. |
| core/Application/Config/Plugin/SuitePlugins.php | Enables the skip plugin. |
| composer.json | Adds skip package wiring. |
| .github/workflows/split-publish.yml | Adds skip release tags. |
| .github/.release-please-config.json | Configures skip releases. |
Review details
Suppressed comments (2)
plugin/skip/Skip.php:60
- This introduces a public test attribute with faithful PHPUnit/Pest equivalents, but omits the bridge Rector rules, fixtures, and
FEATURE_PARITY.mdupdate required byAGENTS.md. Although issue #313 defers that work to PR 2, the repository guideline requires public feature parity in the same change; add the conversions (including class-level handling), or add the documented stub and TODO when faithful conversion is not possible.
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
#[FallbackInterceptor(SkipInterceptor::class)]
final readonly class Skip implements Interceptable
plugin/skip/README.md:28
- “Every report … with its reason” overpromises the reporting behavior: terminal output omits the reason, and compact JSON only includes the skipped count. Limit the claim to the reporters that render the message.
Marks a test, a test class or a test function as skipped without deleting or hiding it. The test is not executed, but stays in every report as Skipped with its reason, so parked tests remain visible until someone returns to them.
- Files reviewed: 63/63 changed files
- Comments generated: 4
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…rviving interceptor docs(skip): stop naming other plugins in the `#[Skip]` docs docs(core): say the skipped flag binds its readers, not lifecycle hooks `Reflection` lists an overriding function's own attribute before the one it inherits, while `ConflictPolicy::Last` kept the final interceptor of the position — so an override that repeated `#[Skip]` with its own reason reported the prototype's. The interceptor no longer takes the occurrence that spawned it: it reads the nearest declaration back with `limit: 1`, falling back to the class, which makes the reported reason independent of how the pipeline collapses the duplicates. `ConflictPolicy::First` now collapses them, since the instances are interchangeable. A skipped test is a core state, so the attribute documents that state and leaves it to `#[Retry]`, the lifecycle hooks and the rest to say what each of them does with it. Both meanings stay distinguishable: the flag is set before the run, a thrown `SkipTest` only once the body has started. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… hooks for an empty case Non-tests do outlive the filtering of tests, which is what keeps a partially filtered or partially skipped case finding all of its hooks. The claim built on top of that — hooks running for a case whose tests were all filtered out — named the wrong cause: a filter deactivates at location time and such a case is dropped before the run. That state came from a case interceptor deactivating during the run, and the hooks no longer fire for it. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs(skip): halve the `#[Skip]` docblock A function has no class to fall back on, so the fallback that composes the message without a reason runs a different path there than the method it was pinned on. The spy list gains the function too, so a regression that let it reach the pipeline is caught where its neighbours already are. The docblock had grown by accretion to forty-four lines for a one-parameter attribute and restated itself: the worked example repeated the paragraph under it, and the message format was given twice. Everything a reader cannot get from the code survives. The reporters that show the reason are named in the user guide instead, where the rest of their behaviour already is. Assisted-By: Claude Fable 5.1 <noreply@anthropic.com> Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🔍 What was changed
#[Skip('reason')]on a test method, class or function: the body never runs and the test is reported asStatus::Skipped, counted in the totals, carrying{testId} is skipped via #[Skip] ==> {reason}in the result's failure message. JUnit, TeamCity and HTML show that message; the terminal prints the skipped line without it.Ships as its own package,
testo/skip.SkipPluginjoins the default suite plugins, and the attribute additionally wires its own interceptor, so the skip still works in a suite that dropped the plugin — only the lifecycle hooks are then unaware of it.core/gains one flag,TestDefinition::$skipped, plus the accessors and the runner fallback that read it. No newStatuscase.How it works
SkipLocatorInterceptorflags the marked tests on their definitions. They stay active, so the case keeps them, runs and reports them.SkipInterceptorreturns the Skipped result at the entry of the per-test pipeline, outer to fibers, data providers,#[Retry]/#[Repeat], coverage and#[BeforeTest]/#[AfterTest]. None of them engage, and a data-driven test yields a single Skipped entry without calling its provider.#[BeforeClass]/#[AfterClass]run while the case still has a test to run. When every test of it is skipped they stay silent and the case class is never constructed.Semantics
#[Skip]wins over the class-level one, reason included.reasonis optional and the attribute is not repeatable.#[Bench]/#[TestInline]targets.Review notes
SuiteFactorydrops a case left without active tests, so a fully skipped case would vanish from the report together with the tests it was supposed to show.throw new SkipTest(...)from an interceptor does not skip anything — it leaves the pipeline and lands asStatus::Aborted. The result has to be returned instead.Checklist
#[Skip]attribute to declaratively skip a test or test class, with a reason #313#[Skip]in all three directions #340#[Retry],#[Repeat]and fibers, the hook counters for a partially and a fully skipped case, and the core fallback for a definition flagged without the attributeDocumentation
Skills
testo-write-tests,testo-plugin-authorandtesto-flaky-testscarry the new contract.