vue-store-extraction.test.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. /**
  2. * Vue store action/mutation/getter extraction (the foundation for finding and
  3. * reading store logic — `codegraph_node login` / `getSessionList`).
  4. *
  5. * Vuex/Pinia define a store's callable surface as object-literal members nested
  6. * under `actions`/`mutations`/`getters`, or as body-local consts in a Pinia setup
  7. * store — none of which were extracted, so the symbols an agent looks for didn't
  8. * exist as nodes. This covers the three dominant forms:
  9. * - Vuex module: non-exported `const actions = {…}` / `const mutations = {…}`.
  10. * - Pinia options: `defineStore({ actions: {…}, getters: {…} })`.
  11. * - Pinia setup: `defineStore('id', () => { const foo = …; return { foo } })`.
  12. * And the precision gate: a non-exported `const actions = {…}` in a file that
  13. * isn't a Vue store contributes nothing.
  14. */
  15. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  16. import * as fs from 'node:fs';
  17. import * as path from 'node:path';
  18. import * as os from 'node:os';
  19. import { CodeGraph } from '../src';
  20. describe('vue store extraction', () => {
  21. let dir: string;
  22. beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vue-store-')); });
  23. afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
  24. it('extracts Vuex module + Pinia options + Pinia setup store members as function nodes', async () => {
  25. // Vuex MODULE form: non-exported `const mutations`/`const actions` collections,
  26. // wired via a default export (element-admin style). Method shorthand + arrow pairs.
  27. fs.writeFileSync(
  28. path.join(dir, 'userModule.js'),
  29. `import { persistToken } from './auth-utils';
  30. const state = { token: '' };
  31. const mutations = {
  32. SET_TOKEN: (state, token) => { state.token = token; },
  33. };
  34. const actions = {
  35. login({ commit }, info) {
  36. persistToken(info.token);
  37. },
  38. async logout({ commit }) {
  39. commit('SET_TOKEN', '');
  40. },
  41. };
  42. export default { namespaced: true, state, mutations, actions };
  43. `
  44. );
  45. fs.writeFileSync(
  46. path.join(dir, 'auth-utils.js'),
  47. `export function persistToken(token) { return token; }
  48. `
  49. );
  50. // Pinia OPTIONS form: actions + getters as object properties of a defineStore config.
  51. fs.writeFileSync(
  52. path.join(dir, 'authStore.ts'),
  53. `import { defineStore } from 'pinia';
  54. export const useAuthStore = defineStore({
  55. id: 'auth',
  56. state: () => ({ name: '' }),
  57. getters: {
  58. upperName: state => state.name.toUpperCase(),
  59. },
  60. actions: {
  61. async fetchMenu() { return loadMenu(); },
  62. setName(n: string) { this.name = n; },
  63. },
  64. });
  65. `
  66. );
  67. // Pinia SETUP form: actions are body-local consts exposed via the return block.
  68. fs.writeFileSync(
  69. path.join(dir, 'chatStore.ts'),
  70. `import { defineStore } from 'pinia';
  71. export const useChatStore = defineStore('chat', () => {
  72. const list = reactive([]);
  73. const getList = async () => { return fetchList(); };
  74. function pushItem(x) { list.push(x); }
  75. return { list, getList, pushItem };
  76. });
  77. `
  78. );
  79. const cg = await CodeGraph.init(dir, { silent: true });
  80. await cg.indexAll();
  81. const db = (cg as any).db.db;
  82. const fn = (name: string) =>
  83. db.prepare(`SELECT count(*) c FROM nodes WHERE name = ? AND kind = 'function'`).get(name).c;
  84. // Vuex module: actions + mutations extracted.
  85. expect(fn('login')).toBeGreaterThan(0);
  86. expect(fn('logout')).toBeGreaterThan(0);
  87. expect(fn('SET_TOKEN')).toBeGreaterThan(0);
  88. // Pinia options: actions + getter extracted.
  89. expect(fn('fetchMenu')).toBeGreaterThan(0);
  90. expect(fn('setName')).toBeGreaterThan(0);
  91. expect(fn('upperName')).toBeGreaterThan(0);
  92. // Pinia setup: body-local actions extracted (and reachable via their bodies).
  93. expect(fn('getList')).toBeGreaterThan(0);
  94. expect(fn('pushItem')).toBeGreaterThan(0);
  95. // The extracted action spans its real body — `login`'s `persistToken(...)`
  96. // call attributes to it (extraction, not the deferred dispatch synthesis).
  97. const loginCalls = db
  98. .prepare(
  99. `SELECT t.name FROM edges e JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target
  100. WHERE s.name = 'login' AND e.kind = 'calls'`
  101. )
  102. .all()
  103. .map((r: any) => r.name);
  104. expect(loginCalls).toContain('persistToken');
  105. cg.close?.();
  106. });
  107. it('does not extract a non-exported `const actions = {…}` outside a Vue store file', async () => {
  108. // A plain module that happens to hold a non-exported `const actions` object of
  109. // functions, but lacks any second Vue-store signal — the gate must not fire.
  110. fs.writeFileSync(
  111. path.join(dir, 'commands.js'),
  112. `const actions = {
  113. doThing() { return 1; },
  114. doOther() { return 2; },
  115. };
  116. export function run(key) { return actions[key](); }
  117. `
  118. );
  119. const cg = await CodeGraph.init(dir, { silent: true });
  120. await cg.indexAll();
  121. const db = (cg as any).db.db;
  122. expect(db.prepare(`SELECT count(*) c FROM nodes WHERE name = 'doThing'`).get().c).toBe(0);
  123. expect(db.prepare(`SELECT count(*) c FROM nodes WHERE name = 'doOther'`).get().c).toBe(0);
  124. // The real exported function is still extracted normally.
  125. expect(db.prepare(`SELECT count(*) c FROM nodes WHERE name = 'run' AND kind='function'`).get().c).toBeGreaterThan(0);
  126. cg.close?.();
  127. });
  128. });