synthos 0.10.1 → 0.11.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.
Files changed (311) hide show
  1. package/README.md +5 -5
  2. package/default-pages/elevenlabs_effects_studio/chat-history.json +1 -0
  3. package/default-pages/elevenlabs_effects_studio/page.html +1345 -1363
  4. package/default-pages/elevenlabs_effects_studio/page.json +13 -11
  5. package/default-pages/elevenlabs_voice_studio/chat-history.json +1 -0
  6. package/default-pages/elevenlabs_voice_studio/page.html +782 -801
  7. package/default-pages/elevenlabs_voice_studio/page.json +13 -11
  8. package/default-pages/json_tools/chat-history.json +1 -0
  9. package/default-pages/json_tools/page.html +70 -90
  10. package/default-pages/json_tools/page.json +12 -10
  11. package/default-pages/my_notes/chat-history.json +1 -0
  12. package/default-pages/my_notes/page.html +115 -131
  13. package/default-pages/my_notes/page.json +14 -12
  14. package/default-pages/neon_asteroids/chat-history.json +1 -0
  15. package/default-pages/neon_asteroids/page.html +1777 -1803
  16. package/default-pages/neon_asteroids/page.json +14 -12
  17. package/default-pages/oregon_trail/chat-history.json +1 -0
  18. package/default-pages/oregon_trail/page.html +290 -307
  19. package/default-pages/oregon_trail/page.json +14 -12
  20. package/default-pages/solar_explorer/chat-history.json +1 -0
  21. package/default-pages/solar_explorer/page.html +1929 -1951
  22. package/default-pages/solar_explorer/page.json +14 -12
  23. package/default-pages/solar_tutorial/chat-history.json +1 -0
  24. package/default-pages/solar_tutorial/page.html +464 -478
  25. package/default-pages/solar_tutorial/page.json +12 -10
  26. package/default-pages/us_map/chat-history.json +1 -0
  27. package/default-pages/us_map/page.html +170 -193
  28. package/default-pages/us_map/page.json +14 -12
  29. package/default-pages/us_map/page.light.png +0 -0
  30. package/default-pages/us_map_1850/chat-history.json +1 -0
  31. package/default-pages/us_map_1850/page.html +302 -326
  32. package/default-pages/us_map_1850/page.json +14 -12
  33. package/default-pages/western_cities_1850/chat-history.json +1 -0
  34. package/default-pages/western_cities_1850/page.html +503 -527
  35. package/default-pages/western_cities_1850/page.json +14 -12
  36. package/default-themes/aurora-dawn.v3.css +15 -14
  37. package/default-themes/aurora-dusk.v3.css +26 -26
  38. package/default-themes/cosmos-dawn.v3.css +15 -14
  39. package/default-themes/cosmos-dusk.v3.css +26 -26
  40. package/default-themes/elemental-dawn.v3.css +200 -0
  41. package/default-themes/nebula-dawn.v3.css +15 -14
  42. package/default-themes/nebula-dusk.v3.css +24 -24
  43. package/default-themes/solar-flare-dawn.v3.css +15 -14
  44. package/default-themes/solar-flare-dusk.v3.css +26 -26
  45. package/dist/builders/anthropic.d.ts +26 -2
  46. package/dist/builders/anthropic.d.ts.map +1 -1
  47. package/dist/builders/anthropic.js +132 -31
  48. package/dist/builders/anthropic.js.map +1 -1
  49. package/dist/builders/claudecode.d.ts +13 -0
  50. package/dist/builders/claudecode.d.ts.map +1 -0
  51. package/dist/builders/claudecode.js +253 -0
  52. package/dist/builders/claudecode.js.map +1 -0
  53. package/dist/builders/index.d.ts +2 -1
  54. package/dist/builders/index.d.ts.map +1 -1
  55. package/dist/builders/index.js +8 -1
  56. package/dist/builders/index.js.map +1 -1
  57. package/dist/builders/openai.js +2 -1
  58. package/dist/builders/openai.js.map +1 -1
  59. package/dist/builders/types.d.ts +31 -7
  60. package/dist/builders/types.d.ts.map +1 -1
  61. package/dist/builders/types.js +60 -28
  62. package/dist/builders/types.js.map +1 -1
  63. package/dist/connectors/types.d.ts +8 -0
  64. package/dist/connectors/types.d.ts.map +1 -1
  65. package/dist/init.d.ts.map +1 -1
  66. package/dist/init.js +13 -6
  67. package/dist/init.js.map +1 -1
  68. package/dist/migrations.d.ts.map +1 -1
  69. package/dist/migrations.js +161 -14
  70. package/dist/migrations.js.map +1 -1
  71. package/dist/models/anthropic.d.ts +1 -0
  72. package/dist/models/anthropic.d.ts.map +1 -1
  73. package/dist/models/anthropic.js +129 -29
  74. package/dist/models/anthropic.js.map +1 -1
  75. package/dist/models/chainOfThought.d.ts.map +1 -1
  76. package/dist/models/chainOfThought.js +32 -19
  77. package/dist/models/chainOfThought.js.map +1 -1
  78. package/dist/models/index.d.ts +2 -2
  79. package/dist/models/index.d.ts.map +1 -1
  80. package/dist/models/index.js +2 -1
  81. package/dist/models/index.js.map +1 -1
  82. package/dist/models/providers.d.ts +1 -0
  83. package/dist/models/providers.d.ts.map +1 -1
  84. package/dist/models/providers.js +12 -4
  85. package/dist/models/providers.js.map +1 -1
  86. package/dist/models/types.d.ts +15 -1
  87. package/dist/models/types.d.ts.map +1 -1
  88. package/dist/models/types.js.map +1 -1
  89. package/dist/pages.d.ts +57 -8
  90. package/dist/pages.d.ts.map +1 -1
  91. package/dist/pages.js +258 -45
  92. package/dist/pages.js.map +1 -1
  93. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  94. package/dist/service/createCompletePrompt.js +5 -0
  95. package/dist/service/createCompletePrompt.js.map +1 -1
  96. package/dist/service/mediaCache.d.ts +36 -0
  97. package/dist/service/mediaCache.d.ts.map +1 -0
  98. package/dist/service/mediaCache.js +182 -0
  99. package/dist/service/mediaCache.js.map +1 -0
  100. package/dist/service/pageValidator.d.ts +25 -0
  101. package/dist/service/pageValidator.d.ts.map +1 -0
  102. package/dist/service/pageValidator.js +315 -0
  103. package/dist/service/pageValidator.js.map +1 -0
  104. package/dist/service/server.d.ts.map +1 -1
  105. package/dist/service/server.js +4 -0
  106. package/dist/service/server.js.map +1 -1
  107. package/dist/service/sharedTableSchema.d.ts +73 -0
  108. package/dist/service/sharedTableSchema.d.ts.map +1 -0
  109. package/dist/service/sharedTableSchema.js +206 -0
  110. package/dist/service/sharedTableSchema.js.map +1 -0
  111. package/dist/service/transformPage.d.ts +49 -11
  112. package/dist/service/transformPage.d.ts.map +1 -1
  113. package/dist/service/transformPage.js +354 -241
  114. package/dist/service/transformPage.js.map +1 -1
  115. package/dist/service/useApiRoutes.d.ts.map +1 -1
  116. package/dist/service/useApiRoutes.js +288 -34
  117. package/dist/service/useApiRoutes.js.map +1 -1
  118. package/dist/service/useConnectorRoutes.d.ts.map +1 -1
  119. package/dist/service/useConnectorRoutes.js +170 -32
  120. package/dist/service/useConnectorRoutes.js.map +1 -1
  121. package/dist/service/useDataRoutes.d.ts.map +1 -1
  122. package/dist/service/useDataRoutes.js +59 -2
  123. package/dist/service/useDataRoutes.js.map +1 -1
  124. package/dist/service/useExtractRoutes.d.ts +4 -0
  125. package/dist/service/useExtractRoutes.d.ts.map +1 -0
  126. package/dist/service/useExtractRoutes.js +304 -0
  127. package/dist/service/useExtractRoutes.js.map +1 -0
  128. package/dist/service/usePageRoutes.d.ts +17 -0
  129. package/dist/service/usePageRoutes.d.ts.map +1 -1
  130. package/dist/service/usePageRoutes.js +1385 -483
  131. package/dist/service/usePageRoutes.js.map +1 -1
  132. package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
  133. package/dist/service/useSharedDataRoutes.js +54 -2
  134. package/dist/service/useSharedDataRoutes.js.map +1 -1
  135. package/dist/settings.d.ts +27 -0
  136. package/dist/settings.d.ts.map +1 -1
  137. package/dist/settings.js +40 -1
  138. package/dist/settings.js.map +1 -1
  139. package/dist/themes.d.ts +0 -5
  140. package/dist/themes.d.ts.map +1 -1
  141. package/dist/themes.js +3 -95
  142. package/dist/themes.js.map +1 -1
  143. package/migration-rules/v2-to-v3.md +277 -119
  144. package/package.json +5 -1
  145. package/{default-pages/application → required-pages/_shell}/page.html +56 -42
  146. package/required-pages/_shell/page.json +14 -0
  147. package/required-pages/_starters/page.html +534 -0
  148. package/required-pages/_starters/page.json +12 -0
  149. package/required-pages/builder/page.html +353 -43
  150. package/required-pages/builder/page.json +12 -10
  151. package/required-pages/pages/page.html +697 -924
  152. package/required-pages/pages/page.json +12 -10
  153. package/required-pages/settings/page.html +1879 -1753
  154. package/required-pages/settings/page.json +12 -10
  155. package/required-pages/synthos_apis/page.html +834 -845
  156. package/required-pages/synthos_apis/page.json +12 -10
  157. package/required-pages/synthos_scripts/page.html +74 -88
  158. package/required-pages/synthos_scripts/page.json +12 -10
  159. package/scripts/append-instructions.py +90 -0
  160. package/scripts/audit-instructions.py +76 -0
  161. package/scripts/cleanup-shell-markup.mjs +112 -0
  162. package/service-connectors/buffer/connector.json +46 -0
  163. package/service-connectors/canva/connector.json +67 -0
  164. package/service-connectors/elevenlabs/connector.json +1 -1
  165. package/src/builders/anthropic.ts +150 -25
  166. package/src/builders/claudecode.ts +310 -0
  167. package/src/builders/index.ts +7 -1
  168. package/src/builders/openai.ts +2 -1
  169. package/src/builders/types.ts +93 -32
  170. package/src/connectors/types.ts +8 -0
  171. package/src/init.ts +13 -7
  172. package/src/migrations.ts +187 -16
  173. package/src/models/anthropic.ts +140 -30
  174. package/src/models/chainOfThought.ts +33 -18
  175. package/src/models/index.ts +2 -2
  176. package/src/models/providers.ts +10 -1
  177. package/src/models/types.ts +21 -1
  178. package/src/pages.ts +271 -35
  179. package/src/service/createCompletePrompt.ts +6 -0
  180. package/src/service/mediaCache.ts +206 -0
  181. package/src/service/pageValidator.ts +337 -0
  182. package/src/service/server.ts +4 -0
  183. package/src/service/sharedTableSchema.ts +236 -0
  184. package/src/service/transformPage.ts +370 -260
  185. package/src/service/useApiRoutes.ts +282 -32
  186. package/src/service/useConnectorRoutes.ts +189 -34
  187. package/src/service/useDataRoutes.ts +198 -116
  188. package/src/service/useExtractRoutes.ts +331 -0
  189. package/src/service/usePageRoutes.ts +1411 -394
  190. package/src/service/useSharedDataRoutes.ts +184 -109
  191. package/src/settings.ts +65 -0
  192. package/src/themes.ts +78 -180
  193. package/starters/blank_starter/chat-history.json +1 -0
  194. package/starters/blank_starter/page.dark.png +0 -0
  195. package/starters/blank_starter/page.html +47 -0
  196. package/starters/blank_starter/page.json +13 -0
  197. package/starters/blank_starter/page.light.png +0 -0
  198. package/starters/calculator_starter/chat-history.json +1 -0
  199. package/starters/calculator_starter/page.dark.png +0 -0
  200. package/starters/calculator_starter/page.html +232 -0
  201. package/starters/calculator_starter/page.json +13 -0
  202. package/starters/calculator_starter/page.light.png +0 -0
  203. package/starters/calendar_starter/chat-history.json +1 -0
  204. package/starters/calendar_starter/page.dark.png +0 -0
  205. package/starters/calendar_starter/page.html +495 -0
  206. package/starters/calendar_starter/page.json +13 -0
  207. package/starters/calendar_starter/page.light.png +0 -0
  208. package/starters/chat_starter/chat-history.json +1 -0
  209. package/starters/chat_starter/page.dark.png +0 -0
  210. package/starters/chat_starter/page.html +351 -0
  211. package/starters/chat_starter/page.json +13 -0
  212. package/starters/chat_starter/page.light.png +0 -0
  213. package/starters/checklist_starter/chat-history.json +1 -0
  214. package/starters/checklist_starter/page.dark.png +0 -0
  215. package/starters/checklist_starter/page.html +437 -0
  216. package/starters/checklist_starter/page.json +13 -0
  217. package/starters/checklist_starter/page.light.png +0 -0
  218. package/starters/dashboard_starter/chat-history.json +1 -0
  219. package/starters/dashboard_starter/page.dark.png +0 -0
  220. package/starters/dashboard_starter/page.html +195 -0
  221. package/starters/dashboard_starter/page.json +13 -0
  222. package/starters/dashboard_starter/page.light.png +0 -0
  223. package/starters/form_starter/chat-history.json +1 -0
  224. package/starters/form_starter/page.dark.png +0 -0
  225. package/starters/form_starter/page.html +313 -0
  226. package/starters/form_starter/page.json +13 -0
  227. package/starters/form_starter/page.light.png +0 -0
  228. package/starters/gallery_starter/chat-history.json +1 -0
  229. package/starters/gallery_starter/page.dark.png +0 -0
  230. package/starters/gallery_starter/page.html +418 -0
  231. package/starters/gallery_starter/page.json +13 -0
  232. package/starters/gallery_starter/page.light.png +0 -0
  233. package/starters/generator_starter/chat-history.json +1 -0
  234. package/starters/generator_starter/page.dark.png +0 -0
  235. package/starters/generator_starter/page.html +261 -0
  236. package/starters/generator_starter/page.json +13 -0
  237. package/starters/generator_starter/page.light.png +0 -0
  238. package/starters/index.html +538 -0
  239. package/starters/kanban_starter/chat-history.json +1 -0
  240. package/starters/kanban_starter/page.dark.png +0 -0
  241. package/starters/kanban_starter/page.html +432 -0
  242. package/starters/kanban_starter/page.json +13 -0
  243. package/starters/kanban_starter/page.light.png +0 -0
  244. package/starters/presentation_builder/chat-history.json +1 -0
  245. package/starters/presentation_builder/page.dark.png +0 -0
  246. package/starters/presentation_builder/page.html +970 -0
  247. package/starters/presentation_builder/page.json +15 -0
  248. package/starters/presentation_builder/page.light.png +0 -0
  249. package/starters/presentation_builder/presentation_voice/voice_config.json +9 -0
  250. package/starters/pulse_starter/chat-history.json +1 -0
  251. package/starters/pulse_starter/page.dark.png +0 -0
  252. package/starters/pulse_starter/page.html +698 -0
  253. package/starters/pulse_starter/page.json +13 -0
  254. package/starters/pulse_starter/page.light.png +0 -0
  255. package/starters/quiz_starter/chat-history.json +1 -0
  256. package/starters/quiz_starter/page.dark.png +0 -0
  257. package/starters/quiz_starter/page.html +292 -0
  258. package/starters/quiz_starter/page.json +13 -0
  259. package/starters/quiz_starter/page.light.png +0 -0
  260. package/starters/reference_starter/chat-history.json +1 -0
  261. package/starters/reference_starter/page.dark.png +0 -0
  262. package/starters/reference_starter/page.html +250 -0
  263. package/starters/reference_starter/page.json +13 -0
  264. package/starters/reference_starter/page.light.png +0 -0
  265. package/starters/retro_game_starter/chat-history.json +1 -0
  266. package/starters/retro_game_starter/page.dark.png +0 -0
  267. package/{default-pages → starters}/retro_game_starter/page.html +1281 -1308
  268. package/starters/retro_game_starter/page.json +15 -0
  269. package/starters/retro_game_starter/page.light.png +0 -0
  270. package/starters/roster_starter/chat-history.json +1 -0
  271. package/starters/roster_starter/page.dark.png +0 -0
  272. package/starters/roster_starter/page.html +600 -0
  273. package/starters/roster_starter/page.json +13 -0
  274. package/starters/roster_starter/page.light.png +0 -0
  275. package/starters/server.js +182 -0
  276. package/starters/start.cmd +1 -0
  277. package/starters/timeline_starter/chat-history.json +1 -0
  278. package/starters/timeline_starter/page.dark.png +0 -0
  279. package/starters/timeline_starter/page.html +446 -0
  280. package/starters/timeline_starter/page.json +13 -0
  281. package/starters/timeline_starter/page.light.png +0 -0
  282. package/starters/tutorial_starter/chat-history.json +1 -0
  283. package/starters/tutorial_starter/page.dark.png +0 -0
  284. package/starters/tutorial_starter/page.html +283 -0
  285. package/starters/tutorial_starter/page.json +13 -0
  286. package/starters/tutorial_starter/page.light.png +0 -0
  287. package/static-files/agent.v3.js +122 -0
  288. package/static-files/connector.v3.js +48 -0
  289. package/static-files/extract.v3.js +188 -0
  290. package/static-files/helpers.v3.js +50 -6
  291. package/static-files/page-bridge.js +114 -0
  292. package/static-files/page.v3.js +1292 -1290
  293. package/static-files/script.v3.js +32 -0
  294. package/static-files/server.v3.js +89 -0
  295. package/static-files/shell-bridge.v3.js +174 -0
  296. package/static-files/shell-modals.v3.js +521 -0
  297. package/static-files/{shell.css → shell.v3.css} +271 -22
  298. package/static-files/shell.v3.js +1865 -0
  299. package/static-files/storage.v3.js +176 -0
  300. package/tests/anthropic.spec.ts +42 -7
  301. package/tests/builders.spec.ts +70 -2
  302. package/tests/pageValidator.spec.ts +548 -0
  303. package/tests/profiles.spec.ts +122 -0
  304. package/tests/sharedTableSchema.spec.ts +242 -0
  305. package/tests/transformPage.spec.ts +62 -81
  306. package/default-pages/application/page.json +0 -10
  307. package/default-pages/retro_game_starter/page.json +0 -12
  308. package/default-pages/sidebar_page/page.html +0 -51
  309. package/default-pages/sidebar_page/page.json +0 -10
  310. package/default-pages/two-panel_page/page.html +0 -68
  311. package/default-pages/two-panel_page/page.json +0 -10
@@ -0,0 +1,548 @@
1
+ import assert from 'assert';
2
+ import {
3
+ validatePage,
4
+ PageValidationResult,
5
+ PageValidationError,
6
+ } from '../src/service/pageValidator';
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Helpers
10
+ // ---------------------------------------------------------------------------
11
+
12
+ /** Wrap inline script(s) in a minimal HTML page. */
13
+ function page(body: string, scripts: string | string[] = []): string {
14
+ const arr = Array.isArray(scripts) ? scripts : [scripts];
15
+ const scriptTags = arr.map(s => `<script>${s}</script>`).join('\n');
16
+ return `<html><head></head><body>${body}\n${scriptTags}</body></html>`;
17
+ }
18
+
19
+ function pageWithIds(ids: string[], scripts: string | string[] = []): string {
20
+ const divs = ids.map(id => `<div id="${id}"></div>`).join('\n');
21
+ return page(divs, scripts);
22
+ }
23
+
24
+ function errorTypes(result: PageValidationResult): string[] {
25
+ return result.errors.map(e => e.type);
26
+ }
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Layer 1: Syntax Errors (Acorn Parse)
30
+ // ---------------------------------------------------------------------------
31
+
32
+ describe('pageValidator', () => {
33
+
34
+ describe('Layer 1 — Syntax errors', () => {
35
+ it('catches unclosed brace', () => {
36
+ const html = page('', 'const x = {');
37
+ const result = validatePage(html);
38
+ assert.strictEqual(result.valid, false);
39
+ assert.strictEqual(result.errors.length, 1);
40
+ assert.strictEqual(result.errors[0].type, 'syntax-error');
41
+ assert.ok(result.errors[0].line !== undefined, 'should include line number');
42
+ assert.ok(result.errors[0].col !== undefined, 'should include column number');
43
+ });
44
+
45
+ it('catches unexpected token', () => {
46
+ const html = page('', 'function() {}');
47
+ const result = validatePage(html);
48
+ assert.strictEqual(result.valid, false);
49
+ assert.ok(result.errors.some(e => e.type === 'syntax-error'));
50
+ });
51
+
52
+ it('reports source as inline-script-N', () => {
53
+ const html = page('', ['const a = 1;', 'const b = {']);
54
+ const result = validatePage(html);
55
+ const err = result.errors.find(e => e.type === 'syntax-error')!;
56
+ assert.ok(err);
57
+ assert.strictEqual(err.source, 'inline-script-1');
58
+ });
59
+
60
+ it('catches errors in multiple scripts independently', () => {
61
+ const html = page('', ['const a = {', 'const b = {']);
62
+ const result = validatePage(html);
63
+ assert.strictEqual(result.valid, false);
64
+ assert.strictEqual(result.errors.filter(e => e.type === 'syntax-error').length, 2);
65
+ });
66
+
67
+ it('parses module scripts correctly', () => {
68
+ const html = `<html><head></head><body>
69
+ <script type="module">
70
+ import { foo } from './bar.js';
71
+ export const x = 1;
72
+ </script>
73
+ </body></html>`;
74
+ const result = validatePage(html);
75
+ assert.strictEqual(result.valid, true);
76
+ assert.strictEqual(result.errors.length, 0);
77
+ });
78
+
79
+ it('skips empty scripts', () => {
80
+ const html = `<html><head></head><body>
81
+ <script></script>
82
+ <script> </script>
83
+ <script>\n\t\n</script>
84
+ </body></html>`;
85
+ const result = validatePage(html);
86
+ assert.strictEqual(result.valid, true);
87
+ assert.strictEqual(result.errors.length, 0);
88
+ });
89
+
90
+ it('skips external scripts (with src attribute)', () => {
91
+ const html = `<html><head></head><body>
92
+ <script src="https://cdn.example.com/broken.js"></script>
93
+ </body></html>`;
94
+ const result = validatePage(html);
95
+ assert.strictEqual(result.valid, true);
96
+ assert.strictEqual(result.errors.length, 0);
97
+ });
98
+
99
+ it('allows top-level await', () => {
100
+ const html = page('', 'const data = await fetch("/api");');
101
+ const result = validatePage(html);
102
+ assert.strictEqual(result.valid, true);
103
+ });
104
+ });
105
+
106
+ // -----------------------------------------------------------------------
107
+ // Injected Scripts — Should Not Produce False Positives
108
+ // -----------------------------------------------------------------------
109
+
110
+ describe('Injected scripts — no false positives', () => {
111
+ it('skips page-bridge script', () => {
112
+ const html = `<html><head></head><body>
113
+ <script id="page-bridge">
114
+ synthos.generate.video();
115
+ const x = {;
116
+ </script>
117
+ </body></html>`;
118
+ const result = validatePage(html);
119
+ assert.strictEqual(result.valid, true);
120
+ assert.strictEqual(result.errors.length, 0);
121
+ });
122
+
123
+ it('skips page-helpers script', () => {
124
+ const html = `<html><head></head><body>
125
+ <script id="page-helpers">
126
+ synthos.nonexistent.method();
127
+ </script>
128
+ </body></html>`;
129
+ const result = validatePage(html);
130
+ assert.strictEqual(result.valid, true);
131
+ });
132
+
133
+ it('skips page-info script', () => {
134
+ const html = `<html><head></head><body>
135
+ <script id="page-info">
136
+ const info = { broken: };
137
+ </script>
138
+ </body></html>`;
139
+ const result = validatePage(html);
140
+ assert.strictEqual(result.valid, true);
141
+ });
142
+
143
+ it('skips page-script (legacy)', () => {
144
+ const html = `<html><head></head><body>
145
+ <script id="page-script">
146
+ synthos.fake.api();
147
+ </script>
148
+ </body></html>`;
149
+ const result = validatePage(html);
150
+ assert.strictEqual(result.valid, true);
151
+ });
152
+
153
+ it('skips synthos-error-capture (legacy)', () => {
154
+ const html = `<html><head></head><body>
155
+ <script id="synthos-error-capture">
156
+ window.onerror = function(;
157
+ </script>
158
+ </body></html>`;
159
+ const result = validatePage(html);
160
+ assert.strictEqual(result.valid, true);
161
+ });
162
+
163
+ it('still validates scripts without known IDs', () => {
164
+ const html = `<html><head></head><body>
165
+ <script id="page-bridge">synthos.fake.call();</script>
166
+ <script id="my-app">synthos.fake.call();</script>
167
+ </body></html>`;
168
+ const result = validatePage(html);
169
+ assert.strictEqual(result.valid, false);
170
+ assert.strictEqual(result.errors.length, 1);
171
+ assert.strictEqual(result.errors[0].source, 'inline-script-0');
172
+ });
173
+ });
174
+
175
+ // -----------------------------------------------------------------------
176
+ // Layer 2: DOM Element ID Inventory
177
+ // -----------------------------------------------------------------------
178
+
179
+ describe('Layer 2 — Missing element IDs', () => {
180
+ it('catches getElementById referencing missing ID', () => {
181
+ const html = pageWithIds(['header'], "document.getElementById('chartArea');");
182
+ const result = validatePage(html);
183
+ assert.strictEqual(result.valid, false);
184
+ assert.strictEqual(result.errors.length, 1);
185
+ assert.strictEqual(result.errors[0].type, 'missing-element');
186
+ assert.ok(result.errors[0].message.includes('#chartArea'));
187
+ });
188
+
189
+ it('catches querySelector referencing missing ID', () => {
190
+ const html = pageWithIds(['app'], "document.querySelector('#missing');");
191
+ const result = validatePage(html);
192
+ assert.strictEqual(result.valid, false);
193
+ assert.ok(result.errors.some(e => e.type === 'missing-element' && e.message.includes('#missing')));
194
+ });
195
+
196
+ it('catches querySelectorAll referencing missing ID', () => {
197
+ const html = pageWithIds([], "document.querySelectorAll('#gone');");
198
+ const result = validatePage(html);
199
+ assert.strictEqual(result.valid, false);
200
+ assert.ok(result.errors.some(e => e.type === 'missing-element'));
201
+ });
202
+
203
+ it('catches jQuery-style $() referencing missing ID', () => {
204
+ const html = pageWithIds(['content'], "$('#sidebar');");
205
+ const result = validatePage(html);
206
+ assert.strictEqual(result.valid, false);
207
+ assert.ok(result.errors.some(e => e.type === 'missing-element' && e.message.includes('#sidebar')));
208
+ });
209
+
210
+ it('passes when all referenced IDs exist', () => {
211
+ const html = pageWithIds(
212
+ ['chartArea', 'sidebar', 'content'],
213
+ [
214
+ "document.getElementById('chartArea');",
215
+ "document.querySelector('#sidebar');",
216
+ "$('#content');",
217
+ ]
218
+ );
219
+ const result = validatePage(html);
220
+ assert.strictEqual(result.valid, true);
221
+ assert.strictEqual(result.errors.length, 0);
222
+ });
223
+
224
+ it('does NOT flag IDs created by innerHTML string literals', () => {
225
+ // A script renders a form at runtime via innerHTML, then queries
226
+ // its own generated IDs. Static inventory would flag #btnSave
227
+ // and #mapDate as missing — dynamic inventory covers them.
228
+ const html = pageWithIds(['root'],
229
+ "document.getElementById('root').innerHTML = '<button id=\"btnSave\">Save</button><input id=\"mapDate\">';" +
230
+ "document.getElementById('btnSave').addEventListener('click', function(){});" +
231
+ "document.querySelector('#mapDate').value = '';"
232
+ );
233
+ const result = validatePage(html);
234
+ assert.strictEqual(result.valid, true,
235
+ 'innerHTML-created IDs should not trigger missing-element errors');
236
+ assert.strictEqual(result.errors.length, 0);
237
+ });
238
+
239
+ it('does NOT flag IDs created via template literal innerHTML', () => {
240
+ // Realistic render() pattern using backtick template string.
241
+ const html = pageWithIds(['qbBody'],
242
+ 'const rows = `<tr><td><input id="btnSaveAll"></td><td><select id="filterCategory"></select></td></tr>`;' +
243
+ "document.getElementById('qbBody').innerHTML = rows;" +
244
+ "document.getElementById('btnSaveAll').onclick = function(){};" +
245
+ "document.getElementById('filterCategory').onchange = function(){};"
246
+ );
247
+ const result = validatePage(html);
248
+ assert.strictEqual(result.valid, true);
249
+ assert.strictEqual(result.errors.length, 0);
250
+ });
251
+
252
+ it('does NOT flag IDs assigned via .id property', () => {
253
+ const html = pageWithIds(['root'],
254
+ "const el = document.createElement('div');" +
255
+ "el.id = 'dynHeader';" +
256
+ "document.getElementById('root').appendChild(el);" +
257
+ "document.getElementById('dynHeader').textContent = 'x';"
258
+ );
259
+ const result = validatePage(html);
260
+ assert.strictEqual(result.valid, true);
261
+ assert.strictEqual(result.errors.length, 0);
262
+ });
263
+
264
+ it('still flags truly missing IDs when script never creates them', () => {
265
+ // Make sure the widened inventory doesn't swallow every reference.
266
+ const html = pageWithIds(['root'],
267
+ "document.getElementById('root').innerHTML = '<div id=\"realId\"></div>';" +
268
+ "document.getElementById('bogusId').click();"
269
+ );
270
+ const result = validatePage(html);
271
+ assert.strictEqual(result.valid, false);
272
+ const missing = result.errors.filter(e => e.type === 'missing-element');
273
+ assert.strictEqual(missing.length, 1);
274
+ assert.ok(missing[0].message.includes('#bogusId'));
275
+ });
276
+
277
+ it('does NOT run when Layer 1 found syntax errors', () => {
278
+ // The script has a syntax error AND references a missing ID.
279
+ // Layer 2 should be skipped, so only the syntax error appears.
280
+ const html = `<html><head></head><body>
281
+ <script>
282
+ document.getElementById('missing');
283
+ const x = {
284
+ </script>
285
+ </body></html>`;
286
+ const result = validatePage(html);
287
+ assert.strictEqual(result.valid, false);
288
+ assert.ok(result.errors.every(e => e.type !== 'missing-element'),
289
+ 'missing-element errors should not appear when there are syntax errors');
290
+ assert.ok(result.errors.some(e => e.type === 'syntax-error'));
291
+ });
292
+ });
293
+
294
+ // -----------------------------------------------------------------------
295
+ // Layer 3: SynthOS API Pattern Checks
296
+ // -----------------------------------------------------------------------
297
+
298
+ describe('Layer 3 — SynthOS API checks', () => {
299
+ it('catches unknown synthos API call', () => {
300
+ const html = page('', 'await synthos.generate.video();');
301
+ const result = validatePage(html);
302
+ assert.strictEqual(result.valid, false);
303
+ assert.ok(result.errors.some(e =>
304
+ e.type === 'unknown-api' && e.message.includes('synthos.generate.video')
305
+ ));
306
+ });
307
+
308
+ it('catches multiple unknown API calls', () => {
309
+ const html = page('', [
310
+ 'synthos.ai.think();',
311
+ 'synthos.render.pdf();',
312
+ ]);
313
+ const result = validatePage(html);
314
+ const unknownApis = result.errors.filter(e => e.type === 'unknown-api');
315
+ assert.strictEqual(unknownApis.length, 2);
316
+ });
317
+
318
+ it('deduplicates same unknown API call across scripts', () => {
319
+ const html = page('', [
320
+ 'synthos.generate.video();',
321
+ 'synthos.generate.video();',
322
+ ]);
323
+ const result = validatePage(html);
324
+ const unknownApis = result.errors.filter(e => e.type === 'unknown-api');
325
+ assert.strictEqual(unknownApis.length, 1);
326
+ });
327
+
328
+ it('deduplicates same unknown API call within one script', () => {
329
+ const html = page('', `
330
+ synthos.generate.video();
331
+ synthos.generate.video();
332
+ synthos.generate.video();
333
+ `);
334
+ const result = validatePage(html);
335
+ const unknownApis = result.errors.filter(e => e.type === 'unknown-api');
336
+ assert.strictEqual(unknownApis.length, 1);
337
+ });
338
+
339
+ it('runs even when there are syntax errors (Layer 3 always runs)', () => {
340
+ const html = page('', [
341
+ 'const x = {', // syntax error
342
+ 'synthos.generate.video();', // unknown API
343
+ ]);
344
+ const result = validatePage(html);
345
+ assert.ok(result.errors.some(e => e.type === 'syntax-error'), 'should have syntax error');
346
+ assert.ok(result.errors.some(e => e.type === 'unknown-api'), 'should have unknown-api error');
347
+ });
348
+
349
+ describe('known methods pass', () => {
350
+ const knownCalls = [
351
+ 'synthos.data.save',
352
+ 'synthos.data.get',
353
+ 'synthos.data.list',
354
+ 'synthos.data.remove',
355
+ 'synthos.files.upload',
356
+ 'synthos.files.url',
357
+ 'synthos.files.list',
358
+ 'synthos.files.remove',
359
+ 'synthos.generate.image',
360
+ 'synthos.generate.completion',
361
+ 'synthos.script.run',
362
+ 'synthos.page.list',
363
+ 'synthos.page.get',
364
+ 'synthos.page.update',
365
+ 'synthos.page.remove',
366
+ 'synthos.page.ask',
367
+ 'synthos.search.web',
368
+ 'synthos.connector.call',
369
+ 'synthos.connector.list',
370
+ 'synthos.agent.list',
371
+ 'synthos.agent.send',
372
+ 'synthos.agent.sendStream',
373
+ 'synthos.agent.chat.send',
374
+ 'synthos.agent.chat.sendStream',
375
+ 'synthos.agent.chat.history',
376
+ 'synthos.agent.chat.abort',
377
+ 'synthos.agent.chat.clear',
378
+ 'synthos.agent.isEnabled',
379
+ 'synthos.agent.getCapabilities',
380
+ 'synthos.shell.navigate',
381
+ 'synthos.shell.submitChat',
382
+ 'synthos.shell.setDirty',
383
+ 'synthos.shell.showLoading',
384
+ 'synthos.shell.hideLoading',
385
+ 'synthos.shell.focusChat',
386
+ 'synthos.shell.on',
387
+ 'synthos.shared.data.save',
388
+ 'synthos.shared.data.get',
389
+ 'synthos.shared.files.upload',
390
+ 'synthos.shared.files.url',
391
+ ];
392
+
393
+ for (const method of knownCalls) {
394
+ it(`${method} → no error`, () => {
395
+ const html = page('', `await ${method}('test');`);
396
+ const result = validatePage(html);
397
+ const unknownApis = result.errors.filter(e => e.type === 'unknown-api');
398
+ assert.strictEqual(unknownApis.length, 0, `${method} should be recognized`);
399
+ });
400
+ }
401
+ });
402
+ });
403
+
404
+ // -----------------------------------------------------------------------
405
+ // Clean Pages — Should Pass
406
+ // -----------------------------------------------------------------------
407
+
408
+ describe('Clean pages pass validation', () => {
409
+ it('simple page with valid script', () => {
410
+ const html = pageWithIds(['app'], "document.getElementById('app').textContent = 'Hello';");
411
+ const result = validatePage(html);
412
+ assert.strictEqual(result.valid, true);
413
+ assert.strictEqual(result.errors.length, 0);
414
+ });
415
+
416
+ it('page with no scripts', () => {
417
+ const html = '<html><head></head><body><h1>Static page</h1></body></html>';
418
+ const result = validatePage(html);
419
+ assert.strictEqual(result.valid, true);
420
+ assert.strictEqual(result.errors.length, 0);
421
+ });
422
+
423
+ it('page with known synthos calls and matching IDs', () => {
424
+ const html = pageWithIds(
425
+ ['output', 'canvas'],
426
+ [
427
+ "const data = await synthos.data.get('myKey');",
428
+ "document.getElementById('output').textContent = data;",
429
+ "const img = await synthos.generate.image('a cat');",
430
+ ]
431
+ );
432
+ const result = validatePage(html);
433
+ assert.strictEqual(result.valid, true);
434
+ assert.strictEqual(result.errors.length, 0);
435
+ });
436
+
437
+ it('page with only external scripts', () => {
438
+ const html = `<html><head>
439
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
440
+ <script src="https://d3js.org/d3.v7.min.js"></script>
441
+ </head><body></body></html>`;
442
+ const result = validatePage(html);
443
+ assert.strictEqual(result.valid, true);
444
+ });
445
+
446
+ it('page mixing external and valid inline scripts', () => {
447
+ const html = `<html><head>
448
+ <script src="https://cdn.example.com/lib.js"></script>
449
+ </head><body>
450
+ <div id="chart"></div>
451
+ <script>document.getElementById('chart').style.height = '400px';</script>
452
+ </body></html>`;
453
+ const result = validatePage(html);
454
+ assert.strictEqual(result.valid, true);
455
+ });
456
+ });
457
+
458
+ // -----------------------------------------------------------------------
459
+ // Result structure
460
+ // -----------------------------------------------------------------------
461
+
462
+ describe('Result structure', () => {
463
+ it('includes durationMs', () => {
464
+ const html = page('', 'const x = 1;');
465
+ const result = validatePage(html);
466
+ assert.ok(typeof result.durationMs === 'number');
467
+ assert.ok(result.durationMs >= 0);
468
+ });
469
+
470
+ it('valid is true when errors is empty', () => {
471
+ const html = page('', 'const x = 1;');
472
+ const result = validatePage(html);
473
+ assert.strictEqual(result.valid, true);
474
+ assert.strictEqual(result.errors.length, 0);
475
+ });
476
+
477
+ it('valid is false when errors is non-empty', () => {
478
+ const html = page('', 'const x = {');
479
+ const result = validatePage(html);
480
+ assert.strictEqual(result.valid, false);
481
+ assert.ok(result.errors.length > 0);
482
+ });
483
+ });
484
+
485
+ // -----------------------------------------------------------------------
486
+ // Combined scenarios
487
+ // -----------------------------------------------------------------------
488
+
489
+ describe('Combined scenarios', () => {
490
+ it('syntax error + unknown API → both reported (no missing-element)', () => {
491
+ const html = `<html><head></head><body>
492
+ <script>
493
+ document.getElementById('nope');
494
+ const x = {
495
+ </script>
496
+ <script>
497
+ synthos.generate.video();
498
+ </script>
499
+ </body></html>`;
500
+ const result = validatePage(html);
501
+ assert.strictEqual(result.valid, false);
502
+ const types = errorTypes(result);
503
+ assert.ok(types.includes('syntax-error'));
504
+ assert.ok(types.includes('unknown-api'));
505
+ assert.ok(!types.includes('missing-element'), 'Layer 2 should be skipped');
506
+ });
507
+
508
+ it('missing element + unknown API → both reported', () => {
509
+ const html = pageWithIds(['header'], [
510
+ "document.getElementById('footer');",
511
+ "synthos.fake.method();",
512
+ ]);
513
+ const result = validatePage(html);
514
+ assert.strictEqual(result.valid, false);
515
+ const types = errorTypes(result);
516
+ assert.ok(types.includes('missing-element'));
517
+ assert.ok(types.includes('unknown-api'));
518
+ });
519
+
520
+ it('realistic page with bridge, helpers, and user script', () => {
521
+ const html = `<html><head></head><body>
522
+ <div id="viewerPanel">
523
+ <div id="chatArea"></div>
524
+ <div id="output"></div>
525
+ </div>
526
+ <script id="page-bridge">
527
+ // Bridge code — should be skipped
528
+ window.parent.postMessage({ type: 'shell:ready' }, '*');
529
+ synthos.internal.hook();
530
+ </script>
531
+ <script id="page-helpers">
532
+ // Helpers — should be skipped
533
+ window.synthos = new Proxy({}, { get: () => () => {} });
534
+ </script>
535
+ <script>
536
+ // User code — should be validated
537
+ const area = document.getElementById('chatArea');
538
+ const out = document.getElementById('output');
539
+ const data = await synthos.data.get('key');
540
+ out.textContent = JSON.stringify(data);
541
+ </script>
542
+ </body></html>`;
543
+ const result = validatePage(html);
544
+ assert.strictEqual(result.valid, true);
545
+ assert.strictEqual(result.errors.length, 0);
546
+ });
547
+ });
548
+ });
@@ -0,0 +1,122 @@
1
+ import assert from 'assert';
2
+ import { UserProfile, renderUserProfile } from '../src/settings';
3
+
4
+ describe('renderUserProfile', () => {
5
+ it('returns empty string when profile is undefined', () => {
6
+ assert.strictEqual(renderUserProfile(undefined), '');
7
+ });
8
+
9
+ it('returns empty string when profile has no filled fields', () => {
10
+ const profile: UserProfile = { personal: {}, business: {} };
11
+ assert.strictEqual(renderUserProfile(profile), '');
12
+ });
13
+
14
+ it('treats whitespace-only fields as empty', () => {
15
+ const profile: UserProfile = {
16
+ personal: { name: ' ', address: '\n\t' },
17
+ business: { name: '' },
18
+ };
19
+ assert.strictEqual(renderUserProfile(profile), '');
20
+ });
21
+
22
+ it('renders only Personal section when business is empty', () => {
23
+ const profile: UserProfile = {
24
+ personal: { name: 'Jane Doe' },
25
+ };
26
+ const out = renderUserProfile(profile);
27
+ assert.ok(out.startsWith('## Personal Information'));
28
+ assert.ok(out.includes('**Name:**\nJane Doe'));
29
+ assert.ok(!out.includes('## Business Information'));
30
+ });
31
+
32
+ it('renders only Business section when personal is empty', () => {
33
+ const profile: UserProfile = {
34
+ business: { name: 'My Bakery', hours: 'Mon-Fri 9-5' },
35
+ };
36
+ const out = renderUserProfile(profile);
37
+ assert.ok(out.startsWith('## Business Information'));
38
+ assert.ok(out.includes('**Name:**\nMy Bakery'));
39
+ assert.ok(out.includes('**Hours:**\nMon-Fri 9-5'));
40
+ assert.ok(!out.includes('## Personal Information'));
41
+ });
42
+
43
+ it('renders both sections with Personal first', () => {
44
+ const profile: UserProfile = {
45
+ personal: { name: 'Jane', address: '123 Main St' },
46
+ business: { name: 'My Bakery', locations: '123 Main St' },
47
+ };
48
+ const out = renderUserProfile(profile);
49
+ assert.ok(out.indexOf('## Personal Information') < out.indexOf('## Business Information'));
50
+ });
51
+
52
+ it('renders fields in the declared order within a section', () => {
53
+ const profile: UserProfile = {
54
+ business: {
55
+ details: 'Family-owned',
56
+ hours: 'Mon-Fri 9-5',
57
+ name: 'My Bakery',
58
+ locations: '123 Main St',
59
+ },
60
+ };
61
+ const out = renderUserProfile(profile);
62
+ const iName = out.indexOf('**Name:**');
63
+ const iLocations = out.indexOf('**Locations:**');
64
+ const iHours = out.indexOf('**Hours:**');
65
+ const iDetails = out.indexOf('**Details:**');
66
+ assert.ok(iName < iLocations);
67
+ assert.ok(iLocations < iHours);
68
+ assert.ok(iHours < iDetails);
69
+ });
70
+
71
+ it('omits individual empty fields within a section', () => {
72
+ const profile: UserProfile = {
73
+ personal: { name: 'Jane', address: '', details: 'vegetarian' },
74
+ };
75
+ const out = renderUserProfile(profile);
76
+ assert.ok(out.includes('**Name:**\nJane'));
77
+ assert.ok(!out.includes('**Address:**'));
78
+ assert.ok(out.includes('**Details:**\nvegetarian'));
79
+ });
80
+
81
+ it('preserves multi-line field values verbatim (trimmed)', () => {
82
+ const profile: UserProfile = {
83
+ business: { locations: '123 Main St\nSpringfield, IL\nSuite 4' },
84
+ };
85
+ const out = renderUserProfile(profile);
86
+ assert.ok(out.includes('**Locations:**\n123 Main St\nSpringfield, IL\nSuite 4'));
87
+ });
88
+
89
+ it('trims leading/trailing whitespace on values', () => {
90
+ const profile: UserProfile = {
91
+ personal: { name: ' Jane Doe \n' },
92
+ };
93
+ const out = renderUserProfile(profile);
94
+ assert.ok(out.includes('**Name:**\nJane Doe'));
95
+ assert.ok(!out.includes(' Jane Doe'));
96
+ });
97
+
98
+ it('renders a fully populated profile with both sections', () => {
99
+ const profile: UserProfile = {
100
+ personal: {
101
+ name: 'Jane Doe',
102
+ address: '123 Main St, Springfield IL',
103
+ details: 'Vegetarian, prefers dark mode',
104
+ },
105
+ business: {
106
+ name: 'My Bakery',
107
+ locations: '123 Main St, Springfield IL',
108
+ hours: 'Mon-Fri 7am-6pm',
109
+ details: 'Family-owned since 2012',
110
+ },
111
+ };
112
+ const out = renderUserProfile(profile);
113
+ assert.ok(out.includes('## Personal Information'));
114
+ assert.ok(out.includes('## Business Information'));
115
+ assert.ok(out.includes('**Name:**\nJane Doe'));
116
+ assert.ok(out.includes('**Address:**\n123 Main St, Springfield IL'));
117
+ assert.ok(out.includes('**Details:**\nVegetarian, prefers dark mode'));
118
+ assert.ok(out.includes('**Name:**\nMy Bakery'));
119
+ assert.ok(out.includes('**Hours:**\nMon-Fri 7am-6pm'));
120
+ assert.ok(out.includes('**Details:**\nFamily-owned since 2012'));
121
+ });
122
+ });