Enforce the one join path in lint; a drive that asserts nothing fails #64

Merged
void merged 1 commit from void/navigator-s8-lows into dev 2026-11-10 23:16:59 +00:00
4 changed files with 94 additions and 2 deletions

View file

@ -1,3 +1,8 @@
// Airbnb's own bans, kept: a `no-restricted-syntax` entry below replaces the whole list.
const airbnbRestrictedSyntax = require('eslint-config-airbnb-base/rules/style').rules[
'no-restricted-syntax'
];
module.exports = {
env: {
browser: true,
@ -62,6 +67,21 @@ module.exports = {
"@typescript-eslint/no-shadow": "error"
},
overrides: [
{
// The one join path: a join is `joinEstateRoom`, which files the DM before it sends the join.
files: ['src/**/*.ts', 'src/**/*.tsx'],
excludedFiles: ['src/app/features/estate/joinEstateRoom.ts', 'src/**/*.test.*'],
rules: {
'no-restricted-syntax': [
...airbnbRestrictedSyntax,
{
selector: "CallExpression[callee.property.name='joinRoom']",
message:
'Join a room through joinEstateRoom (features/estate/joinEstateRoom.ts), the one join path.',
},
],
},
},
{
files: ['*.ts'],
rules: {

View file

@ -2,12 +2,17 @@
// non-zero when the page or the fake homeserver's log says otherwise.
const fs = require('fs');
const fails = [];
let asserted = 0;
exports.expect = (label, ok, detail = '') => {
asserted += 1;
console.log(`[ASSERT] ${ok ? 'PASS' : 'FAIL'} ${label}${ok ? '' : ` :: ${detail}`}`);
if (!ok) fails.push(label);
};
/** Exit code for the drive: 4 when any assertion failed. */
exports.done = () => (fails.length ? 4 : 0);
/** Exit code for the drive: 4 when any assertion failed, 5 when none ran (a drive that asserts nothing proves nothing). */
exports.done = () => {
if (fails.length) return 4;
return asserted === 0 ? 5 : 0;
};
/** The fake homeserver's request log (HSLOG is shared with the server process). */
exports.hsLog = () => fs.readFileSync(process.env.HSLOG, 'utf8').split('\n');
/** Bodies of the account-data PUTs for m.direct, in order. */

View file

@ -0,0 +1,35 @@
// @vitest-environment node
import { createRequire } from 'module';
import { describe, expect, it, vi } from 'vitest';
// A drive that asserts nothing proves nothing: its exit code must say so.
const fresh = () => {
const require = createRequire(import.meta.url);
const path = require.resolve('./check.cjs');
delete require.cache[path];
return require('./check.cjs') as {
expect: (label: string, ok: boolean, detail?: string) => void;
done: () => number;
};
};
describe('estate drive exit code', () => {
it('is 5 when no assertion ran', () => {
expect(fresh().done()).toBe(5);
});
it('is 0 when assertions ran and all held', () => {
vi.spyOn(console, 'log').mockImplementation(() => undefined);
const c = fresh();
c.expect('holds', true);
expect(c.done()).toBe(0);
});
it('is 4 when any assertion failed', () => {
vi.spyOn(console, 'log').mockImplementation(() => undefined);
const c = fresh();
c.expect('holds', true);
c.expect('breaks', false, 'detail');
expect(c.done()).toBe(4);
});
});

View file

@ -0,0 +1,32 @@
// @vitest-environment node
import { describe, expect, it } from 'vitest';
import { ESLint } from 'eslint';
// The product rule is one join path: a join is joinEstateRoom, which files the DM before it sends the join.
// This pins that the lint config enforces it, not just that the code happens to comply today.
const lint = async (filePath: string, code: string) => {
const eslint = new ESLint({ cwd: process.cwd(), useEslintrc: true });
const [result] = await eslint.lintText(code, { filePath });
return result.messages.filter((m) => m.ruleId === 'no-restricted-syntax');
};
const JOIN = 'export const f = (mx: { joinRoom: (id: string) => void }) => mx.joinRoom("!a:b");\n';
describe('the one join path is enforced by lint', () => {
it('rejects a joinRoom call anywhere but joinEstateRoom.ts', async () => {
const found = await lint('src/app/features/estate/EstateNavigator.tsx', JOIN);
expect(found).toHaveLength(1);
expect(found[0].message).toContain('joinEstateRoom');
});
it('allows the call inside joinEstateRoom.ts itself', async () => {
expect(await lint('src/app/features/estate/joinEstateRoom.ts', JOIN)).toHaveLength(0);
});
it('still applies the Airbnb syntax bans in the same files', async () => {
const found = await lint(
'src/app/foo.ts',
'export const g = (o: object) => { for (const k in o) { return k; } return 0; };\n'
);
expect(found.some((m) => m.message.includes('for..in'))).toBe(true);
});
});