Files
stack/agents/filbert/work/queue-63/r2/round-diff.patch
T

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