fix(M09-A): 收口保洁任务事务与并发
This commit is contained in:
@@ -45,6 +45,9 @@ import { BusinessStatisticsRepository } from '../dist/operations/business-statis
|
||||
import { SystemOperationsRepository } from '../dist/operations/system-operations-repository.js';
|
||||
import { AdminAuthRepository, AdminAuthError, hashToken } from '../dist/auth/admin-auth-repository.js';
|
||||
import { hashPassword } from '../dist/auth/password.js';
|
||||
import {
|
||||
CleaningTaskError, CleaningTaskRepository
|
||||
} from '../dist/cleaning/cleaning-task-repository.js';
|
||||
import {
|
||||
executeMigrationPlan,
|
||||
loadMigrationPlan,
|
||||
@@ -2129,6 +2132,408 @@ async function assertIotMessages(pool, context) {
|
||||
assert.deepEqual(linkRows, [{ subId: 'SUB-AUTO-001', subtype: '14', model: '701C' }]);
|
||||
}
|
||||
|
||||
async function assertCleaningTaskTransactions(pool, context) {
|
||||
const [scopeRows] = await pool.query(
|
||||
`SELECT s.id AS storeId, r.id AS roomId
|
||||
FROM qipai_stores s
|
||||
INNER JOIN qipai_rooms r ON r.tenant_id = s.tenant_id AND r.store_id = s.id
|
||||
WHERE s.tenant_id = ? AND s.name = 'M03A Store'
|
||||
ORDER BY r.id LIMIT 1`,
|
||||
[context.tenantId]
|
||||
);
|
||||
const [managerRows] = await pool.query(
|
||||
`SELECT u.id
|
||||
FROM qipai_users u
|
||||
INNER JOIN qipai_user_roles ur ON ur.tenant_id = u.tenant_id AND ur.user_id = u.id
|
||||
INNER JOIN qipai_roles r ON r.tenant_id = ur.tenant_id AND r.id = ur.role_id
|
||||
WHERE u.tenant_id = ? AND r.code = 'TENANT_ADMIN'
|
||||
ORDER BY u.id LIMIT 1`,
|
||||
[context.tenantId]
|
||||
);
|
||||
assert.ok(scopeRows[0], 'M09-A cleaning test requires the M03-A store and room.');
|
||||
assert.ok(managerRows[0], 'M09-A cleaning test requires a tenant administrator.');
|
||||
const storeId = String(scopeRows[0].storeId);
|
||||
const roomId = String(scopeRows[0].roomId);
|
||||
const managerId = String(managerRows[0].id);
|
||||
const managerAccess = await new RbacRepository(pool).getAccessProfile(context.tenantId, managerId);
|
||||
assert.ok(managerAccess.capabilities.includes('tenant.manage'));
|
||||
|
||||
const createCleaner = async (nickname, phone) => {
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO qipai_users (tenant_id, user_type, nickname, phone)
|
||||
VALUES (?, 'STAFF', ?, ?)`,
|
||||
[context.tenantId, nickname, phone]
|
||||
);
|
||||
const cleanerId = String(result.insertId);
|
||||
await pool.query(
|
||||
`INSERT INTO qipai_user_roles (tenant_id, user_id, role_id)
|
||||
SELECT ?, ?, id FROM qipai_roles
|
||||
WHERE tenant_id = ? AND code = 'CLEANER'`,
|
||||
[context.tenantId, cleanerId, context.tenantId]
|
||||
);
|
||||
await pool.query(
|
||||
`INSERT INTO qipai_user_store_scopes (tenant_id, user_id, store_id, scope_type)
|
||||
VALUES (?, ?, ?, 'STAFF')`,
|
||||
[context.tenantId, cleanerId, storeId]
|
||||
);
|
||||
return cleanerId;
|
||||
};
|
||||
const firstCleanerId = await createCleaner('M09A Cleaner One', '13900000901');
|
||||
const secondCleanerId = await createCleaner('M09A Cleaner Two', '13900000902');
|
||||
const cleanerAccess = {
|
||||
roles: ['CLEANER'],
|
||||
capabilities: ['cleaning.task.read', 'cleaning.task.write'],
|
||||
storeIds: [storeId]
|
||||
};
|
||||
const cleanerActor = (userId, traceId) => ({
|
||||
tenantId: context.tenantId,
|
||||
userId,
|
||||
access: cleanerAccess,
|
||||
traceId
|
||||
});
|
||||
const managerActor = (traceId) => ({
|
||||
tenantId: context.tenantId,
|
||||
userId: managerId,
|
||||
access: managerAccess,
|
||||
traceId
|
||||
});
|
||||
const repository = new CleaningTaskRepository(pool);
|
||||
let taskSequence = 0;
|
||||
const insertTask = async ({
|
||||
status = 'WAITING', cleanerUserId = null, rewardCents = 100,
|
||||
photoUrls = [], rejectReason = '', claimedAt = null, startedAt = null,
|
||||
submittedAt = null, completedAt = null, settledAt = null, cancelledAt = null,
|
||||
updatedAt = new Date()
|
||||
} = {}) => {
|
||||
taskSequence += 1;
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO qipai_cleaning_tasks
|
||||
(tenant_id, store_id, room_id, task_no, status, cleaner_user_id,
|
||||
priority, reward_cents, requirement, photo_urls_json, reject_reason,
|
||||
claimed_at, started_at, submitted_at, completed_at, settled_at, cancelled_at,
|
||||
updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 5, ?, 'M09-A transaction test', ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
context.tenantId, storeId, roomId, `M09A-${Date.now()}-${taskSequence}`,
|
||||
status, cleanerUserId, rewardCents, JSON.stringify(photoUrls), rejectReason,
|
||||
claimedAt, startedAt, submittedAt, completedAt, settledAt, cancelledAt, updatedAt
|
||||
]
|
||||
);
|
||||
return String(result.insertId);
|
||||
};
|
||||
const insertMember = async ({
|
||||
taskId, userId, memberRole = 'LEAD', rewardCents = 0,
|
||||
removedAt = null, settledAt = null
|
||||
}) => {
|
||||
await pool.query(
|
||||
`INSERT INTO qipai_cleaning_task_members
|
||||
(tenant_id, task_id, user_id, member_role, reward_cents, joined_at, removed_at, settled_at)
|
||||
VALUES (?, ?, ?, ?, ?, UTC_TIMESTAMP(3), ?, ?)`,
|
||||
[context.tenantId, taskId, userId, memberRole, rewardCents, removedAt, settledAt]
|
||||
);
|
||||
};
|
||||
|
||||
const rollbackTaskId = await insertTask();
|
||||
const rollbackTrace = `m09a-event-failure-${rollbackTaskId}`;
|
||||
await pool.query(
|
||||
`INSERT INTO qipai_cleaning_task_events
|
||||
(tenant_id, task_id, from_status, to_status, action, actor_id, trace_id, note, metadata)
|
||||
VALUES (?, ?, 'WAITING', 'WAITING', 'TEST_BLOCKER', ?, ?, '', JSON_OBJECT())`,
|
||||
[context.tenantId, rollbackTaskId, firstCleanerId, rollbackTrace]
|
||||
);
|
||||
await assert.rejects(
|
||||
() => repository.claim({
|
||||
...cleanerActor(firstCleanerId, 'm09a-event-failure'), taskId: rollbackTaskId
|
||||
}),
|
||||
(error) => error?.code === 'ER_DUP_ENTRY'
|
||||
);
|
||||
const [rollbackRows] = await pool.query(
|
||||
`SELECT t.status, t.cleaner_user_id AS cleanerUserId,
|
||||
(SELECT COUNT(*) FROM qipai_cleaning_task_members m
|
||||
WHERE m.tenant_id = t.tenant_id AND m.task_id = t.id) AS memberCount
|
||||
FROM qipai_cleaning_tasks t WHERE t.tenant_id = ? AND t.id = ?`,
|
||||
[context.tenantId, rollbackTaskId]
|
||||
);
|
||||
assert.equal(rollbackRows[0].status, 'WAITING');
|
||||
assert.equal(rollbackRows[0].cleanerUserId, null);
|
||||
assert.equal(Number(rollbackRows[0].memberCount), 0);
|
||||
|
||||
const concurrentTaskId = await insertTask();
|
||||
const claimResults = await Promise.allSettled([
|
||||
repository.claim({
|
||||
...cleanerActor(firstCleanerId, 'm09a-concurrent-claim-one'), taskId: concurrentTaskId
|
||||
}),
|
||||
repository.claim({
|
||||
...cleanerActor(secondCleanerId, 'm09a-concurrent-claim-two'), taskId: concurrentTaskId
|
||||
})
|
||||
]);
|
||||
assert.equal(claimResults.filter((result) => result.status === 'fulfilled').length, 1);
|
||||
const rejectedClaim = claimResults.find((result) => result.status === 'rejected');
|
||||
assert.ok(rejectedClaim?.reason instanceof CleaningTaskError);
|
||||
assert.equal(rejectedClaim.reason.code, 'CLEANING_TASK_NOT_CLAIMABLE');
|
||||
const [claimedRows] = await pool.query(
|
||||
`SELECT t.status, t.cleaner_user_id AS cleanerUserId,
|
||||
(SELECT COUNT(*) FROM qipai_cleaning_task_events e
|
||||
WHERE e.tenant_id = t.tenant_id AND e.task_id = t.id AND e.action = 'CLAIM') AS eventCount,
|
||||
(SELECT COUNT(*) FROM qipai_cleaning_task_members m
|
||||
WHERE m.tenant_id = t.tenant_id AND m.task_id = t.id
|
||||
AND m.member_role = 'LEAD' AND m.removed_at IS NULL) AS leadCount
|
||||
FROM qipai_cleaning_tasks t WHERE t.tenant_id = ? AND t.id = ?`,
|
||||
[context.tenantId, concurrentTaskId]
|
||||
);
|
||||
assert.equal(claimedRows[0].status, 'CLAIMED');
|
||||
assert.ok([firstCleanerId, secondCleanerId].includes(String(claimedRows[0].cleanerUserId)));
|
||||
assert.equal(Number(claimedRows[0].eventCount), 1);
|
||||
assert.equal(Number(claimedRows[0].leadCount), 1);
|
||||
|
||||
const lifecycleTaskId = await insertTask();
|
||||
await repository.assign({
|
||||
...managerActor('m09a-assign-waiting'), taskId: lifecycleTaskId,
|
||||
cleanerUserId: firstCleanerId, note: 'WAITING assignment'
|
||||
});
|
||||
await repository.assign({
|
||||
...managerActor('m09a-assign-claimed'), taskId: lifecycleTaskId,
|
||||
cleanerUserId: secondCleanerId, note: 'CLAIMED reassignment'
|
||||
});
|
||||
const expiredAt = new Date(Date.now() - 3_600_000);
|
||||
await pool.query(
|
||||
`UPDATE qipai_cleaning_tasks
|
||||
SET status = 'REJECTED', photo_urls_json = JSON_ARRAY('https://old.example/photo.jpg'),
|
||||
reject_reason = 'old rejection', started_at = ?, submitted_at = ?, completed_at = ?,
|
||||
settled_at = ?, cancelled_at = ?
|
||||
WHERE tenant_id = ? AND id = ?`,
|
||||
[expiredAt, expiredAt, expiredAt, expiredAt, expiredAt, context.tenantId, lifecycleTaskId]
|
||||
);
|
||||
await repository.assign({
|
||||
...managerActor('m09a-assign-rejected'), taskId: lifecycleTaskId,
|
||||
cleanerUserId: firstCleanerId, note: 'REJECTED reassignment'
|
||||
});
|
||||
const [resetRows] = await pool.query(
|
||||
`SELECT status, cleaner_user_id AS cleanerUserId,
|
||||
claimed_at IS NOT NULL AS claimedSet,
|
||||
started_at IS NULL AS startedCleared,
|
||||
submitted_at IS NULL AS submittedCleared,
|
||||
completed_at IS NULL AS completedCleared,
|
||||
settled_at IS NULL AS settledCleared,
|
||||
cancelled_at IS NULL AS cancelledCleared,
|
||||
JSON_LENGTH(photo_urls_json) AS photoCount, reject_reason AS rejectReason
|
||||
FROM qipai_cleaning_tasks WHERE tenant_id = ? AND id = ?`,
|
||||
[context.tenantId, lifecycleTaskId]
|
||||
);
|
||||
assert.deepEqual({
|
||||
status: resetRows[0].status,
|
||||
cleanerUserId: String(resetRows[0].cleanerUserId),
|
||||
claimedSet: Number(resetRows[0].claimedSet),
|
||||
startedCleared: Number(resetRows[0].startedCleared),
|
||||
submittedCleared: Number(resetRows[0].submittedCleared),
|
||||
completedCleared: Number(resetRows[0].completedCleared),
|
||||
settledCleared: Number(resetRows[0].settledCleared),
|
||||
cancelledCleared: Number(resetRows[0].cancelledCleared),
|
||||
photoCount: Number(resetRows[0].photoCount),
|
||||
rejectReason: resetRows[0].rejectReason
|
||||
}, {
|
||||
status: 'CLAIMED', cleanerUserId: firstCleanerId, claimedSet: 1,
|
||||
startedCleared: 1, submittedCleared: 1, completedCleared: 1,
|
||||
settledCleared: 1, cancelledCleared: 1, photoCount: 0, rejectReason: ''
|
||||
});
|
||||
const [reassignedMemberRows] = await pool.query(
|
||||
`SELECT user_id AS userId, member_role AS memberRole, reward_cents AS rewardCents,
|
||||
removed_at IS NOT NULL AS removed
|
||||
FROM qipai_cleaning_task_members
|
||||
WHERE tenant_id = ? AND task_id = ? ORDER BY user_id`,
|
||||
[context.tenantId, lifecycleTaskId]
|
||||
);
|
||||
assert.deepEqual(reassignedMemberRows.map((row) => ({
|
||||
userId: String(row.userId), memberRole: row.memberRole,
|
||||
rewardCents: Number(row.rewardCents), removed: Number(row.removed)
|
||||
})), [
|
||||
{ userId: firstCleanerId, memberRole: 'LEAD', rewardCents: 100, removed: 0 },
|
||||
{ userId: secondCleanerId, memberRole: 'LEAD', rewardCents: 0, removed: 1 }
|
||||
].sort((left, right) => Number(left.userId) - Number(right.userId)));
|
||||
|
||||
await repository.start({
|
||||
...cleanerActor(firstCleanerId, 'm09a-start'), taskId: lifecycleTaskId
|
||||
});
|
||||
await repository.submit({
|
||||
...cleanerActor(firstCleanerId, 'm09a-submit-first'), taskId: lifecycleTaskId,
|
||||
photoUrls: ['https://api.txyundm.cn/uploads/m09a-first.webp'], note: 'first submit'
|
||||
});
|
||||
await repository.reject({
|
||||
...managerActor('m09a-reject'), taskId: lifecycleTaskId, reason: 'needs rework'
|
||||
});
|
||||
const [rejectedRows] = await pool.query(
|
||||
`SELECT status, reject_reason AS rejectReason
|
||||
FROM qipai_cleaning_tasks WHERE tenant_id = ? AND id = ?`,
|
||||
[context.tenantId, lifecycleTaskId]
|
||||
);
|
||||
assert.deepEqual(rejectedRows, [{ status: 'REJECTED', rejectReason: 'needs rework' }]);
|
||||
await repository.rework({
|
||||
...cleanerActor(firstCleanerId, 'm09a-rework'), taskId: lifecycleTaskId
|
||||
});
|
||||
await repository.submit({
|
||||
...cleanerActor(firstCleanerId, 'm09a-submit-second'), taskId: lifecycleTaskId,
|
||||
photoUrls: ['https://api.txyundm.cn/uploads/m09a-second.webp'], note: 'second submit'
|
||||
});
|
||||
const completeInput = {
|
||||
...managerActor('m09a-complete'), taskId: lifecycleTaskId, note: 'accepted'
|
||||
};
|
||||
await repository.complete(completeInput);
|
||||
await assert.rejects(
|
||||
() => repository.complete(completeInput),
|
||||
(error) => error instanceof CleaningTaskError
|
||||
&& error.code === 'CLEANING_TASK_STATUS_CONFLICT'
|
||||
);
|
||||
const [lifecycleRows] = await pool.query(
|
||||
`SELECT status, completed_at IS NOT NULL AS completedSet
|
||||
FROM qipai_cleaning_tasks WHERE tenant_id = ? AND id = ?`,
|
||||
[context.tenantId, lifecycleTaskId]
|
||||
);
|
||||
assert.deepEqual({
|
||||
status: lifecycleRows[0].status,
|
||||
completedSet: Number(lifecycleRows[0].completedSet)
|
||||
}, { status: 'COMPLETED', completedSet: 1 });
|
||||
const [lifecycleEventRows] = await pool.query(
|
||||
`SELECT action FROM qipai_cleaning_task_events
|
||||
WHERE tenant_id = ? AND task_id = ? ORDER BY id`,
|
||||
[context.tenantId, lifecycleTaskId]
|
||||
);
|
||||
assert.deepEqual(lifecycleEventRows.map((row) => row.action), [
|
||||
'ASSIGN', 'ASSIGN', 'ASSIGN', 'START', 'SUBMIT', 'REJECT', 'REWORK', 'SUBMIT', 'COMPLETE'
|
||||
]);
|
||||
|
||||
const staleBase = Date.now() - 24 * 3_600_000;
|
||||
const lockedTaskId = await insertTask({
|
||||
status: 'CLAIMED', cleanerUserId: firstCleanerId,
|
||||
claimedAt: new Date(staleBase), updatedAt: new Date(staleBase)
|
||||
});
|
||||
const reclaimTaskId = await insertTask({
|
||||
status: 'STARTED', cleanerUserId: secondCleanerId,
|
||||
claimedAt: new Date(staleBase + 3_600_000), startedAt: new Date(staleBase + 3_600_000),
|
||||
photoUrls: ['https://old.example/reclaim.jpg'], rejectReason: 'stale',
|
||||
updatedAt: new Date(staleBase + 3_600_000)
|
||||
});
|
||||
const outsideBatchTaskId = await insertTask({
|
||||
status: 'CLAIMED', cleanerUserId: firstCleanerId,
|
||||
claimedAt: new Date(staleBase + 7_200_000), updatedAt: new Date(staleBase + 7_200_000)
|
||||
});
|
||||
const settledTaskId = await insertTask({
|
||||
status: 'SETTLED', cleanerUserId: secondCleanerId,
|
||||
claimedAt: new Date(staleBase - 3_600_000), completedAt: new Date(staleBase - 3_600_000),
|
||||
settledAt: new Date(staleBase - 3_600_000), updatedAt: new Date(staleBase - 3_600_000)
|
||||
});
|
||||
await insertMember({ taskId: lockedTaskId, userId: firstCleanerId, rewardCents: 100 });
|
||||
await insertMember({ taskId: reclaimTaskId, userId: secondCleanerId, rewardCents: 70 });
|
||||
await insertMember({
|
||||
taskId: reclaimTaskId, userId: firstCleanerId, memberRole: 'ASSIST', rewardCents: 30
|
||||
});
|
||||
await insertMember({ taskId: outsideBatchTaskId, userId: firstCleanerId, rewardCents: 100 });
|
||||
await insertMember({
|
||||
taskId: settledTaskId, userId: secondCleanerId, rewardCents: 100,
|
||||
settledAt: new Date(staleBase)
|
||||
});
|
||||
|
||||
const lockConnection = await pool.getConnection();
|
||||
let reclaimed;
|
||||
try {
|
||||
await lockConnection.beginTransaction();
|
||||
await lockConnection.query(
|
||||
`SELECT id FROM qipai_cleaning_tasks
|
||||
WHERE tenant_id = ? AND id = ? FOR UPDATE`,
|
||||
[context.tenantId, lockedTaskId]
|
||||
);
|
||||
reclaimed = await repository.reclaimTimeouts({
|
||||
...managerActor('m09a-reclaim'), olderThanMinutes: 30, limit: 1
|
||||
});
|
||||
} finally {
|
||||
await lockConnection.rollback();
|
||||
lockConnection.release();
|
||||
}
|
||||
assert.deepEqual(reclaimed, { reclaimed: 1, taskIds: [reclaimTaskId] });
|
||||
const [reclaimRows] = await pool.query(
|
||||
`SELECT id, status, cleaner_user_id AS cleanerUserId,
|
||||
JSON_LENGTH(photo_urls_json) AS photoCount, reject_reason AS rejectReason
|
||||
FROM qipai_cleaning_tasks
|
||||
WHERE tenant_id = ? AND id IN (?, ?, ?, ?) ORDER BY id`,
|
||||
[context.tenantId, lockedTaskId, reclaimTaskId, outsideBatchTaskId, settledTaskId]
|
||||
);
|
||||
const reclaimById = new Map(reclaimRows.map((row) => [String(row.id), row]));
|
||||
assert.equal(reclaimById.get(lockedTaskId).status, 'CLAIMED');
|
||||
assert.equal(reclaimById.get(outsideBatchTaskId).status, 'CLAIMED');
|
||||
assert.equal(reclaimById.get(settledTaskId).status, 'SETTLED');
|
||||
assert.equal(reclaimById.get(reclaimTaskId).status, 'WAITING');
|
||||
assert.equal(reclaimById.get(reclaimTaskId).cleanerUserId, null);
|
||||
assert.equal(Number(reclaimById.get(reclaimTaskId).photoCount), 0);
|
||||
assert.equal(reclaimById.get(reclaimTaskId).rejectReason, '');
|
||||
const [reclaimedMembers] = await pool.query(
|
||||
`SELECT reward_cents AS rewardCents, removed_at IS NOT NULL AS removed
|
||||
FROM qipai_cleaning_task_members
|
||||
WHERE tenant_id = ? AND task_id = ? ORDER BY user_id`,
|
||||
[context.tenantId, reclaimTaskId]
|
||||
);
|
||||
assert.deepEqual(reclaimedMembers.map((row) => ({
|
||||
rewardCents: Number(row.rewardCents), removed: Number(row.removed)
|
||||
})), [
|
||||
{ rewardCents: 0, removed: 1 }, { rewardCents: 0, removed: 1 }
|
||||
]);
|
||||
const [settledMemberRows] = await pool.query(
|
||||
`SELECT reward_cents AS rewardCents, removed_at AS removedAt,
|
||||
settled_at IS NOT NULL AS settled
|
||||
FROM qipai_cleaning_task_members
|
||||
WHERE tenant_id = ? AND task_id = ?`,
|
||||
[context.tenantId, settledTaskId]
|
||||
);
|
||||
assert.equal(Number(settledMemberRows[0].rewardCents), 100);
|
||||
assert.equal(settledMemberRows[0].removedAt, null);
|
||||
assert.equal(Number(settledMemberRows[0].settled), 1);
|
||||
|
||||
const [orderResult] = await pool.query(
|
||||
`INSERT INTO qipai_orders
|
||||
(tenant_id, store_id, room_id, order_no, status, start_at, end_at,
|
||||
total_amount_cents, paid_amount_cents)
|
||||
VALUES (?, ?, ?, ?, 'FINISHED', ?, ?, 1000, 1000)`,
|
||||
[
|
||||
context.tenantId, storeId, roomId, `M09A-FINISHED-${Date.now()}`,
|
||||
new Date(Date.now() - 7_200_000), new Date(Date.now() - 3_600_000)
|
||||
]
|
||||
);
|
||||
const finishedOrderId = String(orderResult.insertId);
|
||||
const orderConnection = await pool.getConnection();
|
||||
try {
|
||||
await orderConnection.beginTransaction();
|
||||
const creationInput = {
|
||||
tenantId: context.tenantId,
|
||||
orderId: finishedOrderId,
|
||||
actorId: managerId,
|
||||
traceId: 'm09a-finished-order'
|
||||
};
|
||||
await repository.createForFinishedOrder(orderConnection, creationInput);
|
||||
await repository.createForFinishedOrder(orderConnection, creationInput);
|
||||
await orderConnection.commit();
|
||||
} catch (error) {
|
||||
await orderConnection.rollback();
|
||||
throw error;
|
||||
} finally {
|
||||
orderConnection.release();
|
||||
}
|
||||
const [generatedRows] = await pool.query(
|
||||
`SELECT t.status,
|
||||
(SELECT COUNT(*) FROM qipai_cleaning_task_events e
|
||||
WHERE e.tenant_id = t.tenant_id AND e.task_id = t.id
|
||||
AND e.action = 'AUTO_CREATE') AS eventCount
|
||||
FROM qipai_cleaning_tasks t
|
||||
WHERE t.tenant_id = ? AND t.order_id = ?`,
|
||||
[context.tenantId, finishedOrderId]
|
||||
);
|
||||
assert.equal(generatedRows.length, 1);
|
||||
assert.equal(generatedRows[0].status, 'WAITING');
|
||||
assert.equal(Number(generatedRows[0].eventCount), 1);
|
||||
|
||||
console.log(
|
||||
'PASS: M09-A cleaning claims, lifecycle events, rollback, reassignment, timeout reclaim and order generation are transactional.'
|
||||
);
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
assert.equal(config.mysql.passwordConfigured, true, 'Live migration test requires a temporary password.');
|
||||
assert.match(
|
||||
@@ -2197,6 +2602,7 @@ try {
|
||||
await assertProfitSharingDomain(pool, loginContext);
|
||||
await assertDeviceTopology(pool, loginContext);
|
||||
await assertIotMessages(pool, loginContext);
|
||||
await assertCleaningTaskTransactions(pool, loginContext);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: first up, verify, tenant isolation and revocable auth checks completed.');
|
||||
|
||||
@@ -2351,6 +2757,14 @@ try {
|
||||
'door record credential hashing and masking',
|
||||
'low-battery alert upsert',
|
||||
'AddDevice ACK creates 701C parent-child topology'
|
||||
,
|
||||
'concurrent cleaning claim has one winner',
|
||||
'cleaning state and event transaction rollback',
|
||||
'WAITING CLAIMED and REJECTED reassignment cleanup',
|
||||
'idempotent cleaning completion trace',
|
||||
'SKIP LOCKED cleaning timeout reclaim batch',
|
||||
'settled cleaning member preservation',
|
||||
'finished order creates one cleaning task'
|
||||
]
|
||||
}, null, 2));
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user