feat(toolkit): add structured media authoring
ci/woodpecker/push/woodpecker Pipeline was successful
Lint and Build Checks / Lint, Test, and Build (push) Failing after 368h6m42s

This commit is contained in:
2026-06-24 11:29:32 -05:00
parent 52fb52a617
commit 47567589e4
56 changed files with 4222 additions and 686 deletions
+2 -2
View File
@@ -10,8 +10,8 @@ describe('changelog data', () => {
expect(changelogEntries.length).toBeGreaterThan(0);
expect(changelogEntries).toContainEqual(
expect.objectContaining({
id: 'build-metadata-changelog-poc',
title: 'Build Metadata and Changelog Scaffold',
id: 'consolidated-toolkit-content-files',
title: 'Consolidated Toolkit Content Files',
}),
);
});
+17 -1
View File
@@ -1,4 +1,10 @@
import { fallbackCreditsContent, isCreditsContent } from '../src/data';
import {
fallbackCreditsContent,
isCreditsContent,
isLegalContent,
legalContent,
toolkitData,
} from '../src/data';
describe('credits data', () => {
it('provides valid fallback credits content', () => {
@@ -21,4 +27,14 @@ describe('credits data', () => {
}),
).toBe(false);
});
it('exports legal and toolkit data through the root data index', () => {
expect(isLegalContent(legalContent)).toBe(true);
expect(toolkitData.petitioners.length).toBeGreaterThan(0);
expect(toolkitData.rulings.length).toBeGreaterThan(0);
expect(toolkitData.map.settlements.length).toBeGreaterThan(0);
expect(toolkitData.credits.mainLogo.title).toBe('Throne of the Bone King');
expect(toolkitData.changelog.length).toBeGreaterThan(0);
expect(toolkitData.legal.pages.length).toBeGreaterThan(0);
});
});
+2
View File
@@ -32,6 +32,7 @@ describe('routeDefinitions', () => {
'toolkitFlags',
'toolkitStory',
'toolkitLore',
'toolkitImages',
'toolkitAudio',
'toolkitCredits',
'toolkitEndings',
@@ -73,6 +74,7 @@ describe('routeDefinitions', () => {
'toolkitFlags',
'toolkitStory',
'toolkitLore',
'toolkitImages',
'toolkitAudio',
'toolkitCredits',
'toolkitEndings',
+113 -31
View File
@@ -266,6 +266,8 @@ describe('screen scaffold', () => {
const renderer = render(<ChangelogScreen goHome={jest.fn()} />);
const text = getRenderedText(renderer);
expect(text).toContain('Changelog Discipline Guidance');
expect(text).toContain('meaningful commit and push work');
expect(text).toContain('Build Metadata and Changelog Scaffold');
expect(text).toContain('0.0.1-20260616-13aa686');
expect(text).toContain('Seeded data-backed changelog content');
@@ -427,7 +429,6 @@ describe('screen scaffold', () => {
);
const text = getRenderedText(renderer);
expect(text).toContain('Toolkit');
expect(text).toContain('Opening');
expect(text).toContain('Petitioners');
expect(text).toContain('Rulings');
@@ -438,12 +439,11 @@ describe('screen scaffold', () => {
expect(text).toContain('Flags');
expect(text).toContain('Story');
expect(text).toContain('Lore');
expect(text).toContain('Images');
expect(text).toContain('Audio');
expect(text).toContain('Credits');
expect(text).toContain('Endings');
expect(text).toContain('References');
expect(text).toContain('Read-only planning panel');
expect(text).toContain('Future authoring area');
const homeButton = renderer.root.findByProps({ testID: 'toolkit-home' });
@@ -455,23 +455,7 @@ describe('screen scaffold', () => {
expect(renderer.root.findByProps({ testID: 'toolkit-socket-indicator' })).toBeTruthy();
});
it('renders purpose panels for every toolkit section without forms', () => {
const expectedPurposeText = {
toolkitAudio: 'Preview and catalog sound identity',
toolkitEndings: 'Tune collapse states',
toolkitFactions: 'Tune faction trust',
toolkitFlags: 'Debug delayed consequences',
toolkitLore: 'Reference changing canon',
toolkitMap: 'Support settlement and world-map iteration',
toolkitOpening: 'Preview and sequence the first-run premise',
toolkitPetitioners: 'Inspect and eventually author throne-room cases',
toolkitReferences: 'Keep design inspirations',
toolkitResources: 'Tune the core Bones, Minions, and Milk economy',
toolkitRulings: 'Inspect and eventually tune choice outcomes',
toolkitSeed: 'Debug why a day produced specific petitioners',
toolkitStory: 'Organize Act I-IV pressure',
} as const;
it('keeps legacy planning copy out of toolkit sections', () => {
for (const routeName of toolkitRoutes) {
if (routeName === 'toolkitCredits') {
continue;
@@ -486,21 +470,114 @@ describe('screen scaffold', () => {
);
const text = getRenderedText(renderer);
expect(text).toContain(expectedPurposeText[routeName]);
expect(renderer.root.findByProps({ testID: 'toolkit-purpose' })).toBeTruthy();
expect(
renderer.root.findByProps({ testID: 'toolkit-change-note' }),
).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'toolkit-sources' })).toBeTruthy();
renderer.root.findAllByProps({ testID: 'toolkit-purpose' }),
).toHaveLength(0);
expect(
renderer.root.findByProps({ testID: 'toolkit-future-fields' }),
).toBeTruthy();
renderer.root.findAllByProps({ testID: 'toolkit-change-note' }),
).toHaveLength(0);
expect(renderer.root.findAllByProps({ testID: 'toolkit-sources' })).toHaveLength(0);
expect(
renderer.root.findByProps({ testID: 'toolkit-future-placeholder' }),
).toBeTruthy();
renderer.root.findAllByProps({ testID: 'toolkit-future-fields' }),
).toHaveLength(0);
expect(
renderer.root.findAllByProps({ testID: 'toolkit-future-placeholder' }),
).toHaveLength(0);
expect(text).not.toContain('Future authoring area');
expect(text).not.toContain('Source wiki pages');
expect(text).not.toContain('Anticipated future fields / entities');
expect(text).not.toContain('Structured authoring panel');
expect(renderer.root.findByProps({ testID: 'toolkit-record-form' })).toBeTruthy();
}
});
it('renders authoring controls for active toolkit resources', () => {
const renderer = render(
<ToolkitScreen
goHome={jest.fn()}
navigate={jest.fn()}
routeName="toolkitPetitioners"
/>,
);
const text = getRenderedText(renderer);
expect(text).not.toContain('Structured authoring panel');
expect(text).not.toContain('Authoring');
expect(text).not.toContain('Existing records');
expect(text).toContain('Foreman Clatter');
expect(text).toContain('Id');
expect(text).toContain('Name');
expect(text).toContain('Choices');
expect(renderer.root.findByProps({ testID: 'toolkit-editor-petitioners' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'toolkit-record-list' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'toolkit-record-form' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'toolkit-field-id-input' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'toolkit-field-name-input' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'toolkit-field-choices-add' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'toolkit-editor-new' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'toolkit-editor-refresh' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'toolkit-editor-import' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'toolkit-editor-update' })).toBeTruthy();
expect(renderer.root.findAllByProps({ testID: 'toolkit-editor-create' })).toHaveLength(
0,
);
expect(
renderer.root.findByProps({
testID: 'toolkit-record-delete-petitioner_marrowgate_bad_bones_001',
}),
).toBeTruthy();
expect(renderer.root.findAllByProps({ testID: 'toolkit-editor-delete' })).toHaveLength(
0,
);
});
it('renders simplified audio upload and playback controls', () => {
const renderer = render(
<ToolkitScreen
goHome={jest.fn()}
navigate={jest.fn()}
routeName="toolkitAudio"
/>,
);
const text = getRenderedText(renderer);
expect(text).toContain('Throne Room Theme');
expect(text).toContain('Name');
expect(text).toContain('Category');
expect(text).toContain('Description');
expect(text).toContain('File Name');
expect(text).toContain('Audio Preview');
expect(renderer.root.findByProps({ testID: 'toolkit-editor-upload-audio' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'toolkit-audio-play-toggle' })).toBeTruthy();
expect(renderer.root.findAllByProps({ testID: 'toolkit-field-id-input' })).toHaveLength(
0,
);
expect(renderer.root.findAllByProps({ testID: 'toolkit-field-mood-input' })).toHaveLength(
0,
);
expect(
renderer.root.findAllByProps({ testID: 'toolkit-field-suggestedElements' }),
).toHaveLength(0);
});
it('renders image toolkit upload and preview controls', () => {
const renderer = render(
<ToolkitScreen
goHome={jest.fn()}
navigate={jest.fn()}
routeName="toolkitImages"
/>,
);
const text = getRenderedText(renderer);
expect(text).toContain('Images');
expect(text).toContain('Main Menu Background');
expect(text).toContain('Image Preview');
expect(renderer.root.findByProps({ testID: 'toolkit-editor-images' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'toolkit-editor-upload-image' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'toolkit-image-preview' })).toBeTruthy();
});
it('renders the toolkit credits display card from fallback data', () => {
const renderer = render(
<ToolkitScreen
@@ -518,7 +595,10 @@ describe('screen scaffold', () => {
expect(text).toContain('Font Library contributors');
expect(text).toContain(buildVersionLabel);
expect(text).toContain('Bundled fallback credits');
expect(text).not.toContain('Structured authoring panel');
expect(renderer.root.findByProps({ testID: 'toolkit-credits-card' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'toolkit-record-form' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'toolkit-field-mainLogo' })).toBeTruthy();
expect(
renderer.root.findByProps({ testID: 'toolkit-credits-contributors' }),
).toBeTruthy();
@@ -543,7 +623,7 @@ describe('screen scaffold', () => {
const text = getRenderedText(renderer);
const socketDot = renderer.root.findByProps({ testID: 'toolkit-socket-dot' });
expect(text).toContain('Connected');
expect(text).not.toContain('Connected');
expect(socketDot.props.style).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -692,7 +772,9 @@ describe('screen scaffold', () => {
for (const routeName of toolkitRoutes) {
pressByTestID(renderer, `toolkit-${routeName}`);
expect(getRenderedText(renderer)).toContain(routeDefinitions[routeName].title);
expect(
renderer.root.findByProps({ testID: `toolkit-${routeName}` }),
).toBeTruthy();
}
pressByTestID(renderer, 'toolkit-home');
+43
View File
@@ -39,6 +39,15 @@ async function createTemporaryCreditsFile(content) {
return filePath;
}
async function createTemporaryJsonFile(content, prefix = 'blb-content-') {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
const filePath = path.join(directory, 'content.json');
await fs.writeFile(filePath, `${JSON.stringify(content, null, 2)}\n`, 'utf8');
return filePath;
}
describe('socket content store', () => {
it('reads valid JSON content with resource and revision metadata', async () => {
const filePath = await createTemporaryCreditsFile(validCredits);
@@ -94,4 +103,38 @@ describe('socket content store', () => {
resource: 'credits',
});
});
it('creates, updates, and deletes array-backed toolkit records', async () => {
const filePath = await createTemporaryJsonFile([]);
const store = createJsonContentStore({
petitioners: filePath,
});
const record = {
id: 'petitioner_test_001',
name: 'Test Petitioner',
};
const updatedRecord = {
...record,
name: 'Updated Petitioner',
};
await expect(
store.createJsonRecord('petitioners', record),
).resolves.toMatchObject({
content: [record],
resource: 'petitioners',
});
await expect(
store.updateJsonRecord('petitioners', updatedRecord),
).resolves.toMatchObject({
content: [updatedRecord],
resource: 'petitioners',
});
await expect(
store.deleteJsonRecord('petitioners', updatedRecord.id),
).resolves.toMatchObject({
content: [],
resource: 'petitioners',
});
});
});
+84
View File
@@ -2,18 +2,27 @@ import { act } from 'react-test-renderer';
import { fallbackCreditsContent } from '../src/data';
import {
createToolkitContentRecord,
deleteToolkitContentRecord,
readToolkitContent,
saveToolkitContent,
updateToolkitContentRecord,
} from '../src/socket/toolkitSocketClient';
import { useToolkitContentStore } from '../src/state/toolkitContentStore';
jest.mock('../src/socket/toolkitSocketClient', () => ({
createToolkitContentRecord: jest.fn(),
deleteToolkitContentRecord: jest.fn(),
readToolkitContent: jest.fn(),
saveToolkitContent: jest.fn(),
updateToolkitContentRecord: jest.fn(),
}));
const mockedCreateToolkitContentRecord = jest.mocked(createToolkitContentRecord);
const mockedDeleteToolkitContentRecord = jest.mocked(deleteToolkitContentRecord);
const mockedReadToolkitContent = jest.mocked(readToolkitContent);
const mockedSaveToolkitContent = jest.mocked(saveToolkitContent);
const mockedUpdateToolkitContentRecord = jest.mocked(updateToolkitContentRecord);
const changedCredits = {
...fallbackCreditsContent,
@@ -28,12 +37,22 @@ describe('useToolkitContentStore', () => {
beforeEach(() => {
mockedReadToolkitContent.mockReset();
mockedSaveToolkitContent.mockReset();
mockedCreateToolkitContentRecord.mockReset();
mockedDeleteToolkitContentRecord.mockReset();
mockedUpdateToolkitContentRecord.mockReset();
useToolkitContentStore.setState({
contentByResource: {
...useToolkitContentStore.getState().contentByResource,
credits: fallbackCreditsContent,
},
credits: fallbackCreditsContent,
error: null,
errorsByResource: {},
lastLoadedAt: null,
revision: null,
revisionsByResource: {},
status: 'idle',
statusByResource: {},
});
});
@@ -109,4 +128,69 @@ describe('useToolkitContentStore', () => {
status: 'error',
});
});
it('creates, updates, and deletes toolkit records through the socket client', async () => {
const newPetitioner = {
id: 'petitioner_test_001',
name: 'Test Petitioner',
sourceWikiPages: ['Gameplay'],
tags: ['test'],
};
const changedPetitioners = [newPetitioner];
mockedCreateToolkitContentRecord.mockResolvedValue({
content: changedPetitioners,
resource: 'petitioners',
revision: '101',
});
mockedUpdateToolkitContentRecord.mockResolvedValue({
content: changedPetitioners,
resource: 'petitioners',
revision: '102',
});
mockedDeleteToolkitContentRecord.mockResolvedValue({
content: [],
resource: 'petitioners',
revision: '103',
});
await act(async () => {
await useToolkitContentStore
.getState()
.createRecord('petitioners', newPetitioner);
});
expect(mockedCreateToolkitContentRecord).toHaveBeenCalledWith(
'petitioners',
newPetitioner,
);
expect(useToolkitContentStore.getState().contentByResource.petitioners).toEqual(
changedPetitioners,
);
await act(async () => {
await useToolkitContentStore
.getState()
.updateRecord('petitioners', newPetitioner);
});
expect(mockedUpdateToolkitContentRecord).toHaveBeenCalledWith(
'petitioners',
newPetitioner,
);
await act(async () => {
await useToolkitContentStore
.getState()
.deleteRecord('petitioners', newPetitioner.id);
});
expect(mockedDeleteToolkitContentRecord).toHaveBeenCalledWith(
'petitioners',
newPetitioner.id,
);
expect(useToolkitContentStore.getState().contentByResource.petitioners).toEqual(
[],
);
});
});
+24 -6
View File
@@ -1,12 +1,15 @@
const { readFileSync } = require('node:fs');
const { existsSync, readFileSync } = require('node:fs');
const path = require('node:path');
const toolkitPlaceholderFiles = [
const toolkitDataFiles = [
'audio.json',
'changelog.json',
'credits.json',
'endings.json',
'factions.json',
'flags.json',
'images.json',
'legal.json',
'lore.json',
'map.json',
'opening.json',
@@ -18,9 +21,20 @@ const toolkitPlaceholderFiles = [
'story.json',
];
describe('toolkit data placeholders', () => {
it('provides an empty array JSON file for every toolkit section', () => {
for (const fileName of toolkitPlaceholderFiles) {
describe('toolkit data files', () => {
it('keeps app content JSON consolidated under toolkit data', () => {
for (const fileName of ['changelog.json', 'credits.json', 'legal.json']) {
expect(
existsSync(path.join(process.cwd(), 'src', 'data', 'toolkit', fileName)),
).toBe(true);
expect(existsSync(path.join(process.cwd(), 'src', 'data', fileName))).toBe(
false,
);
}
});
it('provides starter JSON content for every toolkit data resource', () => {
for (const fileName of toolkitDataFiles) {
const filePath = path.join(
process.cwd(),
'src',
@@ -30,7 +44,11 @@ describe('toolkit data placeholders', () => {
);
const content = JSON.parse(readFileSync(filePath, 'utf8'));
expect(content).toEqual([]);
if (Array.isArray(content)) {
expect(content.length).toBeGreaterThan(0);
} else {
expect(Object.keys(content).length).toBeGreaterThan(0);
}
}
});
});