@@ -6,6 +6,34 @@ const test = require("node:test");
66const root = path . resolve ( __dirname , ".." ) ;
77const read = ( file ) => fs . readFileSync ( path . join ( root , file ) , "utf8" ) ;
88
9+ function recommendationOutput ( overrides = { } ) {
10+ return {
11+ summary : "适合作为开书底座。" ,
12+ genreId : "genre-a" ,
13+ genreReason : "题材匹配。" ,
14+ primaryStoryModeId : "mode-a" ,
15+ primaryStoryModeReason : "推进稳定。" ,
16+ secondaryStoryModeId : null ,
17+ secondaryStoryModeReason : null ,
18+ powerSystemMode : "none" ,
19+ powerSystemReason : "无需等级体系。" ,
20+ caution : null ,
21+ ...overrides ,
22+ } ;
23+ }
24+
25+ function recommendationInput ( overrides = { } ) {
26+ return {
27+ userIntentSummary : "测试开书" ,
28+ powerSystemPreference : "ai_recommend" ,
29+ genreCatalogText : "" ,
30+ storyModeCatalogText : "" ,
31+ allowedGenreIds : [ "genre-a" , "genre-b" ] ,
32+ allowedStoryModeIds : [ "mode-a" , "mode-b" , "mode-c" ] ,
33+ ...overrides ,
34+ } ;
35+ }
36+
937test ( "resource recommendation records user and AI selection sources" , ( ) => {
1038 const shared = read ( "../shared/types/novelResourceRecommendation.ts" ) ;
1139 const service = read ( "src/services/novel/NovelCreateResourceRecommendationService.ts" ) ;
@@ -23,6 +51,114 @@ test("AI secondary mode cannot duplicate the resolved primary mode", () => {
2351 assert . match ( service , / i t e m \. i d ! = = p r i m a r y \? \. i d / ) ;
2452} ) ;
2553
54+ test ( "resource recommendation resolves structured catalog ordinals without accepting arbitrary ids" , ( ) => {
55+ const { novelCreateResourceRecommendationPrompt } = require (
56+ "../dist/prompting/prompts/novel/resourceRecommendation.prompts.js"
57+ ) ;
58+ const validate = novelCreateResourceRecommendationPrompt . postValidate ;
59+ assert . equal ( typeof validate , "function" ) ;
60+
61+ const resolved = validate (
62+ recommendationOutput ( {
63+ genreId : "2" ,
64+ primaryStoryModeId : "3" ,
65+ secondaryStoryModeId : "1" ,
66+ secondaryStoryModeReason : "补充关系推进。" ,
67+ } ) ,
68+ recommendationInput ( ) ,
69+ { } ,
70+ ) ;
71+ assert . equal ( resolved . genreId , "genre-b" ) ;
72+ assert . equal ( resolved . primaryStoryModeId , "mode-c" ) ;
73+ assert . equal ( resolved . secondaryStoryModeId , "mode-a" ) ;
74+
75+ assert . throws (
76+ ( ) => validate ( recommendationOutput ( { genreId : "genre-missing" } ) , recommendationInput ( ) , { } ) ,
77+ / 题 材 推 荐 结 果 包 含 非 法 I D / ,
78+ ) ;
79+ assert . throws (
80+ ( ) => validate ( recommendationOutput ( { genreId : "3" } ) , recommendationInput ( ) , { } ) ,
81+ / 题 材 推 荐 结 果 包 含 非 法 I D / ,
82+ ) ;
83+ for ( const invalidOrdinal of [ "0" , "-1" , "02" , "2.0" ] ) {
84+ assert . throws (
85+ ( ) => validate ( recommendationOutput ( { genreId : invalidOrdinal } ) , recommendationInput ( ) , { } ) ,
86+ / 题 材 推 荐 结 果 包 含 非 法 I D / ,
87+ ) ;
88+ }
89+ } ) ;
90+
91+ test ( "resource recommendation normalizes numeric catalog ordinals before semantic validation" , ( ) => {
92+ const { novelCreateResourceRecommendationSchema } = require (
93+ "../dist/prompting/prompts/novel/resourceRecommendation.promptSchemas.js"
94+ ) ;
95+ const { novelCreateResourceRecommendationPrompt } = require (
96+ "../dist/prompting/prompts/novel/resourceRecommendation.prompts.js"
97+ ) ;
98+ const parsed = novelCreateResourceRecommendationSchema . parse ( recommendationOutput ( {
99+ genreId : 2 ,
100+ primaryStoryModeId : 3 ,
101+ secondaryStoryModeId : 1 ,
102+ secondaryStoryModeReason : "补充关系推进。" ,
103+ } ) ) ;
104+ const resolved = novelCreateResourceRecommendationPrompt . postValidate (
105+ parsed ,
106+ recommendationInput ( ) ,
107+ { } ,
108+ ) ;
109+
110+ assert . equal ( resolved . genreId , "genre-b" ) ;
111+ assert . equal ( resolved . primaryStoryModeId , "mode-c" ) ;
112+ assert . equal ( resolved . secondaryStoryModeId , "mode-a" ) ;
113+ assert . throws (
114+ ( ) => novelCreateResourceRecommendationPrompt . postValidate (
115+ novelCreateResourceRecommendationSchema . parse ( recommendationOutput ( { genreId : 0 } ) ) ,
116+ recommendationInput ( ) ,
117+ { } ,
118+ ) ,
119+ / 题 材 推 荐 结 果 包 含 非 法 I D / ,
120+ ) ;
121+ } ) ;
122+
123+ test ( "resource recommendation prefers an exact numeric id and rejects duplicate normalized modes" , ( ) => {
124+ const { novelCreateResourceRecommendationPrompt } = require (
125+ "../dist/prompting/prompts/novel/resourceRecommendation.prompts.js"
126+ ) ;
127+ const validate = novelCreateResourceRecommendationPrompt . postValidate ;
128+
129+ const exact = validate (
130+ recommendationOutput ( { genreId : "2" } ) ,
131+ recommendationInput ( { allowedGenreIds : [ "2" , "genre-b" ] } ) ,
132+ { } ,
133+ ) ;
134+ assert . equal ( exact . genreId , "2" ) ;
135+
136+ assert . throws (
137+ ( ) => validate (
138+ recommendationOutput ( {
139+ primaryStoryModeId : "1" ,
140+ secondaryStoryModeId : "mode-a" ,
141+ secondaryStoryModeReason : "重复项。" ,
142+ } ) ,
143+ recommendationInput ( ) ,
144+ { } ,
145+ ) ,
146+ / 副 推 进 模 式 不 能 与 主 推 进 模 式 相 同 / ,
147+ ) ;
148+ } ) ;
149+
150+ test ( "resource recommendation prompt distinguishes candidate ordinals from stable ids" , ( ) => {
151+ const service = read ( "src/services/novel/NovelCreateResourceRecommendationService.ts" ) ;
152+ const prompt = read ( "src/prompting/prompts/novel/resourceRecommendation.prompts.ts" ) ;
153+ const loaders = read ( "src/prompting/registry/promptAssetLoaderEntries.ts" ) ;
154+
155+ assert . match ( service , / 候 选 序 号 ( 仅 用 于 定 位 ) / ) ;
156+ assert . match ( service , / I D ( 选 择 后 必 须 原 样 返 回 ) / ) ;
157+ assert . match ( prompt , / v e r s i o n : " v 3 " / ) ;
158+ assert . match ( prompt , / 绝 对 不 能 把 1 、 2 、 3 等 候 选 序 号 写 进 任 何 I D 字 段 / ) ;
159+ assert . match ( loaders , / n o v e l \. c r e a t e \. r e s o u r c e _ r e c o m m e n d a t i o n @ v 3 / ) ;
160+ } ) ;
161+
26162test ( "candidate workflow persists the resolved production foundation for recovery" , ( ) => {
27163 const stage = read ( "src/services/novel/director/phases/novelDirectorCandidateStage.ts" ) ;
28164 const directorTypes = read ( "../shared/types/novelDirector.ts" ) ;
0 commit comments