Merge pull request #10463 from Kilo-Org/panoramic-existence

fix(jetbrains): fix build/tests after OpenCode merge, document Java 21 setup
This commit is contained in:
Kirill Kalishev
2026-05-21 10:16:31 -04:00
committed by GitHub
11 changed files with 411 additions and 159 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ jobs:
turbo-${{ runner.os }}-
- name: Run unit tests
run: bun turbo test:ci --filter=!@kilocode/kilo-jetbrains # kilocode_change
run: bun turbo test:ci
env:
KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
+8
View File
@@ -17,5 +17,13 @@ jobs:
- name: Setup Bun
uses: ./.github/actions/setup-bun
# kilocode_change start
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
# kilocode_change end
- name: Run typecheck
run: bun typecheck
+2 -1
View File
@@ -12,7 +12,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang
- **Dev**: `bun run dev` (runs from root) or `bun run --cwd packages/opencode --conditions=browser src/index.ts`
- **Dev with params**: `bun dev -- help`
- **Extension**: `bun run extension` (build + launch VS Code with the extension in dev mode). Pass `--no-build` to skip the build.
- **Typecheck**: `bun turbo typecheck` (uses `tsgo`, not `tsc`)
- **Typecheck**: `bun turbo typecheck` (uses `tsgo`, not `tsc`). Includes the JetBrains plugin — requires Java 21. Check with `java -version` before running. If missing, install via SDKMAN: `sdk install java 21-tem && sdk use java 21-tem`. If SDKMAN is not installed, see https://sdkman.io/install.
- **Test**: `bun test` from `packages/opencode/` (NOT from root -- root blocks tests)
- **Single test**: `bun test ./test/tool/tool-define.test.ts` from `packages/opencode/`
- **CLI build artifact size check**: after `bun run script/build.ts --single --skip-install` in `packages/opencode/`, use `du -h dist/*/*/bin/kilo` (scoped package output lives under `dist/@kilocode/`)
@@ -34,6 +34,7 @@ Before saying an implementation is ready, run the smallest relevant checks that
| CLI | From `packages/opencode/`: `bun run typecheck`, `bun test` or targeted `bun test ./path/to/file.test.ts` |
| VS Code extension | From `packages/kilo-vscode/`: `bun run typecheck`, `bun run lint`, `bun run test:unit` or `bun run test` |
| Extension build/package | From `packages/kilo-vscode/`: `bun run compile` or `bun run package` when touching build, packaging, SDK, or webview integration paths |
| JetBrains plugin | From `packages/kilo-jetbrains/`: `./gradlew typecheck`, `./gradlew test`. Requires Java 21 — check first with `java -version`. Install via SDKMAN if missing: `sdk install java 21-tem && sdk use java 21-tem`. |
| CI-only guards | Run affected guards documented above, such as `bun run knip`, `bun run check-kilocode-change`, `bun run script/check-opencode-annotations.ts`, or source link extraction |
Never run root `bun test`; the root script prints `do not run tests from root` and exits with code 1. Use package-level tests instead.
+43 -1
View File
@@ -14,9 +14,34 @@ There are lots of ways to contribute to the project:
The Kilo Community is [on Discord](https://kilo.ai/discord).
## Prerequisites
- **Bun 1.3.13+** — required for all packages.
- **Java 21** — required by the JetBrains plugin. The root `bun turbo typecheck` and `bun turbo test:ci` commands include `@kilocode/kilo-jetbrains` and will fail without Java 21.
The preferred way to install Java is via [SDKMAN](https://sdkman.io/install):
```bash
# Install SDKMAN (if not already installed)
curl -s "https://get.sdkman.io" | bash
# Install and activate Java 21 (Eclipse Temurin)
sdk install java 21-tem
sdk use java 21-tem
# Verify
java -version
```
If you don't plan to work on the JetBrains plugin, you can still run non-JetBrains checks directly:
```bash
bun turbo typecheck --filter=!@kilocode/kilo-jetbrains
```
## Developing Kilo CLI
- **Requirements:** Bun 1.3.13+
- **Requirements:** Bun 1.3.13+, Java 21 (see [Prerequisites](#prerequisites) above)
- Install dependencies and start the dev server from the repo root:
```bash
@@ -34,6 +59,23 @@ bun run extension # Build + launch in dev mode
This auto-detects VS Code on macOS, Linux, and Windows. Override with `--app-path PATH` or `VSCODE_EXEC_PATH`. Use `--insiders` to prefer Insiders, `--workspace PATH` to open a specific folder, or `--clean` to reset cached state.
### Developing the JetBrains Plugin
Requires Java 21 (see [Prerequisites](#prerequisites)). From `packages/kilo-jetbrains/`:
```bash
./gradlew typecheck # Compile-check all Kotlin sources
./gradlew test # Run all tests (backend + frontend)
./gradlew runIde # Launch sandboxed IntelliJ with the plugin
```
Or via the root turbo filter to run only JetBrains checks from the repo root:
```bash
bun turbo typecheck --filter=@kilocode/kilo-jetbrains
bun turbo test:ci --filter=@kilocode/kilo-jetbrains
```
### Running against a different directory
By default, `bun dev` runs Kilo CLI in the `packages/opencode` directory. To run it against a different directory or repository:
+1 -1
View File
@@ -9,7 +9,7 @@
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"dev:storybook": "bun --cwd packages/storybook storybook",
"lint": "oxlint",
"typecheck": "bun turbo typecheck --filter=!@kilocode/kilo-jetbrains",
"typecheck": "bun turbo typecheck",
"postinstall": "bun run --cwd packages/opencode fix-node-pty && bun run script/setup-git.ts",
"prepare": "husky",
"random": "echo 'Random script'",
+11 -1
View File
@@ -9,7 +9,17 @@ AI coding agent plugin for JetBrains IDEs.
### Prerequisites
- **Bun** -- used to build CLI binaries and run build scripts
- **JDK 21+** -- required by Gradle and the IntelliJ Platform SDK
- **JDK 21+** -- required by Gradle and the IntelliJ Platform SDK. Check with `java -version`. The preferred way to install is via [SDKMAN](https://sdkman.io/install):
```bash
# Install SDKMAN (if not already installed)
curl -s "https://get.sdkman.io" | bash
# Install and activate Java 21 (Eclipse Temurin)
sdk install java 21-tem
sdk use java 21-tem
```
- **IntelliJ IDEA** -- to run the plugin in a sandboxed IDE
---
@@ -86,6 +86,10 @@ tasks.named("compileKotlin") {
dependsOn(fixGeneratedApi)
}
tasks.named("compileTestKotlin") {
dependsOn(fixGeneratedApi)
}
val cliDir = layout.buildDirectory.dir("generated/cli/cli")
val production = providers.gradleProperty("production").map { it.toBoolean() }.orElse(false)
@@ -1,24 +0,0 @@
package normalization
internal data class DuplicateTagRule(
val original: String,
val dedups: List<TagDedup>,
)
internal data class TagDedup(
val name: String,
val ops: List<String> = emptyList(),
)
internal val duplicateTagRules = listOf(
DuplicateTagRule(
original = "pty",
dedups = listOf(
TagDedup(name = "pty"),
TagDedup(
name = "pty-connect",
ops = listOf("pty.connect"),
),
),
),
)
@@ -11,105 +11,159 @@ internal object OpenApiSpecNormalizer {
fun normalize(raw: String): String {
val root = Json.parseToJsonElement(raw) as? JsonObject
?: throw GradleException("OpenAPI spec root must be a JSON object.")
val tags = root["tags"] as? JsonArray ?: return raw
val (fixedTags, renames) = fixTags(tags)
if (renames.isEmpty()) return raw
val paths = root["paths"] as? JsonObject
?: throw GradleException("OpenAPI spec paths must be a JSON object.")
val fixed = JsonObject(
root + mapOf(
"tags" to fixedTags,
"paths" to fixPaths(paths, renames),
)
)
// Step 1: Remove duplicate dot-notation schemas and remap their $refs to
// camelCase equivalents so the spec remains self-consistent.
// Step 2: Strip operation-level tags so all routes land in DefaultApi.
// Step 3: Deduplicate the root-level tags array.
// Step 4: Fix nullable fields in the /kilo/profile response that Effect's
// OpenAPI generator incorrectly emits as non-nullable.
val (noDotsRoot, _) = remapDotSchemas(root)
val stripped = stripTags(noDotsRoot)
val deduped = dedupRootTags(stripped)
val fixed = fixProfileNullable(deduped)
return encode(fixed)
}
private fun encode(obj: JsonObject): String {
val json = Json { prettyPrint = true }
.encodeToString(JsonElement.serializer(), fixed)
.encodeToString(JsonElement.serializer(), obj)
return "$json\n"
}
private fun fixTags(tags: JsonArray): Pair<JsonArray, Map<String, Rename>> {
val rules = rules()
val counts = mutableMapOf<String, Int>()
val renames = mutableMapOf<String, Rename>()
val fixed = tags.map { tag ->
val item = tag as? JsonObject ?: return@map tag
val name = text(item["name"]) ?: return@map tag
val rule = rules[name] ?: return@map tag
val index = counts.getOrDefault(name, 0)
counts[name] = index + 1
if (index >= rule.dedups.size) {
throw GradleException("Missing final OpenAPI tag name for duplicate '$name' at index $index.")
/**
* Find schemas whose names contain dots (e.g. "Event.tui.command.execute").
* If a camelCase equivalent (e.g. "EventTuiCommandExecute") exists in the
* same component map, remove the dot schema and rewrite every `$ref` that
* points to it to use the camelCase name instead.
*/
private fun remapDotSchemas(root: JsonObject): Pair<JsonObject, Map<String, String>> {
val components = root["components"] as? JsonObject ?: return root to emptyMap()
val schemas = components["schemas"] as? JsonObject ?: return root to emptyMap()
// Build a map of dot-name → camelCase-name for schemas that have a
// camelCase duplicate in the same spec.
val dotMap = schemas.keys
.filter { "." in it }
.mapNotNull { dot ->
val camel = dot.split(".").joinToString("") { w -> w.replaceFirstChar { c -> c.uppercase() } }
if (camel in schemas) dot to camel else null
}
val dedup = rule.dedups[index]
if (index == 0) return@map tag
renames[dedup.name] = Rename(rule.original, dedup)
JsonObject(item + ("name" to JsonPrimitive(dedup.name)))
}
rules.forEach { (name, rule) ->
val count = counts[name] ?: 0
if (count > 1 && count != rule.dedups.size) {
throw GradleException("Duplicate OpenAPI tag '$name' has $count entries but ${rule.dedups.size} final names.")
}
}
return JsonArray(fixed) to renames
.toMap()
if (dotMap.isEmpty()) return root to emptyMap()
// Remove dot schemas.
val cleaned = JsonObject(schemas.filterKeys { it !in dotMap })
val noDotsComponents = JsonObject(components + mapOf("schemas" to cleaned))
val noDotsRoot = JsonObject(root + mapOf("components" to noDotsComponents))
// Rewrite $ref strings throughout the whole spec.
val rewritten = rewriteRefs(noDotsRoot, dotMap)
return rewritten to dotMap
}
private fun fixPaths(paths: JsonObject, renames: Map<String, Rename>): JsonObject {
val ops = renames.values.flatMap { rename ->
rename.dedup.ops.map { id -> id to rename }
}.toMap()
val hits = mutableMapOf<String, Int>()
val fixed = JsonObject(paths.mapValues { (_, item) ->
/**
* Recursively rewrite every JsonPrimitive `$ref` value that matches a
* dot-notation schema name, replacing it with the camelCase equivalent.
*/
private fun rewriteRefs(element: JsonElement, map: Map<String, String>): JsonObject {
return rewriteElement(element, map) as JsonObject
}
private fun rewriteElement(element: JsonElement, map: Map<String, String>): JsonElement =
when (element) {
is JsonObject -> JsonObject(element.mapValues { (key, value) ->
if (key == "\$ref" && value is JsonPrimitive) {
val ref = value.content
val prefix = "#/components/schemas/"
if (ref.startsWith(prefix)) {
val name = ref.removePrefix(prefix)
val replaced = map[name]
if (replaced != null) JsonPrimitive("$prefix$replaced") else value
} else value
} else rewriteElement(value, map)
})
is JsonArray -> JsonArray(element.map { rewriteElement(it, map) })
else -> element
}
/**
* Remove the "tags" field from every operation so that openapi-generator
* collects all operations into a single DefaultApi class.
*/
private fun stripTags(root: JsonObject): JsonObject {
val paths = root["paths"] as? JsonObject ?: return root
val stripped = JsonObject(paths.mapValues { (_, item) ->
val path = item as? JsonObject ?: return@mapValues item
JsonObject(path.mapValues { (_, op) ->
val obj = op as? JsonObject ?: return@mapValues op
val id = text(obj["operationId"]) ?: return@mapValues op
val rename = ops[id] ?: return@mapValues op
hits[id] = hits.getOrDefault(id, 0) + 1
fixOp(obj, rename)
if ("tags" !in obj) return@mapValues op
JsonObject(obj.filterKeys { it != "tags" })
})
})
ops.keys.forEach { id ->
val count = hits[id] ?: 0
if (count != 1) {
throw GradleException("Expected one OpenAPI operation '$id' for tag normalization, found $count.")
}
}
return fixed
return JsonObject(root + mapOf("paths" to stripped))
}
private fun fixOp(op: JsonObject, rename: Rename): JsonObject {
val tags = op["tags"] as? JsonArray
?: throw GradleException("OpenAPI operation must declare tags before tag normalization.")
val count = tags.count { tag -> text(tag) == rename.from }
if (count != 1) {
throw GradleException("Expected one '${rename.from}' operation tag, found $count.")
}
return JsonObject(op + ("tags" to JsonArray(tags.map { tag ->
if (text(tag) != rename.from) return@map tag
JsonPrimitive(rename.dedup.name)
})))
/**
* Fix the `/kilo/profile` GET 200 response schema: Effect's OpenAPI generator
* emits `balance` and `currentOrgId` as non-nullable required fields even
* though the server schema is `Schema.NullOr(...)`. Wrap each non-nullable
* property in `anyOf: [<original-schema>, {"type": "null"}]` so the generated
* Kotlin model uses a nullable type. Already-nullable properties (those that
* already have `anyOf` containing `{"type":"null"}`) are left untouched.
*/
private fun fixProfileNullable(root: JsonObject): JsonObject {
val paths = root["paths"] as? JsonObject ?: return root
val profileItem = paths["/kilo/profile"] as? JsonObject ?: return root
val getOp = profileItem["get"] as? JsonObject ?: return root
val schema = getOp["responses"]
?.let { it as? JsonObject }?.get("200")
?.let { it as? JsonObject }?.get("content")
?.let { it as? JsonObject }?.get("application/json")
?.let { it as? JsonObject }?.get("schema")
as? JsonObject ?: return root
val props = schema["properties"] as? JsonObject ?: return root
val nullable = setOf("balance", "currentOrgId")
val fixed = JsonObject(props.mapValues { (key, value) ->
if (key !in nullable) return@mapValues value
val obj = value as? JsonObject ?: return@mapValues value
// Skip if already wrapped (has anyOf containing {type:null}).
val existing = obj["anyOf"] as? JsonArray
if (existing != null && existing.any {
(it as? JsonObject)?.get("type")?.let { t -> (t as? JsonPrimitive)?.content } == "null"
}) return@mapValues value
JsonObject(mapOf("anyOf" to JsonArray(listOf(obj, JsonObject(mapOf("type" to JsonPrimitive("null")))))))
})
// Rebuild nested objects up to root.
val responses = getOp["responses"]!! as JsonObject
val resp200 = responses["200"]!! as JsonObject
val content = resp200["content"]!! as JsonObject
val appJson = content["application/json"]!! as JsonObject
val newSchema = JsonObject(schema + mapOf("properties" to fixed))
val newApp = JsonObject(appJson + mapOf("schema" to newSchema))
val newContent = JsonObject(content + mapOf("application/json" to newApp))
val new200 = JsonObject(resp200 + mapOf("content" to newContent))
val newResponses = JsonObject(responses + mapOf("200" to new200))
val newGet = JsonObject(getOp + mapOf("responses" to newResponses))
val newProfile = JsonObject(profileItem + mapOf("get" to newGet))
val newPaths = JsonObject(paths + mapOf("/kilo/profile" to newProfile))
return JsonObject(root + mapOf("paths" to newPaths))
}
private fun rules(): Map<String, DuplicateTagRule> {
if (duplicateTagRules.map { it.original }.toSet().size != duplicateTagRules.size) {
throw GradleException("OpenAPI duplicate tag config must not repeat original tag names.")
}
return duplicateTagRules.associateBy { rule ->
if (rule.dedups.size < 2 || rule.dedups.first().name != rule.original) {
throw GradleException("OpenAPI tag rule '${rule.original}' must keep the original tag first.")
}
if (rule.dedups.map { it.name }.toSet().size != rule.dedups.size) {
throw GradleException("OpenAPI tag rule '${rule.original}' must use unique dedup names.")
}
rule.original
/**
* Deduplicate the root-level "tags" array by name — the spec validator
* rejects repeated tag names even when they describe different things.
*/
private fun dedupRootTags(root: JsonObject): JsonObject {
val tags = root["tags"] as? JsonArray ?: return root
val seen = mutableSetOf<String>()
val deduped = tags.filter { tag ->
val name = (tag as? JsonObject)?.let { (it["name"] as? JsonPrimitive)?.content }
?: return@filter true
seen.add(name)
}
return JsonObject(root + mapOf("tags" to JsonArray(deduped)))
}
private fun text(value: JsonElement?) = (value as? JsonPrimitive)?.content
private data class Rename(
val from: String,
val dedup: TagDedup,
)
}
@@ -5,15 +5,13 @@ import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import org.gradle.api.GradleException
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
import kotlin.test.assertNull
class OpenApiSpecNormalizerTest {
@Test
fun `renames configured duplicate tags and linked operations`() {
fun `strips tags from all operations`() {
val raw = """
{
"paths": {
@@ -23,61 +21,207 @@ class OpenApiSpecNormalizerTest {
"operationId": "pty.list"
}
},
"/pty/{ptyID}/connect": {
"/session": {
"post": {
"tags": ["session"],
"operationId": "session.create"
}
}
}
}
""".trimIndent()
val root = obj(OpenApiSpecNormalizer.normalize(raw))
val paths = obj(root["paths"])
val pty = obj(obj(paths["/pty"])["get"])
val session = obj(obj(paths["/session"])["post"])
assertNull(pty["tags"], "tags should be stripped from pty operation")
assertNull(session["tags"], "tags should be stripped from session operation")
}
@Test
fun `leaves operations without tags unchanged`() {
val raw = """
{
"paths": {
"/health": {
"get": {
"tags": ["pty"],
"operationId": "pty.connect"
"operationId": "health.get"
}
}
}
}
""".trimIndent()
val root = obj(OpenApiSpecNormalizer.normalize(raw))
val paths = obj(root["paths"])
val health = obj(obj(paths["/health"])["get"])
assertNull(health["tags"])
assertEquals("health.get", text(health["operationId"]))
}
@Test
fun `removes dot schemas and rewrites refs to camelCase equivalents`() {
val raw = """
{
"paths": {
"/tui/publish": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"${'$'}ref": "#/components/schemas/Event.tui.command.execute"
}
}
}
}
}
}
},
"components": {
"schemas": {
"EventTuiCommandExecute": { "type": "object" },
"Event.tui.command.execute": { "type": "object" },
"Session": { "type": "object" }
}
}
}
""".trimIndent()
val root = obj(OpenApiSpecNormalizer.normalize(raw))
val schemas = obj(obj(root["components"])["schemas"])
assertNull(schemas["Event.tui.command.execute"], "dot schema should be removed")
assert("EventTuiCommandExecute" in schemas) { "camelCase schema should be kept" }
assert("Session" in schemas) { "non-dot schema should be kept" }
// Check that the $ref was rewritten
val post = obj(obj(obj(obj(root["paths"])["/tui/publish"])["post"])["requestBody"])
val schema = obj(obj(obj(post["content"])["application/json"])["schema"])
assertEquals("#/components/schemas/EventTuiCommandExecute", text(schema["\$ref"]))
}
@Test
fun `makes balance and currentOrgId nullable in kilo profile response`() {
val raw = """
{
"paths": {
"/kilo/profile": {
"get": {
"operationId": "kilo.profile",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"profile": { "type": "object", "properties": { "email": { "type": "string" } }, "required": ["email"], "additionalProperties": false },
"balance": { "type": "object", "properties": { "balance": { "type": "number" } }, "required": ["balance"], "additionalProperties": false },
"currentOrgId": { "type": "string" }
},
"required": ["profile", "balance", "currentOrgId"],
"additionalProperties": false
}
}
}
}
}
}
}
}
}
""".trimIndent()
val root = obj(OpenApiSpecNormalizer.normalize(raw))
val schema = obj(obj(obj(obj(obj(obj(root["paths"])["/kilo/profile"])["get"])["responses"])["200"])["content"])
val props = obj(obj(obj(schema["application/json"])["schema"])["properties"])
// balance must be anyOf [object, null]
val balance = obj(props["balance"])
val balanceAnyOf = arr(balance["anyOf"])
assertEquals(2, balanceAnyOf.size, "balance should have anyOf with 2 entries")
val balanceTypes = balanceAnyOf.map { (it as? JsonObject)?.get("type").let { t -> (t as? JsonPrimitive)?.content } }
assert("null" in balanceTypes) { "balance anyOf should include null but got $balanceTypes" }
assert(balanceAnyOf.any { it is JsonObject && "properties" in it }) { "balance anyOf should include the object schema" }
// currentOrgId must be anyOf [string, null]
val orgId = obj(props["currentOrgId"])
val orgIdAnyOf = arr(orgId["anyOf"])
assertEquals(2, orgIdAnyOf.size, "currentOrgId should have anyOf with 2 entries")
val orgIdTypes = orgIdAnyOf.map { (it as? JsonObject)?.get("type").let { t -> (t as? JsonPrimitive)?.content } }
assert("null" in orgIdTypes) { "currentOrgId anyOf should include null but got $orgIdTypes" }
assert("string" in orgIdTypes) { "currentOrgId anyOf should include string but got $orgIdTypes" }
// profile must remain unchanged (not wrapped in anyOf)
val profile = obj(props["profile"])
assertNull(profile["anyOf"], "profile should not be wrapped in anyOf")
assertEquals("object", text(profile["type"]))
}
@Test
fun `leaves already-nullable fields unchanged in kilo profile response`() {
// If balance already has anyOf (i.e. the spec was generated correctly), normalizer must not double-wrap it.
val raw = """
{
"paths": {
"/kilo/profile": {
"get": {
"operationId": "kilo.profile",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"profile": { "type": "object", "properties": { "email": { "type": "string" } }, "required": ["email"], "additionalProperties": false },
"balance": { "anyOf": [{ "type": "object", "properties": { "balance": { "type": "number" } }, "required": ["balance"], "additionalProperties": false }, { "type": "null" }] },
"currentOrgId": { "anyOf": [{ "type": "string" }, { "type": "null" }] }
},
"required": ["profile", "balance", "currentOrgId"],
"additionalProperties": false
}
}
}
}
}
}
}
}
}
""".trimIndent()
val root = obj(OpenApiSpecNormalizer.normalize(raw))
val schema = obj(obj(obj(obj(obj(obj(root["paths"])["/kilo/profile"])["get"])["responses"])["200"])["content"])
val props = obj(obj(obj(schema["application/json"])["schema"])["properties"])
// balance must still have exactly 2 anyOf entries (not wrapped again)
val balance = obj(props["balance"])
val balanceAnyOf = arr(balance["anyOf"])
assertEquals(2, balanceAnyOf.size, "balance should still have exactly 2 anyOf entries, not be double-wrapped")
}
@Test
fun `deduplicates root-level tags array`() {
val raw = """
{
"paths": {},
"tags": [
{ "name": "pty", "description": "PTY routes." },
{ "name": "pty", "description": "PTY WebSocket route." }
{ "name": "pty", "description": "PTY WebSocket route." },
{ "name": "session", "description": "Session routes." }
]
}
""".trimIndent()
val root = obj(OpenApiSpecNormalizer.normalize(raw))
val tags = arr(root["tags"]).map { text(obj(it)["name"]) }
val paths = obj(root["paths"])
val pty = obj(obj(paths["/pty"])["get"])
val link = obj(obj(paths["/pty/{ptyID}/connect"])["get"])
assertEquals(listOf("pty", "pty-connect"), tags)
assertEquals(listOf("pty"), arr(pty["tags"]).map(::text))
assertEquals(listOf("pty-connect"), arr(link["tags"]).map(::text))
}
@Test
fun `keeps specs without duplicate configured tags unchanged`() {
val raw = """
{
"tags": [
{ "name": "pty", "description": "PTY routes." }
]
}
""".trimIndent()
assertEquals(raw, OpenApiSpecNormalizer.normalize(raw))
}
@Test
fun `fails when configured operation for renamed tag is absent`() {
val raw = """
{
"paths": {},
"tags": [
{ "name": "pty", "description": "PTY routes." },
{ "name": "pty", "description": "PTY WebSocket route." }
]
}
""".trimIndent()
val err = assertFailsWith<GradleException> {
OpenApiSpecNormalizer.normalize(raw)
}
assertTrue(err.message?.contains("Expected one OpenAPI operation 'pty.connect'") == true)
assertEquals(listOf("pty", "session"), tags, "duplicate pty tag should be removed")
}
private fun obj(raw: String) = Json.parseToJsonElement(raw) as JsonObject
+14 -1
View File
@@ -1610,7 +1610,20 @@ unix(
const run = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
yield* llm.wait(1)
yield* Effect.sleep(150)
// kilocode_change start
yield* waitFor(
"large bash output",
sessions.messages({ sessionID: chat.id }).pipe(
Effect.map((msgs) => {
const part = msgs.flatMap((msg) => msg.parts).find((part) => part.type === "tool")
if (part?.type !== "tool") return
if (part.state.status !== "running") return
if (!String(part.state.metadata?.output ?? "").includes("03999")) return
return part
}),
),
)
// kilocode_change end
yield* prompt.cancel(chat.id)
const exit = yield* Fiber.await(run)