Files
codegraph/__tests__/vuex-dispatch-synthesizer.test.ts
T
Colby McHenryandClaude Opus 4.8 80a1044d3d feat(resolution): bridge Vuex string dispatch/commit to actions and mutations
Completes the Vue store dispatch family (the Pinia bridge was 8ea3205). Vuex
dispatches by a runtime STRING key — `dispatch('user/login')` /
`commit('SET_TOKEN')` / `this.$store.dispatch('app/toggleDevice')` — with no
static edge to the handler.

vuexDispatchEdges (callback-synthesizer.ts): the last `/` segment of the key is
the action/mutation name, the preceding segment is the namespace (≈ the module
file). Resolve the name to a function node IN A STORE FILE — the ≥2-signal
store-file gate excludes a same-named `api/` helper (`getInfo`/`login` collide in
practice) — disambiguated by the immediate namespace segment appearing in the
path (handles deep nesting like `d2admin/user/set`), or the same file for a root
local `commit('M')` inside an action. The .vue component is a dispatcher fallback
for top-level setup calls. Surfaces in explore as `dynamic: vuex dispatch`.

Also extracts the canonical Vuex MODULE shape `export default { namespaced,
actions: {…}, mutations: {…} }` (tree-sitter.ts: extractStoreCollectionMethods
off the export_statement, store-file gated) — its object-literal methods were
otherwise never nodes, so d2-admin's actions couldn't be bridged.

Validated 100% precision on three repos — vue-element-admin (55 edges),
vue-admin-template (12), d2-admin (63): 0 non-store targets, 0 namespace
mismatches (54/54 namespaced edges route to the correct module despite 6
colliding `load` actions in d2-admin), 0 on Redux controls (basetool/uwave —
non-string `dispatch()` correctly ignored). Suite green (1613); new
__tests__/vuex-dispatch-synthesizer.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:49:06 -05:00

101 lines
4.0 KiB
TypeScript

/**
* Vuex string-keyed dispatch/commit bridge.
*
* Vuex dispatches actions/mutations by a runtime STRING key — `dispatch('user/login')`,
* `commit('SET_TOKEN')` — with no static edge to the handler (an object-literal
* method in a store module). This bridges the key to its function node: the last
* `/` segment is the action/mutation name, the preceding segment is the namespace
* (≈ the module file). It resolves to a node IN A STORE FILE (excluding a same-named
* `api/` helper — a real collision), disambiguated by the namespace appearing in the
* path, or the same file for a root `commit('M')` inside an action. Redux-style
* `dispatch(actionCreator())` (no string key) produces nothing.
*
* Also exercises the canonical Vuex MODULE shape `export default { namespaced,
* actions: {…}, mutations: {…} }` — whose methods only become nodes via the
* store-collection extraction this bridge depends on.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { CodeGraph } from '../src';
describe('vuex-dispatch synthesizer', () => {
let dir: string;
beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vuex-dispatch-')); });
afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
it('bridges namespaced dispatch + local commit to the right store handler, excluding an api collision', async () => {
fs.mkdirSync(path.join(dir, 'store', 'modules'), { recursive: true });
fs.mkdirSync(path.join(dir, 'api'), { recursive: true });
// Canonical Vuex module: `export default { namespaced, actions, mutations }`.
fs.writeFileSync(
path.join(dir, 'store', 'modules', 'user.js'),
`import { login as apiLogin } from '../../api/user';
export default {
namespaced: true,
state: { token: '' },
mutations: {
SET_TOKEN(state, t) { state.token = t; },
},
actions: {
login({ commit }, info) {
apiLogin(info);
commit('SET_TOKEN', info.token); // root/local key → SET_TOKEN in THIS module
},
},
};
`
);
// Collision: an api helper ALSO named `login` — must never be the dispatch target.
fs.writeFileSync(
path.join(dir, 'api', 'user.js'),
`export function login(info) { return info; }
`
);
// Consumer dispatches by namespaced string key.
fs.writeFileSync(
path.join(dir, 'app.js'),
`import store from './store';
export function bootstrap() {
store.dispatch('user/login', { token: 'x' });
}
`
);
// Redux-style control: a non-string dispatch must produce no vuex edge.
fs.writeFileSync(
path.join(dir, 'reduxy.js'),
`export function reduxy(dispatch) {
dispatch(someAction());
}
`
);
const cg = await CodeGraph.init(dir, { silent: true });
await cg.indexAll();
const db = (cg as any).db.db;
const edges = db
.prepare(
`SELECT s.name source, t.name target, t.file_path tf, json_extract(e.metadata,'$.via') via
FROM edges e JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target
WHERE json_extract(e.metadata,'$.synthesizedBy') = 'vuex-dispatch'`
)
.all();
// bootstrap → login, resolving to the STORE module (not api/user.js).
const loginEdge = edges.find((r: any) => r.source === 'bootstrap' && r.target === 'login');
expect(loginEdge).toBeTruthy();
expect(loginEdge.tf).toMatch(/store[\\/]modules[\\/]user\.js$/);
expect(loginEdge.via).toBe('user/login');
// The api helper of the same name was never targeted.
expect(edges.some((r: any) => /api[\\/]user\.js$/.test(r.tf))).toBe(false);
// Local commit('SET_TOKEN') inside the action → the same module's mutation.
expect(edges.some((r: any) => r.source === 'login' && r.target === 'SET_TOKEN')).toBe(true);
// Redux-style non-string dispatch contributed nothing.
expect(edges.some((r: any) => r.source === 'reduxy')).toBe(false);
cg.close?.();
});
});