Fix Windows expansion journal persistence in alpha.13.1
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0-alpha.13.1 — 2026-09-09
|
||||
|
||||
- Corrige l’arrêt des expansions sous Windows : le journal ne tente plus
|
||||
d’ouvrir son dossier comme un fichier pour le synchroniser. Cette opération
|
||||
reste exigée sur les systèmes de fichiers utilisant le séparateur Unix.
|
||||
- Écriture du fichier synchronisée, remplacement atomique et relecture stricte
|
||||
conservés. Les véritables erreurs de stockage arrêtent toujours les expansions.
|
||||
- Après mise à jour et réouverture du même monde, une réservation valide déjà
|
||||
enregistrée avant l’erreur est reprise automatiquement. Aucun changement de
|
||||
schéma du journal, de génération, de paramètres ou de chunks existants.
|
||||
|
||||
Le smoke du journal passe sur macOS, avec politique Windows simulée, erreurs
|
||||
après remplacement et récupération au rechargement. Aucun essai Windows natif
|
||||
n’a encore été réalisé. Voir le [dépannage](docs/alpha13-worldgen.md#correctif-alpha131--expansions-sous-windows)
|
||||
et les [contrôles ciblés](docs/testing.md#correctif-alpha131--journal-dexpansion-sous-windows).
|
||||
|
||||
## 0.1.0-alpha.13 — 2026-09-09
|
||||
|
||||
- Un seul choix public **Sanctuary**, avec le bouton natif **Personnaliser** :
|
||||
|
||||
@@ -20,6 +20,18 @@ initiale et les outils d’expansion dans un seul choix de monde Sanctuary.
|
||||
Les trois tailles et l’interface native sont vérifiées. L’alpha.13 est publiée
|
||||
et la même instance Prism est synchronisée ; les versions précédentes restent historiques.
|
||||
|
||||
## Alpha.13.1 — correctif des expansions sous Windows
|
||||
|
||||
Le journal d’expansion n’essaie plus d’ouvrir son dossier comme un fichier sous
|
||||
Windows, ce qui provoquait `AccessDeniedException` et bloquait les expansions.
|
||||
La synchronisation du fichier, son remplacement atomique et sa relecture restent
|
||||
obligatoires. Le format du journal et la génération alpha.13 restent identiques.
|
||||
|
||||
Après installation du correctif, **rouvrir le même monde** : une réservation déjà
|
||||
enregistrée est reprise après vérification du journal. Il ne faut pas supprimer
|
||||
`sanctuary-world-v13` ni recréer le monde pour ce correctif. Voir le
|
||||
[dépannage Windows](docs/alpha13-worldgen.md) et la [validation](docs/testing.md).
|
||||
|
||||
## Alpha.13 — un seul monde Sanctuary
|
||||
|
||||
À la création d’un **nouveau monde**, sélectionner **Sanctuary**, puis utiliser
|
||||
|
||||
@@ -120,6 +120,37 @@ temples, cabanes de marais et camps abandonnés dépendent du biome, du diamètr
|
||||
et des fondations ; aucune présence n’est garantie. Les règles de progression
|
||||
futures restent décrites dans [Expansion](expansion.md).
|
||||
|
||||
## Correctif alpha.13.1 — expansions sous Windows
|
||||
|
||||
L’erreur `AccessDeniedException` visant le **dossier**
|
||||
`data/sanctuary-world-v13`, depuis `ExpansionJournal.persist`, vient de la
|
||||
synchronisation du répertoire après l’écriture du journal. Java sous Windows
|
||||
ne permet pas cette ouverture avec `FileChannel` ; ce message ne signifie pas
|
||||
à lui seul que le joueur manque de droits sur sa sauvegarde.
|
||||
|
||||
1. Fermer Minecraft pour arrêter la session dont les expansions sont bloquées.
|
||||
2. Mettre l’instance Prism existante à jour vers **0.1.0-alpha.13.1** via son
|
||||
canal packwiz, puis relancer le jeu.
|
||||
3. Rouvrir **le même monde** et consulter `/sanctuary expansion list`.
|
||||
|
||||
Si l’instance Windows a été importée depuis le MRpack sans auto-update, fermer
|
||||
Minecraft puis remplacer seulement `sanctuary-0.1.0-alpha.13.jar` par le JAR
|
||||
`sanctuary-0.1.0-alpha.13.1.jar` de la release dans son dossier `mods`.
|
||||
Conserver un seul JAR Sanctuary actif ; les autres mods et les sauvegardes
|
||||
restent dans la même instance. Le raccordement au canal stable est décrit dans
|
||||
[Distribution](packwiz.md#raccorder-linstance-prism-existante).
|
||||
|
||||
Le remplacement du journal ayant précédé cette erreur, l’expansion demandée
|
||||
peut déjà être enregistrée comme `reserved`. Au chargement, les réservations
|
||||
valides reprennent automatiquement avec leur identifiant, leur graine et leur
|
||||
position. Il n’est pas nécessaire de recréer le monde ou de supprimer le dossier
|
||||
du journal. La commande `resume` seule ne débloque pas une session dont le
|
||||
journal a été déclaré indisponible : sa relecture exige de rouvrir le monde.
|
||||
|
||||
Le correctif conserve le format et le contenu du journal existant ; il ne
|
||||
migre ni ne régénère les chunks. La [validation ciblée](testing.md#correctif-alpha131--journal-dexpansion-sous-windows)
|
||||
distingue les contrôles locaux de l’essai Windows natif restant à effectuer.
|
||||
|
||||
## Paramètres et sauvegardes
|
||||
|
||||
Le preset public `sanctuary:sanctuary` utilise le nouveau codec de générateur
|
||||
|
||||
@@ -509,6 +509,31 @@ réglages suivis conservés.
|
||||
Les limites, dont l’absence de preuve de camp dans l’origine unifiée des graines
|
||||
inspectées, sont documentées dans [Validation](testing.md).
|
||||
|
||||
### WG-16 — Journal d’expansion sous Windows — alpha.13.1
|
||||
|
||||
**Branche :** `codex/windows-expansion-journal`.
|
||||
|
||||
**Problème :** après le remplacement atomique du journal, l’ouverture de son
|
||||
répertoire par `FileChannel` échoue sous Windows avec `AccessDeniedException`.
|
||||
La session arrête alors toute expansion, même si la réservation a déjà été écrite.
|
||||
|
||||
- Écarter uniquement la synchronisation de répertoire non prise en charge par
|
||||
le système de fichiers Windows ; conserver force du fichier, remplacement
|
||||
atomique, relecture stricte et arrêt sur les véritables erreurs de stockage.
|
||||
- Conserver le schéma, les identifiants, la géométrie et les journaux existants.
|
||||
Après mise à jour et rechargement, reprendre les réservations déjà enregistrées
|
||||
avec le mécanisme existant, sans suppression ni reconstruction de l’historique.
|
||||
- Tester le chemin Windows par injection, une panne après remplacement et la
|
||||
reprise, puis le build et les scénarios moteur dans le développement local.
|
||||
- Publier le correctif sur le canal stable. La machine Windows signalée n’est
|
||||
pas accessible depuis cet environnement Mac : ne pas prétendre l’avoir testée.
|
||||
|
||||
**État :** correctif et tests vérifiés en alpha.13.1. `check build assemblePack`
|
||||
passe, ainsi que le smoke du journal et les scénarios de création/rechargement
|
||||
dans deux processus Minecraft distincts. Validation Windows par injection sur
|
||||
macOS ; aucun essai Windows natif. Publication et synchronisation du canal en
|
||||
cours ; preuves dans [Validation](testing.md).
|
||||
|
||||
## Réserve de thèmes futurs
|
||||
|
||||
Ces thèmes servent à retrouver la vision, pas à demander leur implémentation immédiate. On en extrait un ticket seulement lorsqu'il devient utile au prochain incrément jouable.
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"version": "0.1.0-alpha.13.1",
|
||||
"date": "2026-09-09",
|
||||
"minecraft": "26.3-pre-2",
|
||||
"java": 25,
|
||||
"host_os": "macOS",
|
||||
"windows_native_tested": false,
|
||||
"windows_policy_tested_by_injection": true,
|
||||
"journal_contracts": [
|
||||
"Windows policy skips only directory channel",
|
||||
"file force and atomic replacement and strict read-back remain required",
|
||||
"supported directory AccessDenied propagates after replacement",
|
||||
"failed session refuses further writes",
|
||||
"verified reload recovers persisted reservation",
|
||||
"post-replacement corruption fails closed"
|
||||
],
|
||||
"jar_sha256": "b340dfc58f3d2d1a2a375dc527ac6721c61430600622046db26228e0da46061b",
|
||||
"logs": {
|
||||
"journal-smoke": {
|
||||
"path": "build/alpha131-journal-smoke.log",
|
||||
"sha256": "c02e94713cf3f9a3c76674700cc5c800aecd84b213b2be9ed53629f8eb9cd3d0",
|
||||
"passed": true,
|
||||
"required_game_tests": null
|
||||
},
|
||||
"check": {
|
||||
"path": "build/alpha131-check.log",
|
||||
"sha256": "68f6cb97e2f80669d39e37623076229e030a0d9826c13e9420f51f6ba23885bd",
|
||||
"passed": true,
|
||||
"required_game_tests": 8
|
||||
},
|
||||
"expansion-create": {
|
||||
"path": "build/alpha131-expansion-create.log",
|
||||
"sha256": "61fa4513aef71290a1419c3425a329047c8fe623a561544c6a8d916fe50e226c",
|
||||
"passed": true,
|
||||
"required_game_tests": 2
|
||||
},
|
||||
"expansion-reload": {
|
||||
"path": "build/alpha131-expansion-reload.log",
|
||||
"sha256": "44fafb4636ff90707a068f325a245dbf9820ccb1bdf785c912f7e636fe7cfa43",
|
||||
"passed": true,
|
||||
"required_game_tests": 2
|
||||
}
|
||||
},
|
||||
"binary_comparison": {
|
||||
"base_version": "0.1.0-alpha.13",
|
||||
"version": "0.1.0-alpha.13.1",
|
||||
"added": [
|
||||
"fr/koka/sanctuary/expansion/ExpansionJournal$DirectorySync.class"
|
||||
],
|
||||
"removed": [],
|
||||
"changed": [
|
||||
"fabric.mod.json",
|
||||
"fr/koka/sanctuary/expansion/ExpansionJournal$1.class",
|
||||
"fr/koka/sanctuary/expansion/ExpansionJournal.class"
|
||||
],
|
||||
"synthetic_switch_instructions_identical": true,
|
||||
"all_other_entries_identical": true
|
||||
},
|
||||
"expansion": {
|
||||
"seed": 42,
|
||||
"initial_diameter": 724,
|
||||
"generation": 13,
|
||||
"creation_process": 13148,
|
||||
"reload_process": 13279,
|
||||
"creation_passed": true,
|
||||
"reload_passed": true,
|
||||
"creation_islands": [
|
||||
{
|
||||
"landing_biome": "minecraft:jungle",
|
||||
"landing": [
|
||||
0,
|
||||
202,
|
||||
-1792
|
||||
],
|
||||
"climate": "tropical",
|
||||
"upper_biome": "minecraft:jungle",
|
||||
"diameter": 64,
|
||||
"centre": [
|
||||
0,
|
||||
-1792
|
||||
],
|
||||
"id": "alpha",
|
||||
"parent": "sanctuary"
|
||||
},
|
||||
{
|
||||
"landing_biome": "minecraft:desert",
|
||||
"landing": [
|
||||
2048,
|
||||
198,
|
||||
-1792
|
||||
],
|
||||
"climate": "arid",
|
||||
"upper_biome": "minecraft:desert",
|
||||
"diameter": 64,
|
||||
"centre": [
|
||||
2048,
|
||||
-1792
|
||||
],
|
||||
"id": "beta",
|
||||
"parent": "alpha"
|
||||
}
|
||||
],
|
||||
"reloaded_regions": 3,
|
||||
"saved_generator_codec_decoded": true,
|
||||
"after_reload_expansion": [
|
||||
0,
|
||||
1792
|
||||
],
|
||||
"final_regions": 4,
|
||||
"native_structure_status": "not_observed_after_complete_scan",
|
||||
"native_structure_witness_required": false,
|
||||
"limitation": "Optional native camp witness absent after complete search on seed 42, as in alpha.13; no new structures claim."
|
||||
},
|
||||
"scope": "No generation or journal schema change. Test worlds only; no personal world opened or regenerated. Windows native runtime and power-loss durability not tested."
|
||||
}
|
||||
@@ -19,6 +19,80 @@ Pour générer de vrais chunks et vérifier le spawn sur d'autres graines :
|
||||
La propriété ne s'applique qu'au mod de test, jamais au mod distribué ni à une
|
||||
sauvegarde de joueur. Chaque commande repart d'un monde de développement neuf.
|
||||
|
||||
## Correctif alpha.13.1 — journal d’expansion sous Windows
|
||||
|
||||
Le 9 septembre 2026, le contrôle ciblé suivant passe sur macOS et Java 25 :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:expansionJournalSmoke --console=plain
|
||||
```
|
||||
|
||||
Le journal de cette exécution est `build/alpha131-journal-smoke.log` :
|
||||
`BUILD SUCCESSFUL`, en 7 secondes. Le test écrit et relit de vrais fichiers
|
||||
temporaires ; il n’ouvre aucune sauvegarde Minecraft personnelle.
|
||||
|
||||
Les contrôles existants de persistance, de corruption, d’historique immuable
|
||||
et de compatibilité des racines restent actifs. Trois scénarios couvrent le
|
||||
correctif :
|
||||
|
||||
- La politique Windows, sélectionnée par le séparateur `\`, permet la création
|
||||
du journal, une réservation, sa finalisation et leurs rechargements. Le
|
||||
callback de synchronisation du dossier, qui lèverait `AccessDeniedException`,
|
||||
n’est jamais appelé. Les écritures, remplacements atomiques et relectures du
|
||||
fichier ont réellement lieu sur le système de fichiers du Mac.
|
||||
- La politique Unix conserve une erreur `AccessDeniedException` injectée après
|
||||
le remplacement atomique. La session devient indisponible, garde son ancien
|
||||
état en mémoire et refuse toute nouvelle écriture. Le journal sur disque
|
||||
contient la réservation ; une nouvelle instance la relit et peut la terminer.
|
||||
- Une corruption injectée après le remplacement mais avant la relecture
|
||||
empêche la publication du nouveau plan et conserve le fichier endommagé.
|
||||
Le rechargement refuse ce journal au lieu de créer un historique vide.
|
||||
|
||||
**La branche Windows est simulée ; aucun processus Java ou Minecraft Windows
|
||||
natif n’a encore été exécuté pour cette validation.**
|
||||
|
||||
`check build assemblePack` passe sur les sources alpha.13.1, en 1 min 27 s,
|
||||
avec les huit GameTests du monde Moyen de graine 0. Les scénarios d’expansion
|
||||
passent ensuite dans deux processus Minecraft distincts, sur la graine 42 et
|
||||
le diamètre 724 : création (1 min 32 s), puis réouverture et nouvelle expansion
|
||||
(26 s). Deux tests requis passent à chaque étape. Commandes de reproduction :
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryExpansionTests=true -PsanctuaryOptionalStructureWitness=true -PsanctuaryTestDiameter=724 -PsanctuaryTestSeed=42 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryExpansionTests=true -PsanctuaryExpansionReload=true -PsanctuaryOptionalStructureWitness=true -PsanctuaryTestDiameter=724 -PsanctuaryTestSeed=42 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
```
|
||||
|
||||
Lancer les deux scénarios d’expansion consécutivement ; le second réutilise
|
||||
uniquement leur monde de développement. Comme pour l’alpha.13, le témoin de
|
||||
camp naturel est facultatif et absent après recherche complète sur cette
|
||||
graine ; aucune nouvelle preuve de structure n’est revendiquée.
|
||||
Les [résultats alpha.13.1](benchmarks/alpha131-journal.json) conservent les
|
||||
hashes des journaux de test, les identifiants des deux processus et les régions.
|
||||
La comparaison des JAR confirme que seuls le journal, sa classe de stratégie
|
||||
et la version changent ; le switch synthétique ne change que ses numéros de
|
||||
lignes. Toutes les autres classes et ressources restent identiques.
|
||||
Les résultats des trois tailles et de l’interface ci-dessous restent ceux de
|
||||
la livraison alpha.13 ; ils n’ont pas été réexécutés pour ce correctif de stockage.
|
||||
|
||||
La cause est établie par les sources de
|
||||
[WindowsChannelFactory dans OpenJDK 25](https://github.com/openjdk/jdk/blob/jdk-25-ga/src/java.base/windows/classes/sun/nio/fs/WindowsChannelFactory.java#L206-L272) :
|
||||
`FileChannel.open` n’ajoute pas le drapeau `FILE_FLAG_BACKUP_SEMANTICS`, exigé
|
||||
pour ouvrir un répertoire selon la
|
||||
[documentation Microsoft de CreateFile](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew#directories).
|
||||
Seule cette synchronisation du dossier est omise sous Windows ; les exceptions
|
||||
d’écriture, de remplacement ou de relecture restent bloquantes.
|
||||
|
||||
Le fichier temporaire est toujours synchronisé par `force(true)`, puis remplacé
|
||||
atomiquement et vérifié. Cette séquence ne garantit pas sous Windows la même
|
||||
durabilité du renommage en cas de coupure secteur qu’une synchronisation du
|
||||
répertoire sous Unix :
|
||||
[OpenJDK utilise MoveFileEx sans WRITE_THROUGH](https://github.com/openjdk/jdk/blob/jdk-25-ga/src/java.base/windows/classes/sun/nio/fs/WindowsFileCopy.java#L279-L294),
|
||||
et le contrat de
|
||||
[FileChannel.force](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/nio/channels/FileChannel.html#force(boolean))
|
||||
porte sur les modifications du fichier. Aucun changement de format, de graine,
|
||||
de générateur ou de réservation enregistrée n’est introduit.
|
||||
|
||||
## Alpha.13 — génération unifiée
|
||||
|
||||
`check build assemblePack` passe, y compris le monde Moyen de graine 0 et les
|
||||
|
||||
+2
-2
@@ -8,6 +8,6 @@ loader_version=0.19.5
|
||||
loom_version=1.17.20
|
||||
fabric_api_version=0.160.0+26.3
|
||||
|
||||
mod_version=0.1.0-alpha.13
|
||||
pack_version=0.1.0-alpha.13
|
||||
mod_version=0.1.0-alpha.13.1
|
||||
pack_version=0.1.0-alpha.13.1
|
||||
maven_group=fr.koka.sanctuary
|
||||
|
||||
@@ -40,6 +40,7 @@ public final class ExpansionJournal {
|
||||
private final long seed;
|
||||
private final int rootDiameter;
|
||||
private final int rootGeneration;
|
||||
private final DirectorySync directorySync;
|
||||
private List<ExpansionIsland> islands;
|
||||
private boolean failed;
|
||||
|
||||
@@ -47,10 +48,16 @@ public final class ExpansionJournal {
|
||||
this(directory, seed, rootDiameter, ExpansionIsland.GENERATION);
|
||||
}
|
||||
public ExpansionJournal(Path directory, long seed, int rootDiameter, int rootGeneration) throws IOException {
|
||||
this(directory, seed, rootDiameter, rootGeneration,
|
||||
directorySyncFor(directory.getFileSystem().getSeparator(), ExpansionJournal::forceDirectory));
|
||||
}
|
||||
|
||||
ExpansionJournal(Path directory, long seed, int rootDiameter, int rootGeneration, DirectorySync directorySync) throws IOException {
|
||||
if (rootGeneration != 12 && rootGeneration != 13) throw new IOException("Unsupported root generation");
|
||||
if (rootDiameter != 512 && rootDiameter != 724 && !(rootGeneration == 13 && rootDiameter == 1024))
|
||||
throw new IOException("Unsupported expansion root diameter");
|
||||
this.rootGeneration = rootGeneration;
|
||||
this.directorySync = java.util.Objects.requireNonNull(directorySync);
|
||||
this.directory = directory.toAbsolutePath().normalize();
|
||||
this.file = this.directory.resolve(FILE_NAME);
|
||||
this.seed = seed;
|
||||
@@ -110,7 +117,7 @@ public final class ExpansionJournal {
|
||||
// No fallback to a non-atomic replacement: failure must remain visible to the caller.
|
||||
Files.move(temporary, file, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||
temporary = null;
|
||||
try (FileChannel parent = FileChannel.open(directory, StandardOpenOption.READ)) { parent.force(true); }
|
||||
directorySync.force(directory);
|
||||
if (!read().equals(next)) throw new IOException("Expansion journal verification disagrees with the committed plan");
|
||||
} catch (IOException | RuntimeException e) {
|
||||
failed = true;
|
||||
@@ -123,6 +130,19 @@ public final class ExpansionJournal {
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface DirectorySync { void force(Path directory) throws IOException; }
|
||||
|
||||
static DirectorySync directorySyncFor(String separator, DirectorySync supportedSync) {
|
||||
// Windows FileChannel cannot open directory handles. Only this additional
|
||||
// directory flush is omitted: file force, atomic replacement and read-back remain mandatory.
|
||||
return "\\".equals(separator) ? directory -> {} : supportedSync;
|
||||
}
|
||||
|
||||
private static void forceDirectory(Path directory) throws IOException {
|
||||
try (FileChannel parent = FileChannel.open(directory, StandardOpenOption.READ)) { parent.force(true); }
|
||||
}
|
||||
|
||||
private List<ExpansionIsland> read() throws IOException {
|
||||
if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) throw new IOException("Existing expansion directory has no regular journal file");
|
||||
if (Files.size(file) > MAX_BYTES) throw new IOException("Expansion journal exceeds its size limit");
|
||||
|
||||
+78
-1
@@ -2,12 +2,14 @@ package fr.koka.sanctuary.expansion;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/** Persistence failures must never turn saved continents into a new empty history. No game world is opened. */
|
||||
public final class ExpansionJournalSmoke {
|
||||
@@ -120,10 +122,14 @@ public final class ExpansionJournalSmoke {
|
||||
afterCrash.commit(List.of(root, north.ready(), onward));
|
||||
require(new ExpansionJournal(resumeDirectory, SEED, 512).islands().size() == 3, "Resumed region lost its descendant");
|
||||
|
||||
windowsDirectoryPolicy(scratch.resolve("windows-policy"));
|
||||
directoryFailureAfterReplacement(scratch.resolve("directory-failure"));
|
||||
readbackFailureAfterReplacement(scratch.resolve("readback-failure"));
|
||||
|
||||
var tooMany = new ArrayList<ExpansionIsland>(); tooMany.add(root);
|
||||
for (int i = 0; i < 64; i++) tooMany.add(island("overflow_" + i, "sanctuary", "north", 0, -(i + 1) * 1024));
|
||||
reject(() -> afterCrash.commit(tooMany), "Region count limit");
|
||||
System.out.println("Expansion journal smoke passed: atomic persistence, restart, immutable history, corruption, interrupted reservation and unified root identity");
|
||||
System.out.println("Expansion journal smoke passed: atomic persistence, restart, immutable history, corruption, interrupted reservation, unified root identity, Windows directory policy and post-replacement failures");
|
||||
} finally {
|
||||
try (var paths = Files.walk(scratch)) {
|
||||
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) Files.delete(path);
|
||||
@@ -131,6 +137,77 @@ public final class ExpansionJournalSmoke {
|
||||
}
|
||||
}
|
||||
|
||||
private static void windowsDirectoryPolicy(Path directory) throws Exception {
|
||||
var directoryAttempts = new AtomicInteger();
|
||||
var windowsSync = ExpansionJournal.directorySyncFor("\\", path -> {
|
||||
directoryAttempts.incrementAndGet();
|
||||
throw new AccessDeniedException(path.toString());
|
||||
});
|
||||
var root = ExpansionIsland.origin(724, SEED, 13);
|
||||
var north = island("windows_north", "sanctuary", "north", 0, -1024);
|
||||
var journal = new ExpansionJournal(directory, SEED, 724, 13, windowsSync);
|
||||
require(journal.healthy() && journal.islands().equals(List.of(root)),
|
||||
"Windows policy must bootstrap a verified journal without opening the directory as a file");
|
||||
journal.commit(List.of(root, north));
|
||||
var reserved = new ExpansionJournal(directory, SEED, 724, 13, windowsSync);
|
||||
require(reserved.healthy() && reserved.islands().equals(List.of(root, north)),
|
||||
"Windows policy must persist and reload the exact reservation");
|
||||
reserved.commit(List.of(root, north.ready()));
|
||||
var ready = new ExpansionJournal(directory, SEED, 724, 13, windowsSync);
|
||||
require(ready.healthy() && ready.islands().equals(List.of(root, north.ready())),
|
||||
"Windows policy must complete and reload the same reservation");
|
||||
require(directoryAttempts.get() == 0,
|
||||
"Windows policy must never attempt the unsupported directory channel, including bootstrap and completion");
|
||||
}
|
||||
|
||||
private static void directoryFailureAfterReplacement(Path directory) throws Exception {
|
||||
var root = ExpansionIsland.origin(724, SEED, 13);
|
||||
var north = island("retained_north", "sanctuary", "north", 0, -1024);
|
||||
new ExpansionJournal(directory, SEED, 724, 13);
|
||||
Path state = directory.resolve(ExpansionJournal.FILE_NAME);
|
||||
var attempts = new AtomicInteger();
|
||||
var denied = new AccessDeniedException(directory.toString());
|
||||
var requiredSync = ExpansionJournal.directorySyncFor("/", path -> {
|
||||
attempts.incrementAndGet();
|
||||
throw denied;
|
||||
});
|
||||
var failed = new ExpansionJournal(directory, SEED, 724, 13, requiredSync);
|
||||
try {
|
||||
failed.commit(List.of(root, north));
|
||||
throw new AssertionError("A directory sync failure on a supported filesystem was ignored");
|
||||
} catch (AccessDeniedException actual) {
|
||||
require(actual == denied, "The original directory sync error must reach the caller unchanged");
|
||||
}
|
||||
require(attempts.get() == 1, "A supported filesystem must attempt directory synchronization");
|
||||
require(!failed.healthy() && failed.islands().equals(List.of(root)),
|
||||
"Uncertain persistence must fail closed without publishing the new reservation to world generation");
|
||||
byte[] reservedBytes = Files.readAllBytes(state);
|
||||
var recovered = new ExpansionJournal(directory, SEED, 724, 13);
|
||||
require(recovered.healthy() && recovered.islands().equals(List.of(root, north)),
|
||||
"A verified reload must recover the reservation already installed before directory sync failed");
|
||||
reject(() -> failed.commit(List.of(root, north)), "Retrying a failed journal without reload");
|
||||
require(attempts.get() == 1 && Arrays.equals(reservedBytes, Files.readAllBytes(state)),
|
||||
"A failed session must neither retry storage operations nor overwrite its retained reservation");
|
||||
recovered.commit(List.of(root, north.ready()));
|
||||
require(new ExpansionJournal(directory, SEED, 724, 13).islands().equals(List.of(root, north.ready())),
|
||||
"A recovered reservation must complete normally without changing its identity");
|
||||
}
|
||||
|
||||
private static void readbackFailureAfterReplacement(Path directory) throws Exception {
|
||||
var root = ExpansionIsland.origin(724, SEED, 13);
|
||||
var north = island("readback_north", "sanctuary", "north", 0, -1024);
|
||||
new ExpansionJournal(directory, SEED, 724, 13);
|
||||
Path state = directory.resolve(ExpansionJournal.FILE_NAME);
|
||||
var journal = new ExpansionJournal(directory, SEED, 724, 13,
|
||||
path -> Files.writeString(state, "damaged after atomic replacement", StandardCharsets.UTF_8));
|
||||
reject(() -> journal.commit(List.of(root, north)), "Corruption detected by post-replacement verification");
|
||||
require(!journal.healthy() && journal.islands().equals(List.of(root)),
|
||||
"Successful replacement alone must not publish an unverified reservation");
|
||||
require(Files.readString(state).equals("damaged after atomic replacement"),
|
||||
"A failed readback must preserve evidence instead of silently recreating the journal");
|
||||
reject(() -> new ExpansionJournal(directory, SEED, 724, 13), "Reloading a journal damaged after replacement");
|
||||
}
|
||||
|
||||
private static ExpansionIsland island(String id, String parent, String direction, int x, int z) {
|
||||
return new ExpansionIsland(id, parent, direction, ExpansionIsland.directionalClimate(direction), 64, x, z,
|
||||
ExpansionIsland.deriveSeed(SEED, id), "natural", "reserved", ExpansionIsland.GENERATION);
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "0.1.0-alpha.13"
|
||||
version = "0.1.0-alpha.13.1"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user