Skip to content

Commit 0fd03ea

Browse files
committed
refactor(areas): make the area table user-configurable
The area table was the author's own project list, hardcoded four times over — NOTION_AREAS, AREA_TO_FILE, MORGEN_AREAS, and a hand-maintained SAFE_PATH_RE — plus the same names in the shipped n8n workflow templates, the sample sync state, the docs, and the tests. This repo is public, so that published a client roster, and the table was wrong for every user but one. Areas now come from TASKMAXXING_AREAS via the new src/areas.js. The four structures are derived from one config, so they cannot drift apart, and the path-traversal allowlist is generated from the configured paths rather than hand-kept alongside them. Optional `pathPrefix` and `aliasFiles` cover nested task folders and query-only parent hubs, which were previously special-cased in parseArea by name. Defaults are the two non-identifying areas, URGENT and GENERAL. Degradation is deliberate: an unset, malformed, GENERAL-less, duplicate-keyed, or traversal- bearing config is rejected whole and the defaults apply, with a stderr warning naming the failed check. Everything then files under GENERAL — a half-applied table would route tasks into the wrong project, which is worse. Also fixed in passing: an operator-precedence bug in the urgent co-tag guard (&& binds tighter than ||), and a crash on the no-config path where the built-in defaults were spread without an aliasFiles key. New scripts/test-areas-config.js covers the degradation paths and asserts regex metacharacters in configured paths are escaped rather than interpreted. Wired into `npm test`. 61 tests pass across the three suites. BREAKING: set TASKMAXXING_AREAS to keep per-project routing. Documented under "Configuring your areas" in the README.
1 parent 91845df commit 0fd03ea

15 files changed

Lines changed: 453 additions & 127 deletions

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
1414
- README: tagline + workflow table + project tree updated 3-way → 2-way. A top-of-README NOTE banner explains the cutover.
1515

1616
### Why
17-
- Kit author's own instance (`obsidian-tasks-sync`) dropped Notion on the same date after the Notion bearer in W1+W3 was found to be silently 401-ing on every orchestrator tick. Morgen and Obsidian had become the only legs that mattered. The full investigation is in the kit author's vault under `05-Projects/LAVA-NET/invoices/2026-05-04-morgen-task-creation-and-sync-diagnosis.md` and the cutover memory at `project_notion_drop_2026_05_04.md`.
17+
- Kit author's own instance (`the author's own private sync repo`) dropped Notion on the same date after the Notion bearer in W1+W3 was found to be silently 401-ing on every orchestrator tick. Morgen and Obsidian had become the only legs that mattered. The full investigation is in the kit author's vault under `05-Projects/PROJECT-D/invoices/2026-05-04-morgen-task-creation-and-sync-diagnosis.md` and the cutover memory at `project_notion_drop_2026_05_04.md`.
1818

1919
### Added
2020
- README: social-links badge strip (X · LinkedIn · YouTube · Instagram, ruvnet-style for-the-badge) inserted into the centered header block beneath the project license badge.

README.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ Bidirectional. Edit anywhere; it converges everywhere. Two sub-workflows, one or
9999
100100
┌────────────▼───────────┐
101101
│ GitHub │
102-
obsidian-tasks-sync │
102+
the author's own private sync repo
103103
└─▲────────────────────┬─┘
104104
│ │
105105
W2 commits│ │ W1 reads
@@ -251,6 +251,49 @@ task-maxxing/
251251

252252
---
253253

254+
## Configuring your areas
255+
256+
An "area" is one task file plus the Morgen tag and Notion label it maps to. The
257+
area table is **yours** — this repo ships only two generic areas (`URGENT`,
258+
`GENERAL`) and reads the rest from the `TASKMAXXING_AREAS` env var, a JSON array:
259+
260+
```json
261+
[
262+
{ "key": "URGENT", "file": "TASKS-URGENT.md",
263+
"morgenLabel": "Urgent", "notionLabel": "01 URGENT" },
264+
{ "key": "GENERAL", "file": "TASKS-GENERAL.md",
265+
"morgenLabel": "General", "notionLabel": "02 GENERAL" },
266+
{ "key": "PROJECT-A", "file": "TASKS-PROJECT-A.md",
267+
"morgenLabel": "Project-A", "notionLabel": "03 PROJECT-A" },
268+
{ "key": "NESTED", "file": "NESTED/content/TASKS-NESTED-content.md",
269+
"morgenLabel": "Nested", "notionLabel": "04 NESTED",
270+
"pathPrefix": "NESTED/content/",
271+
"aliasFiles": ["NESTED/TASKS-NESTED.md"] }
272+
]
273+
```
274+
275+
| Field | Required | Meaning |
276+
|---|---|---|
277+
| `key` | yes | Internal area key, uppercase. Also matched as `TASKS-<key>.md`. |
278+
| `file` | yes | Path relative to your tasks directory. |
279+
| `morgenLabel` | yes | Morgen tag label — no number prefix, the chips read better without one. |
280+
| `notionLabel` | yes | Notion select label — keep the number prefix, it drives sort order. |
281+
| `pathPrefix` | no | Any path under this prefix maps to this area. |
282+
| `aliasFiles` | no | Extra paths that resolve here and are safe to write, e.g. a query-only parent hub. |
283+
284+
A `GENERAL` entry is required — it is the fallback for any path the table does
285+
not recognise.
286+
287+
**Degradation is deliberate.** If the variable is unset, malformed, missing
288+
`GENERAL`, or contains a duplicate key or a traversal path, the whole config is
289+
rejected and the built-in defaults apply, with a warning on stderr saying which
290+
check failed. Everything then files under `GENERAL`. A half-applied area table
291+
would route tasks into the *wrong* project, which is worse than routing them all
292+
to one place.
293+
294+
The write allowlist that guards against path traversal is generated from this
295+
table, so it can't drift out of step with it.
296+
254297
## Status
255298

256299
**Alpha.** Running in production on my vault since early 2026, but it's had exactly one user. Looking for testers who:

docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ Two identifiers do different jobs:
398398

399399
Hash format: SHA-256 of the canonical fields, `::`-joined, truncated to 24 hex chars. 96 bits of collision resistance is fine for any single-user task backlog and short enough to eyeball in logs.
400400

401-
Why include `sourceFile` in the hash? So that the same text in `TASKS-URGENT.md` and `TASKS-LORECRAFT.md` counts as two distinct tasks. Why exclude line number? So you can re-order tasks within a file without churning state.
401+
Why include `sourceFile` in the hash? So that the same text in `TASKS-URGENT.md` and `TASKS-PROJECT-A.md` counts as two distinct tasks. Why exclude line number? So you can re-order tasks within a file without churning state.
402402

403403
---
404404

docs/DESIGN-RATIONALE.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ the response isn't paginated. `git log -S "ship the kit"
129129
05-Tasks/TASKS-URGENT.md` answers six months of "when did this task
130130
appear?" questions in one shell command.
131131

132-
**Bulk edits are trivial.** Renaming a project from `LORECRAFT-HQ` to
133-
`LORECRAFT`? `sed -i` across the task files, commit, push, sync
132+
**Bulk edits are trivial.** Renaming a project from `PROJECT-A-HQ` to
133+
`PROJECT-A`? `sed -i` across the task files, commit, push, sync
134134
catches up. Doing the same against a SaaS API means writing a
135135
backfill script, dealing with pagination, and praying the rate limit
136136
holds. Text files are the cheapest schema-migration platform on
@@ -190,7 +190,7 @@ Three properties fall out for free:
190190
Obsidian doesn't surface anyway.
191191
- **Same text in two files = two tasks.** `sourceFile` is part of the
192192
hash input. Moving "ship the kit" from `TASKS-URGENT.md` to
193-
`TASKS-LORECRAFT.md` reads as `archive + create`, which routes the
193+
`TASKS-PROJECT-A.md` reads as `archive + create`, which routes the
194194
task into the right Morgen tag without any special-case logic.
195195

196196
Completion state is **not** in the hash. Marking `- [ ]``- [x]`
@@ -237,7 +237,7 @@ matches `m-[0-9a-f]{8}` exactly. UUIDv4s embedded in 🆔 tokens (which
237237
predate this scheme) silently fail to parse — they're not picked up
238238
by W1, which means tasks created with them sit stranded in Obsidian
239239
without ever reaching Morgen. That failure mode bit hard once
240-
(LAVA-NET, May 2026, six invoice tasks stranded for hours) and is
240+
(PROJECT-D, May 2026, six invoice tasks stranded for hours) and is
241241
the reason `/maketasks` enforces the format.
242242

243243
**What you'd give up to switch.** A client-minted UUIDv4 scheme would
@@ -500,7 +500,7 @@ W2 could surface pinned slots back into a markdown comment, but
500500
the storage shape ("pinned-at" emoji?) isn't decided.
501501

502502
**Multi-area task moves.** Moving a task from `TASKS-URGENT.md` to
503-
`TASKS-LORECRAFT.md` reads as `archive + create` because
503+
`TASKS-PROJECT-A.md` reads as `archive + create` because
504504
`sourceFile` is in the hash. The new Morgen task gets a new ID;
505505
the user-visible `m-XXXXXXXX` token in the markdown line changes
506506
on rewrite. That's fine for now but means a task's history is
@@ -516,7 +516,7 @@ PagerDuty). Once the kit's user base picks a default, it'll get
516516
shipped as a fourth workflow.
517517

518518
**Tag-scheme migration.** During the Notion era we ran two parallel
519-
Morgen tag schemes (`Lava-Network` bare-label vs `09 LAVA-NETWORK`
519+
Morgen tag schemes (`Project-D` bare-label vs `09 PROJECT-D`
520520
Notion-prefixed). The post-cutover state still uses the prefixed
521521
form because W2's `notionLabelToAreaKey` routes by it; switching
522522
to bare labels is a one-shot migration we haven't run. New forks

docs/TROUBLESHOOTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ W1's only join key from markdown → Morgen is the `🆔 m-XXXXXXXX` token. If a
191191

192192
```bash
193193
cd "$VAULT_PATH/05-Tasks"
194-
git log -1 --pretty=format:"%s%n%b" -- TASKS-*.md FIDGETCODING/**/TASKS-*.md
194+
git log -1 --pretty=format:"%s%n%b" -- TASKS-*.md NESTED/**/TASKS-*.md
195195
```
196196

197197
Look at the commit subject. If it starts with **any `[bot:*]` prefix**, that's the problem — W1's echo guard skipped the run.
@@ -306,7 +306,7 @@ W1 only mints new IDs when a task line **lacks** the `🆔` token. The mint is t
306306

307307
### 10. Task lands in TASKS-GENERAL
308308

309-
> You created a task that should have gone into a project area file (`TASKS-LORECRAFT.md`, `TASKS-WAGMI.md`, etc.) but it landed in `TASKS-GENERAL.md`.
309+
> You created a task that should have gone into a project area file (`TASKS-PROJECT-A.md`, `TASKS-PROJECT-G.md`, etc.) but it landed in `TASKS-GENERAL.md`.
310310
311311
**Diagnose**
312312

examples/sample-sync-state.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"_tagCache": {
44
"01 URGENT": "11111111-1111-1111-1111-111111111111",
55
"02 GENERAL": "22222222-2222-2222-2222-222222222222",
6-
"03 LORECRAFT": "33333333-3333-3333-3333-333333333333"
6+
"03 PROJECT-A": "33333333-3333-3333-3333-333333333333"
77
},
88
"entries": {
99
"a1b2c3d4e5f67890abcdef01": {
@@ -44,11 +44,11 @@
4444
},
4545
"c3d4e5f67890abcdef010203": {
4646
"hash": "c3d4e5f67890abcdef010203",
47-
"sourceFile": "TASKS-LORECRAFT.md",
47+
"sourceFile": "TASKS-PROJECT-A.md",
4848
"lineNo": 9,
4949
"lineHash": "deadbeef12345678",
5050
"text": "Update the README with the new install flow",
51-
"area": "LORECRAFT",
51+
"area": "PROJECT-A",
5252
"priority": 5,
5353
"due": "2026-04-18",
5454
"scheduled": "2026-04-16",

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
"node": ">=20"
1111
},
1212
"scripts": {
13-
"test": "node scripts/sync-e2e-tests.js",
13+
"test": "node scripts/test-areas-config.js && node scripts/test-helpers.js && node scripts/sync-e2e-tests.js",
1414
"test:helpers": "node scripts/test-helpers.js",
1515
"validate:state": "node scripts/validate-sync-state.js",
1616
"validate:workflows": "node scripts/validate-workflows.js",
1717
"install:workflows": "./scripts/install-workflows.sh",
1818
"backfill:morgen": "node scripts/morgen-backfill.js",
19-
"lint": "echo 'No linter configured yet'"
19+
"lint": "echo 'No linter configured yet'",
20+
"test:areas": "node scripts/test-areas-config.js"
2021
}
2122
}

scripts/sync-e2e-tests.js

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
'use strict';
2727

28+
require('./test-areas.js'); // must precede sync-helpers
2829
const assert = require('node:assert/strict');
2930
const fs = require('node:fs');
3031
const path = require('node:path');
@@ -420,7 +421,7 @@ function simulateW3({ vaultDir, stateFile, notion, commit }) {
420421
// New row from Notion → append to source file
421422
// row.area is a Notion select label; convert to internal key to pick the right file
422423
const areaKey = notionLabelToAreaKey(row.area) === 'GENERAL' && row.area !== '02 GENERAL'
423-
? row.area // fall back to raw for tests using the flat form "LORECRAFT"
424+
? row.area // fall back to raw for tests using the flat form "PROJECT-A"
424425
: notionLabelToAreaKey(row.area);
425426
// Also tolerate tests that pass the raw internal key as `area`
426427
const keyGuess = Object.prototype.hasOwnProperty.call(helpers._constants.NOTION_AREAS, row.area)
@@ -670,11 +671,11 @@ async function testB_MorgenCompletionW2() {
670671
async function testC_NotionCreationW3() {
671672
const vault = makeTmpVault();
672673
try {
673-
const sourceFile = 'TASKS-LORECRAFT.md';
674+
const sourceFile = 'TASKS-PROJECT-A.md';
674675
writeTaskFile(
675676
vault,
676677
sourceFile,
677-
['# TASKS — LORECRAFT', '', '## Open', '', ''].join('\n')
678+
['# TASKS — PROJECT-A', '', '## Open', '', ''].join('\n')
678679
);
679680
const stateFile = path.join(vault, '.sync-state.json');
680681
saveStateFile(stateFile, loadSyncState(null));
@@ -684,7 +685,7 @@ async function testC_NotionCreationW3() {
684685
{
685686
pageId: 'notion-page-new-1',
686687
title: 'research Lava token model',
687-
area: 'LORECRAFT', // W3 tolerates raw internal key when flat file exists
688+
area: 'PROJECT-A', // W3 tolerates raw internal key when flat file exists
688689
priority: '⏫ High',
689690
status: 'Not Started',
690691
editedAtTs: Date.now(),
@@ -1014,7 +1015,7 @@ async function testJ_IsSafePath() {
10141015
'TASKS-../x.md',
10151016
'05-Tasks/../../etc/passwd',
10161017
'TASKS-HACKED.md', // not in allowlist
1017-
'FIDGETCODING/evil/TASKS-x.md', // wrong subfolder
1018+
'NESTED/evil/TASKS-x.md', // wrong subfolder
10181019
'../TASKS-URGENT.md',
10191020
'',
10201021
null,
@@ -1027,20 +1028,20 @@ async function testJ_IsSafePath() {
10271028
const accepts = [
10281029
'TASKS-URGENT.md',
10291030
'TASKS-GENERAL.md',
1030-
'TASKS-LORECRAFT.md',
1031-
'TASKS-BLOOM.md',
1032-
'TASKS-CART-BLANCHE.md',
1033-
'TASKS-LAVA-NETWORK.md',
1034-
'TASKS-MMA.md',
1035-
'TASKS-PARZVL.md',
1036-
'TASKS-WAGMI.md',
1037-
'FIDGETCODING/content/TASKS-FIDGETCODING-content.md',
1038-
'FIDGETCODING/misc-building/TASKS-FIDGETCODING-misc-building.md',
1039-
'FIDGETCODING/TASKS-FIDGETCODING.md',
1031+
'TASKS-PROJECT-A.md',
1032+
'TASKS-PROJECT-B.md',
1033+
'TASKS-PROJECT-C.md',
1034+
'TASKS-PROJECT-D.md',
1035+
'TASKS-PROJECT-E.md',
1036+
'TASKS-PROJECT-F.md',
1037+
'TASKS-PROJECT-G.md',
1038+
'NESTED/content/TASKS-NESTED-content.md',
1039+
'NESTED/misc-building/TASKS-NESTED-misc-building.md',
1040+
'NESTED/TASKS-NESTED.md',
10401041
'FUTURE-SCHEDULING/TASKS-FUTURE-SCHEDULING.md',
10411042
// with 05-Tasks/ prefix also OK
10421043
'05-Tasks/TASKS-URGENT.md',
1043-
'05-Tasks/FIDGETCODING/content/TASKS-FIDGETCODING-content.md',
1044+
'05-Tasks/NESTED/content/TASKS-NESTED-content.md',
10441045
];
10451046
for (const p of accepts) {
10461047
assert.equal(isSafePath(p), true, `should accept: ${JSON.stringify(p)}`);
@@ -1051,34 +1052,34 @@ async function testK_ParseAreaRoundTrip() {
10511052
const cases = [
10521053
['TASKS-URGENT.md', 'URGENT', '01 URGENT'],
10531054
['TASKS-GENERAL.md', 'GENERAL', '02 GENERAL'],
1054-
['TASKS-LORECRAFT.md', 'LORECRAFT', '03 LORECRAFT'],
1055-
['TASKS-BLOOM.md', 'BLOOM', '04 BLOOM'],
1056-
['TASKS-CART-BLANCHE.md', 'CART-BLANCHE', '05 CART-BLANCHE'],
1055+
['TASKS-PROJECT-A.md', 'PROJECT-A', '03 PROJECT-A'],
1056+
['TASKS-PROJECT-B.md', 'PROJECT-B', '04 PROJECT-B'],
1057+
['TASKS-PROJECT-C.md', 'PROJECT-C', '05 PROJECT-C'],
10571058
[
1058-
'FIDGETCODING/content/TASKS-FIDGETCODING-content.md',
1059-
'FIDGETCODING-CONTENT',
1060-
'06 FIDGETCODING · content',
1059+
'NESTED/content/TASKS-NESTED-content.md',
1060+
'NESTED-CONTENT',
1061+
'06 NESTED · content',
10611062
],
10621063
[
1063-
'FIDGETCODING/misc-building/TASKS-FIDGETCODING-misc-building.md',
1064-
'FIDGETCODING-MISC-BUILDING',
1065-
'07 FIDGETCODING · misc-building',
1064+
'NESTED/misc-building/TASKS-NESTED-misc-building.md',
1065+
'NESTED-BUILD',
1066+
'07 NESTED · misc-building',
10661067
],
10671068
[
10681069
'FUTURE-SCHEDULING/TASKS-FUTURE-SCHEDULING.md',
10691070
'FUTURE-SCHEDULING',
10701071
'08 FUTURE-SCHEDULING',
10711072
],
1072-
['TASKS-LAVA-NETWORK.md', 'LAVA-NETWORK', '09 LAVA-NETWORK'],
1073-
['TASKS-MMA.md', 'MMA', '10 MMA'],
1074-
['TASKS-PARZVL.md', 'PARZVL', '11 PARZVL'],
1075-
['TASKS-WAGMI.md', 'WAGMI', '12 WAGMI'],
1073+
['TASKS-PROJECT-D.md', 'PROJECT-D', '09 PROJECT-D'],
1074+
['TASKS-PROJECT-E.md', 'PROJECT-E', '10 PROJECT-E'],
1075+
['TASKS-PROJECT-F.md', 'PROJECT-F', '11 PROJECT-F'],
1076+
['TASKS-PROJECT-G.md', 'PROJECT-G', '12 PROJECT-G'],
10761077
// Also test with 05-Tasks/ prefix
10771078
['05-Tasks/TASKS-URGENT.md', 'URGENT', '01 URGENT'],
10781079
[
1079-
'05-Tasks/FIDGETCODING/content/TASKS-FIDGETCODING-content.md',
1080-
'FIDGETCODING-CONTENT',
1081-
'06 FIDGETCODING · content',
1080+
'05-Tasks/NESTED/content/TASKS-NESTED-content.md',
1081+
'NESTED-CONTENT',
1082+
'06 NESTED · content',
10821083
],
10831084
];
10841085
for (const [filePath, expectedKey, expectedLabel] of cases) {

scripts/test-areas-config.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/usr/bin/env node
2+
/**
3+
* test-areas-config.js
4+
*
5+
* Tests src/areas.js in isolation — the degradation paths in particular, since
6+
* a crash or a silent mis-parse here misfiles every task in the sync.
7+
* Runs in its own process so it can vary TASKMAXXING_AREAS freely.
8+
*/
9+
'use strict';
10+
const assert = require('node:assert/strict');
11+
const { getAreaConfig, _resetAreaConfig } = require('../src/areas.js');
12+
13+
let pass = 0, fail = 0;
14+
function test(name, fn) {
15+
try { fn(); console.log(` [PASS] ${name}`); pass++; }
16+
catch (e) { console.log(` [FAIL] ${name}\n ${e.message}`); fail++; }
17+
}
18+
function withEnv(value, fn) {
19+
const prev = process.env.TASKMAXXING_AREAS;
20+
if (value === undefined) delete process.env.TASKMAXXING_AREAS;
21+
else process.env.TASKMAXXING_AREAS = value;
22+
_resetAreaConfig();
23+
try { fn(); } finally {
24+
if (prev === undefined) delete process.env.TASKMAXXING_AREAS;
25+
else process.env.TASKMAXXING_AREAS = prev;
26+
_resetAreaConfig();
27+
}
28+
}
29+
const keys = () => Object.keys(getAreaConfig().AREA_TO_FILE);
30+
31+
console.log('\n src/areas.js\n');
32+
33+
test('unset env → built-in defaults, no crash', () => {
34+
withEnv(undefined, () => assert.deepEqual(keys(), ['URGENT', 'GENERAL']));
35+
});
36+
test('unset env → SAFE_PATH_RE still builds', () => {
37+
withEnv(undefined, () => assert.equal(getAreaConfig().SAFE_PATH_RE.test('TASKS-URGENT.md'), true));
38+
});
39+
test('malformed JSON → defaults', () => {
40+
withEnv('{bad', () => assert.deepEqual(keys(), ['URGENT', 'GENERAL']));
41+
});
42+
test('not an array → defaults', () => {
43+
withEnv('{"a":1}', () => assert.deepEqual(keys(), ['URGENT', 'GENERAL']));
44+
});
45+
test('missing GENERAL → defaults', () => {
46+
withEnv(JSON.stringify([{ key: 'X', file: 'TASKS-X.md', morgenLabel: 'X', notionLabel: '01 X' }]),
47+
() => assert.deepEqual(keys(), ['URGENT', 'GENERAL']));
48+
});
49+
test('duplicate key → defaults', () => {
50+
withEnv(JSON.stringify([
51+
{ key: 'GENERAL', file: 'a.md', morgenLabel: 'G', notionLabel: '01 G' },
52+
{ key: 'GENERAL', file: 'b.md', morgenLabel: 'G2', notionLabel: '02 G2' },
53+
]), () => assert.deepEqual(keys(), ['URGENT', 'GENERAL']));
54+
});
55+
test('traversal in file path → defaults', () => {
56+
withEnv(JSON.stringify([
57+
{ key: 'GENERAL', file: '../../etc/passwd', morgenLabel: 'G', notionLabel: '01 G' },
58+
]), () => assert.deepEqual(keys(), ['URGENT', 'GENERAL']));
59+
});
60+
test('traversal in aliasFiles → defaults', () => {
61+
withEnv(JSON.stringify([
62+
{ key: 'GENERAL', file: 'TASKS-GENERAL.md', morgenLabel: 'G', notionLabel: '01 G',
63+
aliasFiles: ['../../etc/passwd'] },
64+
]), () => assert.deepEqual(keys(), ['URGENT', 'GENERAL']));
65+
});
66+
test('valid config is applied', () => {
67+
withEnv(JSON.stringify([
68+
{ key: 'GENERAL', file: 'TASKS-GENERAL.md', morgenLabel: 'General', notionLabel: '02 GENERAL' },
69+
{ key: 'PROJECT-A', file: 'TASKS-PROJECT-A.md', morgenLabel: 'Project-A', notionLabel: '03 PROJECT-A' },
70+
]), () => assert.deepEqual(keys(), ['GENERAL', 'PROJECT-A']));
71+
});
72+
test('regex metacharacters in a path are escaped, not interpreted', () => {
73+
withEnv(JSON.stringify([
74+
{ key: 'GENERAL', file: 'TASKS-GENERAL.md', morgenLabel: 'G', notionLabel: '01 G' },
75+
{ key: 'DOT', file: 'TASKS-a.b.md', morgenLabel: 'Dot', notionLabel: '02 Dot' },
76+
]), () => {
77+
const re = getAreaConfig().SAFE_PATH_RE;
78+
assert.equal(re.test('TASKS-a.b.md'), true, 'literal path accepted');
79+
assert.equal(re.test('TASKS-aXb.md'), false, 'dot must not act as a wildcard');
80+
});
81+
});
82+
83+
console.log(`\n ${pass} passed · ${fail} failed\n`);
84+
process.exit(fail === 0 ? 0 : 1);

0 commit comments

Comments
 (0)