Test Result Caching
Skip Apex tests whose tracked inputs have not changed since their last run, and reuse the recorded pass or fail instead of running them on the org again.
The test result cache stores one result per test method. Its key covers the test’s code, the code it reaches through static references, and the triggers that run on the records that code writes with the code those triggers call (in per-method mode, with the daemon running and Java 11+ available, as read from your local project; in per-class mode as stored in the org), together with your local objects and the metadata types LATdx tracks. Without the daemon, no cached result is reused. In per-method mode, a test whose reached code makes a dynamic Apex call LATdx cannot pin down (such as Type.forName with a class name read from a record), including code a trigger runs on the records it writes, has every local Apex class in its key; in per-class mode, code called only by name at runtime is not part of the key. When none of the inputs LATdx tracks have changed since a test’s last run, LATdx reuses the cached result instead of running the test. Saving an Apex class while the LATdx daemon is running clears the cached results of that class, if it is a test class, and in per-class mode those of recently run test classes that reach it, unless the save only changes comments or whitespace without moving any code (see Invalidation for the exact rules). The cache is on by default for latdx test run (-n, -t, -f, -d, --affected, or no selection flag); only a -f or -d run on the experimental --engine latdx does not use it. To skip it for one run (no reads and no writes), use --no-cache. To re-run everything and refresh the stored results, use --force.
Cache Correctness
The cache is designed to run a test whenever it cannot match the test’s inputs to a stored result. LATdx skips a test when the inputs it tracks for that test match a stored result exactly. A match means nothing LATdx tracks has changed. It does not prove that a fresh run would give the same result. If LATdx cannot compute a test’s cache key, it runs the test on the org, and the excluded cases are never served from the cache. Untracked metadata, and dynamic Apex in per-class mode, are not treated as uncertain, so they do not force a run.
What the cache does not promise:
- No stale-pass guarantee. LATdx aims never to return a stale pass, but it cannot guarantee it. A cached result can be stale when the test depends on: code chosen by name at runtime (dynamic Apex) in per-class mode, or in per-method mode by a call LATdx does not see or one marked
@dynamic-ignore; your local project and the org being out of sync (changes made only in the org, or runs made before you deploy a local edit); metadata types LATdx does not track; or managed-package code the test does not reach through a staticnamespace.ClassNamereference (for example package triggers). If you suspect a stale result, re-run with--forceto refresh it;--no-cacheonly skips the cache for one run and leaves the stored result in place. See Known Limitations. - No speed guarantee. Cache hits avoid org execution, but computing cache keys has a cost. On a large project or right after the daemon starts, a run with few or no hits can be slower than running with
--no-cache.
Three things protect you from stale results:
- The cache key. A stored result is reused only when its key matches. When the LATdx daemon is running, Java 11 or newer is available (via
JAVA_HOMEorPATH) and LATdx’s Java analyzer starts, each test method gets its own key. Otherwise the cache falls back to per-class keys. Per-class keys cover the Apex code of the test class, of every class it reaches through static references, and of the triggers that run when that code writes records (with the classes those triggers call), as stored in the org, together with the same local objects and metadata types as per-method keys, so a trigger or other metadata change invalidates them too. Only the daemon reads those local files: a run without it serves nothing from the cache and stores nothing. In both modes a test is skipped when its key matches a stored result. The--jsonoutput’scacheReport.cachePrecisionshows which mode a run used. See Overview for what each key covers. - Shadow validation. When a run already sends some tests to the org, LATdx also re-runs a small random sample of the cache hits in that same run and compares the results. If a sampled re-run disagrees with the cached result, the fresh result is used in that run’s final results and the mismatch is counted in the
--jsonoutput (cacheReport.shadowMismatches) and appended to~/.latdx/workspaces/<id>/cache/shadow-mismatches.ndjson. No terminal warning is shown and the stored entry is not replaced, so run with--forceto refresh it. Sampling never triggers an org run on its own: when every test is a cache hit, nothing is re-run. Sampled tests ride along in the org run that is already happening, which can add a few tests to it. Set the sample withLATDX_CACHE_SHADOW_RATEas a fraction from 0 to 1. The default is0.01(1%),1re-checks every hit, and0turns sampling off. Values outside that range are ignored and the default is used. - The off switch.
--no-cacheturns the cache off for one run: nothing is read from or written to the cache.LATDX_CACHE_DISABLE=1(alsotrue,onoryes) turns it off for every run where it is set: no cached result is reused and no result is stored, so--forcecannot refresh results while it is set.--forcere-runs every selected test and stores the fresh results. With--affected, all three also turn off the step that leaves out unchanged tests (see below), so every test--affectedselects runs. All three take effect on the next run. No redeploy or daemon restart is needed. The one exception is the daemon’s save-time cleanup, which follows the environment the daemon was started with (see Invalidation).
Cached tests are marked in the run output, and --json shows run-level cache counters (hits, misses, stale, precision, shadow samples and mismatches). latdx test impact <Class> lists the test methods that reach a given class.
With --affected, LATdx first checks each reached test method against the cache and leaves out the ones whose key still matches a stored pass. Those tests are not replayed: they are not marked ↺, not counted in the Summary’s Tests or Cache rows, and not shadow-sampled. With -v the run prints how many were left out, for example 2 changed files reach 4 test methods, running 3 (1 dropped as cache-fresh). A test whose stored result at its current key is a failure stays in the run and is replayed from the cache as a cached failure, so an unchanged failing test still fails the run. Tests the cache never serves stay in the run too: @IsTest(SeeAllData=true) tests, test classes LATdx cannot read from the org, and tests that reach an installed package’s Apex (see What Is Not Cached). --no-cache, --force, --dry-run and LATDX_CACHE_DISABLE=1 turn this step off. If every reached test matches a stored pass, none are left out: they are replayed from the cache as usual. latdx validate runs this same step and has no --no-cache or --force; see latdx validate.
Overview
Results are stored per org, per test class and per test method. Each stored result carries a SHA-256 fingerprint (its key) of what the test depends on, and a result is reused only when the key still matches. LATdx keeps the last few keys per method (up to 5), so undoing a change can hit again. How the key is built depends on the cache mode, which --json reports in result.cacheReport.cachePrecision.
The cache applies to every selection: -n, -t, -f, -d, --affected, or no selection flag. A -f or -d run turns each file into its class name (the file name without .cls; with -m, Class.method) and runs it exactly like -n: the org runs its deployed copy of the class, and cache hits, --force, --no-cache, save-time cleanup and the Test class not found in org warning work as they do for -n. For a class named without its methods, LATdx lists the test methods from its copy of the org’s class, which it first checks against the org, so a test method added directly in the org runs too. If that check fails, the class runs in full, and none of its results is served from or stored in the cache; the run warns Could not check test classes for changes in the org; running them whole, uncached in per-method mode, or Could not check org classes for changes; cached results are not used in per-class mode. Only with the experimental --engine latdx does a -f or -d run deploy your local files and run in-process without the daemon, and then no result is served from or stored in the cache (see Cache & Daemon Lifecycle).
Per-method mode (per-method) is used when the LATdx daemon is running, Java 11 or newer is available via JAVA_HOME or PATH, and LATdx’s Java analyzer starts. latdx test run starts the daemon automatically, except for a -f or -d run on --engine latdx. Java is detected when the daemon starts, so after installing or upgrading Java run latdx daemon restart. The key covers:
- The code the test method can reach through static calls: method and constructor bodies, field declarations, static blocks and class declarations, read from your local project. Comments, formatting and identifier casing in Apex classes are ignored. Edits to code the test method does not reach (for example, other test methods in the same class) do not affect its key, but the class’s
@TestSetupmethod counts as reached by every test in the class, so editing it invalidates them all. - The triggers in your local project that run when that code writes records, and the code those triggers reach. A cached result is reused only while the triggers on the records a test writes, and the code they run, are unchanged. When the test’s code, or a trigger it causes to run, inserts, updates, deletes, upserts, undeletes or merges records (with DML statements or
Databasemethods such asDatabase.insert), loads records withTest.loadData, converts leads withDatabase.convertLead, publishes platform events withEventBus.publishor sends email withMessaging.sendEmail, the key covers the triggers on those objects and the code they call, so editing a trigger handler, or a class it calls, re-runs the tests whose writes run that trigger, and not tests that write only other objects. Records the platform writes in turn count too: the parents of roll-up summary fields, records deleted with their master record (as defined in your local object folders), and the opportunity, quote or order of a line item. When LATdx cannot tell which object a write touches (for exampleinsert records;on aList<SObject>, adeleteof record Ids, a merge, or a flow started from Apex), the code of every trigger in your project counts. Change Data Capture triggers, and triggers LATdx cannot read, count for every test that writes records at all. Triggers that exist only in the org, and code that flows or other declarative automation run, are not covered (see Known Limitations). - The local metadata LATdx tracks: every object folder whose name starts with an uppercase letter (fields, list views, validation rules, record types and so on; compact layouts, search layouts and report types are left out), plus triggers, flows and flow definitions, workflow rules, custom labels, custom metadata records, profiles, permission sets and permission set groups, custom permissions, sharing rules, duplicate and matching rules, assignment rules, approval processes, value sets, Platform Cache partitions, static resources, restriction rules,
sfdx-project.json,.forceignoreand the LATdx analyzer version. These are folded into every test’s key, so a change to any of them invalidates every per-method entry in the project. Trigger and metadata files are hashed as raw text, so even a comment-only or whitespace-only edit to them counts as a change. - Every local Apex class (
.clsfile), for a test whose reached code makes a dynamic Apex call LATdx cannot pin down:Type.forName,JSON.deserializeorJSON.deserializeStrictwhose class LATdx cannot work out. Such a call can reach code no static reference names, so a change to any.clsfile in your project, comment-only edits included, re-runs that test. LATdx works out the class from a string or class literal (Type.forName('Foo'),Type.forName('ns', 'Foo'),JSON.deserialize(json, Foo.class),List<Foo>.class), or from a parameter of a static method, or of a private method that is not virtual or an override, when every call to it in the code the test reaches (triggers included) passes such a literal. A class worked out this way that the test already reaches adds nothing to the key; a name no local class declares (such asBooleanorSchema.Account) only re-runs the test once you add a class with that name.Schema.getGlobalDescribe,Schema.describeSObjects,System.Label.get,SObject.get/put,JSON.deserializeUntypedand theWithBindsforms ofDatabase.query,Database.countQueryandDatabase.getQueryLocatorrun no Apex, and they read what a static query or field access would (or what is already in every key), so they add nothing.Database.query,Database.countQueryandDatabase.getQueryLocatorwithout a binds map add the.clsfile of the class that calls them and of its superclasses, because a:namebind in the query can read a static variable of those classes. A call counts when it sits in a method or constructor the test reaches, anywhere else in a class the test reaches (a field initializer, an initializer block or a property), or anywhere in a trigger that runs on the records the test writes (item 2). Reading a custom label or naming a page directly (System.Label.MyLabel,Page.MyPage) is not a dynamic call. To exclude one call, add a comment containing@dynamic-ignoreat the end of its line, or on a line of its own directly above it (a comment at the end of the line above covers only that line), only when the call cannot reach code the test does not reach through static references. A class or trigger file LATdx fails to read or analyze counts as making such a call. Tests that make no such call keep the key described in items 1 to 3.
Per-method keys are built from local files only. Changes made directly in the org that are not in your local files are not seen. Tests run against the code in the org, so a run made before you deploy a local edit stores the old code’s result under the new key, and that result is reused after you deploy. Run with --force after deploying. This applies to -f and -d runs too: they run the org’s deployed copy of each class, not your local file.
Per-class mode (per-class) is used whenever per-method keys are unavailable: Java is missing or too old, LATdx’s Java analyzer cannot be found or fails to start, or the run falls back to direct execution without the daemon (the run prints Daemon unavailable, using direct execution). Check result.cacheReport.cachePrecision to see which mode a run actually used. The key covers:
- The full text of the test class in the org, comments included. LATdx keeps a copy of the org’s Apex, and before it computes per-class keys it checks every class and trigger those keys use against the org and fetches again the ones that changed, so an edit made directly in the org changes the key on the next run. If that check fails, the run warns
Could not check org classes for changes; cached results are not used(orOrg class changes did not settle; cached results are not used), and no result in it is served from or stored in the cache. All methods of the class share one key. - The source of every Apex class the test class reaches through static references, directly or through other classes, checked against the org the same way. Classes LATdx does not hold yet are fetched from the org during the run, so an edit made in the org to a class the test calls changes the key on the next run. If a class in that chain cannot be read (it fails to parse, or the org has that name only as a class of an installed package or only with hidden source), the test runs and its result is not stored.
- The triggers that run when that code writes records, and every class they reach. When the test class, or a class or trigger it reaches, inserts, updates, deletes, upserts, undeletes or merges records (with DML statements or
Databasemethods such asDatabase.insert), loads records from a static resource withTest.loadData, converts leads withDatabase.convertLead, or publishes platform events withEventBus.publish, the key covers the org’s active triggers on those objects and the classes those triggers reach, checked against the org the same way. An edit made in the org to such a trigger or to a class it calls (a trigger handler), or a trigger added to or removed from one of those objects, changes the key on the next run. When LATdx cannot tell which object a write touches (for exampleinsert records;on aList<SObject>,Database.deleteof record Ids,Test.loadDatawith an object token held in a variable, or a variable whose declaration LATdx does not find where the write uses it), every active trigger counts. Change Data Capture triggers (onAccountChangeEvent,Widget__ChangeEventand so on) count for every test that writes records at all, since platform automation can change their object without the test’s code writing it. A trigger that cannot be read (it fails to parse or its source is hidden) makes every test that writes records run without storing its result. Triggers of installed packages are not covered (see Known Limitations). If LATdx cannot list the org’s triggers, the run warnsCould not check org classes for changes; cached results are not used, and no result in it is served from or stored in the cache. - The local metadata LATdx tracks, read from your project by the daemon: the same metadata types as in per-method mode,
sfdx-project.jsonand.forceignore, and every file in your object folders (whatever the folder’s name, compact layouts, search layouts and report types included). They are folded into every test’s key, so a change to any of them invalidates every per-class entry in the project. These files are hashed as raw text, so a comment-only or whitespace-only edit to them counts as a change. - An internal LATdx version marker.
Only the daemon reads the local metadata, so a run that falls back to direct execution without it serves no result from the cache and stores none. If the daemon cannot read one of those files or folders, the run warns Could not read workspace metadata; cached results are not used, and no result in it is served from or stored in the cache. Per-class keys follow the Apex in the org but the metadata in your local project: a run made before you deploy a local metadata edit stores the result of the org’s old metadata under the new key, which is then reused after you deploy. Run with --force after deploying.
Neither mode takes a snapshot of the org’s schema. In per-class mode, code reached only by name at runtime (Type.forName, Callable) is not included; per-method mode covers it through every local Apex class, as described in item 4 above. Both modes’ keys cover every tracked local object, so dynamic SOQL sees the same object changes as static SOQL.
When anything in a test’s key changes, its next run is a miss. Changes outside the key do not cause a miss, so a stale result can be served. Examples are org data, configuration changed only in the org, dynamically referenced code in per-class mode, managed-package code a test reaches without a static namespace.ClassName reference, and metadata LATdx does not track (see Known Limitations). When that matters, re-run with --force to refresh the stored result (--no-cache only skips the cache for one run).
Storage: one JSON file per test class at:
~/.latdx/workspaces/<id>/cache/<orgId>/test-results/<testClass>.json<id> is the project folder name plus a short hash of its location, so each git worktree (or project folder) gets its own cache directory. Runs with or without the daemon use the same location. ~/.latdx/cache is a legacy location from older versions; latdx cache clear --legacy removes it.
What Is Not Cached
A test whose result can change without any change to its key cannot be cached safely. Such a test could be served a stale result without any warning, because the input that changed is not part of its key. Both modes track the local metadata listed in Overview; per-method mode reads Apex from your local project, and per-class mode reads it from the org.
LATdx never serves these tests from the cache and never stores their results:
@IsTest(SeeAllData=true)tests. These read existing org records, not only data the test creates itself, and that data can change with no code or metadata edit.SeeAllData=trueis detected on the class or on individual methods, in any casing; a plain@IsTestorSeeAllData=falsestays cacheable. These tests always run.- Test classes LATdx could not read. A test class that LATdx cannot fetch from the org (or whose source the org hides) does not run at all, whether or not the cache is on. When tests are named (
-n,-t,-f,-d,--affected, or the default workspace run), it is left out with aTest class not found in orgwarning; a--orgrun reportsCould not parse N classes fetched from orginstead. A named test class that LATdx fetches but cannot parse still runs, with aFailed to analyze classwarning, and its results are not stored. Check the run’s warnings if a class is missing from the results. - Tests that reach an installed package’s Apex. Apex calls a package’s classes through its namespace (
ns.Service.run(),new ns.Widget(),List<ns.Widget>). When the code a test reaches through static references, or through the triggers that run on the records it writes, names the namespace of a package installed in the org this way, the package’s code is not part of the key, so a package upgrade could change the result with no key change. Such tests always run. Your project’s own namespace and platform namespaces such asSystemorDatabasedo not count. A class, variable or field in your code that has the same name as an installed package’s namespace has the same effect. While such a package is installed, a test also always runs when the code it reaches includes a class LATdx cannot read (it fails to parse, or the org has that name only as a class of another package or only with hidden source), because LATdx cannot tell whether that class calls the package. If LATdx cannot list the org’s installed packages, the run warnsCould not list installed package namespaces; tests are run uncachedand no test in it is served from or stored in the cache.
A test whose per-method key cannot be computed also runs, and its result is not stored. In per-class mode, the same applies when a class the test reaches cannot be read, and to every test in the run when the local metadata cannot be read or the run has no daemon (see Overview). A test class whose test methods LATdx cannot list, or whose copy it could not check against the org, runs as a whole class and its results are not stored; when other tests in the run use the cache, the run warns Cannot enumerate test methods for N class(es); running them uncached.
These exclusions are automatic and never turn a real run into a cache hit. They apply whenever the cache is on, in both per-method and per-class mode. Excluded tests that run appear as run, not cached, on every invocation, even when nothing changed.
LATdx does not detect other tests whose result can change without a key change, for example tests that depend on the current date, on users or other records that exist only in the org, on dynamic Apex in per-class mode, on managed-package code they reach without a static namespace.ClassName reference (for example package triggers or flows), or on configuration changed directly in the org (for example profile or permission set changes made in Setup). Those can be served from the cache. Use --force to re-run every test and refresh the stored results, or --no-cache to skip the cache entirely for one run (see Disable).
Disable
The cache is on by default. Turn it off for one run with --no-cache, or for every run with LATDX_CACHE_DISABLE=1. Use --force to re-run everything and refresh the stored results:
# One-shot bypass: no cache reads and no cache writes for this invocation.
latdx test run -n MyTest -o my-org --no-cache
# Refresh: re-run every selected test and store the fresh results.
latdx test run -n MyTest -o my-org --force
# Hard off for one command (also accepts true, on, yes). If this starts the daemon,
# saves stop clearing cached results until `latdx daemon restart`; see Invalidation.
LATDX_CACHE_DISABLE=1 latdx test run -n MyTest -o my-org
# Hard off for the shell session: no cached result is reused or stored,
# and --affected leaves no test out.
export LATDX_CACHE_DISABLE=1These switches work the same for -f and -d runs as for -n and -t. Only a -f or -d run on the experimental --engine latdx does not use the cache, so there they change nothing.
If both --no-cache and --force are passed, --no-cache wins. Per-rule switches (LATDX_CACHE_RULE_<NAME>=off) are accepted but currently do not stop any test from being served from the cache. To force tests to run, use --no-cache, --force, or LATDX_CACHE_DISABLE=1.
The cache environment variables are documented in environment variables; --no-cache and --force are command flags documented in the CLI reference.
Run Summary
Every run ends with a Summary block. When the cache is on it adds a Cache row with the hit ratio, and the Wall row may carry an estimated time saving once LATdx has enough history for this org:
Tests PASS 19 FAIL 0 SKIP 0
Wall 357ms (saved ~2.3s)
Cache ↺ 19/19- Tests: every test is counted once, in the slot of its outcome. A test replayed from the cache counts in PASS, FAIL or SKIP exactly like a test that ran on the org, so the three numbers add up to the tests in the run. SKIP counts skipped tests only. The number of cached tests is in the Cache row.
- Wall: the total elapsed time of the
latdx test runcommand, from when the command started until the summary prints. A dim(saved ~Xs)suffix appears when LATdx estimates the run finished at least 1s faster than it would have without the cache. The estimate needs at least 3 recent comparable runs on this org, so it is often absent on early or CI runs. Estimated savings under 1 second are not shown. See What Saved means for how it is computed and its caveats. - Cache:
↺ hits/total, where total is hits plus tests that had to run. The row is hidden when no tests were checked against the cache. Cached tests are marked with a dim↺on their own row (a cached failure too, as✗ <method> ↺), and the Summary Cache row uses the same symbol in brighter amber. For the detailed counters (misses, stale, written, on/off and precision) run with--jsonand readresult.cacheReport.latdx cache statusshows cache disk usage and daemon state, not run counters. With--affected, tests left out because their key still matched a stored pass are not counted in any of these rows (see Cache Correctness).
With --no-cache or LATDX_CACHE_DISABLE=1 the Summary has no Cache row and no saved estimate, and the header line names the switch: cache off (--no-cache) or cache off (LATDX_CACHE_DISABLE=1).
With --json, the counts match the Tests row: result.summary.passing and result.summary.failing count each test once, cached or not, and result.cacheReport.hits is the cached count. result.cacheReport always contains hits, misses, stale, writtenOnExit, cacheEnabled and killSwitch; cachePrecision (per-method or per-class) when the cache is on; and shadowSamples and shadowMismatches when shadow validation re-ran some cached tests. The saved-time estimate is not included in JSON. With --no-cache there is no cacheReport; with LATDX_CACHE_DISABLE=1 it reports cacheEnabled: false, killSwitch: true and zero counters.
What Saved means
(saved ~Xs) is an estimate, not a direct measurement. LATdx predicts how long this run would have taken without the cache, using the fixed Salesforce overhead and parallelism it has measured on your recent runs against this org, then subtracts how long the run actually took. There are no daily or lifetime saved-time totals. Only the per-run estimate exists, and it is shown on the Wall row, not in JSON.
The suffix appears only when LATdx has enough history on this org: at least 3 of the last 20 recorded runs from the past 7 days with a similar number of test classes (1-5, 6-20, 21-50 or 51+), where each run sent at least one test to Salesforce and its tests, cached and freshly run together, add up to at least 5s of recorded execution time. Fully cached runs, --no-cache runs and LATDX_CACHE_DISABLE=1 runs are not recorded as history. The cache and its run history do not carry over between hosted CI runs, so the suffix (and cache hits) will normally not appear in CI.
The estimate uses the same format as Wall: one decimal below 10s (2.3s), whole seconds up to a minute (14s), then minutes and seconds (2m05s). Estimates under 1s, or negative, are not shown.
Honest caveats
The saved figure is built from real timings of your recent runs on this org, but it is a model, so expect it to be approximate:
- Parallelism and overhead are already in the estimate. How much Salesforce parallelizes a test run varies by org and run size; LATdx measures it from your own runs rather than assuming a fixed number, and no further adjustment is needed. When every selected test is served from the cache, LATdx skips submitting and polling a Salesforce test run entirely, and the estimate includes that fixed overhead.
- It is not tied to cache hits. The estimate does not check how many tests came from the cache, so a run with few or no hits (for example a cold run or a
--forcerun) that finishes faster than predicted can still show a saving.
Treat (saved ~Xs) as an approximate indicator, not a stopwatch. It compares this run with a prediction from your recent runs, so network or org load can move it, and occasionally it can appear even when few or no tests came from the cache.
Invalidation
A cached result stops being used on any of these axes:
- Key change: every run recomputes each test’s key, re-runs any test whose key no longer matches, and stores the new result. In per-method mode, whitespace-only and comment-only edits to Apex classes, and edits to code the test does not reach (code a trigger runs on the records the test writes counts as reached), leave the key unchanged (except for tests that read source line numbers, see Known Limitations, and tests whose reached code makes a dynamic Apex call LATdx cannot pin down, which re-run after any change to a
.clsfile, see Overview), while any change to an object, a trigger (including a comment) or other tracked metadata misses every test. In per-class mode, an edit in the org to the test class, to a class it reaches, or to a trigger on an object that code writes (or a class that trigger reaches) changes the key on the next run (comments included; see Overview), and, as in per-method mode, any change to an object, a trigger (including a comment) or other tracked metadata misses every test. Usually the daemon is running, so saving a code change to a class deletes cached results before the next run (see below). The key comparison catches what the daemon did not clear, such as edits made while it was stopped and tests a save leaves in place (see below), and every metadata or object change. - TTL: a cached result expires 7 days after the test last actually ran. Serving it from cache does not extend that window, so a test served from cache every day still re-runs after 7 days.
- CLI upgrade: an upgrade that changes the cache format or how keys are computed makes earlier results unusable, so the first run after such an upgrade runs everything again. The old files stay on disk until you clear them.
- Org mismatch: results are stored per org (by org ID) and are never reused across orgs. A different or recreated org, such as a new scratch org, starts with an empty cache.
- Explicit clear or refresh:
latdx cache cleardiscards cached results for every org in the workspace;latdx test run ... --forcere-runs the selected tests and stores fresh results. See Cache Management Commands.
The daemon starts automatically with your first latdx test run and stays up until you stop it (see Cache & Daemon Lifecycle). While it is running, after a test run with the cache on, each save of a .cls file that counts as a code change (see below), and each add or delete of one, deletes the cached results of:
- the saved class itself, if it is a test class;
- in per-class mode, every test class that reaches the saved class through static references, directly, through other classes or through the triggers that run when that code writes records, and that ran with the cache on in your latest runs (since the daemon started, or since the last save before those runs);
- every test class whose cached results an earlier save would have deleted but kept because it changed no code (see below), if it reaches the saved class. Those results stay covered across later runs until a save deletes them.
In per-method mode, test classes that reach the saved class are cleared this way only when the org has a package installed whose namespace is not your project’s own, or when LATdx could not list the org’s packages. Otherwise a save deletes only the saved test class’s own results, and the key comparison on your next run decides which other tests run again.
Test classes a save does not clear keep their cached results; the next run compares their keys like any other. The deletion happens before any key comparison, and the whole test class is affected (every method and every stored key). Further saves before your next run are handled the same way, against the same recently run test classes. Tests that reach the class only dynamically are not covered by this step; in per-method mode the key comparison on your next run re-runs them. Edits made while the daemon was not running are caught when the next run compares keys (in per-class mode, once the edit is deployed to the org; see Overview). Each test method keeps up to 5 recent results under different keys, so when the daemon has not cleared them, undoing an edit can bring back an earlier cached result.
A save that changes no Apex code and moves no code keeps the cached results. That covers rewording a comment without adding or removing lines, adding, editing or removing a comment at the end of a line, adding or removing trailing whitespace, switching line endings, and adding comments after the last line of code. Everything else counts as a code change, including adding or removing any line above code (even a blank or comment line), re-indenting code, editing a block comment that sits before code on the same line, and changing only the letter case of a name or keyword. Tests can observe line and column numbers (for example through getLineNumber() and stack traces), so a save that moves code is never treated as unchanged.
These saves always count as code changes:
- Adding or deleting a
.clsfile. Editors that save by deleting the file and writing a new one usually count as deleting it. - A class’s first save after the daemon starts, until the daemon has read that class. The daemon reads your classes in the background after a test run ends, so this mostly affects saves made before, during or just after your first run. The same applies after the daemon’s file watcher restarts following an error.
- A save the daemon cannot read, a save followed by another write before the daemon finished reading it, and a file that is not valid UTF-8 or does not tokenize as Apex (for example an unterminated string).
Before each run with the cache on, the daemon also checks the class files it knows about for changes that did not reach it as saves (for example a missed file system event) and handles each one like a save. It then waits for saves it is still checking, for up to 10 seconds (a longer wait shows as Waiting for saved class changes); any save still unchecked after that counts as a code change. Runs with --no-cache or LATDX_CACHE_DISABLE=1 skip this check.
Full save-time cleanup needs at least one run with the cache on (without --no-cache) through the daemon in that window: since it started, or since the last save before your latest runs. If every run in that window used --no-cache, saves before your next run delete only the saved class’s own results (when it is a test class) and the results described in the last item of the list above; the key comparison on your next run handles the rest. A --no-cache run made after a run with the cache on does not cancel the cleanup. The daemon reads LATDX_CACHE_DISABLE once, when it starts, and save-time cleanup follows that value for as long as it runs. If no daemon is running, the test run you set the variable on starts one. That daemon then never clears cached results on save, even for later runs without the variable, which still read and write the cache. Unsetting the variable does not change a daemon that is already running, and neither does setting it for one command. To get save-time cleanup back, run latdx daemon restart (it stops the daemon) and start your next test run without the variable set. To turn cleanup off on purpose, do the same with the variable exported.
LATDX_CACHE_RULE_POSITION_IDENTICAL_SAVE is also read once, when the daemon starts. Set to off (or 0, false, no), the daemon treats every save as a code change but deletes cached results only on the first save, add, or delete after your latest runs (further saves before your next run are caught by the key comparison instead), and it skips the check of class files before runs. To change it, run latdx daemon restart and start your next test run with the new value.
The run summary shows only ↺ hits/total. With --json, result.cacheReport.stale counts test methods that had a stored result whose key no longer matched. Results already deleted by the daemon on save count as ordinary misses.
Cache Management Commands
# Show the size of this workspace's cache folder and whether the daemon is running.
latdx cache status
latdx cache status --all-workspaces
latdx cache status --json
# Delete all LATdx caches for this workspace, including cached test results for every org.
latdx cache clear
# Clear the caches of every workspace on this machine.
latdx cache clear --all-workspaces
# Refresh cached results for specific tests: re-run them and store the fresh results.
# Name the tests with -n or -t, or point -f or -d at their files.
latdx test run -n MyTest -o my-org --force
# List the test methods that reach a class (no execution; needs Java 11+).
latdx test impact MyClasslatdx cache status does not report per-org test-result counts or ages. There is no per-org clear and no command that clears only test results: latdx cache clear deletes cached test results for every org in the current workspace, together with LATdx’s other caches for that workspace. To refresh cached results for specific tests, run them by name (-n or -t) or by file (-f or -d) with --force.
Known Limitations
Code a test reaches through names resolved at runtime (dynamic Apex) is handled differently in the two modes, and saving that code never clears the test’s cached result by itself:
- Per-method mode covers it: a test whose reached code makes one of the calls listed in Overview item 4 re-runs after any change to any local
.clsfile, unless LATdx works out the class the call names, and so does a test whose writes run a local trigger that makes such a call. A dynamic call it does not reach is not seen, for example one in a class that only a flow or other automation runs, or that only a trigger existing only in the org runs, and neither is a call marked@dynamic-ignoreor code in a managed package created by name. A test whose dynamic calls LATdx works out, or that only makes the calls that add nothing, is keyed like a test without them, so a class that only a flow or a managed package runs is not part of its key. - Per-class mode tracks only static references.
Type.forName('Foo').newInstance()touchesFooby string, so edits toFoodo not invalidate tests that reach it only this way, and the same holds for calls dispatched by name (for example through theCallableinterface on a class created withType.forName) andJSON.deserializeinto types chosen at runtime.
In both modes, any change to a tracked local object already invalidates every test, so dynamic SOQL sees the same object changes as static SOQL. In per-class mode, if a test reaches code only dynamically and that code changes, the cache can return the old pass/fail, even while the daemon is running.
Metadata looked up by name (Schema.getGlobalDescribe().get(name), System.Label.get(...), SObject.get / put by field name) is covered the same way as static references. In both modes, any change to a tracked local object or to custom labels invalidates every test.
Other inputs the key does not cover:
- Local project and org out of sync. Per-method keys are built from your local project, while tests run against the code and configuration in the org. A change made directly in the org is not seen, including a trigger that exists only in the org (per-method keys cover the triggers in your local project and the code they call), and a test run before you deploy a local edit stores the old code’s result under the new key, which is then reused after you deploy. Run with
--forceafter deploying. This applies to-fand-druns too, which run the org’s deployed copy of each class rather than your local file. In per-class mode, keys follow the Apex in the org, which LATdx checks for changes on every run, so a local Apex edit changes them only once it is deployed, while a local metadata edit changes them right away, as in per-method mode (see Overview). - Untracked metadata. Both modes cover only your object folders and the metadata types listed in Overview. Every other type is not tracked in either mode, for example queues and public groups, roles, email templates, Visualforce pages and components, escalation rules and auto-response rules. In per-method mode, object folders whose names start with a lowercase letter, including namespaced managed-package objects such as
npsp__Allocation__c, are not tracked either: changes to fields or object settings in them do not invalidate cached results, though validation rules and record types inside them still do. Per-class mode tracks every object folder. Run with--forceafter changing any of them. - Records written by automation. Both modes follow the records your Apex writes into the triggers on those objects (per-method mode: the triggers in your local project; per-class mode: the org’s active triggers), and per-method mode also follows roll-up summary recalculation and records deleted with their master record. Records written by flows, processes, workflow field updates or other declarative automation are not followed, and neither is Apex that a flow calls: a trigger only such a write runs, an invocable class a flow calls, and the classes they call are not part of the key unless the test’s own code also reaches them. In per-class mode roll-up summary recalculation is not followed either. Run with
--forceafter editing a class that only such automation runs (in per-class mode, once it is deployed). - Managed-package code reached indirectly. Tests whose statically reached code calls an installed package’s Apex as
namespace.ClassNamealways run (see What Is Not Cached). Package code a test reaches any other way is not part of the key: classes created by name at runtime, and package triggers, flows or validation rules that run when the test writes records. Upgrading a package does not invalidate results for those tests, so run with--force, or runlatdx cache clear, after a package upgrade. - Line numbers. Per-method keys ignore whitespace and comments in Apex classes, so an edit that only shifts lines in a class does not change the key for most tests. Tests whose reached code calls
getLineNumber()orgetStackTraceString()are the exception: their key also covers the exact source positions of the classes they reach, so a line shift in any of those classes re-runs them. A cached failure of any other test replays the stored message and stack trace, with the line numbers from the run that recorded it. - Hosted CI. The cache lives in the home directory of the machine that ran the tests, keyed by checkout location and org. Fresh CI runners, and fresh or different CI orgs, start with an empty cache, so CI runs normally get no hits.
Workarounds:
- Run with
--no-cachewhile iterating on dynamically dispatched code that per-method mode does not see, or in per-class mode (no cached results are read or written). - After such a change, re-run the tests that depend on it once with
--forceto refresh their cached results: name them (latdx test run -n MyTest -o my-org --force) or run the full suite with--force.--affecteddoes not select tests that reach code only by name. Or runlatdx cache clearto drop all cached results for the workspace.
If a cached result disagrees with a fresh run (for example, --force gives a different outcome), open an issue at github.com/nebulity/latdx-cli/issues with the test name and the change you made. Each confirmed new stale-result case is added to LATdx’s regression tests for the cache.
Troubleshooting
- Unexpected miss. Common causes: an edit to any object or tracked metadata file (this re-runs every test), a save the daemon saw (when the save changes or moves code, even by adding a blank or comment line above it, it clears the saved test class and, in per-class mode, recently run test classes that reach the saved class; a class’s first save after the daemon starts can do so too), results older than 7 days, a CLI upgrade, a different org,
@IsTest(SeeAllData=true), code that reaches an installed package’s Apex (see What Is Not Cached), or, in per-class mode, an edit in the org to a class the test reaches or to a trigger on an object it writes (or a class that trigger calls), a trigger added to or removed from such an object, a trigger LATdx cannot read while the test writes records, a failed check of the org’s classes (the run warnsCould not check org classes for changes; cached results are not used), local metadata the daemon cannot read (the run warnsCould not read workspace metadata; cached results are not used) or a run without the daemon. In per-method mode, an edit to a class that a trigger on the records the test writes runs (or, when LATdx cannot tell which object one of the test’s writes touches, a class any trigger runs) also changes the key. Adding a new dependency to the code a test reaches also changes its key, so the test runs again. In per-method mode, a test whose reached code makes a dynamic Apex call LATdx cannot pin down (for exampleType.forNamewith a class name read from a record, including a call in a trigger that runs on the records the test writes) re-runs after any edit to a local.clsfile; see Overview item 4. Uselatdx test impact <ClassName>to see which tests reach a class. - Unexpected hit after editing code. The edit probably did not change the key. This happens with a whitespace-only or comment-only edit, code reached only dynamically (in per-class mode, or in per-method mode through a call LATdx does not see), metadata types LATdx does not track, a managed-package upgrade that changes code the test reaches without a static
namespace.ClassNamereference, or, in per-class mode, a local Apex edit you have not deployed yet (per-class keys follow the org’s Apex; see Overview). Re-run with--forceto refresh, or--no-cacheto bypass.latdx daemon statusshows whether the daemon is running. The daemon only sees saves made while it is running, and edits it missed are still caught on the next run by comparing keys (see Invalidation). - Disk growth.
latdx cache statusreports the size of the workspace cache, andlatdx cache clearempties it. Each test method keeps at most 5 stored results; expired ones stay on disk until you clear the cache.