packages/tasks adds the Vikunja v2 client, the eight task verbs, the
board-plus-cursor poll with its 60 s window and the digest. The broker
gains the task verbs and boots trackers from the boot config (lead
decisions 66 to 68). Due dates are truncated to the second and recorded
as truncated (B1). A write that lands but whose final read fails counts
as landed, in update and in create (B2).
Candidate agents/darkwing/work/slice1-s3, build-r2.patch 71ce87e6,
manifest e10e30e3 (28 files). Filbert approved round 2 on #1520
(comment 26853). Darkwing's post-reset rerun: test-release 14/14,
test-task 98/98 (comment 26857).
Integration gate in a worktree on c4baf779 with the patch applied:
bus 67, business 60, control-board 124, discord 173, ledger 78,
mosaic 69, queue 148, seat 19, tasks 51 and webui 14, all with no
failures. Conversation is 149/3. The three cohort kill cases (K1, K3,
K10) fail the same on the unpatched base, and the patch doesn't touch
the package. Every scripts/test-*.sh is green. test-release 14/14 and
test-task 98/98 ran on the existing gate2 compose network, because the
host's Docker address pools are exhausted. No network was created or
pruned.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
101 lines
4.9 KiB
JavaScript
101 lines
4.9 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { world, SCOPES } from './world.mjs';
|
|
import { MISSING, parseVersion } from '../src/startup.mjs';
|
|
const started = async (t, edit) => {
|
|
const w = await world(t, edit);
|
|
await w.adapter.start('demo');
|
|
return w;
|
|
};
|
|
const state = (w) => w.adapter.status()[0];
|
|
test('a correct install starts, and the first reconcile records tasks that already exist', async (t) => {
|
|
const w = await world(t);
|
|
await w.ui('POST', `/projects/${w.project}/tasks`, { title: 'made in the UI' });
|
|
await w.adapter.start('demo');
|
|
assert.equal(state(w).state, 'ready');
|
|
assert.equal(state(w).untested, false);
|
|
const s = w.snapshots(`vikunja:${w.project}/1`);
|
|
assert.equal(s.length, 1);
|
|
assert.equal(s[0].via, 'reconcile');
|
|
assert.equal(s[0].fields.bucket, w.installed.buckets.todo);
|
|
// The probes: missing-task reads and write probes, all on the fixed missing id.
|
|
const probes = w.fake.requests.filter((r) => r.path.includes(String(MISSING)));
|
|
assert.deepEqual(
|
|
probes.map((r) => `${r.method} ${r.status}`),
|
|
['PATCH 401', 'GET 404', 'DELETE 401', 'GET 404', 'DELETE 401', 'POST 401', 'GET 404', 'DELETE 401', 'POST 401'],
|
|
);
|
|
});
|
|
test('verbs refuse while a business is starting and after startup refused it', async (t) => {
|
|
const w = await world(t, { scopes: { sync: '*' } });
|
|
const pm = w.agent('pm');
|
|
await assert.rejects(w.call(pm, 'task.close', { task_ref: 'vikunja:1/1', verdict: 'v' }), /tracker-starting/);
|
|
await w.adapter.start('demo');
|
|
assert.equal(state(w).refused, 'scope-too-broad');
|
|
await assert.rejects(w.call(pm, 'task.close', { task_ref: 'vikunja:1/1', verdict: 'v' }), /scope-too-broad/);
|
|
assert.deepEqual(w.log, ['tasks demo startup scope-too-broad']);
|
|
// The refusal is evidence too.
|
|
assert.equal(w.events('action.refused').at(-1).body.code, 'scope-too-broad');
|
|
});
|
|
test('startup refuses a token that can do more than its role needs', async (t) => {
|
|
for (const [scopes, code] of [
|
|
[{ sync: { ...SCOPES.sync, tasks: ['read_all', 'read_one', 'update'] } }, 'scope-too-broad'],
|
|
[{ pm: { ...SCOPES.pm, tasks: ['read_one', 'create', 'update', 'delete'] } }, 'scope-too-broad'],
|
|
[{ coder: { ...SCOPES.worker, tasks_labels: ['create'] } }, 'scope-too-broad'],
|
|
[{ coder: { ...SCOPES.worker, tasks: ['update'] } }, 'tracker-unauthorized'],
|
|
]) {
|
|
const w = await started(t, { scopes });
|
|
assert.equal(state(w).refused, code, JSON.stringify(scopes));
|
|
}
|
|
});
|
|
test('startup refuses an unsupported version and flags an untested one', async (t) => {
|
|
assert.equal(state(await started(t, { fake: { version: 'v1.0.0' } })).refused, 'tracker-version');
|
|
assert.equal(state(await started(t, { fake: { version: 'v2.3.9' } })).refused, 'tracker-version');
|
|
const w = await started(t, { fake: { version: 'v2.8.1' } });
|
|
assert.equal(state(w).state, 'ready');
|
|
assert.equal(state(w).untested, true);
|
|
assert.deepEqual(w.log, ['tasks demo startup untested-version']);
|
|
assert.deepEqual(parseVersion('v2.7.0-rc1'), [2, 7, 0]);
|
|
assert.equal(parseVersion('unstable'), null);
|
|
});
|
|
test('startup refuses a board that the runbook did not install', async (t) => {
|
|
assert.equal(state(await started(t, { install: false })).refused, 'tracker-install');
|
|
const extra = await world(t);
|
|
extra.fake.addView(extra.project, 'kanban');
|
|
await extra.adapter.start('demo');
|
|
assert.equal(state(extra).refused, 'tracker-install');
|
|
const renamed = await world(t);
|
|
renamed.fake.rename(renamed.installed.buckets.blocked, 'on-hold');
|
|
await renamed.adapter.start('demo');
|
|
assert.equal(state(renamed).refused, 'tracker-install');
|
|
const wiring = await world(t);
|
|
wiring.fake.view(wiring.installed.view).done_bucket_id = wiring.installed.buckets['in-review'];
|
|
await wiring.adapter.start('demo');
|
|
assert.equal(state(wiring).refused, 'tracker-install');
|
|
});
|
|
test('startup refuses a project the sync bot cannot read', async (t) => {
|
|
const w = await started(t, { tracker: { project: 99 } });
|
|
assert.equal(state(w).refused, 'tracker-project');
|
|
});
|
|
test('startup refuses a configured label the pm bot cannot see', async (t) => {
|
|
const w = await started(t, {
|
|
business: (b, { fake }) => {
|
|
const stranger = fake.user('someone');
|
|
b.tracker.labels.private = fake.label('private', stranger);
|
|
},
|
|
});
|
|
assert.equal(state(w).refused, 'tracker-labels');
|
|
});
|
|
test('startup refuses an expired credential and a missing sync credential', async (t) => {
|
|
assert.equal(state(await started(t, { expires: { coder: '2001-01-01' } })).refused, 'credential-expired');
|
|
const w = await started(t, { business: (b) => delete b.tracker.sync.credentials });
|
|
assert.equal(state(w).refused, 'credential-unavailable');
|
|
});
|
|
test('an unreachable tracker refuses with tracker-unavailable', async (t) => {
|
|
const w = await started(t, {
|
|
fetch: async () => {
|
|
throw new TypeError('fetch failed');
|
|
},
|
|
});
|
|
assert.equal(state(w).refused, 'tracker-unavailable');
|
|
});
|