@7365admin1/core 3.137.0 → 3.139.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.
- package/CHANGELOG.md +12 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.js +439 -99
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +439 -99
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/e2e/service-provider-invite-email-new-company.e2e.test.mjs +105 -1
- package/test/member-working-membership.test.mjs +28 -0
- package/test/ounitech-api-2025-10.test.mjs +51 -8
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7365admin1/core",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.139.0",
|
|
5
5
|
"author": "7365admin1",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"build": "tsup src/index.ts --format cjs,esm --dts && shx cp -r src/public dist && shx cp -r src/utils/handlebars dist",
|
|
14
14
|
"release": "yarn run build && changeset publish",
|
|
15
15
|
"lint": "tsc",
|
|
16
|
-
"test": "tsup src/config.ts src/utils/building-map.util.ts src/utils/robot-cloud.util.ts src/utils/robot-adapter.util.ts src/utils/png-encoder.util.ts src/services/demo.adapter.ts src/services/ounitech.adapter.ts src/services/robot-cloud.service.ts src/services/robot-fleet.service.ts src/services/padbot.adapter.ts src/utils/padbot-sign.util.ts src/utils/padbot-mqtt.util.ts src/services/open-rmf.adapter.ts src/utils/robot-credentials.util.ts src/utils/rmf-building.util.ts src/utils/door-contract.util.ts src/utils/robot-fault.util.ts src/utils/robot-tracking.util.ts src/utils/robot-apps.util.ts src/utils/robot-permission.util.ts src/utils/robot-network.util.ts src/utils/lift-contract.util.ts src/models/robot-building-map.model.ts src/utils/anpr-revoke.util.ts src/utils/anpr-plate.util.ts src/utils/vehicle-request.util.ts src/utils/camera-view.util.ts src/utils/camera-bulk-import.util.ts src/utils/dashboard-metrics.util.ts src/utils/camera-capability.util.ts src/utils/camera-playback.util.ts src/services/camera-device-http.service.ts src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts src/utils/site-name.util.ts src/utils/site-owner-edit.util.ts src/utils/service-provider-invite.util.ts src/utils/notification-category.util.ts src/utils/notification-kind.util.ts src/utils/notification-email.util.ts src/models/notification.model.ts src/utils/hid-card.util.ts src/utils/hid-json.util.ts src/utils/hid-access.util.ts src/models/customer-site.model.ts src/models/site-camera.model.ts src/models/site.model.ts src/models/member.model.ts src/models/subscription.model.ts src/utils/session-id.util.ts src/utils/occurrence-query.util.ts src/utils/occurrence-book-create.util.ts src/utils/update-result.util.ts src/utils/hid-amico-authz.util.ts src/utils/promo-code-currency.util.ts src/utils/console-audit.util.ts src/utils/org-suspension.util.ts src/utils/session-revoke.util.ts src/models/hid-amico.model.ts src/models/patrol-email.model.ts src/utils/patrol-email-pdf.util.ts src/utils/visitor-status.util.ts src/utils/self-service-email.util.ts src/utils/hid-secret.util.ts src/models/visitor-invite.model.ts src/models/guard-invite.model.ts src/utils/site-timezone.util.ts src/utils/bulletin-service-apps.util.ts src/models/bulletin-board.model.ts src/utils/vehicle-import.util.ts src/utils/expired-vehicle-sweep.util.ts src/models/vehicle.model.ts src/controllers/chat.controller.ts src/utils/file-owner.util.ts src/utils/console-permission.util.ts src/utils/role-scope.util.ts src/utils/org-default-roles.util.ts src/utils/org-modules.util.ts src/utils/site-modules.util.ts src/utils/resident-app-modules.util.ts src/utils/resident-default-role.util.ts src/utils/invite-role-scope.util.ts src/models/personal-emergency-chain.model.ts src/utils/self-signup-files.util.ts src/models/person.model.ts src/utils/module-gate.util.ts src/utils/member-lookup.util.ts src/utils/notification-access.util.ts src/utils/module-access.util.ts src/services/module-gate.service.ts src/controllers/site.controller.ts src/controllers/organization.controller.ts src/controllers/member.controller.ts src/utils/org-owner-repair.util.ts src/utils/socket-session.util.ts src/utils/people-create-gate.util.ts src/utils/service-app-type.util.ts src/utils/customer-site-list.util.ts src/utils/org-search.util.ts src/utils/invite-origin.util.ts src/utils/member-role-assign.util.ts src/utils/cross-site-role.util.ts src/utils/cross-site-role-fix.util.ts src/utils/visitor-search.util.ts --format esm --out-dir test/.build --no-dts --silent && node --test test/*.test.mjs",
|
|
16
|
+
"test": "tsup src/config.ts src/utils/building-map.util.ts src/utils/robot-cloud.util.ts src/utils/robot-adapter.util.ts src/utils/png-encoder.util.ts src/services/demo.adapter.ts src/services/ounitech.adapter.ts src/utils/ounitech-codes.util.ts src/services/robot-cloud.service.ts src/services/robot-fleet.service.ts src/services/padbot.adapter.ts src/utils/padbot-sign.util.ts src/utils/padbot-mqtt.util.ts src/services/open-rmf.adapter.ts src/utils/robot-credentials.util.ts src/utils/rmf-building.util.ts src/utils/door-contract.util.ts src/utils/robot-fault.util.ts src/utils/robot-tracking.util.ts src/utils/robot-apps.util.ts src/utils/robot-permission.util.ts src/utils/robot-network.util.ts src/utils/lift-contract.util.ts src/models/robot-building-map.model.ts src/utils/anpr-revoke.util.ts src/utils/anpr-plate.util.ts src/utils/vehicle-request.util.ts src/utils/camera-view.util.ts src/utils/camera-bulk-import.util.ts src/utils/dashboard-metrics.util.ts src/utils/camera-capability.util.ts src/utils/camera-playback.util.ts src/services/camera-device-http.service.ts src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts src/utils/site-name.util.ts src/utils/site-owner-edit.util.ts src/utils/service-provider-invite.util.ts src/utils/notification-category.util.ts src/utils/notification-kind.util.ts src/utils/notification-email.util.ts src/models/notification.model.ts src/utils/hid-card.util.ts src/utils/hid-json.util.ts src/utils/hid-access.util.ts src/models/customer-site.model.ts src/models/site-camera.model.ts src/models/site.model.ts src/models/member.model.ts src/models/subscription.model.ts src/utils/session-id.util.ts src/utils/occurrence-query.util.ts src/utils/occurrence-book-create.util.ts src/utils/update-result.util.ts src/utils/hid-amico-authz.util.ts src/utils/promo-code-currency.util.ts src/utils/console-audit.util.ts src/utils/org-suspension.util.ts src/utils/session-revoke.util.ts src/models/hid-amico.model.ts src/models/patrol-email.model.ts src/utils/patrol-email-pdf.util.ts src/utils/visitor-status.util.ts src/utils/self-service-email.util.ts src/utils/hid-secret.util.ts src/models/visitor-invite.model.ts src/models/guard-invite.model.ts src/utils/site-timezone.util.ts src/utils/bulletin-service-apps.util.ts src/models/bulletin-board.model.ts src/utils/vehicle-import.util.ts src/utils/expired-vehicle-sweep.util.ts src/models/vehicle.model.ts src/controllers/chat.controller.ts src/utils/file-owner.util.ts src/utils/console-permission.util.ts src/utils/role-scope.util.ts src/utils/org-default-roles.util.ts src/utils/org-modules.util.ts src/utils/site-modules.util.ts src/utils/resident-app-modules.util.ts src/utils/resident-default-role.util.ts src/utils/invite-role-scope.util.ts src/models/personal-emergency-chain.model.ts src/utils/self-signup-files.util.ts src/models/person.model.ts src/utils/module-gate.util.ts src/utils/member-lookup.util.ts src/utils/notification-access.util.ts src/utils/module-access.util.ts src/services/module-gate.service.ts src/controllers/site.controller.ts src/controllers/organization.controller.ts src/controllers/member.controller.ts src/utils/org-owner-repair.util.ts src/utils/socket-session.util.ts src/utils/people-create-gate.util.ts src/utils/service-app-type.util.ts src/utils/customer-site-list.util.ts src/utils/org-search.util.ts src/utils/invite-origin.util.ts src/utils/member-role-assign.util.ts src/utils/cross-site-role.util.ts src/utils/cross-site-role-fix.util.ts src/utils/visitor-search.util.ts --format esm --out-dir test/.build --no-dts --silent && node --test test/*.test.mjs",
|
|
17
17
|
"test:camera": "yarn test",
|
|
18
18
|
"test:e2e": "yarn build && tsup src/repositories/visitor-transaction.repo.ts --format esm --out-dir test/.build-e2e/repositories --no-dts --silent && node --test --test-concurrency=1 --test-timeout=600000 \"test/e2e/*.test.mjs\""
|
|
19
19
|
},
|
|
@@ -354,12 +354,15 @@ describe("SP invitation: email status, Super Admin alert, new company typed by s
|
|
|
354
354
|
|
|
355
355
|
const repaired = await h.api(`/service-provider-invitations/${inviteId}/wrong-join/repair`, { method: "PUT", sid: adminSid });
|
|
356
356
|
assert.equal(repaired.status, 200, JSON.stringify(repaired.body));
|
|
357
|
-
|
|
357
|
+
// 2 memberships + the unused "Admin Service Provider" role left in JLL's role list
|
|
358
|
+
assert.equal(repaired.body.changed, 3);
|
|
358
359
|
assert.equal(await h.db.collection("members").countDocuments({ user: user._id, status: "active" }), 0);
|
|
360
|
+
assert.equal((await h.db.collection("roles").findOne({ _id: spRole })).status, "deleted");
|
|
359
361
|
|
|
360
362
|
const undone = await h.api(`/service-provider-invitations/${inviteId}/wrong-join/undo`, { method: "PUT", sid: adminSid });
|
|
361
363
|
assert.equal(undone.status, 200, JSON.stringify(undone.body));
|
|
362
364
|
assert.equal(await h.db.collection("members").countDocuments({ user: user._id, status: "active" }), 2);
|
|
365
|
+
assert.equal((await h.db.collection("roles").findOne({ _id: spRole })).status, "active", "undo restores the role");
|
|
363
366
|
|
|
364
367
|
await h.api(`/service-provider-invitations/${inviteId}/wrong-join/repair`, { method: "PUT", sid: adminSid });
|
|
365
368
|
const again = await h.api(`/service-provider-invitations/${inviteId}/wrong-join`, { sid: adminSid });
|
|
@@ -523,6 +526,107 @@ describe("SP invitation: email status, Super Admin alert, new company typed by s
|
|
|
523
526
|
assert.ok(await h.db.collection("customer.sites").findOne({ org: org._id, site, status: "active" }));
|
|
524
527
|
assert.equal((await h.db.collection("verifications").findOne({ _id: new ObjectId(inviteId) })).status, "complete");
|
|
525
528
|
});
|
|
529
|
+
|
|
530
|
+
// ------------------- E. "two organisations" after Finish setup (2026-09-30)
|
|
531
|
+
|
|
532
|
+
// latitude.sec@ on production after Repair + Finish setup: two INACTIVE
|
|
533
|
+
// JLL memberships (older) + the unused "*" role the old sign-up left in JLL,
|
|
534
|
+
// and its own Henderson Security company with org-level owner rows (no site)
|
|
535
|
+
// engaged at Latitude through customer.sites.
|
|
536
|
+
async function latitudeShape(email) {
|
|
537
|
+
const site = new ObjectId();
|
|
538
|
+
await h.db.collection("sites").insertOne({ _id: site, name: "Latitude", orgId: id.pmOrg, status: "active" });
|
|
539
|
+
const inviteId = await invite(email, "security_agency", site);
|
|
540
|
+
await approve(inviteId);
|
|
541
|
+
const sid = await newAccount(email);
|
|
542
|
+
const user = await h.db.collection("users").findOne({ email });
|
|
543
|
+
const spRole = new ObjectId();
|
|
544
|
+
await h.db.collection("roles").insertOne({ _id: spRole, name: "Admin Service Provider", type: "security_agency", org: id.pmOrg.toString(), site: site.toString(), permissions: ["*"], status: "active", createdAt: new Date(Date.now() + 500).toISOString() });
|
|
545
|
+
const later = new Date(Date.now() + 1000);
|
|
546
|
+
await h.db.collection("members").insertMany([
|
|
547
|
+
{ user: user._id, org: id.pmOrg, role: spRole, type: "organization", siteId: site, siteName: "Latitude", status: "active", createdAt: later },
|
|
548
|
+
{ user: user._id, org: id.pmOrg, role: spRole, type: "security_agency", siteId: site, siteName: "Latitude", status: "active", createdAt: later },
|
|
549
|
+
]);
|
|
550
|
+
await h.db.collection("users").updateOne({ _id: user._id }, { $set: { defaultOrg: id.pmOrg.toString() } });
|
|
551
|
+
// the 30 Sep Repair as it ran then: memberships only (the role stayed)
|
|
552
|
+
await h.db.collection("members").updateMany({ user: user._id, org: id.pmOrg }, { $set: { status: "inactive" } });
|
|
553
|
+
await h.db.collection("users").updateOne({ _id: user._id }, { $unset: { defaultOrg: "" } });
|
|
554
|
+
const applied = await h.api(`/service-provider-invitations/${inviteId}/finish-setup`, { method: "PUT", sid: adminSid, body: { companyName: "Henderson Security Services Pte Ltd" } });
|
|
555
|
+
assert.equal(applied.status, 200, JSON.stringify(applied.body));
|
|
556
|
+
return { site, inviteId, sid, user, spRole, orgId: applied.body.orgId };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
it("E1. every app finds the account's OWN company: membership lookup, organisation switcher, sites", async () => {
|
|
560
|
+
const { site, sid, user, orgId } = await latitudeShape("latitude-shape@e2e.example.com");
|
|
561
|
+
for (const app of ["organization", "security_agency"]) {
|
|
562
|
+
const m = await h.api(`/members/user/${user._id}/app/${app}`, { sid });
|
|
563
|
+
assert.equal(m.status, 200, JSON.stringify(m.body));
|
|
564
|
+
assert.equal(String(m.body.org), orgId, `${app}: Henderson, not the inactive JLL row`);
|
|
565
|
+
assert.equal(m.body.status, "active");
|
|
566
|
+
assert.ok(!m.body.siteId, "org-level owner row: no site of its own");
|
|
567
|
+
}
|
|
568
|
+
const orgs = await h.api(`/organizations/user/${user._id}?type=organization&limit=50`, { sid });
|
|
569
|
+
assert.equal(orgs.status, 200, JSON.stringify(orgs.body));
|
|
570
|
+
assert.deepEqual(orgs.body.items.map((o) => String(o.value)), [orgId], "one organisation, not JLL beside it");
|
|
571
|
+
// the security app's (and the mobile apps') site list: the engaged site
|
|
572
|
+
const sites = await h.api(`/customer-sites?org=${orgId}&status=active&limit=50`, { sid });
|
|
573
|
+
assert.equal(sites.status, 200, JSON.stringify(sites.body));
|
|
574
|
+
assert.deepEqual(sites.body.items.map((x) => String(x.site)), [site.toString()]);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("E2. Repair on an already-repaired account retires the leftover full-access role only while nobody uses it; Undo restores it", async () => {
|
|
578
|
+
const { inviteId, spRole, user } = await latitudeShape("latitude-role@e2e.example.com");
|
|
579
|
+
const preview = await h.api(`/service-provider-invitations/${inviteId}/wrong-join`, { sid: adminSid });
|
|
580
|
+
assert.equal(preview.status, 200, JSON.stringify(preview.body));
|
|
581
|
+
assert.equal(preview.body.memberships.length, 0);
|
|
582
|
+
assert.deepEqual(preview.body.leftoverRoles.map((r) => r.name), ["Admin Service Provider"]);
|
|
583
|
+
|
|
584
|
+
// someone at JLL was since given that role -> it is JLL's now, never touched
|
|
585
|
+
const other = await h.db.collection("members").insertOne({ user: new ObjectId(), org: id.pmOrg, role: spRole, type: "security_agency", status: "active", createdAt: new Date() });
|
|
586
|
+
const kept = await h.api(`/service-provider-invitations/${inviteId}/wrong-join/repair`, { method: "PUT", sid: adminSid });
|
|
587
|
+
assert.equal(kept.body.changed, 0, JSON.stringify(kept.body));
|
|
588
|
+
assert.equal((await h.db.collection("roles").findOne({ _id: spRole })).status, "active");
|
|
589
|
+
await h.db.collection("members").deleteOne({ _id: other.insertedId });
|
|
590
|
+
|
|
591
|
+
const repaired = await h.api(`/service-provider-invitations/${inviteId}/wrong-join/repair`, { method: "PUT", sid: adminSid });
|
|
592
|
+
assert.equal(repaired.status, 200, JSON.stringify(repaired.body));
|
|
593
|
+
assert.equal(repaired.body.changed, 1);
|
|
594
|
+
assert.equal((await h.db.collection("roles").findOne({ _id: spRole })).status, "deleted");
|
|
595
|
+
// Henderson is untouched
|
|
596
|
+
assert.equal(await h.db.collection("members").countDocuments({ user: user._id, status: "active" }), 2);
|
|
597
|
+
|
|
598
|
+
const undone = await h.api(`/service-provider-invitations/${inviteId}/wrong-join/undo`, { method: "PUT", sid: adminSid });
|
|
599
|
+
assert.equal(undone.status, 200, JSON.stringify(undone.body));
|
|
600
|
+
assert.equal((await h.db.collection("roles").findOne({ _id: spRole })).status, "active");
|
|
601
|
+
// Undo of a role-only repair re-activates no membership
|
|
602
|
+
assert.equal(await h.db.collection("members").countDocuments({ user: user._id, org: id.pmOrg, status: "active" }), 0);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it("E3. accept-new-company is idempotent per invitation: a retry after a half-finished attempt reuses the company; a second click is refused; one company", async () => {
|
|
606
|
+
const site = new ObjectId();
|
|
607
|
+
await h.db.collection("sites").insertOne({ _id: site, name: "Latitude", orgId: id.pmOrg, status: "active" });
|
|
608
|
+
const email = "double-click@e2e.example.com";
|
|
609
|
+
const inviteId = await invite(email, "security_agency", site);
|
|
610
|
+
await approve(inviteId);
|
|
611
|
+
const sid = await newAccount(email);
|
|
612
|
+
const user = await h.db.collection("users").findOne({ email });
|
|
613
|
+
// the first attempt died after creating the organisation (no roles, no members, invite still pending)
|
|
614
|
+
const half = new ObjectId();
|
|
615
|
+
await h.db.collection("organizations").insertOne({ _id: half, name: "Henderson Security Services Pte Ltd", type: "security_agency", nature: "security_agency", email, createdBy: user._id, invitedFrom: inviteId, status: "active", createdAt: new Date().toISOString() });
|
|
616
|
+
|
|
617
|
+
const retry = await h.api(`/service-provider-invitations/${inviteId}/accept-new-company`, { method: "PUT", sid, body: { companyName: "Henderson Security Services Pte Ltd" } });
|
|
618
|
+
assert.equal(retry.status, 200, JSON.stringify(retry.body));
|
|
619
|
+
assert.equal(retry.body.orgId, half.toString(), "the half-made company, not a second one");
|
|
620
|
+
assert.equal(await h.db.collection("organizations").countDocuments({ invitedFrom: inviteId }), 1);
|
|
621
|
+
const owners = await h.db.collection("members").find({ user: user._id, org: half, status: "active" }).toArray();
|
|
622
|
+
assert.deepEqual(owners.map((m) => m.type).sort(), ["organization", "security_agency"]);
|
|
623
|
+
assert.ok(await h.db.collection("customer.sites").findOne({ org: half, site, status: "active" }));
|
|
624
|
+
|
|
625
|
+
const again = await h.api(`/service-provider-invitations/${inviteId}/accept-new-company`, { method: "PUT", sid, body: { companyName: "Henderson Security Services Pte Ltd" } });
|
|
626
|
+
assert.equal(again.status, 400, JSON.stringify(again.body));
|
|
627
|
+
assert.equal(await h.db.collection("organizations").countDocuments({ createdBy: { $in: [user._id, user._id.toString()] } }), 1);
|
|
628
|
+
assert.equal(await h.db.collection("members").countDocuments({ user: user._id, org: half }), 2, "no duplicate owner rows");
|
|
629
|
+
});
|
|
526
630
|
});
|
|
527
631
|
|
|
528
632
|
async function seed(h) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// latitude.sec@ (2026-09-30): an INACTIVE membership in another organisation
|
|
2
|
+
// (the sender, after Repair) must never be the app's membership over the
|
|
3
|
+
// person's live one. See preferWorkingMembership in member-lookup.util.ts.
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { preferWorkingMembership, pickLookupMembership } from "./.build/utils/member-lookup.util.mjs";
|
|
7
|
+
|
|
8
|
+
const jll = { org: "jll", type: "organization", status: "inactive", siteId: "latitude" };
|
|
9
|
+
const hen = { org: "hen", type: "organization", status: "active", siteId: "" };
|
|
10
|
+
const other = { org: "oth", type: "organization", status: "active" };
|
|
11
|
+
|
|
12
|
+
test("a working row beats an older inactive one", () => {
|
|
13
|
+
assert.equal(preferWorkingMembership([jll, hen]), hen);
|
|
14
|
+
});
|
|
15
|
+
test("the default organisation wins among working rows", () => {
|
|
16
|
+
assert.equal(preferWorkingMembership([jll, other, hen], "HEN"), hen);
|
|
17
|
+
assert.equal(preferWorkingMembership([jll, other, hen]), other);
|
|
18
|
+
});
|
|
19
|
+
test("no working row: the old answer (never not-found), deleted last", () => {
|
|
20
|
+
const sus = { org: "a", status: "suspended" };
|
|
21
|
+
const del = { org: "b", status: "deleted" };
|
|
22
|
+
assert.equal(preferWorkingMembership([del, sus]), sus);
|
|
23
|
+
assert.equal(preferWorkingMembership([del]), del);
|
|
24
|
+
assert.equal(preferWorkingMembership([]), null);
|
|
25
|
+
});
|
|
26
|
+
test("the lookup used by the module gate follows the same rule", () => {
|
|
27
|
+
assert.equal(pickLookupMembership([jll, hen], "organization"), hen);
|
|
28
|
+
});
|
|
@@ -6,10 +6,14 @@ import { after, before, test } from "node:test";
|
|
|
6
6
|
|
|
7
7
|
import { ROBOT_CLOUD_PATHS, OUNITECH_TIMESTAMP_SKEW_S } from "./.build/utils/robot-cloud.util.mjs";
|
|
8
8
|
import { RobotCloudError, useRobotCloudService } from "./.build/services/robot-cloud.service.mjs";
|
|
9
|
+
import { OUNITECH_CODES, ounitechCodeWords } from "./.build/utils/ounitech-codes.util.mjs";
|
|
10
|
+
import { registerRobotBrand, robotManifest } from "./.build/utils/robot-adapter.util.mjs";
|
|
11
|
+
import { robotFaultText } from "./.build/utils/robot-fault.util.mjs";
|
|
9
12
|
import {
|
|
10
13
|
createOunitechAdapter,
|
|
11
14
|
ounitechRobotFromHomeRow,
|
|
12
15
|
ounitechFault,
|
|
16
|
+
OUNITECH_BRAND_DEFINITION,
|
|
13
17
|
ounitechJobWords,
|
|
14
18
|
OUNITECH_FRESH_MS,
|
|
15
19
|
ounitechMapShare,
|
|
@@ -313,7 +317,7 @@ test("status 7 costs one extra read, get-robot-state-v2.0 by robotId, to name th
|
|
|
313
317
|
const r = await adapter().getRobot("ERROR1");
|
|
314
318
|
assert.deepEqual(seen.map((s) => s.path), [ROBOT_CLOUD_PATHS.homeRobotList, ROBOT_CLOUD_PATHS.robotState]);
|
|
315
319
|
assert.equal(seen[1].body.robotId, 427);
|
|
316
|
-
assert.deepEqual(r.fault, { code: "621", message: "Right wheel driver reset
|
|
320
|
+
assert.deepEqual(r.fault, { code: "621", message: "621 · Right wheel driver reset. Normal when the emergency stop is pressed or released - no action needed." });
|
|
317
321
|
assert.deepEqual(ounitechFault([]), { code: "error", message: "The robot reports an error" });
|
|
318
322
|
});
|
|
319
323
|
|
|
@@ -448,7 +452,7 @@ test("live state mapping, recorded reply (pure)", () => {
|
|
|
448
452
|
assert.equal(off.statusName, "Offline");
|
|
449
453
|
// a fault listed live is named even without status 7
|
|
450
454
|
const faulty = ounitechRobotFromHomeRow({ robotSn: "E" }, { statusFlag: 1, rtFaultList: [{ errCode: "302", errMsg: "Clean water tank empty" }] }, now);
|
|
451
|
-
assert.deepEqual(faulty.fault, { code: "302", message: "
|
|
455
|
+
assert.deepEqual(faulty.fault, { code: "302", message: "302 · Robot can't load its map. Call Ounitech support." }, "a listed code: the maker's table wins");
|
|
452
456
|
});
|
|
453
457
|
|
|
454
458
|
test("cleaning readings: water levels, tanks and covers, job progress only while cleaning (pure)", async () => {
|
|
@@ -557,8 +561,8 @@ test("records: jobs, alert history, consumables, power schedule, lifts, readings
|
|
|
557
561
|
);
|
|
558
562
|
assert.deepEqual(r.alerts.items.map((x) => [x.code, x.level, x.message, x.resolved]), [
|
|
559
563
|
["734", "Notice", "IMU zero offset too large", false],
|
|
560
|
-
["540", "Notice", "
|
|
561
|
-
["621", null, "Right wheel driver reset", true],
|
|
564
|
+
["540", "Notice", "Cliff sensor switched on", false],
|
|
565
|
+
["621", null, "Right wheel driver reset. Normal when the emergency stop is pressed or released - no action needed.", true],
|
|
562
566
|
]);
|
|
563
567
|
assert.deepEqual(r.consumables.items, [{ part: "Left side brush", hours: 705.5, days: 30 }]);
|
|
564
568
|
assert.deepEqual(r.power.items, [{ days: [1, 2, 3, 4, 5], time: "20:00", action: "off", enabled: true }]);
|
|
@@ -580,11 +584,11 @@ test("records: a robot not on the account says so", async () => {
|
|
|
580
584
|
|
|
581
585
|
test("alert, reading, task and schedule mapping (pure, recorded shapes)", () => {
|
|
582
586
|
const a = ounitechAlertRecord({ errMsg: "785 - (提示)定位准备完成( 5)", createTime: "2026-09-30 07:02:57" });
|
|
583
|
-
assert.deepEqual(a, { code: "785", level: "Notice", message: "
|
|
587
|
+
assert.deepEqual(a, { code: "785", level: "Notice", message: "Position ready", vendorText: "定位准备完成", resolved: false, at: "2026-09-29T23:02:57.000Z" });
|
|
584
588
|
assert.equal(ounitechAlertRecord({ errMsg: "999 - (警告)未知( 3)" }).message, "Robot alert 999: 未知", "unknown code: code first, the vendor's words kept");
|
|
585
589
|
// Robot 424's other prod alerts (2026-09-30) read in English.
|
|
586
590
|
assert.equal(ounitechAlertRecord({ errMsg: "589 - (提示)定时关机( 5)" }).message, "Switched off by the timed power schedule");
|
|
587
|
-
assert.equal(ounitechAlertRecord({ errMsg: "205 - 边刷驱动器离线( 4)" }).message, "Side
|
|
591
|
+
assert.equal(ounitechAlertRecord({ errMsg: "205 - 边刷驱动器离线( 4)" }).message, "Side-brush driver offline (side brush won't turn). Check the side-brush motor wire.");
|
|
588
592
|
assert.equal(ounitechAlertRecord({ errMsg: "900 - plain words" }).message, "plain words");
|
|
589
593
|
assert.equal(ounitechConsumable({ unitcode: "scraper", workminutes: 60 }).part, "Scraper");
|
|
590
594
|
assert.equal(ounitechAlertRecord({ errMsg: "999 - (警告)未知( 3)" }).level, "Warning");
|
|
@@ -711,11 +715,50 @@ test("diagnose: the ten reads each answered, no Wi-Fi password, no phone number"
|
|
|
711
715
|
|
|
712
716
|
// Live, robot 424, 30 Sep: rtFaultList came as plain strings, and area "default" (centre 0,0) faulted 306.
|
|
713
717
|
test("a string fault from rtFaultList: code kept, English words", () => {
|
|
714
|
-
assert.deepEqual(ounitechFault(["306 - 接收定点指定区域故障( 1)"]), {
|
|
715
|
-
|
|
718
|
+
assert.deepEqual(ounitechFault(["306 - 接收定点指定区域故障( 1)"]), {
|
|
719
|
+
code: "306",
|
|
720
|
+
message: "306 · Robot couldn't receive the chosen area. Clear the fault (press and release the emergency stop), then send the task again.",
|
|
721
|
+
});
|
|
722
|
+
assert.deepEqual(ounitechFault(["999 - 未知( 1)"]), { code: "999", message: "999 · 未知" }, "unknown code: vendor code + its own text");
|
|
723
|
+
assert.deepEqual(ounitechFault(["734 - (提示)IMU零点偏移过大( 5)"]), { code: "734", message: "734 · IMU zero offset too large" }, "not in the maker's list: translated vendor text");
|
|
724
|
+
assert.deepEqual(ounitechFault([{ errCode: 563, errMsg: "机器人已故障" }]).message.startsWith("563 · Robot is in a fault state. Find the cause"), true);
|
|
716
725
|
assert.deepEqual(ounitechFault(["plain words"]), { code: "error", message: "plain words" });
|
|
717
726
|
});
|
|
718
727
|
|
|
728
|
+
test("maker's code table: English + fix for every listed code, ranges filled, fix only where the manual gives one", () => {
|
|
729
|
+
const n = Object.keys(OUNITECH_CODES).length;
|
|
730
|
+
assert.ok(n >= 220, `${n} codes`);
|
|
731
|
+
for (const [code, [what, fix]] of Object.entries(OUNITECH_CODES)) {
|
|
732
|
+
assert.match(code, /^\d{3}$/);
|
|
733
|
+
assert.ok(what && !/[㐀-鿿]/.test(what) && !/[㐀-鿿]/.test(fix ?? ""), `${code} English only`);
|
|
734
|
+
assert.ok(!what.endsWith(".") && !(fix ?? "").endsWith("."), `${code} no trailing period (added when joined)`);
|
|
735
|
+
}
|
|
736
|
+
assert.equal(OUNITECH_CODES["734"], undefined, "734 is not in the maker's list");
|
|
737
|
+
assert.equal(ounitechCodeWords("306"), "Robot couldn't receive the chosen area. Clear the fault (press and release the emergency stop), then send the task again.");
|
|
738
|
+
assert.equal(ounitechCodeWords("588"), "Switched off with the power button", "informational: no fix");
|
|
739
|
+
assert.equal(ounitechCodeWords("750"), "Sensor or camera calibration. Recalibrate if it failed.", "range filled");
|
|
740
|
+
assert.equal(ounitechCodeWords("999"), null);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
test("the fault notice reads the English words + fix once, no doubled period", () => {
|
|
744
|
+
const fault = ounitechFault(["306 - 接收定点指定区域故障( 1)"]);
|
|
745
|
+
assert.equal(
|
|
746
|
+
robotFaultText("HERO-OTH-CLEANING", fault).body,
|
|
747
|
+
"HERO-OTH-CLEANING reports a fault: 306 · Robot couldn't receive the chosen area. Clear the fault (press and release the emergency stop), then send the task again.",
|
|
748
|
+
);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
test("Ounitech manifest carries the manual's fault-clearing steps; other brands none", () => {
|
|
752
|
+
registerRobotBrand(OUNITECH_BRAND_DEFINITION); // each test bundle carries its own registry
|
|
753
|
+
const steps = robotManifest("ounitech", "Q3").profile.clearFault;
|
|
754
|
+
assert.equal(steps.length, 4);
|
|
755
|
+
assert.match(steps[0], /emergency stop.*clockwise/);
|
|
756
|
+
assert.match(steps[1], /charging dock/);
|
|
757
|
+
assert.match(steps[2], /power switch at the back/);
|
|
758
|
+
assert.match(steps[3], /Child lock 3 times/);
|
|
759
|
+
assert.deepEqual(robotManifest("nope").profile.clearFault, []);
|
|
760
|
+
});
|
|
761
|
+
|
|
719
762
|
test("Clean area refuses an area with no centre point before anything reaches the robot", async () => {
|
|
720
763
|
clearOunitechCaches();
|
|
721
764
|
seen.length = 0;
|