Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

// META: title=DecisionModel
// META: script=/resources/testdriver.js
// META: script=/resources/testdriver-vendor.js
// META: script=../resources/util.js
'use strict';
const kSampleSchema = {
context: 'Enterprise customer support ticket router.',
expectedInputs: [{type: 'text', languages: ['en']}],
questions: [
{
id: 'is_urgent',
type: 'boolean',
prompt: 'Does this ticket require immediate incident response?',
},
{
id: 'category',
type: 'choice',
prompt: 'Select the primary support department.',
options: [
{label: 'bug', description: 'Production crash or software defect'},
{label: 'billing', description: 'Invoice or double-charge issue'},
{label: 'feature_request', description: 'Enhancement request'},
],
},
{
id: 'severity',
type: 'score',
prompt: 'Rate the business impact from 1 (minimal) to 5 (critical).',
},
],
};
promise_test(async t => {
const availability = await DecisionModel.availability(kSampleSchema);
assert_in_array(availability,
['available', 'downloadable', 'downloading', 'unavailable']);
}, 'DecisionModel.availability() returns a valid Availability enum value');
promise_test(async t => {
const decisionModel = await createDecisionModel(kSampleSchema);
t.add_cleanup(() => decisionModel.destroy());
assert_equals(decisionModel.preference, 'auto');
const result = await decisionModel.decide(
'Production database connections are failing with timeout errors.');
assert_equals(typeof result, 'object');
const {is_urgent, category, severity} = result;
assert_equals(is_urgent.id, 'is_urgent');
assert_in_array(is_urgent.label, ['true', 'false']);
assert_equals(typeof is_urgent.confidence, 'number');
assert_equals(typeof is_urgent.probabilities, 'object');
assert_false(Array.isArray(is_urgent.probabilities));
assert_array_equals(Object.keys(is_urgent.probabilities), ['true', 'false']);
for (const p of Object.values(is_urgent.probabilities)) {
assert_equals(typeof p, 'number');
}
assert_approx_equals(
Object.values(is_urgent.probabilities).reduce((sum, p) => sum + p, 0),
1.0, 1e-4);
assert_equals(category.id, 'category');
assert_in_array(category.label, ['bug', 'billing', 'feature_request']);
assert_equals(typeof category.confidence, 'number');
assert_equals(typeof category.probabilities, 'object');
assert_false(Array.isArray(category.probabilities));
assert_array_equals(Object.keys(category.probabilities),
['bug', 'billing', 'feature_request']);
for (const p of Object.values(category.probabilities)) {
assert_equals(typeof p, 'number');
}
assert_equals(severity.id, 'severity');
assert_in_array(severity.label, ['1', '2', '3', '4', '5']);
assert_equals(typeof severity.expectedScore, 'number');
assert_equals(typeof severity.confidence, 'number');
assert_equals(typeof severity.probabilities, 'object');
assert_false(Array.isArray(severity.probabilities));
assert_array_equals(Object.keys(severity.probabilities),
['1', '2', '3', '4', '5']);
for (const p of Object.values(severity.probabilities)) {
assert_equals(typeof p, 'number');
}
}, 'DecisionModel.create() and decisionModel.decide() return a record keyed by question id with probabilities keyed by option label');
promise_test(async t => {
const customSchema = {
questions: [
{
id: 'custom_route',
type: 'choice',
prompt: 'Route request to team.',
options: [{label: 'infra'}, {label: 'security'}],
},
{
id: 'custom_rating',
type: 'score',
prompt: 'Rate priority on a 10/20/30 scale.',
options: [{label: '10'}, {label: '20'}, {label: '30'}],
},
],
};
const model = await createDecisionModel(customSchema);
t.add_cleanup(() => model.destroy());
const result = await model.decide('Investigate anomalous outbound traffic.');
assert_array_equals(Object.keys(result.custom_route.probabilities),
['infra', 'security']);
assert_array_equals(Object.keys(result.custom_rating.probabilities),
['10', '20', '30']);
assert_greater_than_equal(result.custom_rating.expectedScore, 10);
assert_less_than_equal(result.custom_rating.expectedScore, 30);
}, 'DecisionModel dynamically reflects custom choice and score question schemas');
promise_test(async t => {
const decisionModel = await createDecisionModel({
questions: [
...kSampleSchema.questions,
{
id: 'sentiment',
type: 'choice',
prompt: 'Determine the overall customer sentiment.',
options: [
{label: 'positive'},
{label: 'negative'},
],
},
],
});
t.add_cleanup(() => decisionModel.destroy());
const result = await decisionModel.decide(
'Production database connections are failing with timeout errors.');
assert_equals(Object.keys(result).length, 4);
assert_equals(result.sentiment.id, 'sentiment');
assert_in_array(result.sentiment.label, ['positive', 'negative']);
assert_equals(Object.keys(result.sentiment.probabilities).length, 2);
}, 'decisionModel.decide() returns one decision per create-time question');
promise_test(async t => {
await test_driver.bless();
const invalidSchemas = [
{questions: []},
{questions: [{id: 'q1', type: 'rhetorical', prompt: 'Invalid type'}]},
{questions: [{id: ' ', type: 'boolean', prompt: 'Blank id'}]},
{questions: [{id: 'q1', type: 'boolean', prompt: ' '}]},
{
questions: [
{id: 'dup', type: 'boolean', prompt: 'First question'},
{id: 'dup', type: 'boolean', prompt: 'Duplicate id'},
],
},
{
questions: [{
id: 'bool_with_options',
type: 'boolean',
prompt: 'Boolean questions must not specify options',
options: [{label: 'yes'}, {label: 'no'}],
}],
},
{
questions: [{
id: 'choice_without_options',
type: 'choice',
prompt: 'Choice without options',
}],
},
{
questions: [{
id: 'choice_with_single_option',
type: 'choice',
prompt: 'Choice with one option',
options: [{label: 'only_one'}],
}],
},
{
questions: [{
id: 'choice_with_duplicate_options',
type: 'choice',
prompt: 'Choice with duplicate options',
options: [{label: 'dup'}, {label: 'dup'}],
}],
},
{
questions: [{
id: 'score_with_single_option',
type: 'score',
prompt: 'Score with one option',
options: [{label: '1'}],
}],
},
];
for (const schema of invalidSchemas) {
await promise_rejects_js(t, TypeError, DecisionModel.availability(schema));
await promise_rejects_js(t, TypeError, DecisionModel.create(schema));
}
await promise_rejects_js(t, TypeError, DecisionModel.create({}));
const invalidLanguageSchema = {
expectedInputs: [{type: 'text', languages: ['invalid_bcp47_tag!']}],
questions: [{id: 'q1', type: 'boolean', prompt: 'Valid type'}],
};
await promise_rejects_js(t, RangeError,
DecisionModel.availability(invalidLanguageSchema));
await promise_rejects_js(t, RangeError,
DecisionModel.create(invalidLanguageSchema));
}, 'DecisionModel rejects malformed question schemas with TypeError and invalid BCP-47 language tags with RangeError');
promise_test(async t => {
const decisionModel = await createDecisionModel(kSampleSchema);
t.add_cleanup(() => decisionModel.destroy());
await promise_rejects_js(t, TypeError, decisionModel.decide(''));
await promise_rejects_js(t, TypeError, decisionModel.decide(' '));
}, 'decisionModel.decide() rejects empty or whitespace-only input with TypeError');
promise_test(async t => {
const decisionModel = await createDecisionModel(kSampleSchema);
t.add_cleanup(() => decisionModel.destroy());
const controller = new AbortController();
controller.abort();
await promise_rejects_dom(
t, 'AbortError',
decisionModel.decide('Aborted input', {signal: controller.signal}));
}, 'decisionModel.decide() rejects with AbortError when passed an aborted AbortSignal');
promise_test(async t => {
const decisionModel = await createDecisionModel(kSampleSchema);
decisionModel.destroy();
await promise_rejects_dom(
t, 'InvalidStateError',
decisionModel.decide('Input after session destruction'));
}, 'decisionModel.decide() rejects with InvalidStateError after destroy()');