| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- /**
- * The Steps view's second reading: the anchor's body in the code's order.
- *
- * `buildProgram` is pure over the records the walk makes — no graph, no
- * source — so this suite hands it records by hand and reads the block tree
- * back. The end-to-end reading over real fixtures is in
- * `ui-steps-api-servers.test.ts`; what is pinned here is the FOLD: which sites
- * become arms of one decision, what ends an arm, and where a helper is drawn.
- */
- import { describe, it, expect } from 'vitest';
- import type { BranchGuard } from '../src/graph/branch-guards';
- import { buildProgram, type ProgramInput, type ProgramSite, type WireBlock, type WireItem } from '../src/ui-server/api/program';
- /* ------------------------------------------------------------ material -- */
- let nextLine = 1;
- /** A guard, with the fields the fold reads: which decision, which arm, how the arm leaves. */
- function g(text: string, opts: Partial<BranchGuard> = {}): BranchGuard {
- return { text, negated: false, form: 'if', line: 1, branch: `b:${text}`, ...opts };
- }
- /** A site at the next line, reaching a step. */
- function at(step: string, guards: BranchGuard[] = [], extra: Partial<ProgramSite> = {}): ProgramSite {
- const line = nextLine++;
- return { step, link: `l:${step}`, at: { line, column: 0, end: { line, column: 40 } }, guards, ...extra };
- }
- /** A site that folds into a helper. */
- function into(fn: string, guards: BranchGuard[] = [], extra: Partial<ProgramSite> = {}): ProgramSite {
- const line = nextLine++;
- return { into: fn, at: { line, column: 0, end: { line, column: 40 } }, guards, ...extra };
- }
- function program(sites: Record<string, ProgramSite[]>, replies: string[] = [], into: Record<string, string> = {}) {
- nextLine = 1;
- const input: ProgramInput = {
- sites: new Map(Object.entries(sites)),
- root: 'root',
- node: (id) => ({ id, kind: 'function', name: id, qualifiedName: id, file: 'a.ts', line: 1, endLine: 2, language: 'typescript', test: false }),
- step: (id) => ({ reply: replies.includes(id), into: into[id] ?? null }),
- };
- return buildProgram(input);
- }
- /** The shape of a block, one line per item, indented — what a reader would see. */
- function shape(block: WireBlock, indent = ''): string[] {
- const out: string[] = [];
- for (const item of block) {
- if (item.kind === 'step') {
- out.push(`${indent}${item.step}${item.again ? ' (again)' : ''}${item.within ? ` inside ${item.within}` : ''}`);
- if (item.body) out.push(...shape(item.body, `${indent} `));
- } else if (item.kind === 'fork') {
- out.push(`${indent}${item.form} ${item.on}`);
- for (const arm of item.arms) {
- out.push(`${indent} arm ${arm.when}${arm.ends ? ` ends:${arm.ends}` : ''}`);
- out.push(...shape(arm.body, `${indent} `));
- }
- } else if (item.kind === 'block') {
- out.push(`${indent}${item.block} ${item.label}${item.again ? ' (again)' : ''}`);
- out.push(...shape(item.body, `${indent} `));
- } else out.push(`${indent}cut ${item.why}`);
- }
- return out;
- }
- /* --------------------------------------------------------------- tests -- */
- describe('buildProgram', () => {
- it('reads a straight line in the code’s order', () => {
- const p = program({ root: [at('a'), at('b'), at('c')] });
- expect(shape(p!.root)).toEqual(['a', 'b', 'c']);
- });
- it('is nothing when the anchor has no body to read', () => {
- expect(program({})).toBeNull();
- expect(buildProgram({ sites: new Map(), root: null, node: () => null, step: () => null })).toBeNull();
- });
- it('makes an if and its else two arms of ONE fork', () => {
- const cond = 'user && ok';
- const p = program({
- root: [at('lookup'), at('sign', [g(cond)]), at('200', [g(cond)]), at('401', [g(cond, { negated: true, form: 'else' })])],
- });
- expect(shape(p!.root)).toEqual([
- 'lookup',
- 'if user && ok',
- ' arm user && ok',
- ' sign',
- ' 200',
- ' arm !(user && ok)',
- ' 401',
- ]);
- const fork = p!.root[1] as Extract<WireItem, { kind: 'fork' }>;
- expect(fork.arms).toHaveLength(2);
- });
- it('ends an arm that answers the request', () => {
- const cond = 'user';
- const p = program(
- { root: [at('200', [g(cond)]), at('401', [g(cond, { negated: true, form: 'else' })])] },
- ['200', '401']
- );
- expect(shape(p!.root)).toEqual(['if user', ' arm user ends:reply', ' 200', ' arm !user ends:reply', ' 401']);
- });
- it('draws an early exit as the fork’s other arm, with how it leaves', () => {
- // `if (!product) { res.status(404); throw }` then the rest — the guard on
- // the code AFTER carries the same branch, negated, and how the exit left.
- const p = program({
- root: [
- at('404', [g('!product', { branch: 'b:1' })]),
- at('save', [g('!product', { negated: true, form: 'guard', branch: 'b:1', exit: 'throw' })]),
- ],
- }, ['404']);
- expect(shape(p!.root)).toEqual(['if !product', ' arm !product ends:reply', ' 404', ' arm product', ' save']);
- });
- it('draws an early exit whose arm holds nothing as a terminal', () => {
- const p = program({ root: [at('go', [g('busy', { negated: true, form: 'guard', exit: 'return' })])] });
- expect(shape(p!.root)).toEqual(['if busy', ' arm busy ends:return', ' arm !busy', ' go']);
- });
- it('nests forks the way the code nests them', () => {
- const outer = g('product', { branch: 'b:outer' });
- const inner = g('reviewed', { branch: 'b:inner' });
- const p = program(
- {
- root: [
- at('400', [outer, inner]),
- at('201', [outer, { ...inner, negated: true, form: 'guard', exit: 'throw' }]),
- at('404', [{ ...outer, negated: true, form: 'else' }]),
- ],
- },
- ['400', '201', '404']
- );
- expect(shape(p!.root)).toEqual([
- 'if product',
- ' arm product',
- ' if reviewed',
- ' arm reviewed ends:reply',
- ' 400',
- ' arm !reviewed ends:reply',
- ' 201',
- ' arm !product ends:reply',
- ' 404',
- ]);
- });
- it('puts every case of one switch under one fork', () => {
- const branch = 'b:switch';
- const p = program({
- root: [
- at('a', [g("kind === 'a'", { form: 'case', branch })]),
- at('b', [g("kind === 'b'", { form: 'case', branch })]),
- at('d', [g('kind: default', { form: 'case', branch })]),
- ],
- });
- expect(shape(p!.root)).toEqual([
- "switch kind === 'a'",
- " arm kind === 'a'",
- ' a',
- " arm kind === 'b'",
- ' b',
- ' arm kind: default',
- ' d',
- ]);
- });
- it('keeps two try/catch blocks apart', () => {
- const p = program({
- root: [
- at('first', [g('on error', { form: 'catch', branch: 'b:try1' })]),
- at('second', [g('on error', { form: 'catch', branch: 'b:try2' })]),
- ],
- });
- expect(shape(p!.root)).toEqual(['try on error', ' arm on error', ' first', 'try on error', ' arm on error', ' second']);
- });
- it('draws a folded helper where it is called, and says what it is inside', () => {
- const p = program({
- root: [into('helper', [], { within: 'res.json' }), at('200')],
- helper: [at('sign')],
- });
- expect(shape(p!.root)).toEqual(['inline via helper', ' sign', '200']);
- const block = p!.root[0] as Extract<WireItem, { kind: 'block' }>;
- expect(block.within).toBe('res.json');
- expect(block.via?.name).toBe('helper');
- });
- it('puts a call written inside another call’s arguments first', () => {
- // `res.json({ token: generateToken(…) })` spans lines 14–21 and the token is
- // signed on line 19: the signing happens BEFORE the reply it is part of.
- const reply: ProgramSite = { step: '200', at: { line: 14, column: 4, end: { line: 21, column: 6 } }, guards: [] };
- const signed: ProgramSite = { step: 'sign', at: { line: 19, column: 13, end: { line: 19, column: 34 } }, guards: [] };
- const p = program({ root: [reply, signed] });
- expect(shape(p!.root)).toEqual(['sign', '200']);
- });
- it('reads a function once, however many times it is called', () => {
- const p = program({
- root: [into('helper'), at('x'), into('helper')],
- helper: [at('work')],
- });
- expect(shape(p!.root)).toEqual(['inline via helper', ' work', 'x', 'inline via helper (again)']);
- });
- it('reads on into a step the walk entered, and stops at one it did not', () => {
- // A step explores from its own function: `store`'s is `storeFn`, whose
- // sites are its body. A boundary — another screen, an effect — has none.
- const entered = program({ root: [at('store')], storeFn: [at('write')] }, [], { store: 'storeFn' });
- expect(shape(entered!.root)).toEqual(['store', ' write']);
- const boundary = program({ root: [at('store')], storeFn: [at('write')] });
- expect(shape(boundary!.root)).toEqual(['store']);
- });
- it('says a helper that calls itself was already read', () => {
- const p = program({ root: [into('a')], a: [at('x'), into('a')] });
- expect(shape(p!.root)).toEqual(['inline via a', ' x', ' inline via a (again)']);
- });
- it('puts work registered to run later in a block of its own', () => {
- const p = program({
- root: [at('now'), at('afterwards', [], { trigger: { kind: 'callback', name: 'then', of: null } })],
- });
- expect(shape(p!.root)).toEqual(['now', 'later later · then', ' afterwards']);
- });
- it('puts calls started together in one block', () => {
- const p = program({
- root: [at('a', [], { within: 'Promise.all' }), at('b', [], { within: 'Promise.all' }), at('c')],
- });
- expect(shape(p!.root)).toEqual(['together together', ' a inside Promise.all', ' b inside Promise.all', 'c']);
- });
- it('closes a fork when the code leaves it', () => {
- const cond = g('ready');
- const p = program({ root: [at('inside', [cond]), at('after')] });
- expect(shape(p!.root)).toEqual(['if ready', ' arm ready', ' inside', 'after']);
- });
- });
|