@@ -8,6 +8,10 @@ const i18nPath = fileURLToPath(new URL("../../src/i18n/index.ts", import.meta.ur
88const iconsPath = fileURLToPath ( new URL ( "../../src/components/icons/index.ts" , import . meta. url ) ) ;
99const utilsPath = fileURLToPath ( new URL ( "../../src/lib/shared/utils.ts" , import . meta. url ) ) ;
1010
11+ const { ASK_USER_QUESTION_TIMEOUT_MS } = createTsModuleLoader ( ) . loadModule (
12+ "src/lib/chat/askUserQuestion.ts" ,
13+ ) ;
14+
1115const questions = [
1216 {
1317 id : "choice" ,
@@ -30,6 +34,11 @@ function createHookHarness(initialState = {}) {
3034 [ 4 , initialState . customTexts ?? { } ] ,
3135 [ 5 , initialState . submitting ?? false ] ,
3236 ] ) ;
37+ // useAnswerCountdown 的 remainingMs(挂载后由 interval tick 驱动);
38+ // 测试用它模拟“采信的截止时间随后归零”。
39+ if ( initialState . remainingMs !== undefined ) {
40+ stateOverrides . set ( 8 , initialState . remainingMs ) ;
41+ }
3342
3443 const react = {
3544 useState ( initialValue ) {
@@ -146,9 +155,11 @@ test("expired countdown disables options, custom input, and submit before tool_r
146155 const card = createCardHarness ( {
147156 customSelected : { choice : true } ,
148157 customTexts : { choice : "My answer" } ,
158+ // 采信的截止时间(挂载时仍在窗口内)随 interval tick 归零。
159+ remainingMs : 0 ,
149160 } ) ;
150161 const tree = card . render ( {
151- deadlineAt : Date . now ( ) - 1 ,
162+ deadlineAt : Date . now ( ) + 60_000 ,
152163 onSubmit : async ( ) => {
153164 submitCalls += 1 ;
154165 return { ok : true } ;
@@ -191,6 +202,57 @@ test("expired countdown disables options, custom input, and submit before tool_r
191202 assert . equal ( submitCalls , 0 ) ;
192203} ) ;
193204
205+ // 截止时间由桌面时钟盖章、倒计时读本机时钟:偏移超界时必须回退挂载近似,
206+ // 不能把仍在挂起的提问卡一挂载就锁死(过期提交由桌面挂起表权威拒绝)。
207+ test ( "a deadline already past at mount is distrusted and the pending card stays answerable" , async ( ) => {
208+ const submitted = [ ] ;
209+ const card = createCardHarness ( { draftSelections : { choice : "Second" } } ) ;
210+ const tree = card . render ( {
211+ // 本机时钟快于桌面盖章时钟:卡片挂载时截止时间看似早已过去。
212+ deadlineAt : Date . now ( ) - 5 * 60 * 1000 ,
213+ onSubmit : async ( answers ) => {
214+ submitted . push ( answers ) ;
215+ return { ok : true } ;
216+ } ,
217+ } ) ;
218+
219+ const optionButtons = findAll (
220+ tree ,
221+ ( node ) => node . type === "button" && node . props ?. role === "radio" ,
222+ ) ;
223+ assert . equal ( optionButtons . length , 2 ) ;
224+ assert . equal ( optionButtons . every ( ( button ) => button . props . disabled === false ) , true ) ;
225+ const customOption = findAll (
226+ tree ,
227+ ( node ) => node . type === "div" && node . props ?. role === "radio" ,
228+ ) [ 0 ] ;
229+ assert . equal ( customOption . props [ "aria-disabled" ] , false ) ;
230+
231+ const submitButton = findSubmitButton ( tree ) ;
232+ assert . equal ( submitButton . props . disabled , false ) ;
233+ submitButton . props . onClick ( ) ;
234+ await flushPromises ( ) ;
235+ assert . equal ( submitted . length , 1 ) ;
236+ assert . equal ( submitted [ 0 ] [ 0 ] . selectedLabel , "Second" ) ;
237+ } ) ;
238+
239+ test ( "a deadline beyond the full answer window is distrusted and clamps the countdown" , ( ) => {
240+ const card = createCardHarness ( ) ;
241+ const tree = card . render ( {
242+ // 本机时钟慢于桌面盖章时钟:截止时间看似远超完整应答窗口。
243+ deadlineAt : Date . now ( ) + ASK_USER_QUESTION_TIMEOUT_MS + 5 * 60 * 1000 ,
244+ onSubmit : async ( ) => ( { ok : true } ) ,
245+ } ) ;
246+
247+ const optionButtons = findAll (
248+ tree ,
249+ ( node ) => node . type === "button" && node . props ?. role === "radio" ,
250+ ) ;
251+ assert . equal ( optionButtons . every ( ( button ) => button . props . disabled === false ) , true ) ;
252+ // 倒计时按挂载近似显示完整窗口,而不是把偏移量当剩余时间。
253+ assert . match ( treeText ( tree ) , / (?: 3 : 0 0 | 2 : 5 9 ) c h a t \. a s k U s e r \. t i m e o u t H i n t / ) ;
254+ } ) ;
255+
194256test ( "a complete answer before the deadline submits the selected non-first option" , async ( ) => {
195257 const submitted = [ ] ;
196258 const card = createCardHarness ( { draftSelections : { choice : "Second" } } ) ;
0 commit comments