Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .claude/workflows/write-behaviour-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
export const meta = {
name: 'write-behaviour-tests',
description: 'Write behaviour-specific unit tests for the 24 untested effects/modifiers',
phases: [
{ title: 'Study + write', detail: 'one agent per module: read the .h, write a faithful behaviour test' },
],
}

// The 24 modules with no unit test (the broken effects.md/modifiers.md [Tests] links).
// Each entry: class name, header path, test-file destination, kind (effect|modifier).
const MODULES = [
['BlurzEffect', 'src/light/effects/BlurzEffect.h', 'effect'],
['BouncingBallsEffect', 'src/light/effects/BouncingBallsEffect.h', 'effect'],
['FixedRectangleEffect', 'src/light/effects/FixedRectangleEffect.h', 'effect'],
['FreqMatrixEffect', 'src/light/effects/FreqMatrixEffect.h', 'effect'],
['FreqSawsEffect', 'src/light/effects/FreqSawsEffect.h', 'effect'],
['GEQ3DEffect', 'src/light/effects/GEQ3DEffect.h', 'effect'],
['GEQEffect', 'src/light/effects/GEQEffect.h', 'effect'],
['LissajousEffect', 'src/light/effects/LissajousEffect.h', 'effect'],
['Noise2DEffect', 'src/light/effects/Noise2DEffect.h', 'effect'],
['NoiseMeterEffect', 'src/light/effects/NoiseMeterEffect.h', 'effect'],
['PaintBrushEffect', 'src/light/effects/PaintBrushEffect.h', 'effect'],
['PraxisEffect', 'src/light/effects/PraxisEffect.h', 'effect'],
['RandomEffect', 'src/light/effects/RandomEffect.h', 'effect'],
['RubiksCubeEffect', 'src/light/effects/RubiksCubeEffect.h', 'effect'],
['SolidEffect', 'src/light/effects/SolidEffect.h', 'effect'],
['SphereMoveEffect', 'src/light/effects/SphereMoveEffect.h', 'effect'],
['StarFieldEffect', 'src/light/effects/StarFieldEffect.h', 'effect'],
['StarSkyEffect', 'src/light/effects/StarSkyEffect.h', 'effect'],
['TetrixEffect', 'src/light/effects/TetrixEffect.h', 'effect'],
['BlockModifier', 'src/light/modifiers/BlockModifier.h', 'modifier'],
['CircleModifier', 'src/light/modifiers/CircleModifier.h', 'modifier'],
['MirrorModifier', 'src/light/modifiers/MirrorModifier.h', 'modifier'],
['RippleXZModifier', 'src/light/modifiers/RippleXZModifier.h', 'modifier'],
['TransposeModifier', 'src/light/modifiers/TransposeModifier.h', 'modifier'],
]

const RESULT = {
type: 'object',
properties: {
module: { type: 'string' },
test_file: { type: 'string' },
behaviours_pinned: { type: 'array', items: { type: 'string' },
description: 'one line per TEST_CASE: what real behaviour it asserts' },
wrote: { type: 'boolean', description: 'true if the test file was written' },
notes: { type: 'string', description: 'anything the caller should know (e.g. audio-driven, needs a fed AudioFrame; or a behaviour that could not be pinned)' },
},
required: ['module', 'test_file', 'wrote'],
}

phase('Study + write')

const results = await parallel(MODULES.map(([cls, header, kind]) => () =>
agent(
`Write a behaviour-specific doctest unit test for the projectMM module **${cls}** (${kind}).

Repo: the current workspace root (the projectMM checkout you're running in) — all paths below are relative to it.

## Study first (do NOT guess behaviour)
1. Read the module header: ${header} — understand what it ACTUALLY does: its controls, its render/modify logic, what it writes to the buffer or how it transforms coordinates. Behaviour is the spec.
2. Read the module's spec entry if useful: docs/moonmodules/light/${kind === 'effect' ? 'effects/effects.md' : 'modifiers/modifiers.md'} (find the ${cls.replace(/Effect$|Modifier$/, '')} section).
3. Read TWO existing tests as your pattern templates — match their idiom EXACTLY (includes, harness, naming, comment style):
- For an EFFECT: test/unit/light/unit_RainbowEffect.cpp (Layouts→GridLayout→Layer→addChild(effect)→onBuildState()→loop()→assert on layer.buffer()).
- For a MODIFIER: test/unit/light/unit_RegionModifier.cpp (call modifyLogical / modifyLogicalSize directly; assert coordinate folding / size).
Pick the one matching this module's kind (${kind}).

## Write the test
- Destination: test/unit/light/unit_${cls}.cpp
- First line MUST be: \`// @module ${cls}\` (this is what the doc generator + MoonDeck read — the whole point is that this module becomes a documented, tested module). Add \`// @also X, Y\` only if the test genuinely also exercises another module.
- Each TEST_CASE gets a single \`//\` comment line ABOVE it describing the behaviour it pins (the generator turns that into the doc description). Write real, present-tense descriptions.
- Assert REAL BEHAVIOUR, not just "renders non-zero". Examples of the bar:
- SolidEffect → the whole buffer is ONE uniform colour (every light equals the configured colour).
- FixedRectangleEffect → only lights inside the configured rect are lit; outside is black; defaults (0,0,0)+(15,15,15) light the origin corner.
- MirrorModifier → a coord and its mirror map to the same logical position; modifyLogicalSize halves the mirrored axis (study the .h for which axis/percentage).
- TransposeModifier → swaps axes (x↔y etc. per the .h); modifyLogicalSize swaps the corresponding size fields.
- FreqMatrix/GEQ/FreqSaws/NoiseMeter are AUDIO effects → they read an AudioFrame. Study how an existing audio path is tested or how the effect gets its data (look for AudioModule / an audio-frame accessor). If the effect needs a fed audio frame to produce output, set it up; if you truly cannot feed audio in a unit test, pin what you CAN (runs at multiple grid sizes incl 0×0 without crashing — the "Effects must run at every grid size" hard rule — and any non-audio behaviour) and say so in notes.
- Respect the hard rule: include a case that runs the effect at a DEGENERATE grid (0×0×0 or 1×1) and asserts no crash, where sensible.
- Keep it to 2–4 focused TEST_CASEs. Match the exact include style and mm:: namespace usage of the template.
- Use doctest macros (TEST_CASE / CHECK / REQUIRE) exactly as the templates do. Do NOT add the file to any CMakeLists — the test build globs test/unit/**.

## Do NOT
- Do NOT run the build or ctest (the caller compiles everything once at the end — per-agent builds would thrash).
- Do NOT edit any file other than creating test/unit/light/unit_${cls}.cpp.
- Do NOT invent controls or behaviour the .h doesn't have.

Return the structured result: the behaviours you pinned (one line each) and any notes (especially if audio-driven or a behaviour you couldn't pin).`,
{ label: `test:${cls}`, phase: 'Study + write', schema: RESULT, agentType: 'general-purpose' }
)
))

const wrote = results.filter(r => r && r.wrote)
const failed = results.filter(r => !r || !r.wrote)
return {
written: wrote.length,
failed: failed.map(r => r ? r.module : 'unknown'),
audio_or_caveats: results.filter(r => r && r.notes).map(r => ({ module: r.module, notes: r.notes })),
summary: results.filter(Boolean).map(r => ({ module: r.module, file: r.test_file, behaviours: r.behaviours_pinned })),
}
Comment on lines +93 to +98

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for other workflow files using the same top-level phase()/agent()/parallel()/return pattern,
# and find the runner that loads/executes .claude/workflows/*.js to see if it wraps the module body.
fd . .claude/workflows --type f
rg -n 'return \{' .claude/workflows -A2
rg -nP '\bnew Function\b|vm\.(Script|createContext|runInContext)|\bimport\(' .claude -g '*.js' -C3

Repository: MoonModules/projectMM

Length of output: 427


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- outline .claude/workflows/write-behaviour-tests.js ---'
wc -l .claude/workflows/write-behaviour-tests.js
sed -n '1,170p' .claude/workflows/write-behaviour-tests.js

echo
echo '--- search for workflow runner / execution wrapper ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  'phase\(|agent\(|parallel\(|write-behaviour-tests|\.claude/workflows|new Function|vm\.(Script|createContext|runInContext)|Function\(' \
  . -C 2

Repository: MoonModules/projectMM

Length of output: 13910


Remove the top-level return here. This file is parsed as JavaScript/ESM, so return { ... } at module scope is a syntax error and will stop the workflow from loading. Wrap the workflow body in a function or export the result instead.

🧰 Tools
🪛 Biome (2.5.1)

[error] 93-98: Illegal return statement outside of a function

(parse)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/workflows/write-behaviour-tests.js around lines 93 - 98, The
module-level return in write-behaviour-tests.js is invalid JavaScript/ESM and
prevents the workflow from loading. Remove the top-level return and either wrap
this result-building block in a named function or export the object directly
from the workflow module, using the existing wrote, failed, and results logic as
the source for the returned data.

Source: Linters/SAST tools

40 changes: 27 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ on:
- 'CMakeLists.txt'
- 'library.json'
- '.github/workflows/release.yml'
# The web installer + landing page are served from Pages by the deploy-pages job
# The web installer + docs site are served from Pages by the deploy-pages job
# below; a change to them must trigger a deploy or it never reaches the live site
# (the eth-only-provisioning fix shipped a commit that didn't auto-deploy because
# docs/install was missing here). src/ui/install-picker*.js is already covered by src/**.
- 'docs/install/**'
- 'docs/landing/**'
# web-installer was missing here). src/ui/install-picker*.js is already covered by src/**.
# docs/** covers every page rendered into the Pages root by MkDocs; mkdocs.yml is
# the site config (nav/theme) — a nav change with no doc change must still redeploy.
- 'docs/**'
- 'web-installer/**'
- 'mkdocs.yml'
Comment on lines +29 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -B3 -A3 "paths:" .github/workflows/release.yml | rg -n -A15 "on:" 
rg -n "web-installer" .github/workflows/release.yml

Repository: MoonModules/projectMM

Length of output: 1369


🏁 Script executed:

#!/bin/bash
cat -n .github/workflows/release.yml | sed -n '1,70p'

Repository: MoonModules/projectMM

Length of output: 3530


🏁 Script executed:

#!/bin/bash
git ls-files | rg '^web-installer(/|$)|/web-installer(/|$)'

Repository: MoonModules/projectMM

Length of output: 467


Add web-installer/** to the release workflow paths .github/workflows/release.yml still triggers on docs/** and mkdocs.yml, but not on web-installer/**; installer-only changes will skip the Pages deploy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 29 - 36, The release workflow
path filter is missing the web installer directory, so installer-only changes
will not trigger the Pages deploy. Update the path list in the release workflow
to include web-installer/** alongside the existing docs/** and mkdocs.yml
entries, using the same trigger block that already covers
src/ui/install-picker*.js.

workflow_dispatch:
inputs:
tag:
Expand Down Expand Up @@ -78,7 +81,7 @@ jobs:
TAG: ${{ inputs.tag || github.ref_name }}
run: uv run scripts/build/verify_version.py --tag "$TAG"

# The shipping firmware list, read from the generated docs/install/firmwares.json
# The shipping firmware list, read from the generated web-installer/firmwares.json
# (projected from build_esp32.py's FIRMWARES dict, drift-guarded by
# check_firmwares.py). Emitted as a JSON array so build-esp32's matrix can
# fromJSON() it — GitHub matrices can't read a file at parse time, so a job
Expand All @@ -95,7 +98,7 @@ jobs:
- id: gen
run: |
set -euo pipefail
echo "list=$(jq -c '[.firmwares[] | select(.ships) | .name]' docs/install/firmwares.json)" >> "$GITHUB_OUTPUT"
echo "list=$(jq -c '[.firmwares[] | select(.ships) | .name]' web-installer/firmwares.json)" >> "$GITHUB_OUTPUT"

build-esp32:
needs: [verify-version, firmwares]
Expand Down Expand Up @@ -338,9 +341,9 @@ jobs:
# — no CORS). The Pages-relative manifests are generated in the
# deploy-pages job, where the web installer (CORS-bound) consumes them.
BASE="https://github.com/${REPO}/releases/download/$TAG"
# The shipping firmware list — the same docs/install/firmwares.json the
# The shipping firmware list — the same web-installer/firmwares.json the
# build matrix reads, so manifests and builds can't drift.
for F in $(jq -r '.firmwares[] | select(.ships) | .name' docs/install/firmwares.json); do
for F in $(jq -r '.firmwares[] | select(.ships) | .name' web-installer/firmwares.json); do
uv run python scripts/build/generate_manifest.py \
--firmware "$F" \
--version "$V" \
Expand Down Expand Up @@ -514,7 +517,7 @@ jobs:
# Install page + the shared install-picker module sit at the root.
# Each release's binaries + manifests live under releases/<tag>/.
mkdir -p pages/install
cp -r docs/install/. pages/install/
cp -r web-installer/. pages/install/
cp src/ui/install-picker.js pages/install/
# The board-catalog / chip-detection half of the picker — web-installer
# only (not embedded in firmware), imported by index.html. Must ship to
Expand All @@ -530,16 +533,27 @@ jobs:
mkdir -p pages/install/assets/boards
# rel is "assets/boards/<slug>.<ext>" (the path served from /install/);
# the source file lives in docs/<rel> (i.e. docs/assets/boards/...).
jq -r '.[].image // empty' docs/install/deviceModels.json | while read -r rel; do
jq -r '.[].image // empty' web-installer/deviceModels.json | while read -r rel; do
src="docs/$rel"
[ -f "$src" ] && cp "$src" "pages/install/$rel" \
|| echo "WARNING: deviceModels.json image not found: $src"
done
# Root landing page (moonmodules.org/projectMM/) → Flash button + repo
# links. Without it the bare root 404s; only /install/ would exist.
cp docs/landing/index.html pages/index.html
ls -la pages/ pages/install/

- name: Build docs site into Pages root
run: |
set -euo pipefail
# Render the docs/ tree (Material for MkDocs) as the Pages ROOT
# (moonmodules.github.io/projectMM/) — the project's front door.
# Config: mkdocs.yml; deps declared inline in the build script (uv
# provisions them). Build to a temp dir, then copy INTO pages/ so the
# installer staged above under pages/install/ survives (a plain
# --site-dir pages would wipe it — mkdocs cleans its output dir).
# history/ and backlog/ are excluded in mkdocs.yml (internal docs).
uv run scripts/docs/build_docs.py --site-dir "$RUNNER_TEMP/docs-site"
cp -r "$RUNNER_TEMP/docs-site/." pages/
ls -la pages/

- uses: actions/upload-pages-artifact@v3
with:
path: pages
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ name: Test
# New Python/JS unit suites land under test/python and test/js and run here.

# Paths cover every input to the host-side tests: the Python/JS sources under test
# (scripts, docs/install), the test files themselves, AND the device-side C++ frame
# (scripts, web-installer), the test files themselves, AND the device-side C++ frame
# contract (src/core/Improv*.h + the platform handler) — a wire-format change in the
# firmware must run the cross-language golden-vector tests so it can't drift from the
# Python/JS builders silently. pull_request gates every PR; push runs main only (a
Expand All @@ -20,7 +20,7 @@ on:
pull_request:
paths: &test-paths
- 'scripts/**'
- 'docs/install/**'
- 'web-installer/**'
- 'src/core/ImprovFrame.h'
- 'src/core/ImprovOpReassembler.h'
- 'src/platform/esp32/platform_esp32_improv.cpp'
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,10 @@ Thumbs.db
# Python
__pycache__/
*.pyc

# MkDocs — built docs site (rendered fresh in CI, never committed)
/site/

# Generated test inventory pages — built into the site by scripts/docs/mkdocs_hooks.py
# (the CLI generate_test_docs.py can still write them locally, but they are not committed).
/docs/tests/*.md
Loading
Loading