forked from phenom/healer-man
Release Healer Man 0.1.22
This commit is contained in:
1 parent
f6f2375145
commit
7f6001723a
144 files changed
+54107
-690
No files matched your search
@@ -3,6 +3,7 @@ import {
|
||||
constants,
|
||||
copyFile,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rename,
|
||||
stat,
|
||||
@@ -13,8 +14,16 @@ import { fileURLToPath } from "node:url";
|
||||
|
||||
const SCRIPT_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
||||
export const PROJECT_ROOT = path.resolve(SCRIPT_DIRECTORY, "../..");
|
||||
const WOW_CAMPAIGN_CATALOG_PATH = path.join(
|
||||
PROJECT_ROOT,
|
||||
"src",
|
||||
"game",
|
||||
"generated",
|
||||
"dungeonCampaignCatalog.json",
|
||||
);
|
||||
export const CONTENT_SCHEMA_VERSION = 1;
|
||||
export const DEFAULT_MINIMUM_APP_VERSION = "0.1.0";
|
||||
export const WOW_CAMPAIGN_DUNGEON_COUNT = 54;
|
||||
export const BUNDLED_ANDROID_PACKS = Object.freeze([
|
||||
"core:ui",
|
||||
"core:equipment",
|
||||
@@ -179,15 +188,112 @@ export async function scanContentSources(sources = DEFAULT_CONTENT_SOURCES) {
|
||||
return [...discovered.values()].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
|
||||
}
|
||||
|
||||
function contentVersion(files) {
|
||||
function contentVersion(files, packs) {
|
||||
const digest = createHash("sha256");
|
||||
for (const file of files) digest.update(`${file.logicalPath}\0${file.sha256}\0${file.bytes}\n`);
|
||||
for (const pack of packs) {
|
||||
digest.update(`pack\0${pack.id}\0${pack.label}\0${pack.bundledOnAndroid}\0${pack.dependencies.join(",")}\n`);
|
||||
}
|
||||
return `content-${digest.digest("hex").slice(0, 16)}`;
|
||||
}
|
||||
|
||||
function assetPackId(url) {
|
||||
if (typeof url !== "string" || !url.startsWith("/")) return null;
|
||||
return contentPackForPath(url.slice(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the logical install topology for the 54-dungeon WoW campaign.
|
||||
* Environment files intentionally remain in their independently downloadable
|
||||
* Manastorm packs; a dungeon pack owns its creatures and pulls in exactly the
|
||||
* environment/shared-creature packs referenced by the generated definition.
|
||||
*/
|
||||
export function campaignDungeonPackDefinitions(definitions) {
|
||||
const packs = new Map();
|
||||
for (const definition of definitions) {
|
||||
const dungeonPackId = `dungeon:${definition.id}`;
|
||||
if (definition.id === "wailing-caverns") {
|
||||
packs.set(dungeonPackId, {
|
||||
id: dungeonPackId,
|
||||
label: definition.title ?? packLabel(dungeonPackId),
|
||||
bundledOnAndroid: true,
|
||||
dependencies: [],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const environmentUrls = [
|
||||
...(definition.assets?.visual ?? []).map((asset) => asset.url),
|
||||
...(definition.assets?.collision ?? []).map((asset) => asset.url),
|
||||
definition.assets?.navigation?.url,
|
||||
].filter(Boolean);
|
||||
const resolvedEnvironmentPacks = environmentUrls.map(assetPackId);
|
||||
if (!resolvedEnvironmentPacks.length || resolvedEnvironmentPacks.some((id) => !id?.startsWith("manastorm:"))) {
|
||||
throw new Error(`${definition.id} has an environment asset outside a Manastorm content pack.`);
|
||||
}
|
||||
const environmentPackIds = new Set(resolvedEnvironmentPacks);
|
||||
if (environmentPackIds.size !== 1) {
|
||||
throw new Error(`${definition.id} must resolve to exactly one Manastorm environment pack.`);
|
||||
}
|
||||
const [environmentPackId] = environmentPackIds;
|
||||
packs.set(environmentPackId, {
|
||||
id: environmentPackId,
|
||||
label: packLabel(environmentPackId),
|
||||
bundledOnAndroid: false,
|
||||
dependencies: [],
|
||||
});
|
||||
|
||||
const creatureUrls = Object.values(definition.entities ?? {})
|
||||
.map((entity) => entity.visual?.model?.url)
|
||||
.filter(Boolean);
|
||||
const resolvedCreaturePacks = creatureUrls.map(assetPackId);
|
||||
if (resolvedCreaturePacks.some((id) => !id)) {
|
||||
throw new Error(`${definition.id} has a creature asset outside a content pack.`);
|
||||
}
|
||||
const referencedCreaturePacks = new Set(resolvedCreaturePacks.filter((id) => id !== dungeonPackId));
|
||||
const unexpectedCreaturePacks = [...referencedCreaturePacks].filter((id) => id !== "shared:creatures");
|
||||
if (unexpectedCreaturePacks.length) {
|
||||
throw new Error(`${definition.id} references unexpected creature packs: ${unexpectedCreaturePacks.join(", ")}`);
|
||||
}
|
||||
packs.set(dungeonPackId, {
|
||||
id: dungeonPackId,
|
||||
label: definition.title ?? packLabel(dungeonPackId),
|
||||
bundledOnAndroid: false,
|
||||
dependencies: [environmentPackId, ...referencedCreaturePacks],
|
||||
});
|
||||
}
|
||||
return [...packs.values()].sort((left, right) => left.id.localeCompare(right.id));
|
||||
}
|
||||
|
||||
export async function loadCampaignDungeonPackDefinitions(
|
||||
catalogPath = WOW_CAMPAIGN_CATALOG_PATH,
|
||||
) {
|
||||
const definitions = await loadCampaignDungeonDefinitions(catalogPath);
|
||||
return campaignDungeonPackDefinitions(definitions);
|
||||
}
|
||||
|
||||
export async function loadCampaignDungeonDefinitions(
|
||||
catalogPath = WOW_CAMPAIGN_CATALOG_PATH,
|
||||
) {
|
||||
const campaign = JSON.parse(await readFile(catalogPath, "utf8"));
|
||||
if (!Array.isArray(campaign.definitions)) {
|
||||
throw new Error(`WoW campaign catalog is missing definitions: ${catalogPath}`);
|
||||
}
|
||||
if (campaign.definitions.length !== WOW_CAMPAIGN_DUNGEON_COUNT) {
|
||||
throw new Error(
|
||||
`WoW campaign catalog must contain ${WOW_CAMPAIGN_DUNGEON_COUNT} dungeons; found ${campaign.definitions.length}.`,
|
||||
);
|
||||
}
|
||||
const dungeonIds = campaign.definitions.map((definition) => definition?.id);
|
||||
if (dungeonIds.some((id) => typeof id !== "string" || !id)
|
||||
|| new Set(dungeonIds).size !== dungeonIds.length) {
|
||||
throw new Error("WoW campaign catalog contains a missing or duplicate dungeon id.");
|
||||
}
|
||||
return campaign.definitions;
|
||||
}
|
||||
|
||||
export function createContentManifest(files, options = {}) {
|
||||
const baseUrl = String(options.baseUrl ?? "/content/objects").replace(/\/+$/, "");
|
||||
const version = options.version ?? contentVersion(files);
|
||||
const packs = new Map();
|
||||
const fileEntries = {};
|
||||
|
||||
@@ -227,16 +333,93 @@ export function createContentManifest(files, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const declared of options.packDefinitions ?? []) {
|
||||
const current = packs.get(declared.id) ?? {
|
||||
id: declared.id,
|
||||
label: packLabel(declared.id),
|
||||
bytes: 0,
|
||||
fileCount: 0,
|
||||
bundledOnAndroid: BUNDLED_ANDROID_PACKS.includes(declared.id),
|
||||
dependencies: [],
|
||||
};
|
||||
packs.set(declared.id, {
|
||||
...current,
|
||||
label: declared.label ?? current.label,
|
||||
bundledOnAndroid: declared.bundledOnAndroid ?? current.bundledOnAndroid,
|
||||
dependencies: [...declared.dependencies],
|
||||
});
|
||||
}
|
||||
|
||||
const sortedPacks = [...packs.values()].sort((left, right) => left.id.localeCompare(right.id));
|
||||
const version = options.version ?? contentVersion(files, sortedPacks);
|
||||
|
||||
return {
|
||||
schemaVersion: CONTENT_SCHEMA_VERSION,
|
||||
version,
|
||||
createdAt: options.createdAt ?? new Date().toISOString(),
|
||||
minimumAppVersion: options.minimumAppVersion ?? DEFAULT_MINIMUM_APP_VERSION,
|
||||
packs: [...packs.values()].sort((left, right) => left.id.localeCompare(right.id)),
|
||||
packs: sortedPacks,
|
||||
files: fileEntries,
|
||||
};
|
||||
}
|
||||
|
||||
export function assertCampaignDungeonManifest(manifest, definitions) {
|
||||
if (!Array.isArray(definitions) || definitions.length !== WOW_CAMPAIGN_DUNGEON_COUNT) {
|
||||
throw new Error(`Release content requires exactly ${WOW_CAMPAIGN_DUNGEON_COUNT} WoW dungeon definitions.`);
|
||||
}
|
||||
const expectedDefinitions = campaignDungeonPackDefinitions(definitions);
|
||||
const manifestPacks = new Map(manifest.packs.map((pack) => [pack.id, pack]));
|
||||
if (manifestPacks.size !== manifest.packs.length) {
|
||||
throw new Error("Release content manifest contains duplicate pack ids.");
|
||||
}
|
||||
|
||||
for (const expected of expectedDefinitions) {
|
||||
const actual = manifestPacks.get(expected.id);
|
||||
if (!actual) throw new Error(`Release content manifest is missing ${expected.id}.`);
|
||||
if (actual.bundledOnAndroid !== expected.bundledOnAndroid
|
||||
|| actual.dependencies.length !== expected.dependencies.length
|
||||
|| actual.dependencies.some((dependency, index) => dependency !== expected.dependencies[index])) {
|
||||
throw new Error(`Release content manifest has stale dependencies for ${expected.id}.`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const pack of manifest.packs) {
|
||||
const packFiles = Object.values(manifest.files).filter((entry) => entry.packId === pack.id);
|
||||
const packBytes = packFiles.reduce((total, entry) => total + entry.bytes, 0);
|
||||
if (pack.fileCount !== packFiles.length || pack.bytes !== packBytes) {
|
||||
throw new Error(`Release content manifest has invalid file totals for ${pack.id}.`);
|
||||
}
|
||||
for (const dependency of pack.dependencies) {
|
||||
if (!manifestPacks.has(dependency)) {
|
||||
throw new Error(`Release content manifest is missing dependency ${dependency} for ${pack.id}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const definition of definitions) {
|
||||
const referencedUrls = [
|
||||
...(definition.assets?.visual ?? []).map((asset) => asset.url),
|
||||
...(definition.assets?.collision ?? []).map((asset) => asset.url),
|
||||
definition.assets?.navigation?.url,
|
||||
...Object.values(definition.entities ?? {}).map((entity) => entity.visual?.model?.url),
|
||||
].filter(Boolean);
|
||||
for (const logicalPath of referencedUrls) {
|
||||
if (!manifest.files[logicalPath]) {
|
||||
throw new Error(`Release content manifest is missing ${definition.id} asset ${logicalPath}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dungeonPackCount = definitions.filter((definition) => (
|
||||
manifestPacks.has(`dungeon:${definition.id}`)
|
||||
)).length;
|
||||
if (dungeonPackCount !== WOW_CAMPAIGN_DUNGEON_COUNT) {
|
||||
throw new Error(
|
||||
`Release content manifest contains ${dungeonPackCount}/${WOW_CAMPAIGN_DUNGEON_COUNT} WoW dungeon packs.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyObjectIfMissing(file, objectDirectory) {
|
||||
const objectName = `${file.sha256}${file.extension}`;
|
||||
const destination = path.join(objectDirectory, objectName);
|
||||
@@ -263,20 +446,35 @@ async function replaceJsonAtomically(destination, value) {
|
||||
|
||||
export async function publishContent(options = {}) {
|
||||
const contentDirectory = path.resolve(options.contentDirectory ?? process.env.CONTENT_DIR ?? path.join(PROJECT_ROOT, "content"));
|
||||
const files = await scanContentSources(options.sources ?? DEFAULT_CONTENT_SOURCES);
|
||||
const includeCampaignPacks = options.includeCampaignPacks !== false;
|
||||
const campaignDefinitions = includeCampaignPacks ? await loadCampaignDungeonDefinitions() : null;
|
||||
const campaignPackDefinitions = campaignDefinitions
|
||||
? campaignDungeonPackDefinitions(campaignDefinitions)
|
||||
: [];
|
||||
const packDefinitions = [
|
||||
...campaignPackDefinitions,
|
||||
...(options.packDefinitions ?? []),
|
||||
];
|
||||
const manifest = createContentManifest(files, { ...options, packDefinitions });
|
||||
if (campaignDefinitions) assertCampaignDungeonManifest(manifest, campaignDefinitions);
|
||||
|
||||
const objectDirectory = path.join(contentDirectory, "objects");
|
||||
const manifestDirectory = path.join(contentDirectory, "manifests");
|
||||
await mkdir(objectDirectory, { recursive: true });
|
||||
await mkdir(manifestDirectory, { recursive: true });
|
||||
|
||||
const files = await scanContentSources(options.sources ?? DEFAULT_CONTENT_SOURCES);
|
||||
let copiedObjects = 0;
|
||||
for (const file of files) {
|
||||
if (await copyObjectIfMissing(file, objectDirectory)) copiedObjects += 1;
|
||||
}
|
||||
const manifest = createContentManifest(files, options);
|
||||
await replaceJsonAtomically(path.join(manifestDirectory, `${manifest.version}.json`), manifest);
|
||||
await replaceJsonAtomically(path.join(contentDirectory, "manifest.json"), manifest);
|
||||
return { contentDirectory, copiedObjects, manifest };
|
||||
return {
|
||||
contentDirectory,
|
||||
copiedObjects,
|
||||
manifest,
|
||||
campaignDungeonPackCount: campaignDefinitions?.length ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
function parseCliArguments(argv) {
|
||||
@@ -296,6 +494,7 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
|
||||
const bytes = result.manifest.packs.reduce((total, pack) => total + pack.bytes, 0);
|
||||
console.log(`Published ${result.manifest.files ? Object.keys(result.manifest.files).length : 0} files (${bytes} bytes)`);
|
||||
console.log(`Content version: ${result.manifest.version}`);
|
||||
console.log(`WoW campaign dungeon packs: ${result.campaignDungeonPackCount}/${WOW_CAMPAIGN_DUNGEON_COUNT}`);
|
||||
console.log(`New immutable objects: ${result.copiedObjects}`);
|
||||
console.log(`Manifest: ${path.join(result.contentDirectory, "manifest.json")}`);
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { access, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import {
|
||||
BUNDLED_ANDROID_PACKS,
|
||||
PROJECT_ROOT,
|
||||
assertCampaignDungeonManifest,
|
||||
contentPackForPath,
|
||||
createContentManifest,
|
||||
loadCampaignDungeonPackDefinitions,
|
||||
publishContent,
|
||||
scanContentSources,
|
||||
} from "./publish-content.mjs";
|
||||
@@ -31,6 +34,160 @@ test("assigns stable player-facing packs from logical asset paths", () => {
|
||||
assert.equal(contentPackForPath("basis/basis_transcoder.wasm"), "core:runtime");
|
||||
});
|
||||
|
||||
test("gives every WoW campaign dungeon an exact install dependency closure", async () => {
|
||||
const campaign = JSON.parse(await readFile(path.join(
|
||||
PROJECT_ROOT,
|
||||
"src",
|
||||
"game",
|
||||
"generated",
|
||||
"dungeonCampaignCatalog.json",
|
||||
), "utf8"));
|
||||
const packDefinitions = await loadCampaignDungeonPackDefinitions();
|
||||
const dungeonDefinitions = packDefinitions.filter((pack) => pack.id.startsWith("dungeon:"));
|
||||
const wailingCreatureManifest = JSON.parse(await readFile(path.join(
|
||||
PROJECT_ROOT,
|
||||
"public",
|
||||
"assets",
|
||||
"creatures",
|
||||
"wailing-caverns",
|
||||
"animated-manifest.json",
|
||||
), "utf8"));
|
||||
assert.equal(campaign.definitions.length, 54);
|
||||
assert.equal(dungeonDefinitions.length, 54);
|
||||
assert.equal(wailingCreatureManifest.creatures.length, 16);
|
||||
|
||||
const referencedSourceFiles = [
|
||||
...campaign.definitions.flatMap((dungeon) => [
|
||||
...[
|
||||
...dungeon.assets.visual,
|
||||
...dungeon.assets.collision,
|
||||
dungeon.assets.navigation,
|
||||
].map((asset) => ({ dungeonId: dungeon.id, url: asset.url })),
|
||||
...Object.values(dungeon.entities).flatMap((entity) => (
|
||||
entity.visual?.model?.url ? [{ dungeonId: dungeon.id, url: entity.visual.model.url }] : []
|
||||
)),
|
||||
]),
|
||||
...wailingCreatureManifest.creatures.map((creature) => ({
|
||||
dungeonId: "wailing-caverns",
|
||||
url: creature.url,
|
||||
})),
|
||||
];
|
||||
await Promise.all(referencedSourceFiles.map(async ({ dungeonId, url }) => {
|
||||
try {
|
||||
await access(path.join(PROJECT_ROOT, "public", ...url.slice(1).split("/")));
|
||||
} catch {
|
||||
assert.fail(`${dungeonId} is missing ${url}`);
|
||||
}
|
||||
}));
|
||||
await Promise.all([
|
||||
"wailing-caverns-visual.glb",
|
||||
"wailing-caverns-collision.glb",
|
||||
"wailing-caverns-navigation.glb",
|
||||
].map((fileName) => access(path.join(
|
||||
PROJECT_ROOT,
|
||||
"src",
|
||||
"assets",
|
||||
"game",
|
||||
"dungeons",
|
||||
"wailing-caverns",
|
||||
fileName,
|
||||
))));
|
||||
|
||||
const logicalFiles = new Map();
|
||||
const addFile = (logicalPath) => logicalFiles.set(logicalPath, {
|
||||
logicalPath,
|
||||
sourcePath: logicalPath,
|
||||
sha256: "a".repeat(64),
|
||||
bytes: 1,
|
||||
extension: path.extname(logicalPath),
|
||||
packId: contentPackForPath(logicalPath),
|
||||
});
|
||||
addFile("assets/game/dungeons/wailing-caverns/visual.glb");
|
||||
for (const dungeon of campaign.definitions) {
|
||||
for (const asset of [
|
||||
...dungeon.assets.visual,
|
||||
...dungeon.assets.collision,
|
||||
dungeon.assets.navigation,
|
||||
]) addFile(asset.url.slice(1));
|
||||
for (const entity of Object.values(dungeon.entities)) {
|
||||
if (entity.visual?.model?.url) addFile(entity.visual.model.url.slice(1));
|
||||
}
|
||||
}
|
||||
const manifest = createContentManifest([...logicalFiles.values()], {
|
||||
packDefinitions,
|
||||
version: "campaign-contract",
|
||||
});
|
||||
assert.doesNotThrow(() => assertCampaignDungeonManifest(manifest, campaign.definitions));
|
||||
const packs = new Map(manifest.packs.map((pack) => [pack.id, pack]));
|
||||
|
||||
for (const dungeon of campaign.definitions) {
|
||||
const pack = packs.get(`dungeon:${dungeon.id}`);
|
||||
assert.ok(pack, dungeon.id);
|
||||
if (dungeon.id === "wailing-caverns") {
|
||||
assert.equal(pack.bundledOnAndroid, true);
|
||||
assert.deepEqual(pack.dependencies, []);
|
||||
continue;
|
||||
}
|
||||
const environmentPack = contentPackForPath(dungeon.assets.visual[0].url.slice(1));
|
||||
const needsSharedCreatures = Object.values(dungeon.entities).some((entity) => (
|
||||
entity.visual?.model?.url?.startsWith("/assets/game/creatures/shared/")
|
||||
));
|
||||
assert.deepEqual(
|
||||
pack.dependencies,
|
||||
[environmentPack, ...(needsSharedCreatures ? ["shared:creatures"] : [])],
|
||||
dungeon.id,
|
||||
);
|
||||
assert.deepEqual(packs.get(environmentPack)?.dependencies, [], dungeon.id);
|
||||
}
|
||||
|
||||
for (const [dungeonId, environmentPack] of [
|
||||
["tinkertech-showdown", "manastorm:821-tinketechshowdown"],
|
||||
["frozen-reach", "manastorm:908-alvaencounter"],
|
||||
["forgotten-mine", "manastorm:1781-forgottenmine"],
|
||||
]) {
|
||||
const pack = packs.get(`dungeon:${dungeonId}`);
|
||||
assert.equal(pack.fileCount, 0);
|
||||
assert.equal(pack.bytes, 0);
|
||||
assert.deepEqual(pack.dependencies, [environmentPack]);
|
||||
}
|
||||
});
|
||||
|
||||
test("default release publishing fails before writing an incomplete campaign manifest", async () => {
|
||||
const directory = await mkdtemp(path.join(os.tmpdir(), "healer-man-incomplete-content-test-"));
|
||||
const sourceDirectory = path.join(directory, "source");
|
||||
const contentDirectory = path.join(directory, "content");
|
||||
await mkdir(sourceDirectory, { recursive: true });
|
||||
await writeFile(path.join(sourceDirectory, "placeholder.glb"), Buffer.from("incomplete"));
|
||||
|
||||
try {
|
||||
await assert.rejects(publishContent({
|
||||
contentDirectory,
|
||||
sources: [{ directory: sourceDirectory, logicalPrefix: "assets/game/dungeons/placeholder" }],
|
||||
}), /Release content manifest is missing/);
|
||||
await assert.rejects(access(contentDirectory), { code: "ENOENT" });
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("content versions change when only logical pack dependencies change", () => {
|
||||
const files = [{
|
||||
logicalPath: "assets/game/dungeons/test/creature.glb",
|
||||
sourcePath: "creature.glb",
|
||||
sha256: "b".repeat(64),
|
||||
bytes: 1,
|
||||
extension: ".glb",
|
||||
packId: "dungeon:test",
|
||||
}];
|
||||
const first = createContentManifest(files, {
|
||||
packDefinitions: [{ id: "dungeon:test", dependencies: ["manastorm:first"] }],
|
||||
});
|
||||
const second = createContentManifest(files, {
|
||||
packDefinitions: [{ id: "dungeon:test", dependencies: ["manastorm:second"] }],
|
||||
});
|
||||
assert.notEqual(first.version, second.version);
|
||||
});
|
||||
|
||||
test("publishes objects before a deterministic latest manifest", async () => {
|
||||
const directory = await mkdtemp(path.join(os.tmpdir(), "healer-man-content-test-"));
|
||||
const sourceDirectory = path.join(directory, "source");
|
||||
@@ -45,7 +202,12 @@ test("publishes objects before a deterministic latest manifest", async () => {
|
||||
assert.equal(Object.keys(firstManifest.files).length, 1);
|
||||
assert.equal(firstManifest.packs[0].id, "dungeon:wailing-caverns");
|
||||
|
||||
const first = await publishContent({ contentDirectory, sources, createdAt: "2026-08-14T00:00:00.000Z" });
|
||||
const first = await publishContent({
|
||||
contentDirectory,
|
||||
sources,
|
||||
includeCampaignPacks: false,
|
||||
createdAt: "2026-08-14T00:00:00.000Z",
|
||||
});
|
||||
assert.equal(first.copiedObjects, 1);
|
||||
const latest = JSON.parse(await readFile(path.join(contentDirectory, "manifest.json"), "utf8"));
|
||||
assert.equal(latest.version, first.manifest.version);
|
||||
@@ -53,7 +215,12 @@ test("publishes objects before a deterministic latest manifest", async () => {
|
||||
assert.match(entry.url, /^\/content\/objects\/[a-f0-9]{64}\.glb$/);
|
||||
assert.equal(await readFile(path.join(contentDirectory, "objects", path.basename(entry.url)), "utf8"), "glb-one");
|
||||
|
||||
const second = await publishContent({ contentDirectory, sources, createdAt: "2026-08-14T00:00:00.000Z" });
|
||||
const second = await publishContent({
|
||||
contentDirectory,
|
||||
sources,
|
||||
includeCampaignPacks: false,
|
||||
createdAt: "2026-08-14T00:00:00.000Z",
|
||||
});
|
||||
assert.equal(second.copiedObjects, 0);
|
||||
assert.equal(second.manifest.version, first.manifest.version);
|
||||
} finally {
|
||||
|
||||
Reference in new issue
Block a user