devlog-tracker 0.22.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.
- package/LICENSE +201 -0
- package/README.md +234 -0
- package/bin/devlog-tracker.js +51 -0
- package/cli/init.js +55 -0
- package/cli/init.test.js +37 -0
- package/cli/merge-hooks.js +60 -0
- package/cli/merge-hooks.test.js +187 -0
- package/cli/platforms/codex.js +13 -0
- package/cli/platforms/cursor.js +13 -0
- package/cli/status.js +14 -0
- package/cli/status.test.js +33 -0
- package/cli/vendor.js +37 -0
- package/cli/vendor.test.js +84 -0
- package/codex/hooks/on-pre-tool.sh +19 -0
- package/codex/hooks/on-session-end.sh +31 -0
- package/codex/hooks/on-session-start.sh +37 -0
- package/codex/hooks/on-stop.sh +18 -0
- package/codex/hooks/on-user-prompt-submit.sh +15 -0
- package/codex/hooks/project-dir.sh +13 -0
- package/codex/hooks/test-adapters.sh +98 -0
- package/codex/hooks.json +54 -0
- package/commands/checkpoint.md +17 -0
- package/commands/clean.md +50 -0
- package/commands/compact.md +24 -0
- package/commands/continue.md +30 -0
- package/commands/keep.md +100 -0
- package/commands/lessons-drift.md +19 -0
- package/commands/lessons-off.md +16 -0
- package/commands/lessons-on.md +20 -0
- package/commands/lessons.md +20 -0
- package/commands/overview.md +32 -0
- package/commands/pause.md +20 -0
- package/commands/resume.md +18 -0
- package/commands/segment-watch.md +21 -0
- package/commands/span.md +13 -0
- package/commands/start.md +19 -0
- package/commands/status.md +14 -0
- package/cursor/hooks/on-pre-tool.sh +24 -0
- package/cursor/hooks/on-session-end.sh +11 -0
- package/cursor/hooks/on-session-start.sh +15 -0
- package/cursor/hooks/on-stop.sh +44 -0
- package/cursor/hooks/on-submit-prompt.sh +28 -0
- package/cursor/hooks/on-tool-failure.sh +16 -0
- package/cursor/hooks/project-dir.sh +10 -0
- package/cursor/hooks/test-adapters.sh +201 -0
- package/cursor/hooks.json +26 -0
- package/hooks/scripts/await-open.sh +23 -0
- package/hooks/scripts/checkpoint-set.sh +44 -0
- package/hooks/scripts/clean-devlog.sh +69 -0
- package/hooks/scripts/close-open-round.sh +186 -0
- package/hooks/scripts/compact-devlog.sh +72 -0
- package/hooks/scripts/detect-pending-question.sh +31 -0
- package/hooks/scripts/devlog-lock.sh +38 -0
- package/hooks/scripts/devlog-md.sh +222 -0
- package/hooks/scripts/devlog-path.sh +70 -0
- package/hooks/scripts/enforce-devlog.sh +629 -0
- package/hooks/scripts/files-snapshot.sh +70 -0
- package/hooks/scripts/json-field.sh +76 -0
- package/hooks/scripts/keep-move.sh +155 -0
- package/hooks/scripts/kept-list.sh +40 -0
- package/hooks/scripts/lessons-append.sh +93 -0
- package/hooks/scripts/lessons-drift-set.sh +42 -0
- package/hooks/scripts/lessons-off.sh +15 -0
- package/hooks/scripts/lessons-on.sh +21 -0
- package/hooks/scripts/lessons-read.sh +60 -0
- package/hooks/scripts/on-session-end.sh +14 -0
- package/hooks/scripts/on-stop-failure.sh +18 -0
- package/hooks/scripts/on-tool-failure.sh +25 -0
- package/hooks/scripts/pause-devlog.sh +13 -0
- package/hooks/scripts/redact-prompt.sh +19 -0
- package/hooks/scripts/resume-devlog.sh +41 -0
- package/hooks/scripts/round-start.sh +333 -0
- package/hooks/scripts/run-tests.sh +26 -0
- package/hooks/scripts/segment-watch-set.sh +47 -0
- package/hooks/scripts/segment-watch.sh +197 -0
- package/hooks/scripts/session-start-devlog.sh +157 -0
- package/hooks/scripts/span-close.sh +9 -0
- package/hooks/scripts/span-open.sh +21 -0
- package/hooks/scripts/start-devlog.sh +24 -0
- package/hooks/scripts/status-devlog.sh +43 -0
- package/hooks/scripts/tests/test-await-open.sh +112 -0
- package/hooks/scripts/tests/test-branch-scoped-integration.sh +108 -0
- package/hooks/scripts/tests/test-checkpoint-set.sh +36 -0
- package/hooks/scripts/tests/test-clean-devlog.sh +160 -0
- package/hooks/scripts/tests/test-cli-init-e2e.sh +37 -0
- package/hooks/scripts/tests/test-close-open-round.sh +325 -0
- package/hooks/scripts/tests/test-compact-devlog.sh +62 -0
- package/hooks/scripts/tests/test-devlog-lock.sh +37 -0
- package/hooks/scripts/tests/test-devlog-md.sh +530 -0
- package/hooks/scripts/tests/test-devlog-path.sh +118 -0
- package/hooks/scripts/tests/test-enforce-devlog-files.sh +233 -0
- package/hooks/scripts/tests/test-enforce-devlog-handoff-order.sh +223 -0
- package/hooks/scripts/tests/test-enforce-devlog-workspace.sh +333 -0
- package/hooks/scripts/tests/test-enforce-devlog.sh +1290 -0
- package/hooks/scripts/tests/test-files-snapshot.sh +142 -0
- package/hooks/scripts/tests/test-json-field.sh +57 -0
- package/hooks/scripts/tests/test-keep-move.sh +127 -0
- package/hooks/scripts/tests/test-kept-list.sh +80 -0
- package/hooks/scripts/tests/test-lessons-append.sh +138 -0
- package/hooks/scripts/tests/test-lessons-drift-set.sh +36 -0
- package/hooks/scripts/tests/test-lessons-on-off.sh +75 -0
- package/hooks/scripts/tests/test-lessons-read.sh +97 -0
- package/hooks/scripts/tests/test-on-interrupt.sh +186 -0
- package/hooks/scripts/tests/test-redact-prompt.sh +27 -0
- package/hooks/scripts/tests/test-resume-devlog.sh +35 -0
- package/hooks/scripts/tests/test-round-start.sh +825 -0
- package/hooks/scripts/tests/test-segment-watch-set.sh +75 -0
- package/hooks/scripts/tests/test-segment-watch.sh +468 -0
- package/hooks/scripts/tests/test-session-start-devlog.sh +508 -0
- package/hooks/scripts/tests/test-start-pause-devlog.sh +84 -0
- package/hooks/scripts/tests/test-status-span.sh +50 -0
- package/hooks/scripts/tests/test-workspace-snapshot.sh +130 -0
- package/hooks/scripts/workspace-snapshot.sh +69 -0
- package/package.json +37 -0
- package/skills/devlog-tracker/SKILL.md +411 -0
- package/skills/devlog-tracker/references/checkpoint-mode.md +42 -0
- package/skills/devlog-tracker/references/lessons-mode.md +32 -0
- package/skills/devlog-tracker/references/reply-fold.md +129 -0
- package/skills/devlog-tracker/references/round-segments.md +55 -0
- package/skills/devlog-tracker/references/span-mode.md +65 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Self-check for files-snapshot.sh's files_snapshot(), covering the
|
|
3
|
+
# uncommitted branch (git status), the committed branch (git diff-tree),
|
|
4
|
+
# rename-as-delete+add, and .devlog/ exclusion in both branches.
|
|
5
|
+
# No framework — plain assert-and-exit, matching this repo's existing style.
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
9
|
+
. "$SCRIPT_DIR/files-snapshot.sh"
|
|
10
|
+
|
|
11
|
+
TMP_ROOT="$(mktemp -d)"
|
|
12
|
+
trap 'rm -rf "$TMP_ROOT"' EXIT
|
|
13
|
+
|
|
14
|
+
FAIL=0
|
|
15
|
+
assert_eq() {
|
|
16
|
+
local desc="$1" expected="$2" actual="$3"
|
|
17
|
+
if [ "$actual" = "$expected" ]; then
|
|
18
|
+
echo "PASS: $desc"
|
|
19
|
+
else
|
|
20
|
+
echo "FAIL: $desc"
|
|
21
|
+
printf ' expected: %s\n' "$expected"
|
|
22
|
+
printf ' actual: %s\n' "$actual"
|
|
23
|
+
FAIL=1
|
|
24
|
+
fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# --- non-git directory ----------------------------------------------------
|
|
28
|
+
NONGIT="$TMP_ROOT/nongit"
|
|
29
|
+
mkdir -p "$NONGIT"
|
|
30
|
+
OUT="$(files_snapshot "$NONGIT")"
|
|
31
|
+
assert_eq "non-git directory" "" "$OUT"
|
|
32
|
+
|
|
33
|
+
# --- repo setup -------------------------------------------------------------
|
|
34
|
+
REPO="$TMP_ROOT/repo"
|
|
35
|
+
mkdir -p "$REPO/.devlog"
|
|
36
|
+
git -C "$REPO" init -q -b main
|
|
37
|
+
git -C "$REPO" config user.email test@example.com
|
|
38
|
+
git -C "$REPO" config user.name test
|
|
39
|
+
echo one > "$REPO/a.txt"
|
|
40
|
+
echo two > "$REPO/b.txt"
|
|
41
|
+
echo bookkeeping > "$REPO/.devlog/devlog.md"
|
|
42
|
+
git -C "$REPO" add a.txt b.txt .devlog/devlog.md
|
|
43
|
+
git -C "$REPO" commit -q -m init
|
|
44
|
+
|
|
45
|
+
# --- clean tree -> empty ------------------------------------------------------
|
|
46
|
+
OUT="$(files_snapshot "$REPO")"
|
|
47
|
+
assert_eq "clean tree" "" "$OUT"
|
|
48
|
+
|
|
49
|
+
# --- untracked file -> 新增 ----------------------------------------------------
|
|
50
|
+
echo new > "$REPO/c.txt"
|
|
51
|
+
OUT="$(files_snapshot "$REPO")"
|
|
52
|
+
assert_eq "untracked file" "新增:c.txt" "$OUT"
|
|
53
|
+
rm -f "$REPO/c.txt"
|
|
54
|
+
|
|
55
|
+
# --- modified tracked file -> 修改 ---------------------------------------------
|
|
56
|
+
echo changed >> "$REPO/a.txt"
|
|
57
|
+
OUT="$(files_snapshot "$REPO")"
|
|
58
|
+
assert_eq "modified tracked file" "修改:a.txt" "$OUT"
|
|
59
|
+
git -C "$REPO" checkout -q -- a.txt
|
|
60
|
+
|
|
61
|
+
# --- deleted tracked file -> 刪除 ----------------------------------------------
|
|
62
|
+
rm -f "$REPO/b.txt"
|
|
63
|
+
OUT="$(files_snapshot "$REPO")"
|
|
64
|
+
assert_eq "deleted tracked file" "刪除:b.txt" "$OUT"
|
|
65
|
+
git -C "$REPO" checkout -q -- b.txt
|
|
66
|
+
|
|
67
|
+
# --- mixed: new + modified + deleted, fixed category order --------------------
|
|
68
|
+
echo new > "$REPO/c.txt"
|
|
69
|
+
echo changed >> "$REPO/a.txt"
|
|
70
|
+
rm -f "$REPO/b.txt"
|
|
71
|
+
OUT="$(files_snapshot "$REPO")"
|
|
72
|
+
EXPECTED="新增:c.txt
|
|
73
|
+
修改:a.txt
|
|
74
|
+
刪除:b.txt"
|
|
75
|
+
assert_eq "mixed changes, fixed category order" "$EXPECTED" "$OUT"
|
|
76
|
+
rm -f "$REPO/c.txt"
|
|
77
|
+
git -C "$REPO" checkout -q -- a.txt b.txt
|
|
78
|
+
|
|
79
|
+
# --- .devlog/ excluded, real change still reported -----------------------------
|
|
80
|
+
echo bookkeeping change >> "$REPO/.devlog/devlog.md"
|
|
81
|
+
echo real change >> "$REPO/a.txt"
|
|
82
|
+
OUT="$(files_snapshot "$REPO")"
|
|
83
|
+
assert_eq ".devlog/ excluded (uncommitted branch)" "修改:a.txt" "$OUT"
|
|
84
|
+
git -C "$REPO" checkout -q -- a.txt .devlog/devlog.md
|
|
85
|
+
|
|
86
|
+
# --- rename shows as delete+add (no -M anywhere) --------------------------------
|
|
87
|
+
git -C "$REPO" mv a.txt a-renamed.txt
|
|
88
|
+
OUT="$(files_snapshot "$REPO")"
|
|
89
|
+
EXPECTED="新增:a-renamed.txt
|
|
90
|
+
刪除:a.txt"
|
|
91
|
+
assert_eq "rename -> delete+add" "$EXPECTED" "$OUT"
|
|
92
|
+
git -C "$REPO" reset -q --hard HEAD
|
|
93
|
+
|
|
94
|
+
# --- committed branch: add + modify + delete in one commit -----------------------
|
|
95
|
+
echo new-committed > "$REPO/d.txt"
|
|
96
|
+
echo changed-committed >> "$REPO/a.txt"
|
|
97
|
+
rm -f "$REPO/b.txt"
|
|
98
|
+
git -C "$REPO" add -A
|
|
99
|
+
git -C "$REPO" commit -q -m "mixed commit"
|
|
100
|
+
HASH="$(git -C "$REPO" rev-parse --short HEAD)"
|
|
101
|
+
OUT="$(files_snapshot "$REPO" "$HASH")"
|
|
102
|
+
EXPECTED="新增:d.txt
|
|
103
|
+
修改:a.txt
|
|
104
|
+
刪除:b.txt"
|
|
105
|
+
assert_eq "committed branch, mixed" "$EXPECTED" "$OUT"
|
|
106
|
+
|
|
107
|
+
# --- committed branch: .devlog/ excluded there too --------------------------------
|
|
108
|
+
echo bookkeeping2 >> "$REPO/.devlog/devlog.md"
|
|
109
|
+
echo e > "$REPO/e.txt"
|
|
110
|
+
git -C "$REPO" add -A
|
|
111
|
+
git -C "$REPO" commit -q -m "with devlog bookkeeping"
|
|
112
|
+
HASH2="$(git -C "$REPO" rev-parse --short HEAD)"
|
|
113
|
+
OUT="$(files_snapshot "$REPO" "$HASH2")"
|
|
114
|
+
assert_eq ".devlog/ excluded (committed branch)" "新增:e.txt" "$OUT"
|
|
115
|
+
|
|
116
|
+
# --- root commit with --root flag (should show files, not empty) ------------------
|
|
117
|
+
ROOTREPO="$TMP_ROOT/root"
|
|
118
|
+
mkdir -p "$ROOTREPO/.devlog"
|
|
119
|
+
git -C "$ROOTREPO" init -q -b main
|
|
120
|
+
git -C "$ROOTREPO" config user.email test@example.com
|
|
121
|
+
git -C "$ROOTREPO" config user.name test
|
|
122
|
+
echo file1 > "$ROOTREPO/file1.txt"
|
|
123
|
+
echo file2 > "$ROOTREPO/file2.txt"
|
|
124
|
+
echo bookkeeping > "$ROOTREPO/.devlog/devlog.md"
|
|
125
|
+
git -C "$ROOTREPO" add file1.txt file2.txt .devlog/devlog.md
|
|
126
|
+
git -C "$ROOTREPO" commit -q -m "initial root commit"
|
|
127
|
+
ROOT_HASH="$(git -C "$ROOTREPO" rev-parse --short HEAD)"
|
|
128
|
+
OUT="$(files_snapshot "$ROOTREPO" "$ROOT_HASH")"
|
|
129
|
+
EXPECTED="新增:file1.txt, file2.txt"
|
|
130
|
+
assert_eq "root commit with multiple files (--root)" "$EXPECTED" "$OUT"
|
|
131
|
+
|
|
132
|
+
# --- unresolvable hash -> empty (fail-open) ---------------------------------------
|
|
133
|
+
OUT="$(files_snapshot "$REPO" "0000000")"
|
|
134
|
+
assert_eq "unresolvable hash -> empty" "" "$OUT"
|
|
135
|
+
|
|
136
|
+
if [ "$FAIL" -eq 0 ]; then
|
|
137
|
+
echo "All checks passed."
|
|
138
|
+
exit 0
|
|
139
|
+
else
|
|
140
|
+
echo "Some checks FAILED."
|
|
141
|
+
exit 1
|
|
142
|
+
fi
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
4
|
+
# shellcheck source=json-field.sh
|
|
5
|
+
. "$SCRIPT_DIR/json-field.sh"
|
|
6
|
+
TMP="$(mktemp)"
|
|
7
|
+
trap 'rm -f "$TMP"' EXIT
|
|
8
|
+
FAIL=0
|
|
9
|
+
assert_eq() {
|
|
10
|
+
local desc="$1" expected="$2" actual="$3"
|
|
11
|
+
if [ "$expected" = "$actual" ]; then echo "PASS: $desc"
|
|
12
|
+
else echo "FAIL: $desc (expected [$expected] got [$actual])"; FAIL=1; fi
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
printf '%s\n' '{"ticks_since_checkin": 2, "max_silent_ticks": 5}' > "$TMP"
|
|
16
|
+
assert_eq "int get" "2" "$(json_int_get "$TMP" ticks_since_checkin)"
|
|
17
|
+
assert_eq "int get max" "5" "$(json_int_get "$TMP" max_silent_ticks)"
|
|
18
|
+
assert_eq "missing key" "" "$(json_int_get "$TMP" no_such)"
|
|
19
|
+
|
|
20
|
+
printf '%s\n' '{"ticks_since_checkin": 08}' > "$TMP"
|
|
21
|
+
assert_eq "reject octal pad" "" "$(json_int_get "$TMP" ticks_since_checkin)"
|
|
22
|
+
|
|
23
|
+
printf '%s\n' '{"opened_at": "2026-09-08T21:40:00+08:00", "round": 12}' > "$TMP"
|
|
24
|
+
assert_eq "str get" "2026-09-08T21:40:00+08:00" "$(json_str_get "$TMP" opened_at)"
|
|
25
|
+
assert_eq "int round" "12" "$(json_int_get "$TMP" round)"
|
|
26
|
+
|
|
27
|
+
printf '%s\n' '{"ticks_since_checkin": 2, "max_silent_ticks": 5}' > "$TMP"
|
|
28
|
+
json_int_set "$TMP" ticks_since_checkin 0
|
|
29
|
+
assert_eq "int set" "0" "$(json_int_get "$TMP" ticks_since_checkin)"
|
|
30
|
+
assert_eq "int set preserves sibling" "5" "$(json_int_get "$TMP" max_silent_ticks)"
|
|
31
|
+
|
|
32
|
+
printf '%s\n' '{"last_seen_cksum": "old", "last_change_epoch": 1}' > "$TMP"
|
|
33
|
+
json_str_set "$TMP" last_seen_cksum "123 2"
|
|
34
|
+
assert_eq "str set" "123 2" "$(json_str_get "$TMP" last_seen_cksum)"
|
|
35
|
+
assert_eq "str set preserves epoch" "1" "$(json_int_get "$TMP" last_change_epoch)"
|
|
36
|
+
|
|
37
|
+
printf '%s\n' 'not json' > "$TMP"
|
|
38
|
+
assert_eq "malformed int" "" "$(json_int_get "$TMP" round)"
|
|
39
|
+
assert_eq "malformed str" "" "$(json_str_get "$TMP" opened_at)"
|
|
40
|
+
|
|
41
|
+
INPUT='{"session_id": "abc-123", "prompt": "hello world"}'
|
|
42
|
+
assert_eq "str field get" "abc-123" "$(json_str_field "$INPUT" session_id)"
|
|
43
|
+
assert_eq "str field get other key" "hello world" "$(json_str_field "$INPUT" prompt)"
|
|
44
|
+
assert_eq "str field missing key" "" "$(json_str_field "$INPUT" no_such)"
|
|
45
|
+
|
|
46
|
+
INPUT='{"reason": null}'
|
|
47
|
+
assert_eq "str field null" "" "$(json_str_field "$INPUT" reason)"
|
|
48
|
+
|
|
49
|
+
INPUT='not json'
|
|
50
|
+
assert_eq "str field malformed" "" "$(json_str_field "$INPUT" reason)"
|
|
51
|
+
|
|
52
|
+
assert_eq "slugify basic" "foo-bar" "$(slugify 'foo bar')"
|
|
53
|
+
assert_eq "slugify collapses runs" "foo-bar" "$(slugify 'foo bar')"
|
|
54
|
+
assert_eq "slugify trims edges" "foo" "$(slugify ' foo ')"
|
|
55
|
+
|
|
56
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
57
|
+
else echo "Some checks FAILED."; exit 1; fi
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
4
|
+
TMP="$(mktemp -d)"
|
|
5
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
6
|
+
FAIL=0
|
|
7
|
+
assert_eq() {
|
|
8
|
+
local d="$1" e="$2" a="$3"
|
|
9
|
+
if [ "$e" = "$a" ]; then echo "PASS: $d"; else echo "FAIL: $d expected [$e] got [$a]"; FAIL=1; fi
|
|
10
|
+
}
|
|
11
|
+
make_round() {
|
|
12
|
+
local file="$1" n="$2" status="$3"
|
|
13
|
+
cat >> "$file" <<EOF
|
|
14
|
+
## Round $n — 2026-09-09T12:00:00+08:00
|
|
15
|
+
|
|
16
|
+
### Status
|
|
17
|
+
$status
|
|
18
|
+
EOF
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export CLAUDE_PROJECT_DIR="$TMP/missing"
|
|
22
|
+
bash "$SCRIPT_DIR/keep-move.sh" --from 1 --to 1 --name x >/dev/null 2>&1
|
|
23
|
+
assert_eq "missing log exit 1" 1 "$?"
|
|
24
|
+
|
|
25
|
+
BAD="$TMP/bad-name"
|
|
26
|
+
mkdir -p "$BAD/.devlog"
|
|
27
|
+
export CLAUDE_PROJECT_DIR="$BAD"
|
|
28
|
+
make_round "$BAD/.devlog/devlog.md" 1 DONE
|
|
29
|
+
bash "$SCRIPT_DIR/keep-move.sh" --from 1 --to 1 --name devlog.md >/dev/null 2>&1
|
|
30
|
+
assert_eq "devlog.md name rejected" 1 "$?"
|
|
31
|
+
[ ! -e "$BAD/.devlog/devlog.md.md" ] || { echo "FAIL: devlog.md.md created"; FAIL=1; }
|
|
32
|
+
|
|
33
|
+
EP="$TMP/episode"
|
|
34
|
+
mkdir -p "$EP/.devlog"
|
|
35
|
+
export CLAUDE_PROJECT_DIR="$EP"
|
|
36
|
+
printf '# project\n\n' > "$EP/.devlog/devlog.md"
|
|
37
|
+
make_round "$EP/.devlog/devlog.md" 1 DONE
|
|
38
|
+
make_round "$EP/.devlog/devlog.md" 2 DONE
|
|
39
|
+
make_round "$EP/.devlog/devlog.md" 3 IN_PROGRESS
|
|
40
|
+
printf '%s\n' '{"round": 3, "opened_at": "now"}' > "$EP/.devlog/.round-open"
|
|
41
|
+
bash "$SCRIPT_DIR/keep-move.sh" --from 1 --to 1 --name first >/dev/null
|
|
42
|
+
assert_eq "episode keep exits 0" 0 "$?"
|
|
43
|
+
grep -q '^# Kept log' "$EP/.devlog/devlog.first.md" && echo "PASS: provenance written" || { echo "FAIL: provenance"; FAIL=1; }
|
|
44
|
+
grep -q '^## Round 1' "$EP/.devlog/devlog.first.md" && echo "PASS: named file has round" || { echo "FAIL: named round"; FAIL=1; }
|
|
45
|
+
grep -q '^## Round 1' "$EP/.devlog/devlog.md" && { echo "FAIL: moved round remains"; FAIL=1; } || echo "PASS: moved round removed"
|
|
46
|
+
grep -q '^## Round 3' "$EP/.devlog/devlog.md" && echo "PASS: open round remains" || { echo "FAIL: open moved"; FAIL=1; }
|
|
47
|
+
bash "$SCRIPT_DIR/keep-move.sh" --from 3 --to 3 --name open >/dev/null 2>&1
|
|
48
|
+
assert_eq "open round rejected" 1 "$?"
|
|
49
|
+
bash "$SCRIPT_DIR/keep-move.sh" --from 2 --to 2 --name first >/dev/null 2>&1
|
|
50
|
+
assert_eq "overwrite rejected" 1 "$?"
|
|
51
|
+
|
|
52
|
+
FULL="$TMP/full"
|
|
53
|
+
mkdir -p "$FULL/.devlog"
|
|
54
|
+
export CLAUDE_PROJECT_DIR="$FULL"
|
|
55
|
+
printf '# project\n\n' > "$FULL/.devlog/devlog.md"
|
|
56
|
+
make_round "$FULL/.devlog/devlog.md" 1 DONE
|
|
57
|
+
make_round "$FULL/.devlog/devlog.md" 2 DONE
|
|
58
|
+
make_round "$FULL/.devlog/devlog.md" 3 IN_PROGRESS
|
|
59
|
+
printf '%s\n' '{"round": 3, "opened_at": "now"}' > "$FULL/.devlog/.round-open"
|
|
60
|
+
printf '%s\n' '{"round": 2, "ticks": 1, "max_silent_ticks": 5}' > "$FULL/.devlog/.span-open"
|
|
61
|
+
printf '%s\n' '{"rounds_since_checkpoint": 19, "max_silent_rounds": 20, "checkpoint_marker_count": 0}' > "$FULL/.devlog/.checkpoint-state"
|
|
62
|
+
bash "$SCRIPT_DIR/keep-move.sh" --from 1 --to 2 --name episode >/dev/null
|
|
63
|
+
assert_eq "full keep exits 0" 0 "$?"
|
|
64
|
+
grep -q '^## Round 1 ' "$FULL/.devlog/devlog.md" && echo "PASS: open round renumbered" || { echo "FAIL: full renumber"; FAIL=1; }
|
|
65
|
+
grep -q '"round": 1' "$FULL/.devlog/.round-open" && echo "PASS: round state reset" || { echo "FAIL: round state"; FAIL=1; }
|
|
66
|
+
grep -q '"rounds_since_checkpoint": 0' "$FULL/.devlog/.checkpoint-state" && echo "PASS: checkpoint reset" || { echo "FAIL: checkpoint state"; FAIL=1; }
|
|
67
|
+
grep -q '"max_silent_rounds": 20' "$FULL/.devlog/.checkpoint-state" && echo "PASS: checkpoint max kept" || { echo "FAIL: checkpoint max"; FAIL=1; }
|
|
68
|
+
[ ! -e "$FULL/.devlog/.span-open" ] && echo "PASS: span removed" || { echo "FAIL: span remains"; FAIL=1; }
|
|
69
|
+
|
|
70
|
+
grep -q '^## Kept 索引' "$FULL/.devlog/devlog.md" && echo "PASS: full keep wrote a Kept 索引 block" || { echo "FAIL: full keep should write a Kept 索引 block"; FAIL=1; }
|
|
71
|
+
grep -q 'devlog.episode.md' "$FULL/.devlog/devlog.md" && echo "PASS: full keep index line names devlog.episode.md" || { echo "FAIL: full keep index line missing devlog.episode.md"; FAIL=1; }
|
|
72
|
+
|
|
73
|
+
INDEX="$TMP/index"
|
|
74
|
+
mkdir -p "$INDEX/.devlog"
|
|
75
|
+
export CLAUDE_PROJECT_DIR="$INDEX"
|
|
76
|
+
printf '# project\n\n' > "$INDEX/.devlog/devlog.md"
|
|
77
|
+
make_round "$INDEX/.devlog/devlog.md" 1 DONE
|
|
78
|
+
make_round "$INDEX/.devlog/devlog.md" 2 DONE
|
|
79
|
+
make_round "$INDEX/.devlog/devlog.md" 3 DONE
|
|
80
|
+
# Keep an open round so the 3rd keep below (--from 3 --to 3) stays a *partial*
|
|
81
|
+
# keep — a full keep drops everything before the first round, which would
|
|
82
|
+
# leave ## Kept 索引 at line 1 and silently stop testing the blank-line
|
|
83
|
+
# separator regression checked below.
|
|
84
|
+
make_round "$INDEX/.devlog/devlog.md" 4 IN_PROGRESS
|
|
85
|
+
printf '%s\n' '{"round": 4, "opened_at": "now"}' > "$INDEX/.devlog/.round-open"
|
|
86
|
+
bash "$SCRIPT_DIR/keep-move.sh" --from 1 --to 1 --name topic-a >/dev/null
|
|
87
|
+
KEPT_COUNT_1="$(grep -c '^- ' "$INDEX/.devlog/devlog.md")"
|
|
88
|
+
[ "$KEPT_COUNT_1" -eq 1 ] && echo "PASS: first partial keep adds exactly one index line" || { echo "FAIL: expected 1 index line, got $KEPT_COUNT_1"; FAIL=1; }
|
|
89
|
+
bash "$SCRIPT_DIR/keep-move.sh" --from 2 --to 2 --name topic-b >/dev/null
|
|
90
|
+
KEPT_COUNT_2="$(grep -c '^- ' "$INDEX/.devlog/devlog.md")"
|
|
91
|
+
[ "$KEPT_COUNT_2" -eq 2 ] && echo "PASS: second partial keep accumulates to two index lines" || { echo "FAIL: expected 2 index lines, got $KEPT_COUNT_2"; FAIL=1; }
|
|
92
|
+
KEPT_BLOCKS="$(grep -c '^## Kept 索引' "$INDEX/.devlog/devlog.md")"
|
|
93
|
+
[ "$KEPT_BLOCKS" -eq 1 ] && echo "PASS: exactly one Kept 索引 heading after two keeps" || { echo "FAIL: expected exactly 1 Kept 索引 heading, got $KEPT_BLOCKS"; FAIL=1; }
|
|
94
|
+
grep -q 'devlog.topic-a.md' "$INDEX/.devlog/devlog.md" && echo "PASS: first line survives the second keep's rewrite" || { echo "FAIL: devlog.topic-a.md line lost after second keep"; FAIL=1; }
|
|
95
|
+
bash "$SCRIPT_DIR/keep-move.sh" --from 3 --to 3 --name topic-c >/dev/null
|
|
96
|
+
KEPT_COUNT_3="$(grep -c '^- ' "$INDEX/.devlog/devlog.md")"
|
|
97
|
+
[ "$KEPT_COUNT_3" -eq 3 ] && echo "PASS: third partial keep accumulates to three index lines" || { echo "FAIL: expected 3 index lines, got $KEPT_COUNT_3"; FAIL=1; }
|
|
98
|
+
HEADING_LINE="$(grep -n '^## Kept 索引' "$INDEX/.devlog/devlog.md" | head -1 | cut -d: -f1)"
|
|
99
|
+
PREV_LINE="$(sed -n "$((HEADING_LINE - 1))p" "$INDEX/.devlog/devlog.md")"
|
|
100
|
+
PREV2_LINE="$(sed -n "$((HEADING_LINE - 2))p" "$INDEX/.devlog/devlog.md")"
|
|
101
|
+
if [ -z "$PREV_LINE" ] && [ -n "$PREV2_LINE" ]; then
|
|
102
|
+
echo "PASS: exactly one blank line separates content from ## Kept 索引 after three keep-move.sh calls"
|
|
103
|
+
else
|
|
104
|
+
echo "FAIL: expected exactly one blank line before ## Kept 索引 after three keep-move.sh calls, got [$PREV_LINE] then [$PREV2_LINE]"
|
|
105
|
+
FAIL=1
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
DESC="$TMP/desc"
|
|
109
|
+
mkdir -p "$DESC/.devlog"
|
|
110
|
+
export CLAUDE_PROJECT_DIR="$DESC"
|
|
111
|
+
printf '# project\n\n' > "$DESC/.devlog/devlog.md"
|
|
112
|
+
make_round "$DESC/.devlog/devlog.md" 1 DONE
|
|
113
|
+
make_round "$DESC/.devlog/devlog.md" 2 DONE
|
|
114
|
+
make_round "$DESC/.devlog/devlog.md" 3 IN_PROGRESS
|
|
115
|
+
printf '%s\n' '{"round": 3, "opened_at": "now"}' > "$DESC/.devlog/.round-open"
|
|
116
|
+
bash "$SCRIPT_DIR/keep-move.sh" --from 1 --to 1 --name with-desc --desc "調整 span 模式的門檻" >/dev/null
|
|
117
|
+
grep -q '調整 span 模式的門檻' "$DESC/.devlog/devlog.md" && echo "PASS: --desc text written into Kept 索引 line" || { echo "FAIL: expected desc text in index line"; FAIL=1; }
|
|
118
|
+
bash "$SCRIPT_DIR/keep-move.sh" --from 2 --to 2 --name no-desc >/dev/null
|
|
119
|
+
LINE_NO_DESC="$(grep 'devlog.no-desc.md' "$DESC/.devlog/devlog.md")"
|
|
120
|
+
case "$LINE_NO_DESC" in
|
|
121
|
+
*"kept_at "*","*) echo "FAIL: unexpected extra trailing segment without --desc: $LINE_NO_DESC"; FAIL=1 ;;
|
|
122
|
+
*"kept_at "*) echo "PASS: omitting --desc keeps the plain kept_at-only line" ;;
|
|
123
|
+
*) echo "FAIL: index line for no-desc missing: $LINE_NO_DESC"; FAIL=1 ;;
|
|
124
|
+
esac
|
|
125
|
+
|
|
126
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
127
|
+
else echo "Some checks FAILED."; exit 1; fi
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Self-check for kept-list.sh (docs/design/keep.md "Kept index" +
|
|
3
|
+
# /devlog-tracker:overview). Plain read only -- no workspace verification.
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
6
|
+
TMP_ROOT="$(mktemp -d)"
|
|
7
|
+
trap 'rm -rf "$TMP_ROOT"' EXIT
|
|
8
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
9
|
+
DEVLOG_DIR="$TMP_ROOT/.devlog"
|
|
10
|
+
mkdir -p "$DEVLOG_DIR"
|
|
11
|
+
FAIL=0
|
|
12
|
+
assert_exit() {
|
|
13
|
+
local desc="$1" expected="$2" actual="$3"
|
|
14
|
+
if [ "$actual" -eq "$expected" ]; then echo "PASS: $desc"
|
|
15
|
+
else echo "FAIL: $desc (expected $expected got $actual)"; FAIL=1; fi
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# --- No devlog.md at all -> NO_INDEX ----------------------------------------
|
|
19
|
+
OUT="$(bash "$SCRIPT_DIR/kept-list.sh" 2>&1)"
|
|
20
|
+
assert_exit "no devlog.md -> 0" 0 $?
|
|
21
|
+
case "$OUT" in
|
|
22
|
+
*"NO_INDEX"*) echo "PASS: reports NO_INDEX when devlog.md is absent" ;;
|
|
23
|
+
*) echo "FAIL: expected NO_INDEX, got $OUT"; FAIL=1 ;;
|
|
24
|
+
esac
|
|
25
|
+
|
|
26
|
+
# --- devlog.md exists but no Kept 索引 -> NO_INDEX --------------------------
|
|
27
|
+
echo '## Round 1 — 2026-09-12T00:00:00+08:00
|
|
28
|
+
|
|
29
|
+
### Summary
|
|
30
|
+
fixture
|
|
31
|
+
|
|
32
|
+
### Handoff
|
|
33
|
+
#### 現況
|
|
34
|
+
fixture
|
|
35
|
+
|
|
36
|
+
### Status
|
|
37
|
+
DONE' > "$DEVLOG_DIR/devlog.md"
|
|
38
|
+
OUT="$(bash "$SCRIPT_DIR/kept-list.sh" 2>&1)"
|
|
39
|
+
assert_exit "devlog.md with no index -> 0" 0 $?
|
|
40
|
+
case "$OUT" in
|
|
41
|
+
*"NO_INDEX"*) echo "PASS: reports NO_INDEX when no Kept 索引 block exists" ;;
|
|
42
|
+
*) echo "FAIL: expected NO_INDEX, got $OUT"; FAIL=1 ;;
|
|
43
|
+
esac
|
|
44
|
+
|
|
45
|
+
# --- Kept 索引 present, target file exists -> EXISTS=1 ----------------------
|
|
46
|
+
printf 'kept content\n' > "$DEVLOG_DIR/devlog.foo-bar.md"
|
|
47
|
+
printf '\n## Kept 索引\n- `devlog.foo-bar.md`:Round 1-1,kept_at 2026-09-12T00:00:00+08:00,foo bar 主題\n' >> "$DEVLOG_DIR/devlog.md"
|
|
48
|
+
OUT="$(bash "$SCRIPT_DIR/kept-list.sh" 2>&1)"
|
|
49
|
+
assert_exit "index with one existing file -> 0" 0 $?
|
|
50
|
+
case "$OUT" in
|
|
51
|
+
*"FILE="*"devlog.foo-bar.md"*"EXISTS=1"*) echo "PASS: existing kept file reported with EXISTS=1" ;;
|
|
52
|
+
*) echo "FAIL: expected FILE=...devlog.foo-bar.md EXISTS=1, got $OUT"; FAIL=1 ;;
|
|
53
|
+
esac
|
|
54
|
+
|
|
55
|
+
# --- Ghost row: index line present, file missing on disk -> EXISTS=0 -------
|
|
56
|
+
rm -f "$DEVLOG_DIR/devlog.foo-bar.md"
|
|
57
|
+
OUT="$(bash "$SCRIPT_DIR/kept-list.sh" 2>&1)"
|
|
58
|
+
assert_exit "ghost row -> 0" 0 $?
|
|
59
|
+
case "$OUT" in
|
|
60
|
+
*"FILE="*"devlog.foo-bar.md"*"EXISTS=0"*) echo "PASS: ghost kept file reported with EXISTS=0" ;;
|
|
61
|
+
*) echo "FAIL: expected FILE=...devlog.foo-bar.md EXISTS=0, got $OUT"; FAIL=1 ;;
|
|
62
|
+
esac
|
|
63
|
+
|
|
64
|
+
# --- Multiple entries listed, one per line -----------------------------------
|
|
65
|
+
printf 'kept content two\n' > "$DEVLOG_DIR/devlog.second-topic.md"
|
|
66
|
+
sed -i.bak '$ a\
|
|
67
|
+
- `devlog.second-topic.md`:Round 2-2,kept_at 2026-09-13T00:00:00+08:00
|
|
68
|
+
' "$DEVLOG_DIR/devlog.md"
|
|
69
|
+
rm -f "$DEVLOG_DIR/devlog.md.bak"
|
|
70
|
+
OUT="$(bash "$SCRIPT_DIR/kept-list.sh" 2>&1)"
|
|
71
|
+
assert_exit "index with two entries -> 0" 0 $?
|
|
72
|
+
LINE_COUNT="$(printf '%s\n' "$OUT" | grep -c '^FILE=')"
|
|
73
|
+
[ "$LINE_COUNT" -eq 2 ] && echo "PASS: two FILE= lines printed for two index entries" || { echo "FAIL: expected 2 FILE= lines, got $LINE_COUNT"; FAIL=1; }
|
|
74
|
+
case "$OUT" in
|
|
75
|
+
*"devlog.second-topic.md"*"EXISTS=1"*) echo "PASS: second-topic file reported with EXISTS=1" ;;
|
|
76
|
+
*) echo "FAIL: expected devlog.second-topic.md EXISTS=1, got $OUT"; FAIL=1 ;;
|
|
77
|
+
esac
|
|
78
|
+
|
|
79
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
80
|
+
else echo "Some checks FAILED."; exit 1; fi
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Self-check for lessons-append.sh (docs/design/lessons-mode.md "Storage" /
|
|
3
|
+
# "## Lessons 索引" / "Write mechanism"). No framework, plain
|
|
4
|
+
# assert-and-exit, matching this repo's existing style.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
7
|
+
TMP_ROOT="$(mktemp -d)"
|
|
8
|
+
trap 'rm -rf "$TMP_ROOT"' EXIT
|
|
9
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
10
|
+
DEVLOG_DIR="$TMP_ROOT/.devlog"
|
|
11
|
+
mkdir -p "$DEVLOG_DIR"
|
|
12
|
+
FAIL=0
|
|
13
|
+
assert_exit() {
|
|
14
|
+
local desc="$1" expected="$2" actual="$3"
|
|
15
|
+
if [ "$actual" -eq "$expected" ]; then echo "PASS: $desc"
|
|
16
|
+
else echo "FAIL: $desc (expected $expected got $actual)"; FAIL=1; fi
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
echo '## Round 1 — 2026-09-12T00:00:00+08:00
|
|
20
|
+
|
|
21
|
+
### Summary
|
|
22
|
+
fixture
|
|
23
|
+
|
|
24
|
+
### Reply
|
|
25
|
+
fixture reply.
|
|
26
|
+
|
|
27
|
+
### Handoff
|
|
28
|
+
#### 現況
|
|
29
|
+
fixture
|
|
30
|
+
|
|
31
|
+
### Status
|
|
32
|
+
DONE' > "$DEVLOG_DIR/devlog.md"
|
|
33
|
+
|
|
34
|
+
# --- Refused without the main switch ---------------------------------------
|
|
35
|
+
OUT="$(bash "$SCRIPT_DIR/lessons-append.sh" --topic foo --text "x" 2>&1)"
|
|
36
|
+
assert_exit "no .enabled -> exit 1" 1 $?
|
|
37
|
+
case "$OUT" in
|
|
38
|
+
*"NOT_ENABLED"*) echo "PASS: reports NOT_ENABLED" ;;
|
|
39
|
+
*) echo "FAIL: expected NOT_ENABLED, got $OUT"; FAIL=1 ;;
|
|
40
|
+
esac
|
|
41
|
+
|
|
42
|
+
touch "$DEVLOG_DIR/.enabled"
|
|
43
|
+
|
|
44
|
+
# --- Refused without lessons-enabled ---------------------------------------
|
|
45
|
+
OUT="$(bash "$SCRIPT_DIR/lessons-append.sh" --topic foo --text "x" 2>&1)"
|
|
46
|
+
assert_exit "enabled but lessons off -> exit 1" 1 $?
|
|
47
|
+
case "$OUT" in
|
|
48
|
+
*"LESSONS_NOT_ENABLED"*) echo "PASS: reports LESSONS_NOT_ENABLED" ;;
|
|
49
|
+
*) echo "FAIL: expected LESSONS_NOT_ENABLED, got $OUT"; FAIL=1 ;;
|
|
50
|
+
esac
|
|
51
|
+
|
|
52
|
+
touch "$DEVLOG_DIR/.lessons-enabled"
|
|
53
|
+
|
|
54
|
+
# --- Missing --text ----------------------------------------------------------
|
|
55
|
+
bash "$SCRIPT_DIR/lessons-append.sh" --topic foo >/dev/null 2>&1
|
|
56
|
+
assert_exit "missing --text -> exit 1" 1 $?
|
|
57
|
+
|
|
58
|
+
# --- Invalid topic (path traversal) -----------------------------------------
|
|
59
|
+
bash "$SCRIPT_DIR/lessons-append.sh" --topic "../etc" --text "x" >/dev/null 2>&1
|
|
60
|
+
assert_exit "path-traversal topic -> exit 1" 1 $?
|
|
61
|
+
|
|
62
|
+
# --- Reserved topic names ----------------------------------------------------
|
|
63
|
+
bash "$SCRIPT_DIR/lessons-append.sh" --topic "archive" --text "x" >/dev/null 2>&1
|
|
64
|
+
assert_exit "topic 'archive' rejected -> exit 1" 1 $?
|
|
65
|
+
bash "$SCRIPT_DIR/lessons-append.sh" --topic "lessons" --text "x" >/dev/null 2>&1
|
|
66
|
+
assert_exit "topic 'lessons' rejected -> exit 1" 1 $?
|
|
67
|
+
|
|
68
|
+
# --- First entry creates the topic file and the header ----------------------
|
|
69
|
+
OUT="$(bash "$SCRIPT_DIR/lessons-append.sh" --topic "keep-move-bug" --text "第一次卡在 keep-move.sh 沒處理缺號範圍。後來改成逐一檢查連續性。")"
|
|
70
|
+
assert_exit "first entry -> 0" 0 $?
|
|
71
|
+
case "$OUT" in
|
|
72
|
+
*"PATH=.devlog/devlog.lessons.keep-move-bug.md"*) echo "PASS: reports PATH" ;;
|
|
73
|
+
*) echo "FAIL: expected PATH=..., got $OUT"; FAIL=1 ;;
|
|
74
|
+
esac
|
|
75
|
+
TARGET="$DEVLOG_DIR/devlog.lessons.keep-move-bug.md"
|
|
76
|
+
if [ -f "$TARGET" ]; then echo "PASS: topic file created"; else echo "FAIL: topic file missing"; FAIL=1; fi
|
|
77
|
+
grep -q '^# Lessons: keep-move-bug$' "$TARGET" && echo "PASS: header present" || { echo "FAIL: header missing"; FAIL=1; }
|
|
78
|
+
grep -q '^## 2026-\|^## [0-9]' "$TARGET" && echo "PASS: timestamped entry heading present" || { echo "FAIL: no entry heading"; FAIL=1; }
|
|
79
|
+
grep -q '第一次卡在 keep-move.sh' "$TARGET" && echo "PASS: entry text present" || { echo "FAIL: entry text missing"; FAIL=1; }
|
|
80
|
+
|
|
81
|
+
# --- devlog.<name>.md / .md normalization on --topic input ------------------
|
|
82
|
+
OUT2="$(bash "$SCRIPT_DIR/lessons-append.sh" --topic "devlog.lessons.keep-move-bug.md" --text "第二筆,測試正規化。")"
|
|
83
|
+
assert_exit "prefixed/suffixed topic input normalizes to the same file -> 0" 0 $?
|
|
84
|
+
case "$OUT2" in
|
|
85
|
+
*"PATH=.devlog/devlog.lessons.keep-move-bug.md"*) echo "PASS: normalized-input call reports the same PATH" ;;
|
|
86
|
+
*) echo "FAIL: expected PATH=.devlog/devlog.lessons.keep-move-bug.md, got: $OUT2"; FAIL=1 ;;
|
|
87
|
+
esac
|
|
88
|
+
ENTRY_COUNT="$(grep -c '^## ' "$TARGET")"
|
|
89
|
+
if [ "$ENTRY_COUNT" = "2" ]; then
|
|
90
|
+
echo "PASS: second call appended to the SAME file (normalization collapsed devlog.lessons.<x>.md to <x>)"
|
|
91
|
+
else
|
|
92
|
+
echo "FAIL: expected 2 entries in one file, got $ENTRY_COUNT (normalization likely created a second file)"
|
|
93
|
+
FAIL=1
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# --- Lessons 索引 rebuilt: one line, correct count and latest title ---------
|
|
97
|
+
INDEX_BLOCK="$(awk '/^## Lessons 索引/{grab=1;next} grab{print}' "$DEVLOG_DIR/devlog.md")"
|
|
98
|
+
case "$INDEX_BLOCK" in
|
|
99
|
+
*"devlog.lessons.keep-move-bug.md"*"2 則"*"第二筆,測試正規化。"*)
|
|
100
|
+
echo "PASS: index line has file, count=2, and latest entry's title" ;;
|
|
101
|
+
*)
|
|
102
|
+
echo "FAIL: unexpected index block: $INDEX_BLOCK"; FAIL=1 ;;
|
|
103
|
+
esac
|
|
104
|
+
|
|
105
|
+
# --- A second topic gets its own file and its own index line ----------------
|
|
106
|
+
bash "$SCRIPT_DIR/lessons-append.sh" --topic "span-mode-detour" --text "一開始想在 hook 裡分辨自動續接跟真人插話,後來確認做不到,改成明確標記。" >/dev/null
|
|
107
|
+
INDEX_BLOCK2="$(awk '/^## Lessons 索引/{grab=1;next} grab{print}' "$DEVLOG_DIR/devlog.md")"
|
|
108
|
+
LINE_COUNT="$(printf '%s\n' "$INDEX_BLOCK2" | grep -c '^- `devlog.lessons\.')"
|
|
109
|
+
if [ "$LINE_COUNT" = "2" ]; then
|
|
110
|
+
echo "PASS: index has one line per topic file (2 topics -> 2 lines)"
|
|
111
|
+
else
|
|
112
|
+
echo "FAIL: expected 2 index lines, got $LINE_COUNT: $INDEX_BLOCK2"
|
|
113
|
+
FAIL=1
|
|
114
|
+
fi
|
|
115
|
+
case "$INDEX_BLOCK2" in
|
|
116
|
+
*"devlog.lessons.span-mode-detour.md"*"1 則"*) echo "PASS: second topic's own line present" ;;
|
|
117
|
+
*) echo "FAIL: second topic missing from index: $INDEX_BLOCK2"; FAIL=1 ;;
|
|
118
|
+
esac
|
|
119
|
+
|
|
120
|
+
# --- Index rebuild never duplicates the heading -----------------------------
|
|
121
|
+
HEADING_COUNT="$(grep -c '^## Lessons 索引' "$DEVLOG_DIR/devlog.md")"
|
|
122
|
+
if [ "$HEADING_COUNT" = "1" ]; then
|
|
123
|
+
echo "PASS: exactly one '## Lessons 索引' heading after multiple appends"
|
|
124
|
+
else
|
|
125
|
+
echo "FAIL: expected exactly one heading, got $HEADING_COUNT"
|
|
126
|
+
FAIL=1
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# --- Title truncates at the first 。/. -------------------------------------
|
|
130
|
+
bash "$SCRIPT_DIR/lessons-append.sh" --topic "punct" --text "先卡住了。後來才發現是路徑問題,多寫了一段沒用的重試邏輯。" >/dev/null
|
|
131
|
+
INDEX_BLOCK3="$(awk '/^## Lessons 索引/{grab=1;next} grab{print}' "$DEVLOG_DIR/devlog.md")"
|
|
132
|
+
case "$INDEX_BLOCK3" in
|
|
133
|
+
*"「先卡住了。」"*) echo "PASS: title truncated at first 。" ;;
|
|
134
|
+
*) echo "FAIL: expected title truncated at first 。, got: $INDEX_BLOCK3"; FAIL=1 ;;
|
|
135
|
+
esac
|
|
136
|
+
|
|
137
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
138
|
+
else echo "Some checks FAILED."; exit 1; fi
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
4
|
+
FAIL=0
|
|
5
|
+
TMP="$(mktemp -d)"
|
|
6
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
7
|
+
|
|
8
|
+
OUT="$(CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/lessons-drift-set.sh" 5)"
|
|
9
|
+
[ "$OUT" = "NOT_STARTED" ] && echo "PASS: not started" || { echo "FAIL: $OUT"; FAIL=1; }
|
|
10
|
+
|
|
11
|
+
mkdir -p "$TMP/.devlog"
|
|
12
|
+
touch "$TMP/.devlog/.enabled"
|
|
13
|
+
OUT="$(CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/lessons-drift-set.sh" 5)"
|
|
14
|
+
[ "$OUT" = "LESSONS_NOT_ENABLED" ] && echo "PASS: lessons not enabled" || { echo "FAIL: $OUT"; FAIL=1; }
|
|
15
|
+
|
|
16
|
+
touch "$TMP/.devlog/.lessons-enabled"
|
|
17
|
+
printf '%s\n' '{"mismatch_count": 2, "threshold": 3}' > "$TMP/.devlog/.lessons-drift-state"
|
|
18
|
+
|
|
19
|
+
OUT="$(CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/lessons-drift-set.sh" 5)"
|
|
20
|
+
case "$OUT" in *LESSONS_DRIFT_THRESHOLD=5*) echo "PASS: set 5" ;; *) echo "FAIL: $OUT"; FAIL=1 ;; esac
|
|
21
|
+
grep -q '"mismatch_count": 2' "$TMP/.devlog/.lessons-drift-state" \
|
|
22
|
+
&& echo "PASS: count preserved" || { echo "FAIL: count reset"; FAIL=1; }
|
|
23
|
+
grep -q '"threshold": 5' "$TMP/.devlog/.lessons-drift-state" \
|
|
24
|
+
&& echo "PASS: threshold written" || { echo "FAIL: threshold"; FAIL=1; }
|
|
25
|
+
|
|
26
|
+
CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/lessons-drift-set.sh" 0 >/dev/null 2>&1
|
|
27
|
+
[ $? -eq 1 ] && echo "PASS: reject 0" || { echo "FAIL: accepted 0"; FAIL=1; }
|
|
28
|
+
|
|
29
|
+
rm -f "$TMP/.devlog/.lessons-drift-state"
|
|
30
|
+
OUT="$(CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/lessons-drift-set.sh" 7)"
|
|
31
|
+
case "$OUT" in *LESSONS_DRIFT_THRESHOLD=7*) echo "PASS: create 7" ;; *) echo "FAIL: create $OUT"; FAIL=1 ;; esac
|
|
32
|
+
grep -q '"mismatch_count": 0' "$TMP/.devlog/.lessons-drift-state" \
|
|
33
|
+
&& echo "PASS: create count 0" || { echo "FAIL: create count"; FAIL=1; }
|
|
34
|
+
|
|
35
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
36
|
+
else echo "Some checks FAILED."; exit 1; fi
|