@7365admin1/core 3.136.2 → 3.138.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 +60 -1
- package/dist/index.js +507 -44
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +615 -152
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/e2e/harness.mjs +6 -0
- package/test/e2e/service-provider-invite-email-new-company.e2e.test.mjs +139 -0
- package/test/ounitech-api-2025-10.test.mjs +51 -8
- package/test/service-provider-invite.test.mjs +9 -0
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.138.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
|
},
|
package/test/e2e/harness.mjs
CHANGED
|
@@ -573,6 +573,9 @@ function buildApp(nsu, core) {
|
|
|
573
573
|
wrongJoinPreview,
|
|
574
574
|
wrongJoinRepair,
|
|
575
575
|
wrongJoinUndo,
|
|
576
|
+
finishSetupPreview,
|
|
577
|
+
finishSetupApply,
|
|
578
|
+
finishSetupUndo,
|
|
576
579
|
linkSite,
|
|
577
580
|
received,
|
|
578
581
|
getOne,
|
|
@@ -594,6 +597,9 @@ function buildApp(nsu, core) {
|
|
|
594
597
|
invitations.get("/:id/wrong-join", requireAuth, wrongJoinPreview);
|
|
595
598
|
invitations.put("/:id/wrong-join/repair", requireAuth, wrongJoinRepair);
|
|
596
599
|
invitations.put("/:id/wrong-join/undo", requireAuth, wrongJoinUndo);
|
|
600
|
+
invitations.get("/:id/finish-setup", requireAuth, finishSetupPreview);
|
|
601
|
+
invitations.put("/:id/finish-setup", requireAuth, finishSetupApply);
|
|
602
|
+
invitations.put("/:id/finish-setup/undo", requireAuth, finishSetupUndo);
|
|
597
603
|
invitations.put("/:id/link-site", requireAuth, linkSite);
|
|
598
604
|
}
|
|
599
605
|
|
|
@@ -384,6 +384,145 @@ describe("SP invitation: email status, Super Admin alert, new company typed by s
|
|
|
384
384
|
"the account now opens its own company",
|
|
385
385
|
);
|
|
386
386
|
});
|
|
387
|
+
|
|
388
|
+
// ------------------------- D. sign-in keeps landing in the sender (2026-10-01)
|
|
389
|
+
|
|
390
|
+
// The production state of latitude.sec@ after the 30 Sep Repair: the two
|
|
391
|
+
// memberships in JLL are inactive, but the account's default organisation
|
|
392
|
+
// still points at JLL - which is what every later sign-in opened.
|
|
393
|
+
async function repairedAccount(email) {
|
|
394
|
+
const site = new ObjectId();
|
|
395
|
+
await h.db.collection("sites").insertOne({ _id: site, name: "Latitude", orgId: id.pmOrg, status: "active" });
|
|
396
|
+
const inviteId = await invite(email, "security_agency", site);
|
|
397
|
+
await approve(inviteId);
|
|
398
|
+
const sid = await newAccount(email);
|
|
399
|
+
const user = await h.db.collection("users").findOne({ email });
|
|
400
|
+
const later = new Date(Date.now() + 1000);
|
|
401
|
+
await h.db.collection("members").insertMany([
|
|
402
|
+
{ user: user._id, org: id.pmOrg, role: new ObjectId(), type: "organization", siteId: site, siteName: "Latitude", status: "inactive", createdAt: later },
|
|
403
|
+
{ user: user._id, org: id.pmOrg, role: new ObjectId(), type: "security_agency", siteId: site, siteName: "Latitude", status: "inactive", createdAt: later },
|
|
404
|
+
]);
|
|
405
|
+
await h.db.collection("users").updateOne({ _id: user._id }, { $set: { defaultOrg: id.pmOrg.toString() } });
|
|
406
|
+
return { site, inviteId, sid, user };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
it("D1. Repair also clears a default organisation that points at the sender; Undo restores it", async () => {
|
|
410
|
+
const email = "repair-default@e2e.example.com";
|
|
411
|
+
const { inviteId, user } = await repairedAccount(email);
|
|
412
|
+
// memberships are already inactive: only the default org is left to fix
|
|
413
|
+
const preview = await h.api(`/service-provider-invitations/${inviteId}/wrong-join`, { sid: adminSid });
|
|
414
|
+
assert.equal(preview.body.memberships.length, 0);
|
|
415
|
+
assert.equal(preview.body.defaultOrgIsSender, true);
|
|
416
|
+
|
|
417
|
+
const repaired = await h.api(`/service-provider-invitations/${inviteId}/wrong-join/repair`, { method: "PUT", sid: adminSid });
|
|
418
|
+
assert.equal(repaired.status, 200, JSON.stringify(repaired.body));
|
|
419
|
+
assert.equal(String((await h.db.collection("users").findOne({ _id: user._id })).defaultOrg ?? ""), "");
|
|
420
|
+
|
|
421
|
+
const undone = await h.api(`/service-provider-invitations/${inviteId}/wrong-join/undo`, { method: "PUT", sid: adminSid });
|
|
422
|
+
assert.equal(undone.status, 200, JSON.stringify(undone.body));
|
|
423
|
+
assert.equal(String((await h.db.collection("users").findOne({ _id: user._id })).defaultOrg), id.pmOrg.toString());
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("D2. Finish setup for invitee: Super Admin only; preview writes nothing; apply creates the typed company, links the site, accepts; undo reverses; apply again", async () => {
|
|
427
|
+
const email = "latitude-finish@e2e.example.com";
|
|
428
|
+
const { site, inviteId, sid, user } = await repairedAccount(email);
|
|
429
|
+
const NAME = "Henderson Security Services Pte Ltd";
|
|
430
|
+
|
|
431
|
+
for (const who of [pmSid, sid]) {
|
|
432
|
+
const refused = await h.api(`/service-provider-invitations/${inviteId}/finish-setup`, { sid: who });
|
|
433
|
+
assert.ok([401, 403].includes(refused.status), JSON.stringify(refused.body));
|
|
434
|
+
const refusedPut = await h.api(`/service-provider-invitations/${inviteId}/finish-setup`, { method: "PUT", sid: who, body: { companyName: NAME } });
|
|
435
|
+
assert.ok([401, 403].includes(refusedPut.status), JSON.stringify(refusedPut.body));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const orgsBefore = await h.db.collection("organizations").countDocuments();
|
|
439
|
+
const preview = await h.api(`/service-provider-invitations/${inviteId}/finish-setup?companyName=${encodeURIComponent(NAME)}`, { sid: adminSid });
|
|
440
|
+
assert.equal(preview.status, 200, JSON.stringify(preview.body));
|
|
441
|
+
assert.equal(preview.body.canApply, true, preview.body.blocked);
|
|
442
|
+
assert.equal(preview.body.companyName, NAME);
|
|
443
|
+
assert.equal(preview.body.service, "security_agency");
|
|
444
|
+
assert.equal(preview.body.usesExistingCompany, false);
|
|
445
|
+
assert.equal(preview.body.currentDefaultOrgName, "JLL Singapore");
|
|
446
|
+
assert.equal(await h.db.collection("organizations").countDocuments(), orgsBefore, "preview writes nothing");
|
|
447
|
+
|
|
448
|
+
const applied = await h.api(`/service-provider-invitations/${inviteId}/finish-setup`, { method: "PUT", sid: adminSid, body: { companyName: ` ${NAME} ` } });
|
|
449
|
+
assert.equal(applied.status, 200, JSON.stringify(applied.body));
|
|
450
|
+
assert.equal(applied.body.message, `${NAME} is set up and linked to Latitude.`);
|
|
451
|
+
const org = await h.db.collection("organizations").findOne({ _id: new ObjectId(applied.body.orgId) });
|
|
452
|
+
assert.equal(org.name, NAME);
|
|
453
|
+
assert.equal(org.nature, "security_agency");
|
|
454
|
+
assert.equal(String(org.createdBy), user._id.toString(), "the invitee's company, not the Super Admin's");
|
|
455
|
+
const owners = await h.db.collection("members").find({ user: user._id, org: org._id, status: "active" }).toArray();
|
|
456
|
+
assert.deepEqual(owners.map((m) => m.type).sort(), ["organization", "security_agency"]);
|
|
457
|
+
assert.ok(await h.db.collection("customer.sites").findOne({ org: org._id, site, status: "active" }), "site linked");
|
|
458
|
+
assert.equal((await h.db.collection("verifications").findOne({ _id: new ObjectId(inviteId) })).status, "complete");
|
|
459
|
+
assert.equal(String((await h.db.collection("users").findOne({ _id: user._id })).defaultOrg), org._id.toString());
|
|
460
|
+
// and the invitee's own sign-in now finds its Security company
|
|
461
|
+
const mine = await h.api(`/members/users/${user._id}`, { sid });
|
|
462
|
+
assert.ok(mine.body.items.some((m) => String(m.org) === org._id.toString() && m.type === "security_agency" && m.status === "active"));
|
|
463
|
+
|
|
464
|
+
// a second apply is refused (no longer waiting), nothing duplicated
|
|
465
|
+
const twice = await h.api(`/service-provider-invitations/${inviteId}/finish-setup`, { method: "PUT", sid: adminSid, body: { companyName: NAME } });
|
|
466
|
+
assert.equal(twice.status, 400, JSON.stringify(twice.body));
|
|
467
|
+
|
|
468
|
+
const undone = await h.api(`/service-provider-invitations/${inviteId}/finish-setup/undo`, { method: "PUT", sid: adminSid });
|
|
469
|
+
assert.equal(undone.status, 200, JSON.stringify(undone.body));
|
|
470
|
+
assert.equal((await h.db.collection("verifications").findOne({ _id: new ObjectId(inviteId) })).status, "pending");
|
|
471
|
+
assert.equal(await h.db.collection("customer.sites").countDocuments({ org: org._id, site, status: "active" }), 0);
|
|
472
|
+
assert.equal(await h.db.collection("members").countDocuments({ org: org._id, status: "active" }), 0);
|
|
473
|
+
assert.equal(String((await h.db.collection("users").findOne({ _id: user._id })).defaultOrg), id.pmOrg.toString());
|
|
474
|
+
|
|
475
|
+
const again = await h.api(`/service-provider-invitations/${inviteId}/finish-setup`, { method: "PUT", sid: adminSid, body: { companyName: NAME } });
|
|
476
|
+
assert.equal(again.status, 200, JSON.stringify(again.body));
|
|
477
|
+
assert.notEqual(again.body.orgId, org._id.toString(), "a fresh company, not the undone one");
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("D3. existing account signs in to accept: the typed name (sanitised) + invited type + site link; plain sign-in can find the open invitation", async () => {
|
|
481
|
+
const email = "latitude-signin@e2e.example.com";
|
|
482
|
+
const { site, inviteId, sid, user } = await repairedAccount(email);
|
|
483
|
+
|
|
484
|
+
// plain sign-in (no invitation in the link): the account has no company,
|
|
485
|
+
// and the open invitation is found from its own session
|
|
486
|
+
const received = await h.api(`/service-provider-invitations/received`, { sid });
|
|
487
|
+
assert.equal(received.status, 200, JSON.stringify(received.body));
|
|
488
|
+
const row = received.body.items.find((r) => String(r._id) === inviteId);
|
|
489
|
+
assert.ok(row, "the pending invitation is listed for the signed-in invitee");
|
|
490
|
+
assert.equal(row.service, "security_agency");
|
|
491
|
+
|
|
492
|
+
const res = await h.api(`/service-provider-invitations/${inviteId}/accept-new-company`, {
|
|
493
|
+
method: "PUT",
|
|
494
|
+
sid,
|
|
495
|
+
body: { companyName: " <b>Henderson</b> Security Services Pte Ltd " },
|
|
496
|
+
});
|
|
497
|
+
assert.equal(res.status, 200, JSON.stringify(res.body));
|
|
498
|
+
assert.equal(res.body.companyName, "b Henderson /b Security Services Pte Ltd");
|
|
499
|
+
assert.equal(res.body.siteName, "Latitude");
|
|
500
|
+
const org = await h.db.collection("organizations").findOne({ _id: new ObjectId(res.body.orgId) });
|
|
501
|
+
assert.equal(org.nature, "security_agency");
|
|
502
|
+
assert.ok(await h.db.collection("customer.sites").findOne({ org: org._id, site, status: "active" }));
|
|
503
|
+
assert.equal(String((await h.db.collection("users").findOne({ _id: user._id })).defaultOrg), org._id.toString());
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("D4. a new account creates: the same result", async () => {
|
|
507
|
+
const site = new ObjectId();
|
|
508
|
+
await h.db.collection("sites").insertOne({ _id: site, name: "Latitude", orgId: id.pmOrg, status: "active" });
|
|
509
|
+
const email = "brand-new-guard@e2e.example.com";
|
|
510
|
+
const inviteId = await invite(email, "security_agency", site);
|
|
511
|
+
await approve(inviteId);
|
|
512
|
+
const sid = await newAccount(email);
|
|
513
|
+
const res = await h.api(`/service-provider-invitations/${inviteId}/accept-new-company`, {
|
|
514
|
+
method: "PUT",
|
|
515
|
+
sid,
|
|
516
|
+
body: { companyName: "Henderson Security Services Pte Ltd", country: "Singapore" },
|
|
517
|
+
});
|
|
518
|
+
assert.equal(res.status, 200, JSON.stringify(res.body));
|
|
519
|
+
assert.equal(res.body.companyName, "Henderson Security Services Pte Ltd");
|
|
520
|
+
assert.equal(res.body.nature, "security_agency");
|
|
521
|
+
const org = await h.db.collection("organizations").findOne({ _id: new ObjectId(res.body.orgId) });
|
|
522
|
+
assert.equal(org.name, "Henderson Security Services Pte Ltd");
|
|
523
|
+
assert.ok(await h.db.collection("customer.sites").findOne({ org: org._id, site, status: "active" }));
|
|
524
|
+
assert.equal((await h.db.collection("verifications").findOne({ _id: new ObjectId(inviteId) })).status, "complete");
|
|
525
|
+
});
|
|
387
526
|
});
|
|
388
527
|
|
|
389
528
|
async function seed(h) {
|
|
@@ -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;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
isServiceProviderInviteExpired,
|
|
14
14
|
serviceProviderInviteViewStatus,
|
|
15
15
|
} from "./.build/utils/service-provider-invite.util.mjs";
|
|
16
|
+
import { sanitizeCompanyName } from "./.build/utils/service-provider-invite.util.mjs";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Inviting a service provider to a site.
|
|
@@ -391,3 +392,11 @@ test("accept is bound to a company the caller belongs to, and records which comp
|
|
|
391
392
|
assert.match(cs, /"metadata\.providerOrg": providerOrgId/);
|
|
392
393
|
assert.match(cs, /set: accepted,\s*session,/);
|
|
393
394
|
});
|
|
395
|
+
|
|
396
|
+
test("sanitizeCompanyName: keeps the typed name, drops markup and control characters", () => {
|
|
397
|
+
assert.equal(sanitizeCompanyName(" Henderson Security Services Pte Ltd "), "Henderson Security Services Pte Ltd");
|
|
398
|
+
assert.equal(sanitizeCompanyName("<b>Acme</b>\u0000 Guards"), "b Acme /b Guards");
|
|
399
|
+
assert.equal(sanitizeCompanyName("x".repeat(300)).length, 120);
|
|
400
|
+
assert.equal(sanitizeCompanyName(null), "");
|
|
401
|
+
assert.equal(sanitizeCompanyName(" \n\t "), "");
|
|
402
|
+
});
|