1e17d2c852
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m44s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m57s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m44s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m53s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m21s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m27s
Project CI / Frontend tests (pull_request) Successful in 7m4s
Project CI / AI game creator shell web tests (pull_request) Successful in 6m22s
Project CI / Native shell tests (pull_request) Successful in 10m13s
AI 润色回包与原文相同时给出明确提示,不再落下点了等于没点的「恢复原文」假入口 润色「与原文相同」的判据改为规范化之后要写回宿主的文本,回包被长度上限截回原文同样算没变化 共享聊天输入区显示润色提示,并让提示随用户手改提示词失效 快速编辑未提交草稿改按资源路径归属,收起面板或被旁路重投影后都不再静默丢掉 恢复入口按路径与本会话草稿对号,并区分「本会话未提交」与原生账本条目 账本读取失败时入口仍报出本会话草稿条数,「继续编辑」重开面板回填提示词与 @ 引用 派生资源名预检接入快速编辑与角色动画两条提交路径,早于正规化写盘 名称预检自身抛错时回写面板失败态,不再留下「点了没反应」的提交按钮 浮层关闭判定改在捕获阶段冻结,避免浮层内按钮卸载自己时整块面板被自己人收掉 恢复入口动作按钮收进统一容器,补移动端纵向排布与禁用态样式 空草稿表改工厂函数,跨语言对表用例按模块路径解析 Rust 源码 补回归用例:旁路重投影后的草稿、原样回包提示、提交前名称拦截、草稿表读写与跨卡隔离 同步 decision-log 与画布验收待办的进展说明
507 lines
18 KiB
TypeScript
507 lines
18 KiB
TypeScript
import { readFileSync } from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
import { describe, expect, it } from 'vitest';
|
||
|
||
import {
|
||
canonicalProjectedResourceMediaType,
|
||
defaultCharacterAnimationResourceName,
|
||
defaultDerivedResourceName,
|
||
DERIVED_RESOURCE_NAME_SUFFIXES,
|
||
resolveDerivedResourceNameCheck,
|
||
resolveProjectCharacterAnimationCapability,
|
||
resolveProjectResourceEditCapability,
|
||
resolveResourceEditNameCheck,
|
||
resolveResourceEditRequestIdentity,
|
||
RESOURCE_EDIT_NAME_INVALID_NOTICE,
|
||
RESOURCE_EDIT_NAME_MAX_LENGTH,
|
||
RESOURCE_EDIT_PROMPT_TRUNCATED_NOTICE,
|
||
resourceEditPromptMaxLength,
|
||
truncateResourceEditPrompt,
|
||
} from '../src/view/project-development/resourceEditModel';
|
||
import type { ProjectResource } from '../src/view/project-development/resourceProjectionModel';
|
||
|
||
function resource(
|
||
input: Partial<ProjectResource> &
|
||
Pick<ProjectResource, 'id' | 'category' | 'path'>,
|
||
): ProjectResource {
|
||
return {
|
||
subtype: 'task-artifact',
|
||
label: input.path,
|
||
mediaType: '',
|
||
sourceLabel: '测试来源',
|
||
taskTitle: null,
|
||
manifestAssetId: null,
|
||
producerTaskId: 'task-1',
|
||
externalResourceId: null,
|
||
referenceResourceIds: [],
|
||
dependencies: [],
|
||
dependencyDepth: 0,
|
||
...input,
|
||
};
|
||
}
|
||
|
||
describe('全类型资源编辑能力模型', () => {
|
||
it('把现役资源稳定分流到图片派生、媒体派生、文本派生和版本分支', () => {
|
||
const cases: Array<{
|
||
resource: ProjectResource;
|
||
route: 'derive';
|
||
editKind?: string;
|
||
}> = [
|
||
{
|
||
resource: resource({
|
||
id: 'asset:png',
|
||
category: 'unclassified',
|
||
path: 'assets/hero.png',
|
||
mediaType: 'image/png',
|
||
manifestAssetId: 'png',
|
||
}),
|
||
route: 'derive',
|
||
editKind: 'image-reference',
|
||
},
|
||
{
|
||
resource: resource({
|
||
id: 'task:art:hero.webp',
|
||
category: 'unclassified',
|
||
path: 'hero.webp',
|
||
mediaType: '美术产物',
|
||
}),
|
||
route: 'derive',
|
||
editKind: 'image-reference',
|
||
},
|
||
{
|
||
resource: resource({
|
||
id: 'asset:gif',
|
||
category: 'unclassified',
|
||
path: 'assets/hero.gif',
|
||
mediaType: 'image/gif',
|
||
manifestAssetId: 'gif',
|
||
}),
|
||
route: 'derive',
|
||
editKind: 'image-reference',
|
||
},
|
||
{
|
||
resource: resource({
|
||
id: 'asset:svg',
|
||
category: 'unclassified',
|
||
path: 'assets/icon.svg',
|
||
mediaType: 'image/svg+xml',
|
||
manifestAssetId: 'svg',
|
||
}),
|
||
route: 'derive',
|
||
editKind: 'svg',
|
||
},
|
||
{
|
||
resource: resource({
|
||
id: 'task:art:intro.mp4',
|
||
category: 'unclassified',
|
||
path: 'assets/intro.mp4',
|
||
mediaType: '美术产物',
|
||
}),
|
||
route: 'derive',
|
||
editKind: 'video',
|
||
},
|
||
{
|
||
resource: resource({
|
||
id: 'asset:bgm',
|
||
category: 'audio',
|
||
path: 'audio/theme.ogg',
|
||
subtype: 'background-music',
|
||
mediaType: 'audio/ogg',
|
||
manifestAssetId: 'bgm',
|
||
}),
|
||
route: 'derive',
|
||
editKind: 'background-music',
|
||
},
|
||
{
|
||
resource: resource({
|
||
id: 'asset:sfx',
|
||
category: 'audio',
|
||
path: 'audio/hit.wav',
|
||
subtype: 'sound-effect',
|
||
mediaType: 'audio/wav',
|
||
manifestAssetId: 'sfx',
|
||
}),
|
||
route: 'derive',
|
||
editKind: 'sound-effect',
|
||
},
|
||
{
|
||
resource: resource({
|
||
id: 'task:code:main.ts',
|
||
category: 'document',
|
||
path: 'src/main.ts',
|
||
mediaType: '项目文档',
|
||
}),
|
||
route: 'derive',
|
||
editKind: 'text',
|
||
},
|
||
{
|
||
resource: resource({
|
||
id: 'agent-result:design:message-1',
|
||
category: 'document',
|
||
path: '专业 Agent 文本回执',
|
||
subtype: 'agent-result',
|
||
producerTaskId: null,
|
||
content: '回执',
|
||
}),
|
||
route: 'derive',
|
||
editKind: 'agent-result',
|
||
},
|
||
{
|
||
resource: resource({
|
||
id: 'version:v1',
|
||
category: 'version',
|
||
path: '项目版本 · v1',
|
||
subtype: 'project-version',
|
||
producerTaskId: null,
|
||
version: {
|
||
versionId: 'v1',
|
||
parentVersionId: null,
|
||
projectRevision: 1,
|
||
resourceBindings: [],
|
||
createdReason: 'initial',
|
||
createdAt: 1,
|
||
label: '版本 1',
|
||
childVersionIds: [],
|
||
},
|
||
}),
|
||
route: 'derive',
|
||
editKind: 'version',
|
||
},
|
||
];
|
||
|
||
for (const expected of cases) {
|
||
const capability = resolveProjectResourceEditCapability(
|
||
expected.resource,
|
||
);
|
||
expect(capability.route, expected.resource.id).toBe(expected.route);
|
||
expect(capability.editKind, expected.resource.id).toBe(expected.editKind);
|
||
}
|
||
});
|
||
|
||
it('用路径补齐任务产物缺失的标准媒体类型', () => {
|
||
expect(
|
||
canonicalProjectedResourceMediaType(
|
||
resource({
|
||
id: 'task:art:clip.mov',
|
||
category: 'unclassified',
|
||
path: 'assets/clip.mov',
|
||
mediaType: '美术产物',
|
||
}),
|
||
),
|
||
).toBe('video/quicktime');
|
||
expect(
|
||
canonicalProjectedResourceMediaType(
|
||
resource({
|
||
id: 'task:audio:effect.wav',
|
||
category: 'audio',
|
||
path: 'audio/effect.wav',
|
||
mediaType: '美术产物',
|
||
}),
|
||
),
|
||
).toBe('audio/wav');
|
||
expect(
|
||
canonicalProjectedResourceMediaType(
|
||
resource({
|
||
id: 'task:code:main.ts',
|
||
category: 'document',
|
||
path: 'src/main.ts',
|
||
mediaType: '项目文档',
|
||
}),
|
||
),
|
||
).toBe('text/typescript');
|
||
});
|
||
|
||
it('按现役媒体接口限制编辑提示词长度', () => {
|
||
expect(resourceEditPromptMaxLength('background-music')).toBe(140);
|
||
expect(resourceEditPromptMaxLength('sound-effect')).toBe(1_900);
|
||
expect(resourceEditPromptMaxLength('video')).toBe(4_000);
|
||
expect(resourceEditPromptMaxLength('character-animation')).toBe(4_000);
|
||
expect(resourceEditPromptMaxLength('text')).toBe(32_000);
|
||
});
|
||
|
||
it('超上限的润色结果按类型截断并给出替代文案,边界上不动内容', () => {
|
||
const exactlyLimit = '音'.repeat(140);
|
||
expect(
|
||
truncateResourceEditPrompt(exactlyLimit, 'background-music'),
|
||
).toEqual({
|
||
text: exactlyLimit,
|
||
notice: null,
|
||
});
|
||
const overLimit = `${exactlyLimit}多出来的一个字`;
|
||
expect(truncateResourceEditPrompt(overLimit, 'background-music')).toEqual({
|
||
text: exactlyLimit,
|
||
notice: RESOURCE_EDIT_PROMPT_TRUNCATED_NOTICE,
|
||
});
|
||
expect(RESOURCE_EDIT_PROMPT_TRUNCATED_NOTICE).toBe('已按长度上限截断');
|
||
// 上限按类型走,不是一刀切。
|
||
expect(
|
||
truncateResourceEditPrompt('字'.repeat(200), 'sound-effect').notice,
|
||
).toBeNull();
|
||
expect(
|
||
truncateResourceEditPrompt('字'.repeat(1_901), 'sound-effect').text
|
||
.length,
|
||
).toBe(1_900);
|
||
expect(
|
||
truncateResourceEditPrompt('字'.repeat(32_001), 'text').text.length,
|
||
).toBe(32_000);
|
||
});
|
||
|
||
it('请求身份只在提示词没变时复用', () => {
|
||
const first = resolveResourceEditRequestIdentity(null, '原始提示词');
|
||
expect(first.prompt).toBe('原始提示词');
|
||
expect(first.operationId).toBeTruthy();
|
||
expect(first.idempotencyKey).toBeTruthy();
|
||
|
||
// 同句重试:同一份身份,命中同一 operation 账本。
|
||
expect(resolveResourceEditRequestIdentity(first, '原始提示词')).toBe(first);
|
||
|
||
// 提示词变了(改了、或润色回填过):必须重铸,否则 Rust 的
|
||
// request_fingerprint(含 prompt)会把重试判成「已绑定到不同资源编辑请求」。
|
||
const second = resolveResourceEditRequestIdentity(first, '润色后的提示词');
|
||
expect(second).not.toBe(first);
|
||
expect(second.operationId).not.toBe(first.operationId);
|
||
expect(second.idempotencyKey).not.toBe(first.idempotencyKey);
|
||
expect(second.prompt).toBe('润色后的提示词');
|
||
});
|
||
|
||
it('图片资源可进入角色动画派生入口', () => {
|
||
const capability = resolveProjectCharacterAnimationCapability(
|
||
resource({
|
||
id: 'asset:hero',
|
||
category: 'unclassified',
|
||
path: 'assets/hero.png',
|
||
mediaType: 'image/png',
|
||
manifestAssetId: 'hero',
|
||
}),
|
||
);
|
||
expect(capability?.editKind).toBe('character-animation');
|
||
expect(capability?.generationMode).toBe('derive');
|
||
expect(
|
||
resolveProjectCharacterAnimationCapability(
|
||
resource({
|
||
id: 'asset:bgm',
|
||
category: 'audio',
|
||
path: 'audio/theme.mp3',
|
||
mediaType: 'audio/mpeg',
|
||
manifestAssetId: 'bgm',
|
||
}),
|
||
),
|
||
).toBeNull();
|
||
});
|
||
});
|
||
|
||
/**
|
||
* Rust 源码路径按**本文件位置**解析,不按仓库根 cwd 解析:这个用例是跨语言对表,
|
||
* 从别的工作目录单跑这一个文件时 `ENOENT` 会把「两边分叉」误报成一次环境问题。
|
||
*/
|
||
const RUST_RESOURCE_EDITOR_PATH = fileURLToPath(
|
||
new URL('../src-tauri/src/project/resource_editor.rs', import.meta.url),
|
||
);
|
||
|
||
/**
|
||
* Rust `normalize_resource_edit_name` 的函数体原文。
|
||
*
|
||
* 派生资源名的门禁在 Rust 侧,前端这份只是镜像;一旦两边数字或文案分叉,用户就会先看到
|
||
* 一句「提交前拦下」的话、再被后端换成另一句话,所以这里直接拿源码对表。
|
||
*/
|
||
function rustResourceEditNameNormalizer() {
|
||
const source = readFileSync(RUST_RESOURCE_EDITOR_PATH, 'utf8');
|
||
const start = source.indexOf('fn normalize_resource_edit_name(');
|
||
expect(start).toBeGreaterThan(-1);
|
||
const body = source.slice(start, source.indexOf('\n}\n', start));
|
||
expect(body.length).toBeGreaterThan(0);
|
||
return body;
|
||
}
|
||
|
||
/** 只换 label 的 Agent 回执资源:AGC 侧 label 直接取任务标题,是最容易触发名称门禁的一类。 */
|
||
function labelResource(label: string) {
|
||
return resource({
|
||
id: 'agent-result:design:message-1',
|
||
category: 'document',
|
||
path: '专业 Agent 文本回执',
|
||
subtype: 'agent-result',
|
||
producerTaskId: null,
|
||
content: '回执',
|
||
label,
|
||
});
|
||
}
|
||
|
||
/** 默认名函数与 kind 的对应关系,供「默认名 == 检查名」的断言复用。 */
|
||
function defaultDerivedNameFor(
|
||
target: ProjectResource,
|
||
kind: 'edit' | 'character-animation',
|
||
) {
|
||
return kind === 'edit'
|
||
? defaultDerivedResourceName(target)
|
||
: defaultCharacterAnimationResourceName(target);
|
||
}
|
||
|
||
describe('派生资源名称:提交前镜像 Rust 的 1..=120 与禁止控制字符', () => {
|
||
/**
|
||
* Rust 是权威:`value.trim()` → 非空 → `chars().count() > 120` → `char::is_control`,
|
||
* 任一不满足就 `Err`,并且**不截断**(没有 `truncate` / `chars().take(...)`)。
|
||
* 前端镜像必须逐条对齐,包括「宁可按原话拒绝、也不截成可能重名的名字」这条口径。
|
||
*/
|
||
it('上限与提示文案逐字取自 Rust 的 normalize_resource_edit_name', () => {
|
||
const body = rustResourceEditNameNormalizer();
|
||
|
||
expect(body).toMatch(/\bvalue\.trim\(\)/u);
|
||
expect(body).toMatch(/value\.is_empty\(\)/u);
|
||
expect(body).toMatch(/char::is_control/u);
|
||
expect(body).toMatch(/return Err\(/u);
|
||
expect(body).not.toMatch(/truncate|\.take\(/u);
|
||
|
||
const limit = /chars\(\)\.count\(\)\s*>\s*(\d+)/u.exec(body);
|
||
expect(limit?.[1]).toBe(String(RESOURCE_EDIT_NAME_MAX_LENGTH));
|
||
|
||
const notice = /"([^"]*派生资源名称[^"]*)"/u.exec(body);
|
||
expect(notice?.[1]).toBe(RESOURCE_EDIT_NAME_INVALID_NOTICE);
|
||
});
|
||
|
||
it('恰好 120 码点放行,多一个字就按 Rust 原话拦住且不截断', () => {
|
||
const exactlyLimit = '字'.repeat(RESOURCE_EDIT_NAME_MAX_LENGTH);
|
||
expect(resolveResourceEditNameCheck(exactlyLimit)).toEqual({
|
||
name: exactlyLimit,
|
||
error: null,
|
||
});
|
||
|
||
const overLimit = `${exactlyLimit}多`;
|
||
const overCheck = resolveResourceEditNameCheck(overLimit);
|
||
expect(overCheck.error).toBe(RESOURCE_EDIT_NAME_INVALID_NOTICE);
|
||
// 提示里报的长度上限必须是拦下它的那个数字,且原样返回(不返回截断名)。
|
||
expect(RESOURCE_EDIT_NAME_INVALID_NOTICE).toContain(
|
||
`1..=${RESOURCE_EDIT_NAME_MAX_LENGTH}`,
|
||
);
|
||
expect(overCheck.name).toBe(overLimit);
|
||
});
|
||
|
||
it('先 trim 再判空:首尾空白不算违规,只有空白则拦下', () => {
|
||
expect(resolveResourceEditNameCheck(' 小猫咪-编辑版 ')).toEqual({
|
||
name: '小猫咪-编辑版',
|
||
error: null,
|
||
});
|
||
expect(resolveResourceEditNameCheck('').error).toBe(
|
||
RESOURCE_EDIT_NAME_INVALID_NOTICE,
|
||
);
|
||
expect(resolveResourceEditNameCheck(' ').error).toBe(
|
||
RESOURCE_EDIT_NAME_INVALID_NOTICE,
|
||
);
|
||
expect(resolveResourceEditNameCheck('\n\t').error).toBe(
|
||
RESOURCE_EDIT_NAME_INVALID_NOTICE,
|
||
);
|
||
});
|
||
|
||
it('拒绝 Cc 控制字符(含内嵌换行 / 制表符),但放行 Rust 也放行的 Cf 格式符', () => {
|
||
for (const control of ['\u0000', '\u0007', '\u001f', '\u007f', '\u009f']) {
|
||
expect(
|
||
resolveResourceEditNameCheck(`角色${control}形象-编辑版`).error,
|
||
`U+${control.codePointAt(0)!.toString(16)}`,
|
||
).toBe(RESOURCE_EDIT_NAME_INVALID_NOTICE);
|
||
}
|
||
// 任务标题里的换行是真实成因:label 直接取任务标题,内嵌换行原样传下来。
|
||
for (const embedded of ['\n', '\r', '\t']) {
|
||
expect(
|
||
resolveResourceEditNameCheck(`生成角色${embedded}正面形象-编辑版`)
|
||
.error,
|
||
).toBe(RESOURCE_EDIT_NAME_INVALID_NOTICE);
|
||
}
|
||
// 首尾换行被 trim 掉,Rust 同样放行——不能在这里多收窄一条客户端自有规则。
|
||
expect(
|
||
resolveResourceEditNameCheck('\n角色形象-编辑版\n').error,
|
||
).toBeNull();
|
||
expect(
|
||
resolveResourceEditNameCheck('角色形象\u200b-编辑版').error,
|
||
).toBeNull();
|
||
});
|
||
|
||
it('按码点计长(chars().count() 口径),不按 UTF-16 长度误判', () => {
|
||
const emojiLimit = '😀'.repeat(RESOURCE_EDIT_NAME_MAX_LENGTH);
|
||
// UTF-16 长度是 240,码点数是 120:Rust 接受,前端不许拦。
|
||
expect(emojiLimit.length).toBe(RESOURCE_EDIT_NAME_MAX_LENGTH * 2);
|
||
expect(resolveResourceEditNameCheck(emojiLimit).error).toBeNull();
|
||
expect(
|
||
resolveResourceEditNameCheck(
|
||
`${'字'.repeat(RESOURCE_EDIT_NAME_MAX_LENGTH - 5)}${'😀'.repeat(5)}`,
|
||
).error,
|
||
).toBeNull();
|
||
expect(resolveResourceEditNameCheck(`${emojiLimit}😀`).error).toBe(
|
||
RESOURCE_EDIT_NAME_INVALID_NOTICE,
|
||
);
|
||
});
|
||
|
||
it('编辑版与角色动画共用同一张后缀表,默认名与检查名逐个相等', () => {
|
||
expect(DERIVED_RESOURCE_NAME_SUFFIXES).toEqual({
|
||
edit: '-编辑版',
|
||
'character-animation': '-角色动画',
|
||
});
|
||
const hero = resource({
|
||
id: 'asset:hero',
|
||
category: 'unclassified',
|
||
path: 'assets/hero.png',
|
||
label: 'hero.png',
|
||
mediaType: 'image/png',
|
||
manifestAssetId: 'hero',
|
||
});
|
||
expect(resolveDerivedResourceNameCheck(hero, 'edit')).toEqual({
|
||
name: 'hero-编辑版',
|
||
error: null,
|
||
});
|
||
expect(
|
||
resolveDerivedResourceNameCheck(hero, 'character-animation'),
|
||
).toEqual({ name: 'hero-角色动画', error: null });
|
||
// 默认名与检查名是同一份拼接结果,调用方不必各自拼一遍。
|
||
expect(defaultDerivedResourceName(hero)).toBe('hero-编辑版');
|
||
expect(defaultCharacterAnimationResourceName(hero)).toBe('hero-角色动画');
|
||
// 去扩展名 + 空基名兜底的口径不变。
|
||
expect(defaultDerivedResourceName(labelResource('.png'))).toBe(
|
||
'资源-编辑版',
|
||
);
|
||
expect(defaultDerivedResourceName(labelResource('theme.mp3'))).toBe(
|
||
'theme-编辑版',
|
||
);
|
||
});
|
||
|
||
/**
|
||
* 两个后缀都要处理:快速编辑的「-编辑版」和角色动画的「-角色动画」是同一个源名的两种
|
||
* 派生。后缀只多 4~5 个字,所以超限的源名在两条路径上都必须被拦下,而不是只拦一条。
|
||
*/
|
||
it('超长源名在编辑版与角色动画两条路径上都被拦下,不截成可能重名的名字', () => {
|
||
const base = '生成角色正面半身战斗姿态设定稿'.repeat(8);
|
||
const first = labelResource(`${base}A.png`);
|
||
const second = labelResource(`${base}B.png`);
|
||
|
||
for (const kind of ['edit', 'character-animation'] as const) {
|
||
const firstCheck = resolveDerivedResourceNameCheck(first, kind);
|
||
const secondCheck = resolveDerivedResourceNameCheck(second, kind);
|
||
expect(firstCheck.error).toBe(RESOURCE_EDIT_NAME_INVALID_NOTICE);
|
||
expect(secondCheck.error).toBe(RESOURCE_EDIT_NAME_INVALID_NOTICE);
|
||
// 不截断:检查结果仍是原样的长名(否则两个同前缀的长名会被截成同一个可用名字)。
|
||
expect([...firstCheck.name].length).toBeGreaterThan(
|
||
RESOURCE_EDIT_NAME_MAX_LENGTH,
|
||
);
|
||
expect(firstCheck.name).not.toBe(secondCheck.name);
|
||
expect(firstCheck.name).toBe(defaultDerivedNameFor(first, kind));
|
||
}
|
||
});
|
||
|
||
/**
|
||
* 真实成因:`agent-result` 资源的 label 直接取任务标题(`resourceProjectionModel`),
|
||
* 用户写的长标题或带换行的标题会原样进入派生名。
|
||
*/
|
||
it('任务标题型源名(超长 / 带换行)在派生前就给出同一句中文提示', () => {
|
||
const longTitle = `${'把主角改成夜行侠并加上战斗姿态'.repeat(9)}.png`;
|
||
expect(
|
||
resolveDerivedResourceNameCheck(labelResource(longTitle), 'edit').error,
|
||
).toBe(RESOURCE_EDIT_NAME_INVALID_NOTICE);
|
||
|
||
const multilineTitle = '生成角色\n正面形象.png';
|
||
expect(
|
||
resolveDerivedResourceNameCheck(
|
||
labelResource(multilineTitle),
|
||
'character-animation',
|
||
).error,
|
||
).toBe(RESOURCE_EDIT_NAME_INVALID_NOTICE);
|
||
});
|
||
});
|