|
| 1 | +import assert from 'node:assert/strict'; |
| 2 | +import { test } from 'node:test'; |
| 3 | +import { |
| 4 | + DAEMON_PLATFORM_BOUNDARY_RULE, |
| 5 | + daemonPlatformBoundaryViolations, |
| 6 | + findDaemonPlatformDependencies, |
| 7 | +} from './daemon-platform-boundary.ts'; |
| 8 | + |
| 9 | +const daemonFile = 'src/daemon/terminal-boundary-fixture.ts'; |
| 10 | + |
| 11 | +function dependencies(source: string, file = daemonFile) { |
| 12 | + return findDaemonPlatformDependencies([{ path: file, source }]); |
| 13 | +} |
| 14 | + |
| 15 | +function violations(source: string, file = daemonFile) { |
| 16 | + return daemonPlatformBoundaryViolations([{ path: file, source }]); |
| 17 | +} |
| 18 | + |
| 19 | +test('R65 rejects static, type-only, dynamic, type, and re-export dependencies with source lines', () => { |
| 20 | + const source = [ |
| 21 | + "import { old } from '../platforms/android/old.ts';", |
| 22 | + "import type { OldType } from '../platforms/android/types.ts';", |
| 23 | + "const lazy = import('../platforms/android/lazy.ts');", |
| 24 | + "type ImportedType = import('../platforms/android/type.ts').ImportedType;", |
| 25 | + "export { old } from '../platforms/android/re-export.ts';", |
| 26 | + "export type { OldType } from '@agent-device/platform-android/types';", |
| 27 | + "export * from '@agent-device/platform-apple';", |
| 28 | + ].join('\n'); |
| 29 | + |
| 30 | + assert.deepEqual( |
| 31 | + dependencies(source).map(({ kind, spec, line, target }) => ({ kind, spec, line, target })), |
| 32 | + [ |
| 33 | + { |
| 34 | + kind: 'static import', |
| 35 | + spec: '../platforms/android/old.ts', |
| 36 | + line: 1, |
| 37 | + target: 'src/platforms/android/old.ts', |
| 38 | + }, |
| 39 | + { |
| 40 | + kind: 'type-only import', |
| 41 | + spec: '../platforms/android/types.ts', |
| 42 | + line: 2, |
| 43 | + target: 'src/platforms/android/types.ts', |
| 44 | + }, |
| 45 | + { |
| 46 | + kind: 'dynamic import', |
| 47 | + spec: '../platforms/android/lazy.ts', |
| 48 | + line: 3, |
| 49 | + target: 'src/platforms/android/lazy.ts', |
| 50 | + }, |
| 51 | + { |
| 52 | + kind: 'type import', |
| 53 | + spec: '../platforms/android/type.ts', |
| 54 | + line: 4, |
| 55 | + target: 'src/platforms/android/type.ts', |
| 56 | + }, |
| 57 | + { |
| 58 | + kind: 're-export', |
| 59 | + spec: '../platforms/android/re-export.ts', |
| 60 | + line: 5, |
| 61 | + target: 'src/platforms/android/re-export.ts', |
| 62 | + }, |
| 63 | + { |
| 64 | + kind: 'type-only re-export', |
| 65 | + spec: '@agent-device/platform-android/types', |
| 66 | + line: 6, |
| 67 | + target: '@agent-device/platform-android/types', |
| 68 | + }, |
| 69 | + { |
| 70 | + kind: 're-export', |
| 71 | + spec: '@agent-device/platform-apple', |
| 72 | + line: 7, |
| 73 | + target: '@agent-device/platform-apple', |
| 74 | + }, |
| 75 | + ], |
| 76 | + ); |
| 77 | + |
| 78 | + assert.deepEqual( |
| 79 | + violations(source).map(({ rule, file, line }) => ({ rule, file, line })), |
| 80 | + Array.from({ length: 7 }, (_, index) => ({ |
| 81 | + rule: DAEMON_PLATFORM_BOUNDARY_RULE, |
| 82 | + file: daemonFile, |
| 83 | + line: index + 1, |
| 84 | + })), |
| 85 | + ); |
| 86 | +}); |
| 87 | + |
| 88 | +test('R65 recognizes side-effect and aliased imports, including nested daemon paths', () => { |
| 89 | + const source = [ |
| 90 | + "import '../../platforms/web/runtime.ts';", |
| 91 | + "import { runtime as platformRuntime } from '@agent-device/platform-web/runtime';", |
| 92 | + ].join('\n'); |
| 93 | + |
| 94 | + assert.deepEqual( |
| 95 | + dependencies(source, 'src/daemon/handlers/terminal-boundary-fixture.ts').map((found) => ({ |
| 96 | + kind: found.kind, |
| 97 | + line: found.line, |
| 98 | + spec: found.spec, |
| 99 | + target: found.target, |
| 100 | + })), |
| 101 | + [ |
| 102 | + { |
| 103 | + kind: 'static import', |
| 104 | + line: 1, |
| 105 | + spec: '../../platforms/web/runtime.ts', |
| 106 | + target: 'src/platforms/web/runtime.ts', |
| 107 | + }, |
| 108 | + { |
| 109 | + kind: 'static import', |
| 110 | + line: 2, |
| 111 | + spec: '@agent-device/platform-web/runtime', |
| 112 | + target: '@agent-device/platform-web/runtime', |
| 113 | + }, |
| 114 | + ], |
| 115 | + ); |
| 116 | +}); |
| 117 | + |
| 118 | +test('R65 ignores comments, ordinary strings, unresolved dynamic imports, and lookalike names', () => { |
| 119 | + const source = [ |
| 120 | + 'const documentation = "import(\'../platforms/android/comment.ts\'); @agent-device/platform-android";', |
| 121 | + "// import { ignored } from '../platforms/android/comment.ts';", |
| 122 | + "const packageName = '@agent-device/platform-android';", |
| 123 | + "const relativeName = '../platforms/android/not-an-import.ts';", |
| 124 | + 'const computed = import(platformSpecifier);', |
| 125 | + "import '../platforms-sibling/not-platform.ts';", |
| 126 | + "import '@agent-device/platforms';", |
| 127 | + "import '@agent-device/platform';", |
| 128 | + ].join('\n'); |
| 129 | + |
| 130 | + assert.deepEqual(dependencies(source), []); |
| 131 | +}); |
| 132 | + |
| 133 | +test('R65 rejects require, import-equals, and template-literal type imports', () => { |
| 134 | + const source = [ |
| 135 | + "const android = require('@agent-device/platform-android');", |
| 136 | + 'const runtime = require(`../platforms/android/runtime.ts`);', |
| 137 | + "import web = require('@agent-device/platform-web');", |
| 138 | + 'type Apple = import(`@agent-device/platform-apple`).Apple;', |
| 139 | + 'type Android = import(`../platforms/android/types.ts`).Android;', |
| 140 | + ].join('\n'); |
| 141 | + |
| 142 | + assert.deepEqual( |
| 143 | + dependencies(source).map(({ kind, spec, line }) => ({ kind, spec, line })), |
| 144 | + [ |
| 145 | + { kind: 'require', spec: '@agent-device/platform-android', line: 1 }, |
| 146 | + { kind: 'require', spec: '../platforms/android/runtime.ts', line: 2 }, |
| 147 | + { kind: 'import equals', spec: '@agent-device/platform-web', line: 3 }, |
| 148 | + { kind: 'type import', spec: '@agent-device/platform-apple', line: 4 }, |
| 149 | + { kind: 'type import', spec: '../platforms/android/types.ts', line: 5 }, |
| 150 | + ], |
| 151 | + ); |
| 152 | +}); |
| 153 | + |
| 154 | +test('R65 folds statically constructed dynamic platform specifiers', () => { |
| 155 | + const source = [ |
| 156 | + "const apple = import('@agent-device/' + 'platform-apple');", |
| 157 | + 'const android = import(`../platforms/${"android"}/runtime.ts`);', |
| 158 | + ].join('\n'); |
| 159 | + |
| 160 | + assert.deepEqual( |
| 161 | + dependencies(source).map(({ kind, spec, line }) => ({ kind, spec, line })), |
| 162 | + [ |
| 163 | + { kind: 'dynamic import', spec: '@agent-device/platform-apple', line: 1 }, |
| 164 | + { kind: 'dynamic import', spec: '../platforms/android/runtime.ts', line: 2 }, |
| 165 | + ], |
| 166 | + ); |
| 167 | +}); |
| 168 | + |
| 169 | +test('R65 ignores non-daemon and test-shaped records even when their syntax is red', () => { |
| 170 | + const source = "import { platform } from '../platforms/android/runtime.ts';"; |
| 171 | + assert.deepEqual(dependencies(source, 'src/core/terminal-boundary-fixture.ts'), []); |
| 172 | + assert.deepEqual(dependencies(source, 'src/daemon/terminal-boundary-fixture.test.ts'), []); |
| 173 | + assert.deepEqual(dependencies(source, 'src/daemon/__tests__/terminal-boundary-fixture.ts'), []); |
| 174 | +}); |
| 175 | + |
| 176 | +test('R65 treats a relative specifier as legacy platform code only after path resolution', () => { |
| 177 | + const source = [ |
| 178 | + "import { platform } from '../../platforms/android/runtime.ts';", |
| 179 | + "import { sibling } from '../platforms-sibling/runtime.ts';", |
| 180 | + "import { exact } from '../platforms';", |
| 181 | + ].join('\n'); |
| 182 | + |
| 183 | + assert.deepEqual( |
| 184 | + dependencies(source).map(({ spec, target, line }) => ({ spec, target, line })), |
| 185 | + [ |
| 186 | + { |
| 187 | + spec: '../platforms', |
| 188 | + target: 'src/platforms', |
| 189 | + line: 3, |
| 190 | + }, |
| 191 | + ], |
| 192 | + ); |
| 193 | +}); |
0 commit comments