@@ -4,8 +4,96 @@ import {
44 enforceBrainstormControls ,
55 enforceGenerateControls ,
66 isAskStreamingModeAllowed ,
7+ validateControlInput ,
78} from "./control-enforcement.ts" ;
89
10+ const ENUM_CONTROLS = {
11+ ask : {
12+ mode : [ "general" , "clarify" , "summarize" , "proscons" , "argumentmap" ] ,
13+ } ,
14+ attack : {
15+ aggressiveness : [ "mild" , "moderate" , "ruthless" ] ,
16+ judgeParadigm : [ "logic" , "values" , "policy" ] ,
17+ } ,
18+ brainstorm : {
19+ side : [ "pro" , "con" , "both" ] ,
20+ outputMode : [ "mindmap" , "buckets" , "topfive" ] ,
21+ } ,
22+ factoids : {
23+ direction : [ "for" , "against" ] ,
24+ sourceType : [ "academic" , "government" , "news" , "any" ] ,
25+ } ,
26+ generate : {
27+ side : [ "pro" , "con" ] ,
28+ format : [ "custom" , "bp" , "policy" , "classroom" ] ,
29+ role : [ "intro" , "r1" , "r2" , "conc" , "general" ] ,
30+ length : [ "brief" , "standard" , "comprehensive" ] ,
31+ audience : [ "beginner" , "intermediate" , "advanced" ] ,
32+ tone : [ "formal" , "conversational" , "aggressive" ] ,
33+ } ,
34+ } as const ;
35+
36+ const BOOLEAN_CONTROLS = {
37+ ask : [ "citationMode" , "webSearch" , "prosConsMode" , "argumentMapMode" ] ,
38+ attack : [ "detectFallacies" , "analyzeWeighing" ] ,
39+ } as const ;
40+
41+ Deno . test ( "control validator accepts every enum value and omitted defaults" , ( ) => {
42+ for ( const [ feature , controls ] of Object . entries ( ENUM_CONTROLS ) ) {
43+ if ( validateControlInput ( feature , { } ) !== null ) throw new Error ( `${ feature } rejected omitted controls` ) ;
44+ for ( const [ control , values ] of Object . entries ( controls ) ) {
45+ for ( const value of values ) {
46+ const error = validateControlInput ( feature , { [ control ] : value } ) ;
47+ if ( error !== null ) throw new Error ( `${ feature } .${ control } rejected ${ value } : ${ error } ` ) ;
48+ }
49+ }
50+ }
51+ } ) ;
52+
53+ Deno . test ( "control validator rejects every unsupported enum" , ( ) => {
54+ for ( const [ feature , controls ] of Object . entries ( ENUM_CONTROLS ) ) {
55+ for ( const control of Object . keys ( controls ) ) {
56+ const error = validateControlInput ( feature , { [ control ] : "unsupported" } ) ;
57+ if ( ! error ?. includes ( `${ feature } .${ control } ` ) ) throw new Error ( `${ feature } .${ control } accepted an unsupported value` ) ;
58+ }
59+ }
60+ } ) ;
61+
62+ Deno . test ( "control validator accepts booleans and rejects wrong boolean types" , ( ) => {
63+ for ( const [ feature , controls ] of Object . entries ( BOOLEAN_CONTROLS ) ) {
64+ for ( const control of controls ) {
65+ for ( const value of [ true , false ] ) {
66+ if ( validateControlInput ( feature , { [ control ] : value } ) !== null ) {
67+ throw new Error ( `${ feature } .${ control } rejected ${ value } ` ) ;
68+ }
69+ }
70+ const error = validateControlInput ( feature , { [ control ] : "false" } ) ;
71+ if ( ! error ?. includes ( `${ feature } .${ control } ` ) ) throw new Error ( `${ feature } .${ control } accepted a string boolean` ) ;
72+ }
73+ }
74+ } ) ;
75+
76+ Deno . test ( "factoids typed controls enforce their existing boundaries" , ( ) => {
77+ for ( const sourceCount of [ 3 , 5 , 15 ] ) {
78+ if ( validateControlInput ( "factoids" , { sourceCount } ) !== null ) throw new Error ( `rejected sourceCount ${ sourceCount } ` ) ;
79+ }
80+ if ( validateControlInput ( "factoids" , { timeframe : "2020-2026" } ) !== null ) throw new Error ( "rejected string timeframe" ) ;
81+
82+ for ( const input of [
83+ { sourceCount : "5" } ,
84+ { sourceCount : Number . NaN } ,
85+ { sourceCount : Number . POSITIVE_INFINITY } ,
86+ { sourceCount : 2 } ,
87+ { sourceCount : 16 } ,
88+ { timeframe : 2026 } ,
89+ ] ) {
90+ if ( validateControlInput ( "factoids" , input ) === null ) {
91+ throw new Error ( `accepted invalid factoids control ${ JSON . stringify ( input ) } ` ) ;
92+ }
93+ }
94+ if ( validateControlInput ( "unknown" , { mode : "unsupported" } ) !== null ) throw new Error ( "unknown feature must be ignored" ) ;
95+ } ) ;
96+
997Deno . test ( "ask pros/cons and argument map toggles deterministically shape the output" , ( ) => {
1098 const output : Record < string , unknown > = {
1199 response : "r" ,
@@ -48,8 +136,10 @@ Deno.test("clarify mode guarantees a visible uncertainty note when no follow-up
48136 if ( ( good . uncertainties as string [ ] ) . length !== 0 ) throw new Error ( "clarify with follow-ups must not add noise" ) ;
49137} ) ;
50138
51- Deno . test ( "clarify mode is excluded from streaming while general mode remains eligible" , ( ) => {
52- if ( isAskStreamingModeAllowed ( "clarify" ) ) throw new Error ( "clarify mode must not stream" ) ;
139+ Deno . test ( "only general ask mode is eligible for streaming" , ( ) => {
140+ for ( const mode of [ "clarify" , "summarize" , "proscons" , "argumentmap" , "unsupported" , undefined ] ) {
141+ if ( isAskStreamingModeAllowed ( mode ) ) throw new Error ( `${ mode } mode must not stream` ) ;
142+ }
53143 if ( ! isAskStreamingModeAllowed ( "general" ) ) throw new Error ( "general mode should remain streamable" ) ;
54144} ) ;
55145
0 commit comments