archsync 1.0.0 → 1.0.1
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 +8 -4
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archsync",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "ArchSync CLI — Sync codebase to architecture graph",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -23,20 +23,24 @@
|
|
|
23
23
|
"archsync"
|
|
24
24
|
],
|
|
25
25
|
"bin": {
|
|
26
|
-
"archsync": "
|
|
26
|
+
"archsync": "dist/archsync.cjs"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
|
-
"
|
|
30
|
-
"
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md"
|
|
31
31
|
],
|
|
32
32
|
"scripts": {
|
|
33
33
|
"start": "node bin/cli.js",
|
|
34
34
|
"dev": "node bin/cli.js",
|
|
35
|
+
"build": "node build.mjs",
|
|
36
|
+
"prepublishOnly": "npm run build",
|
|
35
37
|
"test": "vitest",
|
|
36
38
|
"test:run": "vitest run",
|
|
37
39
|
"coverage": "vitest run --coverage"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
42
|
+
"esbuild": "^0.24.0",
|
|
43
|
+
"javascript-obfuscator": "^4.1.1",
|
|
40
44
|
"vitest": "^3.2.0",
|
|
41
45
|
"@vitest/coverage-v8": "^3.2.0"
|
|
42
46
|
},
|
package/bin/cli.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { initCommand } from '../src/commands/init.js';
|
|
5
|
-
import { scanCommand } from '../src/commands/scan.js';
|
|
6
|
-
import { pushCommand } from '../src/commands/push.js';
|
|
7
|
-
import { diffCommand } from '../src/commands/diff.js';
|
|
8
|
-
import { watchCommand } from '../src/commands/watch.js';
|
|
9
|
-
import { statusCommand } from '../src/commands/status.js';
|
|
10
|
-
import { serveCommand } from '../src/commands/serve.js';
|
|
11
|
-
|
|
12
|
-
const program = new Command();
|
|
13
|
-
|
|
14
|
-
program
|
|
15
|
-
.name('archsync')
|
|
16
|
-
.description(chalk.bold.blue('ArchSync CLI') + ' — Sync codebase to architecture graph')
|
|
17
|
-
.version('1.0.0');
|
|
18
|
-
|
|
19
|
-
program
|
|
20
|
-
.command('serve')
|
|
21
|
-
.description('Start the localhost-only code bridge so the canvas can show live source code')
|
|
22
|
-
.option('-d, --dir <path>', 'Project directory (must contain .archsync-schema.json)', '.')
|
|
23
|
-
.option('-p, --port <port>', 'Port to bind on 127.0.0.1', '4317')
|
|
24
|
-
.action(serveCommand);
|
|
25
|
-
|
|
26
|
-
program
|
|
27
|
-
.command('init')
|
|
28
|
-
.description('Initialize ArchSync in the current project')
|
|
29
|
-
.option('-p, --project <id>', 'Supabase project ID to link')
|
|
30
|
-
.option('-f, --framework <type>', 'Framework: node, flutter, react, express, nestjs, nextjs')
|
|
31
|
-
.option('-s, --system <label>', 'System label for cross-system merging: backend, mobile, web, admin (overrides the framework default)')
|
|
32
|
-
.action(initCommand);
|
|
33
|
-
|
|
34
|
-
program
|
|
35
|
-
.command('scan')
|
|
36
|
-
.description('Scan codebase and build schema from source')
|
|
37
|
-
.option('-d, --dir <path>', 'Directory to scan', '.')
|
|
38
|
-
.option('--include <patterns>', 'Glob patterns to include', '**/*.{js,ts,jsx,tsx,dart,py,go,java,kt}')
|
|
39
|
-
.option('--exclude <patterns>', 'Glob patterns to exclude', '**/node_modules/**')
|
|
40
|
-
.option('--ast', 'Use AST parsing (slower, more accurate)', false)
|
|
41
|
-
.option('--debug', 'Show detailed debug output (entity sources, relation tracing)', false)
|
|
42
|
-
.option('-w, --workspace', 'Scan a multi-project workspace (e.g. backend + flutter app + react admin) and cross-link systems', false)
|
|
43
|
-
.action(scanCommand);
|
|
44
|
-
|
|
45
|
-
program
|
|
46
|
-
.command('push')
|
|
47
|
-
.description('Push local schema to Supabase')
|
|
48
|
-
.option('-b, --branch <name>', 'Branch name', 'main')
|
|
49
|
-
.option('-m, --message <msg>', 'Commit message')
|
|
50
|
-
.option('--dry-run', 'Show what would be pushed without pushing')
|
|
51
|
-
.action(pushCommand);
|
|
52
|
-
|
|
53
|
-
program
|
|
54
|
-
.command('diff')
|
|
55
|
-
.description('Show diff between local and remote schema')
|
|
56
|
-
.option('-b, --branch <name>', 'Branch to compare against', 'main')
|
|
57
|
-
.option('--json', 'Output as JSON')
|
|
58
|
-
.action(diffCommand);
|
|
59
|
-
|
|
60
|
-
program
|
|
61
|
-
.command('watch')
|
|
62
|
-
.description('Watch for file changes and auto-sync')
|
|
63
|
-
.option('-d, --dir <path>', 'Directory to watch', '.')
|
|
64
|
-
.option('--debounce <ms>', 'Debounce interval in ms', '2000')
|
|
65
|
-
.action(watchCommand);
|
|
66
|
-
|
|
67
|
-
program
|
|
68
|
-
.command('status')
|
|
69
|
-
.description('Show current sync status')
|
|
70
|
-
.action(statusCommand);
|
|
71
|
-
|
|
72
|
-
// Don't exit with code 1 when no command is provided — just show help
|
|
73
|
-
program.exitOverride();
|
|
74
|
-
try {
|
|
75
|
-
if (!process.argv.slice(2).length) {
|
|
76
|
-
program.outputHelp();
|
|
77
|
-
} else {
|
|
78
|
-
program.parse();
|
|
79
|
-
}
|
|
80
|
-
} catch (err) {
|
|
81
|
-
// Commander throws on --help / --version — treat as clean exits
|
|
82
|
-
const cleanCodes = ['commander.help', 'commander.helpDisplayed', 'commander.version'];
|
|
83
|
-
if (cleanCodes.includes(err.code)) {
|
|
84
|
-
process.exit(0);
|
|
85
|
-
}
|
|
86
|
-
if (err.code === 'commander.unknownCommand') {
|
|
87
|
-
console.error(chalk.red(`Unknown command. Run 'archsync --help' to see available commands.`));
|
|
88
|
-
process.exit(1);
|
|
89
|
-
}
|
|
90
|
-
throw err;
|
|
91
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import os from 'os';
|
|
5
|
-
|
|
6
|
-
let tmpDir;
|
|
7
|
-
|
|
8
|
-
beforeAll(() => {
|
|
9
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'archsync-e2e-'));
|
|
10
|
-
// Create fixture files
|
|
11
|
-
fs.writeFileSync(path.join(tmpDir, 'App.jsx'), `
|
|
12
|
-
import React from 'react';
|
|
13
|
-
import UserService from './UserService';
|
|
14
|
-
export default function App() { return <div>Hello</div>; }
|
|
15
|
-
`);
|
|
16
|
-
fs.writeFileSync(path.join(tmpDir, 'UserService.js'), `
|
|
17
|
-
const db = require('./db');
|
|
18
|
-
class UserService {
|
|
19
|
-
async getUser(id) { return db.users.findById(id); }
|
|
20
|
-
}
|
|
21
|
-
module.exports = UserService;
|
|
22
|
-
`);
|
|
23
|
-
fs.writeFileSync(path.join(tmpDir, 'server.js'), `
|
|
24
|
-
const express = require('express');
|
|
25
|
-
const app = express();
|
|
26
|
-
app.get('/api/users', (req, res) => res.json([]));
|
|
27
|
-
app.post('/api/users', (req, res) => res.json({}));
|
|
28
|
-
module.exports = app;
|
|
29
|
-
`);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
afterAll(() => {
|
|
33
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
describe('CLI Scan → Build Pipeline E2E', () => {
|
|
37
|
-
it('scans fixture directory and produces schema', async () => {
|
|
38
|
-
// Import parsers
|
|
39
|
-
const { parseFile } = await import('../../src/parsers/index.js');
|
|
40
|
-
const { buildSchema } = await import('../../src/core/schemaBuilder.js');
|
|
41
|
-
|
|
42
|
-
const files = fs.readdirSync(tmpDir).map(f => path.join(tmpDir, f));
|
|
43
|
-
const parsed = files.flatMap(f => {
|
|
44
|
-
try {
|
|
45
|
-
const content = fs.readFileSync(f, 'utf8');
|
|
46
|
-
const result = parseFile(f, content);
|
|
47
|
-
return result ? [result] : [];
|
|
48
|
-
} catch { return []; }
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const schema = buildSchema(parsed, tmpDir);
|
|
52
|
-
expect(schema).toBeDefined();
|
|
53
|
-
expect(schema.nodes).toBeDefined();
|
|
54
|
-
expect(Array.isArray(schema.nodes)).toBe(true);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('produces deterministic schema hash', async () => {
|
|
58
|
-
const { hashSchema } = await import('../../src/core/hashEngine.js');
|
|
59
|
-
const schema = { nodes: [{ id: '1', name: 'Test', type: 'service' }], edges: [] };
|
|
60
|
-
const hash1 = hashSchema(schema);
|
|
61
|
-
const hash2 = hashSchema(schema);
|
|
62
|
-
expect(hash1).toBe(hash2);
|
|
63
|
-
expect(typeof hash1).toBe('string');
|
|
64
|
-
expect(hash1.length).toBeGreaterThan(0);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { hashNode, hashSchema, schemasEqual } from '../core/hashEngine.js';
|
|
3
|
-
|
|
4
|
-
// ─── hashNode ────────────────────────────────────────────────
|
|
5
|
-
describe('hashNode', () => {
|
|
6
|
-
it('produces a 16-character hex string', () => {
|
|
7
|
-
const node = { entityType: 'service', system: 'backend', text: 'AuthService', data: {}, metadata: {} };
|
|
8
|
-
const hash = hashNode(node);
|
|
9
|
-
expect(typeof hash).toBe('string');
|
|
10
|
-
expect(hash).toHaveLength(16);
|
|
11
|
-
expect(hash).toMatch(/^[0-9a-f]+$/);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('identical nodes produce the same hash', () => {
|
|
15
|
-
const node = { entityType: 'service', system: 'backend', text: 'AuthService', data: {}, metadata: {} };
|
|
16
|
-
expect(hashNode(node)).toBe(hashNode(node));
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('different nodes produce different hashes', () => {
|
|
20
|
-
const a = { entityType: 'service', system: 'backend', text: 'AuthService', data: {}, metadata: {} };
|
|
21
|
-
const b = { entityType: 'service', system: 'backend', text: 'PaymentService', data: {}, metadata: {} };
|
|
22
|
-
expect(hashNode(a)).not.toBe(hashNode(b));
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('missing data/metadata defaults to empty objects and still hashes', () => {
|
|
26
|
-
const node = { entityType: 'model', system: 'backend', text: 'User' };
|
|
27
|
-
expect(() => hashNode(node)).not.toThrow();
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
// ─── hashSchema ──────────────────────────────────────────────
|
|
32
|
-
describe('hashSchema', () => {
|
|
33
|
-
const baseSchema = {
|
|
34
|
-
nodes: [
|
|
35
|
-
{ entityType: 'service', system: 'backend', text: 'AuthService', data: {}, metadata: {} },
|
|
36
|
-
],
|
|
37
|
-
edges: [
|
|
38
|
-
{ source: 'n1', relation: 'calls', target: 'n2' },
|
|
39
|
-
],
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
it('identical schemas produce the same hash', () => {
|
|
43
|
-
const hash1 = hashSchema(baseSchema);
|
|
44
|
-
const hash2 = hashSchema(baseSchema);
|
|
45
|
-
expect(hash1).toBe(hash2);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('adding a node changes the schema hash', () => {
|
|
49
|
-
const extended = {
|
|
50
|
-
...baseSchema,
|
|
51
|
-
nodes: [
|
|
52
|
-
...baseSchema.nodes,
|
|
53
|
-
{ entityType: 'model', system: 'backend', text: 'UserModel', data: {}, metadata: {} },
|
|
54
|
-
],
|
|
55
|
-
};
|
|
56
|
-
expect(hashSchema(baseSchema)).not.toBe(hashSchema(extended));
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('hash is stable regardless of node property insertion order', () => {
|
|
60
|
-
const schema1 = {
|
|
61
|
-
nodes: [{ entityType: 'service', system: 'backend', text: 'A', data: {}, metadata: {} }],
|
|
62
|
-
edges: [],
|
|
63
|
-
};
|
|
64
|
-
// Same logical content, different property order when serialised
|
|
65
|
-
const schema2 = {
|
|
66
|
-
nodes: [{ text: 'A', entityType: 'service', system: 'backend', metadata: {}, data: {} }],
|
|
67
|
-
edges: [],
|
|
68
|
-
};
|
|
69
|
-
// hashNode uses JSON.stringify with fixed key order — both schemas are equivalent
|
|
70
|
-
expect(hashSchema(schema1)).toBe(hashSchema(schema2));
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('produces a non-empty string for an empty schema', () => {
|
|
74
|
-
const hash = hashSchema({ nodes: [], edges: [] });
|
|
75
|
-
expect(typeof hash).toBe('string');
|
|
76
|
-
expect(hash.length).toBeGreaterThan(0);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('handles schema without edges gracefully', () => {
|
|
80
|
-
const schema = { nodes: [{ entityType: 'service', system: 'backend', text: 'X', data: {}, metadata: {} }] };
|
|
81
|
-
expect(() => hashSchema(schema)).not.toThrow();
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// ─── schemasEqual ────────────────────────────────────────────
|
|
86
|
-
describe('schemasEqual', () => {
|
|
87
|
-
it('two null schemas are equal', () => {
|
|
88
|
-
expect(schemasEqual(null, null)).toBe(true);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('null vs non-null schema is not equal', () => {
|
|
92
|
-
expect(schemasEqual(null, { nodes: [], edges: [] })).toBe(false);
|
|
93
|
-
expect(schemasEqual({ nodes: [], edges: [] }, null)).toBe(false);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('identical schemas are equal', () => {
|
|
97
|
-
const schema = {
|
|
98
|
-
nodes: [{ entityType: 'service', system: 'backend', text: 'Svc', data: {}, metadata: {} }],
|
|
99
|
-
edges: [],
|
|
100
|
-
};
|
|
101
|
-
expect(schemasEqual(schema, schema)).toBe(true);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('schemas with different nodes are not equal', () => {
|
|
105
|
-
const a = { nodes: [{ entityType: 'service', system: 'backend', text: 'A', data: {}, metadata: {} }], edges: [] };
|
|
106
|
-
const b = { nodes: [{ entityType: 'service', system: 'backend', text: 'B', data: {}, metadata: {} }], edges: [] };
|
|
107
|
-
expect(schemasEqual(a, b)).toBe(false);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the impact engine: breaking-change classification and
|
|
3
|
-
* transitive, cross-system dependent discovery.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect } from 'vitest';
|
|
7
|
-
import { analyzeImpact, findDependents, embedWarnings } from '../core/impactEngine.js';
|
|
8
|
-
|
|
9
|
-
// screen(app) → api(app) → route(backend) → controller(backend)
|
|
10
|
-
// service(backend) → model(backend)
|
|
11
|
-
function makeSchema() {
|
|
12
|
-
const nodes = [
|
|
13
|
-
{ id: 'screen1', entityType: 'screen', system: 'app', text: 'LoginScreen', data: {} },
|
|
14
|
-
{ id: 'api1', entityType: 'api', system: 'app', text: 'POST /api/login', data: { method: 'POST', path: '/api/login' } },
|
|
15
|
-
{ id: 'route1', entityType: 'route', system: 'backend', text: 'POST /api/login', data: { method: 'POST', path: '/login', fullPath: '/api/login' } },
|
|
16
|
-
{ id: 'ctrl1', entityType: 'controller', system: 'backend', text: 'login', data: {} },
|
|
17
|
-
{ id: 'svc1', entityType: 'service', system: 'backend', text: 'UserService', data: {} },
|
|
18
|
-
{ id: 'model1', entityType: 'model', system: 'backend', text: 'User', data: {} },
|
|
19
|
-
];
|
|
20
|
-
const edges = [
|
|
21
|
-
{ id: 'e1', source: 'screen1', target: 'api1', relation: 'consumes' },
|
|
22
|
-
{ id: 'e2', source: 'api1', target: 'route1', relation: 'calls' },
|
|
23
|
-
{ id: 'e3', source: 'route1', target: 'ctrl1', relation: 'handles' },
|
|
24
|
-
{ id: 'e4', source: 'ctrl1', target: 'svc1', relation: 'uses' },
|
|
25
|
-
{ id: 'e5', source: 'svc1', target: 'model1', relation: 'queries' },
|
|
26
|
-
];
|
|
27
|
-
return { system: 'backend', nodes, edges };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function withoutNode(schema, id) {
|
|
31
|
-
return {
|
|
32
|
-
...schema,
|
|
33
|
-
nodes: schema.nodes.filter(n => n.id !== id),
|
|
34
|
-
edges: schema.edges.filter(e => e.source !== id && e.target !== id),
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
describe('findDependents', () => {
|
|
39
|
-
it('walks the dependency graph transitively across systems', () => {
|
|
40
|
-
const schema = makeSchema();
|
|
41
|
-
const deps = findDependents(schema, 'model1');
|
|
42
|
-
const ids = deps.map(d => d.id);
|
|
43
|
-
// model ← service ← controller ← route ← api ← screen
|
|
44
|
-
expect(ids).toEqual(['svc1', 'ctrl1', 'route1', 'api1', 'screen1']);
|
|
45
|
-
expect(deps.find(d => d.id === 'svc1').depth).toBe(1);
|
|
46
|
-
expect(deps.find(d => d.id === 'screen1').depth).toBe(5);
|
|
47
|
-
expect(deps.find(d => d.id === 'screen1').system).toBe('app');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('returns nothing for a leaf with no dependents', () => {
|
|
51
|
-
expect(findDependents(makeSchema(), 'screen1')).toEqual([]);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe('analyzeImpact', () => {
|
|
56
|
-
it('returns no warnings without a previous snapshot', () => {
|
|
57
|
-
expect(analyzeImpact(null, makeSchema())).toEqual([]);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('flags a removed route as breaking and lists cross-system dependents', () => {
|
|
61
|
-
const oldSchema = makeSchema();
|
|
62
|
-
const newSchema = withoutNode(oldSchema, 'route1');
|
|
63
|
-
|
|
64
|
-
const warnings = analyzeImpact(oldSchema, newSchema);
|
|
65
|
-
expect(warnings).toHaveLength(1);
|
|
66
|
-
|
|
67
|
-
const w = warnings[0];
|
|
68
|
-
expect(w.severity).toBe('breaking');
|
|
69
|
-
expect(w.changeKind).toBe('removed');
|
|
70
|
-
expect(w.nodeText).toBe('POST /api/login');
|
|
71
|
-
expect(w.affected.map(a => a.id)).toEqual(['api1', 'screen1']);
|
|
72
|
-
expect(w.affectedSystems).toEqual(['app']);
|
|
73
|
-
expect(w.affectedByType).toEqual({ api: 1, screen: 1 });
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('flags a contract change (path) on a node with dependents', () => {
|
|
77
|
-
const oldSchema = makeSchema();
|
|
78
|
-
const newSchema = makeSchema();
|
|
79
|
-
const route = newSchema.nodes.find(n => n.id === 'route1');
|
|
80
|
-
route.data = { ...route.data, fullPath: '/api/v2/login' };
|
|
81
|
-
|
|
82
|
-
const warnings = analyzeImpact(oldSchema, newSchema);
|
|
83
|
-
expect(warnings).toHaveLength(1);
|
|
84
|
-
expect(warnings[0].changeKind).toBe('modified');
|
|
85
|
-
expect(warnings[0].message).toContain('fullPath');
|
|
86
|
-
expect(warnings[0].severity).toBe('breaking');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('ignores cosmetic changes (position, metadata)', () => {
|
|
90
|
-
const oldSchema = makeSchema();
|
|
91
|
-
const newSchema = makeSchema();
|
|
92
|
-
const route = newSchema.nodes.find(n => n.id === 'route1');
|
|
93
|
-
route.x = 999;
|
|
94
|
-
route.metadata = { parsedAt: 'now' };
|
|
95
|
-
|
|
96
|
-
expect(analyzeImpact(oldSchema, newSchema)).toEqual([]);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('skips modified nodes nobody depends on', () => {
|
|
100
|
-
const oldSchema = makeSchema();
|
|
101
|
-
const newSchema = makeSchema();
|
|
102
|
-
const screen = newSchema.nodes.find(n => n.id === 'screen1');
|
|
103
|
-
screen.data = { parentClass: 'HookWidget' };
|
|
104
|
-
|
|
105
|
-
expect(analyzeImpact(oldSchema, newSchema)).toEqual([]);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe('embedWarnings', () => {
|
|
110
|
-
it('attaches schema-level warnings and node-level flags', () => {
|
|
111
|
-
const oldSchema = makeSchema();
|
|
112
|
-
const newSchema = withoutNode(oldSchema, 'route1');
|
|
113
|
-
const warnings = analyzeImpact(oldSchema, newSchema);
|
|
114
|
-
|
|
115
|
-
embedWarnings(newSchema, warnings);
|
|
116
|
-
|
|
117
|
-
expect(newSchema.warnings).toHaveLength(1);
|
|
118
|
-
const api = newSchema.nodes.find(n => n.id === 'api1');
|
|
119
|
-
const screen = newSchema.nodes.find(n => n.id === 'screen1');
|
|
120
|
-
expect(api.impactedBy).toEqual(['route1']);
|
|
121
|
-
expect(screen.impactedBy).toEqual(['route1']);
|
|
122
|
-
// the removed node is gone — survivors only carry impactedBy
|
|
123
|
-
expect(newSchema.nodes.every(n => !n.warning)).toBe(true);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('flags the changed node itself when it survives', () => {
|
|
127
|
-
const oldSchema = makeSchema();
|
|
128
|
-
const newSchema = makeSchema();
|
|
129
|
-
const route = newSchema.nodes.find(n => n.id === 'route1');
|
|
130
|
-
route.data = { ...route.data, fullPath: '/api/v2/login' };
|
|
131
|
-
|
|
132
|
-
const warnings = analyzeImpact(oldSchema, newSchema);
|
|
133
|
-
embedWarnings(newSchema, warnings);
|
|
134
|
-
|
|
135
|
-
expect(newSchema.nodes.find(n => n.id === 'route1').warning).toMatchObject({ severity: 'breaking' });
|
|
136
|
-
});
|
|
137
|
-
});
|