Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions src/main/kotlin/ai/opencode/ide/jetbrains/OpenCodeService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ class OpenCodeService(private val project: Project) : Disposable {
ApplicationManager.getApplication().invokeLater(it)
}

internal var pasteDispatch: (() -> Unit) -> Unit = { task ->
AppExecutorUtil.getAppScheduledExecutorService().schedule({
ApplicationManager.getApplication().invokeLater { task() }
}, 100L, TimeUnit.MILLISECONDS)
}

// ==================== Public API ====================

fun focusOrCreateTerminal(interactive: Boolean = false) {
Expand Down Expand Up @@ -322,14 +328,12 @@ class OpenCodeService(private val project: Project) : Disposable {
try {
val files = diffs.mapNotNull { diff ->
PathUtil.resolveProjectPath(project, diff.file)?.let {
val ioFile = java.io.File(it)
// If file is deleted, we must refresh the parent directory to detect deletion
if (!ioFile.exists()) ioFile.parentFile else ioFile
java.io.File(it).takeIf { it.exists() }
}
}
if (files.isNotEmpty()) {
logger.info("[OpenCode] Forcing VFS refresh for ${files.size} paths: ${files.map { it.absolutePath }}")
com.intellij.openapi.vfs.LocalFileSystem.getInstance().refreshIoFiles(files, false, false, null)
logger.info("[OpenCode] Forcing VFS refresh for ${files.size} paths")
com.intellij.openapi.vfs.LocalFileSystem.getInstance().refreshIoFiles(files, true, false, null)
}
} catch (e: Exception) {
logger.debug("[OpenCode] VFS refresh skipped: ${e.message}")
Expand Down Expand Up @@ -815,15 +819,13 @@ class OpenCodeService(private val project: Project) : Disposable {
}
return
}
AppExecutorUtil.getAppScheduledExecutorService().schedule({
ApplicationManager.getApplication().invokeLater {
if (!project.isDisposed) {
if (!pasteToTerminal(t)) {
schedulePasteAttempt(t, l - 1, d)
}
pasteDispatch {
if (!project.isDisposed) {
if (!pasteToTerminal(t)) {
schedulePasteAttempt(t, l - 1, d)
}
}
}, d, TimeUnit.MILLISECONDS)
}
}
}

private fun extractPartMessageInfo(p: JsonElement): PartMessageInfo? { if (!p.isJsonObject) return null; val o = p.asJsonObject; val mId = o.get("messageID")?.asString; val sId = o.get("sessionID")?.asString; return if (mId != null && sId != null) PartMessageInfo(sId, mId) else null }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import ai.opencode.ide.jetbrains.session.SessionManager
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.components.service
import com.intellij.testFramework.PlatformTestUtil
import com.intellij.testFramework.MapDataContext
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean

class SendSelectionToTerminalActionTest : BasePlatformTestCase() {
Expand Down Expand Up @@ -68,19 +68,17 @@ class SendSelectionToTerminalActionTest : BasePlatformTestCase() {

val event = AnActionEvent.createFromDataContext("place", null, dataContext)

// 3. Perform Action
// 3. Override paste dispatch to run synchronously (no thread hops)
service.pasteDispatch = { task -> task() }

// 4. Perform Action
action.actionPerformed(event)

// 4. Verify
// Wait for server to receive request (async)
val start = System.currentTimeMillis()
while (server?.receivedPrompts?.isEmpty() == true && System.currentTimeMillis() - start < 2000) {
PlatformTestUtil.dispatchAllEventsInIdeEventQueue()
Thread.sleep(50)
}
// 5. Verify
val latch = server?.promptLatch ?: return
assertTrue("Server should have received a prompt", latch.await(5, TimeUnit.SECONDS))

val prompts = server?.receivedPrompts ?: emptyList()
assertFalse("Server should have received a prompt", prompts.isEmpty())

val lastPrompt = prompts.last()
println("Received prompt JSON: $lastPrompt")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import java.io.OutputStream
import java.net.InetSocketAddress
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors

class FakeOpenCodeServer(val port: Int) {
Expand All @@ -13,6 +14,7 @@ class FakeOpenCodeServer(val port: Int) {
private val diffResponses = ConcurrentHashMap<String, String>()
private val diffDelays = ConcurrentHashMap<String, Long>()
val receivedPrompts = CopyOnWriteArrayList<String>()
val promptLatch = CountDownLatch(1)

val activePort: Int
get() = server.address.port
Expand All @@ -32,6 +34,7 @@ class FakeOpenCodeServer(val port: Int) {
println(" [FakeServer] POST /tui/append-prompt: $body")
// Keep raw JSON for assertions in tests.
receivedPrompts.add(body)
promptLatch.countDown()

val resp = "{}".toByteArray()
ex.sendResponseHeaders(200, resp.size.toLong())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,20 @@ import ai.opencode.ide.jetbrains.diff.DiffViewerService
import ai.opencode.ide.jetbrains.session.SessionManager
import com.intellij.history.Label
import com.intellij.openapi.project.Project
import java.lang.reflect.Proxy
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import java.util.concurrent.CopyOnWriteArrayList
import org.junit.After
import org.junit.Before
import org.junit.Test

/**
* Logic Unit Test using Fake Server.
* Simplified suite covering core Diff scenarios.
*/
class OpenCodeLogicTest {
@Before
fun setUp() {
class OpenCodeLogicTest : BasePlatformTestCase() {

override fun setUp() {
super.setUp()
OpenCodeService.DEBOUNCE_MS = 0L
}

@After
fun tearDown() {
}

@Test

fun testDiffScenarios() {
println("=== Starting OpenCode Logic Tests (Diff Isolation) ===")

Expand All @@ -38,12 +30,14 @@ class OpenCodeLogicTest {
val port = server.activePort

// Setup Test Runner (Mock IDE)
val runner = TestRunner(port)
val runner = TestRunner(project, port)

val r = runner
val s = server
val baseDir = project.basePath!!

try {
java.io.File(baseDir).mkdirs()
// Wait for connection to establish
Thread.sleep(500)

Expand Down Expand Up @@ -127,8 +121,7 @@ class OpenCodeLogicTest {
Thread.sleep(100)

// 2. Simulate AI writing a new file to disk
val tempDir = System.getProperty("java.io.tmpdir")
val newFile = java.io.File(tempDir, "new.kt")
val newFile = java.io.File(baseDir, "new.kt")
newFile.writeText("fun new() {}")
r.sessionManager.simulateFileCreation("new.kt") // Signal VFS creation

Expand Down Expand Up @@ -196,7 +189,7 @@ class OpenCodeLogicTest {
r.resetState()

// 1. Prepare file
val toDelete = java.io.File(tempDir, "to_delete.kt")
val toDelete = java.io.File(baseDir, "to_delete.kt")
toDelete.writeText("delete me")

// 2. Start Turn
Expand Down Expand Up @@ -266,7 +259,7 @@ class OpenCodeLogicTest {
s.broadcast("""{"type":"session.status","properties":{"sessionID":"s1","status":{"type":"busy"}}}""")
Thread.sleep(100)

val file1 = java.io.File(tempDir, "1.md")
val file1 = java.io.File(baseDir, "1.md")
file1.writeText("created content")
r.sessionManager.simulateFileCreation("1.md")
s.broadcast("""{"type":"file.edited","properties":{"file":"1.md"}}""")
Expand Down Expand Up @@ -320,36 +313,19 @@ class OpenCodeLogicTest {
}
}

class TestRunner(val serverPort: Int) {
val mockProject: Project
class TestRunner(val project: Project, val serverPort: Int) {
val mockDiffViewer: MockDiffViewerService
val sessionManager: TestSessionManager
val openCodeService: OpenCodeService

init {
// Setup Mocks
val tempDir = System.getProperty("java.io.tmpdir")
mockProject = Proxy.newProxyInstance(
Project::class.java.classLoader,
arrayOf(Project::class.java)
) { _, method, _ ->
when (method.name) {
"getBasePath" -> tempDir
"isDisposed" -> false
"toString" -> "MockProject"
"hashCode" -> 12345
"equals" -> false
else -> null
}
} as Project

mockDiffViewer = MockDiffViewerService(mockProject)
sessionManager = TestSessionManager(mockProject)
mockDiffViewer = MockDiffViewerService(project)
sessionManager = TestSessionManager(project)

// Initialize Service with REAL ApiClient pointing to Fake Server
val apiClient = OpenCodeApiClient("127.0.0.1", serverPort)

openCodeService = OpenCodeService(mockProject)
openCodeService = OpenCodeService(project)
openCodeService.setTestDeps(sessionManager, mockDiffViewer, apiClient)

// Mock UI execution
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ class RealProcessIntegrationTest : BasePlatformTestCase() {
port = PortFinder.findAvailablePort()

// Find opencode executable
val isWin = System.getProperty("os.name", "").lowercase().contains("windows")
val candidates = if (isWin) listOf("opencode.cmd", "opencode.bat", "opencode.exe", "opencode") else listOf("opencode")
val path = System.getenv("PATH").split(File.pathSeparator)
.map { File(it, "opencode") }
.flatMap { dir -> candidates.map { File(dir, it) } }
.firstOrNull { it.exists() && it.canExecute() }
?.absolutePath ?: throw IllegalStateException("opencode binary not found in PATH")

Expand Down