Files
redefined-designs/backend/eslint.config.mjs
T
bermudalambandClaude Opus 5 0b6cc85c4f
Linting / lint (pull_request) Successful in 2m55s
SonarQube Analysis / sonarqube (pull_request) Successful in 30m43s
fix(lint): bring the backend test suites into scope (#298)
The backend lint script covered src and scripts; the frontend's has always covered src and tests. So roughly sixty backend test files had never been linted at all.

That was a documented deferral rather than an oversight — the config said so in as many words, because tsconfig.json includes only src and type-aware rules had no program to resolve the test files against. tsconfig.test.json is that program, exactly as frontend/tsconfig.test.json was for the same problem in #137. It is separate from tsconfig.json rather than a widening of it, because that one drives the build and emits to dist, and pulling the suite in would ship the tests. The files were already type-checked at run time by ts-jest; this adds nothing to that, only to what the linter can see.

Pointing it at tests produced 77 warnings and no errors. Sixty of those were rules that cannot be true in a test, so they are switched off here rather than left to accumulate — #60's argument, that a gate nobody reads is not a gate, and that a rule which cannot be true is noise hiding the rules that can. Forty-one alone were hardcoded passwords, which are the entire point of a test and which this project's own rule says must live only in test paths, which is here. The rest were a stub server on http to a socket the test opened itself, an RFC 5737 documentation IP, os.tmpdir, Math.random for a run id, and sorting two arrays to compare them.

What was left was signal, and it found a real one on the first run. testDb.ts cleaned up settings with LIKE 'email\_%', and in a JavaScript string that backslash does nothing: the pattern is 'email_%', and an underscore in SQL LIKE matches any single character. It meant "email plus any one character" rather than "email_". It deleted the right rows only because no other key begins with those letters followed by something else — a setting called emailing_enabled would have been swept away between suites, silently, in a file that never mentions it. It now uses an explicit ESCAPE clause.

It also found five dead `const before: string[] = []` declarations in uploadValidation, left over from #228's redesign of that suite. The tests assert properly through filesSettlingTo; the variables did nothing.

Seven warnings remain, all in routesAreWrapped and workflowGate, and all judgement calls about guard-test complexity rather than defects. Leaving them visible is the point of having lint here at all.

Closes #298

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 12:41:58 -05:00

141 lines
5.8 KiB
JavaScript

import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import sonarjs from 'eslint-plugin-sonarjs';
import globals from 'globals';
// Named `.mjs` because this package is CommonJS — `eslint.config.js` would be
// parsed as CJS and the imports above would fail.
//
// Policy: every preset is downgraded to advisory, and the rules that actually
// fail the build are listed once at the bottom. That way the CI gate is
// readable in one place rather than inferred from four presets' defaults.
// The reasoning behind the split, and the measurements it rests on, are in
// docs/superpowers/specs/2026-08-19-eslint-design.md.
/**
* Rewrites a preset's enabled rules to `warn`, preserving each rule's options.
* Rules the preset explicitly turned off stay off — a preset that disables a
* rule means it, and flipping those to `warn` turns the whole of SonarJS's
* opt-in catalogue (file headers, naming conventions) into daily noise.
*/
const advisory = (config) => ({
...config,
rules: Object.fromEntries(
Object.entries(config.rules ?? {}).map(([rule, level]) => {
const severity = Array.isArray(level) ? level[0] : level;
if (severity === 'off' || severity === 0) return [rule, level];
return [rule, Array.isArray(level) ? ['warn', ...level.slice(1)] : 'warn'];
})
),
});
export default tseslint.config(
// src/db-drizzle/schema.ts and relations.ts are `drizzle-kit pull` output, not
// written by anyone here. #261 hand-fixed an unused-parameter warning in the
// schema and #217's re-pull put it straight back, which is the whole argument:
// linting generated code buys a fix that the next regeneration undoes. The
// hand-written files in that directory are still linted.
{
ignores: [
'dist/**',
'coverage/**',
'eslint.config.mjs',
'src/db-drizzle/schema.ts',
'src/db-drizzle/relations.ts'
]
},
...[js.configs.recommended, ...tseslint.configs.recommended, sonarjs.configs.recommended].map(
advisory
),
{
files: ['src/**/*.ts'],
languageOptions: {
globals: globals.node,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
// The two rules this repo has actually been bitten by. #59 is the whole
// argument: an async handler whose rejection nothing forwards produces no
// response at all, and the request hangs rather than failing visibly.
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': [
'error',
{ checksVoidReturn: { attributes: false } },
],
},
},
{
// The test suites, in scope since #298. They had never been linted at all:
// this config said tests were out of scope because tsconfig.json includes
// only `src`, and that stayed true for long enough that two defects lived
// here undetected — a unit test that opened a real TLS connection to Gmail
// on every run, and integration tests that mocked the shared pg pool and
// made a suite unrunnable. Neither is something lint would necessarily have
// caught, but neither was ever looked at.
//
// `project` rather than `projectService`, for the reason the frontend's
// equivalent block records: the service resolves each file to the nearest
// tsconfig.json, which for tests/ is the one that excludes them, and every
// file then errors as not part of a project.
files: ['tests/**/*.ts'],
languageOptions: {
globals: { ...globals.node, ...globals.jest },
parserOptions: {
project: ['./tsconfig.test.json'],
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
// The same rule src is held to, and it matters at least as much here.
// An unawaited promise in a test does not fail the test — it passes,
// having asserted nothing, and the failure surfaces later as a suite that
// will not exit.
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': [
'error',
{ checksVoidReturn: { attributes: false } },
],
// Everything below is switched off for tests rather than left as a
// warning, on #60's argument: bringing these files in scope produced 77
// warnings, of which 60 were rules that cannot be true in a test. A rule
// that cannot be true here is noise, and noise hides the rules that can.
// What is left is signal — unused variables, useless escapes, a regex
// worth a second look.
// 41 of the 77. Test credentials are the entire point of a test, and this
// project's own rule is that they must live only in test paths — which is
// here. Flagging them where they belong trains a reader to skip the rule
// where they do not.
'sonarjs/no-hardcoded-passwords': 'off',
// Stub servers and fixtures: `http://127.0.0.1:<port>`. There is no
// transport to secure between a test and a socket it opened itself.
'sonarjs/no-clear-text-protocols': 'off',
// 203.0.113.5 is TEST-NET-3, reserved by RFC 5737 for exactly this. A
// documentation address is the correct thing to hardcode.
'sonarjs/no-hardcoded-ip': 'off',
// `os.tmpdir()`, via mkdtemp, which is how these suites get a scratch
// uploads directory they can delete afterwards.
'sonarjs/publicly-writable-directories': 'off',
// Math.random for a run id. Nothing here is a secret; it only has to not
// collide with a parallel worker.
'sonarjs/pseudo-random': 'off',
// Sorting two string arrays to compare them is how several guards assert
// set equality. The locale-aware comparator the rule wants would change
// nothing except the reading.
'sonarjs/no-alphabetical-sort': 'off',
},
}
);