Skip to content
Merged
3 changes: 2 additions & 1 deletion .lantern/lang/en/test/helpers.lantern.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"macro-lab-world.ts": "Generates the invented Orchard Market project (six areas, eighteen features, five platforms with an infrastructure one and two chat bots, a foundation band, a feature left to assign, a pull request across three areas with its Flow, outside systems with different counts and some talked to by the foundation) and derives its Macro payload with the real derivation.",
"lantern-plan-world.ts": "builds a throwaway project from the shared plan fixtures, with or without a remote, and runs the plan and sync scripts against it for the plan tests",
"lantern-vocab-project.ts": "writes a synthetic project with its map, features, platforms, systems list and areas to a temp folder, runs the lantern-setup scripts on it and snapshots its .lantern files",
"lantern-map.ts": "builders for map nodes, whole maps (with their plans and unreadable plan files) and PR records",
"source-blocks.ts": "finds a function body, an IPC handler's arguments or every occurrence of a call inside a source file by matching brackets, so wiring tests check where a call sits rather than counting matches",
"lantern-map.ts":"builders for map nodes, whole maps (with their plans and unreadable plan files) and PR records",
"lantern-sections.ts": "section fixtures (RBAC, states) and a mount helper for the map view",
"macro-sizes-world.ts": "Generates a small (4 areas, 8 features) and a large (7 areas, 24 features) invented project and derives their Macro payloads with the real derivation.",
"memory-fixtures.ts": "reads the recorded ps, top, lsof and memory_pressure text and fakes the command runner",
Expand Down
19 changes: 12 additions & 7 deletions .lantern/map/test.lantern
Original file line number Diff line number Diff line change
Expand Up @@ -347,10 +347,11 @@ test/dom.test.ts . test 54fb7da385e4
= src/renderer/lib/util.ts
< src/shared/code-preview.ts

test/editor-demand-wiring.test.ts . test cfed95f7565a
test/editor-demand-wiring.test.ts . test 08482bc6b8e1
#feature serve-web
= src/main/main.ts
= src/main/windows.ts
= test/helpers/source-blocks.ts

test/editor-tab.test.ts . test f3f282dd27ed
#feature serve-web
Expand Down Expand Up @@ -441,7 +442,7 @@ test/idle-tabs.test.ts . test 7dece3b876fc
#feature serve-web
= src/renderer/lib/idle-tabs.ts

test/install-skills.test.ts . test 646437f73dc3
test/install-skills.test.ts . test e0e74002199c
#feature skill-installer
= scripts/install-skills.mjs
= scripts/lantern-bundle.mjs
Expand Down Expand Up @@ -877,7 +878,7 @@ test/lantern-macro-layout.test.ts . test 3fea77a5c0a1
= test/fixtures/lantern-macro-own-view.json
= test/helpers/lantern-macro-fixture.ts#macroFixtureView

test/lantern-macro-load.test.ts . test 8da441c3f027
test/lantern-macro-load.test.ts . test 0deea99dce39
#feature macro-view
= src/main/lantern-macro-load.ts#readRegisteredMacro
= src/main/lantern-macro-systems.ts#macroSystemsByPath
Expand All @@ -887,6 +888,7 @@ test/lantern-macro-load.test.ts . test 8da441c3f027
= src/renderer/features/lantern.ts
= test/helpers/lantern-macro-fixture.ts#macroFixtureBlocks
= test/helpers/lantern-macro-fixture.ts#macroFixtureView
= test/helpers/source-blocks.ts

test/lantern-macro-platform-uses-view.test.ts . test 192debd3300f
#feature macro-view
Expand Down Expand Up @@ -1024,13 +1026,14 @@ test/lantern-panel.test.ts . test 2e3ec75e2ee0
> src/renderer/lib/providers.ts
= test/helpers/lantern-map.ts

test/lantern-permissions-triggers.test.ts . test 4ccf978663f8
test/lantern-permissions-triggers.test.ts . test 74de70063f74
#feature skill-installer
= src/main/bundle.ts
= src/main/extensions.ts#saveSkillLinker
< src/main/extensions-ipc.ts
= src/main/extensions-ipc.ts#registerExtensionsIpc
< src/main/main.ts
< skills/lantern-delegate/scripts/permissions.mjs#missingPermissions
= test/helpers/source-blocks.ts
= test/support/bundle-fixtures.ts

test/lantern-pixi-edge-style.test.ts . test b0bded696957
Expand Down Expand Up @@ -1608,8 +1611,9 @@ test/lantern-sync-record-mirror.test.ts . test 6ade1aeb2746
= test/helpers/git-repo.ts
= test/helpers/lantern-plan-world.ts

test/lantern-sync-selftest.test.ts . test 8a7a15c03f9c
test/lantern-sync-selftest.test.ts . test 6f4ec74e1322
#feature map-sync
= skills/lantern-setup/references/model-floor.json
= skills/lantern-setup/scripts/model.mjs
= skills/lantern-sync/scripts/selftest.mjs

Expand Down Expand Up @@ -1984,9 +1988,10 @@ test/project-release.test.ts . test fcaf35dd232c
test/project-ident.test.ts . test c98778ad4b70
#feature projects-hub
= src/main/project-ident/index.ts#projectIdent
test/project-window.test.ts . test b6e790e93fff
test/project-window.test.ts . test 85f60c1b2ebb
#feature projects-hub
= src/renderer/commands/index.ts
< src/shared/commands/index.ts#COMMANDS
< src/renderer/state.ts#app
< src/renderer/workspace.css
< src/renderer/workspace.html
Expand Down
3 changes: 3 additions & 0 deletions .lantern/map/test/helpers.lantern
Original file line number Diff line number Diff line change
Expand Up @@ -217,5 +217,8 @@ test/helpers/updater-world.ts . test 577549414f03
#feature app-updates
= src/main/updater/service.ts#createUpdater

test/helpers/source-blocks.ts . test 45cdb0e2dee8
#feature serve-web

test/helpers/pr-rows.ts . test ef812afdf693
#feature pull-requests
46 changes: 46 additions & 0 deletions .lantern/prs/33.pr
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
lantern-pr 1
# 33
#title Make six brittle tests derive their expectations from the source
#url https://github.com/MobileReality/lantern/pull/33
#branch chore/robust-brittle-tests
#base main
#opened 2026-11-11
#head 22c4fa26190ce84c9da064b6396d25c6ca494032
#describe what Six brittle tests now derive expectations from the command registry, the skill bundle, model-floor.json and the real handlers.
#describe before Hard-coded 20-command and 16-skill lists, counted regex matches in main.ts.
#describe after Lists come from the source of truth; wiring is checked by where a call sits.
#describe verified typecheck, lint and the full test run are green; each contract was broken in source and its test failed.
+ test/helpers/source-blocks.ts
~ test/editor-demand-wiring.test.ts
~ test/install-skills.test.ts
~ test/lantern-macro-load.test.ts
~ test/lantern-permissions-triggers.test.ts
~ test/lantern-sync-selftest.test.ts
~ test/project-window.test.ts
#order test/helpers/source-blocks.ts
#order test/editor-demand-wiring.test.ts
#order test/lantern-permissions-triggers.test.ts
#order test/lantern-macro-load.test.ts
#order test/project-window.test.ts
#order test/install-skills.test.ts
#order test/lantern-sync-selftest.test.ts
#reason test/helpers/source-blocks.ts Shared bracket matcher so wiring tests locate handlers and function bodies without brittle regexes.
#reason test/editor-demand-wiring.test.ts Asserts where editor-server calls sit instead of counting regex matches, so renames and reorders do not break it.
#reason test/install-skills.test.ts Takes the skill list from the bundle and the skills folder instead of a hand-kept list of sixteen.
#reason test/lantern-macro-load.test.ts Drops the literal block and edge counts and checks the handler wiring by location, not exact text.
#reason test/lantern-permissions-triggers.test.ts Checks that saving the linker or importing a list syncs rules by calling the real handlers.
#reason test/lantern-sync-selftest.test.ts Drives the model policy check from model-floor.json instead of a pinned list of ids and a sentence.
#reason test/project-window.test.ts Expects the registry's commands to be registered instead of a hand-kept list of command names.
#story Tests stop breaking on unrelated edits
#step actor A developer renames or reorders code
#file test/helpers/source-blocks.ts
#step action runs the test suite
#step subject the six rewritten tests
#file test/editor-demand-wiring.test.ts
#file test/project-window.test.ts
#file test/install-skills.test.ts
#below interaction Sees a failure only when a real contract breaks
#file test/lantern-permissions-triggers.test.ts
#file test/lantern-macro-load.test.ts
#file test/lantern-sync-selftest.test.ts
#below consequence A new command or skill needed a list edit => The list comes from the registry or bundle
44 changes: 21 additions & 23 deletions test/editor-demand-wiring.test.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,38 @@
import fs from 'node:fs';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { functionBody, ipcHandler, occurrences, within } from './helpers/source-blocks';

const MAIN = fs.readFileSync(path.join(__dirname, '..', 'src', 'main', 'main.ts'), 'utf8');
const WINDOWS = fs.readFileSync(path.join(__dirname, '..', 'src', 'main', 'windows.ts'), 'utf8');

function bodyOf(source: string, header: string) {
const start = source.indexOf(header);
expect(start, `${header} exists`).toBeGreaterThan(-1);
const end = source.indexOf('\n}\n', start);
return source.slice(start, end);
}

function handlerOf(channel: string) {
const start = MAIN.indexOf(`ipcMain.handle('${channel}'`);
expect(start, `${channel} handler exists`).toBeGreaterThan(-1);
return MAIN.slice(start, MAIN.indexOf('}));', start));
}
const REQUEST = 'editorDemand.request(';
const KEEP_WARM = 'editorDemand.keepWarm(';
const ON_DEMAND_CHANNELS = ['ws:need-editor', 'ws:restart-editor'];

describe('main.ts starts the editor server only on demand', () => {
it('does not start or ask in init(), openWorkspace() or a project window', () => {
for (const body of [bodyOf(MAIN, 'async function init() {'), bodyOf(MAIN, 'function openWorkspace() {'), bodyOf(WINDOWS, 'function openProjectWindow(')]) {
expect(body).not.toMatch(/editorDemand\.|launchEditor\(|startEditor|warmEditor|serveweb\.start/);
}
const bodies = [functionBody(MAIN, 'init'), functionBody(MAIN, 'openWorkspace'), functionBody(WINDOWS, 'openProjectWindow')];
for (const body of bodies) expect(body.text).not.toMatch(/editorDemand\.|launchEditor\(|startEditor|warmEditor|serveweb\.start/);
});

it('asks for the server only inside the need-editor and restart handlers, and each of them asks', () => {
const handlers = ON_DEMAND_CHANNELS.map((channel) => ipcHandler(MAIN, channel));
for (const handler of handlers) expect(handler.text, 'handler asks').toContain(REQUEST);
const stray = occurrences(MAIN, REQUEST).filter((index) => !handlers.some((handler) => within(index, handler)));
expect(stray, 'request() outside the on-demand handlers').toEqual([]);
});

it('reaches the server start from the need-editor and restart channels alone', () => {
expect(handlerOf('ws:need-editor')).toContain('editorDemand.request()');
expect(handlerOf('ws:restart-editor')).toContain('editorDemand.request()');
expect([...MAIN.matchAll(/editorDemand\.request\(\)/g)]).toHaveLength(2);
expect([...MAIN.matchAll(/launchEditor\(/g)]).toHaveLength(1);
expect(MAIN).toContain('createEditorDemand(launchEditor)');
it('hands launchEditor to the demand gate and calls it nowhere else', () => {
expect(MAIN).toMatch(/createEditorDemand\(\s*launchEditor\s*\)/);
const withoutGate = MAIN.replace(/function launchEditor\(/, '').replace(/createEditorDemand\(\s*launchEditor\s*\)/, '');
expect(withoutGate).not.toContain('launchEditor');
});

it('only keeps the server warm from the scan, which waits for a first request', () => {
expect([...MAIN.matchAll(/editorDemand\.keepWarm\(\)/g)]).toHaveLength(1);
expect(bodyOf(MAIN, 'function runScan() {')).toContain('editorDemand.keepWarm()');
const scan = functionBody(MAIN, 'runScan');
const warm = occurrences(MAIN, KEEP_WARM);
expect(warm.length).toBeGreaterThan(0);
expect(warm.filter((index) => !within(index, scan))).toEqual([]);
});
});
44 changes: 44 additions & 0 deletions test/helpers/source-blocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export interface SourceRange {
start: number;
end: number;
text: string;
}

const CLOSERS: Record<string, string> = { '(': ')', '{': '}', '[': ']' };

function balancedFrom(source: string, open: number): SourceRange {
const opener = source[open];
const closer = CLOSERS[opener];
let depth = 0;
for (let i = open; i < source.length; i++) {
if (source[i] === opener) depth++;
else if (source[i] === closer && --depth === 0) return { start: open, end: i + 1, text: source.slice(open, i + 1) };
}
throw new Error(`unbalanced ${opener} at ${open}`);
}

export function functionBody(source: string, name: string): SourceRange {
const at = source.search(new RegExp(`function ${name}\\(`));
if (at < 0) throw new Error(`function ${name} not found`);
const params = balancedFrom(source, source.indexOf('(', at));
return balancedFrom(source, source.indexOf('{', params.end));
}

export function callArguments(source: string, callee: string): SourceRange {
const at = source.indexOf(`${callee}(`);
if (at < 0) throw new Error(`${callee}( not found`);
return balancedFrom(source, at + callee.length);
}

export function ipcHandler(source: string, channel: string): SourceRange {
const at = source.search(new RegExp(`ipcMain\\.handle\\(\\s*'${channel}'`));
if (at < 0) throw new Error(`no ipcMain.handle for ${channel}`);
return balancedFrom(source, source.indexOf('(', at));
}

export function occurrences(source: string, needle: string | RegExp): number[] {
const pattern = typeof needle === 'string' ? new RegExp(needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g') : new RegExp(needle.source, 'g');
return [...source.matchAll(pattern)].map((m) => m.index ?? 0);
}

export const within = (index: number, range: SourceRange) => index >= range.start && index < range.end;
10 changes: 7 additions & 3 deletions test/install-skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AGENT_NAMES, git, makeBundle, makeHome, makeLinker, makeRepo, snapshot

const run = promisify(execFile);
const SCRIPT = path.join(__dirname, '..', 'scripts', 'install-skills.mjs');
const SKILLS_SOURCE = path.join(__dirname, '..', 'skills');
const SKILL_NAMES = BUNDLE_SKILLS.map((s) => s.name);

let tmp: string;
Expand Down Expand Up @@ -60,10 +61,13 @@ describe('install-skills', () => {
expect(fs.existsSync(path.join(skills, 'lantern-sync', SKILL_MARKER))).toBe(true);
});

it('installs all sixteen bundled skills into a fresh project, lantern-local-loop with its nested files and executable loop.sh', async () => {
it('installs every bundled skill into a fresh project, lantern-local-loop with its nested files and executable loop.sh', async () => {
const { stdout } = await install(project);
const all = ['lantern-setup', 'lantern-sync', 'lantern-delegate', 'lantern-delegate-complex', 'lantern-plan', 'lantern-plan-impact', 'lantern-plan-stages', 'lantern-plan-review', 'lantern-plan-handoff', 'lantern-commit', 'lantern-local-loop', 'lantern-no-comments', 'lantern-code-reviewer', 'lantern-jira', 'lantern-bitbucket', 'lantern-github'];
expect(SKILL_NAMES).toEqual(all);
const shipped = fs.readdirSync(SKILLS_SOURCE).filter((entry) => fs.existsSync(path.join(SKILLS_SOURCE, entry, 'SKILL.md'))).sort();
const all = SKILL_NAMES;
expect(all.length).toBeGreaterThan(0);
expect(new Set(all).size).toBe(all.length);
expect([...all].sort()).toEqual(shipped);
for (const name of all) {
expect(stdout, name).toMatch(new RegExp(`installed +${name}\\b`));
expect(fs.existsSync(path.join(skills, name, 'SKILL.md')), name).toBe(true);
Expand Down
26 changes: 12 additions & 14 deletions test/lantern-macro-load.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { loadMacro } from '../src/main/lantern';
import { MIN_STRONG_EDGE } from '../src/main/lantern-macro';
import { NO_MAP, readRegisteredMacro } from '../src/main/lantern-macro-load';
import { macroSystemsByPath } from '../src/main/lantern-macro-systems';
import { ipcHandler } from './helpers/source-blocks';
import { macroFixtureBlocks, macroFixtureRecords, macroFixtureView } from './helpers/lantern-macro-fixture';

const FIXTURE_ROOT = path.join(__dirname, 'fixtures', 'lantern-macro');
Expand All @@ -17,14 +18,14 @@ afterEach(() => {
});

describe('lantern:macro handler', () => {
it('returns deriveMacroView of the registered project: 14 blocks, 58 edges', async () => {
it('returns deriveMacroView of the registered project', async () => {
const result = await readRegisteredMacro(registeredOnly(FIXTURE_ROOT), loadMacro, FIXTURE_ROOT);

expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.view).toEqual(macroFixtureView());
expect(result.view.blocks).toHaveLength(14);
expect(result.view.edges).toHaveLength(58);
expect(result.view.blocks.length).toBeGreaterThan(0);
expect(result.view.edges.length).toBeGreaterThan(0);
expect(result.minStrongEdge).toBe(MIN_STRONG_EDGE);
expect(result.texts).toEqual({});
});
Expand Down Expand Up @@ -156,23 +157,20 @@ describe('lantern:macro texts', () => {
describe('lantern:macro wiring', () => {
const mainSource = fs.readFileSync(path.join(__dirname, '..', 'src', 'main', 'main.ts'), 'utf8');
const preloadSource = fs.readFileSync(path.join(__dirname, '..', 'src', 'preload', 'workspace.ts'), 'utf8');
const rendererSource = fs.readFileSync(path.join(__dirname, '..', 'src', 'renderer', 'features', 'lantern.ts'), 'utf8');
const handler = () => ipcHandler(mainSource, 'lantern:macro').text;

it('registers the channel behind the trusted-sender wrapper', () => {
expect(mainSource).toMatch(/ipcMain\.handle\('lantern:macro', fromWorkspace\(/);
expect(handler()).toMatch(/^\(\s*'lantern:macro',\s*fromWorkspace\(/);
});

it('checks the project against the registered projects before reading', () => {
const handler = mainSource.slice(mainSource.indexOf("ipcMain.handle('lantern:macro'"));
expect(handler.slice(0, handler.indexOf('));'))).toContain('isRegisteredDir');
expect(handler()).toMatch(/readRegisteredMacro\(\s*isRegisteredDir\b/);
});

it('exposes one wrapped invoke in the preload, not ipcRenderer itself', () => {
expect(preloadSource).toContain("lanternMacro: (project: string, lang?: string) => ipcRenderer.invoke('lantern:macro', project, lang)");
});

it('passes the viewer language from the renderer to the main process', () => {
const rendererSource = fs.readFileSync(path.join(__dirname, '..', 'src', 'renderer', 'features', 'lantern.ts'), 'utf8');
expect(mainSource).toMatch(/readRegisteredMacro\([^]*lantern\.loadMacro, project, lang\)/);
expect(rendererSource).toContain('window.ccmws.lanternMacro(project, app.lanternLang.get(project))');
it('passes the viewer language from the preload and the renderer on to the reader', () => {
expect(preloadSource).toMatch(/lanternMacro:\s*\(project: string, lang\?: string\)\s*=>\s*ipcRenderer\.invoke\('lantern:macro', project, lang\)/);
expect(handler()).toMatch(/readRegisteredMacro\([^]*lantern\.loadMacro,\s*project,\s*lang\s*\)/);
expect(rendererSource).toMatch(/lanternMacro\(project,\s*app\.lanternLang\.get\(project\)\)/);
});
});
Loading
Loading