implements PR-36

This commit is contained in:
bQUARKz 2026-03-20 09:49:16 +00:00
parent fa527720a4
commit 31621303d1
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
3 changed files with 418 additions and 3 deletions

View File

@ -10,6 +10,8 @@ import p.packer.events.PackerEventKind;
import p.packer.events.PackerEventSink; import p.packer.events.PackerEventSink;
import p.packer.events.PackerProgress; import p.packer.events.PackerProgress;
import p.packer.dtos.PackerDiagnosticDTO; import p.packer.dtos.PackerDiagnosticDTO;
import p.packer.dtos.PackerEmittedArtifactDTO;
import p.packer.dtos.PackerPackExecutionSummaryDTO;
import p.packer.dtos.PackerPackSummaryAssetDTO; import p.packer.dtos.PackerPackSummaryAssetDTO;
import p.packer.dtos.PackerPackSummaryDTO; import p.packer.dtos.PackerPackSummaryDTO;
import p.packer.dtos.PackerPackValidationAssetDTO; import p.packer.dtos.PackerPackValidationAssetDTO;
@ -19,12 +21,18 @@ import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity; import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.*; import p.packer.models.*;
import p.packer.repositories.FileSystemPackerCacheRepository; import p.packer.repositories.FileSystemPackerCacheRepository;
import p.packer.repositories.PackerAssetWalker;
import p.packer.repositories.PackerContractFingerprint; import p.packer.repositories.PackerContractFingerprint;
import p.packer.repositories.PackerRuntimeAssetMaterializer;
import p.packer.repositories.PackerRuntimeRegistry; import p.packer.repositories.PackerRuntimeRegistry;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.*; import java.util.*;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -178,7 +186,9 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
@Override @Override
public PackWorkspaceResult packWorkspace(PackWorkspaceRequest request) { public PackWorkspaceResult packWorkspace(PackWorkspaceRequest request) {
throw new UnsupportedOperationException("pack workspace execution is not implemented yet"); final PackWorkspaceRequest safeRequest = Objects.requireNonNull(request, "request");
final PackerProjectContext project = safeRequest.project();
return writeCoordinator.execute(project, () -> packWorkspaceInWriteLane(safeRequest));
} }
@Override @Override
@ -303,6 +313,71 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
} }
} }
private PackWorkspaceResult packWorkspaceInWriteLane(PackWorkspaceRequest request) {
final PackerProjectContext project = request.project();
final long startedAt = System.currentTimeMillis();
workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));
final PackerRuntimeSnapshot refreshedSnapshot = runtimeRegistry.refresh(project).snapshot();
final PackerRuntimeSnapshot frozenSnapshot = createFrozenPackingSnapshot(refreshedSnapshot);
final List<PackerPackValidationAssetDTO> blockedAssets = frozenSnapshot.assets().stream()
.filter(runtimeAsset -> runtimeAsset.registryEntry().isPresent() && runtimeAsset.registryEntry().get().includedInBuild())
.map(this::toPackValidationAssetDTO)
.filter(PackerPackValidationAssetDTO::blocked)
.toList();
if (!blockedAssets.isEmpty()) {
return new PackWorkspaceResult(
PackerOperationStatus.PARTIAL,
"Pack validation found blocking diagnostics in " + blockedAssets.size() + " included assets.",
new PackerPackExecutionSummaryDTO("assets.pa", 0, System.currentTimeMillis() - startedAt, List.of()));
}
final Path buildRoot = project.rootPath().resolve("build").toAbsolutePath().normalize();
final String operationId = randomOperationId();
final Path stagingRoot = buildRoot.resolve(".staging").resolve(operationId);
final Path stagedAssetsPath = stagingRoot.resolve("assets.pa");
final Path stagedAssetTablePath = stagingRoot.resolve("asset_table.json");
final Path stagedPreloadPath = stagingRoot.resolve("preload.json");
final Path stagedAssetTableMetadataPath = stagingRoot.resolve("asset_table_metadata.json");
try {
Files.createDirectories(stagingRoot);
final PackedWorkspaceArtifacts packedWorkspace = buildPackedWorkspaceArtifacts(frozenSnapshot);
writeJson(stagedAssetTablePath, packedWorkspace.assetTable());
writeJson(stagedPreloadPath, packedWorkspace.preload());
writeJson(stagedAssetTableMetadataPath, packedWorkspace.assetTableMetadata());
Files.write(stagedAssetsPath, packedWorkspace.assetsPaBytes());
final Path finalAssetsPath = buildRoot.resolve("assets.pa");
final Path finalAssetTablePath = buildRoot.resolve("asset_table.json");
final Path finalPreloadPath = buildRoot.resolve("preload.json");
final Path finalAssetTableMetadataPath = buildRoot.resolve("asset_table_metadata.json");
Files.createDirectories(buildRoot);
Files.move(stagedAssetsPath, finalAssetsPath, StandardCopyOption.REPLACE_EXISTING);
Files.move(stagedAssetTablePath, finalAssetTablePath, StandardCopyOption.REPLACE_EXISTING);
Files.move(stagedPreloadPath, finalPreloadPath, StandardCopyOption.REPLACE_EXISTING);
Files.move(stagedAssetTableMetadataPath, finalAssetTableMetadataPath, StandardCopyOption.REPLACE_EXISTING);
final List<PackerEmittedArtifactDTO> emittedArtifacts = List.of(
emittedArtifact("assets.pa", finalAssetsPath, true),
emittedArtifact("asset_table.json", finalAssetTablePath, false),
emittedArtifact("preload.json", finalPreloadPath, false),
emittedArtifact("asset_table_metadata.json", finalAssetTableMetadataPath, false));
return new PackWorkspaceResult(
PackerOperationStatus.SUCCESS,
"Pack workspace completed successfully.",
new PackerPackExecutionSummaryDTO(
"assets.pa",
packedWorkspace.packedAssetCount(),
System.currentTimeMillis() - startedAt,
emittedArtifacts));
} catch (Exception exception) {
return new PackWorkspaceResult(
PackerOperationStatus.FAILED,
"Pack workspace failed: " + exception.getMessage(),
new PackerPackExecutionSummaryDTO("assets.pa", 0, System.currentTimeMillis() - startedAt, List.of()));
}
}
private RegisterAssetResult registerAssetInWriteLane( private RegisterAssetResult registerAssetInWriteLane(
RegisterAssetRequest request, RegisterAssetRequest request,
PackerOperationEventEmitter events) { PackerOperationEventEmitter events) {
@ -728,6 +803,268 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
cacheRepository.save(project, snapshot.cacheState()); cacheRepository.save(project, snapshot.cacheState());
} }
private PackerRuntimeSnapshot createFrozenPackingSnapshot(PackerRuntimeSnapshot snapshot) {
final PackerRuntimeAssetMaterializer materializer = new PackerRuntimeAssetMaterializer(new PackerAssetWalker(mapper));
final List<PackerRuntimeAsset> assets = snapshot.assets().stream()
.map(asset -> rematerializeForPacking(snapshot, asset, materializer))
.toList();
return new PackerRuntimeSnapshot(snapshot.generation(), snapshot.registry(), assets, snapshot.cacheState());
}
private PackerRuntimeAsset rematerializeForPacking(
PackerRuntimeSnapshot snapshot,
PackerRuntimeAsset asset,
PackerRuntimeAssetMaterializer materializer) {
if (asset.registryEntry().isEmpty() || !asset.registryEntry().get().includedInBuild() || !asset.parsedDeclaration().valid()) {
return asset;
}
return materializer.materialize(
asset.assetRoot(),
asset.manifestPath(),
asset.registryEntry(),
asset.parsedDeclaration(),
asset.registryEntry().flatMap(entry -> snapshot.cacheState().findAsset(entry.assetId())),
PackerRuntimeMaterializationConfig.packingBuild()).runtimeAsset();
}
private PackedWorkspaceArtifacts buildPackedWorkspaceArtifacts(PackerRuntimeSnapshot snapshot) throws IOException {
final List<PackedAsset> packedAssets = snapshot.assets().stream()
.filter(asset -> asset.registryEntry().isPresent() && asset.registryEntry().get().includedInBuild())
.sorted(Comparator.comparingInt(asset -> asset.registryEntry().get().assetId()))
.map(this::packRuntimeAsset)
.toList();
final List<Map<String, Object>> assetTable = new ArrayList<>();
final List<Map<String, Object>> preload = new ArrayList<>();
final List<Map<String, Object>> assetTableMetadata = new ArrayList<>();
final ByteArrayOutputStream payload = new ByteArrayOutputStream();
for (PackedAsset packedAsset : packedAssets) {
final int offset = payload.size();
payload.write(packedAsset.payload());
assetTable.add(new LinkedHashMap<>(Map.of(
"asset_id", packedAsset.assetId(),
"asset_name", packedAsset.assetName(),
"bank_type", packedAsset.bankType(),
"offset", offset,
"size", packedAsset.payload().length,
"decoded_size", packedAsset.decodedSize(),
"codec", packedAsset.codec(),
"metadata", packedAsset.metadata())));
if (packedAsset.preloadEnabled()) {
preload.add(new LinkedHashMap<>(Map.of("asset_id", packedAsset.assetId())));
}
assetTableMetadata.add(new LinkedHashMap<>(Map.of(
"asset_id", packedAsset.assetId(),
"metadata", packedAsset.metadata())));
}
final byte[] headerBytes = canonicalJsonBytes(Map.of(
"asset_table", assetTable,
"preload", preload));
final byte[] preludeBytes = buildPrelude(headerBytes.length);
final ByteArrayOutputStream assetsPa = new ByteArrayOutputStream();
assetsPa.write(preludeBytes);
assetsPa.write(headerBytes);
assetsPa.write(payload.toByteArray());
return new PackedWorkspaceArtifacts(
assetsPa.toByteArray(),
assetTable,
preload,
assetTableMetadata,
packedAssets.size());
}
private PackedAsset packRuntimeAsset(PackerRuntimeAsset runtimeAsset) {
final PackerAssetDeclaration declaration = runtimeAsset.parsedDeclaration().declaration();
if (declaration == null || declaration.assetFamily() != AssetFamilyCatalog.TILE_BANK) {
throw new IllegalStateException("Unsupported pack output family for current implementation.");
}
return packTileBank(runtimeAsset, declaration);
}
private PackedAsset packTileBank(PackerRuntimeAsset runtimeAsset, PackerAssetDeclaration declaration) {
final int tileSize = parseTileSize(declaration.outputMetadata().get("tile_size"));
final int width = 256;
final int height = 256;
final int tilesPerRow = width / tileSize;
final byte[] sheetPixels = new byte[width * height];
final Map<String, PackerRuntimeWalkFile> selectedByPath = new LinkedHashMap<>();
runtimeAsset.walkProjection().buildCandidateFiles().forEach(file -> selectedByPath.put(file.relativePath(), file));
declaration.artifacts().stream()
.sorted(Comparator.comparingInt(PackerAssetArtifactSelection::index))
.forEach(artifact -> {
final PackerRuntimeWalkFile walkFile = selectedByPath.get(artifact.file());
if (walkFile == null) {
throw new IllegalStateException("Selected tile artifact is missing from packing snapshot: " + artifact.file());
}
final Object tileValue = walkFile.metadata().get("tile");
if (!(tileValue instanceof PackerTileIndexedV1 tile)) {
throw new IllegalStateException("Selected tile artifact is missing normalized tile metadata: " + artifact.file());
}
final int tileX = (artifact.index() % tilesPerRow) * tileSize;
final int tileY = (artifact.index() / tilesPerRow) * tileSize;
for (int localY = 0; localY < tile.height(); localY += 1) {
for (int localX = 0; localX < tile.width(); localX += 1) {
final int sourceOffset = localY * tile.width() + localX;
final int targetOffset = (tileY + localY) * width + (tileX + localX);
sheetPixels[targetOffset] = tile.paletteIndices()[sourceOffset];
}
}
});
final byte[] packedPixels = packNibbles(sheetPixels);
final byte[] paletteBytes = emitTileBankPalettes(declaration);
final byte[] payload = new byte[packedPixels.length + paletteBytes.length];
System.arraycopy(packedPixels, 0, payload, 0, packedPixels.length);
System.arraycopy(paletteBytes, 0, payload, packedPixels.length, paletteBytes.length);
final LinkedHashMap<String, Object> metadata = new LinkedHashMap<>();
metadata.put("tile_size", tileSize);
metadata.put("width", width);
metadata.put("height", height);
metadata.put("palette_count", 64);
metadata.put("codec", Map.of());
metadata.put("pipeline", PackerReadMessageMapper.normalizeMetadata(declaration.outputPipelineMetadata()));
declaration.outputMetadata().forEach((key, value) -> {
if (!"tile_size".equals(key)) {
metadata.putIfAbsent(key, value);
}
});
final PackerRegistryEntry registryEntry = runtimeAsset.registryEntry()
.orElseThrow(() -> new IllegalStateException("Packed runtime asset must be registered"));
return new PackedAsset(
registryEntry.assetId(),
declaration.name(),
"TILES",
"NONE",
payload,
width * height + 2048,
metadata,
declaration.preloadEnabled());
}
private int parseTileSize(String value) {
return switch (Objects.requireNonNullElse(value, "")) {
case "8x8" -> 8;
case "16x16" -> 16;
case "32x32" -> 32;
default -> throw new IllegalStateException("Unsupported tile_size for tile-bank packing: " + value);
};
}
private byte[] packNibbles(byte[] logicalPixels) {
final byte[] packed = new byte[(logicalPixels.length + 1) / 2];
for (int offset = 0; offset < logicalPixels.length; offset += 2) {
final int high = logicalPixels[offset] & 0x0F;
final int low = offset + 1 < logicalPixels.length ? logicalPixels[offset + 1] & 0x0F : 0;
packed[offset / 2] = (byte) ((high << 4) | low);
}
return packed;
}
private byte[] emitTileBankPalettes(PackerAssetDeclaration declaration) {
final byte[] bytes = new byte[64 * 16 * 2];
final JsonNode palettesNode = declaration.outputPipelineMetadata().get("palettes");
if (!(palettesNode instanceof com.fasterxml.jackson.databind.node.ArrayNode palettesArray)) {
return bytes;
}
for (JsonNode declarationNode : palettesArray) {
final JsonNode indexNode = declarationNode.path("index");
final JsonNode paletteNode = declarationNode.path("palette");
if (!indexNode.isInt() || !paletteNode.isObject()) {
continue;
}
final int paletteIndex = indexNode.intValue();
if (paletteIndex < 0 || paletteIndex >= 64) {
continue;
}
final JsonNode convertedNode = paletteNode.path("convertedRgb565");
if (!convertedNode.isArray()) {
continue;
}
for (int colorIndex = 0; colorIndex < Math.min(16, convertedNode.size()); colorIndex += 1) {
final int rgb565 = convertedNode.get(colorIndex).asInt(0);
final int baseOffset = ((paletteIndex * 16) + colorIndex) * 2;
bytes[baseOffset] = (byte) (rgb565 & 0xFF);
bytes[baseOffset + 1] = (byte) ((rgb565 >>> 8) & 0xFF);
}
}
return bytes;
}
private byte[] buildPrelude(int headerLength) {
final int preludeLength = 24;
final ByteBuffer buffer = ByteBuffer.allocate(preludeLength).order(ByteOrder.LITTLE_ENDIAN);
buffer.put((byte) 'P').put((byte) 'P').put((byte) 'A').put((byte) 'K');
buffer.putInt(1);
buffer.putInt(headerLength);
buffer.putInt(preludeLength + headerLength);
buffer.putInt(0);
buffer.putInt(0);
return buffer.array();
}
private byte[] canonicalJsonBytes(Object value) throws IOException {
final Object canonical = canonicalizeJsonValue(value);
return mapper.copy()
.configure(com.fasterxml.jackson.databind.SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
.writeValueAsBytes(canonical);
}
private Object canonicalizeJsonValue(Object value) {
if (value instanceof Map<?, ?> map) {
final LinkedHashMap<String, Object> sorted = new LinkedHashMap<>();
map.entrySet().stream()
.sorted(Comparator.comparing(entry -> String.valueOf(entry.getKey())))
.forEach(entry -> sorted.put(String.valueOf(entry.getKey()), canonicalizeJsonValue(entry.getValue())));
return sorted;
}
if (value instanceof List<?> list) {
return list.stream().map(this::canonicalizeJsonValue).toList();
}
return value;
}
private void writeJson(Path path, Object value) throws IOException {
Files.createDirectories(path.getParent());
mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), value);
}
private PackerEmittedArtifactDTO emittedArtifact(String label, Path path, boolean canonical) throws IOException {
return new PackerEmittedArtifactDTO(label, path, canonical, Files.size(path));
}
private String randomOperationId() {
final String alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
final StringBuilder builder = new StringBuilder(20);
final Random random = new Random();
for (int index = 0; index < 20; index += 1) {
builder.append(alphabet.charAt(random.nextInt(alphabet.length())));
}
return builder.toString();
}
private record PackedAsset(
int assetId,
String assetName,
String bankType,
String codec,
byte[] payload,
int decodedSize,
Map<String, Object> metadata,
boolean preloadEnabled) {
}
private record PackedWorkspaceArtifacts(
byte[] assetsPaBytes,
List<Map<String, Object>> assetTable,
List<Map<String, Object>> preload,
List<Map<String, Object>> assetTableMetadata,
int packedAssetCount) {
}
private PackerRegistryState removeRegistryEntry( private PackerRegistryState removeRegistryEntry(
PackerRegistryState registry, PackerRegistryState registry,
PackerRegistryEntry entry) { PackerRegistryEntry entry) {

View File

@ -220,6 +220,80 @@ final class FileSystemPackerWorkspaceServiceTest {
.anyMatch(diagnostic -> diagnostic.message().contains("must declare at least one palette"))); .anyMatch(diagnostic -> diagnostic.message().contains("must declare at least one palette")));
} }
@Test
void packWorkspaceRerunsGateOnFreshSnapshotBeforeEmission() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("pack-workspace-rerun-gate"));
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
final Path manifestPath = assetRoot.resolve("asset.json");
final FileSystemPackerWorkspaceService service = service();
final var validation = service.validatePackWorkspace(new ValidatePackWorkspaceRequest(project(projectRoot)));
assertEquals(PackerOperationStatus.SUCCESS, validation.status());
writeTilePng(assetRoot.resolve("confirm.png"), 16);
final ObjectNode manifest = (ObjectNode) MAPPER.readTree(manifestPath.toFile());
final var artifacts = manifest.putArray("artifacts");
artifacts.addObject().put("file", "confirm.png").put("index", 0);
MAPPER.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
final var result = service.packWorkspace(new PackWorkspaceRequest(project(projectRoot)));
assertEquals(PackerOperationStatus.PARTIAL, result.status());
assertEquals("assets.pa", result.result().canonicalArtifactName());
assertTrue(result.result().emittedArtifacts().isEmpty());
assertFalse(Files.exists(projectRoot.resolve("build/assets.pa")));
}
@Test
void packWorkspaceEmitsTileBankArtifactsFromFrozenSnapshot() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("pack-workspace-success"));
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
final Path manifestPath = assetRoot.resolve("asset.json");
writeTilePng(assetRoot.resolve("confirm.png"), 16);
final ObjectNode manifest = (ObjectNode) MAPPER.readTree(manifestPath.toFile());
final ObjectNode output = (ObjectNode) manifest.path("output");
output.putObject("metadata").put("tile_size", "16x16");
final ObjectNode pipeline = output.putObject("pipeline");
final var palettes = pipeline.putArray("palettes");
final ObjectNode palette = palettes.addObject();
palette.put("index", 0);
palette.putObject("palette")
.putArray("originalArgb8888").add(0xFFFF0000);
((ObjectNode) palette.path("palette"))
.putArray("convertedRgb565").add(0xF800);
final var artifacts = manifest.putArray("artifacts");
artifacts.addObject().put("file", "confirm.png").put("index", 0);
MAPPER.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
final FileSystemPackerWorkspaceService service = service();
final var result = service.packWorkspace(new PackWorkspaceRequest(project(projectRoot)));
assertEquals(PackerOperationStatus.SUCCESS, result.status());
assertEquals("assets.pa", result.result().canonicalArtifactName());
assertEquals(1, result.result().packedAssetCount());
assertEquals(4, result.result().emittedArtifacts().size());
assertTrue(Files.isRegularFile(projectRoot.resolve("build/assets.pa")));
assertTrue(Files.isRegularFile(projectRoot.resolve("build/asset_table.json")));
assertTrue(Files.isRegularFile(projectRoot.resolve("build/preload.json")));
assertTrue(Files.isRegularFile(projectRoot.resolve("build/asset_table_metadata.json")));
final var assetTable = MAPPER.readTree(projectRoot.resolve("build/asset_table.json").toFile());
assertEquals(1, assetTable.size());
assertEquals(1, assetTable.get(0).path("asset_id").asInt());
assertEquals("TILES", assetTable.get(0).path("bank_type").asText());
assertEquals("NONE", assetTable.get(0).path("codec").asText());
assertEquals(16, assetTable.get(0).path("metadata").path("tile_size").asInt());
assertEquals(256, assetTable.get(0).path("metadata").path("width").asInt());
assertEquals(256, assetTable.get(0).path("metadata").path("height").asInt());
assertEquals(64, assetTable.get(0).path("metadata").path("palette_count").asInt());
assertTrue(assetTable.get(0).path("metadata").path("pipeline").path("palettes").isArray());
final var preload = MAPPER.readTree(projectRoot.resolve("build/preload.json").toFile());
assertEquals(1, preload.size());
assertEquals(1, preload.get(0).path("asset_id").asInt());
}
@Test @Test
void packValidationIncludesBlockingFileScopedTileDiagnostics() throws Exception { void packValidationIncludesBlockingFileScopedTileDiagnostics() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("pack-validation-file-diagnostics")); final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("pack-validation-file-diagnostics"));

View File

@ -99,7 +99,9 @@ final class PackerAssetDetailsServiceTest {
final PackerAssetDetailsService service = service(); final PackerAssetDetailsService service = service();
final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), AssetReference.forAssetId(1))); final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), AssetReference.forAssetId(1)));
assertEquals(PackerOperationStatus.SUCCESS, result.status()); assertEquals(PackerOperationStatus.PARTIAL, result.status());
assertTrue(result.diagnostics().stream()
.anyMatch(diagnostic -> diagnostic.message().contains("must declare at least one palette")));
assertTrue(result.details().bankComposition().availableFiles().stream() assertTrue(result.details().bankComposition().availableFiles().stream()
.anyMatch(file -> file.path().equals("confirm.png"))); .anyMatch(file -> file.path().equals("confirm.png")));
assertTrue(result.details().bankComposition().selectedFiles().stream() assertTrue(result.details().bankComposition().selectedFiles().stream()
@ -131,7 +133,9 @@ final class PackerAssetDetailsServiceTest {
final PackerAssetDetailsService service = service(); final PackerAssetDetailsService service = service();
final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), AssetReference.forAssetId(1))); final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), AssetReference.forAssetId(1)));
assertEquals(PackerOperationStatus.SUCCESS, result.status()); assertEquals(PackerOperationStatus.PARTIAL, result.status());
assertTrue(result.diagnostics().stream()
.anyMatch(diagnostic -> diagnostic.message().contains("must declare at least one palette")));
assertEquals( assertEquals(
List.of("b.png", "a.png"), List.of("b.png", "a.png"),
result.details().bankComposition().selectedFiles().stream().map(file -> file.path()).toList()); result.details().bankComposition().selectedFiles().stream().map(file -> file.path()).toList());