Separate CAD task assignment from task acceptance
- Reserve tasks as `available`/`assigned` before leader acknowledgement - Update CAD and task lifecycle handling and docs to reflect the new flow - Remove startup heartbeat reset and reset task backend explicitly in preInit
This commit is contained in:
parent
e49b4e4dd9
commit
9b31184f0c
@ -48,6 +48,16 @@ The addon listens to and emits events through the event bus:
|
||||
|
||||
Successful mutations may invalidate CAD state globally so clients refresh their views.
|
||||
|
||||
## Contract Lifecycle
|
||||
CAD assignment and task acceptance are intentionally separate. Dispatch
|
||||
assignment reserves a contract for a group and marks the CAD assignment
|
||||
`assigned`; it does not start task logic. The assigned group leader must
|
||||
acknowledge the assignment before the task is accepted and ownership is bound.
|
||||
If the leader declines, CAD removes the assignment and the contract returns to
|
||||
the open board. Task status follows the same lifecycle: `available` on
|
||||
creation, `assigned` after dispatch assignment, and `active` after
|
||||
acknowledgement.
|
||||
|
||||
## Notes
|
||||
CAD hydrate payloads include active task catalog entries from `TaskStore` and
|
||||
CAD hydrate payloads include assignable task catalog entries from `TaskStore` and
|
||||
organization context from `ActorStore`.
|
||||
|
||||
@ -50,9 +50,10 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
if ((_y getOrDefault ["state", ""]) isNotEqualTo "acknowledged") then { continue; };
|
||||
if ((_y getOrDefault ["acknowledgedByUid", ""]) isEqualTo "") then { continue; };
|
||||
if ((_dispatchOrderRegistry getOrDefault [_x, createHashMap]) isNotEqualTo createHashMap) then { continue; };
|
||||
if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) isNotEqualTo "active") then { continue; };
|
||||
if !((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) in ["assigned", "active"]) then { continue; };
|
||||
|
||||
EGVAR(task,TaskStore) call ["bindTaskOwnership", [_x, _y getOrDefault ["acknowledgedByUid", ""]]];
|
||||
EGVAR(task,TaskStore) call ["acceptTask", [_x, _y getOrDefault ["acknowledgedByUid", ""]]];
|
||||
EGVAR(task,TaskStore) call ["setTaskStatus", [_x, "active"]];
|
||||
} forEach _assignmentRegistry;
|
||||
|
||||
_self set ["ownershipHydrated", true];
|
||||
@ -77,7 +78,7 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
};
|
||||
|
||||
private _status = EGVAR(task,TaskStore) call ["getTaskStatus", [_x]];
|
||||
if !(_status in ["active", ""]) then {
|
||||
if !(_status in ["available", "assigned", "active", ""]) then {
|
||||
_keysToRemove pushBack _x;
|
||||
};
|
||||
} forEach _assignmentRegistry;
|
||||
@ -145,7 +146,7 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
|
||||
private _dispatchOrder = +(_dispatchOrderRegistry getOrDefault [_x, createHashMap]);
|
||||
if (_dispatchOrder isEqualTo createHashMap) then {
|
||||
if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) isNotEqualTo "active") then { continue; };
|
||||
if !((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) in ["assigned", "active"]) then { continue; };
|
||||
_taskID = _x;
|
||||
} else {
|
||||
_taskID = _dispatchOrder getOrDefault ["title", _x];
|
||||
@ -235,8 +236,8 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
private _dispatchOrderRegistry = _state getOrDefault ["dispatchOrders", createHashMap];
|
||||
private _isDispatchOrder = (_dispatchOrderRegistry getOrDefault [_taskID, createHashMap]) isNotEqualTo createHashMap;
|
||||
|
||||
if (!_isDispatchOrder && { (EGVAR(task,TaskStore) call ["getTaskStatus", [_taskID]]) isNotEqualTo "active" }) exitWith {
|
||||
_result set ["message", "Task is no longer active."];
|
||||
if (!_isDispatchOrder && { (EGVAR(task,TaskStore) call ["getTaskStatus", [_taskID]]) isNotEqualTo "available" }) exitWith {
|
||||
_result set ["message", "Task is no longer available."];
|
||||
_result
|
||||
};
|
||||
|
||||
@ -300,23 +301,15 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
_activityRepository call ["appendEntry", [_activityEntry]];
|
||||
};
|
||||
|
||||
if (!_isDispatchOrder) then {
|
||||
EGVAR(task,TaskStore) call ["setTaskStatus", [_taskID, "assigned"]];
|
||||
};
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", _assignData getOrDefault ["message", ["Task assigned.", "Dispatch order assigned."] select _isDispatchOrder]];
|
||||
_result set ["assignment", _assignment];
|
||||
_result set ["leaderUid", _leaderUid];
|
||||
_result set ["isDispatchOrder", _isDispatchOrder];
|
||||
if (!_isDispatchOrder && { !(isNil QEGVAR(task,TaskStore)) }) then {
|
||||
private _acceptResult = EGVAR(task,TaskStore) call ["acceptTask", [_taskID, _leaderUid]];
|
||||
if !(_acceptResult getOrDefault ["success", false]) then {
|
||||
["WARNING", format [
|
||||
"CAD assigned task %1 to group %2 but could not mark it accepted for leader %3: %4",
|
||||
_taskID,
|
||||
_groupID,
|
||||
_leaderUid,
|
||||
_acceptResult getOrDefault ["message", "Unknown error."]
|
||||
]] call EFUNC(common,log);
|
||||
};
|
||||
};
|
||||
if (_isDispatchOrder) then {
|
||||
_result set ["order", +(_dispatchOrderRegistry getOrDefault [_taskID, createHashMap])];
|
||||
};
|
||||
@ -524,16 +517,18 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
switch (_transition) do {
|
||||
case "acknowledge": {
|
||||
if (!_isDispatchOrder) then {
|
||||
private _bindResult = EGVAR(task,TaskStore) call ["bindTaskOwnership", [_taskID, _requesterUid]];
|
||||
if !(_bindResult getOrDefault ["success", false]) exitWith {
|
||||
_result set ["message", _bindResult getOrDefault ["message", "Failed to bind task ownership."]];
|
||||
private _acceptResult = EGVAR(task,TaskStore) call ["acceptTask", [_taskID, _requesterUid]];
|
||||
if !(_acceptResult getOrDefault ["success", false]) exitWith {
|
||||
_result set ["message", _acceptResult getOrDefault ["message", "Failed to accept task."]];
|
||||
_result
|
||||
};
|
||||
EGVAR(task,TaskStore) call ["setTaskStatus", [_taskID, "active"]];
|
||||
};
|
||||
};
|
||||
case "decline": {
|
||||
if (!_isDispatchOrder) then {
|
||||
EGVAR(task,TaskStore) call ["releaseTaskOwnership", [_taskID]];
|
||||
EGVAR(task,TaskStore) call ["setTaskStatus", [_taskID, "available"]];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@ -100,7 +100,11 @@ system-generated content rather than a hand-authored task creation path.
|
||||
|
||||
### CAD Compatibility
|
||||
CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must
|
||||
have a catalog entry and active task status before CAD can show and assign it.
|
||||
have a catalog entry and a task status of `available`, `assigned`, or `active`
|
||||
before CAD can show it.
|
||||
CAD assignment reserves a task for a group, but task logic waits until the
|
||||
assigned group leader acknowledges the assignment. Declined assignments return
|
||||
to the open CAD board.
|
||||
|
||||
CAD-compatible creation paths:
|
||||
- Eden modules: compatible because they delegate to `fnc_startTask.sqf`
|
||||
@ -111,11 +115,11 @@ CAD-compatible creation paths:
|
||||
|
||||
Limited or incompatible paths:
|
||||
- `fnc_handler.sqf`: only compatible if a catalog entry was already registered
|
||||
elsewhere. The handler sets active status and ownership, but it does not
|
||||
elsewhere. The handler sets available status and ownership, but it does not
|
||||
create the BIS task shown in the map task tab or upsert the catalog entry
|
||||
- direct task function calls: not CAD-compatible by default. They bypass
|
||||
`fnc_startTask.sqf` and usually do not register the task catalog entry or
|
||||
active status that CAD hydrates from. They also only call
|
||||
available/assigned/active status that CAD hydrates from. They also only call
|
||||
`BIS_fnc_taskSetState` at completion/failure; they do not create the BIS task
|
||||
first
|
||||
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions
|
||||
* Registers Entity and starts heartbeat
|
||||
*
|
||||
* Arguments:
|
||||
* 0: The entity <OBJECT>
|
||||
* 1: Type of the entity <STRING>
|
||||
* 2: The countdown timer <NUMBER>
|
||||
*
|
||||
* Return Value:
|
||||
* None
|
||||
*
|
||||
* Example:
|
||||
* [_entity, "entity_type", 30] spawn FUNC(heartBeat);
|
||||
*
|
||||
* Public: Yes
|
||||
*/
|
||||
|
||||
params [["_entity", nil, [objNull, 0, [], sideUnknown, grpNull, ""]], ["_typeOf", "", [""]], ["_time", 0, [0]]];
|
||||
|
||||
private _nearPlayers = [];
|
||||
|
||||
switch (_typeOf) do {
|
||||
case "hostage": {
|
||||
_entity setCaptive true;
|
||||
_entity enableAIFeature ["MOVE", false];
|
||||
_entity playMove "acts_executionvictim_loop";
|
||||
|
||||
waitUntil {
|
||||
sleep 1;
|
||||
_nearPlayers = allPlayers inAreaArray [ASLToAGL getPosASL _entity, 2, 2, 0, false, 2];
|
||||
count _nearPlayers > 0
|
||||
};
|
||||
|
||||
private _nearPlayer = _nearPlayers select 0;
|
||||
|
||||
[_entity] joinSilent (group _nearPlayer);
|
||||
|
||||
// Keep rescued hostages protected while they follow the player group.
|
||||
_entity setCaptive true;
|
||||
_entity enableAIFeature ["MOVE", true];
|
||||
_entity playMove "acts_executionvictim_unbow";
|
||||
};
|
||||
case "hvt": {
|
||||
waitUntil {
|
||||
sleep 1;
|
||||
_nearPlayers = allPlayers inAreaArray [ASLToAGL getPosASL _entity, 2, 2, 0, false, 2];
|
||||
count _nearPlayers > 0
|
||||
};
|
||||
|
||||
_entity setCaptive true;
|
||||
doStop _entity;
|
||||
};
|
||||
case "ied": {
|
||||
private _taskID = _entity getVariable ["assignedTask", ""];
|
||||
if (_taskID isNotEqualTo "") then {
|
||||
waitUntil {
|
||||
sleep 1;
|
||||
GVAR(TaskStore) call ["isTaskAccepted", [_taskID]]
|
||||
};
|
||||
};
|
||||
|
||||
while { alive _entity && _time > 0} do {
|
||||
if (_time > 10) then { _entity say3D "FORGE_timerBeep" };
|
||||
if (_time <= 10 && _time > 5) then { _entity say3D "FORGE_timerBeepShort" };
|
||||
if (_time <= 5) then { _entity say3D "FORGE_timerEnd" };
|
||||
if (_time <= 0) exitWith { _entity setDamage 1 };
|
||||
|
||||
_time = _time -1;
|
||||
sleep 1;
|
||||
};
|
||||
|
||||
if (alive _entity && _time <= 0) then { _entity setDamage 1 };
|
||||
};
|
||||
};
|
||||
@ -71,7 +71,7 @@ if (_taskID isNotEqualTo "") then {
|
||||
]] call EFUNC(common,log);
|
||||
};
|
||||
|
||||
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "active"]];
|
||||
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "available"]];
|
||||
};
|
||||
|
||||
switch (_taskType) do {
|
||||
|
||||
@ -5,9 +5,9 @@
|
||||
* Initializes the task store for task entity tracking, participant
|
||||
* contribution tracking, and task outcome application.
|
||||
*
|
||||
* Task metadata is extension-backed but intentionally transient. The
|
||||
* task backend is reset when this store is created so task/catalog/status
|
||||
* state starts clean for each server or mission lifecycle.
|
||||
* Task metadata is extension-backed but intentionally transient. The task
|
||||
* backend is reset explicitly from task preInit so task/catalog/status state
|
||||
* starts clean before mission setup repopulates contracts.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
@ -37,7 +37,6 @@ GVAR(TaskStore) = createHashMapObject [[
|
||||
["targets", createHashMap]
|
||||
]];
|
||||
|
||||
_self call ["resetMissionState", []];
|
||||
}],
|
||||
["resetMissionState", compileFinal {
|
||||
_self set ["participantRegistry", createHashMap];
|
||||
@ -62,6 +61,7 @@ GVAR(TaskStore) = createHashMapObject [[
|
||||
false
|
||||
};
|
||||
|
||||
["INFO", "Task backend state reset for mission lifecycle."] call EFUNC(common,log);
|
||||
true
|
||||
}],
|
||||
["callTaskStateEnvelope", compileFinal {
|
||||
|
||||
@ -126,6 +126,14 @@ private _result = "forge_server" callExtension ["cad:orders:create_from_context"
|
||||
|
||||
## Assignment Workflow
|
||||
|
||||
Task contracts have two separate phases. Dispatch assignment reserves a
|
||||
contract for a group and sets the CAD assignment state to `assigned`, but it
|
||||
does not accept or start the task. The assigned group leader must acknowledge
|
||||
the assignment before task ownership is bound and task logic starts. If the
|
||||
leader declines, the CAD assignment is removed and the contract returns to the
|
||||
open board. Task status follows the same lifecycle: `available` on creation,
|
||||
`assigned` after dispatch assignment, and `active` after acknowledgement.
|
||||
|
||||
```sqf
|
||||
private _assignment = createHashMapFromArray [
|
||||
["groupId", "bravo"],
|
||||
|
||||
@ -145,7 +145,12 @@ system-generated content rather than a hand-authored task creation path.
|
||||
## CAD Compatibility
|
||||
|
||||
CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must
|
||||
have a catalog entry and active task status before CAD can show and assign it.
|
||||
have a catalog entry and a task status of `available`, `assigned`, or `active`
|
||||
before CAD can show it.
|
||||
CAD assignment only reserves a task for a group. The task is accepted and task
|
||||
logic starts after the assigned group leader acknowledges the assignment. If
|
||||
the leader declines, the CAD assignment is removed and the task returns to the
|
||||
open contract board.
|
||||
|
||||
CAD-compatible creation paths:
|
||||
|
||||
@ -159,7 +164,7 @@ CAD-compatible creation paths:
|
||||
Limited or incompatible paths:
|
||||
|
||||
- `forge_server_task_fnc_handler`: only compatible if a catalog entry was
|
||||
already registered elsewhere. The handler sets active status and ownership,
|
||||
already registered elsewhere. The handler sets available status and ownership,
|
||||
but it does not create the BIS task shown in the map task tab or upsert the
|
||||
catalog entry.
|
||||
- Direct task function calls: not CAD-compatible by default. They bypass
|
||||
@ -220,7 +225,8 @@ Use these rules for every Forge task:
|
||||
- `CBRNZone`
|
||||
3. Prefer `RECTANGLE` or `ELLIPSE` markers with real size.
|
||||
4. Set success and fail limits explicitly instead of relying on defaults.
|
||||
5. If a task uses a timer, the countdown now waits until the task is assigned.
|
||||
5. If a task uses a timer, the countdown now waits until the assigned group
|
||||
leader acknowledges the task.
|
||||
6. Grouping modules such as `Explosive Entities`, `Protected Entities`,
|
||||
`Cargo`, `Hostages`, and `Shooters` should be synced to real world objects,
|
||||
not other logic modules.
|
||||
@ -368,7 +374,8 @@ Notes:
|
||||
|
||||
- Hostages and shooters are filtered to real units only.
|
||||
- Hostages are protected immediately on task registration to avoid startup race conditions.
|
||||
- The hostage timer now waits until the task is assigned before counting down.
|
||||
- The hostage timer now waits until the assigned group leader acknowledges the
|
||||
task before counting down.
|
||||
- `ExtZone` is checked with `inArea`, so it must be an area marker.
|
||||
|
||||
### HVT Task
|
||||
@ -395,7 +402,8 @@ Notes:
|
||||
|
||||
- Capture mode uses `ExtZone` with `inArea`, so use an area marker.
|
||||
- Elimination mode does not require an extraction zone.
|
||||
- The HVT timer now waits until the task is assigned before counting down.
|
||||
- The HVT timer now waits until the assigned group leader acknowledges the task
|
||||
before counting down.
|
||||
|
||||
### Defend Task
|
||||
|
||||
|
||||
@ -144,7 +144,12 @@ system-generated content rather than a hand-authored task creation path.
|
||||
## CAD Compatibility
|
||||
|
||||
CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must
|
||||
have a catalog entry and active task status before CAD can show and assign it.
|
||||
have a catalog entry and a task status of `available`, `assigned`, or `active`
|
||||
before CAD can show it.
|
||||
CAD assignment only reserves a task for a group. The task is accepted and task
|
||||
logic starts after the assigned group leader acknowledges the assignment. If
|
||||
the leader declines, the CAD assignment is removed and the task returns to the
|
||||
open contract board.
|
||||
|
||||
CAD-compatible creation paths:
|
||||
|
||||
@ -158,7 +163,7 @@ CAD-compatible creation paths:
|
||||
Limited or incompatible paths:
|
||||
|
||||
- `forge_server_task_fnc_handler`: only compatible if a catalog entry was
|
||||
already registered elsewhere. The handler sets active status and ownership,
|
||||
already registered elsewhere. The handler sets available status and ownership,
|
||||
but it does not create the BIS task shown in the map task tab or upsert the
|
||||
catalog entry.
|
||||
- Direct task function calls: not CAD-compatible by default. They bypass
|
||||
@ -219,7 +224,8 @@ Use these rules for every Forge task:
|
||||
- `CBRNZone`
|
||||
3. Prefer `RECTANGLE` or `ELLIPSE` markers with real size.
|
||||
4. Set success and fail limits explicitly instead of relying on defaults.
|
||||
5. If a task uses a timer, the countdown now waits until the task is assigned.
|
||||
5. If a task uses a timer, the countdown now waits until the assigned group
|
||||
leader acknowledges the task.
|
||||
6. Grouping modules such as `Explosive Entities`, `Protected Entities`,
|
||||
`Cargo`, `Hostages`, and `Shooters` should be synced to real world objects,
|
||||
not other logic modules.
|
||||
@ -367,7 +373,8 @@ Notes:
|
||||
|
||||
- Hostages and shooters are filtered to real units only.
|
||||
- Hostages are protected immediately on task registration to avoid startup race conditions.
|
||||
- The hostage timer now waits until the task is assigned before counting down.
|
||||
- The hostage timer now waits until the assigned group leader acknowledges the
|
||||
task before counting down.
|
||||
- `ExtZone` is checked with `inArea`, so it must be an area marker.
|
||||
|
||||
### HVT Task
|
||||
@ -394,7 +401,8 @@ Notes:
|
||||
|
||||
- Capture mode uses `ExtZone` with `inArea`, so use an area marker.
|
||||
- Elimination mode does not require an extraction zone.
|
||||
- The HVT timer now waits until the task is assigned before counting down.
|
||||
- The HVT timer now waits until the assigned group leader acknowledges the task
|
||||
before counting down.
|
||||
|
||||
### Defend Task
|
||||
|
||||
|
||||
@ -124,6 +124,14 @@ private _result = "forge_server" callExtension ["cad:orders:create_from_context"
|
||||
|
||||
## Assignment Workflow
|
||||
|
||||
Task contracts have two separate phases. Dispatch assignment reserves a
|
||||
contract for a group and sets the CAD assignment state to `assigned`, but it
|
||||
does not accept or start the task. The assigned group leader must acknowledge
|
||||
the assignment before task ownership is bound and task logic starts. If the
|
||||
leader declines, the CAD assignment is removed and the contract returns to the
|
||||
open board. Task status follows the same lifecycle: `available` on creation,
|
||||
`assigned` after dispatch assignment, and `active` after acknowledgement.
|
||||
|
||||
```sqf
|
||||
private _assignment = createHashMapFromArray [
|
||||
["groupId", "bravo"],
|
||||
|
||||
@ -49,7 +49,7 @@ impl<R: TaskRepository> TaskStateService<R> {
|
||||
let mut active_entries = Vec::new();
|
||||
|
||||
for (task_id, status) in active_statuses {
|
||||
if status != "active" {
|
||||
if !matches!(status.as_str(), "available" | "assigned" | "active") {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -129,8 +129,11 @@ impl<R: TaskRepository> TaskStateService<R> {
|
||||
return Err("Missing task ID or requester UID.".to_string());
|
||||
}
|
||||
|
||||
if self.get_status(entry_id.clone())? != "active" {
|
||||
return Err("Task is no longer active.".to_string());
|
||||
if !matches!(
|
||||
self.get_status(entry_id.clone())?.as_str(),
|
||||
"assigned" | "active"
|
||||
) {
|
||||
return Err("Task is not assigned or active.".to_string());
|
||||
}
|
||||
|
||||
if let Some(existing) = self.repository.get_ownership(&entry_id)?
|
||||
@ -381,9 +384,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_active_catalog_only_returns_active_entries() {
|
||||
fn list_active_catalog_returns_assignable_and_active_entries() {
|
||||
let service = TaskStateService::new(InMemoryTaskRepository::new());
|
||||
|
||||
service
|
||||
.upsert_catalog_entry(
|
||||
"task-available".to_string(),
|
||||
r#"{"title":"Available"}"#.to_string(),
|
||||
)
|
||||
.expect("available catalog upsert should succeed");
|
||||
service
|
||||
.upsert_catalog_entry(
|
||||
"task-assigned".to_string(),
|
||||
r#"{"title":"Assigned"}"#.to_string(),
|
||||
)
|
||||
.expect("assigned catalog upsert should succeed");
|
||||
service
|
||||
.upsert_catalog_entry(
|
||||
"task-active".to_string(),
|
||||
@ -393,6 +408,12 @@ mod tests {
|
||||
service
|
||||
.upsert_catalog_entry("task-done".to_string(), r#"{"title":"Done"}"#.to_string())
|
||||
.expect("done catalog upsert should succeed");
|
||||
service
|
||||
.set_status("task-available".to_string(), "available".to_string())
|
||||
.expect("available status update should succeed");
|
||||
service
|
||||
.set_status("task-assigned".to_string(), "assigned".to_string())
|
||||
.expect("assigned status update should succeed");
|
||||
service
|
||||
.set_status("task-active".to_string(), "active".to_string())
|
||||
.expect("active status update should succeed");
|
||||
@ -404,10 +425,14 @@ mod tests {
|
||||
.list_active_catalog()
|
||||
.expect("active catalog should build");
|
||||
|
||||
assert_eq!(active_catalog.len(), 1);
|
||||
assert_eq!(
|
||||
active_catalog[0].get("taskId").and_then(Value::as_str),
|
||||
Some("task-active")
|
||||
);
|
||||
let task_ids: Vec<_> = active_catalog
|
||||
.iter()
|
||||
.filter_map(|entry| entry.get("taskId").and_then(Value::as_str))
|
||||
.collect();
|
||||
|
||||
assert_eq!(active_catalog.len(), 3);
|
||||
assert!(task_ids.contains(&"task-available"));
|
||||
assert!(task_ids.contains(&"task-assigned"));
|
||||
assert!(task_ids.contains(&"task-active"));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user