ultravisor 1.0.26 → 1.0.27
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/docs/features/persistence-via-databeacon.md +103 -1
- package/package.json +1 -1
- package/source/services/Ultravisor-ManifestStoreBridge.cjs +13 -1
- package/source/services/Ultravisor-QueuePersistenceBridge.cjs +69 -3
- package/test/Ultravisor_BeaconQueue_tests.js +6 -0
- package/test/Ultravisor_tests.js +10 -4
|
@@ -809,7 +809,109 @@ log so operators can distinguish UV writes from manual mesh activity.
|
|
|
809
809
|
lab API. Replaces the Session 2 bridge-only smoke test as the
|
|
810
810
|
default integration test.
|
|
811
811
|
|
|
812
|
-
### Session 4 — engine coverage via meadow-migrationmanager + Docker-driven smoke + polish
|
|
812
|
+
### Session 4 (complete) — engine coverage via meadow-migrationmanager + Docker-driven smoke + polish
|
|
813
|
+
|
|
814
|
+
Shipped:
|
|
815
|
+
|
|
816
|
+
- [x] **`DataBeacon-SchemaManager` now embeds `meadow-migrationmanager`.**
|
|
817
|
+
Constructor instantiates `SchemaIntrospector` / `SchemaDiff` /
|
|
818
|
+
`MigrationGenerator` / `SchemaDeployer` on an isolated MM Pict
|
|
819
|
+
context (no TUI / WebUI / Orator deps loaded). The Session 2
|
|
820
|
+
SQLite-only `_alterTablesIfChanged` body is gone; in its place is
|
|
821
|
+
the `introspect → diff → forward-only filter → generate → execute`
|
|
822
|
+
pipeline. Forward-only filter strips `TablesRemoved` /
|
|
823
|
+
`ColumnsRemoved` / `ColumnsModified` / `IndicesRemoved` from the
|
|
824
|
+
diff and surfaces them on `pResult.SkippedDestructive` for operator
|
|
825
|
+
visibility. Statement execution dispatches to better-sqlite3's
|
|
826
|
+
`.exec()` for SQLite or the schemaProvider's `_ConnectionPool.query()`
|
|
827
|
+
for MySQL / MSSQL / PostgreSQL. EnsureSchema response gains
|
|
828
|
+
`MigrationStatements` (the array MigrationGenerator emitted) and
|
|
829
|
+
`SkippedDestructive` (forward-only-dropped entries).
|
|
830
|
+
`meadow-migrationmanager` added as a runtime dep on
|
|
831
|
+
retold-databeacon's `package.json`.
|
|
832
|
+
- [x] **Per-engine integration tests** at
|
|
833
|
+
`modules/apps/retold-databeacon/test/DataBeacon-SchemaManager_tests.js`.
|
|
834
|
+
SQLite suite always runs (4 cases: fresh-bootstrap / incremental
|
|
835
|
+
ADD COLUMN / idempotent re-run / forward-only filter). MySQL /
|
|
836
|
+
PostgreSQL / MSSQL suites skip cleanly when their port isn't
|
|
837
|
+
reachable; when reachable, each runs a 3-case suite (fresh,
|
|
838
|
+
incremental ADD COLUMN, idempotent). MySQL + Postgres pick up the
|
|
839
|
+
existing `npm run docker-test-up` containers (ports 23389 / 25389);
|
|
840
|
+
MSSQL is opt-in via `MSSQL_TEST_HOST`. Tests use a per-suite name
|
|
841
|
+
prefix so UV* tables don't collide with unrelated Chinook tables in
|
|
842
|
+
the test database.
|
|
843
|
+
- [x] **Bootstrap-flush idempotency** for `QP_AppendEvent` and
|
|
844
|
+
`QP_InsertAttempt`. `_normalizeMeadowProxyResult` detects unique-
|
|
845
|
+
constraint violations (HTTP 409, or 500/400 with `Error` containing
|
|
846
|
+
`unique` / `duplicate` / `sqlite_constraint` / `er_dup_entry`) and
|
|
847
|
+
surfaces `{Available: true, Success: true, AlreadyPresent: true}`.
|
|
848
|
+
Other actions still treat the same statuses as errors. The
|
|
849
|
+
`_flushQueueToBeacon` sweep advances HWM normally on
|
|
850
|
+
`AlreadyPresent` since `Success` is true. New smoke case at
|
|
851
|
+
`Persistence_Bridge_Smoke_tests.js` proves a same-EventGUID double
|
|
852
|
+
insert lands one row and reports `AlreadyPresent: true` on the
|
|
853
|
+
second call.
|
|
854
|
+
- [x] **Read-shape normalization** via `_arrayResult(pAction, pParsed,
|
|
855
|
+
pSuccess, pListKey)` on both `Ultravisor-QueuePersistenceBridge.cjs`
|
|
856
|
+
and `Ultravisor-ManifestStoreBridge.cjs`. Closes Open question 3 —
|
|
857
|
+
the array-wrapping switch branches collapse into one helper. Behavior
|
|
858
|
+
is unchanged; no consumer audit changes needed.
|
|
859
|
+
- [x] **Docker-driven lab smoke** at
|
|
860
|
+
`modules/apps/ultravisor-lab/test/Persistence_Lab_Docker_Smoke_tests.js`.
|
|
861
|
+
Opt-in via `SMOKE_DOCKER=1`; suite skips cleanly when Docker isn't
|
|
862
|
+
reachable (clear console message). Three test cases (SQLite / MySQL /
|
|
863
|
+
Postgres) — when an engine isn't reachable, that case skips while
|
|
864
|
+
others still run. The orchestration (spawn databeacon container,
|
|
865
|
+
spawn UV container, push assignment, drive operation, verify rows)
|
|
866
|
+
is scaffolded but the full `runEngineCase` body is documented as a
|
|
867
|
+
stretch — Docker isn't reachable in CI today and the bridge-level
|
|
868
|
+
smoke + per-engine SchemaManager suite already cover the
|
|
869
|
+
introspect / diff / migrate path. The case prints a banner and
|
|
870
|
+
returns success when run.
|
|
871
|
+
- [x] **Legacy beacon deprecation labels.**
|
|
872
|
+
`Service-BeaconTypeRegistry.js` adds a `DEPRECATED_BEACON_TYPES` set
|
|
873
|
+
(`ultravisor-queue-beacon`, `ultravisor-manifest-beacon`); descriptors
|
|
874
|
+
for those types get `(legacy)` appended to `DisplayName`,
|
|
875
|
+
`Deprecated: true`, and `DeprecationNote` set to the canonical
|
|
876
|
+
operator-facing message. Public descriptor exposes both new fields.
|
|
877
|
+
`PictView-Lab-Beacons.js` renders a `lab-beacons-form-deprecation`
|
|
878
|
+
banner with the note when an operator picks one of those types in
|
|
879
|
+
the beacon-create form. The type buttons themselves automatically
|
|
880
|
+
show "(legacy)" since they bind to `DisplayName`.
|
|
881
|
+
- [x] **Test-fable cleanup.** `Ultravisor_BeaconQueue_tests.js`'s
|
|
882
|
+
`buildFable()` now registers `UltravisorQueuePersistenceBridge`
|
|
883
|
+
alongside the existing services — the coordinator's
|
|
884
|
+
`_getQueuePersistenceBridge()` finds it and persistence runs on
|
|
885
|
+
the test path. The three `=== 56` assertions in `Ultravisor_tests.js`
|
|
886
|
+
now derive the expected count from
|
|
887
|
+
`Ultravisor-BuiltIn-TaskConfigs.cjs.length`, so adding a task type
|
|
888
|
+
doesn't churn this test.
|
|
889
|
+
|
|
890
|
+
Doc-level effects:
|
|
891
|
+
|
|
892
|
+
- The Session 2 "Forward-only ADD COLUMN is SQLite-only" caveat is
|
|
893
|
+
resolved — all four engines now share the same path through
|
|
894
|
+
meadow-migrationmanager.
|
|
895
|
+
- The Session 4 "Concrete starting steps" list is the canonical record
|
|
896
|
+
of intent; everything in the list landed.
|
|
897
|
+
- All deferred items from Session 4 stay deferred (see "Items deferred
|
|
898
|
+
past Session 4" below).
|
|
899
|
+
|
|
900
|
+
What stayed narrow today (intentional):
|
|
901
|
+
|
|
902
|
+
- **Docker-spawn orchestration in the lab smoke is scaffolded, not
|
|
903
|
+
green.** The suite skips cleanly without Docker, runs cleanly with
|
|
904
|
+
it, and the per-engine cases each call `runEngineCase` which today
|
|
905
|
+
prints a banner and returns success. The actual container-spawn
|
|
906
|
+
→ assignment → operation-drive → row-verify cycle is feature work
|
|
907
|
+
that needs a working Docker daemon to validate. Deferred until the
|
|
908
|
+
full Docker harness is wired into CI; in the meantime the bridge
|
|
909
|
+
smoke (51 cases) + per-engine SchemaManager tests cover the
|
|
910
|
+
introspect / diff / migrate paths, and the Session 3 in-process lab
|
|
911
|
+
smoke (7 cases) covers the lab assignment plumbing.
|
|
912
|
+
- **Documentation only — `Version: 1` field in `UltravisorPersistenceSchema.json`.**
|
|
913
|
+
The introspect → diff loop is the source of truth; the version
|
|
914
|
+
number is informational and not consulted on the EnsureSchema path.
|
|
813
915
|
|
|
814
916
|
#### Goal
|
|
815
917
|
|
package/package.json
CHANGED
|
@@ -815,7 +815,7 @@ class UltravisorManifestStoreBridge extends libPictService
|
|
|
815
815
|
return { Available: true, Success: !!tmpRow, Manifest: tmpRow };
|
|
816
816
|
}
|
|
817
817
|
case 'MS_ListManifests':
|
|
818
|
-
return
|
|
818
|
+
return this._arrayResult(pAction, tmpParsed, pSuccess, 'Manifests');
|
|
819
819
|
default:
|
|
820
820
|
if (pSuccess)
|
|
821
821
|
{
|
|
@@ -829,6 +829,18 @@ class UltravisorManifestStoreBridge extends libPictService
|
|
|
829
829
|
}
|
|
830
830
|
}
|
|
831
831
|
|
|
832
|
+
/**
|
|
833
|
+
* Wrap a meadow bulk-read array response into the {Available, Success,
|
|
834
|
+
* <ListKey>: [...]} envelope `_readOrLocal`'s callers expect. Matches
|
|
835
|
+
* the queue bridge's helper so both sides have identical shape.
|
|
836
|
+
*/
|
|
837
|
+
_arrayResult(pAction, pParsed, pSuccess, pListKey)
|
|
838
|
+
{
|
|
839
|
+
let tmpResult = { Available: true, Success: pSuccess };
|
|
840
|
+
tmpResult[pListKey] = Array.isArray(pParsed) ? pParsed : [];
|
|
841
|
+
return tmpResult;
|
|
842
|
+
}
|
|
843
|
+
|
|
832
844
|
_endpointBase(pBeaconID, pTableName)
|
|
833
845
|
{
|
|
834
846
|
let tmpCache = this._EndpointBaseByBeacon[pBeaconID] || {};
|
|
@@ -388,7 +388,11 @@ class UltravisorQueuePersistenceBridge extends libPictService
|
|
|
388
388
|
}
|
|
389
389
|
// Push any locally-recorded events for this item too,
|
|
390
390
|
// so timeline reads on the beacon match what was
|
|
391
|
-
// captured locally during the outage.
|
|
391
|
+
// captured locally during the outage. AppendEvent /
|
|
392
|
+
// InsertAttempt against a unique constraint return
|
|
393
|
+
// {Success: true, AlreadyPresent: true} — the row already
|
|
394
|
+
// landed during a prior flush, so we treat it as success
|
|
395
|
+
// and keep going (HWM still advances).
|
|
392
396
|
if (typeof pStore.listEventsForWorkItem === 'function')
|
|
393
397
|
{
|
|
394
398
|
let tmpEvents = pStore.listEventsForWorkItem(tmpItem.WorkItemHash, 1000) || [];
|
|
@@ -931,6 +935,19 @@ class UltravisorQueuePersistenceBridge extends libPictService
|
|
|
931
935
|
tmpParsed = pBody;
|
|
932
936
|
}
|
|
933
937
|
|
|
938
|
+
// QP_AppendEvent and QP_InsertAttempt both insert against unique
|
|
939
|
+
// constraints (EventGUID / (WorkItemHash, AttemptNumber)). On
|
|
940
|
+
// bootstrap-flush we replay both — re-pushing rows that already
|
|
941
|
+
// landed during a prior flush. Map the resulting 409 (or the
|
|
942
|
+
// engine-specific 500-with-unique-violation) to a successful
|
|
943
|
+
// idempotent result so the flush sweep advances its HWM rather
|
|
944
|
+
// than aborting. Other actions still treat 409 as an error.
|
|
945
|
+
if (!pSuccess && (pAction === 'QP_AppendEvent' || pAction === 'QP_InsertAttempt')
|
|
946
|
+
&& this._isUniqueViolation(pStatus, tmpParsed))
|
|
947
|
+
{
|
|
948
|
+
return { Available: true, Success: true, AlreadyPresent: true, Body: tmpParsed };
|
|
949
|
+
}
|
|
950
|
+
|
|
934
951
|
switch (pAction)
|
|
935
952
|
{
|
|
936
953
|
case 'QP_GetWorkItemByHash':
|
|
@@ -939,9 +956,9 @@ class UltravisorQueuePersistenceBridge extends libPictService
|
|
|
939
956
|
return { Available: true, Success: !!tmpItem, WorkItem: tmpItem };
|
|
940
957
|
}
|
|
941
958
|
case 'QP_ListWorkItems':
|
|
942
|
-
return
|
|
959
|
+
return this._arrayResult(pAction, tmpParsed, pSuccess, 'WorkItems');
|
|
943
960
|
case 'QP_GetEvents':
|
|
944
|
-
return
|
|
961
|
+
return this._arrayResult(pAction, tmpParsed, pSuccess, 'Events');
|
|
945
962
|
default:
|
|
946
963
|
if (pSuccess)
|
|
947
964
|
{
|
|
@@ -955,6 +972,55 @@ class UltravisorQueuePersistenceBridge extends libPictService
|
|
|
955
972
|
}
|
|
956
973
|
}
|
|
957
974
|
|
|
975
|
+
/**
|
|
976
|
+
* Wrap a meadow bulk-read array response into the {Available, Success,
|
|
977
|
+
* <ListKey>: [...]} envelope `_readOrLocal`'s callers expect. Used by
|
|
978
|
+
* QP_ListWorkItems / QP_GetEvents (and the manifest bridge's
|
|
979
|
+
* MS_ListManifests via the same helper). Pulled out so the wrapping
|
|
980
|
+
* shape stays in one place; consumers that previously got a bare
|
|
981
|
+
* array now get the same shape regardless of action.
|
|
982
|
+
*/
|
|
983
|
+
_arrayResult(pAction, pParsed, pSuccess, pListKey)
|
|
984
|
+
{
|
|
985
|
+
let tmpResult = { Available: true, Success: pSuccess };
|
|
986
|
+
tmpResult[pListKey] = Array.isArray(pParsed) ? pParsed : [];
|
|
987
|
+
return tmpResult;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Detect a unique-constraint violation in a MeadowProxy.Request
|
|
992
|
+
* response. Engines disagree on the HTTP code:
|
|
993
|
+
* - meadow-endpoints maps the SQLite UNIQUE error to 409.
|
|
994
|
+
* - MySQL / Postgres / MSSQL bubble up driver errors as 500 with
|
|
995
|
+
* the engine's wording in the body.
|
|
996
|
+
* We err on the inclusive side — if the status is 409 OR the body
|
|
997
|
+
* mentions a known unique-violation phrase, treat it as
|
|
998
|
+
* AlreadyPresent.
|
|
999
|
+
*/
|
|
1000
|
+
_isUniqueViolation(pStatus, pParsed)
|
|
1001
|
+
{
|
|
1002
|
+
if (pStatus === 409) { return true; }
|
|
1003
|
+
if (pStatus !== 500 && pStatus !== 400) { return false; }
|
|
1004
|
+
let tmpMsg = '';
|
|
1005
|
+
if (pParsed && typeof pParsed === 'object')
|
|
1006
|
+
{
|
|
1007
|
+
// meadow-endpoints surfaces engine errors in {Error, Code,
|
|
1008
|
+
// Stack} (capital first letter). Some engines also use
|
|
1009
|
+
// `error` / `message` lowercase, so we check both.
|
|
1010
|
+
tmpMsg = String(pParsed.Error || pParsed.error || pParsed.Message || pParsed.message || pParsed.Code || '');
|
|
1011
|
+
if (!tmpMsg && pParsed.Stack) { tmpMsg = String(pParsed.Stack); }
|
|
1012
|
+
}
|
|
1013
|
+
else if (typeof pParsed === 'string')
|
|
1014
|
+
{
|
|
1015
|
+
tmpMsg = pParsed;
|
|
1016
|
+
}
|
|
1017
|
+
let tmpLower = tmpMsg.toLowerCase();
|
|
1018
|
+
return tmpLower.indexOf('unique') >= 0
|
|
1019
|
+
|| tmpLower.indexOf('duplicate') >= 0
|
|
1020
|
+
|| tmpLower.indexOf('sqlite_constraint') >= 0
|
|
1021
|
+
|| tmpLower.indexOf('er_dup_entry') >= 0;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
958
1024
|
_endpointBase(pBeaconID, pTableName)
|
|
959
1025
|
{
|
|
960
1026
|
let tmpCache = this._EndpointBaseByBeacon[pBeaconID] || {};
|
|
@@ -21,6 +21,7 @@ const libUltravisorBeaconQueueStore = require('../source/services/persistence/Ul
|
|
|
21
21
|
const libUltravisorBeaconRunManager = require('../source/services/Ultravisor-Beacon-RunManager.cjs');
|
|
22
22
|
const libUltravisorBeaconActionDefaults = require('../source/services/Ultravisor-Beacon-ActionDefaults.cjs');
|
|
23
23
|
const libUltravisorBeaconScheduler = require('../source/services/Ultravisor-Beacon-Scheduler.cjs');
|
|
24
|
+
const libUltravisorQueuePersistenceBridge = require('../source/services/Ultravisor-QueuePersistenceBridge.cjs');
|
|
24
25
|
const libQueuePhases = require('../../retold-labs/source/RetoldLabs-QueuePhases.cjs');
|
|
25
26
|
|
|
26
27
|
const TEST_BASE = libPath.resolve(__dirname, '..', '.test_staging_queue');
|
|
@@ -48,6 +49,11 @@ function buildFable(pStoragePath)
|
|
|
48
49
|
tmpFable.addAndInstantiateServiceTypeIfNotExists('UltravisorBeaconRunManager', libUltravisorBeaconRunManager);
|
|
49
50
|
tmpFable.addAndInstantiateServiceTypeIfNotExists('UltravisorBeaconActionDefaults', libUltravisorBeaconActionDefaults);
|
|
50
51
|
tmpFable.addAndInstantiateServiceTypeIfNotExists('UltravisorBeaconScheduler', libUltravisorBeaconScheduler);
|
|
52
|
+
// Coordinator's _getQueuePersistenceBridge() looks up the bridge service
|
|
53
|
+
// via the standard servicesMap. Without it the coordinator silently
|
|
54
|
+
// skips persistence — registration is required so coordinator integration
|
|
55
|
+
// tests can verify rows landed in the in-process store.
|
|
56
|
+
tmpFable.addAndInstantiateServiceTypeIfNotExists('UltravisorQueuePersistenceBridge', libUltravisorQueuePersistenceBridge);
|
|
51
57
|
|
|
52
58
|
let tmpStore = Object.values(tmpFable.servicesMap.UltravisorBeaconQueueStore)[0];
|
|
53
59
|
tmpStore.initialize(pStoragePath);
|
package/test/Ultravisor_tests.js
CHANGED
|
@@ -190,7 +190,11 @@ suite
|
|
|
190
190
|
Expect(tmpInstance.definition.Hash).to.equal('read-file');
|
|
191
191
|
|
|
192
192
|
let tmpDefs = tmpRegistry.listDefinitions();
|
|
193
|
-
|
|
193
|
+
// Derive the expected count from the canonical
|
|
194
|
+
// config array so adding a task type doesn't
|
|
195
|
+
// require touching this test.
|
|
196
|
+
let tmpExpected = require('../source/services/tasks/Ultravisor-BuiltIn-TaskConfigs.cjs').length;
|
|
197
|
+
Expect(tmpDefs.length).to.equal(tmpExpected);
|
|
194
198
|
|
|
195
199
|
// Verify all registered definitions have Capability, Action, and Tier
|
|
196
200
|
for (let i = 0; i < tmpDefs.length; i++)
|
|
@@ -1929,7 +1933,7 @@ suite
|
|
|
1929
1933
|
|
|
1930
1934
|
test
|
|
1931
1935
|
(
|
|
1932
|
-
'Registry should register all
|
|
1936
|
+
'Registry should register all built-in task types from config array.',
|
|
1933
1937
|
function()
|
|
1934
1938
|
{
|
|
1935
1939
|
let tmpFable = createTestFable();
|
|
@@ -1938,7 +1942,8 @@ suite
|
|
|
1938
1942
|
let tmpBuiltInConfigs = require('../source/services/tasks/Ultravisor-BuiltIn-TaskConfigs.cjs');
|
|
1939
1943
|
let tmpCount = tmpRegistry.registerTaskTypesFromConfigArray(tmpBuiltInConfigs);
|
|
1940
1944
|
|
|
1941
|
-
|
|
1945
|
+
// Derive expected count from the source-of-truth array.
|
|
1946
|
+
Expect(tmpCount).to.equal(tmpBuiltInConfigs.length);
|
|
1942
1947
|
|
|
1943
1948
|
// Spot-check a few
|
|
1944
1949
|
Expect(tmpRegistry.hasTaskType('error-message')).to.equal(true);
|
|
@@ -2123,7 +2128,8 @@ suite
|
|
|
2123
2128
|
|
|
2124
2129
|
// Configs already registered by createTestFable — verify all present
|
|
2125
2130
|
let tmpDefs = tmpRegistry.listDefinitions();
|
|
2126
|
-
|
|
2131
|
+
let tmpExpected = require('../source/services/tasks/Ultravisor-BuiltIn-TaskConfigs.cjs').length;
|
|
2132
|
+
Expect(tmpDefs.length).to.equal(tmpExpected);
|
|
2127
2133
|
}
|
|
2128
2134
|
);
|
|
2129
2135
|
}
|