package com.opendance.studio import com.opendance.studio.ui.Credits import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test /** * T-AND-008:开源声明一致性与完整性 * * 文档 §8.2 列出的最低 10 项必须全部声明;每条都必须有许可证与 https 源链接。 * 客户端「关于」页与 THIRD_PARTY.md、contracts/open-source-credits.md 三处一致。 */ class CreditsTest { private val requiredNames = listOf( "MediaPipe", "Three.js", "@pixiv/three-vrm", "Assimp", "Godot Engine", "godot-vrm", "NestJS", "TypeORM", "OkHttp", "bvh", ) @Test fun coversDocumentedMinimumSet() { val names = Credits.ENTRIES.map { it.name } for (required in requiredNames) { assertTrue("文档要求的开源项目「$required」必须声明", names.contains(required)) } assertTrue("声明条目不少于 10 项", Credits.entryCount() >= 10) } @Test fun everyEntryHasLicenseAndHttpsLink() { for (entry in Credits.ENTRIES) { assertTrue("${entry.name} 有许可证", entry.license.isNotBlank()) assertTrue("${entry.name} 的源链接为 https", entry.url.startsWith("https://")) assertTrue("${entry.name} 注明了用途", entry.usage.isNotBlank()) } } @Test fun knownLicensesAreCorrect() { val expected = mapOf( "MediaPipe" to "Apache License 2.0", "Godot Engine" to "MIT", "Assimp" to "BSD-3-Clause", "OkHttp" to "Apache License 2.0", "CameraX" to "Apache License 2.0", "Jetpack Compose" to "Apache License 2.0", "bvh" to "MIT", ) for (entry in Credits.ENTRIES) { expected[entry.name]?.let { license -> assertEquals("${entry.name} 的许可证", license, entry.license) } } } @Test fun thisAppOwnDependenciesAreDeclared() { // 本端实际引入的运行时依赖必须出现在声明里 val names = Credits.ENTRIES.map { it.name } for (own in listOf("MediaPipe", "OkHttp", "CameraX", "Jetpack Compose")) { assertTrue("本端依赖「$own」必须声明", names.contains(own)) } } @Test fun entryNamesAreUnique() { val names = Credits.ENTRIES.map { it.name } assertEquals("开源项目名不得重复", names.size, names.toSet().size) } @Test fun contractVersionIsPinned() { // 本端是 websocket-keypoints 的发送方,这项必须与契约快照一致 assertEquals( "websocket-keypoints 契约版本必须与 opendance-web 冻结版本一致", "0.2.0", Credits.CONTRACT_VERSION, ) } @Test fun everyContractVersionMatchesTheSnapshot() { // 契约版本是"单一来源":contracts/VERSION 决定一切,界面不得手写 val snapshot = readContractVersionFile() assertTrue("能读到 contracts/VERSION", snapshot.isNotEmpty()) assertEquals( "契约项数与 contracts/VERSION 一致(新增契约时忘记同步会导致这里失败)", snapshot.size, Credits.contractVersions.size, ) for ((name, version) in Credits.contractVersions) { assertEquals( "契约 $name 的版本与快照一致", snapshot[name] ?: "", version, ) } } @Test fun appVersionMatchesProjectYaml() { // 文档 §11.2:界面展示的版本必须与 project.yaml / 产物文件名同源 val yaml = readProjectYamlVersion() assertTrue("能读到 project.yaml 的 version", yaml.isNotEmpty()) assertEquals("BuildConfig.VERSION_NAME 必须等于 project.yaml 的 version", yaml, Credits.appVersion) assertEquals("Credits.appVersion 与 BuildConfig 同源(不得手写)", BuildConfig.VERSION_NAME, Credits.appVersion) } @Test fun contractSummaryListsAllContracts() { val summary = Credits.contractSummary() for ((name, version) in Credits.contractVersions) { assertTrue("摘要含 $name", summary.contains(name)) assertTrue("摘要含 $name 的版本 v$version", summary.contains("v$version")) } } // ------------------------------------------------------------ 文件读取 /** 从模块目录逐级向上找仓库根(含 contracts/VERSION 的那一级) */ private fun repoFile(relative: String): java.io.File? { var dir: java.io.File = java.io.File(System.getProperty("user.dir")).absoluteFile repeat(6) { val candidate = java.io.File(dir, relative) if (candidate.exists()) return candidate dir = dir.parentFile ?: return null } return null } private fun readContractVersionFile(): Map { val file = repoFile("contracts/VERSION") ?: return emptyMap() return file.readLines() .map { it.trim() } .filter { it.isNotEmpty() && !it.startsWith("#") && it.contains(":") } .associate { it.substringBefore(":").trim() to it.substringAfter(":").trim() } } private fun readProjectYamlVersion(): String { val file = repoFile("project.yaml") ?: return "" return file.readLines() .firstOrNull { it.trimStart().startsWith("version:") } ?.substringAfter(":") ?.trim() ?: "" } }