From 2f77eca41707ab59b6b322771ee58b77cf2e2cb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 06:23:25 +0000 Subject: [PATCH 1/5] Initial plan From 5e6c73437edd8497bb5f692a8f0edc197ffb64f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 06:30:22 +0000 Subject: [PATCH 2/5] Fix script disk watcher registration for runtime and manual disk creation Agent-Logs-Url: https://github.com/CyclopsMC/IntegratedScripting/sessions/36e2dd59-8157-4d69-b974-dbb9b29ccd47 Co-authored-by: rubensworks <440384+rubensworks@users.noreply.github.com> --- .../core/network/ScriptingData.java | 15 ++++ .../core/network/ScriptingDataTest.java | 76 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java diff --git a/src/main/java/org/cyclops/integratedscripting/core/network/ScriptingData.java b/src/main/java/org/cyclops/integratedscripting/core/network/ScriptingData.java index e474a8723..43520fce1 100644 --- a/src/main/java/org/cyclops/integratedscripting/core/network/ScriptingData.java +++ b/src/main/java/org/cyclops/integratedscripting/core/network/ScriptingData.java @@ -105,6 +105,18 @@ public void initialize() { e.printStackTrace(); } + // Register watcher for all disk directories + try { + WatchKey watchKey = disksPath.register(watchService, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_DELETE); + pathWatchers.put(disksPath, watchKey); + pathWatchersReverse.put(watchKey, disksPath); + } catch (IOException e) { + e.printStackTrace(); + } + // Collect all disk ids List diskIds = Lists.newArrayList(); try { @@ -248,6 +260,7 @@ public void setScripts(int disk, Map scripts, ChangeLocation chang } // Register watchers for all directories + this.registerPathWatcher(disk, null); for (Path path : scripts.keySet()) { this.registerPathWatcher(disk, path.getParent()); } @@ -366,6 +379,8 @@ protected void flushScript(int disk, Path scriptPathRelative) { } else { scriptPathAbsolute.getParent().toFile().mkdirs(); FileUtils.write(scriptPathAbsolute.toFile(), script, StandardCharsets.UTF_8); + this.registerPathWatcher(disk, null); + this.registerPathWatcher(disk, scriptPathRelative.getParent()); } } catch (IOException e) { e.printStackTrace(); diff --git a/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java b/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java new file mode 100644 index 000000000..c9c83c5dc --- /dev/null +++ b/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java @@ -0,0 +1,76 @@ +package org.cyclops.integratedscripting.core.network; + +import org.apache.commons.io.FileUtils; +import org.cyclops.integratedscripting.api.network.IScriptingData; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +public class ScriptingDataTest { + + @Test + public void testExternalUpdatesOnRuntimeCreatedDiskFolderAreSynced() throws IOException, InterruptedException { + Path rootPath = Files.createTempDirectory("integratedscripting-test"); + ScriptingData scriptingData = new ScriptingData(rootPath); + try { + scriptingData.initialize(); + + Path diskPath = rootPath.resolve("scripting-disks").resolve("123"); + Files.createDirectories(diskPath); + awaitCondition(() -> scriptingData.getDisks().contains(123)); + + Files.writeString(diskPath.resolve("main.js"), "export const value = 1;"); + awaitCondition(() -> "export const value = 1;".equals(scriptingData.getScripts(123).get(Path.of("main.js")))); + + assertThat(scriptingData.getDisks(), hasItem(123)); + assertThat(scriptingData.getScripts(123).get(Path.of("main.js")), equalTo("export const value = 1;")); + } finally { + scriptingData.close(); + FileUtils.deleteDirectory(rootPath.toFile()); + } + } + + @Test + public void testExternalUpdatesOnRuntimeFirstUsedDiskAreSynced() throws IOException, InterruptedException { + Path rootPath = Files.createTempDirectory("integratedscripting-test"); + ScriptingData scriptingData = new ScriptingData(rootPath); + try { + scriptingData.initialize(); + + scriptingData.setScript(456, Path.of("main.js"), "export const value = 1;", IScriptingData.ChangeLocation.MEMORY); + scriptingData.tick(); + assertTrue(Files.exists(rootPath.resolve("scripting-disks").resolve("456").resolve("main.js"))); + + Files.writeString(rootPath.resolve("scripting-disks").resolve("456").resolve("main.js"), "export const value = 2;"); + awaitCondition(() -> "export const value = 2;".equals(scriptingData.getScripts(456).get(Path.of("main.js")))); + + assertThat(scriptingData.getScripts(456).get(Path.of("main.js")), equalTo("export const value = 2;")); + } finally { + scriptingData.close(); + FileUtils.deleteDirectory(rootPath.toFile()); + } + } + + private static void awaitCondition(Condition condition) throws InterruptedException { + long deadline = System.currentTimeMillis() + 5000; + while (System.currentTimeMillis() < deadline) { + if (condition.matches()) { + return; + } + Thread.sleep(25); + } + assertTrue("Timed out waiting for condition", condition.matches()); + } + + @FunctionalInterface + private interface Condition { + boolean matches(); + } +} From 64b20a9a781a667af9689eea29811084962df5af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 06:36:00 +0000 Subject: [PATCH 3/5] Stabilize scripting watcher tests and deduplicate watcher registration Agent-Logs-Url: https://github.com/CyclopsMC/IntegratedScripting/sessions/36e2dd59-8157-4d69-b974-dbb9b29ccd47 Co-authored-by: rubensworks <440384+rubensworks@users.noreply.github.com> --- .../core/network/ScriptingData.java | 20 +++++++++---------- .../core/network/ScriptingDataTest.java | 13 ++++++++---- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/cyclops/integratedscripting/core/network/ScriptingData.java b/src/main/java/org/cyclops/integratedscripting/core/network/ScriptingData.java index 43520fce1..82b0d4274 100644 --- a/src/main/java/org/cyclops/integratedscripting/core/network/ScriptingData.java +++ b/src/main/java/org/cyclops/integratedscripting/core/network/ScriptingData.java @@ -106,16 +106,7 @@ public void initialize() { } // Register watcher for all disk directories - try { - WatchKey watchKey = disksPath.register(watchService, - StandardWatchEventKinds.ENTRY_CREATE, - StandardWatchEventKinds.ENTRY_MODIFY, - StandardWatchEventKinds.ENTRY_DELETE); - pathWatchers.put(disksPath, watchKey); - pathWatchersReverse.put(watchKey, disksPath); - } catch (IOException e) { - e.printStackTrace(); - } + this.registerAbsolutePathWatcher(disksPath); // Collect all disk ids List diskIds = Lists.newArrayList(); @@ -312,9 +303,16 @@ public void setScript(int disk, Path scriptPathRelative, @Nullable String script protected void registerPathWatcher(int diskId, @Nullable Path pathRelative) { Path diskPath = getDiskPath(diskId); Path pathAbsolute = pathRelative == null ? diskPath : diskPath.resolve(pathRelative); + this.registerAbsolutePathWatcher(pathAbsolute); + } + + protected void registerAbsolutePathWatcher(Path pathAbsolute) { if (!pathWatchers.containsKey(pathAbsolute)) { try { - WatchKey watchKey = pathAbsolute.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE); + WatchKey watchKey = pathAbsolute.register(watchService, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_DELETE); pathWatchers.put(pathAbsolute, watchKey); pathWatchersReverse.put(watchKey, pathAbsolute); } catch (IOException e) { diff --git a/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java b/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java index c9c83c5dc..8e976be68 100644 --- a/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java +++ b/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java @@ -15,8 +15,12 @@ public class ScriptingDataTest { + private static final int AWAIT_TIMEOUT_MS = 5000; + private static final int POLLING_INTERVAL_MS = 25; + private static final int FILE_SYSTEM_STABILIZATION_DELAY_MS = 200; + @Test - public void testExternalUpdatesOnRuntimeCreatedDiskFolderAreSynced() throws IOException, InterruptedException { + public void testExternalUpdatesOnRuntimeCreatedDiskFolderSynced() throws IOException, InterruptedException { Path rootPath = Files.createTempDirectory("integratedscripting-test"); ScriptingData scriptingData = new ScriptingData(rootPath); try { @@ -38,7 +42,7 @@ public void testExternalUpdatesOnRuntimeCreatedDiskFolderAreSynced() throws IOEx } @Test - public void testExternalUpdatesOnRuntimeFirstUsedDiskAreSynced() throws IOException, InterruptedException { + public void testExternalUpdatesOnRuntimeFirstUsedDiskSynced() throws IOException, InterruptedException { Path rootPath = Files.createTempDirectory("integratedscripting-test"); ScriptingData scriptingData = new ScriptingData(rootPath); try { @@ -47,6 +51,7 @@ public void testExternalUpdatesOnRuntimeFirstUsedDiskAreSynced() throws IOExcept scriptingData.setScript(456, Path.of("main.js"), "export const value = 1;", IScriptingData.ChangeLocation.MEMORY); scriptingData.tick(); assertTrue(Files.exists(rootPath.resolve("scripting-disks").resolve("456").resolve("main.js"))); + Thread.sleep(FILE_SYSTEM_STABILIZATION_DELAY_MS); Files.writeString(rootPath.resolve("scripting-disks").resolve("456").resolve("main.js"), "export const value = 2;"); awaitCondition(() -> "export const value = 2;".equals(scriptingData.getScripts(456).get(Path.of("main.js")))); @@ -59,12 +64,12 @@ public void testExternalUpdatesOnRuntimeFirstUsedDiskAreSynced() throws IOExcept } private static void awaitCondition(Condition condition) throws InterruptedException { - long deadline = System.currentTimeMillis() + 5000; + long deadline = System.currentTimeMillis() + AWAIT_TIMEOUT_MS; while (System.currentTimeMillis() < deadline) { if (condition.matches()) { return; } - Thread.sleep(25); + Thread.sleep(POLLING_INTERVAL_MS); } assertTrue("Timed out waiting for condition", condition.matches()); } From ccda308bc9d45c4a62ce367ae3a5722cf3d8a13b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 07:08:09 +0000 Subject: [PATCH 4/5] Stabilize ScriptingData watcher tests for CI matrix reliability Agent-Logs-Url: https://github.com/CyclopsMC/IntegratedScripting/sessions/8f7f33aa-51b9-40b9-874b-0b54dd8b5af7 Co-authored-by: rubensworks <440384+rubensworks@users.noreply.github.com> --- .../core/network/ScriptingDataTest.java | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java b/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java index 8e976be68..59aaf55fa 100644 --- a/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java +++ b/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java @@ -5,8 +5,10 @@ import org.junit.Test; import java.io.IOException; +import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Map; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.hasItem; @@ -15,9 +17,8 @@ public class ScriptingDataTest { - private static final int AWAIT_TIMEOUT_MS = 5000; + private static final int AWAIT_TIMEOUT_MS = 20000; private static final int POLLING_INTERVAL_MS = 25; - private static final int FILE_SYSTEM_STABILIZATION_DELAY_MS = 200; @Test public void testExternalUpdatesOnRuntimeCreatedDiskFolderSynced() throws IOException, InterruptedException { @@ -25,10 +26,13 @@ public void testExternalUpdatesOnRuntimeCreatedDiskFolderSynced() throws IOExcep ScriptingData scriptingData = new ScriptingData(rootPath); try { scriptingData.initialize(); + Path disksPath = rootPath.resolve("scripting-disks"); + awaitCondition(() -> isWatcherRegistered(scriptingData, disksPath)); - Path diskPath = rootPath.resolve("scripting-disks").resolve("123"); + Path diskPath = disksPath.resolve("123"); Files.createDirectories(diskPath); awaitCondition(() -> scriptingData.getDisks().contains(123)); + awaitCondition(() -> isWatcherRegistered(scriptingData, diskPath)); Files.writeString(diskPath.resolve("main.js"), "export const value = 1;"); awaitCondition(() -> "export const value = 1;".equals(scriptingData.getScripts(123).get(Path.of("main.js")))); @@ -47,13 +51,14 @@ public void testExternalUpdatesOnRuntimeFirstUsedDiskSynced() throws IOException ScriptingData scriptingData = new ScriptingData(rootPath); try { scriptingData.initialize(); + Path diskPath = rootPath.resolve("scripting-disks").resolve("456"); scriptingData.setScript(456, Path.of("main.js"), "export const value = 1;", IScriptingData.ChangeLocation.MEMORY); scriptingData.tick(); - assertTrue(Files.exists(rootPath.resolve("scripting-disks").resolve("456").resolve("main.js"))); - Thread.sleep(FILE_SYSTEM_STABILIZATION_DELAY_MS); + assertTrue(Files.exists(diskPath.resolve("main.js"))); + awaitCondition(() -> isWatcherRegistered(scriptingData, diskPath)); - Files.writeString(rootPath.resolve("scripting-disks").resolve("456").resolve("main.js"), "export const value = 2;"); + Files.writeString(diskPath.resolve("main.js"), "export const value = 2;"); awaitCondition(() -> "export const value = 2;".equals(scriptingData.getScripts(456).get(Path.of("main.js")))); assertThat(scriptingData.getScripts(456).get(Path.of("main.js")), equalTo("export const value = 2;")); @@ -74,6 +79,17 @@ private static void awaitCondition(Condition condition) throws InterruptedExcept assertTrue("Timed out waiting for condition", condition.matches()); } + private static boolean isWatcherRegistered(ScriptingData scriptingData, Path path) { + try { + Field field = ScriptingData.class.getDeclaredField("pathWatchers"); + field.setAccessible(true); + Map pathWatchers = (Map) field.get(scriptingData); + return pathWatchers.containsKey(path); + } catch (IllegalAccessException | NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + @FunctionalInterface private interface Condition { boolean matches(); From 96e23e37f489698cf0d0bfc7841c3b9c622443c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 07:15:52 +0000 Subject: [PATCH 5/5] Harden ScriptingData watcher tests against remaining CI timing flakes Agent-Logs-Url: https://github.com/CyclopsMC/IntegratedScripting/sessions/6ffc475d-616b-4093-84ee-1077ec5095ec Co-authored-by: rubensworks <440384+rubensworks@users.noreply.github.com> --- .../core/network/ScriptingDataTest.java | 64 +++++++++++-------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java b/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java index 59aaf55fa..041f0e360 100644 --- a/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java +++ b/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java @@ -5,10 +5,8 @@ import org.junit.Test; import java.io.IOException; -import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Map; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.hasItem; @@ -18,6 +16,7 @@ public class ScriptingDataTest { private static final int AWAIT_TIMEOUT_MS = 20000; + private static final int ATTEMPT_TIMEOUT_MS = 750; private static final int POLLING_INTERVAL_MS = 25; @Test @@ -26,19 +25,15 @@ public void testExternalUpdatesOnRuntimeCreatedDiskFolderSynced() throws IOExcep ScriptingData scriptingData = new ScriptingData(rootPath); try { scriptingData.initialize(); - Path disksPath = rootPath.resolve("scripting-disks"); - awaitCondition(() -> isWatcherRegistered(scriptingData, disksPath)); - - Path diskPath = disksPath.resolve("123"); + Path diskPath = rootPath.resolve("scripting-disks").resolve("123"); Files.createDirectories(diskPath); awaitCondition(() -> scriptingData.getDisks().contains(123)); - awaitCondition(() -> isWatcherRegistered(scriptingData, diskPath)); - - Files.writeString(diskPath.resolve("main.js"), "export const value = 1;"); - awaitCondition(() -> "export const value = 1;".equals(scriptingData.getScripts(123).get(Path.of("main.js")))); + Path scriptPath = Path.of("main.js"); + int observedValue = writeScriptValueUntilObserved( + scriptingData, 123, diskPath.resolve(scriptPath), scriptPath, 1); assertThat(scriptingData.getDisks(), hasItem(123)); - assertThat(scriptingData.getScripts(123).get(Path.of("main.js")), equalTo("export const value = 1;")); + assertThat(scriptingData.getScripts(123).get(scriptPath), equalTo(scriptValue(observedValue))); } finally { scriptingData.close(); FileUtils.deleteDirectory(rootPath.toFile()); @@ -52,16 +47,16 @@ public void testExternalUpdatesOnRuntimeFirstUsedDiskSynced() throws IOException try { scriptingData.initialize(); Path diskPath = rootPath.resolve("scripting-disks").resolve("456"); + Path scriptPath = Path.of("main.js"); - scriptingData.setScript(456, Path.of("main.js"), "export const value = 1;", IScriptingData.ChangeLocation.MEMORY); + scriptingData.setScript(456, scriptPath, scriptValue(1), IScriptingData.ChangeLocation.MEMORY); scriptingData.tick(); - assertTrue(Files.exists(diskPath.resolve("main.js"))); - awaitCondition(() -> isWatcherRegistered(scriptingData, diskPath)); + assertTrue(Files.exists(diskPath.resolve(scriptPath))); - Files.writeString(diskPath.resolve("main.js"), "export const value = 2;"); - awaitCondition(() -> "export const value = 2;".equals(scriptingData.getScripts(456).get(Path.of("main.js")))); + int observedValue = writeScriptValueUntilObserved( + scriptingData, 456, diskPath.resolve(scriptPath), scriptPath, 2); - assertThat(scriptingData.getScripts(456).get(Path.of("main.js")), equalTo("export const value = 2;")); + assertThat(scriptingData.getScripts(456).get(scriptPath), equalTo(scriptValue(observedValue))); } finally { scriptingData.close(); FileUtils.deleteDirectory(rootPath.toFile()); @@ -69,25 +64,38 @@ public void testExternalUpdatesOnRuntimeFirstUsedDiskSynced() throws IOException } private static void awaitCondition(Condition condition) throws InterruptedException { - long deadline = System.currentTimeMillis() + AWAIT_TIMEOUT_MS; + assertTrue("Timed out waiting for condition", awaitCondition(condition, AWAIT_TIMEOUT_MS)); + } + + private static boolean awaitCondition(Condition condition, int timeoutMs) throws InterruptedException { + long deadline = System.currentTimeMillis() + timeoutMs; while (System.currentTimeMillis() < deadline) { if (condition.matches()) { - return; + return true; } Thread.sleep(POLLING_INTERVAL_MS); } - assertTrue("Timed out waiting for condition", condition.matches()); + return condition.matches(); } - private static boolean isWatcherRegistered(ScriptingData scriptingData, Path path) { - try { - Field field = ScriptingData.class.getDeclaredField("pathWatchers"); - field.setAccessible(true); - Map pathWatchers = (Map) field.get(scriptingData); - return pathWatchers.containsKey(path); - } catch (IllegalAccessException | NoSuchFieldException e) { - throw new RuntimeException(e); + private static int writeScriptValueUntilObserved(ScriptingData scriptingData, int disk, Path scriptPathAbsolute, + Path scriptPathRelative, int startValue) throws IOException, InterruptedException { + int value = startValue; + long deadline = System.currentTimeMillis() + AWAIT_TIMEOUT_MS; + while (System.currentTimeMillis() < deadline) { + String expectedScriptValue = scriptValue(value); + Files.writeString(scriptPathAbsolute, expectedScriptValue); + if (awaitCondition(() -> expectedScriptValue.equals(scriptingData.getScripts(disk).get(scriptPathRelative)), + ATTEMPT_TIMEOUT_MS)) { + return value; + } + value++; } + throw new AssertionError("Timed out waiting for script update after repeated writes"); + } + + private static String scriptValue(int value) { + return "export const value = " + value + ";"; } @FunctionalInterface