object-registry-synthesizer.test.ts 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. /**
  2. * Object-literal registry dispatch synthesizer.
  3. *
  4. * A command registry maps keys → handler classes/functions in an object literal, then
  5. * dispatches by a RUNTIME key (`new registry[command]().execute()`) that static parsing
  6. * can't follow. The synthesizer links each dispatching method → each registered handler's
  7. * callable entry. Validates: a class registry resolves to the handler's `.execute` method;
  8. * the field-initializer form (`commands = {…}` matched against a `this.commands[k]` dispatch);
  9. * and the dispatch GATE — a look-alike object literal that is only ever accessed statically
  10. * (never `registry[var]`) yields no edges.
  11. */
  12. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  13. import * as fs from 'node:fs';
  14. import * as path from 'node:path';
  15. import * as os from 'node:os';
  16. import { CodeGraph } from '../src';
  17. describe('object-registry synthesizer', () => {
  18. let dir: string;
  19. beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'obj-registry-')); });
  20. afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
  21. it('links a dispatcher to each registered command class’s execute method, gated on dynamic dispatch', async () => {
  22. fs.writeFileSync(
  23. path.join(dir, 'commands.ts'),
  24. `export class AddCommand { execute() { return 'add'; } }
  25. export class RemoveCommand { execute() { return 'remove'; } }
  26. export class MoveCommand { execute() { return 'move'; } }
  27. `
  28. );
  29. fs.writeFileSync(
  30. path.join(dir, 'manager.ts'),
  31. `import { AddCommand, RemoveCommand, MoveCommand } from './commands';
  32. const Cmd = { ADD: 'add', REMOVE: 'remove', MOVE: 'move' };
  33. class CommandManager {
  34. commands = {
  35. [Cmd.ADD]: AddCommand,
  36. [Cmd.REMOVE]: RemoveCommand,
  37. [Cmd.MOVE]: MoveCommand,
  38. };
  39. executeCommand(command: string) {
  40. return new this.commands[command]().execute();
  41. }
  42. }
  43. `
  44. );
  45. // A look-alike registry that is NEVER dynamically dispatched (only a static `.add`
  46. // member access) — must yield NO edges. The dynamic `registry[var]` dispatch is the gate.
  47. fs.writeFileSync(
  48. path.join(dir, 'static.ts'),
  49. `import { AddCommand, RemoveCommand } from './commands';
  50. const table = { add: AddCommand, remove: RemoveCommand };
  51. export function direct() { return new table.add().execute(); }
  52. `
  53. );
  54. const cg = await CodeGraph.init(dir, { silent: true });
  55. await cg.indexAll();
  56. const db = (cg as any).db.db;
  57. const rows = db
  58. .prepare(
  59. `SELECT s.name source_name, t.name target_name, t.kind target_kind, t.file_path target_file
  60. FROM edges e
  61. JOIN nodes s ON s.id = e.source
  62. JOIN nodes t ON t.id = e.target
  63. WHERE json_extract(e.metadata,'$.synthesizedBy') = 'object-registry'`
  64. )
  65. .all();
  66. cg.close?.();
  67. // Exactly the 3 dispatcher→handler-entry edges: executeCommand → {Add,Remove,Move}Command.execute.
  68. expect(rows.length).toBe(3);
  69. expect(rows.every((r: any) => r.source_name === 'executeCommand')).toBe(true);
  70. expect(rows.every((r: any) => r.target_kind === 'method' && r.target_name === 'execute')).toBe(true);
  71. expect(rows.every((r: any) => /commands\.ts$/.test(r.target_file))).toBe(true);
  72. // The statically-accessed look-alike registry contributed nothing.
  73. expect(rows.some((r: any) => /static\.ts$/.test(r.target_file))).toBe(false);
  74. });
  75. });