mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
b2d9b25bb6
Four small things, and one that was not this branch's fault but is now its neighbour. The driving limits saved on every keystroke. Typing "180" sent three settings writes carrying 1, 18 and 180, unordered and concurrent over HTTP/2, so whichever the server committed last won: the number that decides every over-budget warning could quietly end up a tenth of what was typed, and only show it after the next reload. Typed locally now, committed on blur or Enter, and a value that did not actually change writes nothing. The empty-result message always blamed the name box. Four controls narrow that list and only one of them is the name, so picking a section with no hits produced `Nothing on the way matches ""` — empty quotes naming a filter the reader never set. The count line above it was gated the same way, so the section case lost its "x of y" as well. Both follow whatever actually narrowed the list. The check-out dropdown printed a bare ISO date, the only place in the planner that did; it goes through the same helper as the rail beside it. Switching an addon off left open MCP sessions holding its write tools. A `when:` gate runs once per session, at attach time, so the tools stay registered and callable while the REST half answers 404 for the same user in the same moment — and the list that decides whether to tear those sessions down had drifted: airtrail and collections gate tools and were missing from it, which this addon would have inherited. The list now sits beside ADDON_IDS and a parity test reads the gates out of the source and holds it there. Read out of the source rather than out of a runtime registry on purpose: a runtime set only holds the gates whose modules the caller happened to import, so it would pass in a suite that imports none — which is the shape of test that hid this in the first place. RoadtripLimitsCard gets the test it never had, including the one case that matters: three keystrokes are not three writes.
83 lines
3.6 KiB
TypeScript
83 lines
3.6 KiB
TypeScript
/**
|
|
* MCP-GATE-PARITY-001..002 — the list of MCP-gated addons against the gates.
|
|
*
|
|
* `MCP_GATED_ADDON_IDS` decides whether switching an addon off tears down the
|
|
* live MCP sessions. A `when:` gate is evaluated once per session, at attach
|
|
* time, so a client that connected while the addon was on keeps its tools
|
|
* registered and callable afterwards — while the REST half answers 404 for the
|
|
* same user in the same moment. An addon missing from the list is that hole.
|
|
*
|
|
* It was a hand-kept list and it had drifted: airtrail and collections gate
|
|
* tools and were not on it. Read out of the source rather than out of the
|
|
* module registry on purpose — a runtime set would only contain the gates whose
|
|
* modules the caller happened to import, so it would pass in a suite that
|
|
* imports none, which is exactly the shape of test that hid this.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { readdirSync, readFileSync, statSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { ADDON_IDS, MCP_GATED_ADDON_IDS } from '../../../src/addons';
|
|
|
|
const SRC = join(__dirname, '../../../src');
|
|
|
|
function walk(dir: string): string[] {
|
|
return readdirSync(dir).flatMap((entry) => {
|
|
const full = join(dir, entry);
|
|
if (statSync(full).isDirectory()) return walk(full);
|
|
return full.endsWith('.ts') ? [full] : [];
|
|
});
|
|
}
|
|
|
|
/** Every addon id named by an addonGate(...) or collabFeatureGate(...) call. */
|
|
function gatedInSource(): { ids: Set<string>; where: Map<string, string[]> } {
|
|
const ids = new Set<string>();
|
|
const where = new Map<string, string[]>();
|
|
for (const file of walk(SRC)) {
|
|
const text = readFileSync(file, 'utf8');
|
|
for (const m of text.matchAll(/\baddonGate\(\s*ADDON_IDS\.([A-Z_]+)\s*\)/g)) {
|
|
const id = (ADDON_IDS as Record<string, string>)[m[1]];
|
|
if (!id) continue;
|
|
ids.add(id);
|
|
where.set(id, [...(where.get(id) ?? []), file]);
|
|
}
|
|
// Collab's sub-features ride the collab addon on top of their own flag.
|
|
if (/\bcollabFeatureGate\(/.test(text) && !file.endsWith('addon-gate.ts')) {
|
|
ids.add(ADDON_IDS.COLLAB);
|
|
where.set(ADDON_IDS.COLLAB, [...(where.get(ADDON_IDS.COLLAB) ?? []), file]);
|
|
}
|
|
}
|
|
return { ids, where };
|
|
}
|
|
|
|
describe('MCP-gated addons', () => {
|
|
it('MCP-GATE-PARITY-001: every addon that gates a tool is on the list', () => {
|
|
const { ids, where } = gatedInSource();
|
|
const listed = new Set<string>(MCP_GATED_ADDON_IDS);
|
|
|
|
const missing = [...ids].filter(id => !listed.has(id)).sort();
|
|
expect(
|
|
missing,
|
|
missing.length
|
|
? `gated but not listed: ${missing.map(id => `${id} (${where.get(id)?.[0]})`).join(', ')}`
|
|
: '',
|
|
).toEqual([]);
|
|
});
|
|
|
|
it('MCP-GATE-PARITY-002: nothing on the list is there for no reason', () => {
|
|
// The other direction, so a removed gate does not leave an entry behind that
|
|
// tears down every session for nothing. `mcp` is the exception: it gates the
|
|
// whole surface rather than any single tool, so it appears in no gate call.
|
|
const { ids } = gatedInSource();
|
|
const spurious = MCP_GATED_ADDON_IDS.filter(id => id !== ADDON_IDS.MCP && !ids.has(id)).sort();
|
|
expect(spurious, spurious.length ? `listed but nothing gates on it: ${spurious.join(', ')}` : '').toEqual([]);
|
|
});
|
|
|
|
it('MCP-GATE-PARITY-003: the source scan finds gates at all', () => {
|
|
// Guards the guard: a regex that matched nothing would make both cases pass
|
|
// on an empty set and quietly stop testing anything.
|
|
const { ids } = gatedInSource();
|
|
expect(ids.size).toBeGreaterThanOrEqual(5);
|
|
expect(ids.has(ADDON_IDS.ROADTRIP)).toBe(true);
|
|
});
|
|
});
|