synthos 0.10.1 → 0.11.1
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.
- package/README.md +5 -5
- package/default-pages/elevenlabs_effects_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_effects_studio/page.html +1345 -1363
- package/default-pages/elevenlabs_effects_studio/page.json +13 -11
- package/default-pages/elevenlabs_voice_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_voice_studio/page.html +782 -801
- package/default-pages/elevenlabs_voice_studio/page.json +13 -11
- package/default-pages/json_tools/chat-history.json +1 -0
- package/default-pages/json_tools/page.html +70 -90
- package/default-pages/json_tools/page.json +12 -10
- package/default-pages/my_notes/chat-history.json +1 -0
- package/default-pages/my_notes/page.html +115 -131
- package/default-pages/my_notes/page.json +14 -12
- package/default-pages/neon_asteroids/chat-history.json +1 -0
- package/default-pages/neon_asteroids/page.html +1777 -1803
- package/default-pages/neon_asteroids/page.json +14 -12
- package/default-pages/oregon_trail/chat-history.json +1 -0
- package/default-pages/oregon_trail/page.html +290 -307
- package/default-pages/oregon_trail/page.json +14 -12
- package/default-pages/solar_explorer/chat-history.json +1 -0
- package/default-pages/solar_explorer/page.html +1929 -1951
- package/default-pages/solar_explorer/page.json +14 -12
- package/default-pages/solar_tutorial/chat-history.json +1 -0
- package/default-pages/solar_tutorial/page.html +464 -478
- package/default-pages/solar_tutorial/page.json +12 -10
- package/default-pages/us_map/chat-history.json +1 -0
- package/default-pages/us_map/page.html +170 -193
- package/default-pages/us_map/page.json +14 -12
- package/default-pages/us_map/page.light.png +0 -0
- package/default-pages/us_map_1850/chat-history.json +1 -0
- package/default-pages/us_map_1850/page.html +302 -326
- package/default-pages/us_map_1850/page.json +14 -12
- package/default-pages/western_cities_1850/chat-history.json +1 -0
- package/default-pages/western_cities_1850/page.html +503 -527
- package/default-pages/western_cities_1850/page.json +14 -12
- package/default-themes/aurora-dawn.v3.css +15 -14
- package/default-themes/aurora-dusk.v3.css +26 -26
- package/default-themes/cosmos-dawn.v3.css +15 -14
- package/default-themes/cosmos-dusk.v3.css +26 -26
- package/default-themes/elemental-dawn.v3.css +200 -0
- package/default-themes/nebula-dawn.v3.css +15 -14
- package/default-themes/nebula-dusk.v3.css +24 -24
- package/default-themes/solar-flare-dawn.v3.css +15 -14
- package/default-themes/solar-flare-dusk.v3.css +26 -26
- package/dist/builders/anthropic.d.ts +26 -2
- package/dist/builders/anthropic.d.ts.map +1 -1
- package/dist/builders/anthropic.js +132 -31
- package/dist/builders/anthropic.js.map +1 -1
- package/dist/builders/claudecode.d.ts +13 -0
- package/dist/builders/claudecode.d.ts.map +1 -0
- package/dist/builders/claudecode.js +253 -0
- package/dist/builders/claudecode.js.map +1 -0
- package/dist/builders/index.d.ts +2 -1
- package/dist/builders/index.d.ts.map +1 -1
- package/dist/builders/index.js +8 -1
- package/dist/builders/index.js.map +1 -1
- package/dist/builders/openai.js +2 -1
- package/dist/builders/openai.js.map +1 -1
- package/dist/builders/types.d.ts +31 -7
- package/dist/builders/types.d.ts.map +1 -1
- package/dist/builders/types.js +60 -28
- package/dist/builders/types.js.map +1 -1
- package/dist/connectors/types.d.ts +8 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +13 -6
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +161 -14
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +1 -0
- package/dist/models/anthropic.d.ts.map +1 -1
- package/dist/models/anthropic.js +129 -29
- package/dist/models/anthropic.js.map +1 -1
- package/dist/models/chainOfThought.d.ts.map +1 -1
- package/dist/models/chainOfThought.js +32 -19
- package/dist/models/chainOfThought.js.map +1 -1
- package/dist/models/index.d.ts +2 -2
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +2 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/providers.d.ts +1 -0
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/models/providers.js +12 -4
- package/dist/models/providers.js.map +1 -1
- package/dist/models/types.d.ts +15 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js.map +1 -1
- package/dist/pages.d.ts +57 -8
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +258 -45
- package/dist/pages.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +5 -0
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/mediaCache.d.ts +36 -0
- package/dist/service/mediaCache.d.ts.map +1 -0
- package/dist/service/mediaCache.js +182 -0
- package/dist/service/mediaCache.js.map +1 -0
- package/dist/service/pageValidator.d.ts +25 -0
- package/dist/service/pageValidator.d.ts.map +1 -0
- package/dist/service/pageValidator.js +315 -0
- package/dist/service/pageValidator.js.map +1 -0
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +4 -0
- package/dist/service/server.js.map +1 -1
- package/dist/service/sharedTableSchema.d.ts +73 -0
- package/dist/service/sharedTableSchema.d.ts.map +1 -0
- package/dist/service/sharedTableSchema.js +206 -0
- package/dist/service/sharedTableSchema.js.map +1 -0
- package/dist/service/transformPage.d.ts +49 -11
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +354 -241
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +285 -34
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts.map +1 -1
- package/dist/service/useConnectorRoutes.js +170 -32
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useDataRoutes.d.ts.map +1 -1
- package/dist/service/useDataRoutes.js +59 -2
- package/dist/service/useDataRoutes.js.map +1 -1
- package/dist/service/useExtractRoutes.d.ts +4 -0
- package/dist/service/useExtractRoutes.d.ts.map +1 -0
- package/dist/service/useExtractRoutes.js +304 -0
- package/dist/service/useExtractRoutes.js.map +1 -0
- package/dist/service/usePageRoutes.d.ts +17 -0
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +1388 -483
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
- package/dist/service/useSharedDataRoutes.js +54 -2
- package/dist/service/useSharedDataRoutes.js.map +1 -1
- package/dist/settings.d.ts +27 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +40 -1
- package/dist/settings.js.map +1 -1
- package/dist/themes.d.ts +0 -5
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +3 -95
- package/dist/themes.js.map +1 -1
- package/migration-rules/v2-to-v3.md +277 -119
- package/package.json +5 -1
- package/{default-pages/application → required-pages/_shell}/page.html +56 -42
- package/required-pages/_shell/page.json +14 -0
- package/required-pages/_starters/page.html +534 -0
- package/required-pages/_starters/page.json +12 -0
- package/required-pages/builder/page.html +353 -43
- package/required-pages/builder/page.json +12 -10
- package/required-pages/pages/page.html +697 -924
- package/required-pages/pages/page.json +12 -10
- package/required-pages/settings/page.html +1888 -1753
- package/required-pages/settings/page.json +12 -10
- package/required-pages/synthos_apis/page.html +834 -845
- package/required-pages/synthos_apis/page.json +12 -10
- package/required-pages/synthos_scripts/page.html +74 -88
- package/required-pages/synthos_scripts/page.json +12 -10
- package/scripts/append-instructions.py +90 -0
- package/scripts/audit-instructions.py +76 -0
- package/scripts/cleanup-shell-markup.mjs +112 -0
- package/service-connectors/buffer/connector.json +46 -0
- package/service-connectors/canva/connector.json +67 -0
- package/service-connectors/elevenlabs/connector.json +1 -1
- package/src/builders/anthropic.ts +150 -25
- package/src/builders/claudecode.ts +310 -0
- package/src/builders/index.ts +7 -1
- package/src/builders/openai.ts +2 -1
- package/src/builders/types.ts +93 -32
- package/src/connectors/types.ts +8 -0
- package/src/init.ts +13 -7
- package/src/migrations.ts +187 -16
- package/src/models/anthropic.ts +140 -30
- package/src/models/chainOfThought.ts +33 -18
- package/src/models/index.ts +2 -2
- package/src/models/providers.ts +10 -1
- package/src/models/types.ts +21 -1
- package/src/pages.ts +271 -35
- package/src/service/createCompletePrompt.ts +6 -0
- package/src/service/mediaCache.ts +206 -0
- package/src/service/pageValidator.ts +337 -0
- package/src/service/server.ts +4 -0
- package/src/service/sharedTableSchema.ts +236 -0
- package/src/service/transformPage.ts +370 -260
- package/src/service/useApiRoutes.ts +283 -32
- package/src/service/useConnectorRoutes.ts +189 -34
- package/src/service/useDataRoutes.ts +198 -116
- package/src/service/useExtractRoutes.ts +331 -0
- package/src/service/usePageRoutes.ts +1414 -394
- package/src/service/useSharedDataRoutes.ts +184 -109
- package/src/settings.ts +65 -0
- package/src/themes.ts +78 -180
- package/starters/blank_starter/chat-history.json +1 -0
- package/starters/blank_starter/page.dark.png +0 -0
- package/starters/blank_starter/page.html +47 -0
- package/starters/blank_starter/page.json +13 -0
- package/starters/blank_starter/page.light.png +0 -0
- package/starters/calculator_starter/chat-history.json +1 -0
- package/starters/calculator_starter/page.dark.png +0 -0
- package/starters/calculator_starter/page.html +232 -0
- package/starters/calculator_starter/page.json +13 -0
- package/starters/calculator_starter/page.light.png +0 -0
- package/starters/calendar_starter/chat-history.json +1 -0
- package/starters/calendar_starter/page.dark.png +0 -0
- package/starters/calendar_starter/page.html +495 -0
- package/starters/calendar_starter/page.json +13 -0
- package/starters/calendar_starter/page.light.png +0 -0
- package/starters/chat_starter/chat-history.json +1 -0
- package/starters/chat_starter/page.dark.png +0 -0
- package/starters/chat_starter/page.html +351 -0
- package/starters/chat_starter/page.json +13 -0
- package/starters/chat_starter/page.light.png +0 -0
- package/starters/checklist_starter/chat-history.json +1 -0
- package/starters/checklist_starter/page.dark.png +0 -0
- package/starters/checklist_starter/page.html +437 -0
- package/starters/checklist_starter/page.json +13 -0
- package/starters/checklist_starter/page.light.png +0 -0
- package/starters/dashboard_starter/chat-history.json +1 -0
- package/starters/dashboard_starter/page.dark.png +0 -0
- package/starters/dashboard_starter/page.html +195 -0
- package/starters/dashboard_starter/page.json +13 -0
- package/starters/dashboard_starter/page.light.png +0 -0
- package/starters/form_starter/chat-history.json +1 -0
- package/starters/form_starter/page.dark.png +0 -0
- package/starters/form_starter/page.html +313 -0
- package/starters/form_starter/page.json +13 -0
- package/starters/form_starter/page.light.png +0 -0
- package/starters/gallery_starter/chat-history.json +1 -0
- package/starters/gallery_starter/page.dark.png +0 -0
- package/starters/gallery_starter/page.html +418 -0
- package/starters/gallery_starter/page.json +13 -0
- package/starters/gallery_starter/page.light.png +0 -0
- package/starters/generator_starter/chat-history.json +1 -0
- package/starters/generator_starter/page.dark.png +0 -0
- package/starters/generator_starter/page.html +261 -0
- package/starters/generator_starter/page.json +13 -0
- package/starters/generator_starter/page.light.png +0 -0
- package/starters/index.html +538 -0
- package/starters/kanban_starter/chat-history.json +1 -0
- package/starters/kanban_starter/page.dark.png +0 -0
- package/starters/kanban_starter/page.html +432 -0
- package/starters/kanban_starter/page.json +13 -0
- package/starters/kanban_starter/page.light.png +0 -0
- package/starters/presentation_builder/chat-history.json +1 -0
- package/starters/presentation_builder/page.dark.png +0 -0
- package/starters/presentation_builder/page.html +970 -0
- package/starters/presentation_builder/page.json +15 -0
- package/starters/presentation_builder/page.light.png +0 -0
- package/starters/presentation_builder/presentation_voice/voice_config.json +9 -0
- package/starters/pulse_starter/chat-history.json +1 -0
- package/starters/pulse_starter/page.dark.png +0 -0
- package/starters/pulse_starter/page.html +698 -0
- package/starters/pulse_starter/page.json +13 -0
- package/starters/pulse_starter/page.light.png +0 -0
- package/starters/quiz_starter/chat-history.json +1 -0
- package/starters/quiz_starter/page.dark.png +0 -0
- package/starters/quiz_starter/page.html +292 -0
- package/starters/quiz_starter/page.json +13 -0
- package/starters/quiz_starter/page.light.png +0 -0
- package/starters/reference_starter/chat-history.json +1 -0
- package/starters/reference_starter/page.dark.png +0 -0
- package/starters/reference_starter/page.html +250 -0
- package/starters/reference_starter/page.json +13 -0
- package/starters/reference_starter/page.light.png +0 -0
- package/starters/retro_game_starter/chat-history.json +1 -0
- package/starters/retro_game_starter/page.dark.png +0 -0
- package/{default-pages → starters}/retro_game_starter/page.html +1281 -1308
- package/starters/retro_game_starter/page.json +15 -0
- package/starters/retro_game_starter/page.light.png +0 -0
- package/starters/roster_starter/chat-history.json +1 -0
- package/starters/roster_starter/page.dark.png +0 -0
- package/starters/roster_starter/page.html +600 -0
- package/starters/roster_starter/page.json +13 -0
- package/starters/roster_starter/page.light.png +0 -0
- package/starters/server.js +182 -0
- package/starters/start.cmd +1 -0
- package/starters/timeline_starter/chat-history.json +1 -0
- package/starters/timeline_starter/page.dark.png +0 -0
- package/starters/timeline_starter/page.html +446 -0
- package/starters/timeline_starter/page.json +13 -0
- package/starters/timeline_starter/page.light.png +0 -0
- package/starters/tutorial_starter/chat-history.json +1 -0
- package/starters/tutorial_starter/page.dark.png +0 -0
- package/starters/tutorial_starter/page.html +283 -0
- package/starters/tutorial_starter/page.json +13 -0
- package/starters/tutorial_starter/page.light.png +0 -0
- package/static-files/agent.v3.js +122 -0
- package/static-files/connector.v3.js +48 -0
- package/static-files/extract.v3.js +188 -0
- package/static-files/helpers.v3.js +50 -6
- package/static-files/page-bridge.js +114 -0
- package/static-files/page.v3.js +1292 -1290
- package/static-files/script.v3.js +32 -0
- package/static-files/server.v3.js +89 -0
- package/static-files/shell-bridge.v3.js +174 -0
- package/static-files/shell-modals.v3.js +521 -0
- package/static-files/{shell.css → shell.v3.css} +271 -22
- package/static-files/shell.v3.js +1865 -0
- package/static-files/storage.v3.js +176 -0
- package/tests/anthropic.spec.ts +42 -7
- package/tests/builders.spec.ts +70 -2
- package/tests/pageValidator.spec.ts +548 -0
- package/tests/profiles.spec.ts +122 -0
- package/tests/sharedTableSchema.spec.ts +242 -0
- package/tests/transformPage.spec.ts +62 -81
- package/default-pages/application/page.json +0 -10
- package/default-pages/retro_game_starter/page.json +0 -12
- package/default-pages/sidebar_page/page.html +0 -51
- package/default-pages/sidebar_page/page.json +0 -10
- package/default-pages/two-panel_page/page.html +0 -68
- package/default-pages/two-panel_page/page.json +0 -10
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import {
|
|
6
|
+
deleteSchema,
|
|
7
|
+
isValidSchemaPayload,
|
|
8
|
+
listTables,
|
|
9
|
+
loadSchema,
|
|
10
|
+
mergeSchema,
|
|
11
|
+
newSchemaWrapper,
|
|
12
|
+
saveSchema,
|
|
13
|
+
schemaFile,
|
|
14
|
+
updateSchemaWrapper,
|
|
15
|
+
} from '../src/service/sharedTableSchema';
|
|
16
|
+
import { SynthOSConfig } from '../src/init';
|
|
17
|
+
import { FsStorageProvider } from '../src/storage';
|
|
18
|
+
|
|
19
|
+
function makeConfig(pagesFolder: string): SynthOSConfig {
|
|
20
|
+
return {
|
|
21
|
+
localFolder: '.synthos',
|
|
22
|
+
pagesFolder,
|
|
23
|
+
requiredPagesFolders: [],
|
|
24
|
+
defaultPagesFolders: [],
|
|
25
|
+
defaultScriptsFolders: [],
|
|
26
|
+
defaultThemesFolders: [],
|
|
27
|
+
staticFilesFolders: [],
|
|
28
|
+
serviceConnectorsFolders: [],
|
|
29
|
+
requiredPages: [],
|
|
30
|
+
storageProvider: new FsStorageProvider(),
|
|
31
|
+
debug: false,
|
|
32
|
+
debugPageUpdates: false,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SAMPLE_SCHEMA: Record<string, unknown> = {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
first_name: { type: 'string' },
|
|
40
|
+
last_name: { type: 'string' },
|
|
41
|
+
},
|
|
42
|
+
required: ['first_name'],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
describe('sharedTableSchema — pure helpers', () => {
|
|
46
|
+
describe('schemaFile', () => {
|
|
47
|
+
it('returns <parent>/<table>.schema.json', () => {
|
|
48
|
+
assert.strictEqual(schemaFile('/p/shared', 'employees'), path.join('/p/shared', 'employees.schema.json'));
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('isValidSchemaPayload', () => {
|
|
53
|
+
it('accepts plain objects', () => {
|
|
54
|
+
assert.strictEqual(isValidSchemaPayload({ type: 'object' }), true);
|
|
55
|
+
});
|
|
56
|
+
it('rejects arrays, null, primitives', () => {
|
|
57
|
+
assert.strictEqual(isValidSchemaPayload([]), false);
|
|
58
|
+
assert.strictEqual(isValidSchemaPayload(null), false);
|
|
59
|
+
assert.strictEqual(isValidSchemaPayload('schema'), false);
|
|
60
|
+
assert.strictEqual(isValidSchemaPayload(42), false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('newSchemaWrapper', () => {
|
|
65
|
+
it('initializes version, timestamps, schema, definedBy', () => {
|
|
66
|
+
const w = newSchemaWrapper(SAMPLE_SCHEMA, '2026-04-25T00:00:00.000Z', 'builder');
|
|
67
|
+
assert.strictEqual(w.version, 1);
|
|
68
|
+
assert.strictEqual(w.createdAt, '2026-04-25T00:00:00.000Z');
|
|
69
|
+
assert.strictEqual(w.updatedAt, '2026-04-25T00:00:00.000Z');
|
|
70
|
+
assert.strictEqual(w.definedBy, 'builder');
|
|
71
|
+
assert.deepStrictEqual(w.schema, SAMPLE_SCHEMA);
|
|
72
|
+
});
|
|
73
|
+
it('omits definedBy when not provided', () => {
|
|
74
|
+
const w = newSchemaWrapper(SAMPLE_SCHEMA, '2026-04-25T00:00:00.000Z');
|
|
75
|
+
assert.strictEqual(w.definedBy, undefined);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('updateSchemaWrapper', () => {
|
|
80
|
+
it('preserves createdAt, advances updatedAt', () => {
|
|
81
|
+
const orig = newSchemaWrapper(SAMPLE_SCHEMA, '2026-04-25T00:00:00.000Z', 'page-a');
|
|
82
|
+
const updated = updateSchemaWrapper(orig, { type: 'object' }, '2026-04-26T00:00:00.000Z', 'page-b');
|
|
83
|
+
assert.strictEqual(updated.createdAt, '2026-04-25T00:00:00.000Z');
|
|
84
|
+
assert.strictEqual(updated.updatedAt, '2026-04-26T00:00:00.000Z');
|
|
85
|
+
assert.strictEqual(updated.definedBy, 'page-b');
|
|
86
|
+
});
|
|
87
|
+
it('keeps existing definedBy when no override given', () => {
|
|
88
|
+
const orig = newSchemaWrapper(SAMPLE_SCHEMA, '2026-04-25T00:00:00.000Z', 'page-a');
|
|
89
|
+
const updated = updateSchemaWrapper(orig, { type: 'object' }, '2026-04-26T00:00:00.000Z');
|
|
90
|
+
assert.strictEqual(updated.definedBy, 'page-a');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('mergeSchema', () => {
|
|
95
|
+
it('replace mode wins outright', () => {
|
|
96
|
+
const { merged, conflicts } = mergeSchema(SAMPLE_SCHEMA, { type: 'object', properties: { name: { type: 'string' } } }, 'replace');
|
|
97
|
+
assert.deepStrictEqual(conflicts, []);
|
|
98
|
+
assert.deepStrictEqual(merged, { type: 'object', properties: { name: { type: 'string' } } });
|
|
99
|
+
});
|
|
100
|
+
it('additive: adds new fields, keeps existing', () => {
|
|
101
|
+
const incoming: Record<string, unknown> = {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
phone: { type: 'string' },
|
|
105
|
+
},
|
|
106
|
+
required: ['phone'],
|
|
107
|
+
};
|
|
108
|
+
const { merged, conflicts } = mergeSchema(SAMPLE_SCHEMA, incoming, 'additive');
|
|
109
|
+
assert.deepStrictEqual(conflicts, []);
|
|
110
|
+
const props = (merged.properties as Record<string, unknown>);
|
|
111
|
+
assert.ok(props.first_name);
|
|
112
|
+
assert.ok(props.last_name);
|
|
113
|
+
assert.ok(props.phone);
|
|
114
|
+
assert.deepStrictEqual(merged.required, ['first_name', 'phone']);
|
|
115
|
+
});
|
|
116
|
+
it('additive: existing wins on overlap when types match', () => {
|
|
117
|
+
const incoming: Record<string, unknown> = {
|
|
118
|
+
type: 'object',
|
|
119
|
+
properties: {
|
|
120
|
+
first_name: { type: 'string', description: 'changed' },
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
const { merged, conflicts } = mergeSchema(SAMPLE_SCHEMA, incoming, 'additive');
|
|
124
|
+
assert.deepStrictEqual(conflicts, []);
|
|
125
|
+
const fn = (merged.properties as Record<string, any>).first_name;
|
|
126
|
+
assert.strictEqual(fn.description, undefined, 'existing definition should be preserved');
|
|
127
|
+
});
|
|
128
|
+
it('additive: type conflicts produce 409-style result with merged === existing', () => {
|
|
129
|
+
const incoming: Record<string, unknown> = {
|
|
130
|
+
type: 'object',
|
|
131
|
+
properties: {
|
|
132
|
+
first_name: { type: 'number' },
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
const { merged, conflicts } = mergeSchema(SAMPLE_SCHEMA, incoming, 'additive');
|
|
136
|
+
assert.strictEqual(conflicts.length, 1);
|
|
137
|
+
assert.strictEqual(conflicts[0].field, 'first_name');
|
|
138
|
+
assert.strictEqual(merged, SAMPLE_SCHEMA);
|
|
139
|
+
});
|
|
140
|
+
it('additive: undefined existing returns incoming as-is', () => {
|
|
141
|
+
const incoming: Record<string, unknown> = { type: 'object', properties: { x: { type: 'string' } } };
|
|
142
|
+
const { merged, conflicts } = mergeSchema(undefined, incoming, 'additive');
|
|
143
|
+
assert.deepStrictEqual(conflicts, []);
|
|
144
|
+
assert.deepStrictEqual(merged, incoming);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('sharedTableSchema — IO + listing', () => {
|
|
150
|
+
let tmpDir: string;
|
|
151
|
+
let config: SynthOSConfig;
|
|
152
|
+
let parent: string;
|
|
153
|
+
|
|
154
|
+
beforeEach(async () => {
|
|
155
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'synthos-schema-test-'));
|
|
156
|
+
parent = path.join(tmpDir, 'shared');
|
|
157
|
+
config = makeConfig(tmpDir);
|
|
158
|
+
await fs.mkdir(parent, { recursive: true });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
afterEach(async () => {
|
|
162
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('saveSchema + loadSchema roundtrip', async () => {
|
|
166
|
+
const wrapper = newSchemaWrapper(SAMPLE_SCHEMA, '2026-04-25T00:00:00.000Z', 'builder');
|
|
167
|
+
await saveSchema(config, parent, 'employees', wrapper);
|
|
168
|
+
const loaded = await loadSchema(config, parent, 'employees');
|
|
169
|
+
assert.deepStrictEqual(loaded, wrapper);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('loadSchema returns undefined for missing sidecar', async () => {
|
|
173
|
+
const loaded = await loadSchema(config, parent, 'ghost');
|
|
174
|
+
assert.strictEqual(loaded, undefined);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('loadSchema returns undefined for malformed sidecar', async () => {
|
|
178
|
+
await fs.writeFile(path.join(parent, 'bad.schema.json'), 'not-json');
|
|
179
|
+
const loaded = await loadSchema(config, parent, 'bad');
|
|
180
|
+
assert.strictEqual(loaded, undefined);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('deleteSchema removes the sidecar; idempotent on missing', async () => {
|
|
184
|
+
const wrapper = newSchemaWrapper(SAMPLE_SCHEMA, '2026-04-25T00:00:00.000Z');
|
|
185
|
+
await saveSchema(config, parent, 'employees', wrapper);
|
|
186
|
+
await deleteSchema(config, parent, 'employees');
|
|
187
|
+
assert.strictEqual(await loadSchema(config, parent, 'employees'), undefined);
|
|
188
|
+
// Second delete should not throw
|
|
189
|
+
await deleteSchema(config, parent, 'employees');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('listTables', () => {
|
|
193
|
+
it('lists subfolder tables with record count', async () => {
|
|
194
|
+
const empFolder = path.join(parent, 'employees');
|
|
195
|
+
await fs.mkdir(empFolder, { recursive: true });
|
|
196
|
+
await fs.writeFile(path.join(empFolder, 'a.json'), '{}');
|
|
197
|
+
await fs.writeFile(path.join(empFolder, 'b.json'), '{}');
|
|
198
|
+
|
|
199
|
+
const tables = await listTables(config, parent);
|
|
200
|
+
assert.deepStrictEqual(tables, [
|
|
201
|
+
{ name: 'employees', hasSchema: false, recordCount: 2 },
|
|
202
|
+
]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('lists schema-only tables (no records yet)', async () => {
|
|
206
|
+
const wrapper = newSchemaWrapper(SAMPLE_SCHEMA, '2026-04-25T00:00:00.000Z');
|
|
207
|
+
await saveSchema(config, parent, 'locations', wrapper);
|
|
208
|
+
const tables = await listTables(config, parent);
|
|
209
|
+
assert.deepStrictEqual(tables, [
|
|
210
|
+
{ name: 'locations', hasSchema: true, recordCount: 0 },
|
|
211
|
+
]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('combines records + schema entries', async () => {
|
|
215
|
+
const empFolder = path.join(parent, 'employees');
|
|
216
|
+
await fs.mkdir(empFolder, { recursive: true });
|
|
217
|
+
await fs.writeFile(path.join(empFolder, 'a.json'), '{}');
|
|
218
|
+
const wrapper = newSchemaWrapper(SAMPLE_SCHEMA, '2026-04-25T00:00:00.000Z');
|
|
219
|
+
await saveSchema(config, parent, 'employees', wrapper);
|
|
220
|
+
const tables = await listTables(config, parent);
|
|
221
|
+
assert.deepStrictEqual(tables, [
|
|
222
|
+
{ name: 'employees', hasSchema: true, recordCount: 1 },
|
|
223
|
+
]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('respects reserved set', async () => {
|
|
227
|
+
const filesFolder = path.join(parent, 'files');
|
|
228
|
+
await fs.mkdir(filesFolder, { recursive: true });
|
|
229
|
+
const empFolder = path.join(parent, 'employees');
|
|
230
|
+
await fs.mkdir(empFolder, { recursive: true });
|
|
231
|
+
|
|
232
|
+
const tables = await listTables(config, parent, new Set(['files']));
|
|
233
|
+
assert.strictEqual(tables.length, 1);
|
|
234
|
+
assert.strictEqual(tables[0].name, 'employees');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('returns [] for missing namespace', async () => {
|
|
238
|
+
const tables = await listTables(config, path.join(tmpDir, 'doesnotexist'));
|
|
239
|
+
assert.deepStrictEqual(tables, []);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
stripNodeIds,
|
|
8
8
|
applyChangeList,
|
|
9
9
|
parseChangeList,
|
|
10
|
-
injectError,
|
|
11
10
|
deduplicateInlineScripts,
|
|
12
11
|
normalizedIndexOf,
|
|
13
12
|
transformPage,
|
|
@@ -21,13 +20,13 @@ import { Builder, BuilderResult, ContextSection } from '../src/builders/types';
|
|
|
21
20
|
// ---------------------------------------------------------------------------
|
|
22
21
|
|
|
23
22
|
describe('assignNodeIds', () => {
|
|
24
|
-
it('assigns sequential data-
|
|
23
|
+
it('assigns sequential data-nid to every element', () => {
|
|
25
24
|
const html = '<html><head></head><body><div><p>Hello</p></div></body></html>';
|
|
26
25
|
const { html: result, nodeCount } = assignNodeIds(html);
|
|
27
|
-
assert.ok(result.includes('data-
|
|
26
|
+
assert.ok(result.includes('data-nid="0"'));
|
|
28
27
|
assert.ok(nodeCount > 0);
|
|
29
28
|
// Every tag should have an id — count occurrences
|
|
30
|
-
const ids = result.match(/data-
|
|
29
|
+
const ids = result.match(/data-nid="/g);
|
|
31
30
|
assert.strictEqual(ids?.length, nodeCount);
|
|
32
31
|
});
|
|
33
32
|
|
|
@@ -38,16 +37,16 @@ describe('assignNodeIds', () => {
|
|
|
38
37
|
assert.ok(nodeCount >= 4); // html, head, body, div, span, span = 6
|
|
39
38
|
});
|
|
40
39
|
|
|
41
|
-
it('assigns data-
|
|
40
|
+
it('assigns data-nid to script and style elements', () => {
|
|
42
41
|
const html = '<html><head><style>.a{color:red}</style><script>var x=1;</script></head><body><script src="/app.js"></script></body></html>';
|
|
43
42
|
const { html: result, nodeCount } = assignNodeIds(html);
|
|
44
43
|
// All 6 elements should have ids: html, head, style, script(inline), body, script(src)
|
|
45
|
-
const ids = result.match(/data-
|
|
44
|
+
const ids = result.match(/data-nid="/g);
|
|
46
45
|
assert.strictEqual(ids?.length, nodeCount);
|
|
47
46
|
assert.strictEqual(nodeCount, 6);
|
|
48
47
|
// Verify the style and script tags specifically got ids
|
|
49
|
-
assert.ok(result.match(/<style[^>]+data-
|
|
50
|
-
assert.ok(result.match(/<script[^>]+data-
|
|
48
|
+
assert.ok(result.match(/<style[^>]+data-nid="/), 'style element should have data-nid');
|
|
49
|
+
assert.ok(result.match(/<script[^>]+data-nid="/), 'script element should have data-nid');
|
|
51
50
|
});
|
|
52
51
|
});
|
|
53
52
|
|
|
@@ -56,10 +55,10 @@ describe('assignNodeIds', () => {
|
|
|
56
55
|
// ---------------------------------------------------------------------------
|
|
57
56
|
|
|
58
57
|
describe('stripNodeIds', () => {
|
|
59
|
-
it('removes all data-
|
|
60
|
-
const html = '<div data-
|
|
58
|
+
it('removes all data-nid attributes', () => {
|
|
59
|
+
const html = '<div data-nid="0"><p data-nid="1">Hi</p></div>';
|
|
61
60
|
const result = stripNodeIds(html);
|
|
62
|
-
assert.ok(!result.includes('data-
|
|
61
|
+
assert.ok(!result.includes('data-nid'));
|
|
63
62
|
assert.ok(result.includes('<p>Hi</p>'));
|
|
64
63
|
});
|
|
65
64
|
});
|
|
@@ -69,12 +68,12 @@ describe('stripNodeIds', () => {
|
|
|
69
68
|
// ---------------------------------------------------------------------------
|
|
70
69
|
|
|
71
70
|
describe('assignNodeIds -> stripNodeIds roundtrip', () => {
|
|
72
|
-
it('produces HTML without data-
|
|
71
|
+
it('produces HTML without data-nid attributes', () => {
|
|
73
72
|
const original = '<html><head></head><body><div><p>Hello</p></div></body></html>';
|
|
74
73
|
const { html: annotated } = assignNodeIds(original);
|
|
75
|
-
assert.ok(annotated.includes('data-
|
|
74
|
+
assert.ok(annotated.includes('data-nid'));
|
|
76
75
|
const stripped = stripNodeIds(annotated);
|
|
77
|
-
assert.ok(!stripped.includes('data-
|
|
76
|
+
assert.ok(!stripped.includes('data-nid'));
|
|
78
77
|
assert.ok(stripped.includes('<p>Hello</p>'));
|
|
79
78
|
});
|
|
80
79
|
});
|
|
@@ -86,7 +85,7 @@ describe('assignNodeIds -> stripNodeIds roundtrip', () => {
|
|
|
86
85
|
describe('applyChangeList', () => {
|
|
87
86
|
// Helper: wrap content in a minimal annotated structure
|
|
88
87
|
const annotated = '<html><head></head><body>' +
|
|
89
|
-
'<div data-
|
|
88
|
+
'<div data-nid="10"><p data-nid="11">Old text</p></div>' +
|
|
90
89
|
'</body></html>';
|
|
91
90
|
|
|
92
91
|
it('applies "update" — replaces innerHTML', () => {
|
|
@@ -94,7 +93,7 @@ describe('applyChangeList', () => {
|
|
|
94
93
|
{ op: 'update', nodeId: '11', html: 'New text' },
|
|
95
94
|
];
|
|
96
95
|
const result = applyChangeList(annotated, changes);
|
|
97
|
-
assert.ok(result.includes('<p data-
|
|
96
|
+
assert.ok(result.includes('<p data-nid="11">New text</p>'));
|
|
98
97
|
});
|
|
99
98
|
|
|
100
99
|
it('applies "replace" — replaces outerHTML', () => {
|
|
@@ -103,7 +102,7 @@ describe('applyChangeList', () => {
|
|
|
103
102
|
];
|
|
104
103
|
const result = applyChangeList(annotated, changes);
|
|
105
104
|
assert.ok(result.includes('<span>Replaced</span>'));
|
|
106
|
-
assert.ok(!result.includes('data-
|
|
105
|
+
assert.ok(!result.includes('data-nid="11"'));
|
|
107
106
|
});
|
|
108
107
|
|
|
109
108
|
it('applies "delete" — removes element', () => {
|
|
@@ -111,7 +110,7 @@ describe('applyChangeList', () => {
|
|
|
111
110
|
{ op: 'delete', nodeId: '11' },
|
|
112
111
|
];
|
|
113
112
|
const result = applyChangeList(annotated, changes);
|
|
114
|
-
assert.ok(!result.includes('data-
|
|
113
|
+
assert.ok(!result.includes('data-nid="11"'));
|
|
115
114
|
assert.ok(!result.includes('Old text'));
|
|
116
115
|
});
|
|
117
116
|
|
|
@@ -130,7 +129,7 @@ describe('applyChangeList', () => {
|
|
|
130
129
|
const result = applyChangeList(annotated, changes);
|
|
131
130
|
// Prepended element should appear before the <p>
|
|
132
131
|
const prependIdx = result.indexOf('<em>Prepended</em>');
|
|
133
|
-
const pIdx = result.indexOf('<p data-
|
|
132
|
+
const pIdx = result.indexOf('<p data-nid="11">');
|
|
134
133
|
assert.ok(prependIdx < pIdx);
|
|
135
134
|
});
|
|
136
135
|
|
|
@@ -140,7 +139,7 @@ describe('applyChangeList', () => {
|
|
|
140
139
|
];
|
|
141
140
|
const result = applyChangeList(annotated, changes);
|
|
142
141
|
const beforeIdx = result.indexOf('<em>Before</em>');
|
|
143
|
-
const pIdx = result.indexOf('<p data-
|
|
142
|
+
const pIdx = result.indexOf('<p data-nid="11">');
|
|
144
143
|
assert.ok(beforeIdx < pIdx);
|
|
145
144
|
});
|
|
146
145
|
|
|
@@ -150,7 +149,7 @@ describe('applyChangeList', () => {
|
|
|
150
149
|
];
|
|
151
150
|
const result = applyChangeList(annotated, changes);
|
|
152
151
|
const afterIdx = result.indexOf('<em>After</em>');
|
|
153
|
-
const pIdx = result.indexOf('<p data-
|
|
152
|
+
const pIdx = result.indexOf('<p data-nid="11">');
|
|
154
153
|
assert.ok(afterIdx > pIdx);
|
|
155
154
|
});
|
|
156
155
|
|
|
@@ -163,11 +162,14 @@ describe('applyChangeList', () => {
|
|
|
163
162
|
assert.ok(!result.includes('Ghost'));
|
|
164
163
|
});
|
|
165
164
|
|
|
166
|
-
it('
|
|
165
|
+
it('warns but does not throw on missing parent for insert', () => {
|
|
167
166
|
const changes: ChangeList = [
|
|
168
167
|
{ op: 'insert', parentId: '999', position: 'append', html: '<em>Fail</em>' },
|
|
169
168
|
];
|
|
170
|
-
|
|
169
|
+
// Should not throw — warns and skips
|
|
170
|
+
const result = applyChangeList(annotated, changes);
|
|
171
|
+
assert.ok(!result.includes('Fail'));
|
|
172
|
+
assert.ok(result.includes('Old text')); // original content preserved
|
|
171
173
|
});
|
|
172
174
|
|
|
173
175
|
it('applies "style-element" — sets style attribute on unlocked element', () => {
|
|
@@ -176,12 +178,12 @@ describe('applyChangeList', () => {
|
|
|
176
178
|
];
|
|
177
179
|
const result = applyChangeList(annotated, changes);
|
|
178
180
|
assert.ok(result.includes('style="color: red; font-size: 16px"'));
|
|
179
|
-
assert.ok(result.includes('data-
|
|
181
|
+
assert.ok(result.includes('data-nid="11"'));
|
|
180
182
|
});
|
|
181
183
|
|
|
182
184
|
it('skips "style-element" on a data-locked element', () => {
|
|
183
185
|
const lockedHtml = '<html><head></head><body>' +
|
|
184
|
-
'<div data-
|
|
186
|
+
'<div data-nid="10"><p data-nid="11" data-locked>Locked text</p></div>' +
|
|
185
187
|
'</body></html>';
|
|
186
188
|
const changes: ChangeList = [
|
|
187
189
|
{ op: 'style-element', nodeId: '11', style: 'color: red' },
|
|
@@ -200,9 +202,9 @@ describe('applyChangeList', () => {
|
|
|
200
202
|
|
|
201
203
|
it('allows delete of unlocked child inside a data-locked parent', () => {
|
|
202
204
|
const lockedParentHtml = '<html><head></head><body>' +
|
|
203
|
-
'<div data-
|
|
204
|
-
'<p data-
|
|
205
|
-
'<p data-
|
|
205
|
+
'<div data-nid="10" data-locked="true">' +
|
|
206
|
+
'<p data-nid="11">Child message</p>' +
|
|
207
|
+
'<p data-nid="12">Another child</p>' +
|
|
206
208
|
'</div></body></html>';
|
|
207
209
|
const changes: ChangeList = [
|
|
208
210
|
{ op: 'delete', nodeId: '11' },
|
|
@@ -214,7 +216,7 @@ describe('applyChangeList', () => {
|
|
|
214
216
|
|
|
215
217
|
it('blocks delete of element that itself has data-locked', () => {
|
|
216
218
|
const lockedHtml = '<html><head></head><body>' +
|
|
217
|
-
'<div data-
|
|
219
|
+
'<div data-nid="10"><p data-nid="11" data-locked="true">Locked</p></div>' +
|
|
218
220
|
'</body></html>';
|
|
219
221
|
const changes: ChangeList = [
|
|
220
222
|
{ op: 'delete', nodeId: '11' },
|
|
@@ -225,8 +227,8 @@ describe('applyChangeList', () => {
|
|
|
225
227
|
|
|
226
228
|
it('allows replace of unlocked child inside a data-locked parent', () => {
|
|
227
229
|
const lockedParentHtml = '<html><head></head><body>' +
|
|
228
|
-
'<div data-
|
|
229
|
-
'<p data-
|
|
230
|
+
'<div data-nid="10" data-locked="true">' +
|
|
231
|
+
'<p data-nid="11">Old child</p>' +
|
|
230
232
|
'</div></body></html>';
|
|
231
233
|
const changes: ChangeList = [
|
|
232
234
|
{ op: 'replace', nodeId: '11', html: '<span>New child</span>' },
|
|
@@ -246,7 +248,7 @@ describe('applyChangeList', () => {
|
|
|
246
248
|
|
|
247
249
|
it('skips replace on a data-locked element', () => {
|
|
248
250
|
const lockedHtml = '<html><head></head><body>' +
|
|
249
|
-
'<div data-
|
|
251
|
+
'<div data-nid="10"><p data-nid="11" data-locked>Locked</p></div>' +
|
|
250
252
|
'</body></html>';
|
|
251
253
|
const changes: ChangeList = [
|
|
252
254
|
{ op: 'replace', nodeId: '11', html: '<span>Replaced</span>' },
|
|
@@ -265,11 +267,14 @@ describe('applyChangeList', () => {
|
|
|
265
267
|
assert.ok(result.includes('Old text'));
|
|
266
268
|
});
|
|
267
269
|
|
|
268
|
-
it('
|
|
270
|
+
it('warns but does not throw on unknown insert position', () => {
|
|
269
271
|
const changes = [
|
|
270
272
|
{ op: 'insert', parentId: '10', position: 'sideways', html: '<em>Oops</em>' },
|
|
271
273
|
] as unknown as ChangeList;
|
|
272
|
-
|
|
274
|
+
// Should not throw — warns and skips
|
|
275
|
+
const result = applyChangeList(annotated, changes);
|
|
276
|
+
assert.ok(!result.includes('Oops'));
|
|
277
|
+
assert.ok(result.includes('Old text')); // original content preserved
|
|
273
278
|
});
|
|
274
279
|
|
|
275
280
|
it('throws on unknown op', () => {
|
|
@@ -313,34 +318,9 @@ describe('parseChangeList', () => {
|
|
|
313
318
|
});
|
|
314
319
|
|
|
315
320
|
// ---------------------------------------------------------------------------
|
|
316
|
-
// injectError
|
|
321
|
+
// injectError — REMOVED (iframe model: errors reported via shell postMessage)
|
|
317
322
|
// ---------------------------------------------------------------------------
|
|
318
323
|
|
|
319
|
-
describe('injectError', () => {
|
|
320
|
-
it('injects error script block into body', () => {
|
|
321
|
-
const html = '<html><head></head><body><p>Page</p></body></html>';
|
|
322
|
-
const result = injectError(html, 'Oops', 'details here');
|
|
323
|
-
assert.ok(result.includes('<script id="error" type="application/json">'));
|
|
324
|
-
assert.ok(result.includes('"message":"Oops"'));
|
|
325
|
-
assert.ok(result.includes('"details":"details here"'));
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it('replaces existing error block', () => {
|
|
329
|
-
const html = '<html><head></head><body><script id="error" type="application/json">{"message":"old"}</script></body></html>';
|
|
330
|
-
const result = injectError(html, 'New', 'new detail');
|
|
331
|
-
// Should have exactly one error script
|
|
332
|
-
const matches = result.match(/<script id="error"/g);
|
|
333
|
-
assert.strictEqual(matches?.length, 1);
|
|
334
|
-
assert.ok(result.includes('"message":"New"'));
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it('appends to end if no body tag', () => {
|
|
338
|
-
const html = '<div>No body</div>';
|
|
339
|
-
const result = injectError(html, 'Err', 'det');
|
|
340
|
-
assert.ok(result.includes('<script id="error"'));
|
|
341
|
-
});
|
|
342
|
-
});
|
|
343
|
-
|
|
344
324
|
// ---------------------------------------------------------------------------
|
|
345
325
|
// deduplicateInlineScripts
|
|
346
326
|
// ---------------------------------------------------------------------------
|
|
@@ -497,7 +477,7 @@ describe('normalizedIndexOf', () => {
|
|
|
497
477
|
|
|
498
478
|
describe('applyChangeList — search-replace / search-insert ops', () => {
|
|
499
479
|
const scriptHtml = '<html><head></head><body>' +
|
|
500
|
-
'<script data-
|
|
480
|
+
'<script data-nid="5">let a = 1;\nlet b = 2;\nlet c = 3;\nlet d = 4;\nlet e = 5;</script>' +
|
|
501
481
|
'</body></html>';
|
|
502
482
|
|
|
503
483
|
it('search-replace replaces exact text match', () => {
|
|
@@ -524,7 +504,7 @@ describe('applyChangeList — search-replace / search-insert ops', () => {
|
|
|
524
504
|
it('search-replace falls back to normalized match', () => {
|
|
525
505
|
// Script has single spaces, search has different whitespace
|
|
526
506
|
const html = '<html><head></head><body>' +
|
|
527
|
-
'<script data-
|
|
507
|
+
'<script data-nid="5">function foo() {\n return 1;\n}</script>' +
|
|
528
508
|
'</body></html>';
|
|
529
509
|
const changes: ChangeList = [
|
|
530
510
|
{ op: 'search-replace', nodeId: '5', search: 'function foo() { return 1; }', replace: 'function foo() { return 2; }' },
|
|
@@ -545,7 +525,7 @@ describe('applyChangeList — search-replace / search-insert ops', () => {
|
|
|
545
525
|
});
|
|
546
526
|
|
|
547
527
|
it('search-replace works on style blocks', () => {
|
|
548
|
-
const styleHtml = '<html><head><style data-
|
|
528
|
+
const styleHtml = '<html><head><style data-nid="3">.a { color: red; }\n.b { color: blue; }\n.c { color: green; }</style></head><body></body></html>';
|
|
549
529
|
const changes: ChangeList = [
|
|
550
530
|
{ op: 'search-replace', nodeId: '3', search: '.b { color: blue; }', replace: '.b { color: purple; }' },
|
|
551
531
|
];
|
|
@@ -602,11 +582,11 @@ describe('transformPage', () => {
|
|
|
602
582
|
<div id="thoughts" style="display: none;"></div>
|
|
603
583
|
</body></html>`;
|
|
604
584
|
|
|
605
|
-
/** Extract the data-
|
|
585
|
+
/** Extract the data-nid for an element identified by a CSS-style attribute (e.g. id="content") from annotated HTML. */
|
|
606
586
|
function findNodeId(annotatedHtml: string, idAttr: string): string {
|
|
607
|
-
// Match a tag that contains both data-
|
|
608
|
-
const pattern1 = new RegExp(`data-
|
|
609
|
-
const pattern2 = new RegExp(`${idAttr}[^>]*data-
|
|
587
|
+
// Match a tag that contains both data-nid="X" and the target id, in either order
|
|
588
|
+
const pattern1 = new RegExp(`data-nid="(\\d+)"[^>]*${idAttr}`);
|
|
589
|
+
const pattern2 = new RegExp(`${idAttr}[^>]*data-nid="(\\d+)"`);
|
|
610
590
|
const m = annotatedHtml.match(pattern1) || annotatedHtml.match(pattern2);
|
|
611
591
|
return m ? m[1] : '99999';
|
|
612
592
|
}
|
|
@@ -639,11 +619,11 @@ describe('transformPage', () => {
|
|
|
639
619
|
assert.ok(result.value);
|
|
640
620
|
assert.ok(result.value.html.includes('Updated content'));
|
|
641
621
|
assert.strictEqual(result.value.changeCount, 1);
|
|
642
|
-
// Should not contain data-
|
|
643
|
-
assert.ok(!result.value.html.includes('data-
|
|
622
|
+
// Should not contain data-nid attributes
|
|
623
|
+
assert.ok(!result.value.html.includes('data-nid'));
|
|
644
624
|
});
|
|
645
625
|
|
|
646
|
-
it('returns
|
|
626
|
+
it('returns errorText when builder returns error (shell displays it)', async () => {
|
|
647
627
|
const builder = makeBuilder(async () => ({
|
|
648
628
|
kind: 'error',
|
|
649
629
|
error: new Error('API quota exceeded'),
|
|
@@ -652,11 +632,12 @@ describe('transformPage', () => {
|
|
|
652
632
|
const result = await transformPage(makeArgs(builder));
|
|
653
633
|
assert.strictEqual(result.completed, true);
|
|
654
634
|
assert.ok(result.value);
|
|
655
|
-
assert.
|
|
656
|
-
assert.
|
|
635
|
+
assert.strictEqual(result.value.errorText, 'API quota exceeded');
|
|
636
|
+
assert.ok(!result.value.html.includes('id="error"'), 'error should not be injected into HTML');
|
|
637
|
+
assert.strictEqual(result.value.changeCount, -1);
|
|
657
638
|
});
|
|
658
639
|
|
|
659
|
-
it('
|
|
640
|
+
it('returns replyText when builder returns reply (shell displays it)', async () => {
|
|
660
641
|
const builder = makeBuilder(async () => ({
|
|
661
642
|
kind: 'reply',
|
|
662
643
|
text: 'I cannot help with that.',
|
|
@@ -668,10 +649,9 @@ describe('transformPage', () => {
|
|
|
668
649
|
});
|
|
669
650
|
assert.strictEqual(result.completed, true);
|
|
670
651
|
assert.ok(result.value);
|
|
671
|
-
assert.
|
|
672
|
-
assert.ok(result.value.html.includes('
|
|
673
|
-
assert.
|
|
674
|
-
assert.strictEqual(result.value.changeCount, 0);
|
|
652
|
+
assert.strictEqual(result.value.replyText, 'I cannot help with that.');
|
|
653
|
+
assert.ok(!result.value.html.includes('User:'), 'chat messages should not be injected into HTML');
|
|
654
|
+
assert.strictEqual(result.value.changeCount, -1);
|
|
675
655
|
});
|
|
676
656
|
|
|
677
657
|
it('handles missing nodes gracefully (no repair pass)', async () => {
|
|
@@ -783,7 +763,7 @@ describe('transformPage', () => {
|
|
|
783
763
|
assert.ok(!result.value!.html.includes('color: red'));
|
|
784
764
|
});
|
|
785
765
|
|
|
786
|
-
it('catches exceptions from builder and
|
|
766
|
+
it('catches exceptions from builder and returns errorText (shell displays it)', async () => {
|
|
787
767
|
const builder = makeBuilder(async () => {
|
|
788
768
|
throw new Error('Unexpected builder crash');
|
|
789
769
|
});
|
|
@@ -791,8 +771,9 @@ describe('transformPage', () => {
|
|
|
791
771
|
const result = await transformPage(makeArgs(builder));
|
|
792
772
|
assert.strictEqual(result.completed, true);
|
|
793
773
|
assert.ok(result.value);
|
|
794
|
-
assert.
|
|
795
|
-
assert.
|
|
774
|
+
assert.strictEqual(result.value.errorText, 'Unexpected builder crash');
|
|
775
|
+
assert.ok(!result.value.html.includes('id="error"'), 'error should not be injected into HTML');
|
|
776
|
+
assert.strictEqual(result.value.changeCount, -1);
|
|
796
777
|
});
|
|
797
778
|
|
|
798
779
|
it('detects newBuild when isBuilder is true and only one chat message', async () => {
|
|
@@ -864,7 +845,7 @@ describe('transformPage', () => {
|
|
|
864
845
|
// Edit should be applied
|
|
865
846
|
assert.ok(result.value.html.includes('let count = 42;'));
|
|
866
847
|
// Node ids should be stripped
|
|
867
|
-
assert.ok(!result.value.html.includes('data-
|
|
848
|
+
assert.ok(!result.value.html.includes('data-nid'));
|
|
868
849
|
assert.strictEqual(result.value.changeCount, 1);
|
|
869
850
|
});
|
|
870
851
|
});
|