Files
codegraph/__tests__/ui-program-model.test.ts
T
Colby McHenryandClaude Opus 5 9acab0020f feat(steps): the rail — a handler read top to bottom, forks and all
The reading the walk records now has a picture. `#/steps?…&view=order` draws
the anchor, then its body: a box per step in the order the code writes them, a
fork where the code forks with its arms side by side under the condition, a
helper drawn where it is called, and an arm that answers, returns or throws
ending there — so proshop's login reads *look the user up · if the password
matches, sign a token inside the reply and answer 200 · otherwise 401*, which
is what the code says and what a row of four boxes could not.

- `program-model.ts` decides the words: the fork carries the decision once and
  its arms say only which side they are (WHEN / WHEN NOT), except a `switch`,
  whose arms each have a case to say, and a `try`, which says `on error` once.
- `StepBox.svelte` is the box both readings draw — the canvas wraps it in
  handles, the rail lets it size to its words. Same look, same click, same
  double-click-to-start-here.
- `StepsKey.svelte` is the key, floating over the canvas as before and last in
  the document on the rail, which scrolls and cannot have things sitting on it.
- The reading travels in the URL (`view=order` / `view=tree`) and the summary
  offers both; without one, the answer's own default decides — the code's order
  for a handler or an endpoint, the tree for a screen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-29 13:35:17 -05:00

133 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* What the rail SAYS. The block tree the server sends is turned into rows of
* boxes and words here (`ui/src/lib/program-model.ts`); this pins the words —
* which is the part a reader actually meets.
*/
import { describe, it, expect } from 'vitest';
import { joinTokens } from '../ui/src/lib/conditions';
import { armWords, buildRailModel, endWords, groupLabel } from '../ui/src/lib/program-model';
import type { WireArm, WireBlock, WireItem, WireStep, WireStepsPayload } from '../ui/src/lib/wire';
const step = (id: string, over: Partial<WireStep> = {}): WireStep => ({
id,
kind: 'effect',
anchor: false,
node: null,
label: id,
sub: `response · handler`,
depth: 1,
cut: null,
...over,
});
function payload(steps: WireStep[], root: WireBlock): WireStepsPayload {
return {
anchor: { id: 'a', kind: 'route', name: 'POST /login', qualifiedName: 'POST /login', file: 'r.js', line: 1, endLine: 1, language: 'javascript', test: false },
ambiguous: [],
project: 'api',
steps,
links: [],
program: { root, truncated: 0 },
defaultView: 'order',
depth: 8,
limit: 120,
through: false,
truncated: { steps: 0, hubs: 0, chrome: 0 },
index: { lastIndexedAt: null, edges: 0, files: 0 },
timing: { elapsedMs: 1 },
};
}
const arm = (when: string, over: Partial<WireArm> = {}): WireArm => ({ when, ends: null, body: [], ...over });
describe('the rails words', () => {
it('says the decision once, and each arm only which side it is', () => {
const on = 'user && (await user.matchPassword(password))';
const fork: WireItem = {
kind: 'fork',
form: 'if',
on,
arms: [arm(on, { ends: 'reply', body: [{ kind: 'step', step: '200' }] }), arm(`!(${on})`, { not: true, ends: 'reply', body: [{ kind: 'step', step: '401' }] })],
};
const model = buildRailModel(payload([step('200'), step('401')], [fork]));
expect(model).toHaveLength(1);
const rail = model[0]!;
if (rail.kind !== 'fork') throw new Error('expected a fork');
expect(joinTokens(rail.words)).toBe('user AND (await user.matchPassword(password))');
expect(rail.arms.map((a) => joinTokens(a.words))).toEqual(['WHEN', 'WHEN NOT']);
expect(rail.arms.map((a) => a.ends)).toEqual(['answers here', 'answers here']);
});
it('keeps a disjunction whole rather than reading it as two ways of arriving', () => {
// `!image || unlimitedCollection` is ONE condition, and the parentheses
// `guardLabel` puts round it are what stop the OR from splitting it.
const on = '(!image || unlimitedCollection)';
const model = buildRailModel(payload([], [{ kind: 'fork', form: 'if', on, arms: [arm(on)] }]));
const rail = model[0]!;
if (rail.kind !== 'fork') throw new Error('expected a fork');
expect(joinTokens(rail.words)).toBe('(!image || unlimitedCollection)');
});
it('gives a switchs arms their own conditions', () => {
const fork: WireItem = {
kind: 'fork',
form: 'switch',
on: 'kind',
arms: [arm("kind === 'a'"), arm('kind: default')],
};
const model = buildRailModel(payload([], [fork]));
const rail = model[0]!;
if (rail.kind !== 'fork') throw new Error('expected a fork');
expect(joinTokens(rail.words)).toBe('kind');
expect(rail.arms.map((a) => joinTokens(a.words))).toEqual(["WHEN kind === 'a'", 'WHEN kind: default']);
});
it('lets a try say `on error` once', () => {
expect(armWords('try', 'on error', arm('on error'))).toEqual([]);
const model = buildRailModel(payload([], [{ kind: 'fork', form: 'try', on: 'on error', arms: [arm('on error')] }]));
const rail = model[0]!;
if (rail.kind !== 'fork') throw new Error('expected a fork');
expect(joinTokens(rail.words)).toBe('on error');
});
it('says how each arm leaves', () => {
expect(endWords('reply')).toBe('answers here');
expect(endWords('return')).toBe('returns here');
expect(endWords('throw')).toBe('throws here');
expect(endWords('exit')).toBe('leaves here');
});
it('names each kind of bracketed run', () => {
const via = { id: 'f', kind: 'function' as const, name: 'generateToken', qualifiedName: 'generateToken', file: 'a.js', line: 1, endLine: 2, language: 'javascript', test: false };
expect(groupLabel({ kind: 'block', block: 'inline', via, body: [] })).toBe('via generateToken');
expect(groupLabel({ kind: 'block', block: 'inline', body: [] })).toBe('via a helper');
expect(groupLabel({ kind: 'block', block: 'later', by: 'then', body: [] })).toBe('later · then');
expect(groupLabel({ kind: 'block', block: 'loop', by: 'item of items', body: [] })).toBe('for each item of items');
expect(groupLabel({ kind: 'block', block: 'together', by: 'Promise.all', body: [] })).toBe('together · Promise.all');
});
it('carries a boxs two lines and where the call is written', () => {
const model = buildRailModel(
payload([step('200', { label: '200', sub: 'response · authUser' })], [{ kind: 'step', step: '200', within: 'res.json' }])
);
const rail = model[0]!;
if (rail.kind !== 'step') throw new Error('expected a step');
expect(rail.info?.label).toBe('200');
expect(rail.info?.sub).toBe('response · authUser');
expect(rail.within).toBe('res.json');
});
it('says where the reading stopped', () => {
const model = buildRailModel(payload([], [{ kind: 'cut', why: 'folded' }, { kind: 'cut', why: 'depth' }]));
expect(model.map((i) => (i.kind === 'cut' ? i.text : ''))).toEqual([
'reads back into itself — the rest is the same code again',
'as deep as this reading goes — start at a step below to read on',
]);
});
it('is empty when there is no body to read', () => {
expect(buildRailModel({ ...payload([], []), program: null })).toEqual([]);
});
});