fix(state): enable first-time state file save and block overwrites until explicit load

This commit is contained in:
2026-04-20 22:40:39 +08:00
parent d1da99c44d
commit a5cee79a8f
2 changed files with 111 additions and 6 deletions

View File

@@ -33,6 +33,7 @@ object StateCenter {
}
if (!finalStatePath.exists()) {
stateRecord.saveEnabled = true
return null
}
@@ -43,18 +44,18 @@ object StateCenter {
return null
}
stateRecord.loaded = true
stateRecord.saveEnabled = true
return JSONObject.parseObject(finalStatePath.readText())
}
fun load(path: Path) {
val finalStatePath = ConfigCenter.paths.stateDir.normalize().resolve(path).normalize()
if (!stateRegistry.containsKey(path)) {
if (!stateRegistry.containsKey(finalStatePath)) {
return
}
val record = stateRegistry[finalStatePath] ?: return
record.loaded = true
record.saveEnabled = true
if (!finalStatePath.exists()) {
return
}
@@ -68,7 +69,7 @@ object StateCenter {
fun save() {
stateRegistry.forEach { (path, record) ->
if (!record.loaded) {
if (!record.saveEnabled) {
return@forEach
}
path.parent?.let(Files::createDirectories)
@@ -156,5 +157,5 @@ sealed interface StateValue {
data class StateRecord(
val serializable: StateSerializable,
var loaded: Boolean = false
var saveEnabled: Boolean = false
)

View File

@@ -0,0 +1,104 @@
package work.slhaf.partner.framework.agent.state;
import com.alibaba.fastjson2.JSONObject;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import work.slhaf.partner.framework.agent.config.ConfigCenter;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class StateCenterTest {
@SuppressWarnings("unchecked")
private static void clearRegistry() throws Exception {
Field registryField = StateCenter.class.getDeclaredField("stateRegistry");
registryField.setAccessible(true);
ConcurrentHashMap<Path, StateRecord> registry = (ConcurrentHashMap<Path, StateRecord>) registryField.get(StateCenter.INSTANCE);
registry.clear();
}
@AfterEach
void tearDown() throws Exception {
clearRegistry();
}
@Test
void shouldCreateStateFileOnFirstSaveWhenNoExistingFile() throws Exception {
Path relativePath = Path.of("state-center-test", UUID.randomUUID() + ".json");
Path finalPath = ConfigCenter.INSTANCE.getPaths().getStateDir().resolve(relativePath).normalize();
Files.deleteIfExists(finalPath);
StubStateSerializable serializable = new StubStateSerializable(relativePath, false, "fresh");
serializable.register();
StateCenter.INSTANCE.save();
assertTrue(Files.exists(finalPath));
JSONObject saved = JSONObject.parseObject(Files.readString(finalPath, StandardCharsets.UTF_8));
assertEquals("fresh", saved.getString("value"));
}
@Test
void shouldNotOverwriteExistingFileUntilManualLoadWhenAutoLoadDisabled() throws Exception {
Path relativePath = Path.of("state-center-test", UUID.randomUUID() + ".json");
Path finalPath = ConfigCenter.INSTANCE.getPaths().getStateDir().resolve(relativePath).normalize();
Files.createDirectories(finalPath.getParent());
Files.writeString(finalPath, "{\"value\":\"original\"}", StandardCharsets.UTF_8);
StubStateSerializable serializable = new StubStateSerializable(relativePath, false, "new-value");
serializable.register();
StateCenter.INSTANCE.save();
JSONObject untouched = JSONObject.parseObject(Files.readString(finalPath, StandardCharsets.UTF_8));
assertEquals("original", untouched.getString("value"));
serializable.load();
serializable.value = "after-load";
StateCenter.INSTANCE.save();
JSONObject saved = JSONObject.parseObject(Files.readString(finalPath, StandardCharsets.UTF_8));
assertEquals("after-load", saved.getString("value"));
}
private static final class StubStateSerializable implements StateSerializable {
private final Path statePath;
private final boolean autoLoadOnRegister;
private String value;
private StubStateSerializable(Path statePath, boolean autoLoadOnRegister, String value) {
this.statePath = statePath;
this.autoLoadOnRegister = autoLoadOnRegister;
this.value = value;
}
@Override
public @NotNull Path statePath() {
return statePath;
}
@Override
public void load(JSONObject state) {
value = state.getString("value");
}
@Override
public @NotNull State convert() {
State state = new State();
state.append("value", StateValue.Companion.str(value));
return state;
}
@Override
public boolean autoLoadOnRegister() {
return autoLoadOnRegister;
}
}
}