Merge branch 'codex/fix/option-board-support-alert'

This commit is contained in:
Tommaso Casaburi
2026-05-24 19:17:55 +07:00
9 changed files with 220 additions and 50 deletions
@@ -1,18 +1,40 @@
import { describe, expect, it } from 'vitest';
import { getNonokoPendingAccountCommentIndex, getNonokoPendingRouteState, getUnsupportedPostOptionsMessage, hasNonokoOption } from '../post-options-utils';
import {
getNonokoPendingAccountCommentIndex,
getNonokoPendingRouteState,
getPostOptionsValidationError,
getUnsupportedPostOptionsMessage,
hasNonokoOption,
} from '../post-options-utils';
describe('post-options-utils', () => {
it('rejects additional dice options instead of dropping them', () => {
expect(getUnsupportedPostOptionsMessage('dice+1d6 dice+1d20', 'qst')).toBe('unsupported options: dice+1d20');
expect(getUnsupportedPostOptionsMessage('dice+1d6 dice+1d20', 'qst')).toBe('Unsupported options: dice+1d20.');
});
it('supports nonoko while keeping sage unsupported', () => {
expect(getUnsupportedPostOptionsMessage('nonoko', undefined)).toBeNull();
expect(getUnsupportedPostOptionsMessage('sage', 'b')).toBe('unsupported options: sage');
expect(getUnsupportedPostOptionsMessage('sage', 'b')).toBe('Unsupported options: sage.');
expect(hasNonokoOption('fortune nonoko')).toBe(true);
expect(hasNonokoOption('nonokosage')).toBe(false);
});
it('describes board-specific options with supported directories', () => {
expect(getPostOptionsValidationError('fortune', 'mu')).toEqual({
unsupportedOptions: ['fortune'],
supportedDirectoryCodesByOption: [{ option: 'fortune', directoryCodes: ['b', 's5s'] }],
});
expect(getUnsupportedPostOptionsMessage('fortune', 'mu')).toBe('Unsupported options: fortune. Option "fortune" is supported on: /b/, /s5s/.');
expect(getUnsupportedPostOptionsMessage('sage fortune', 'pol')).toBe('Unsupported options: sage, fortune. Option "fortune" is supported on: /b/, /s5s/.');
expect(getPostOptionsValidationError('fortune dice+1d6', 'mu')).toEqual({
unsupportedOptions: ['fortune', 'dice+1d6'],
supportedDirectoryCodesByOption: [
{ option: 'fortune', directoryCodes: ['b', 's5s'] },
{ option: 'dice+1d6', directoryCodes: ['qst', 'tg'] },
],
});
});
it('reads the nonoko pending account comment index from direct and wrapped route state', () => {
expect(getNonokoPendingRouteState(7)).toEqual({ nonokoPendingAccountCommentIndex: 7 });
expect(getNonokoPendingAccountCommentIndex({ nonokoPendingAccountCommentIndex: 7 })).toBe(7);
+59 -10
View File
@@ -19,6 +19,20 @@ export interface DiceRoll {
total: number;
}
export interface PostOptionsValidationError {
unsupportedOptions: string[];
supportedDirectoryCodesByOption: Array<{ option: string; directoryCodes: string[] }>;
}
export const isPostOptionsValidationError = (error: unknown): error is PostOptionsValidationError => {
if (!error || typeof error !== 'object') {
return false;
}
const value = error as Partial<PostOptionsValidationError>;
return Array.isArray(value.unsupportedOptions) && Array.isArray(value.supportedDirectoryCodesByOption);
};
interface PostOptionsDirectory {
directoryCode?: string;
title?: string;
@@ -88,27 +102,62 @@ const isSupportedPostOption = (option: string, directoryCode: string | undefined
return !!parseDiceOption(option) && !!directoryCode && DICE_DIRECTORY_CODES.has(directoryCode);
};
const getUnsupportedPostOptions = (value: string, directoryCode: string | undefined): string[] => {
let hasDiceOption = false;
const getSupportedDirectoryCodes = (option: string): string[] => {
if (option === 'fortune') {
return [...FORTUNE_DIRECTORY_CODES];
}
return parsePostOptions(value).filter((option) => {
return parseDiceOption(option) ? [...DICE_DIRECTORY_CODES] : [];
};
export const getPostOptionsValidationError = (value: string, directoryCode: string | undefined): PostOptionsValidationError | null => {
let hasDiceOption = false;
const unsupportedOptions: string[] = [];
const supportedDirectoryCodesByOption: Array<{ option: string; directoryCodes: string[] }> = [];
for (const option of parsePostOptions(value)) {
const diceOption = parseDiceOption(option);
if (diceOption && directoryCode && DICE_DIRECTORY_CODES.has(directoryCode)) {
const isExtraDiceOption = hasDiceOption;
hasDiceOption = true;
return isExtraDiceOption;
if (isExtraDiceOption) {
unsupportedOptions.push(option);
}
continue;
}
return !isSupportedPostOption(option, directoryCode);
});
if (isSupportedPostOption(option, directoryCode)) {
continue;
}
const optionDirectoryCodes = getSupportedDirectoryCodes(option);
if (optionDirectoryCodes.length > 0) {
unsupportedOptions.push(option);
if (!supportedDirectoryCodesByOption.some((entry) => entry.option === option)) {
supportedDirectoryCodesByOption.push({ option, directoryCodes: optionDirectoryCodes });
}
} else {
unsupportedOptions.push(option);
}
}
return unsupportedOptions.length > 0 ? { unsupportedOptions, supportedDirectoryCodesByOption } : null;
};
export const getUnsupportedPostOptionsMessage = (value: string, directoryCode: string | undefined): string | null => {
const unsupportedOptions = getUnsupportedPostOptions(value, directoryCode);
return unsupportedOptions.length > 0 ? `unsupported options: ${unsupportedOptions.join(', ')}` : null;
};
const error = getPostOptionsValidationError(value, directoryCode);
if (!error) {
return null;
}
export const isUnsupportedPostOptionsMessage = (message: string | null): boolean => message?.startsWith('unsupported options:') === true;
let message = `Unsupported options: ${error.unsupportedOptions.join(', ')}.`;
for (const { option, directoryCodes } of error.supportedDirectoryCodesByOption) {
message += ` Option "${option}" is supported on: ${directoryCodes.map((code) => `/${code}/`).join(', ')}.`;
}
return message;
};
export const hasNonokoOption = (value: string): boolean => parsePostOptions(value).includes('nonoko');