120 lines
6.0 KiB
Diff
120 lines
6.0 KiB
Diff
--- r1/packages/tasks/tests/verbs.test.mjs
|
|
+++ r2/packages/tasks/tests/verbs.test.mjs
|
|
@@ -312,15 +312,15 @@
|
|
await sleep(20);
|
|
assert.deepEqual(await w.adapter.tick('demo'), { snapshots: 0, events: 0 });
|
|
});
|
|
-// Arms a fault on the final read: after `trigger` answers, the next GET of task 1 fails.
|
|
-const thenFailRead = (trigger) => {
|
|
+// Arms a fault on the final read: after `trigger` answers, the next GET of task `id` fails.
|
|
+const thenFailRead = (trigger, id = 1) => {
|
|
const fake = new FakeVikunja();
|
|
let armed = false;
|
|
const fetch = async (url, init = {}) => {
|
|
const res = await fake.fetch(url, init);
|
|
if (armed && trigger(init.method ?? 'GET', new URL(url).pathname)) {
|
|
armed = false;
|
|
- fake.fault('GET', /^\/tasks\/1$/, 0);
|
|
+ fake.fault('GET', new RegExp(`^/tasks/${id}$`), 0);
|
|
}
|
|
return res;
|
|
};
|
|
@@ -412,13 +412,13 @@
|
|
});
|
|
// Row B: a tree read with its descendants, as in tree.test.mjs. The child is installed before
|
|
// start, so the first reconcile reads it with its own board; the grandchild keeps Vikunja's
|
|
-// default buckets, so it is watch-only.
|
|
+// default buckets, so it is watch-only. tree()'s `edit` goes to world() with TREE.
|
|
const TREE = {
|
|
tracker: { descendants: true },
|
|
scopes: { sync: { ...SCOPES.sync, projects: [...SCOPES.sync.projects, 'read_all'] } },
|
|
};
|
|
-const tree = async (t) => {
|
|
- const w = await world(t, TREE);
|
|
+const tree = async (t, edit = {}) => {
|
|
+ const w = await world(t, { ...TREE, ...edit });
|
|
const child = w.fake.project('Child', w.owner, { parent: w.project });
|
|
const board = w.fake.install(child);
|
|
const grandchild = w.fake.project('Grandchild', w.owner, { parent: child });
|
|
@@ -568,3 +568,64 @@
|
|
assert.deepEqual(w.fake.task(id).assignees, [w.bots.coder]);
|
|
assert.equal(w.snapshots(`vikunja:${child}/${id}`).at(-1).fields.bucket, board.buckets.todo);
|
|
});
|
|
+// The child versions of the re-read and fallback paths the root's tests above cover (Dewey's
|
|
+// and Filbert's round 1 reviews): each fails if its path goes back to the root's project or view.
|
|
+test('a verb and a create in the child record the final read of the child\'s board', async (t) => {
|
|
+ const { w, child } = await tree(t);
|
|
+ const pm = w.agent('pm');
|
|
+ const made = await task(w, { project: child });
|
|
+ assert.notEqual(w.snapshots(made.task_ref).at(-1).etag, null);
|
|
+ await w.call(pm, 'task.assign', { task_ref: made.task_ref, role: 'coder' });
|
|
+ assert.notEqual(w.snapshots(made.task_ref).at(-1).etag, null);
|
|
+});
|
|
+test('a create in the child whose final read fails records the child\'s todo bucket', async (t) => {
|
|
+ // The grandchild's task is 1, so the create makes task 2.
|
|
+ const f = thenFailRead((m, p) => m === 'POST' && /\/projects\/\d+\/tasks$/.test(p), 2);
|
|
+ const { w, child, board } = await tree(t, f.edit);
|
|
+ w.agent('pm');
|
|
+ f.arm();
|
|
+ const r = await task(w, { project: child });
|
|
+ assert.equal(r.task_ref, `vikunja:${child}/2`);
|
|
+ assert.equal(w.events('task.created').length, 1);
|
|
+ const s = w.snapshots(r.task_ref).at(-1);
|
|
+ assert.equal(s.source, 'self');
|
|
+ assert.equal(s.etag, null);
|
|
+ assert.equal(s.fields.bucket, board.buckets.todo);
|
|
+ await sleep(20);
|
|
+ await w.adapter.tick('demo');
|
|
+ // The start's reconcile recorded the grandchild's task as external; the create isn't.
|
|
+ assert.deepEqual(w.events('task.changed.external').map((e) => e.subject), [`vikunja:${w.fake.task(1).project}/1`]);
|
|
+});
|
|
+test('a lost answer in the child is settled by a re-read of the child\'s board', async (t) => {
|
|
+ const { w, child, board } = await tree(t);
|
|
+ const pm = w.agent('pm');
|
|
+ const coder = w.agent('coder');
|
|
+ const { task_ref } = await task(w, { project: child });
|
|
+ await w.call(pm, 'task.assign', { task_ref, role: 'coder' });
|
|
+ // A state move and a close that landed but answered 500: each re-read shows it, so each succeeds.
|
|
+ w.fake.fault('PUT', /\/buckets\/\d+\/tasks$/, 500, { apply: true });
|
|
+ await w.call(coder, 'task.update.assigned', { task_ref, state: 'in-progress' });
|
|
+ assert.equal(w.events('task.state').length, 1);
|
|
+ assert.equal(w.snapshots(task_ref).at(-1).fields.bucket, board.buckets['in-progress']);
|
|
+ w.fake.fault('PUT', /\/buckets\/\d+\/tasks$/, 500, { apply: true });
|
|
+ await w.call(pm, 'task.close', { task_ref, verdict: 'queue:63' });
|
|
+ assert.equal(w.events('task.closed').length, 1);
|
|
+ assert.equal(w.snapshots(task_ref).at(-1).fields.bucket, board.buckets.done);
|
|
+ assert.equal(w.fake.requests.filter((r) => r.method === 'PUT').length, 2);
|
|
+});
|
|
+test('every related task is read before authority, not only the first', async (t) => {
|
|
+ const { w, child } = await tree(t);
|
|
+ const pm = w.agent('pm');
|
|
+ const root = await task(w);
|
|
+ const other = await task(w);
|
|
+ const id = Number(other.task_ref.split('/')[1]);
|
|
+ const before = writes(w);
|
|
+ await assert.rejects(
|
|
+ w.call(pm, 'task.schedule', {
|
|
+ task_ref: root.task_ref,
|
|
+ relations: { add: [{ kind: 'related', task_ref: other.task_ref }, { kind: 'blocking', task_ref: `vikunja:${child}/${id}` }] },
|
|
+ }),
|
|
+ refusedWith('task-moved'),
|
|
+ );
|
|
+ assert.equal(writes(w), before);
|
|
+});
|
|
--- r1/packages/tasks/README.md
|
|
+++ r2/packages/tasks/README.md
|
|
@@ -191,9 +191,10 @@
|
|
in, the root when left out. Any other project than an installed one of
|
|
the tree refuses with `project-not-installed`, and a value that isn't a
|
|
positive integer with `invalid-request`. The decision a create may
|
|
- need is bound to no target, so it doesn't name the project. `task.created` is recorded even when a label or relation
|
|
- write after the create fails. The task exists, and the verb then
|
|
- refuses with that write's code.
|
|
+ need is bound to no target, so it doesn't name the project.
|
|
+ `task.created` is recorded even when a label or relation write after
|
|
+ the create fails. The task exists, and the verb then refuses with that
|
|
+ write's code.
|
|
- `task.assign` refuses a task a bot already holds (`task-assigned`).
|
|
`task.reassign` refuses one no bot holds (`task-unassigned`). Both leave
|
|
people's assignments alone, and remove only bots the compare-read
|