Skip to content

Commit ca04366

Browse files
Script examples & scripting docs, Open VSX deployment, and broader test coverage (#18)
<!-- agent-pmo:74cf183 --> ## TLDR Adds the `scriptexamples` body of work — runnable scripting examples + rewritten scripting docs, broader F# Core/LSP/DotHttp + VS Code test coverage, and an **Open VSX deployment channel** (plus Dependabot) so the per-platform VSIXs ship to the VS Code forks alongside the Marketplace. ## Details **Deployment / CI (`.github/`)** - `release.yml`: new **`publish-openvsx`** job mirroring `publish-marketplace`. Downloads every `vsix-*` artifact and publishes **one VSIX per platform** to the Open VSX Registry (Cursor / Windsurf / VSCodium / Gitpod / Eclipse Theia) via version-pinned `npx ovsx@1.0.0 publish`. Fully independent of the Marketplace/Release/Homebrew/Scoop jobs (`!cancelled() && package-vsix != skipped`), so a missing token only stalls Open VSX. Token is passed **only** through the `OVSX_PAT` env (never on argv); the job runs least-privilege (`contents: read`, `actions: read`) and prints an actionable error (with the `ovsx create-namespace` hint) when the secret is absent. Implements `[SWR-VSIX-PUBLISH]`, `[SWR-SEC-TOKEN-PRIVILEGE]`. - New `.github/dependabot.yml`: 5 ecosystems (github-actions, npm ×2, nuget, cargo), each grouped `patterns: ["*"]` into one combined PR per run to keep pinned action SHAs + deps fresh. Implements `[SWR-SEC-ACTION-PINNING]`. - `ci.yml`: minor gate additions. **Scripting examples & docs** - New `examples/scripts/`, `examples/scripting-ctx/`, `examples/jsonplaceholder/` — runnable scripting samples (the branch's namesake). - Rewritten `website/src/docs/*` scripting pages (F#/C#/JS/Python scripting, assertions, nap/naplist files, OpenAPI import, quick-start) + new installation/onboarding screenshots. **Core / LSP / converter + tests** - `src/Napper.Core`, `src/Napper.Lsp`, `src/DotHttp` source changes with matching test additions in `Napper.Core.Tests` (26 files), `DotHttp.Tests` (13), `Napper.Lsp.Tests` (6). - VS Code extension (`src/Napper.VsCode`, 43 files): new `cliResolver` unit tests (+132 lines), `stryker.conf.json` mutation config, watcher/provider updates. 54 files added, 124 modified, 4 renamed. ## How Do The Automated Tests Prove It Works? - **Open VSX workflow** is statically validated: `actionlint .github/workflows/release.yml` → exit 0 with **zero findings on the new `publish-openvsx` job**; the job's `Require Open VSX credential` step fails fast and visibly if `OPEN_VSX_PAT` is unset rather than emitting an opaque registry auth error. The per-platform VSIXs it publishes are the same artifacts the existing `package-vsix` job already content-verifies (`bin/<platform>/napper`, `shipwright.json` present, no foreign-platform binaries). - **Manifest contract**: `npx @nimblesite/shipwright-validate-manifest --schema schemas/shipwright.schema.json src/Napper.VsCode/shipwright.json` → `valid`; CI's version-contract gate asserts `napper --version` == `napper <ver>` and `--version --json` matches the schema, and `aot-smoke` drives a real `initialize` handshake through the NativeAOT `napper lsp` binary. - **Core/converter/LSP behaviour**: the added assertions in `Napper.Core.Tests`, `DotHttp.Tests`, and `Napper.Lsp.Tests` exercise the changed parsing/scripting/LSP paths; the VS Code `cliResolver` unit tests cover the resolve/retry policy and are included in the StrykerJS mutation gate. - **Dependabot config** parses as valid YAML (`version: 2`, 5 ecosystems). ## Breaking Changes None. All changes are additive: new examples, docs, and tests, plus a new **opt-in** Open VSX publish channel that no-ops (with a clear error) until `OPEN_VSX_PAT` is configured. No existing CLI/extension behaviour or public contract changes. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1a8642c commit ca04366

182 files changed

Lines changed: 7138 additions & 2369 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/dependabot.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Keeps SHA-pinned GitHub Actions and every language dependency fresh.
2+
# Implements [SWR-SEC-ACTION-PINNING]: each ecosystem groups ALL updates into ONE
3+
# combined PR per run (patterns: ["*"]) so pinned action SHAs get bumped together
4+
# instead of one PR per package.
5+
version: 2
6+
updates:
7+
- package-ecosystem: github-actions
8+
directory: /
9+
schedule:
10+
interval: weekly
11+
groups:
12+
actions:
13+
patterns: ["*"]
14+
15+
- package-ecosystem: npm
16+
directory: /src/Napper.VsCode
17+
schedule:
18+
interval: weekly
19+
groups:
20+
vscode-extension:
21+
patterns: ["*"]
22+
23+
- package-ecosystem: npm
24+
directory: /website
25+
schedule:
26+
interval: weekly
27+
groups:
28+
website:
29+
patterns: ["*"]
30+
31+
- package-ecosystem: nuget
32+
directory: /
33+
schedule:
34+
interval: weekly
35+
groups:
36+
dotnet:
37+
patterns: ["*"]
38+
39+
- package-ecosystem: cargo
40+
directory: /src/Napper.Zed
41+
schedule:
42+
interval: weekly
43+
groups:
44+
zed-extension:
45+
patterns: ["*"]

.github/workflows/ci.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,3 +340,29 @@ jobs:
340340
- name: Build
341341
working-directory: website
342342
run: npx eleventy
343+
344+
mutation-ts:
345+
name: Mutation (TypeScript, pure layer)
346+
runs-on: ubuntu-latest
347+
timeout-minutes: 10
348+
needs: lint
349+
steps:
350+
- uses: actions/checkout@v4
351+
352+
- uses: actions/setup-node@v4
353+
with:
354+
node-version: 22
355+
cache: npm
356+
cache-dependency-path: src/Napper.VsCode/package-lock.json
357+
358+
- name: Install dependencies
359+
working-directory: src/Napper.VsCode
360+
run: npm ci
361+
362+
# StrykerJS over the host-free unit suite ONLY (htmlUtils escaping/builders +
363+
# cliResolver retry policy). The slow vscode-test/e2e extension-host layer is
364+
# intentionally NOT mutated — that would be far too slow. Runs in ~2 min and breaks
365+
# below the 80% mutation-score threshold in src/Napper.VsCode/stryker.conf.json.
366+
- name: Mutation test pure TypeScript modules
367+
working-directory: src/Napper.VsCode
368+
run: npm run mutation

.github/workflows/release.yml

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# Implements [CLI-INSTALL-HOMEBREW], [CLI-INSTALL-SCOOP], [CLI-INSTALL-DOTNET-TOOL]
12
name: Release
23

34
# Tag-triggered Shipwright release. Implements [SWR-REL-WORKFLOW], [SWR-REL-GITHUB].
@@ -407,16 +408,139 @@ jobs:
407408
- uses: actions/setup-node@v4
408409
with:
409410
node-version: 22
411+
# Install vsce ONCE at a pinned, known-replicated version. `npx @vscode/vsce`
412+
# re-resolves the package on every call, so a transient npm `latest`-tag replication
413+
# race (ETARGET) on a single iteration can abort the whole publish mid-loop and
414+
# strand some platforms. One install, one binary, reused for all targets.
415+
- name: Install vsce (pinned)
416+
run: npm install -g @vscode/vsce@3.9.1
410417
- name: Download all per-platform VSIXs
411418
uses: actions/download-artifact@v4
412419
with:
413420
path: vsix-artifacts
414421
pattern: vsix-*
415422
merge-multiple: true
416-
- name: Publish all platforms in one atomic call
417-
run: npx @vscode/vsce publish --packagePath $(find vsix-artifacts -name '*.vsix' | tr '\n' ' ')
423+
# One `vsce publish` per platform VSIX: vsce silently uses only the FIRST when several
424+
# are passed in a single call (the previous single-call step published only one
425+
# platform). Each --target VSIX MUST be published on its own. The publish is
426+
# IDEMPOTENT — a target whose (version, platform) is already on the Marketplace
427+
# ("already exists") counts as success, so a re-run after a partial publish completes
428+
# the remaining platforms instead of aborting on the first duplicate. Transient errors
429+
# retry up to 3x; one failed platform never blocks the others.
430+
# Salvaged from repo-standardization@319159f. Implements [SWR-VSIX-PUBLISH].
431+
- name: Publish each platform VSIX
418432
env:
419433
VSCE_PAT: ${{ secrets.VSCE_PAT }}
434+
run: |
435+
set -uo pipefail
436+
shopt -s globstar nullglob
437+
flag=""
438+
if [[ "${GITHUB_REF_NAME}" == *-* ]]; then
439+
flag="--pre-release"
440+
echo "Prerelease tag ${GITHUB_REF_NAME}; publishing with --pre-release"
441+
fi
442+
publish_one() {
443+
local vsix="$1" attempt out rc
444+
for attempt in 1 2 3; do
445+
out="$(vsce publish ${flag} --packagePath "${vsix}" 2>&1)"; rc=$?
446+
echo "${out}"
447+
if [ "${rc}" -eq 0 ]; then return 0; fi
448+
if echo "${out}" | grep -qiE "already exists"; then
449+
echo "→ ${vsix} already on Marketplace; treating as published."
450+
return 0
451+
fi
452+
echo "→ attempt ${attempt} failed (rc=${rc}); retrying in $((attempt*10))s..."
453+
sleep $((attempt*10))
454+
done
455+
return 1
456+
}
457+
published=0; failed=0
458+
for vsix in vsix-artifacts/**/*.vsix; do
459+
echo "Publishing ${vsix}"
460+
if publish_one "${vsix}"; then
461+
published=$((published + 1))
462+
else
463+
echo "::error::Failed to publish ${vsix} after retries"
464+
failed=$((failed + 1))
465+
fi
466+
done
467+
if [ "${published}" -eq 0 ]; then
468+
echo "::error::No VSIX artifacts found to publish"
469+
exit 1
470+
fi
471+
echo "Published/confirmed ${published} VSIX(es); ${failed} failed."
472+
[ "${failed}" -eq 0 ]
473+
474+
# ── Publish per-platform VSIXs to the Open VSX Registry ──────── [SWR-VSIX-PUBLISH]
475+
# Open VSX serves the VS Code FORKS — Cursor, Windsurf, VSCodium, Gitpod, Eclipse
476+
# Theia, Antigravity — none of which can reach the Microsoft Marketplace. Fully
477+
# independent of publish-marketplace: the Open VSX push must not be gated on the MS
478+
# Marketplace publish, and vice versa. The GitHub Release + Homebrew + Scoop ship
479+
# regardless, so a missing OVSX token can NEVER block the native-binary release —
480+
# only the Open VSX publish waits. Implements [SWR-SEC-OIDC-PUBLISH] (per-channel).
481+
publish-openvsx:
482+
name: Publish to Open VSX Registry
483+
needs: [validate-tag, package-vsix]
484+
if: ${{ !cancelled() && needs.package-vsix.result != 'skipped' }}
485+
runs-on: ubuntu-latest
486+
timeout-minutes: 10
487+
# Least privilege: this job only downloads same-run artifacts and pushes to an
488+
# external registry, so it drops the inherited top-level `contents: write` to
489+
# read-only. Open VSX has no OIDC/trusted-publishing path, so no `id-token` is
490+
# granted. Implements [SWR-SEC-TOKEN-PRIVILEGE].
491+
permissions:
492+
contents: read
493+
actions: read
494+
steps:
495+
# Turn Open VSX's opaque auth failure (what you get from an empty/blank PAT)
496+
# into an actionable, operator-facing error before anything else runs. The
497+
# GitHub Release, Marketplace, Homebrew, and Scoop do NOT depend on this job,
498+
# so a missing token only stalls the Open VSX publish.
499+
- name: Require Open VSX credential
500+
env:
501+
OVSX_PAT: ${{ secrets.OPEN_VSX_PAT }}
502+
run: |
503+
set -euo pipefail
504+
if [ -z "${OVSX_PAT:-}" ]; then
505+
echo "::error title=OPEN_VSX_PAT secret is not set::Add an Open VSX access token as the repo (or Nimblesite org) secret OPEN_VSX_PAT, then re-run, to publish the per-platform VSIXs to Open VSX. The GitHub Release (with all VSIX + CLI assets), VS Code Marketplace, Homebrew, and Scoop already shipped independently. Create a token at https://open-vsx.org/user-settings/tokens and create the publisher namespace once with: npx ovsx create-namespace nimblesite -p \$OVSX_PAT"
506+
exit 1
507+
fi
508+
echo "OPEN_VSX_PAT present — proceeding with Open VSX publish."
509+
- uses: actions/setup-node@v4
510+
with:
511+
node-version: 22
512+
- name: Download all per-platform VSIXs
513+
uses: actions/download-artifact@v4
514+
with:
515+
path: vsix-artifacts
516+
pattern: vsix-*
517+
merge-multiple: true
518+
- name: Publish every per-platform VSIX to Open VSX
519+
# The token is exposed ONLY as an env var, never on the command line: ovsx
520+
# reads OVSX_PAT automatically when -p is omitted, keeping the secret out of
521+
# the process argv (where a `ps`/dump could read it). ovsx is version-pinned —
522+
# a floating `npx ovsx` would fetch and run the latest release at publish time,
523+
# inside the very job that holds the token (a supply-chain risk). Bump it
524+
# deliberately. Implements [SWR-SEC-FROZEN-INSTALL].
525+
env:
526+
OVSX_PAT: ${{ secrets.OPEN_VSX_PAT }}
527+
run: |
528+
set -euo pipefail
529+
shopt -s nullglob
530+
# One publish per platform-specific VSIX: the target is baked into each
531+
# VSIX, so no --target flag is needed, but each must be pushed separately —
532+
# a single glob into one call would publish only the first.
533+
published=0
534+
for vsix in vsix-artifacts/*.vsix; do
535+
echo "Publishing $vsix to Open VSX"
536+
npx --yes ovsx@1.0.0 publish --packagePath "$vsix"
537+
published=$((published + 1))
538+
done
539+
if [ "$published" -eq 0 ]; then
540+
echo "::error::No VSIX artifacts found to publish to Open VSX"
541+
exit 1
542+
fi
543+
echo "Published $published VSIX(es) to Open VSX"
420544
421545
# ── SECONDARY, best-effort dotnet-tool NuGet package ──────────────────────────
422546
# continue-on-error + NOT a dependency of release / marketplace / brew / scoop, so a

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ coverage.out
5757
coverage-summary.json
5858
TestResults/
5959
mutants.out/
60+
.stryker-tmp/
6061

6162
# =============================================================================
6263
# F# / .NET
@@ -106,3 +107,7 @@ tests/Napper.Core.Tests/.spec-cache/
106107
scripts/logs/
107108

108109
scripts/.too_many_cooks/
110+
111+
__pycache__/
112+
113+
.playwright-mcp/

Claude.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
## Too Many Cooks
44

5-
⚠️ NEVER KILL VSCODE PROCESSES
5+
⚠️ NEVER KILL VSCODE PROCESSES ⚠️
6+
7+
⚠️ DON'T ASK QUESTIONS. USE YOUR JUDGMENT ⚠️
68

79
## Coding Rules
810

@@ -21,7 +23,7 @@
2123
- **Keep files under 450 LOC and functions under 20 LOC**
2224
- **No commented-out code** - Delete it
2325
- **No placeholders** - If incomplete, leave LOUD compilation error with TODO
24-
- **Spec IDs are hierarchical, descriptive, and non-numeric.** Every spec section MUST have a unique ID in the format `[GROUP-TOPIC]` or `[GROUP-TOPIC-DETAIL]` (e.g., `[CLI-PARSE-NAP]`, `[LSP-COMPLETION-VARS]`, `[HTTP-REQ-HEADERS]`). The first word is the **group** — all sections in the same group MUST be adjacent in the spec's TOC. NEVER use sequential numbers like `[SPEC-001]`. All code, tests, and design docs that implement a spec section MUST reference its ID in a comment (e.g., `// Implements [LSP-COMPLETION-VARS]`).
26+
- **Spec IDs are uniquem hierarchical, descriptive, and non-numeric.** Every spec section MUST have a unique ID in the format `[GROUP-TOPIC]` or `[GROUP-TOPIC-DETAIL]` (e.g., `[CLI-PARSE-NAP]`, `[LSP-COMPLETION-VARS]`, `[HTTP-REQ-HEADERS]`). The first word is the **group** — all sections in the same group MUST be adjacent in the spec's TOC. NEVER use sequential numbers like `[SPEC-001]`. All code, tests, and design docs that implement a spec section MUST reference its ID in a comment (e.g., `// Implements [LSP-COMPLETION-VARS]`).
2527

2628
### Rust
2729
- Keep files under 500 LOC
@@ -74,6 +76,29 @@
7476
- The test VSIX must call the actual, real CLI.
7577
- VSIX tests run in actual VS Code window
7678

79+
### HTTP: Local Test Server vs Real-World Smoke Tests
80+
81+
We test HTTP behaviour two ways, and BOTH are mandatory:
82+
83+
- **Local test server (the workhorse).** Most HTTP tests MUST hit a rich, in-process
84+
local HTTP server (`Napper.Core.Tests/LocalHttpServer.fs`, exposed as
85+
`LocalHttpServer.baseUrl`), NOT a public API. It is **hermetic**: deterministic,
86+
offline, and immune to outages. It MUST deliver a BROAD surface so tests can exercise
87+
real behaviour — many status/error codes (`/status/{code}`), variable latency
88+
(`/delay/{ms}`), several content types (`/json`, `/html`, `/xml`, `/bytes/{n}`),
89+
method/header/body echoing (`/get`, `/post`, `/anything`), and rich nested payloads.
90+
**Hammer this server as much as you like.** When behaviour you need to test isn't
91+
covered, ADD an endpoint to the local server — never reach for a public API to get it.
92+
- **Real-world smoke tests (the truth check).** Each suite MUST ALSO make a SMALL number
93+
of calls — **one or two per suite, per real API** (e.g. jsonplaceholder) — against the
94+
genuine public service. These exist precisely so a real outage or contract break
95+
**TANKS the suite** — that is the entire point of a real-world test; do not mock it
96+
away. But **never pound the same public server**: one or two calls per suite per API,
97+
no more. Cache only where a suite would otherwise repeat the same fetch.
98+
99+
Rule of thumb: breadth and volume of HTTP assertions live on the local server; a thin,
100+
deliberate layer of real-network calls proves the tool actually works against the wild.
101+
77102
### Test First Process
78103

79104
- Write test that fails because of bug/missing feature

Makefile

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# =============================================================================
44
# agent-pmo:74cf183
55

6-
.PHONY: build test lint fmt clean ci setup package-vsix test-fsharp build-zed stamp generate-types
6+
.PHONY: build test lint fmt clean ci setup package-vsix test-fsharp build-zed stamp generate-types mutation mutation-ts
77

88
# --- Cross-platform support ---
99
ifeq ($(OS),Windows_NT)
@@ -166,6 +166,21 @@ package-vsix: clean build
166166
# test-fsharp: F#-only test subset (consumed by CI's F# coverage step).
167167
test-fsharp: generate-types _test_fsharp
168168

169+
# mutation: mutation-test the F# scripting engine. Stryker.NET does not support F#
170+
# (it throws "Language not supported: Fsharp"), so this runs a curated mutant catalog —
171+
# each mutant must be KILLED by a CtxScriptTests test, or the run fails. See
172+
# scripts/mutation-test.fsx. Requires the script runtimes (node, python3, dotnet script).
173+
mutation: generate-types
174+
dotnet fsi scripts/mutation-test.fsx
175+
176+
# mutation-ts: mutation-test the PURE, host-free TypeScript modules (htmlUtils escaping +
177+
# JSON/HTML builders, cliResolver retry policy) with StrykerJS. Runs ONLY the fast unit
178+
# suite (npm run test:mutation — plain mocha, no VS Code extension host), so it stays quick;
179+
# the slow e2e/extension-host layer is deliberately NOT mutated. Gate breaks below 80%.
180+
# See src/Napper.VsCode/stryker.conf.json.
181+
mutation-ts:
182+
cd src/Napper.VsCode && npm ci && npm run mutation
183+
169184
# generate-types: regenerate Napper.Core ADTs from the typeDiagram source of truth.
170185
# Types.td is canonical and checked in; Types.Generated.fs is gitignored and
171186
# rebuilt here. The preamble adds the namespace and the one host-type bridge

0 commit comments

Comments
 (0)