archsync 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/dist/archsync.cjs +2 -0
- package/package.json +11 -7
- package/bin/cli.js +0 -91
- package/src/__tests__/e2e-workflow.test.js +0 -66
- package/src/__tests__/hashEngine.test.js +0 -109
- package/src/__tests__/impact.test.js +0 -137
- package/src/__tests__/parsers.test.js +0 -496
- package/src/__tests__/scan-pipeline.test.js +0 -332
- package/src/__tests__/schemaBuilder.test.js +0 -145
- package/src/__tests__/workspace.test.js +0 -178
- package/src/commands/backup.js +0 -54
- package/src/commands/connect.js +0 -129
- package/src/commands/diff.js +0 -228
- package/src/commands/export.js +0 -125
- package/src/commands/impactReport.js +0 -50
- package/src/commands/import.js +0 -126
- package/src/commands/init.js +0 -80
- package/src/commands/login.js +0 -116
- package/src/commands/plugin.js +0 -28
- package/src/commands/push.js +0 -194
- package/src/commands/register.js +0 -127
- package/src/commands/scan.js +0 -498
- package/src/commands/serve.js +0 -133
- package/src/commands/setup.js +0 -233
- package/src/commands/status.js +0 -56
- package/src/commands/validate.js +0 -245
- package/src/commands/watch.js +0 -70
- package/src/core/credentialStore.js +0 -76
- package/src/core/hashEngine.js +0 -34
- package/src/core/impactEngine.js +0 -192
- package/src/core/monorepoDetector.js +0 -41
- package/src/core/pluginManager.js +0 -40
- package/src/core/relationshipEngine.js +0 -917
- package/src/core/requestSigning.js +0 -16
- package/src/core/schemaBuilder.js +0 -230
- package/src/core/schemaDeduplicator.js +0 -54
- package/src/core/supabaseClient.js +0 -68
- package/src/core/workspaceDetector.js +0 -113
- package/src/parsers/astParser.js +0 -274
- package/src/parsers/configParser.js +0 -49
- package/src/parsers/dependencyGraph.js +0 -31
- package/src/parsers/flutterParser.js +0 -98
- package/src/parsers/goParser.js +0 -99
- package/src/parsers/index.js +0 -211
- package/src/parsers/javaParser.js +0 -89
- package/src/parsers/nodeParser.js +0 -429
- package/src/parsers/pythonParser.js +0 -109
- package/src/parsers/reactParser.js +0 -368
- package/src/parsers/smartComment.js +0 -144
|
@@ -1,496 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for CLI parsers:
|
|
3
|
-
* - reactParser (parseReactFile)
|
|
4
|
-
* - nodeParser (parseNodeFile)
|
|
5
|
-
* - smartComment (parseSmartComments)
|
|
6
|
-
*
|
|
7
|
-
* All tests use vi.mock to intercept fs.readFileSync so no real files are
|
|
8
|
-
* needed on disk.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
12
|
-
import { parseReactFile } from '../parsers/reactParser.js';
|
|
13
|
-
import { parseNodeFile } from '../parsers/nodeParser.js';
|
|
14
|
-
import { parseSmartComments } from '../parsers/smartComment.js';
|
|
15
|
-
|
|
16
|
-
// ─── Mock fs module ──────────────────────────────────────────
|
|
17
|
-
// The parsers use `import fs from 'fs'` (default import), so the mock must
|
|
18
|
-
// replace readFileSync on the default export as well as the named one.
|
|
19
|
-
vi.mock('fs', async (importOriginal) => {
|
|
20
|
-
const actual = await importOriginal();
|
|
21
|
-
const readFileSync = vi.fn();
|
|
22
|
-
return {
|
|
23
|
-
...actual,
|
|
24
|
-
readFileSync,
|
|
25
|
-
default: { ...actual.default, readFileSync },
|
|
26
|
-
};
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
import fs from 'fs';
|
|
30
|
-
|
|
31
|
-
function setFileContent(content) {
|
|
32
|
-
fs.readFileSync.mockReturnValue(content);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// ─── reactParser ─────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
describe('reactParser — parseReactFile', () => {
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
vi.clearAllMocks();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('extracts a React functional component by name', () => {
|
|
43
|
-
setFileContent(`
|
|
44
|
-
export default function HomePage() {
|
|
45
|
-
return <div>Home</div>;
|
|
46
|
-
}
|
|
47
|
-
`);
|
|
48
|
-
const result = parseReactFile('/web/pages/HomePage.jsx');
|
|
49
|
-
expect(result.entities.length).toBeGreaterThan(0);
|
|
50
|
-
const comp = result.entities.find(e => e.name === 'HomePage');
|
|
51
|
-
expect(comp).toBeDefined();
|
|
52
|
-
expect(comp.entityType).toBe('screen');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('extracts a named export component', () => {
|
|
56
|
-
setFileContent(`
|
|
57
|
-
export function UserCard() {
|
|
58
|
-
return <div />;
|
|
59
|
-
}
|
|
60
|
-
`);
|
|
61
|
-
const result = parseReactFile('/web/components/UserCard.jsx');
|
|
62
|
-
const comp = result.entities.find(e => e.name === 'UserCard');
|
|
63
|
-
expect(comp).toBeDefined();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('classifies "page"-named components as screen entityType', () => {
|
|
67
|
-
setFileContent(`
|
|
68
|
-
export function DashboardPage() {
|
|
69
|
-
return <div />;
|
|
70
|
-
}
|
|
71
|
-
`);
|
|
72
|
-
const result = parseReactFile('/web/pages/DashboardPage.jsx');
|
|
73
|
-
const comp = result.entities.find(e => e.name === 'DashboardPage');
|
|
74
|
-
expect(comp.entityType).toBe('screen');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('extracts a custom hook as a service entity', () => {
|
|
78
|
-
setFileContent(`
|
|
79
|
-
export function useAuthState() {
|
|
80
|
-
const [user, setUser] = useState(null);
|
|
81
|
-
return user;
|
|
82
|
-
}
|
|
83
|
-
`);
|
|
84
|
-
const result = parseReactFile('/web/hooks/useAuthState.js');
|
|
85
|
-
const hook = result.entities.find(e => e.name === 'useAuthState');
|
|
86
|
-
expect(hook).toBeDefined();
|
|
87
|
-
expect(hook.entityType).toBe('service');
|
|
88
|
-
expect(hook.data.type).toBe('hook');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('extracts a fetch() API call as an api entity', () => {
|
|
92
|
-
setFileContent(`
|
|
93
|
-
export function ProfilePage() {
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
fetch('/api/users/me').then(r => r.json());
|
|
96
|
-
}, []);
|
|
97
|
-
return <div />;
|
|
98
|
-
}
|
|
99
|
-
`);
|
|
100
|
-
const result = parseReactFile('/web/pages/ProfilePage.jsx');
|
|
101
|
-
const apiEntity = result.entities.find(e => e.entityType === 'api');
|
|
102
|
-
expect(apiEntity).toBeDefined();
|
|
103
|
-
expect(apiEntity.data.path).toBe('/api/users/me');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('extracts an axios API call as an api entity', () => {
|
|
107
|
-
setFileContent(`
|
|
108
|
-
export function OrdersPage() {
|
|
109
|
-
useEffect(() => {
|
|
110
|
-
axios.post('/api/orders', payload);
|
|
111
|
-
}, []);
|
|
112
|
-
return <div />;
|
|
113
|
-
}
|
|
114
|
-
`);
|
|
115
|
-
const result = parseReactFile('/web/pages/OrdersPage.jsx');
|
|
116
|
-
const apiEntity = result.entities.find(e => e.entityType === 'api' && e.data.method === 'POST');
|
|
117
|
-
expect(apiEntity).toBeDefined();
|
|
118
|
-
expect(apiEntity.data.path).toBe('/api/orders');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('does not extract lowercase utility functions as components', () => {
|
|
122
|
-
setFileContent(`
|
|
123
|
-
function formatDate(d) { return d.toISOString(); }
|
|
124
|
-
export function Header() { return <header />; }
|
|
125
|
-
`);
|
|
126
|
-
const result = parseReactFile('/web/components/Header.jsx');
|
|
127
|
-
const utils = result.entities.filter(e => e.name === 'formatDate');
|
|
128
|
-
expect(utils).toHaveLength(0);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('sets system to "admin" for files inside /admin/ paths', () => {
|
|
132
|
-
setFileContent(`
|
|
133
|
-
export function AdminDashboard() { return <div />; }
|
|
134
|
-
`);
|
|
135
|
-
const result = parseReactFile('/admin/pages/AdminDashboard.jsx');
|
|
136
|
-
const comp = result.entities.find(e => e.name === 'AdminDashboard');
|
|
137
|
-
expect(comp?.system).toBe('admin');
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('does not extract built-in hooks (useState, useEffect, useContext)', () => {
|
|
141
|
-
setFileContent(`
|
|
142
|
-
export function Counter() {
|
|
143
|
-
const [n, setN] = useState(0);
|
|
144
|
-
useEffect(() => {}, []);
|
|
145
|
-
return <div />;
|
|
146
|
-
}
|
|
147
|
-
`);
|
|
148
|
-
const result = parseReactFile('/web/Counter.jsx');
|
|
149
|
-
const builtins = result.entities.filter(e =>
|
|
150
|
-
['useState', 'useEffect', 'useContext'].includes(e.name)
|
|
151
|
-
);
|
|
152
|
-
expect(builtins).toHaveLength(0);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('creates import relations for relative component imports', () => {
|
|
156
|
-
setFileContent(`
|
|
157
|
-
import Button from './Button';
|
|
158
|
-
export function LoginPage() {
|
|
159
|
-
return <Button />;
|
|
160
|
-
}
|
|
161
|
-
`);
|
|
162
|
-
const result = parseReactFile('/web/pages/LoginPage.jsx');
|
|
163
|
-
// LoginPage should exist and have a relation pointing to Button
|
|
164
|
-
expect(result.relations.length).toBeGreaterThan(0);
|
|
165
|
-
expect(result.relations[0].target).toBe('Button');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('returns empty entities for empty file', () => {
|
|
169
|
-
setFileContent('');
|
|
170
|
-
const result = parseReactFile('/web/empty.jsx');
|
|
171
|
-
expect(result.entities).toHaveLength(0);
|
|
172
|
-
expect(result.relations).toHaveLength(0);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('handles syntax that looks like a component but is a variable assignment', () => {
|
|
176
|
-
setFileContent(`
|
|
177
|
-
const config = { theme: 'dark' };
|
|
178
|
-
export default config;
|
|
179
|
-
`);
|
|
180
|
-
const result = parseReactFile('/web/config.js');
|
|
181
|
-
// No PascalCase function component should be found
|
|
182
|
-
const comps = result.entities.filter(e => e.entityType === 'screen');
|
|
183
|
-
expect(comps).toHaveLength(0);
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// ─── nodeParser ──────────────────────────────────────────────
|
|
188
|
-
|
|
189
|
-
describe('nodeParser — parseNodeFile', () => {
|
|
190
|
-
beforeEach(() => {
|
|
191
|
-
vi.clearAllMocks();
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('extracts Express GET routes', () => {
|
|
195
|
-
setFileContent(`
|
|
196
|
-
const router = express.Router();
|
|
197
|
-
router.get('/users', getUsers);
|
|
198
|
-
router.get('/users/:id', getUserById);
|
|
199
|
-
`);
|
|
200
|
-
const result = parseNodeFile('/backend/routes/userRoutes.js');
|
|
201
|
-
const routes = result.entities.filter(e => e.entityType === 'route');
|
|
202
|
-
expect(routes).toHaveLength(2);
|
|
203
|
-
expect(routes[0].data.method).toBe('GET');
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('extracts POST, PUT, DELETE routes', () => {
|
|
207
|
-
setFileContent(`
|
|
208
|
-
router.post('/items', createItem);
|
|
209
|
-
router.put('/items/:id', updateItem);
|
|
210
|
-
router.delete('/items/:id', deleteItem);
|
|
211
|
-
`);
|
|
212
|
-
const result = parseNodeFile('/backend/routes/itemRoutes.js');
|
|
213
|
-
const routes = result.entities.filter(e => e.entityType === 'route');
|
|
214
|
-
expect(routes.map(r => r.data.method).sort()).toEqual(['DELETE', 'POST', 'PUT']);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('extracts route names with full path', () => {
|
|
218
|
-
setFileContent(`router.get('/health', healthCheck);`);
|
|
219
|
-
const result = parseNodeFile('/backend/routes/health.js');
|
|
220
|
-
const route = result.entities.find(e => e.entityType === 'route');
|
|
221
|
-
expect(route.name).toBe('GET /health');
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('extracts Mongoose models', () => {
|
|
225
|
-
setFileContent(`
|
|
226
|
-
const User = mongoose.model('User', userSchema);
|
|
227
|
-
const Post = mongoose.model('Post', postSchema);
|
|
228
|
-
`);
|
|
229
|
-
const result = parseNodeFile('/backend/models/User.model.js');
|
|
230
|
-
const models = result.entities.filter(e => e.entityType === 'model' && e.data.orm === 'mongoose');
|
|
231
|
-
expect(models).toHaveLength(2);
|
|
232
|
-
expect(models.map(m => m.name)).toContain('User');
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('extracts Sequelize models', () => {
|
|
236
|
-
setFileContent(`const Order = sequelize.define('Order', {});`);
|
|
237
|
-
const result = parseNodeFile('/backend/models/Order.js');
|
|
238
|
-
const model = result.entities.find(e => e.data.orm === 'sequelize');
|
|
239
|
-
expect(model).toBeDefined();
|
|
240
|
-
expect(model.name).toBe('Order');
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('extracts Firestore collection references', () => {
|
|
244
|
-
setFileContent(`
|
|
245
|
-
db.collection('users').get();
|
|
246
|
-
db.collection('products').where('active', '==', true);
|
|
247
|
-
`);
|
|
248
|
-
const result = parseNodeFile('/backend/services/db.js');
|
|
249
|
-
const dbs = result.entities.filter(e => e.entityType === 'database');
|
|
250
|
-
expect(dbs).toHaveLength(2);
|
|
251
|
-
expect(dbs.map(d => d.data.collection)).toContain('users');
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('deduplicates Firestore collections referenced multiple times', () => {
|
|
255
|
-
setFileContent(`
|
|
256
|
-
db.collection('users').get();
|
|
257
|
-
db.collection('users').where('active', '==', true);
|
|
258
|
-
`);
|
|
259
|
-
const result = parseNodeFile('/backend/services/userService.js');
|
|
260
|
-
const dbs = result.entities.filter(e => e.entityType === 'database');
|
|
261
|
-
expect(dbs).toHaveLength(1);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('extracts class-based controllers', () => {
|
|
265
|
-
setFileContent(`
|
|
266
|
-
class UserController {
|
|
267
|
-
async getUser(req, res) {}
|
|
268
|
-
async createUser(req, res) {}
|
|
269
|
-
}
|
|
270
|
-
`);
|
|
271
|
-
const result = parseNodeFile('/backend/controllers/userController.js');
|
|
272
|
-
const ctrl = result.entities.find(e => e.name === 'UserController');
|
|
273
|
-
expect(ctrl).toBeDefined();
|
|
274
|
-
expect(ctrl.entityType).toBe('controller');
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it('extracts exported arrow functions from controller files', () => {
|
|
278
|
-
setFileContent(`
|
|
279
|
-
export const getUser = async (req, res) => {
|
|
280
|
-
res.json({});
|
|
281
|
-
};
|
|
282
|
-
export const createUser = async (req, res) => {
|
|
283
|
-
res.json({});
|
|
284
|
-
};
|
|
285
|
-
`);
|
|
286
|
-
const result = parseNodeFile('/backend/controllers/userController.js');
|
|
287
|
-
const controllers = result.entities.filter(e => e.entityType === 'controller');
|
|
288
|
-
expect(controllers.length).toBeGreaterThanOrEqual(2);
|
|
289
|
-
expect(controllers.map(c => c.name)).toContain('getUser');
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it('extracts mount points from app.use()', () => {
|
|
293
|
-
setFileContent(`
|
|
294
|
-
app.use('/api/v1/users', userRoutes);
|
|
295
|
-
app.use('/api/v1/orders', orderRoutes);
|
|
296
|
-
`);
|
|
297
|
-
const result = parseNodeFile('/backend/app.js');
|
|
298
|
-
const mounts = result.entities.filter(e => e.entityType === 'mount');
|
|
299
|
-
expect(mounts).toHaveLength(2);
|
|
300
|
-
expect(mounts[0].data.prefix).toBe('/api/v1/users');
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it('extracts custom middleware references (non-built-in)', () => {
|
|
304
|
-
setFileContent(`
|
|
305
|
-
app.use(authMiddleware);
|
|
306
|
-
app.use(rateLimiter);
|
|
307
|
-
app.use(cors);
|
|
308
|
-
`);
|
|
309
|
-
const result = parseNodeFile('/backend/app.js');
|
|
310
|
-
const mws = result.entities.filter(e => e.entityType === 'middleware');
|
|
311
|
-
// cors is in the built-in skip list; authMiddleware and rateLimiter should be extracted
|
|
312
|
-
const names = mws.map(m => m.name);
|
|
313
|
-
expect(names).toContain('authMiddleware');
|
|
314
|
-
expect(names).toContain('rateLimiter');
|
|
315
|
-
expect(names).not.toContain('cors');
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it('returns empty entities for an empty file', () => {
|
|
319
|
-
setFileContent('');
|
|
320
|
-
const result = parseNodeFile('/backend/empty.js');
|
|
321
|
-
expect(result.entities).toHaveLength(0);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it('does not throw on unusual but valid JavaScript patterns', () => {
|
|
325
|
-
setFileContent(`
|
|
326
|
-
const obj = { nested: { deep: true } };
|
|
327
|
-
module.exports = obj;
|
|
328
|
-
`);
|
|
329
|
-
expect(() => parseNodeFile('/backend/misc.js')).not.toThrow();
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it('creates parent-class relations for classes that extend a base', () => {
|
|
333
|
-
setFileContent(`
|
|
334
|
-
class AdminController extends BaseController {
|
|
335
|
-
async list(req, res) {}
|
|
336
|
-
}
|
|
337
|
-
`);
|
|
338
|
-
const result = parseNodeFile('/backend/controllers/adminController.js');
|
|
339
|
-
const rel = result.relations.find(r => r.relation === 'extends');
|
|
340
|
-
expect(rel).toBeDefined();
|
|
341
|
-
expect(rel.source).toBe('AdminController');
|
|
342
|
-
expect(rel.target).toBe('BaseController');
|
|
343
|
-
});
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
// ─── smartComment parser ──────────────────────────────────────
|
|
347
|
-
|
|
348
|
-
describe('smartComment — parseSmartComments', () => {
|
|
349
|
-
beforeEach(() => {
|
|
350
|
-
vi.clearAllMocks();
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
it('extracts an @archsync:entity directive', () => {
|
|
354
|
-
setFileContent(`
|
|
355
|
-
// @archsync:entity service AuthService
|
|
356
|
-
function doSomething() {}
|
|
357
|
-
`);
|
|
358
|
-
const result = parseSmartComments('/backend/auth.js');
|
|
359
|
-
expect(result.entities).toHaveLength(1);
|
|
360
|
-
expect(result.entities[0].name).toBe('AuthService');
|
|
361
|
-
expect(result.entities[0].entityType).toBe('service');
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it('extracts an @archsync:api directive', () => {
|
|
365
|
-
setFileContent(`
|
|
366
|
-
// @archsync:api POST /api/auth/login
|
|
367
|
-
router.post('/login', loginHandler);
|
|
368
|
-
`);
|
|
369
|
-
const result = parseSmartComments('/backend/routes/auth.js');
|
|
370
|
-
expect(result.entities).toHaveLength(1);
|
|
371
|
-
expect(result.entities[0].entityType).toBe('api');
|
|
372
|
-
expect(result.entities[0].data.method).toBe('POST');
|
|
373
|
-
expect(result.entities[0].data.path).toBe('/api/auth/login');
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
it('extracts a @archsync:model directive with a name', () => {
|
|
377
|
-
setFileContent(`
|
|
378
|
-
// @archsync:model UserProfile
|
|
379
|
-
class UserProfile {}
|
|
380
|
-
`);
|
|
381
|
-
const result = parseSmartComments('/backend/models/userProfile.js');
|
|
382
|
-
expect(result.entities).toHaveLength(1);
|
|
383
|
-
expect(result.entities[0].entityType).toBe('model');
|
|
384
|
-
expect(result.entities[0].name).toBe('UserProfile');
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
it('extracts @archsync:relation directive', () => {
|
|
388
|
-
setFileContent(`
|
|
389
|
-
// @archsync:relation AuthService -> UserModel queries
|
|
390
|
-
`);
|
|
391
|
-
const result = parseSmartComments('/backend/auth.js');
|
|
392
|
-
expect(result.relations).toHaveLength(1);
|
|
393
|
-
expect(result.relations[0].source).toBe('AuthService');
|
|
394
|
-
expect(result.relations[0].target).toBe('UserModel');
|
|
395
|
-
expect(result.relations[0].relation).toBe('queries');
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
it('uses "uses" as default relation when none is specified', () => {
|
|
399
|
-
setFileContent(`
|
|
400
|
-
// @archsync:relation ServiceA -> ServiceB
|
|
401
|
-
`);
|
|
402
|
-
const result = parseSmartComments('/backend/foo.js');
|
|
403
|
-
expect(result.relations[0].relation).toBe('uses');
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it('handles @archsync:system directive and applies it to subsequent entities', () => {
|
|
407
|
-
setFileContent(`
|
|
408
|
-
// @archsync:system web
|
|
409
|
-
// @archsync:entity screen LandingPage
|
|
410
|
-
`);
|
|
411
|
-
const result = parseSmartComments('/web/landing.js');
|
|
412
|
-
expect(result.entities[0].system).toBe('web');
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
it('attaches @archsync:tag values to the current entity metadata', () => {
|
|
416
|
-
setFileContent(`
|
|
417
|
-
// @archsync:entity service NotificationService
|
|
418
|
-
// @archsync:tag notifications email push
|
|
419
|
-
`);
|
|
420
|
-
const result = parseSmartComments('/backend/notif.js');
|
|
421
|
-
expect(result.entities[0].metadata.tags).toContain('notifications');
|
|
422
|
-
expect(result.entities[0].metadata.tags).toContain('email');
|
|
423
|
-
expect(result.entities[0].metadata.tags).toContain('push');
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
it('attaches @archsync:description to the current entity', () => {
|
|
427
|
-
setFileContent(`
|
|
428
|
-
// @archsync:entity service PaymentService
|
|
429
|
-
// @archsync:description Handles Stripe payments
|
|
430
|
-
`);
|
|
431
|
-
const result = parseSmartComments('/backend/payment.js');
|
|
432
|
-
expect(result.entities[0].description).toBe('Handles Stripe payments');
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
it('returns empty entities and relations for @archsync:ignore', () => {
|
|
436
|
-
setFileContent(`
|
|
437
|
-
// @archsync:ignore
|
|
438
|
-
// @archsync:entity service ShouldNotAppear
|
|
439
|
-
`);
|
|
440
|
-
const result = parseSmartComments('/backend/ignored.js');
|
|
441
|
-
expect(result.entities).toHaveLength(0);
|
|
442
|
-
expect(result.relations).toHaveLength(0);
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
it('returns empty results for an empty file', () => {
|
|
446
|
-
setFileContent('');
|
|
447
|
-
const result = parseSmartComments('/backend/empty.js');
|
|
448
|
-
expect(result.entities).toHaveLength(0);
|
|
449
|
-
expect(result.relations).toHaveLength(0);
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
it('ignores lines that are not @archsync comments', () => {
|
|
453
|
-
setFileContent(`
|
|
454
|
-
// This is a regular comment
|
|
455
|
-
const x = 1;
|
|
456
|
-
/* Another comment */
|
|
457
|
-
`);
|
|
458
|
-
const result = parseSmartComments('/backend/plain.js');
|
|
459
|
-
expect(result.entities).toHaveLength(0);
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
it('extracts multiple entities from a single file', () => {
|
|
463
|
-
setFileContent(`
|
|
464
|
-
// @archsync:entity service UserService
|
|
465
|
-
// @archsync:entity service OrderService
|
|
466
|
-
// @archsync:entity model UserModel
|
|
467
|
-
`);
|
|
468
|
-
const result = parseSmartComments('/backend/multi.js');
|
|
469
|
-
expect(result.entities).toHaveLength(3);
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
it('records the source line number in entity metadata', () => {
|
|
473
|
-
setFileContent(`
|
|
474
|
-
// @archsync:entity service LineTracked
|
|
475
|
-
`);
|
|
476
|
-
const result = parseSmartComments('/backend/lineno.js');
|
|
477
|
-
expect(result.entities[0].metadata.sourceLine).toBe(2);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
it('extracts @archsync:api with default path "/" when path is missing', () => {
|
|
481
|
-
setFileContent(`
|
|
482
|
-
// @archsync:api GET
|
|
483
|
-
`);
|
|
484
|
-
const result = parseSmartComments('/backend/route.js');
|
|
485
|
-
expect(result.entities[0].data.path).toBe('/');
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
it('sets scope via @archsync:scope directive on current entity', () => {
|
|
489
|
-
setFileContent(`
|
|
490
|
-
// @archsync:entity api PublicEndpoint
|
|
491
|
-
// @archsync:scope public
|
|
492
|
-
`);
|
|
493
|
-
const result = parseSmartComments('/backend/endpoints.js');
|
|
494
|
-
expect(result.entities[0].data.scope).toBe('public');
|
|
495
|
-
});
|
|
496
|
-
});
|