aboutsummaryrefslogtreecommitdiff
path: root/scripts/tests/update-skills.bats
blob: d74da1ca858ebe003647d2c5d11a559efac0684a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
#!/usr/bin/env bats
# update-skills.py keeps forked skills/commands in sync with their upstreams
# via per-fork manifests (upstreams/<name>/manifest.json), a committed baseline
# snapshot (upstreams/<name>/baseline/), and 3-way merges against it. The
# script is read-only against fork targets: check classifies, merge-file
# merges to stdout; only bootstrap and mark-synced write (manifest + baseline).

setup() {
    REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
    SCRIPT="$REPO_ROOT/scripts/update-skills.py"
    TMP="$(mktemp -d)"
    CACHE="$TMP/cache"
}

teardown() {
    rm -rf "$TMP"
}

# --- fixture helpers -------------------------------------------------------

git_up() {
    git -C "$TMP/up" -c user.name=test -c user.email=test@test "$@"
}

make_upstream() {
    mkdir -p "$TMP/up/skills/demo"
    git -C "$TMP/up" init -q -b main
    printf 'line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\n' \
        > "$TMP/up/skills/demo/SKILL.md"
    printf 'def helper():\n    return 1\n' > "$TMP/up/skills/demo/helper.py"
    git_up add -A
    git_up commit -qm "initial"
}

make_repo() {
    mkdir -p "$TMP/root/upstreams/demo" "$TMP/root/demo-skill"
    cp "$TMP/up/skills/demo/SKILL.md" "$TMP/up/skills/demo/helper.py" \
        "$TMP/root/demo-skill/"
    cat > "$TMP/root/upstreams/demo/manifest.json" <<EOF
{
  "name": "demo",
  "url": "file://$TMP/up",
  "ref": "main",
  "subpath": "skills/demo",
  "target": "demo-skill",
  "license": "MIT",
  "last_synced_commit": null
}
EOF
}

run_us() {
    run python3 "$SCRIPT" --root "$TMP/root" --cache "$CACHE" "$@"
}

bootstrap_demo() {
    python3 "$SCRIPT" --root "$TMP/root" --cache "$CACHE" bootstrap demo
}

# --- list ------------------------------------------------------------------

@test "list shows fork with never-synced marker" {
    make_upstream
    make_repo
    run_us list
    [ "$status" -eq 0 ]
    [[ "$output" == *"demo"* ]]
    [[ "$output" == *"never synced"* ]]
}

# --- bootstrap -------------------------------------------------------------

@test "bootstrap snapshots baseline and records upstream commit" {
    make_upstream
    make_repo
    run_us bootstrap demo
    [ "$status" -eq 0 ]
    [ -f "$TMP/root/upstreams/demo/baseline/SKILL.md" ]
    [ -f "$TMP/root/upstreams/demo/baseline/helper.py" ]
    sha=$(git_up rev-parse HEAD)
    grep -q "$sha" "$TMP/root/upstreams/demo/manifest.json"
}

# --- check classification --------------------------------------------------

@test "check classifies identical tree as unchanged" {
    make_upstream
    make_repo
    bootstrap_demo
    run_us check demo
    [ "$status" -eq 0 ]
    [[ "$output" == *"unchanged"* ]]
    [[ "$output" != *"both-changed"* ]]
    [[ "$output" != *"local-only"* ]]
    [[ "$output" != *"upstream-changed"* ]]
}

@test "check classifies upstream-changed after upstream edit" {
    make_upstream
    make_repo
    bootstrap_demo
    sed -i 's/line1/line1 upstream edit/' "$TMP/up/skills/demo/SKILL.md"
    git_up commit -qam "upstream edit"
    run_us check demo
    [ "$status" -eq 0 ]
    [[ "$output" == *"upstream-changed"*"SKILL.md"* ]]
}

@test "check classifies local-only after target edit" {
    make_upstream
    make_repo
    bootstrap_demo
    sed -i 's/line8/line8 local edit/' "$TMP/root/demo-skill/SKILL.md"
    run_us check demo
    [ "$status" -eq 0 ]
    [[ "$output" == *"local-only"*"SKILL.md"* ]]
}

@test "check classifies both-changed when both sides edited" {
    make_upstream
    make_repo
    bootstrap_demo
    sed -i 's/line1/line1 upstream edit/' "$TMP/up/skills/demo/SKILL.md"
    git_up commit -qam "upstream edit"
    sed -i 's/line8/line8 local edit/' "$TMP/root/demo-skill/SKILL.md"
    run_us check demo
    [ "$status" -eq 0 ]
    [[ "$output" == *"both-changed"*"SKILL.md"* ]]
}

@test "check classifies upstream-new, local-new, and upstream-deleted" {
    make_upstream
    make_repo
    bootstrap_demo
    echo "new upstream file" > "$TMP/up/skills/demo/new-up.md"
    git_up rm -q skills/demo/helper.py
    git_up add -A
    git_up commit -qm "add one, delete one"
    echo "new local file" > "$TMP/root/demo-skill/new-local.md"
    run_us check demo
    [ "$status" -eq 0 ]
    [[ "$output" == *"upstream-new"*"new-up.md"* ]]
    [[ "$output" == *"local-new"*"new-local.md"* ]]
    [[ "$output" == *"upstream-deleted"*"helper.py"* ]]
}

@test "check --json emits valid JSON with upstream commit and files" {
    make_upstream
    make_repo
    bootstrap_demo
    run_us check demo --json
    [ "$status" -eq 0 ]
    echo "$output" | python3 -c '
import json, sys
d = json.load(sys.stdin)
assert d["name"] == "demo"
assert len(d["upstream_commit"]) == 40
assert any(f["path"] == "SKILL.md" for f in d["files"])
'
}

@test "check without baseline degrades to no-baseline statuses" {
    make_upstream
    make_repo
    sed -i 's/line8/line8 local edit/' "$TMP/root/demo-skill/SKILL.md"
    run_us check demo
    [ "$status" -eq 0 ]
    [[ "$output" == *"no-baseline"* ]]
}

# --- merge-file ------------------------------------------------------------

@test "merge-file merges non-overlapping edits cleanly" {
    make_upstream
    make_repo
    bootstrap_demo
    sed -i 's/line1/line1 upstream edit/' "$TMP/up/skills/demo/SKILL.md"
    git_up commit -qam "upstream edit"
    sed -i 's/line8/line8 local edit/' "$TMP/root/demo-skill/SKILL.md"
    python3 "$SCRIPT" --root "$TMP/root" --cache "$CACHE" check demo > /dev/null
    run_us merge-file demo SKILL.md
    [ "$status" -eq 0 ]
    [[ "$output" == *"line1 upstream edit"* ]]
    [[ "$output" == *"line8 local edit"* ]]
    [[ "$output" != *"<<<<<<<"* ]]
}

@test "merge-file emits conflict markers on overlapping edits" {
    make_upstream
    make_repo
    bootstrap_demo
    sed -i 's/line4/line4 upstream edit/' "$TMP/up/skills/demo/SKILL.md"
    git_up commit -qam "upstream edit"
    sed -i 's/line4/line4 local edit/' "$TMP/root/demo-skill/SKILL.md"
    python3 "$SCRIPT" --root "$TMP/root" --cache "$CACHE" check demo > /dev/null
    run_us merge-file demo SKILL.md
    [ "$status" -eq 1 ]
    [[ "$output" == *"<<<<<<<"* ]]
    [[ "$output" == *"line4 upstream edit"* ]]
    [[ "$output" == *"line4 local edit"* ]]
}

@test "merge-file reports a hard git error instead of masking it as a conflict" {
    make_upstream
    make_repo
    bootstrap_demo
    printf 'up\x00stream' > "$TMP/up/skills/demo/SKILL.md"
    git_up commit -qam "binary upstream"
    printf 'lo\x00cal' > "$TMP/root/demo-skill/SKILL.md"
    python3 "$SCRIPT" --root "$TMP/root" --cache "$CACHE" check demo > /dev/null
    run_us merge-file demo SKILL.md
    [ "$status" -eq 2 ]
    [[ "$output" == *"merge-file failed"* ]]
}

# --- mark-synced -----------------------------------------------------------

@test "mark-synced refreshes baseline and last_synced_commit" {
    make_upstream
    make_repo
    bootstrap_demo
    sed -i 's/line1/line1 upstream edit/' "$TMP/up/skills/demo/SKILL.md"
    git_up commit -qam "upstream edit"
    sha=$(git_up rev-parse HEAD)
    python3 "$SCRIPT" --root "$TMP/root" --cache "$CACHE" check demo > /dev/null
    run_us mark-synced demo
    [ "$status" -eq 0 ]
    grep -q "line1 upstream edit" "$TMP/root/upstreams/demo/baseline/SKILL.md"
    grep -q "$sha" "$TMP/root/upstreams/demo/manifest.json"
}

# --- files map (the arch-decide shape) --------------------------------------

@test "files map restricts tracking to mapped files under target paths" {
    make_upstream
    make_repo
    mkdir -p "$TMP/root/commands"
    cp "$TMP/up/skills/demo/SKILL.md" "$TMP/root/commands/demo.md"
    cat > "$TMP/root/upstreams/demo/manifest.json" <<EOF
{
  "name": "demo",
  "url": "file://$TMP/up",
  "ref": "main",
  "subpath": "skills/demo",
  "target": "commands",
  "files": {"SKILL.md": "demo.md"},
  "license": "MIT",
  "last_synced_commit": null
}
EOF
    bootstrap_demo
    [ -f "$TMP/root/upstreams/demo/baseline/demo.md" ]
    run_us check demo
    [ "$status" -eq 0 ]
    [[ "$output" == *"demo.md"* ]]
    # helper.py exists upstream but is unmapped — must not be tracked
    [[ "$output" != *"helper.py"* ]]
}

# --- exclusions --------------------------------------------------------------

@test "dependency and cache dirs excluded from classification" {
    make_upstream
    make_repo
    bootstrap_demo
    mkdir -p "$TMP/root/demo-skill/node_modules/x" "$TMP/root/demo-skill/__pycache__"
    echo "x" > "$TMP/root/demo-skill/node_modules/x/x.js"
    echo "x" > "$TMP/root/demo-skill/__pycache__/y.pyc"
    run_us check demo
    [ "$status" -eq 0 ]
    [[ "$output" != *"node_modules"* ]]
    [[ "$output" != *"__pycache__"* ]]
}

# --- errors ------------------------------------------------------------------

@test "unknown fork errors and names it" {
    make_upstream
    make_repo
    run_us check nosuchfork
    [ "$status" -ne 0 ]
    [[ "$output" == *"nosuchfork"* ]]
}

@test "unreachable upstream degrades with a clear error" {
    make_upstream
    make_repo
    python3 - "$TMP/root/upstreams/demo/manifest.json" <<'EOF'
import json, sys
p = sys.argv[1]
d = json.load(open(p))
d["url"] = "file:///nonexistent/upstream/repo"
json.dump(d, open(p, "w"), indent=2)
EOF
    run_us check demo
    [ "$status" -ne 0 ]
    [[ "$output" == *"demo"* ]]
    [[ "$output" == *"clone"* ]]
}