test/elicitation-schema-parity.test.js
Rama main · réplica de lectura del repositorio original.
var test = require("node:test");
var assert = require("node:assert/strict");
var path = require("node:path");
var pathToFileURL = require("node:url").pathToFileURL;
var server = require("../lib/yoke/elicitation-schema");
var userInput = require("../lib/yoke/user-input");
function loadClient() {
return import(pathToFileURL(path.join(__dirname, "../lib/public/modules/elicitation-schema.js")).href);
}
var COLORS = { type: "array", items: { anyOf: [{ const: "#f00", title: "Red" }, { const: "#0f0", title: "Green" }] }, minItems: 1, maxItems: 2 };
var TAGS = { type: "array", items: { type: "string", enum: ["a", "b", "c"] } };
var STRATEGY = { type: "string", oneOf: [{ const: "fast", title: "Fast" }, { const: "safe", title: "Safe" }] };
var NAME = { type: "string", minLength: 1, maxLength: 8, pattern: "^[a-z_][a-z0-9_]*$" };
var COUNT = { type: "integer", minimum: 0, maximum: 10 };
var RATIO = { type: "number", minimum: 0.5 };
// [description, property, typed value, expected valid]
var CASES = [
["anyOf multi-select accepts listed values", COLORS, ["#f00", "#0f0"], true],
["anyOf multi-select rejects unlisted values", COLORS, ["not-a-choice"], false],
["anyOf multi-select enforces minItems", COLORS, [], false],
["anyOf multi-select rejects duplicates", COLORS, ["#f00", "#f00"], false],
["enum multi-select accepts empty without minItems", TAGS, [], true],
["enum multi-select enforces maxItems when present", Object.assign({ maxItems: 1 }, TAGS), ["a", "b"], false],
["oneOf single-select accepts a const", STRATEGY, "safe", true],
["oneOf single-select rejects a title", STRATEGY, "Safe", false],
["plain enum rejects unlisted value", { type: "string", enum: ["x", "y"] }, "z", false],
["string without minLength accepts empty text", { type: "string" }, "", true],
["minLength rejects empty text", NAME, "", false],
["maxLength counts code points", { type: "string", maxLength: 2 }, "😀😀", true],
["bounded pattern is enforced", NAME, "Bad-Name", false],
["bounded pattern accepts a match", NAME, "ok_name", true],
["unbounded pattern stays an unenforced hint", { type: "string", pattern: "^(a+)+$" }, "aaaa!", true],
["integer rejects fractions", COUNT, 1.5, false],
["integer enforces maximum", COUNT, 11, false],
["integer accepts zero", COUNT, 0, true],
["number enforces minimum", RATIO, 0.25, false],
["boolean accepts explicit false", { type: "boolean" }, false, true],
["boolean rejects strings in typed content", { type: "boolean" }, "false", false],
["unknown types are not validated as a known control", { type: "_custom" }, { any: true }, true],
];
test("server and browser validators agree on the supported elicitation schema subset", async function () {
var client = await loadClient();
CASES.forEach(function (row) {
var serverError = server.valueError(row[2], row[1]);
var clientError = client.valueError(row[2], row[1]);
assert.equal(serverError, clientError, row[0] + ": server and browser messages differ");
assert.equal(serverError === null, row[3], row[0]);
});
});
test("required means presence on both sides, and omitted optional fields stay omitted", async function () {
var client = await loadClient();
var schema = { type: "object", required: ["note", "enabled"], properties: { note: { type: "string" }, enabled: { type: "boolean" }, count: COUNT } };
assert.deepEqual(client.validateForm({ note: "", enabled: false }, schema).errors, {});
assert.deepEqual(server.validateContent({ note: "", enabled: false }, schema), { note: "", enabled: false });
assert.deepEqual(Object.keys(client.validateForm({ enabled: true }, schema).errors), ["note"]);
assert.throws(function () { server.validateContent({ enabled: true }, schema); }, /required for note/);
assert.equal(Object.prototype.hasOwnProperty.call(server.validateContent({ note: "x", enabled: true }, schema), "count"), false);
});
test("bounded pattern classifier is identical on both sides and rejects costly constructs", async function () {
var client = await loadClient();
var patterns = [
["^[a-zA-Z_][a-zA-Z0-9_]*$", true],
["^[^@]+@[^@]+$", true],
["^\\d{3}-\\d{4}$", true],
["^(?:ab|cd)x*$", true],
["^(a+)+$", false],
["^(ab)*$", false],
["(a)\\1", false],
["^(?=a)a$", false],
["^a*b*c*$", false],
["^a{1,}b{2,5}c+$", false],
["[unclosed", false],
["x".repeat(201), false],
];
patterns.forEach(function (row) {
assert.equal(server.patternIsBounded(row[0]), row[1], row[0]);
assert.equal(client.patternIsBounded(row[0]), row[1], row[0]);
});
var long = "a".repeat(server.PATTERN_MAX_INPUT_LENGTH + 1);
assert.equal(server.patternMatches("^b*$", long), null);
assert.equal(client.patternMatches("^b*$", long), null);
});
test("form elicitations are not subject to structured question limits", function () {
var properties = {};
var longName = "property_" + "x".repeat(150);
properties[longName] = { type: "string", description: "d".repeat(2500) };
["b", "c", "d", "e"].forEach(function (key) { properties[key] = { type: "string" }; });
var schema = { type: "object", properties: properties, required: [longName] };
var questions = server.questionsFromSchema({ serverName: "Agent", requestedSchema: schema });
assert.equal(questions.length, 5);
assert.equal(questions[0].id, longName);
assert.ok(questions[0].question.length <= 2000);
assert.throws(function () { userInput.normalizeQuestions({ questions: questions }); }, /1-3 questions/, "genuine question tools keep their limit");
var content = { b: "1", c: "2", d: "3", e: "4" };
content[longName] = "value";
return userInput.dispatchElicitation(function (request, respond) {
assert.equal(request.presentation, "elicitation");
assert.equal(request.questions.length, 5);
respond.submitContent(content);
}, { serverName: "Agent", requestedSchema: schema }, {}).then(function (result) {
assert.deepEqual(userInput.elicitationResponse(result, schema), { action: "accept", content: content });
});
});
test("decline and cancel stay distinct in provider responses", function () {
return userInput.dispatchElicitation(function (request, respond) {
assert.equal(respond.decline("No"), true);
assert.equal(respond.cancel("Late"), false);
}, { requestedSchema: { type: "object", properties: {} } }, {}).then(function (declined) {
assert.deepEqual(userInput.elicitationResponse(declined), { action: "decline" });
assert.deepEqual(userInput.elicitationResponse({ status: "cancelled" }), { action: "cancel" });
});
});