Repeating tasks and subtasks
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4625848048
commit
e26b38a119
6 files changed
+119
-19
No files matched your search
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env node
|
||||
// Tasks: repeating tasks roll forward, subtasks nest one level and go away with their parent.
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs'), os = require('os'), path = require('path'), crypto = require('crypto');
|
||||
const root = path.join(__dirname, '..'), dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-tasks-'));
|
||||
const port = 20000 + Math.floor(Math.random() * 20000), base = `http://127.0.0.1:${port}`;
|
||||
let failed = 0;
|
||||
const ok = (name, cond, extra) => { console.log((cond ? 'PASS ' : 'FAIL ') + name + (cond ? '' : ' -> ' + JSON.stringify(extra))); if (!cond) failed++; };
|
||||
const SECRET = 'tasks-'.repeat(8), UID = 'utasks';
|
||||
fs.writeFileSync(path.join(dir, 'users.json'), JSON.stringify({ users: [{ id: UID, name: 'Admin', role: 'admin', tabs: [], handle: 'aGFuZGxl', epoch: 1, disabled: false, created: Date.now() }] }));
|
||||
const child = spawn(process.execPath, ['server.js'], { cwd: root, env: { ...process.env, PORT: String(port), DATA_DIR: dir, COOKIE_SECRET: SECRET } });
|
||||
const exp = Date.now() + 864e5, cookie = `hub_session=${UID}.1.${exp}.${crypto.createHmac('sha256', SECRET).update(`s${UID}.1.${exp}`).digest('base64url')}`;
|
||||
const call = async (p, body, method) => { const r = await fetch(base + p, { method: method || (body ? 'POST' : 'GET'), headers: { cookie, 'content-type': 'application/json' }, body: body && JSON.stringify(body) }); return { status: r.status, json: await r.json().catch(() => ({})) }; };
|
||||
const ymd = d => d.toISOString().slice(0, 10), plus = n => { const d = new Date(); d.setUTCDate(d.getUTCDate() + n); return ymd(d); };
|
||||
const wait = ms => new Promise(r => setTimeout(r, ms));
|
||||
(async () => {
|
||||
for (let i = 0; i < 50; i++) { try { await fetch(base + '/login'); break; } catch { await wait(100); } }
|
||||
const L = 'local:inbox', list = async () => (await call('/api/tasks?list=' + L)).json;
|
||||
await call('/api/tasks', { list: L, fields: { title: 'Water plants', notes: '', due: plus(-3), done: false, repeat: 'daily' } });
|
||||
await call('/api/tasks', { list: L, fields: { title: 'Plain', notes: '', due: '', done: false } });
|
||||
let ts = await list(), rep = ts.find(t => t.title === 'Water plants'), plain = ts.find(t => t.title === 'Plain');
|
||||
ok('a task remembers that it repeats', rep.repeat === 'daily' && plain.repeat === 'none', ts);
|
||||
let r = await call('/api/tasks/toggle', { list: L, id: rep.id, done: true });
|
||||
ts = await list(); rep = ts.find(t => t.title === 'Water plants');
|
||||
ok('completing a repeating task moves it to the next date (after today) and keeps it open', r.json.rolled === plus(1) && rep.due === plus(1) && !rep.done, [r.json, rep]);
|
||||
r = await call('/api/tasks/toggle', { list: L, id: plain.id, done: true }); ts = await list();
|
||||
ok('a plain task is simply completed', ts.find(t => t.title === 'Plain').done === true && !r.json.rolled, r.json);
|
||||
r = await call('/api/tasks', { list: L, id: rep.id, fields: { title: 'Water plants', notes: '', due: rep.due, done: false, repeat: 'bogus' } }); ts = await list();
|
||||
ok('an unknown repeat is treated as none', ts.find(t => t.id === rep.id).repeat === 'none', ts);
|
||||
await call('/api/tasks', { list: L, fields: { title: 'Sub 1', notes: '', due: '', done: false, parent: rep.id } });
|
||||
ts = await list(); const sub = ts.find(t => t.title === 'Sub 1');
|
||||
ok('a subtask is stored under its parent', sub && sub.parent === rep.id, ts);
|
||||
r = await call('/api/tasks', { list: L, fields: { title: 'Deep', notes: '', due: '', done: false, parent: sub.id } });
|
||||
ok('a subtask cannot have subtasks of its own', r.status === 400, r);
|
||||
r = await call('/api/tasks', { list: L, fields: { title: 'Orphan', notes: '', due: '', done: false, parent: 'nope' } });
|
||||
ok('a missing parent is refused', r.status === 400, r);
|
||||
r = await call('/api/tasks/delete', { list: L, id: rep.id });
|
||||
ts = await list(); ok('deleting the parent deletes its subtasks', !ts.some(t => t.id === rep.id || t.id === sub.id), ts);
|
||||
ok('the server is still running', child.exitCode === null);
|
||||
child.kill(); fs.rmSync(dir, { recursive: true, force: true });
|
||||
console.log(failed ? `\n${failed} check(s) failed` : '\nAll task checks passed'); process.exit(failed ? 1 : 0);
|
||||
})();
|
||||
Reference in new issue
Block a user