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.
Files changed (50) hide show
  1. package/README.md +67 -0
  2. package/dist/archsync.cjs +2 -0
  3. package/package.json +11 -7
  4. package/bin/cli.js +0 -91
  5. package/src/__tests__/e2e-workflow.test.js +0 -66
  6. package/src/__tests__/hashEngine.test.js +0 -109
  7. package/src/__tests__/impact.test.js +0 -137
  8. package/src/__tests__/parsers.test.js +0 -496
  9. package/src/__tests__/scan-pipeline.test.js +0 -332
  10. package/src/__tests__/schemaBuilder.test.js +0 -145
  11. package/src/__tests__/workspace.test.js +0 -178
  12. package/src/commands/backup.js +0 -54
  13. package/src/commands/connect.js +0 -129
  14. package/src/commands/diff.js +0 -228
  15. package/src/commands/export.js +0 -125
  16. package/src/commands/impactReport.js +0 -50
  17. package/src/commands/import.js +0 -126
  18. package/src/commands/init.js +0 -80
  19. package/src/commands/login.js +0 -116
  20. package/src/commands/plugin.js +0 -28
  21. package/src/commands/push.js +0 -194
  22. package/src/commands/register.js +0 -127
  23. package/src/commands/scan.js +0 -498
  24. package/src/commands/serve.js +0 -133
  25. package/src/commands/setup.js +0 -233
  26. package/src/commands/status.js +0 -56
  27. package/src/commands/validate.js +0 -245
  28. package/src/commands/watch.js +0 -70
  29. package/src/core/credentialStore.js +0 -76
  30. package/src/core/hashEngine.js +0 -34
  31. package/src/core/impactEngine.js +0 -192
  32. package/src/core/monorepoDetector.js +0 -41
  33. package/src/core/pluginManager.js +0 -40
  34. package/src/core/relationshipEngine.js +0 -917
  35. package/src/core/requestSigning.js +0 -16
  36. package/src/core/schemaBuilder.js +0 -230
  37. package/src/core/schemaDeduplicator.js +0 -54
  38. package/src/core/supabaseClient.js +0 -68
  39. package/src/core/workspaceDetector.js +0 -113
  40. package/src/parsers/astParser.js +0 -274
  41. package/src/parsers/configParser.js +0 -49
  42. package/src/parsers/dependencyGraph.js +0 -31
  43. package/src/parsers/flutterParser.js +0 -98
  44. package/src/parsers/goParser.js +0 -99
  45. package/src/parsers/index.js +0 -211
  46. package/src/parsers/javaParser.js +0 -89
  47. package/src/parsers/nodeParser.js +0 -429
  48. package/src/parsers/pythonParser.js +0 -109
  49. package/src/parsers/reactParser.js +0 -368
  50. package/src/parsers/smartComment.js +0 -144
@@ -1,332 +0,0 @@
1
- /**
2
- * Integration tests for the Scan → Push pipeline.
3
- *
4
- * Strategy:
5
- * - Write real fixture files to a temp directory using os.tmpdir()
6
- * - Invoke `parseFile` (used internally by scan) on them
7
- * - Run `buildSchema` to build the schema from parsed results
8
- * - Assert the schema has the expected nodes and edges
9
- * - Mock the `pushSchemaCommit` function and verify it receives the correct schema
10
- * - Clean up the temp directory after each test
11
- */
12
-
13
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
14
- import fs from 'fs';
15
- import path from 'path';
16
- import os from 'os';
17
- import { parseFile } from '../parsers/index.js';
18
- import { buildSchema } from '../core/schemaBuilder.js';
19
- import { hashSchema } from '../core/hashEngine.js';
20
-
21
- // ─── Temp directory helpers ──────────────────────────────────
22
-
23
- let tmpDir;
24
-
25
- function createTmpDir() {
26
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'archsync-test-'));
27
- return tmpDir;
28
- }
29
-
30
- function removeTmpDir() {
31
- if (tmpDir && fs.existsSync(tmpDir)) {
32
- fs.rmSync(tmpDir, { recursive: true, force: true });
33
- }
34
- }
35
-
36
- function writeTmpFile(relativePath, content) {
37
- const fullPath = path.join(tmpDir, relativePath);
38
- fs.mkdirSync(path.dirname(fullPath), { recursive: true });
39
- fs.writeFileSync(fullPath, content, 'utf-8');
40
- return fullPath;
41
- }
42
-
43
- // ─── Fixture source code ─────────────────────────────────────
44
-
45
- const REACT_PAGE = `
46
- import React from 'react';
47
- import Header from './Header';
48
-
49
- export default function DashboardPage() {
50
- return (
51
- <div>
52
- <Header />
53
- </div>
54
- );
55
- }
56
- `;
57
-
58
- const REACT_COMPONENT = `
59
- export function Header() {
60
- return <header>ArchSync</header>;
61
- }
62
- `;
63
-
64
- const EXPRESS_ROUTES = `
65
- const router = require('express').Router();
66
- router.get('/users', getUsers);
67
- router.post('/users', createUser);
68
- router.get('/users/:id', getUserById);
69
- router.delete('/users/:id', deleteUser);
70
- module.exports = router;
71
- `;
72
-
73
- const EXPRESS_SERVICE = `
74
- // /backend/services/userService.js
75
- export class UserService {
76
- async findAll() {}
77
- async findById(id) {}
78
- async create(data) {}
79
- }
80
- `;
81
-
82
- const EXPRESS_CONTROLLER = `
83
- // /backend/controllers/userController.js
84
- export const getUsers = async (req, res) => res.json([]);
85
- export const createUser = async (req, res) => res.json({});
86
- export const getUserById = async (req, res) => res.json({});
87
- export const deleteUser = async (req, res) => res.status(204).end();
88
- `;
89
-
90
- const MONGOOSE_MODEL = `
91
- import mongoose from 'mongoose';
92
- const userSchema = new mongoose.Schema({ name: String, email: String });
93
- const User = mongoose.model('User', userSchema);
94
- export default User;
95
- `;
96
-
97
- // ─── Tests ───────────────────────────────────────────────────
98
-
99
- describe('scan pipeline — parseFile on fixture files', () => {
100
- beforeEach(() => { createTmpDir(); });
101
- afterEach(() => { removeTmpDir(); });
102
-
103
- it('parseFile extracts React components from a .jsx page file', () => {
104
- const filePath = writeTmpFile('web/pages/DashboardPage.jsx', REACT_PAGE);
105
- const result = parseFile(filePath, { framework: 'react' });
106
-
107
- expect(result.entities.length).toBeGreaterThan(0);
108
- const comp = result.entities.find(e => e.name === 'DashboardPage');
109
- expect(comp).toBeDefined();
110
- expect(comp.entityType).toBe('screen');
111
- });
112
-
113
- it('parseFile extracts Express routes from a route file', () => {
114
- const filePath = writeTmpFile('backend/routes/userRoutes.js', EXPRESS_ROUTES);
115
- const result = parseFile(filePath, { framework: 'express' });
116
-
117
- const routes = result.entities.filter(e => e.entityType === 'route');
118
- expect(routes.length).toBeGreaterThanOrEqual(4);
119
- const methods = routes.map(r => r.data.method);
120
- expect(methods).toContain('GET');
121
- expect(methods).toContain('POST');
122
- expect(methods).toContain('DELETE');
123
- });
124
-
125
- it('parseFile extracts service classes from a service file', () => {
126
- const filePath = writeTmpFile('backend/services/userService.js', EXPRESS_SERVICE);
127
- const result = parseFile(filePath, { framework: 'node' });
128
-
129
- const svc = result.entities.find(e => e.name === 'UserService');
130
- expect(svc).toBeDefined();
131
- expect(svc.entityType).toBe('service');
132
- });
133
-
134
- it('parseFile extracts controller functions from a controller file', () => {
135
- const filePath = writeTmpFile('backend/controllers/userController.js', EXPRESS_CONTROLLER);
136
- const result = parseFile(filePath, { framework: 'node' });
137
-
138
- const controllers = result.entities.filter(e => e.entityType === 'controller');
139
- expect(controllers.length).toBeGreaterThanOrEqual(3);
140
- });
141
-
142
- it('parseFile extracts Mongoose model from a model file', () => {
143
- const filePath = writeTmpFile('backend/models/User.model.js', MONGOOSE_MODEL);
144
- const result = parseFile(filePath, { framework: 'node' });
145
-
146
- const model = result.entities.find(e => e.name === 'User');
147
- expect(model).toBeDefined();
148
- expect(model.entityType).toBe('model');
149
- });
150
- });
151
-
152
- describe('scan pipeline — buildSchema from multiple parsed files', () => {
153
- beforeEach(() => { createTmpDir(); });
154
- afterEach(() => { removeTmpDir(); });
155
-
156
- it('buildSchema produces a valid schema with nodes and meta', () => {
157
- const routesPath = writeTmpFile('backend/routes/userRoutes.js', EXPRESS_ROUTES);
158
- const modelPath = writeTmpFile('backend/models/User.model.js', MONGOOSE_MODEL);
159
-
160
- const parsed = [
161
- parseFile(routesPath, { framework: 'node' }),
162
- parseFile(modelPath, { framework: 'node' }),
163
- ];
164
-
165
- const schema = buildSchema(parsed);
166
- expect(schema).toHaveProperty('nodes');
167
- expect(schema).toHaveProperty('edges');
168
- expect(schema).toHaveProperty('meta');
169
- expect(schema.nodes.length).toBeGreaterThan(0);
170
- });
171
-
172
- it('buildSchema assigns stable (deterministic) IDs to nodes', () => {
173
- const filePath = writeTmpFile('backend/routes/userRoutes.js', EXPRESS_ROUTES);
174
- const parsed = [parseFile(filePath, { framework: 'node' })];
175
-
176
- const schema1 = buildSchema(parsed);
177
- const schema2 = buildSchema(parsed);
178
-
179
- const ids1 = schema1.nodes.map(n => n.id).sort();
180
- const ids2 = schema2.nodes.map(n => n.id).sort();
181
- expect(ids1).toEqual(ids2);
182
- });
183
-
184
- it('buildSchema deduplicates nodes with identical entityType/name/system', () => {
185
- const routesPath = writeTmpFile('backend/routes/userRoutes.js', EXPRESS_ROUTES);
186
-
187
- // Parse the same file twice (simulates duplicate detection)
188
- const p1 = parseFile(routesPath, { framework: 'node' });
189
- const p2 = parseFile(routesPath, { framework: 'node' });
190
-
191
- const schema = buildSchema([p1, p2]);
192
- const uniqueIds = new Set(schema.nodes.map(n => n.id));
193
- expect(uniqueIds.size).toBe(schema.nodes.length);
194
- });
195
-
196
- it('buildSchema meta reflects correct node and file counts', () => {
197
- const routesPath = writeTmpFile('backend/routes/userRoutes.js', EXPRESS_ROUTES);
198
- const modelPath = writeTmpFile('backend/models/User.model.js', MONGOOSE_MODEL);
199
-
200
- const parsed = [
201
- parseFile(routesPath, { framework: 'node' }),
202
- parseFile(modelPath, { framework: 'node' }),
203
- ];
204
-
205
- const schema = buildSchema(parsed);
206
- expect(schema.meta.fileCount).toBe(2);
207
- expect(schema.meta.nodeCount).toBe(schema.nodes.length);
208
- expect(schema.meta.edgeCount).toBe(schema.edges.length);
209
- });
210
-
211
- it('each schema node has required fields: id, entityType, system, text', () => {
212
- const filePath = writeTmpFile('backend/routes/userRoutes.js', EXPRESS_ROUTES);
213
- const schema = buildSchema([parseFile(filePath, { framework: 'node' })]);
214
-
215
- for (const node of schema.nodes) {
216
- expect(node).toHaveProperty('id');
217
- expect(node).toHaveProperty('entityType');
218
- expect(node).toHaveProperty('system');
219
- expect(node).toHaveProperty('text');
220
- }
221
- });
222
-
223
- it('buildSchema for an empty parsed-files list returns empty schema', () => {
224
- const schema = buildSchema([]);
225
- expect(schema.nodes).toHaveLength(0);
226
- expect(schema.edges).toHaveLength(0);
227
- });
228
- });
229
-
230
- describe('scan pipeline — hashSchema', () => {
231
- it('produces a consistent hash for the same schema', () => {
232
- const schema = {
233
- nodes: [
234
- { entityType: 'route', system: 'backend', text: 'GET /users', data: {}, metadata: {} },
235
- ],
236
- edges: [],
237
- };
238
- expect(hashSchema(schema)).toBe(hashSchema(schema));
239
- });
240
-
241
- it('produces different hashes for different schemas', () => {
242
- const schema1 = {
243
- nodes: [{ entityType: 'route', system: 'backend', text: 'GET /users', data: {}, metadata: {} }],
244
- edges: [],
245
- };
246
- const schema2 = {
247
- nodes: [{ entityType: 'service', system: 'backend', text: 'UserService', data: {}, metadata: {} }],
248
- edges: [],
249
- };
250
- expect(hashSchema(schema1)).not.toBe(hashSchema(schema2));
251
- });
252
-
253
- it('returns a hex string of length 64 (sha256)', () => {
254
- const schema = { nodes: [], edges: [] };
255
- const hash = hashSchema(schema);
256
- expect(hash).toMatch(/^[a-f0-9]{64}$/);
257
- });
258
- });
259
-
260
- describe('scan pipeline — push mock', () => {
261
- beforeEach(() => { createTmpDir(); });
262
- afterEach(() => { removeTmpDir(); });
263
-
264
- it('mock pushSchemaCommit is called with the built schema', async () => {
265
- const filePath = writeTmpFile('backend/routes/userRoutes.js', EXPRESS_ROUTES);
266
- const schema = buildSchema([parseFile(filePath, { framework: 'node' })]);
267
-
268
- // Mock the push function
269
- const pushMock = vi.fn().mockResolvedValue({ id: 'commit-abc-123', branch: 'main' });
270
-
271
- const result = await pushMock('mock-project-id', 'main', schema, 'Test push');
272
-
273
- expect(pushMock).toHaveBeenCalledTimes(1);
274
- expect(pushMock).toHaveBeenCalledWith(
275
- 'mock-project-id',
276
- 'main',
277
- schema,
278
- 'Test push'
279
- );
280
- expect(result.id).toBe('commit-abc-123');
281
- });
282
-
283
- it('push receives schema with correct node count for a React + Express project', () => {
284
- const reactPath = writeTmpFile('web/pages/DashboardPage.jsx', REACT_PAGE);
285
- const routesPath = writeTmpFile('backend/routes/userRoutes.js', EXPRESS_ROUTES);
286
- const modelPath = writeTmpFile('backend/models/User.model.js', MONGOOSE_MODEL);
287
-
288
- const parsed = [
289
- parseFile(reactPath, { framework: 'react' }),
290
- parseFile(routesPath, { framework: 'node' }),
291
- parseFile(modelPath, { framework: 'node' }),
292
- ];
293
-
294
- const schema = buildSchema(parsed);
295
- const pushMock = vi.fn();
296
-
297
- pushMock(schema);
298
-
299
- const receivedSchema = pushMock.mock.calls[0][0];
300
- // Should have nodes from React (screen), routes, and model
301
- expect(receivedSchema.nodes.length).toBeGreaterThan(3);
302
-
303
- const types = receivedSchema.nodes.map(n => n.entityType);
304
- expect(types).toContain('screen');
305
- expect(types).toContain('route');
306
- expect(types).toContain('model');
307
- });
308
-
309
- it('schema passed to push includes a generated hash', () => {
310
- const filePath = writeTmpFile('backend/routes/userRoutes.js', EXPRESS_ROUTES);
311
- const schema = buildSchema([parseFile(filePath, { framework: 'node' })]);
312
- schema.hash = hashSchema(schema);
313
-
314
- const pushMock = vi.fn();
315
- pushMock(schema);
316
-
317
- const receivedSchema = pushMock.mock.calls[0][0];
318
- expect(receivedSchema.hash).toBeTruthy();
319
- expect(receivedSchema.hash).toMatch(/^[a-f0-9]{64}$/);
320
- });
321
-
322
- it('push is not called when schema has zero nodes (guard check)', () => {
323
- const emptySchema = buildSchema([]);
324
- const pushMock = vi.fn();
325
-
326
- if (emptySchema.nodes.length > 0) {
327
- pushMock(emptySchema);
328
- }
329
-
330
- expect(pushMock).not.toHaveBeenCalled();
331
- });
332
- });
@@ -1,145 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { buildSchema, diffSchemas } from '../core/schemaBuilder.js';
3
-
4
- // ─── Helpers ────────────────────────────────────────────────
5
- function makeFileResult(filePath, entities = [], relations = []) {
6
- return { filePath, entities, relations };
7
- }
8
-
9
- function makeEntity(name, entityType, system = 'backend') {
10
- return { name, entityType, system };
11
- }
12
-
13
- // ─── buildSchema ─────────────────────────────────────────────
14
- describe('buildSchema', () => {
15
- it('returns a schema with empty nodes and edges for no parsed files', () => {
16
- const schema = buildSchema([]);
17
- expect(schema.nodes).toEqual([]);
18
- expect(schema.edges).toEqual([]);
19
- expect(schema.meta.nodeCount).toBe(0);
20
- });
21
-
22
- it('builds the correct node count from parsed files', () => {
23
- const files = [
24
- makeFileResult('src/auth.js', [
25
- makeEntity('AuthService', 'service'),
26
- makeEntity('AuthController', 'controller'),
27
- ]),
28
- makeFileResult('src/user.js', [
29
- makeEntity('UserService', 'service'),
30
- ]),
31
- ];
32
- const schema = buildSchema(files);
33
- expect(schema.nodes).toHaveLength(3);
34
- expect(schema.meta.nodeCount).toBe(3);
35
- expect(schema.meta.fileCount).toBe(2);
36
- });
37
-
38
- it('deduplicates same-named entities within one file, keeps them distinct across files', () => {
39
- const duplicateEntity = makeEntity('SharedService', 'service', 'backend');
40
- // Same file → one node
41
- const sameFile = buildSchema([
42
- makeFileResult('src/a.js', [duplicateEntity, { ...duplicateEntity }]),
43
- ]);
44
- expect(sameFile.nodes).toHaveLength(1);
45
- // Different files → two nodes (two `main.py`/`index.js` files must
46
- // never merge into one)
47
- const twoFiles = buildSchema([
48
- makeFileResult('src/a.js', [duplicateEntity]),
49
- makeFileResult('src/b.js', [{ ...duplicateEntity }]),
50
- ]);
51
- expect(twoFiles.nodes).toHaveLength(2);
52
- });
53
-
54
- it('still merges name-identity types (api/route) across files', () => {
55
- const apiRef = makeEntity('GET /api/users', 'api', 'backend');
56
- const schema = buildSchema([
57
- makeFileResult('src/a.js', [apiRef]),
58
- makeFileResult('src/b.js', [{ ...apiRef }]),
59
- ]);
60
- expect(schema.nodes).toHaveLength(1);
61
- });
62
-
63
- it('assigns the entity system from the entity data', () => {
64
- const files = [
65
- makeFileResult('src/app/Screen.jsx', [makeEntity('HomeScreen', 'screen', 'app')]),
66
- makeFileResult('src/backend/svc.js', [makeEntity('UserService', 'service', 'backend')]),
67
- ];
68
- const schema = buildSchema(files);
69
- const homeScreen = schema.nodes.find(n => n.text === 'HomeScreen');
70
- const userService = schema.nodes.find(n => n.text === 'UserService');
71
- expect(homeScreen.system).toBe('app');
72
- expect(userService.system).toBe('backend');
73
- });
74
-
75
- it('falls back to config.defaultSystem when entity has no system', () => {
76
- const files = [
77
- makeFileResult('src/x.js', [{ name: 'UnknownService', entityType: 'service' }]),
78
- ];
79
- const schema = buildSchema(files, { defaultSystem: 'web' });
80
- expect(schema.nodes[0].system).toBe('web');
81
- });
82
-
83
- it('falls back to "backend" when no system and no config.defaultSystem', () => {
84
- const files = [
85
- makeFileResult('src/x.js', [{ name: 'GenericService', entityType: 'service' }]),
86
- ];
87
- const schema = buildSchema(files);
88
- expect(schema.nodes[0].system).toBe('backend');
89
- });
90
-
91
- it('assigns stable deterministic IDs (same input = same ID)', () => {
92
- const files = [makeFileResult('src/svc.js', [makeEntity('MyService', 'service', 'backend')])];
93
- const schema1 = buildSchema(files);
94
- const schema2 = buildSchema(files);
95
- expect(schema1.nodes[0].id).toBe(schema2.nodes[0].id);
96
- });
97
-
98
- it('stores the source file path in node metadata', () => {
99
- const files = [makeFileResult('src/auth.js', [makeEntity('AuthService', 'service')])];
100
- const schema = buildSchema(files);
101
- expect(schema.nodes[0].metadata.sourceFile).toBe('src/auth.js');
102
- });
103
-
104
- it('includes generated meta fields', () => {
105
- const schema = buildSchema([]);
106
- expect(schema.meta.generatedAt).toBeDefined();
107
- expect(schema.meta.source).toBe('archsync-cli');
108
- });
109
- });
110
-
111
- // ─── diffSchemas ─────────────────────────────────────────────
112
- describe('diffSchemas', () => {
113
- it('returns all nodes as added when base is null', () => {
114
- const incoming = {
115
- nodes: [{ id: 'n1', text: 'A', entityType: 'service', data: {} }],
116
- };
117
- const diff = diffSchemas(null, incoming);
118
- expect(diff.added).toHaveLength(1);
119
- expect(diff.added[0].status).toBe('added');
120
- });
121
-
122
- it('returns all nodes as deleted when incoming is null', () => {
123
- const base = {
124
- nodes: [{ id: 'n1', text: 'A', entityType: 'service', data: {} }],
125
- };
126
- const diff = diffSchemas(base, null);
127
- expect(diff.deleted).toHaveLength(1);
128
- expect(diff.deleted[0].status).toBe('deleted');
129
- });
130
-
131
- it('detects unchanged nodes', () => {
132
- const node = { id: 'n1', text: 'A', entityType: 'service', data: {} };
133
- const schema = { nodes: [node] };
134
- const diff = diffSchemas(schema, schema);
135
- expect(diff.unchanged).toHaveLength(1);
136
- });
137
-
138
- it('detects modified nodes when text changes', () => {
139
- const base = { nodes: [{ id: 'n1', text: 'OldName', entityType: 'service', data: {} }] };
140
- const incoming = { nodes: [{ id: 'n1', text: 'NewName', entityType: 'service', data: {} }] };
141
- const diff = diffSchemas(base, incoming);
142
- expect(diff.modified).toHaveLength(1);
143
- expect(diff.modified[0].text).toBe('NewName');
144
- });
145
- });
@@ -1,178 +0,0 @@
1
- /**
2
- * End-to-end test for multi-project workspace scanning:
3
- * a Node/Express backend, a Flutter app, and a React admin side-by-side,
4
- * scanned together and cross-linked by API method+path.
5
- */
6
-
7
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
8
- import fs from 'fs';
9
- import path from 'path';
10
- import os from 'os';
11
- import { detectWorkspaceProjects, detectProjectFramework, includePatternsFor } from '../core/workspaceDetector.js';
12
- import { parseFile } from '../parsers/index.js';
13
- import { buildRelationships } from '../core/relationshipEngine.js';
14
- import { buildSchema } from '../core/schemaBuilder.js';
15
-
16
- let root;
17
-
18
- function write(rel, content) {
19
- const full = path.join(root, rel);
20
- fs.mkdirSync(path.dirname(full), { recursive: true });
21
- fs.writeFileSync(full, content);
22
- }
23
-
24
- beforeAll(() => {
25
- root = fs.mkdtempSync(path.join(os.tmpdir(), 'archsync-ws-'));
26
-
27
- // ── Node/Express backend ──────────────────────────────
28
- write('backend/package.json', JSON.stringify({ name: 'be', dependencies: { express: '^4.0.0' } }));
29
- write('backend/server.js', `
30
- const express = require('express');
31
- const userRoutes = require('./routes/user.routes');
32
- const app = express();
33
- app.use('/api/users', userRoutes);
34
- app.listen(3000);
35
- `);
36
- write('backend/routes/user.routes.js', `
37
- const express = require('express');
38
- const { listUsers, getUser } = require('../controllers/user.controller');
39
- const router = express.Router();
40
- router.get('/', listUsers);
41
- router.get('/:id', getUser);
42
- module.exports = router;
43
- `);
44
- write('backend/controllers/user.controller.js', `
45
- const UserService = require('../services/user.service');
46
- async function listUsers(req, res) { res.json(await UserService.findAll()); }
47
- async function getUser(req, res) { res.json(await UserService.findById(req.params.id)); }
48
- module.exports = { listUsers, getUser };
49
- `);
50
- write('backend/services/user.service.js', `
51
- const User = require('../models/user.model');
52
- class UserService {
53
- static findAll() { return User.find({}); }
54
- static findById(id) { return User.findById(id); }
55
- }
56
- module.exports = UserService;
57
- `);
58
- write('backend/models/user.model.js', `
59
- const mongoose = require('mongoose');
60
- module.exports = mongoose.model('User', new mongoose.Schema({ email: String }));
61
- `);
62
-
63
- // ── Flutter app ───────────────────────────────────────
64
- write('mobile_app/pubspec.yaml', 'name: demo_mobile\n');
65
- write('mobile_app/lib/screens/profile_screen.dart', `
66
- import 'package:http/http.dart' as http;
67
- class ProfileScreen extends StatelessWidget {
68
- Future<void> load(String id) async {
69
- await http.get(Uri.parse('https://api.example.com/api/users/\$id'));
70
- }
71
- }
72
- `);
73
-
74
- // ── React admin ───────────────────────────────────────
75
- write('admin/package.json', JSON.stringify({ name: 'admin', dependencies: { react: '^18.0.0', axios: '^1.0.0' } }));
76
- write('admin/src/pages/UsersPage.jsx', `
77
- import axios from 'axios';
78
- export default function UsersPage() {
79
- const load = () => axios.get('/api/users');
80
- return <div onClick={load}>users</div>;
81
- }
82
- `);
83
- });
84
-
85
- afterAll(() => {
86
- fs.rmSync(root, { recursive: true, force: true });
87
- });
88
-
89
- describe('workspaceDetector', () => {
90
- it('detects all three project types with correct frameworks and systems', () => {
91
- const projects = detectWorkspaceProjects(root);
92
- const byName = Object.fromEntries(projects.map(p => [p.name, p]));
93
-
94
- expect(byName.backend).toMatchObject({ framework: 'express', system: 'backend' });
95
- expect(byName.mobile_app).toMatchObject({ framework: 'flutter', system: 'mobile_app' });
96
- expect(byName.admin).toMatchObject({ framework: 'react', system: 'admin' });
97
- });
98
-
99
- it('classifies single project dirs', () => {
100
- expect(detectProjectFramework(path.join(root, 'backend'))).toBe('express');
101
- expect(detectProjectFramework(path.join(root, 'mobile_app'))).toBe('flutter');
102
- expect(detectProjectFramework(path.join(root, 'admin'))).toBe('react');
103
- expect(detectProjectFramework(root)).toBeNull();
104
- });
105
-
106
- it('scopes flutter scans to lib/**.dart', () => {
107
- expect(includePatternsFor('flutter')).toEqual(['lib/**/*.dart']);
108
- });
109
- });
110
-
111
- describe('workspace scan pipeline', () => {
112
- function scanWorkspace() {
113
- const projects = detectWorkspaceProjects(root);
114
- const allParsed = [];
115
- const exts = { express: /\.(js|ts)$/, flutter: /\.dart$/, react: /\.(jsx?|tsx?)$/ };
116
-
117
- for (const project of projects) {
118
- const stack = [project.path];
119
- while (stack.length) {
120
- const dir = stack.pop();
121
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
122
- const full = path.join(dir, entry.name);
123
- if (entry.isDirectory()) { stack.push(full); continue; }
124
- if (!exts[project.framework].test(entry.name)) continue;
125
- const result = parseFile(full, { framework: project.framework });
126
- if (result.entities.length || result.relations.length) {
127
- for (const e of result.entities) e.system = project.system;
128
- allParsed.push(result);
129
- }
130
- }
131
- }
132
- }
133
-
134
- const relations = buildRelationships(allParsed);
135
- return buildSchema(allParsed, { defaultSystem: 'backend' }, relations);
136
- }
137
-
138
- it('produces a multi-system schema with no dangling edges', () => {
139
- const schema = scanWorkspace();
140
-
141
- expect(schema.multiSystem).toBe(true);
142
- expect(schema.systems.sort()).toEqual(['admin', 'backend', 'mobile_app']);
143
-
144
- const ids = new Set(schema.nodes.map(n => n.id));
145
- for (const edge of schema.edges) {
146
- expect(ids.has(edge.source)).toBe(true);
147
- expect(ids.has(edge.target)).toBe(true);
148
- }
149
- });
150
-
151
- it('composes public route paths from app.use() mounts', () => {
152
- const schema = scanWorkspace();
153
- const routeTexts = schema.nodes.filter(n => n.entityType === 'route').map(n => n.text);
154
- expect(routeTexts).toContain('GET /api/users');
155
- expect(routeTexts).toContain('GET /api/users/:id');
156
- });
157
-
158
- it('cross-links the Flutter app and React admin to backend routes', () => {
159
- const schema = scanWorkspace();
160
- const byId = new Map(schema.nodes.map(n => [n.id, n]));
161
- const cross = schema.edges
162
- .filter(e => e.isCrossSystem)
163
- .map(e => `${byId.get(e.source).system}:${byId.get(e.source).text} -> ${byId.get(e.target).system}:${byId.get(e.target).text}`);
164
-
165
- expect(cross).toContain('admin:GET /api/users -> backend:GET /api/users');
166
- expect(cross.some(c => c.startsWith('mobile_app:GET /api/users/') && c.endsWith('backend:GET /api/users/:id'))).toBe(true);
167
- });
168
-
169
- it('keeps the backend chain route → controller → service → model intact', () => {
170
- const schema = scanWorkspace();
171
- const byId = new Map(schema.nodes.map(n => [n.id, n]));
172
- const rels = schema.edges.map(e => `${byId.get(e.source).text} -[${e.relation}]-> ${byId.get(e.target).text}`);
173
-
174
- expect(rels).toContain('GET /api/users -[handles]-> listUsers');
175
- expect(rels).toContain('listUsers -[uses]-> UserService');
176
- expect(rels).toContain('UserService -[queries]-> User');
177
- });
178
- });