Route tests through a #testing alias, ban ../ imports, and document the alias ownership rule
Check / check (pull_request) Successful in 46s
Check / check (pull_request) Successful in 46s
This commit is contained in:
@@ -17,6 +17,9 @@ as skipped instead of silently passing.
|
||||
`codex`/`claude`/`npx` binaries and a fake Gitea through
|
||||
`protect-source.sh` -> `prepare.ts` -> `run.ts` -> `post.ts`; repeat that
|
||||
for changes to the phase flow ([[bot-agents-runtime-no-build]]).
|
||||
- `src/testing.ts` holds the shared test helpers; tests follow
|
||||
- `src/testing.ts` holds the shared test helpers and is the `#testing` file
|
||||
alias. Every test imports it as `#testing`, never `./testing.ts`, and it
|
||||
re-exports the source helpers `tests/` needs, since `tests/` may not use
|
||||
`../`. Tests follow
|
||||
[[tests-layout-vitest]] (unit tests in `src/*.test.ts`, repo-level ones in
|
||||
`tests/`).
|
||||
|
||||
@@ -1,75 +1,85 @@
|
||||
---
|
||||
description: Shared code comes through # aliases in package.json imports, each src/ subfolder's entry is index.ts, barrels keep imports few; never ../ out of a directory
|
||||
description: An alias owns its target (folder index.ts = folder and below, any other file = that file); no overlap; outside an area only the exact alias reaches it; ../ is banned in every file; no folder names in the rule
|
||||
---
|
||||
|
||||
## `#` aliases
|
||||
## `#` aliases own their targets
|
||||
|
||||
Shared code is imported through Node subpath imports declared in
|
||||
`package.json` `imports`, never by a relative path that climbs out of a
|
||||
directory.
|
||||
`package.json` `imports`. Node, TypeScript, Vite, Vite+'s config bundler, and
|
||||
Oxlint's plugin loader all resolve them natively, so `.ts` source runs
|
||||
unbuilt. `vp pack` inlines them, so they never reach package consumers.
|
||||
|
||||
**Targets (user rule, 2026-10-16):** an alias may point ONLY at a direct
|
||||
subfolder of `src/`: its `index.ts` (`"#helpers": "./src/helpers/index.ts"`)
|
||||
or a `#x/*` pattern inside that one folder. Never alias anything outside
|
||||
`src/` (`scripts/`, root files), `src/` itself (no `#plugin` for
|
||||
`./src/index.ts`), or a deeper folder (`./src/a/b/`). Code that would need
|
||||
such an alias uses a relative import instead; `scripts/` and
|
||||
`vite.config.ts` import each other relatively. Layer rules
|
||||
(`enforce-layer-order`) must still pass, so reorder entries if needed.
|
||||
One rule for every file, never tied to folder or file names (user rule
|
||||
2026-10-17; it replaces the earlier "aliases only point at direct `src/`
|
||||
subfolders" rule and the "import-check checks only aliased folders" scope):
|
||||
|
||||
- **An alias may point anywhere** and owns its target. An alias to a
|
||||
folder's `index.ts` owns that folder and everything below it; an alias to
|
||||
any other file owns only that file.
|
||||
- **No overlap.** No file may be owned by 2 aliases: no alias inside another
|
||||
alias's area, and no 2 aliases with the same target. import-check's
|
||||
`enforce-layer-order` reports it once at the top of every file 2 aliases
|
||||
claim. Fix by merging the aliases or moving files apart.
|
||||
- **Outside an area, only the exact alias reaches it.** A deeper alias
|
||||
specifier (`#a/one.ts`, `no-barrel-bypass`) and a relative path into the
|
||||
area (`./a/one.ts`, `no-relative-bypass`; `./b/f.ts` to a file alias,
|
||||
`prefer-alias-import`, fixed to the alias) are bypasses.
|
||||
- **Inside an area, relative imports of siblings and subfolders are free.**
|
||||
- **`../` is banned in every file**, owned or not (`no-parent-import`).
|
||||
Anything above a file needs an alias.
|
||||
- **Files no alias owns** need no `index.ts` and no alias, and import `./`
|
||||
siblings and subfolders freely.
|
||||
- Unchanged: no `/*` subpath patterns, the key order of `imports` is the
|
||||
layer order `enforce-layer-order` checks (add a new alias at the right
|
||||
layer), and entry files are `index.ts` only.
|
||||
|
||||
```jsonc
|
||||
"imports": {
|
||||
"#helpers": "./src/helpers/index.ts",
|
||||
"#components/*": "./src/components/*"
|
||||
"#a": "./a/index.ts", // owns a/**
|
||||
"#f": "./b/f.ts", // owns only b/f.ts
|
||||
"#a-sub": "./a/sub/index.ts", // no: inside #a
|
||||
"#a2": "./a/index.ts" // no: same target as #a
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
import { typeNameTail } from "#helpers"; // yes
|
||||
import { typeNameTail } from "../helpers/ast.ts"; // no: import/no-relative-parent-imports
|
||||
// in main.ts, which no alias owns
|
||||
import { a } from "#a"; // yes
|
||||
import { one } from "#a/one.ts"; // no: deeper alias specifier
|
||||
import { one } from "./a/one.ts"; // no: relative path into #a
|
||||
import { f } from "#f"; // yes
|
||||
import { f } from "./b/f.ts"; // no: relative path to a file alias
|
||||
import { g } from "./b/g.ts"; // yes: unowned
|
||||
// in a/one.ts, owned by #a
|
||||
import { two } from "./two.ts"; // yes: sibling
|
||||
import { deep } from "./sub/deep.ts"; // yes: subfolder of the same area
|
||||
import { x } from "../x.ts"; // no: no ../ anywhere
|
||||
```
|
||||
|
||||
Node, TypeScript, Vite, Vite+'s config bundler, and Oxlint's plugin loader
|
||||
all resolve them natively, so `.ts` source runs unbuilt. `vp pack` inlines them,
|
||||
so they never reach package consumers.
|
||||
|
||||
- Deno's trailing-slash mappings (`"#components/"`) don't exist in Node; they
|
||||
become patterns, `"#components/*": "./src/components/*"`.
|
||||
- Aliases point at `.ts` files only. There is no `#package` alias
|
||||
([[ts-only-files-and-imports]]).
|
||||
- In repos linted by import-check, the key order of `imports` is the layer
|
||||
order `enforce-layer-order` checks; add a new alias at the right layer.
|
||||
- Lint `jsPlugins` entries in `vite.config.ts` stay path strings, even when
|
||||
the file has an alias.
|
||||
- Keep test-only helpers in their own alias (e.g. `#testing`) so they never
|
||||
land in a bundle.
|
||||
|
||||
## `index.ts` entries and barrels
|
||||
## Barrels
|
||||
|
||||
- Every `src/` subfolder's entry file is `index.ts`, never `mod.ts` (decided
|
||||
2026-10-12; import-check's `enforce-mod-file` requires it only in folders a
|
||||
`#` alias owns). Folders outside `src/`, such as `scripts/`, have none.
|
||||
- A directory used from outside exposes its API through its `index.ts`
|
||||
barrel, and callers import the barrel (`#ui`), not files inside it
|
||||
(`no-barrel-bypass`).
|
||||
- An area used from outside exposes its API through its alias target, and
|
||||
callers import the alias (`#ui`), not files inside it.
|
||||
- Barrels are also the standard fix for `import/max-dependencies`: one barrel
|
||||
import replaces many direct ones ([[lint-overrides-policy]]).
|
||||
- `import/no-cycle` is on; a barrel must not create a cycle.
|
||||
|
||||
Relative imports inside one directory (`./sibling.ts`) are fine, with the
|
||||
`.ts` extension written out.
|
||||
Relative imports are written with the `.ts` extension.
|
||||
|
||||
## `scripts/` is flat (user rule, 2026-10-16)
|
||||
|
||||
- `scripts/` has no subfolders and no `index.ts`. Barrels and `index.ts`
|
||||
entries exist only inside `src/`; delete any `index.ts` outside it.
|
||||
- `scripts/` has no subfolders and no `index.ts`.
|
||||
- Each tool is one file, `scripts/<name>.ts`, imported directly by file:
|
||||
`import { decorators } from "./scripts/decorators.ts";` in
|
||||
`vite.config.ts`.
|
||||
`vite.config.ts`. No alias owns `scripts/`, so these `./` imports are free.
|
||||
- Code in a former `scripts/<tool>/` folder is merged or renamed into flat
|
||||
`scripts/<name>.ts` files. Non-TS assets (HTML, images, fixtures) move to
|
||||
where they belong (next to the tool's output, `.agents/`, or a gitignored
|
||||
scratch dir). A subfolder that is really an app moves under `src/` as an
|
||||
aliased folder.
|
||||
- import-check 0.10.0+ checks folder layout only where the importer or the
|
||||
target sits in a `#`-aliased folder, so `vite.config.ts` →
|
||||
`./scripts/x.ts` passes without any override. Remove overrides that only
|
||||
dodged import-check.
|
||||
scratch dir). A subfolder that is really an app moves under an alias.
|
||||
|
||||
@@ -11,9 +11,7 @@ practice.
|
||||
off for `src/rules/index.ts`, the rule registry, which must import every
|
||||
rule. It is not a precedent for apps.
|
||||
- **`scripts/**`:** a narrow override is allowed when a tooling script truly
|
||||
needs it, e.g. `no-console` in a CLI script. Never one that only dodges
|
||||
import-check (e.g. `no-relative-bypass` for `vite.config.ts`); import-check
|
||||
ignores folders no `#` alias owns.
|
||||
needs it, e.g. `no-console` in a CLI script.
|
||||
- **Generated files:** list them in `ignorePatterns`, never `overrides`.
|
||||
- **Anything else needs the user's approval first.** When the user is away,
|
||||
don't add it: refactor, or leave the violation failing and log the question.
|
||||
@@ -45,3 +43,8 @@ pedantic hits in 274 files, of which `max-lines-per-function` 206,
|
||||
`max-lines` 145, `max-classes-per-file` 59, `max-dependencies` 45 (median 14
|
||||
imports, worst 29). The user chose refactoring over raising limits or
|
||||
matching memona's old hugoalh limits (2048 lines per file, no function limit).
|
||||
|
||||
Alias rule: an alias may point anywhere and owns its target (a folder's
|
||||
`index.ts` owns the folder and below, any other file owns only itself); no
|
||||
two aliases may own the same file, and `../` is banned everywhere
|
||||
([[imports-aliases-barrels]]).
|
||||
|
||||
@@ -30,3 +30,9 @@ The `imports` key order can carry meaning: import-check's
|
||||
|
||||
Refresh the lockfile with `npm install` (or `npm install
|
||||
--package-lock-only` for a version bump) and commit it with the change.
|
||||
|
||||
Alias rule: an alias may point anywhere and owns its target (a folder's
|
||||
`index.ts` owns the folder and everything below it; any other file owns only
|
||||
itself). No two aliases may own the same file, outside an area only the exact
|
||||
alias reaches it, and `../` is banned in every file
|
||||
([[imports-aliases-barrels]]).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: Vitest via vp test; unit tests sit next to code as *.test.ts, regression/integration tests in tests/ via the public entry; scripts/ has no tests
|
||||
description: Vitest via vp test; unit tests sit next to code as *.test.ts, regression/integration tests in tests/ via the public entry (through its # alias, never ../); scripts/ has no tests
|
||||
---
|
||||
|
||||
Decided 2026-10-14 for all repos.
|
||||
@@ -11,9 +11,12 @@ Decided 2026-10-14 for all repos.
|
||||
config needed. Packages publish only `dist/`, so colocated tests cost nothing
|
||||
in the tarball.
|
||||
- **Regression and integration tests** live in the top-level `tests/` folder
|
||||
and use only the public entry point through a relative import
|
||||
(`../src/index.ts`; no alias may target `src/` itself, see
|
||||
[[imports-aliases-barrels]]), never internals. `vp test tests` runs just these.
|
||||
and use only the public entry point, imported through its `#` alias (e.g.
|
||||
`#plugin` for `./src/plugin/index.ts`), never internals and never `../`,
|
||||
which is banned in every file. `vp test tests` runs just these
|
||||
([[imports-aliases-barrels]]). In bot-agents, `tests/` imports only
|
||||
`#testing` (`./src/testing.ts`), which re-exports the few source helpers
|
||||
those tests need.
|
||||
- **No in-source tests** (`import.meta.vitest`).
|
||||
- **`scripts/` gets no tests** (2026-10-15): it is repo tooling, not
|
||||
production code (`project-lint.ts`, `decorators.ts`, `lit-check.ts`,
|
||||
|
||||
@@ -19,10 +19,14 @@ dependency. Keep the sources to erasable syntax and `.ts` imports, which
|
||||
type-checks, and `vp test` runs Vitest. Unit tests sit next to the code as
|
||||
`src/*.test.ts`; repository-level tests live in `tests/`.
|
||||
|
||||
`src/` is flat, so `package.json` has no `imports` aliases and modules, tests,
|
||||
and `scripts/` import each other relatively. If one is ever added, it may point
|
||||
only at a direct subfolder of `src/` (its `index.ts`, or a `#x/*` pattern
|
||||
inside it); never at `src/` itself, a deeper folder, or anything outside `src/`.
|
||||
An alias in `package.json` `imports` owns its target: a folder's `index.ts`
|
||||
owns that folder and everything below it, any other file owns only that file.
|
||||
No file may be owned by 2 aliases. Outside an area, only the exact alias
|
||||
reaches it (no deeper `#` specifier, no relative path); inside it, relative
|
||||
imports of siblings and subfolders are free. `../` is banned in every file
|
||||
(`import/no-relative-parent-imports`). The one alias is `#testing`
|
||||
(`./src/testing.ts`): every test imports it by alias, and `tests/` reaches
|
||||
`src/` only through it. Other `src/` files import `./` siblings freely.
|
||||
`scripts/` is flat: one file per tool, no subfolders and no `index.ts`
|
||||
(barrels live only in `src/`), imported directly by file path.
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
"private": true,
|
||||
"description": "A Gitea composite action that runs a coding agent for approved issue and pull request automation.",
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#testing": "./src/testing.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@types/node": "^24.19.2",
|
||||
|
||||
@@ -203,6 +203,8 @@ const rules: DummyRuleMap = {
|
||||
],
|
||||
// The explicit-types template's extras.
|
||||
"import/no-cycle": "error",
|
||||
// `../` is banned in every file; anything above a file needs a `#` alias.
|
||||
"import/no-relative-parent-imports": "error",
|
||||
"typescript/consistent-type-imports": "error",
|
||||
"typescript/no-import-type-side-effects": "error",
|
||||
"typescript/no-non-null-assertion": "error",
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import {
|
||||
retryInvalidToken,
|
||||
stageCodexAuth,
|
||||
} from "./auth.ts";
|
||||
import { currentGid, currentUid, IS_WINDOWS, makeTempDir } from "./testing.ts";
|
||||
import { currentGid, currentUid, IS_WINDOWS, makeTempDir } from "#testing";
|
||||
|
||||
test("retries once after an invalid refresh token", async () => {
|
||||
let attempts = 0;
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
readProfileFile,
|
||||
} from "./isolation.ts";
|
||||
import type { RuntimeProfile } from "./runtime.ts";
|
||||
import { currentUid, IS_ROOT, IS_WINDOWS, makeTempDir } from "./testing.ts";
|
||||
import { currentUid, IS_ROOT, IS_WINDOWS, makeTempDir } from "#testing";
|
||||
|
||||
const profile = (uid: number, home: string): RuntimeProfile => ({
|
||||
index: uid - 20_000,
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { isNotFound } from "./errors.ts";
|
||||
import { postRuntime } from "./post.ts";
|
||||
import type { PostContext } from "./post.ts";
|
||||
import type { RuntimeManifest, RuntimeOutcome } from "./runtime.ts";
|
||||
import { currentGid, currentUid, IS_WINDOWS, makeTempDir } from "./testing.ts";
|
||||
import { currentGid, currentUid, IS_WINDOWS, makeTempDir } from "#testing";
|
||||
|
||||
const context: PostContext = {
|
||||
apiUrl: "https://gitea.example/api/v1",
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import { assert, expect, test } from "vite-plus/test";
|
||||
import { output } from "./command.ts";
|
||||
import { prepareRuntime } from "./runtime.ts";
|
||||
import type { RuntimeProfile } from "./runtime.ts";
|
||||
import { IS_ROOT, makeTempDir } from "./testing.ts";
|
||||
import { IS_ROOT, makeTempDir } from "#testing";
|
||||
|
||||
const MASTER_KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8";
|
||||
const ENCRYPTED = "v1:ICEiIyQlJicoKSor:oV_FAgnsN3p1FyegJxwGVdyjfyPio6qDeSUwbA";
|
||||
|
||||
+8
-1
@@ -1,4 +1,6 @@
|
||||
// Test support: the current user's IDs and a fresh temporary directory.
|
||||
// Test support: the current user's IDs and a fresh temporary directory, plus
|
||||
// the source helpers that the repository-level tests in tests/ need. Those
|
||||
// tests sit outside src/ and reach it only through the `#testing` alias.
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -21,3 +23,8 @@ export const IS_WINDOWS: boolean = process.platform === "win32";
|
||||
export function makeTempDir(): Promise<string> {
|
||||
return mkdtemp(join(tmpdir(), "bot-agents-test-"));
|
||||
}
|
||||
|
||||
export { output } from "./command.ts";
|
||||
export { isNotFound } from "./errors.ts";
|
||||
export { isMapping } from "./json.ts";
|
||||
export type { JsonObject } from "./json.ts";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
|
||||
|
||||
import { expect, test } from "vite-plus/test";
|
||||
|
||||
import { output } from "../src/command.ts";
|
||||
import { output } from "#testing";
|
||||
|
||||
// Each phase runs as `node src/<phase>.ts` with no build step. Node loads the
|
||||
// whole import graph, stripping types, before the phase reads its first
|
||||
|
||||
@@ -4,11 +4,8 @@ import process from "node:process";
|
||||
import { assert, expect, test } from "vite-plus/test";
|
||||
import { parse } from "yaml";
|
||||
|
||||
import { output } from "../src/command.ts";
|
||||
import { isNotFound } from "../src/errors.ts";
|
||||
import { isMapping } from "../src/json.ts";
|
||||
import type { JsonObject } from "../src/json.ts";
|
||||
import { makeTempDir } from "../src/testing.ts";
|
||||
import { isMapping, isNotFound, makeTempDir, output } from "#testing";
|
||||
import type { JsonObject } from "#testing";
|
||||
|
||||
const mapping = (value: unknown, at: string): JsonObject => {
|
||||
assert(isMapping(value), at);
|
||||
|
||||
Reference in New Issue
Block a user