Skip to content

CodeUseTelemetrySmokeTest.kt

src/jvmMain/kotlin/afield/examples/CodeUseTelemetrySmokeTest.kt · 365 lines

package afield.examples

import afield.app.diagnostics.CodeUseSemanticAnalyzeRequest
import afield.app.diagnostics.CodeUseTelemetryService
import java.nio.file.Files
import java.sql.DriverManager

object CodeUseTelemetrySmokeTest

FIELD_TEST id: examples.code-use.telemetry-store runner: jvm-main entrypoint: afield.examples.CodeUseTelemetrySmokeTest tags: [smoke, code-use, telemetry, sqlite] timeoutSec: 20 expect: exitCode: 0 stdoutContains: - "CodeUseTelemetrySmokeTest ok"

object CodeUseTelemetrySmokeTest {

fun main(args: Array<String>)

body — 343 lines, from line 22
    @JvmStatic
    fun main(args: Array<String>) {
        val tempDir = Files.createTempDirectory("afield-code-use-telemetry")
        val database = tempDir.resolve("field-code-use.sqlite")
        System.setProperty("afield.codeUse.path", database.toString())
        System.setProperty("afield.codeUse.semanticSidecar.url", "")

        val snippetId = CodeUseTelemetryService.newSnippetKey()
        val source = """
            val n = listOf(1, 2, 3).sum()
            println("telemetry smoke ${'$'}n")
            CodeUseTelemetryService.summary()
        """.trimIndent()
        val context = CodeUseTelemetryService.CodeUseContext(
            document = "code-use-smoke",
            documentPath = tempDir.resolve("code-use-smoke").toString(),
            boxUid = "box-code-use-smoke",
            boxName = "Code Use Smoke",
            property = "code",
            mode = "execute",
            strand = "__main__",
            parentIds = listOf("root"),
            preambleHash = CodeUseTelemetryService.sha256("preamble")
        )

        CodeUseTelemetryService.recordCompileResult(
            snippetId = snippetId,
            source = source,
            sourceName = "CodeUseTelemetrySmoke.kts",
            context = context.copy(mode = "compile"),
            success = true,
            diagnostics = listOf("INFO: clean"),
            elapsedMs = 1.25,
            classpathHash = "classpath-smoke"
        )
        CodeUseTelemetryService.recordEvaluationResult(
            snippetId = snippetId,
            context = context.copy(mode = "evaluate"),
            success = true,
            runtimeClass = "afield.examples.CodeUseTelemetrySmokeScript",
            returnType = "kotlin.Unit",
            elapsedMs = 0.75
        )
        CodeUseTelemetryService.recordLaunchLifecycle(
            snippetId = snippetId,
            source = source,
            context = context,
            lifecycle = "execute-start",
            watermark = 7
        )
        CodeUseTelemetryService.recordLaunchLifecycle(
            snippetId = snippetId,
            source = "",
            context = context,
            lifecycle = "execute-finished",
            watermark = 7,
            returnType = "kotlin.Unit"
        )
        val duplicateSiteSnippetId = CodeUseTelemetryService.newSnippetKey()
        CodeUseTelemetryService.recordCompileResult(
            snippetId = duplicateSiteSnippetId,
            source = source,
            sourceName = "CodeUseTelemetrySmokeDuplicateSite.kts",
            context = context.copy(mode = "compile:user-code"),
            success = true,
            diagnostics = emptyList(),
            elapsedMs = 0.1,
            classpathHash = "classpath-smoke"
        )
        val compileFailureId = CodeUseTelemetryService.newSnippetKey()
        CodeUseTelemetryService.recordCompileResult(
            snippetId = compileFailureId,
            source = "missingGrowthPoint(",
            sourceName = "CodeUseTelemetryCompileFailure.kts",
            context = context.copy(mode = "compile"),
            success = false,
            diagnostics = listOf("ERROR: unresolved reference: missingGrowthPoint"),
            elapsedMs = 0.25
        )
        val importOnlyId = CodeUseTelemetryService.newSnippetKey()
        val importOnlySource = """
            import afield.*
            import afield.lang.nano.*
            import afield.stdlib.*
            import afield.audio.synth.*
            import afield.math.linalg.Vec2
            import afield.math.linalg.Vec3
            import afield.math.linalg.Vec4
            import afield.math.linalg.Quat
            import afield.document.graph.Box
            import afield.graphics.vulkan.util.*
            import afield.graphics.*
        """.trimIndent()
        CodeUseTelemetryService.recordCompileResult(
            snippetId = importOnlyId,
            source = importOnlySource,
            sourceName = "CodeUseTelemetryImportsOnly.kts",
            context = context.copy(mode = "compile:imports-or-preamble"),
            success = true,
            diagnostics = emptyList(),
            elapsedMs = 0.1
        )
        val ambiguousSourceIndexId = CodeUseTelemetryService.newSnippetKey()
        CodeUseTelemetryService.recordCompileResult(
            snippetId = ambiguousSourceIndexId,
            source = "sin(0.0)",
            sourceName = "CodeUseTelemetryAmbiguousSourceIndex.kts",
            context = context.copy(mode = "compile:user-code"),
            success = true,
            diagnostics = emptyList(),
            elapsedMs = 0.1
        )
        val runtimeFailureId = CodeUseTelemetryService.newSnippetKey()
        CodeUseTelemetryService.recordLaunchLifecycle(
            snippetId = runtimeFailureId,
            source = """error("runtime growth point")""",
            context = context.copy(mode = "execute"),
            lifecycle = "execute-start",
            watermark = 9
        )
        CodeUseTelemetryService.recordLaunchLifecycle(
            snippetId = runtimeFailureId,
            source = "",
            context = context.copy(mode = "execute"),
            lifecycle = "execute-exception",
            watermark = 9,
            returnType = IllegalStateException::class.java.name,
            exception = IllegalStateException("runtime growth point")
        )

        val summary = waitForSummary {
            it.enabled && it.snippets >= 6 && it.compileEvents >= 5 && it.evalEvents >= 1 && it.launchEvents >= 4
        }
        require(summary.enabled) { summary.message }
        require(summary.droppedEvents == 0L) { "Unexpected dropped events: ${summary.droppedEvents}" }
        require(summary.droppedAnalysisJobs == 0L) { "Unexpected dropped analysis jobs: ${summary.droppedAnalysisJobs}" }
        require(summary.analyzerBackend == "source-index") { "Unexpected analyzer backend: ${summary.analyzerBackend}" }
        require(summary.analyzerMessage.isNotBlank()) { "Missing analyzer status message: $summary" }
        require(summary.sourceBlobs >= 1) { "Expected at least one source blob: $summary" }
        require(analysisRowCount(database.toString(), importOnlyId) == 0) {
            "Import-only/preamble snippet produced analyzer rows"
        }
        val ambiguousReasons = waitForUnresolvedReasons(database.toString(), ambiguousSourceIndexId)
        require(symbolUseRowCount(database.toString(), ambiguousSourceIndexId) == 0) {
            "Ambiguous source-index snippet produced resolved symbol rows"
        }
        require(ambiguousReasons.any { it.startsWith("ambiguous-source-index-match") }) {
            "Ambiguous source-index snippet did not produce an ambiguity row: $ambiguousReasons"
        }
        require(sourceIndexMatchRowCount(database.toString()) == 0) {
            "Legacy source-index-match confidence rows should not be written"
        }

        val snippet = CodeUseTelemetryService.recentSnippets(20).firstOrNull { it.id == snippetId }
            ?: error("Telemetry snippet was not queryable")
        require(snippet.sourceHash == CodeUseTelemetryService.sha256(source)) {
            "Unexpected source hash: ${snippet.sourceHash}"
        }
        require(snippet.document == "code-use-smoke") { snippet.document }
        require(snippet.boxName == "Code Use Smoke") { snippet.boxName }
        require(snippet.lastCompileSuccess == true) { "Compile result was not recorded: $snippet" }
        require(snippet.lastEvalSuccess == true) { "Eval result was not recorded: $snippet" }
        require(snippet.lastReturnType == "kotlin.Unit") { snippet.lastReturnType }

        val lifecycles = CodeUseTelemetryService.recentLaunches(20)
            .filter { it.snippetId == snippetId }
            .map { it.lifecycle }
            .toSet()
        require("execute-start" in lifecycles && "execute-finished" in lifecycles) {
            "Missing launch lifecycle records: $lifecycles"
        }
        val failures = CodeUseTelemetryService.failures(20)
        require(failures.any { it.snippetId == compileFailureId && it.kind == "compile" && "missingGrowthPoint" in it.diagnosticsJson }) {
            "Missing compile failure row: $failures"
        }
        require(failures.any { it.snippetId == runtimeFailureId && it.lifecycle == "execute-exception" && "runtime growth point" in it.exceptionJson }) {
            "Missing runtime exception failure row: $failures"
        }

        val telemetrySource = java.nio.file.Paths.get("src/jvmMain/kotlin/afield/app/diagnostics/CodeUseTelemetryService.kt")
        val summaryLine = Files.readAllLines(telemetrySource).indexOfFirst { it.contains("fun summary()") } + 1
        require(summaryLine > 0) { "Could not locate CodeUseTelemetryService.summary source line" }
        val (target, callers) = CodeUseTelemetryService.callersForSourceLine(
            sourcePath = telemetrySource.toString(),
            line = summaryLine,
            limit = 10
        )
        require(target?.symbolName?.contains("summary") == true) { "Unexpected source target: $target" }
        require(callers.any { it.snippetId == snippetId && it.lastReturnType == "kotlin.Unit" }) {
            "Missing source-line caller row: target=$target callers=$callers"
        }
        val resolvedSymbols = waitForSymbols {
            it.any { row -> row.fqName.contains("summary") && row.useCount >= 1 && row.uniqueSnippetCount >= 1 }
        }
        require(resolvedSymbols.any { it.fqName.contains("summary") && it.useCount >= 1 }) {
            "Analyzer did not populate summary symbol use: $resolvedSymbols"
        }

        val references = CodeUseTelemetryService.referencesForCode(
            """CodeUseTelemetryService.summary()""",
            limit = 5
        )
        require(references.any { it.symbolName.contains("summary") && it.sourceFile.endsWith("CodeUseTelemetryService.kt") }) {
            "Missing references-for-code row: $references"
        }

        val related = CodeUseTelemetryService.relatedSourceForCode(
            """CodeUseTelemetryService.summary()""",
            symbolLimit = 5,
            callersPerSymbol = 5,
            sourceRadiusLines = 8
        )
        require(
            related.any {
                it.symbolName.contains("summary") &&
                    it.sourceExcerpt.contains("fun summary()") &&
                    it.useCount >= 1 &&
                    it.peerCallers.any { caller -> caller.snippetId == snippetId }
            }
        ) {
            "Missing related source bundle row: $related"
        }

        val semantic = CodeUseTelemetryService.semanticAnalyzeCode(
            CodeUseSemanticAnalyzeRequest(
                code = """CodeUseTelemetryService.summary()""",
                sourceName = "CodeUseTelemetrySmokeAgent.kt",
                limit = 5,
                callersPerSymbol = 5,
                sourceRadiusLines = 8
            )
        )
        require(semantic.success) { "Semantic analyze fallback failed: $semantic" }
        require(semantic.backend == "source-index-fallback") { "Unexpected semantic backend: $semantic" }
        require(semantic.references.any { it.symbolName.contains("summary") }) {
            "Missing semantic references fallback row: $semantic"
        }
        require(semantic.relatedSources.any { it.sourceExcerpt.contains("fun summary()") }) {
            "Missing semantic related source fallback row: $semantic"
        }
        val ambiguousReferences = CodeUseTelemetryService.referencesForCode("""sin(0.0)""", limit = 20)
        require(ambiguousReferences.none { it.symbolName.substringAfterLast('.') == "sin" }) {
            "Ambiguous simple-name reference query should not produce guessed sin source refs: $ambiguousReferences"
        }

        val hot = CodeUseTelemetryService.hotSourceSymbols("all", 20)
        val hotSummary = hot.firstOrNull { it.symbolName.contains("summary") }
        require(hotSummary != null) { "Missing hot source symbol row: $hot" }
        require(hotSummary.confidence != "resolved" && "source-index" in hotSummary.confidence) {
            "Source-index hot row should not be reported as semantic resolved: $hotSummary"
        }
        require(hotSummary.useCount == 1L && hotSummary.uniqueSnippetCount >= 2L) {
            "Hot source symbols should count distinct code sites, not raw snippet observations: $hotSummary"
        }

        val early = CodeUseTelemetryService.earlyDocumentSymbols("code-use-smoke", earlyWindowMs = 60_000, limit = 20)
        require(early.any { it.symbolName.contains("summary") }) { "Missing early document symbol row: $early" }
        require(database.toFile().exists()) { "SQLite database was not created at $database" }

        println("CodeUseTelemetrySmokeTest ok")
    }

    private fun waitForSummary(predicate: (afield.app.diagnostics.CodeUseSummary) -> Boolean): afield.app.diagnostics.CodeUseSummary {
        val deadline = System.currentTimeMillis() + 5_000
        var latest = CodeUseTelemetryService.summary()
        while (System.currentTimeMillis() < deadline) {
            latest = CodeUseTelemetryService.summary()
            if (predicate(latest)) return latest
            Thread.sleep(50)
        }
        error("Timed out waiting for code-use telemetry writes: $latest")
    }

    private fun waitForSymbols(predicate: (List<afield.app.diagnostics.CodeUseSymbolRow>) -> Boolean): List<afield.app.diagnostics.CodeUseSymbolRow> {
        val deadline = System.currentTimeMillis() + 5_000
        var latest = CodeUseTelemetryService.symbols(50)
        while (System.currentTimeMillis() < deadline) {
            latest = CodeUseTelemetryService.symbols(50)
            if (predicate(latest)) return latest
            Thread.sleep(50)
        }
        error("Timed out waiting for code-use analyzer symbols: $latest")
    }

    private fun analysisRowCount(databasePath: String, snippetId: String): Int =
        DriverManager.getConnection("jdbc:sqlite:$databasePath").use { conn ->
            conn.prepareStatement(
                """
                SELECT
                    (SELECT COUNT(*) FROM symbol_uses WHERE snippet_id = ?) +
                    (SELECT COUNT(*) FROM unresolved_uses WHERE snippet_id = ?) AS row_count
                """.trimIndent()
            ).use { ps ->
                ps.setString(1, snippetId)
                ps.setString(2, snippetId)
                ps.executeQuery().use { rs ->
                    if (rs.next()) rs.getInt("row_count") else 0
                }
            }
        }

    private fun symbolUseRowCount(databasePath: String, snippetId: String): Int =
        DriverManager.getConnection("jdbc:sqlite:$databasePath").use { conn ->
            conn.prepareStatement("SELECT COUNT(*) AS row_count FROM symbol_uses WHERE snippet_id = ?").use { ps ->
                ps.setString(1, snippetId)
                ps.executeQuery().use { rs ->
                    if (rs.next()) rs.getInt("row_count") else 0
                }
            }
        }

    private fun sourceIndexMatchRowCount(databasePath: String): Int =
        DriverManager.getConnection("jdbc:sqlite:$databasePath").use { conn ->
            conn.prepareStatement("SELECT COUNT(*) AS row_count FROM symbol_uses WHERE confidence = 'source-index-match'").use { ps ->
                ps.executeQuery().use { rs ->
                    if (rs.next()) rs.getInt("row_count") else 0
                }
            }
        }

    private fun waitForUnresolvedReasons(databasePath: String, snippetId: String): List<String> {
        val deadline = System.currentTimeMillis() + 5_000
        var latest = unresolvedReasons(databasePath, snippetId)
        while (System.currentTimeMillis() < deadline) {
            latest = unresolvedReasons(databasePath, snippetId)
            if (latest.isNotEmpty()) return latest
            Thread.sleep(50)
        }
        error("Timed out waiting for unresolved analyzer rows: $latest")
    }

    private fun unresolvedReasons(databasePath: String, snippetId: String): List<String> =
        DriverManager.getConnection("jdbc:sqlite:$databasePath").use { conn ->
            conn.prepareStatement("SELECT reason FROM unresolved_uses WHERE snippet_id = ? ORDER BY id").use { ps ->
                ps.setString(1, snippetId)
                ps.executeQuery().use { rs ->
                    buildList {
                        while (rs.next()) add(rs.getString("reason").orEmpty())
                    }
                }
            }
        }
}