Skip to content

check-cross-package-test-inputs' literal collector cannot see an escaping relative IMPORT specifier, so a test that imports a module outside its package goes undeclared silently #10452

Description

@os-zhuang

Filed unassigned from the #9863 dev seat (session session_01DdCnBGcHeufjrq7drTD3wt, PR #10450). Measured, not reasoned: the gate passed green over the read, and the declaration in that PR was added by hand.

The measurement

packages/cli/src/commands/serve-audit-registration.contract.test.ts (new in PR #10450) contains:

import{maskComments}from'../../../../scripts/js-comment-mask.mjs';

That specifier climbs out of packages/cli and lands in scripts/. It is a real input: the test's verdict is a function of that module's masking behaviour. With no roster entry for it, pnpm check:cross-package-test-inputs reported:

OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.

The consequence is the exact failure the gate exists to close, quoted from its own text: "a test whose real inputs are wider than its package is invisible to BOTH the affected-subset filter and the turbo cache, so it can go red on main while every PR reports green." An edit to js-comment-mask.mjs would not have re-run cli's suite.

Why the collector misses it

The gate is a deliberate source scan and publishes the spellings it recognises — all of them are path-shaped file reads seeded from import.meta.url/__dirname:

const P = resolve(HERE, '<rel>');
const P = fileURLToPath(new URL('<rel>', import.meta.url));
readFileSync(resolve(HERE, '<rel>'))
…

An ES module specifier is none of those. It is a bare string in import position that the module resolver — not node:path — turns into a file. So the escape is invisible, and per the gate's own framing that means no declaration, silently.

Not the same as the neighbours

Worth noting that check-test-source-alias.mjs already resolves test import specifiers for its own purpose, so the parsing work may not need to be written from scratch.

Suggested shape, and the trap in it

Extend the collector to recognise static import/export … from specifiers (and await import('…')) that begin with ../ and resolve outside the package root, judged on the shallowest point reached, exactly as the path forms already are. Add a --self-test case per spelling in the same edit — the gate's own instruction, and the reason its recognised list is published rather than private.

⚠️ The trap: a bare package specifier (@objectstack/verify) must NOT be collected. Those are declared dependencies resolved through node_modules, which the gate already excludes by design — "an installed dependency is not a repo source input, and no turbo glob can name it." Only relative specifiers that escape the package are in scope. Getting that boundary wrong would put every package's test suite on every workspace sibling.

Interim state

PR #10450 declares scripts/js-comment-mask.mjs for @objectstack/cli in CROSS_PACKAGE_TEST_INPUTS and adds the matching $TURBO_ROOT$ input to @objectstack/cli#test, with a comment recording that the gate did not demand it. That closes the one instance; it does not close the class, and any future test importing across the boundary is silent again.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions