@junghanacs/entwurf 0.12.4 → 0.12.6

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,129 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-user-scope-citizen — deterministic gate for pi packages[] citizen
3
+ # registration/removal (register-pi-package.py). Offline/hermetic: a fake
4
+ # settings file + a fake REPO_DIR under $TMP, no pi, no network, no ~/.pi touched.
5
+ #
6
+ # Guards the wiring that dropped when `pi install` was removed from setup
7
+ # (2026-07-03: `--entwurf-control` / `--emacs-agent-socket` unknown in a foreign
8
+ # cwd because entwurf was absent from ~/.pi/agent/settings.json packages[]).
9
+ # statusline `?` had a tripwire; THIS omission had none and hid until GLG hit it
10
+ # in another repo. This gate is that missing tripwire: if the registration ever
11
+ # regresses, `pnpm check` goes red.
12
+ set -euo pipefail
13
+
14
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ REPO="$(cd "$HERE/.." && pwd)"
16
+ REG="$REPO/scripts/register-pi-package.py"
17
+
18
+ fail=0
19
+ ok() { echo " ok $*"; }
20
+ bad() { echo " FAIL $*"; fail=1; }
21
+
22
+ command -v python3 >/dev/null || { echo "FAIL: python3 not on PATH"; exit 1; }
23
+
24
+ TMP="$(mktemp -d -t psa-user-citizen.XXXXXX)"
25
+ trap 'rm -rf "$TMP"' EXIT
26
+ S="$TMP/settings.json"
27
+ FAKE_REPO="$TMP/fake/entwurf"; mkdir -p "$FAKE_REPO"
28
+ RESOLVED="$(cd "$FAKE_REPO" && pwd)"
29
+
30
+ has_pkg() { python3 -c "import json,sys;p=json.load(open('$S'))['packages'];sys.exit(0 if '$1' in p else 1)"; }
31
+ count_entwurf() { python3 -c "import json;p=json.load(open('$S'))['packages'];print(sum(1 for x in p if isinstance((x if isinstance(x,str) else x.get('source')),str) and (x if isinstance(x,str) else x.get('source')).rstrip('/').split('/')[-1]=='entwurf' or (x if isinstance(x,str) else x.get('source'))=='$RESOLVED'))"; }
32
+
33
+ # 1. absent settings file → created with entwurf registered
34
+ python3 "$REG" "$S" "$FAKE_REPO" >/dev/null
35
+ if [ -f "$S" ] && has_pkg "$RESOLVED"; then ok "absent settings → entwurf registered (file created)"; else bad "entwurf not registered into a fresh settings file"; fi
36
+
37
+ # 2. idempotent: second run is a no-op AND does not even rewrite the file (mtime stable)
38
+ MT1="$(stat -c %Y "$S")"
39
+ sleep 1
40
+ OUT2="$(python3 "$REG" "$S" "$FAKE_REPO")"
41
+ MT2="$(stat -c %Y "$S")"
42
+ if printf '%s' "$OUT2" | grep -q 'no-op'; then ok "second run is a no-op (idempotent)"; else bad "second run was not a no-op: $OUT2"; fi
43
+ if [ "$MT1" = "$MT2" ]; then ok "no-op does NOT rewrite the file (mtime stable — strongest idempotence)"; else bad "no-op rewrote the file (mtime changed $MT1 -> $MT2)"; fi
44
+ if [ "$(count_entwurf)" = "1" ]; then ok "no duplicate entwurf entry after idempotent re-run"; else bad "duplicate entwurf entries: $(count_entwurf)"; fi
45
+
46
+ # 3. preserves unrelated packages AND unrelated keys — including look-alike repos
47
+ # whose path merely CONTAINS 'entwurf' and remote repos whose last segment is
48
+ # 'entwurf' but are not THIS package.
49
+ cat > "$S" <<'JSON'
50
+ {
51
+ "defaultProvider": "openai-codex",
52
+ "theme": "glg-dark",
53
+ "packages": ["../../repos/gh/andenken", "git:github.com/badlogic/pi-telegram",
54
+ "/home/me/entwurf-notes", "/x/openclaw-entwurf",
55
+ "git:github.com/someone/entwurf", "https://github.com/someone/entwurf"]
56
+ }
57
+ JSON
58
+ python3 "$REG" "$S" "$FAKE_REPO" >/dev/null
59
+ if python3 -c "
60
+ import json
61
+ d=json.load(open('$S')); p=d['packages']
62
+ assert '../../repos/gh/andenken' in p, 'andenken dropped'
63
+ assert 'git:github.com/badlogic/pi-telegram' in p, 'pi-telegram dropped'
64
+ assert '/home/me/entwurf-notes' in p, 'entwurf-notes wrongly filtered (substring false-positive)'
65
+ assert '/x/openclaw-entwurf' in p, 'openclaw-entwurf wrongly filtered (substring false-positive)'
66
+ assert 'git:github.com/someone/entwurf' in p, 'foreign git repo named entwurf wrongly filtered'
67
+ assert 'https://github.com/someone/entwurf' in p, 'foreign URL repo named entwurf wrongly filtered'
68
+ assert '$RESOLVED' in p, 'entwurf missing'
69
+ assert d['defaultProvider']=='openai-codex' and d['theme']=='glg-dark', 'unrelated key dropped'
70
+ " 2>/dev/null; then ok "preserves unrelated packages incl. look-alikes/foreign entwurf repos and keys"; else bad "clobbered unrelated packages/keys or a look-alike repo"; fi
71
+
72
+ # 4. stale entwurf paths (bare local 'entwurf' dir, npm install path, explicit npm
73
+ # package source) normalize to one canonical REPO_DIR.
74
+ cat > "$S" <<'JSON'
75
+ {"packages": ["/old/moved/entwurf", "/some/root/node_modules/@junghanacs/entwurf",
76
+ "npm:@junghanacs/entwurf@0.12.4", "../../repos/gh/andenken"]}
77
+ JSON
78
+ python3 "$REG" "$S" "$FAKE_REPO" >/dev/null
79
+ if python3 -c "
80
+ import json
81
+ p=json.load(open('$S'))['packages']
82
+ assert '/old/moved/entwurf' not in p, 'stale bare-entwurf path not dropped'
83
+ assert '/some/root/node_modules/@junghanacs/entwurf' not in p, 'stale npm path not dropped'
84
+ assert 'npm:@junghanacs/entwurf@0.12.4' not in p, 'stale explicit npm source not dropped'
85
+ assert '$RESOLVED' in p, 'entwurf not normalized to REPO_DIR'
86
+ assert '../../repos/gh/andenken' in p, 'andenken dropped'
87
+ ent=[x for x in p if isinstance((x if isinstance(x,str) else x.get('source')),str) and ((x if isinstance(x,str) else x.get('source')).rstrip('/').split('/')[-1]=='entwurf' or (x if isinstance(x,str) else x.get('source')).endswith('/node_modules/@junghanacs/entwurf') or (x if isinstance(x,str) else x.get('source'))=='$RESOLVED')]
88
+ assert len(ent)==1, f'expected exactly 1 entwurf entry, got {ent}'
89
+ " 2>/dev/null; then ok "stale entwurf paths/sources (bare + npm) normalized to a single REPO_DIR"; else bad "stale entwurf normalization failed"; fi
90
+
91
+ # 5. object-form entwurf entries collapse to one canonical string (no duplicate)
92
+ cat > "$S" <<JSON
93
+ {"packages": [{"source": "$RESOLVED"}, "../../repos/gh/andenken"]}
94
+ JSON
95
+ python3 "$REG" "$S" "$FAKE_REPO" >/dev/null
96
+ python3 "$REG" "$S" "$FAKE_REPO" >/dev/null # twice → still one
97
+ if python3 -c "
98
+ import json
99
+ p=json.load(open('$S'))['packages']
100
+ srcs=[(x if isinstance(x,str) else x.get('source')) for x in p]
101
+ assert srcs.count('$RESOLVED')==1, f'object-form produced duplicate/none: {p}'
102
+ assert all(not isinstance(x,dict) or x.get('source')!='$RESOLVED' for x in p), 'object-form entwurf survived (should collapse to string)'
103
+ assert '../../repos/gh/andenken' in p, 'andenken dropped'
104
+ " 2>/dev/null; then ok "object-form entwurf collapses to a single canonical string (no dup across re-runs)"; else bad "object-form entwurf caused a duplicate or survived as object"; fi
105
+
106
+ # 6. a non-object settings file fails loud (never silently wipes operator config)
107
+ echo '[]' > "$S"
108
+ if python3 "$REG" "$S" "$FAKE_REPO" >/dev/null 2>&1; then bad "non-object settings should fail, not overwrite"; else ok "non-object settings fails loud (no silent clobber)"; fi
109
+
110
+ # 7. a non-array packages value fails loud (never silently coerced to [])
111
+ echo '{"packages": {"broken": true}}' > "$S"
112
+ if python3 "$REG" "$S" "$FAKE_REPO" >/dev/null 2>&1; then bad "non-array packages should fail, not coerce to []"; else ok "non-array packages fails loud (no silent drop of operator packages)"; fi
113
+
114
+ # 8. --remove drops every entwurf entry but preserves unrelated + look-alikes
115
+ # (symmetric with install: same is_entwurf_source predicate, no over-delete).
116
+ cat > "$S" <<JSON
117
+ {"packages": ["$RESOLVED", "../../repos/gh/andenken", "/home/me/entwurf-notes"]}
118
+ JSON
119
+ python3 "$REG" "$S" "$FAKE_REPO" --remove >/dev/null
120
+ if python3 -c "
121
+ import json
122
+ p=json.load(open('$S'))['packages']
123
+ assert '$RESOLVED' not in p, 'entwurf not removed'
124
+ assert '../../repos/gh/andenken' in p, 'andenken over-removed'
125
+ assert '/home/me/entwurf-notes' in p, 'entwurf-notes over-removed (substring over-delete)'
126
+ " 2>/dev/null; then ok "--remove drops entwurf, preserves unrelated + look-alikes (symmetric with install)"; else bad "--remove over-deleted a look-alike or missed entwurf"; fi
127
+
128
+ echo
129
+ if [ "$fail" -eq 0 ]; then echo "smoke-user-scope-citizen: PASS"; else echo "smoke-user-scope-citizen: FAIL (see above)"; exit 1; fi