@plumbus/ai-decision-laya 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,170 @@
1
+ """Adversarial HTTP and inference-boundary regressions; fake models only."""
2
+
3
+ import http.client
4
+ import json
5
+ import socket
6
+ import threading
7
+ import unittest
8
+ from types import SimpleNamespace
9
+ from unittest.mock import Mock
10
+
11
+ import test_server as fixtures
12
+ from server import LayaBackend, RequestError, check_token_budget, create_server, validate_body
13
+
14
+
15
+ class HttpAudit(unittest.TestCase):
16
+ def setUp(self):
17
+ self.backend = fixtures.FakeBackend()
18
+ self.server = create_server(("127.0.0.1", 0), self.backend, "test-key")
19
+ self.thread = threading.Thread(target=lambda: self.server.serve_forever(poll_interval=0.01), daemon=True)
20
+ self.thread.start()
21
+
22
+ def tearDown(self):
23
+ self.server.shutdown()
24
+ self.server.server_close()
25
+ self.thread.join()
26
+
27
+ def send(self, data=None, extra=(), content_type="application/json", length=None, incomplete=False):
28
+ data = json.dumps(fixtures.request_body()).encode() if data is None else data
29
+ conn = http.client.HTTPConnection(*self.server.server_address, timeout=2)
30
+ try:
31
+ conn.putrequest("POST", "/v1/systemone")
32
+ conn.putheader("Authorization", "Bearer test-key")
33
+ conn.putheader("Content-Type", content_type)
34
+ conn.putheader("Content-Length", str(len(data) if length is None else length))
35
+ for k, v in extra:
36
+ conn.putheader(k, v)
37
+ conn.endheaders(data)
38
+ if incomplete:
39
+ conn.sock.shutdown(socket.SHUT_WR)
40
+ response = conn.getresponse()
41
+ return response.status, json.loads(response.read())
42
+ finally:
43
+ conn.close()
44
+
45
+ def test_L01_numeric_overflow_cannot_enter_inference(self):
46
+ data = b'{"state":{"value":1e999},"questions":{"p":{"type":"noul","instructions":"?"}}}'
47
+ self.assertEqual(self.send(data)[0], 422)
48
+ self.assertEqual(self.backend.calls, [])
49
+
50
+ def test_L02_duplicate_json_keys_are_rejected(self):
51
+ data = b'{"state":"first","state":"second","questions":{"p":{"type":"noul","instructions":"?"}}}'
52
+ self.assertEqual(self.send(data)[0], 422)
53
+ self.assertEqual(self.backend.calls, [])
54
+
55
+ def test_L03_duplicate_content_lengths_are_rejected(self):
56
+ data = json.dumps(fixtures.request_body()).encode()
57
+ self.assertEqual(self.send(data, extra=[("Content-Length", str(len(data)))])[0], 400)
58
+
59
+ def test_L04_duplicate_auth_headers_are_rejected(self):
60
+ self.assertEqual(self.send(extra=[("Authorization", "Bearer other")])[0], 401)
61
+
62
+ def test_L05_incomplete_body_is_not_run_as_a_valid_shorter_request(self):
63
+ self.assertEqual(self.send(length=999, incomplete=True)[0], 400)
64
+ self.assertEqual(self.backend.calls, [])
65
+
66
+ def test_L06_unsupported_content_encoding_is_explicit(self):
67
+ self.assertEqual(self.send(extra=[("Content-Encoding", "gzip")])[0], 415)
68
+
69
+ def test_L07_wrong_content_type_is_rejected(self):
70
+ self.assertEqual(self.send(content_type="text/plain")[0], 415)
71
+
72
+ def test_L08_utf16_is_not_accepted_as_utf8_json(self):
73
+ data = json.dumps(fixtures.request_body()).encode("utf-16")
74
+ self.assertEqual(self.send(data)[0], 422)
75
+
76
+ def test_L09_inference_value_errors_are_server_errors(self):
77
+ self.backend.failure = ValueError("private checkpoint failure")
78
+ self.assertEqual(self.send(), (500, {"error": "Laya inference failed"}))
79
+
80
+ def test_L10_invalid_model_output_is_server_error(self):
81
+ self.backend.predict = lambda _body: {"answers": {"p": float("nan")}}
82
+ self.assertEqual(self.send()[0], 500)
83
+
84
+ def test_L11_output_size_is_bounded(self):
85
+ self.backend.predict = lambda _body: {"data": "x" * (2 * 1024 * 1024)}
86
+ self.assertEqual(self.send()[0], 500)
87
+
88
+ def test_L12_unpaired_unicode_surrogates_are_rejected_before_inference(self):
89
+ data = b'{"state":"\\ud800","questions":{"p":{"type":"noul","instructions":"?"}}}'
90
+ self.assertEqual(self.send(data)[0], 422)
91
+ self.assertEqual(self.backend.calls, [])
92
+
93
+ def test_L13_non_ascii_service_keys_fail_configuration(self):
94
+ with self.assertRaises(RequestError):
95
+ server = create_server(("127.0.0.1", 0), self.backend, "secret-ש")
96
+ server.server_close()
97
+
98
+ def test_L14_non_ascii_text_roundtrips_with_byte_content_length(self):
99
+ body = fixtures.request_body()
100
+ body["state"] = {"text": "נא להחזיר כסף 😀"}
101
+ self.assertEqual(self.send(json.dumps(body, ensure_ascii=False).encode())[0], 200)
102
+ self.assertEqual(self.backend.calls[0]["state"], body["state"])
103
+
104
+
105
+ class BackendAudit(unittest.TestCase):
106
+ def make_backend(self, model="english"):
107
+ backend = LayaBackend.__new__(LayaBackend)
108
+ backend.names = ("english",)
109
+ backend.lock = threading.Lock()
110
+ class Route(dict):
111
+ @property
112
+ def model(self):
113
+ return self["model"]
114
+ agent = fixtures.BudgetTests().agent()
115
+ agent.predict = Mock(return_value={"answers": {}})
116
+ backend.router = SimpleNamespace(route=Mock(return_value=Route(model=model, repo="fake", reason="test")), load=Mock(return_value=agent))
117
+ backend.render_options = lambda _q: ["false", "true"]
118
+ backend.serialize_state = lambda value: value
119
+ return backend, agent
120
+
121
+ def test_L15_unpreloaded_checkpoint_does_not_trigger_loading(self):
122
+ backend, _ = self.make_backend("multilingual")
123
+ with self.assertRaises(RequestError):
124
+ backend.predict(fixtures.request_body())
125
+ backend.router.load.assert_not_called()
126
+
127
+ def test_L16_failed_inference_releases_device_lock(self):
128
+ backend, agent = self.make_backend()
129
+ agent.predict.side_effect = [RuntimeError("failure"), {"answers": {}}]
130
+ with self.assertRaises(RuntimeError):
131
+ backend.predict(fixtures.request_body())
132
+ self.assertIsNotNone(backend.predict(fixtures.request_body()))
133
+
134
+ def test_L17_concurrent_inference_does_not_load_twice(self):
135
+ backend, agent = self.make_backend()
136
+ entered, finish = threading.Event(), threading.Event()
137
+ def slow(_state, _questions):
138
+ entered.set()
139
+ finish.wait(2)
140
+ return {"answers": {}}
141
+ agent.predict.side_effect = slow
142
+ thread = threading.Thread(target=lambda: backend.predict(fixtures.request_body()))
143
+ thread.start()
144
+ try:
145
+ self.assertTrue(entered.wait(1))
146
+ self.assertIsNone(backend.predict(fixtures.request_body()))
147
+ backend.router.load.assert_called_once()
148
+ finally:
149
+ finish.set()
150
+ thread.join()
151
+
152
+ def test_L18_exact_state_token_budget_then_one_token_over(self):
153
+ agent = fixtures.BudgetTests().agent()
154
+ question = {"q": {"type": "noul", "instructions": "?"}}
155
+ # head 3 + two option markers/labels 4 + special tokens 4 = 11.
156
+ check_token_budget(agent, "word " * 21, question, lambda _q: ["false", "true"], lambda s: s)
157
+ with self.assertRaises(RequestError):
158
+ check_token_budget(agent, "word " * 22, question, lambda _q: ["false", "true"], lambda s: s)
159
+
160
+ def test_L19_blank_model_and_language_fail_request_validation(self):
161
+ for field in ("model", "lang"):
162
+ with self.subTest(field=field), self.assertRaises(RequestError):
163
+ validate_body({**fixtures.request_body(), field: " "})
164
+
165
+ def test_L20_invalid_route_is_a_client_error_without_loading(self):
166
+ backend, _ = self.make_backend()
167
+ backend.router.route.side_effect = ValueError("Unknown checkpoint")
168
+ with self.assertRaises(RequestError):
169
+ backend.predict(fixtures.request_body())
170
+ backend.router.load.assert_not_called()
@@ -0,0 +1,165 @@
1
+ """Second service audit: lifecycle, HTTP metadata and batch boundaries."""
2
+ import http.client
3
+ import json
4
+ import socket
5
+ import struct
6
+ import sys
7
+ import threading
8
+ import unittest
9
+ from types import ModuleType
10
+ from unittest.mock import Mock, patch
11
+
12
+ import test_audit as previous
13
+ import test_server as fixtures
14
+ from server import LayaBackend, RequestError, check_token_budget, create_server
15
+
16
+
17
+ class ServiceRoundTwo(unittest.TestCase):
18
+ def setUp(self):
19
+ self.backend = fixtures.FakeBackend()
20
+ self.server = create_server(("127.0.0.1", 0), self.backend, "test-key")
21
+ self.thread = threading.Thread(target=lambda: self.server.serve_forever(poll_interval=0.01), daemon=True)
22
+ self.thread.start()
23
+
24
+ def tearDown(self):
25
+ self.server.shutdown()
26
+ self.server.server_close()
27
+ self.thread.join()
28
+
29
+ def send(self, extra=(), length="auto", auth="Bearer test-key", address=None):
30
+ data = json.dumps(fixtures.request_body()).encode()
31
+ conn = http.client.HTTPConnection(*(address or self.server.server_address), timeout=2)
32
+ try:
33
+ conn.putrequest("POST", "/v1/systemone")
34
+ conn.putheader("Authorization", auth)
35
+ conn.putheader("Content-Type", "application/json")
36
+ if length is not None:
37
+ conn.putheader("Content-Length", str(len(data)) if length == "auto" else length)
38
+ for key, value in extra:
39
+ conn.putheader(key, value)
40
+ conn.endheaders(data)
41
+ response = conn.getresponse()
42
+ return response.status, json.loads(response.read())
43
+ finally:
44
+ conn.close()
45
+
46
+ def test_L23_missing_length_cannot_start_inference(self):
47
+ self.assertEqual(self.send(length=None)[0], 400)
48
+ self.assertEqual(self.backend.calls, [])
49
+
50
+ def test_L24_signed_length_cannot_start_inference(self):
51
+ self.assertEqual(self.send(length="+100")[0], 400)
52
+ self.assertEqual(self.backend.calls, [])
53
+
54
+ def test_L25_conflicting_content_types_are_rejected(self):
55
+ self.assertEqual(self.send(extra=[("Content-Type", "text/plain")])[0], 415)
56
+
57
+ def test_L26_conflicting_content_encodings_are_rejected(self):
58
+ self.assertEqual(self.send(extra=[("Content-Encoding", "identity"), ("Content-Encoding", "gzip")])[0], 415)
59
+
60
+ def test_L27_authentication_scheme_is_case_insensitive(self):
61
+ self.assertEqual(self.send(auth="bEaReR test-key")[0], 200)
62
+
63
+ def test_L28_client_disconnect_during_inference_does_not_escape_handler(self):
64
+ entered, finish = threading.Event(), threading.Event()
65
+ original = self.backend.predict
66
+ def wait(body):
67
+ entered.set()
68
+ finish.wait(2)
69
+ return original(body)
70
+ self.backend.predict = wait
71
+ self.server.handle_error = Mock()
72
+ connection = http.client.HTTPConnection(*self.server.server_address, timeout=2)
73
+ body = json.dumps(fixtures.request_body())
74
+ try:
75
+ connection.request("POST", "/v1/systemone", body=body, headers={"Authorization": "Bearer test-key", "Content-Type": "application/json"})
76
+ self.assertTrue(entered.wait(1))
77
+ connection.sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0))
78
+ connection.close()
79
+ finally:
80
+ connection.close()
81
+ finish.set()
82
+ self.server.shutdown()
83
+ self.server.server_close() # Join handlers before inspecting their error outcome.
84
+ self.server.handle_error.assert_not_called()
85
+
86
+ def test_L29_connections_are_bounded_and_capacity_is_reusable(self):
87
+ backend = fixtures.FakeBackend()
88
+ entered, finish = threading.Event(), threading.Event()
89
+ original = backend.predict
90
+ def wait(body):
91
+ entered.set()
92
+ finish.wait(2)
93
+ return original(body)
94
+ backend.predict = wait
95
+ server = create_server(("127.0.0.1", 0), backend, "test-key", max_connections=1)
96
+ thread = threading.Thread(target=lambda: server.serve_forever(poll_interval=0.01), daemon=True)
97
+ results = []
98
+ first = threading.Thread(target=lambda: results.append(self.send(address=server.server_address)[0]))
99
+ thread.start()
100
+ first.start()
101
+ try:
102
+ self.assertTrue(entered.wait(1))
103
+ self.assertEqual(self.send(address=server.server_address)[0], 503)
104
+ finish.set()
105
+ first.join(2)
106
+ self.assertEqual(results, [200])
107
+ self.assertEqual(self.send(address=server.server_address)[0], 200)
108
+ finally:
109
+ finish.set()
110
+ first.join(2)
111
+ server.shutdown()
112
+ server.server_close()
113
+ thread.join()
114
+
115
+ def test_L30_invalid_unicode_output_is_a_safe_server_error(self):
116
+ self.backend.predict = lambda _body: {"result": "\ud800"}
117
+ self.assertEqual(self.send(), (500, {"error": "Laya inference failed"}))
118
+
119
+ def fake_modules(self):
120
+ laya, common = ModuleType("laya"), ModuleType("laya.common")
121
+ laya.Router = Mock()
122
+ common.render_options = Mock()
123
+ common.serialize_state = Mock()
124
+ return laya, {"laya": laya, "laya.common": common}
125
+
126
+ def test_L31_invalid_preload_list_does_not_construct_router(self):
127
+ module, modules = self.fake_modules()
128
+ with patch.dict(sys.modules, modules), patch.dict("os.environ", {"LAYA_MODELS": "invalid"}):
129
+ with self.assertRaises(RequestError):
130
+ LayaBackend()
131
+ module.Router.assert_not_called()
132
+
133
+ def test_L32_duplicate_preload_names_do_not_increase_model_count(self):
134
+ module, modules = self.fake_modules()
135
+ with patch.dict(sys.modules, modules), patch.dict("os.environ", {"LAYA_MODELS": "english,english,multilingual", "LAYA_DEVICE": "cpu"}):
136
+ LayaBackend()
137
+ module.Router.assert_called_once_with(device="cpu", max_loaded=2)
138
+ module.Router.return_value.preload.assert_called_once_with(["english", "multilingual"])
139
+
140
+ def test_L33_exact_option_budget_keeps_all_48_description_tokens(self):
141
+ agent = fixtures.BudgetTests().agent()
142
+ agent.cfg = {"max_len": 256, "head_max_len": 192}
143
+ check_token_budget(agent, "short", {"q": {"type": "choice", "instructions": "?"}}, lambda _q: ["word " * 48, "other"], lambda s: s)
144
+ with self.assertRaises(RequestError):
145
+ check_token_budget(agent, "short", {"q": {"type": "choice", "instructions": "?"}}, lambda _q: ["word " * 49, "other"], lambda s: s)
146
+
147
+ def test_L34_structured_state_reaches_model_without_replacement_by_serialized_text(self):
148
+ backend, agent = previous.BackendAudit().make_backend()
149
+ backend.serialize_state = lambda value: json.dumps(value, ensure_ascii=False)
150
+ body = {**fixtures.request_body(), "state": {"text": "שלום"}}
151
+ backend.predict(body)
152
+ agent.predict.assert_called_once_with(body["state"], body["questions"])
153
+
154
+ def test_L35_one_oversized_question_rejects_the_whole_batch_before_inference(self):
155
+ backend, agent = previous.BackendAudit().make_backend()
156
+ body = fixtures.request_body()
157
+ body["questions"]["too_long"] = {"type": "noul", "instructions": "word " * 100}
158
+ with self.assertRaises(RequestError):
159
+ backend.predict(body)
160
+ agent.predict.assert_not_called()
161
+
162
+ def test_L36_callers_cannot_override_the_server_device(self):
163
+ from server import validate_body
164
+ with self.assertRaises(RequestError):
165
+ validate_body({**fixtures.request_body(), "device": "cuda:9"})
@@ -0,0 +1,145 @@
1
+ """Offline service tests. No Laya/torch import, model downloads, or GPU required."""
2
+
3
+ import http.client
4
+ import json
5
+ import threading
6
+ import unittest
7
+ import urllib.error
8
+ import urllib.request
9
+ from types import SimpleNamespace
10
+
11
+ from server import LayaBackend, RequestError, check_token_budget, create_server, validate_body
12
+
13
+
14
+ class FakeBackend:
15
+ names = ("english",)
16
+
17
+ def __init__(self):
18
+ self.calls = []
19
+ self.busy = False
20
+ self.failure = None
21
+
22
+ def predict(self, body):
23
+ self.calls.append(body)
24
+ if self.failure:
25
+ raise self.failure
26
+ if self.busy:
27
+ return None
28
+ return {"model": "fake-checkpoint", "usage": {"input_tokens": 10, "output_tokens": 0}, "answers": {"refund": {"type": "noul", "noul": 0.9}}}
29
+
30
+
31
+ def request_body():
32
+ return {"state": "Refund please", "questions": {"refund": {"type": "noul", "instructions": "Refund requested?"}}}
33
+
34
+
35
+ class ServiceTests(unittest.TestCase):
36
+ def setUp(self):
37
+ self.backend = FakeBackend()
38
+ self.server = create_server(("127.0.0.1", 0), self.backend, "test-key")
39
+ self.thread = threading.Thread(target=lambda: self.server.serve_forever(poll_interval=0.01), daemon=True)
40
+ self.thread.start()
41
+
42
+ def tearDown(self):
43
+ self.server.shutdown()
44
+ self.server.server_close()
45
+ self.thread.join()
46
+
47
+ def call(self, body=None, key="test-key", path="/v1/systemone", method="POST"):
48
+ data = json.dumps(body if body is not None else request_body()).encode() if method == "POST" else None
49
+ req = urllib.request.Request(f"http://127.0.0.1:{self.server.server_address[1]}{path}", data=data, method=method, headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"})
50
+ try:
51
+ response = urllib.request.urlopen(req, timeout=2)
52
+ except urllib.error.HTTPError as error:
53
+ response = error
54
+ with response:
55
+ return response.status, json.loads(response.read())
56
+
57
+ def test_predict(self):
58
+ status, result = self.call()
59
+ self.assertEqual(status, 200)
60
+ self.assertEqual(result["answers"]["refund"]["noul"], 0.9)
61
+ self.assertEqual(len(self.backend.calls), 1)
62
+
63
+ def test_auth_before_inference(self):
64
+ self.assertEqual(self.call(key="wrong")[0], 401)
65
+ self.assertEqual(self.backend.calls, [])
66
+
67
+ def test_health_and_not_found(self):
68
+ self.assertEqual(self.call(path="/healthz", method="GET"), (200, {"ready": True, "models": ["english"]}))
69
+ self.assertEqual(self.call(path="/bad")[0], 404)
70
+
71
+ def test_invalid_request_and_oversize(self):
72
+ self.assertEqual(self.call(body={"state": None, "questions": {}})[0], 422)
73
+ # Send only the oversized Content-Length. Rejection must precede reading
74
+ # the body; sending megabytes races the server closing the connection.
75
+ connection = http.client.HTTPConnection(*self.server.server_address, timeout=2)
76
+ try:
77
+ connection.putrequest("POST", "/v1/systemone")
78
+ connection.putheader("Authorization", "Bearer test-key")
79
+ connection.putheader("Content-Length", str(2 * 1024 * 1024 + 1))
80
+ connection.endheaders()
81
+ response = connection.getresponse()
82
+ self.assertEqual(response.status, 413)
83
+ response.read()
84
+ finally:
85
+ connection.close()
86
+ self.assertEqual(self.backend.calls, [])
87
+
88
+ def test_busy_and_safe_failure(self):
89
+ self.backend.busy = True
90
+ self.assertEqual(self.call()[0], 503)
91
+ self.backend.failure = RuntimeError("private-user-input")
92
+ self.assertEqual(self.call(), (500, {"error": "Laya inference failed"}))
93
+
94
+ def test_truncation_failure(self):
95
+ self.backend.failure = RequestError("Too long")
96
+ self.assertEqual(self.call()[0], 422)
97
+
98
+ def test_no_unauthenticated_configuration(self):
99
+ with self.assertRaises(RequestError):
100
+ create_server(("127.0.0.1", 0), self.backend, "")
101
+
102
+
103
+ class BudgetTests(unittest.TestCase):
104
+ def agent(self):
105
+ class Tokenizer:
106
+ mask_token = "[MASK]"
107
+
108
+ def __call__(self, text, **_kwargs):
109
+ return {"input_ids": text.split()}
110
+
111
+ return SimpleNamespace(tok=Tokenizer(), cfg={"max_len": 32, "head_max_len": 24}, _to_internal=lambda q: {"t": q["type"], "ins": q["instructions"]})
112
+
113
+ def check(self, state, instructions="Refund?", options=None):
114
+ check_token_budget(self.agent(), state, {"q": {"type": "noul", "instructions": instructions}}, lambda _q: options or ["false", "true"], lambda value: value)
115
+
116
+ def test_short_request_and_truncation_boundaries(self):
117
+ self.check("Refund please")
118
+ for state, instructions, options in [
119
+ ("word " * 40, "Refund?", None),
120
+ ("short", "instruction " * 40, None),
121
+ ("short", "Refund?", ["option " * 49, "other"]),
122
+ ]:
123
+ with self.assertRaises(RequestError):
124
+ self.check(state, instructions, options)
125
+
126
+ def test_busy_inference_returns_without_loading_a_checkpoint(self):
127
+ backend = LayaBackend.__new__(LayaBackend)
128
+ backend.lock = threading.Lock()
129
+ backend.lock.acquire()
130
+ self.assertIsNone(backend.predict(request_body()))
131
+ backend.lock.release()
132
+
133
+ def test_wire_question_validation(self):
134
+ for question in [
135
+ {"type": "choice", "instructions": "?", "criteria": {"only": None}},
136
+ {"type": "score", "instructions": "?", "criteria": ["one"]},
137
+ {"type": "noul", "instructions": "?", "criteria": {"invalid": "yes"}},
138
+ {"type": "unknown", "instructions": "?"},
139
+ ]:
140
+ with self.assertRaises(RequestError):
141
+ validate_body({"state": "text", "questions": {"q": question}})
142
+
143
+
144
+ if __name__ == "__main__":
145
+ unittest.main()