devlog-tracker 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+ const test = require('node:test');
3
+ const assert = require('node:assert/strict');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+ const { render, renderMarkdown, safeHref, escapeHtml } = require('./timeline-render');
7
+
8
+ function round(extra) {
9
+ return { branch: 'main', file: 'devlog.md', line: 1, n: 1, at: '2026-09-01T10:00:00+0800',
10
+ status: 'DONE', summary: 's', reply: '', handoff: '', segments: [], ...extra };
11
+ }
12
+
13
+ test('escapeHtml escapes the five HTML metacharacters', () => {
14
+ assert.equal(escapeHtml(`<a href="x">'&'</a>`), '&lt;a href=&quot;x&quot;&gt;&#39;&amp;&#39;&lt;/a&gt;');
15
+ });
16
+
17
+ test('safeHref allows http(s) and relative, rejects other schemes', () => {
18
+ assert.equal(safeHref('https://example.com'), 'https://example.com');
19
+ assert.equal(safeHref('docs/design/x.md'), 'docs/design/x.md');
20
+ assert.equal(safeHref('javascript:alert(1)'), null);
21
+ assert.equal(safeHref('JaVaScRiPt:alert(1)'), null);
22
+ assert.equal(safeHref('data:text/html,x'), null);
23
+ assert.equal(safeHref('//evil.example'), null);
24
+ });
25
+
26
+ test('safeHref rejects a leading C0 control char that browsers strip', () => {
27
+ assert.equal(safeHref('\u0001javascript:alert(1)'), null);
28
+ const html = renderMarkdown('[x](\u0001javascript:alert(1))');
29
+ assert.ok(!/href=/i.test(html));
30
+ });
31
+
32
+ test('renderMarkdown never emits raw HTML from content', () => {
33
+ const html = renderMarkdown('<script>alert(1)</script>\n<img src=x onerror=alert(1)>');
34
+ assert.ok(!html.includes('<script>'));
35
+ assert.ok(!html.includes('<img'));
36
+ assert.ok(html.includes('&lt;script&gt;'));
37
+ });
38
+
39
+ test('renderMarkdown neutralises javascript: links', () => {
40
+ const html = renderMarkdown('[x](javascript:alert(1)) [ok](https://a.example)');
41
+ assert.ok(!/href="javascript/i.test(html));
42
+ assert.ok(html.includes('<a href="https://a.example">ok</a>'));
43
+ });
44
+
45
+ test('renderMarkdown subset: headings, lists, fences, inline code, bold, tables', () => {
46
+ const html = renderMarkdown('#### 現況\n- a\n - b\n1. one\n\n```\n<b>\n```\nuse `x<y` and **bold**\n\n| h | i |\n|---|---|\n| 1 | 2 |');
47
+ assert.ok(html.includes('<h6>現況</h6>'));
48
+ assert.ok(html.includes('<ul><li>a</li><li class="nested">b</li></ul>'));
49
+ assert.ok(html.includes('<ol><li>one</li></ol>'));
50
+ assert.ok(html.includes('<pre><code>&lt;b&gt;</code></pre>'));
51
+ assert.ok(html.includes('<code>x&lt;y</code>'));
52
+ assert.ok(html.includes('<strong>bold</strong>'));
53
+ assert.ok(html.includes('<th>h</th>') && html.includes('<td>2</td>'));
54
+ });
55
+
56
+ test('a pipe line that is not a table does not hang', () => {
57
+ assert.equal(renderMarkdown('| not a table'), '<p>| not a table</p>');
58
+ });
59
+
60
+ test('render: status badges, CJK, checkpoint after its preceding round', () => {
61
+ const html = render({
62
+ started: true, branch: 'main', rounds_total: 2, status_done: 1, status_blocked: 1,
63
+ first_round_at: '2026-09-01T10:00:00+0800', last_round_at: '2026-09-02T10:00:00+0800',
64
+ rounds: [
65
+ round({ n: 2, line: 20, at: '2026-09-02T10:00:00+0800', status: 'BLOCKED', summary: '卡住了' }),
66
+ round({ n: 1, line: 1, summary: '中文摘要', segments: ['段落 探索\nseg body'] }),
67
+ ],
68
+ checkpoint_blocks: [{ branch: 'main', file: 'devlog.md', line: 10, heading: 'Checkpoint(Round 1-1)', body: '### 決策\n- d' }],
69
+ });
70
+ assert.ok(html.startsWith('<!doctype html>'));
71
+ assert.ok(html.includes('中文摘要') && html.includes('卡住了'));
72
+ assert.ok(html.includes('data-status="BLOCKED"') && html.includes('b-DONE'));
73
+ const r1 = html.indexOf('Round 1</strong>');
74
+ const cp = html.indexOf('Checkpoint(Round 1-1)');
75
+ const r2 = html.indexOf('Round 2</strong>');
76
+ assert.ok(r1 < cp && cp < r2, 'order should be Round 1, Checkpoint, Round 2');
77
+ assert.ok(html.includes('<h6>段落 探索</h6>'));
78
+ assert.ok(!/<(script|link)[^>]+(src|href)=/i.test(html), 'no external resources');
79
+ });
80
+
81
+ test('render: unknown status renders as NONE; no input section by default', () => {
82
+ const html = render({ rounds: [round({ status: '' })], checkpoint_blocks: [] });
83
+ assert.ok(html.includes('data-status="NONE"'));
84
+ assert.ok(!html.includes('User Input'));
85
+ const withInput = render({ rounds: [round({ input: '<secret>' })], checkpoint_blocks: [] });
86
+ assert.ok(withInput.includes('User Input') && withInput.includes('&lt;secret&gt;'));
87
+ });
88
+
89
+ test('render: empty data shows an empty-state message', () => {
90
+ assert.ok(render({ started: true, rounds: [], checkpoint_blocks: [] }).includes('沒有 Round 可顯示'));
91
+ });
92
+
93
+ test('CLI: stdin JSON -> stdout HTML; invalid JSON -> exit 1', () => {
94
+ const script = path.join(__dirname, 'timeline-render.js');
95
+ const ok = spawnSync(process.execPath, [script], { input: JSON.stringify({ rounds: [round({})] }), encoding: 'utf8' });
96
+ assert.equal(ok.status, 0);
97
+ assert.ok(ok.stdout.includes('Round 1'));
98
+ const bad = spawnSync(process.execPath, [script], { input: 'not json', encoding: 'utf8' });
99
+ assert.equal(bad.status, 1);
100
+ assert.equal(bad.stderr, 'INVALID_JSON\n');
101
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlog-tracker",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "npx installer for devlog-tracker's Claude Code, Cursor and Codex adapters.",
5
5
  "bin": {
6
6
  "devlog-tracker": "bin/devlog-tracker.js"
@@ -21,7 +21,7 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "scripts": {
24
- "test": "node --test cli/*.test.js scripts/*.test.js",
24
+ "test": "node --test cli/*.test.js cli/platforms/*.test.js scripts/*.test.js core/scripts/*.test.js",
25
25
  "version": "node scripts/sync-version.js && git add .claude-plugin/plugin.json .claude-plugin/marketplace.json README.md README.zh-TW.md"
26
26
  },
27
27
  "license": "Apache-2.0",
@@ -315,7 +315,7 @@ Stop hook 會檢查最後一個 Round 是否同時有 `### Summary`、`### Reply
315
315
 
316
316
  這一輪如果是使用者直接呼叫 devlog-tracker 自己的純管理指令——`/devlog-tracker:checkpoint`、
317
317
  `clean`、`compact`、`keep`、`lessons`、`lessons-drift`、`lessons-off`、`lessons-on`、
318
- `overview`、`pause`、`search`、`segment-watch`、`span`、`start`、`status`——`round-start.sh`
318
+ `overview`、`pause`、`report`、`search`、`segment-watch`、`span`、`start`、`status`、`timeline`——`round-start.sh`
319
319
  會整輪直接放行,不開 Round、不動任何計數器,等於這個 tick 沒發生過;不用、也不會被
320
320
  Stop hook 要求補寫 Summary/Reply/Handoff。這些指令本身就是在操作 devlog 系統,不是開發
321
321
  工作,記錄下來對接續開發沒有幫助。