diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index dd4d784..50852bf 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -7,9 +7,10 @@ # # Pipeline: # 0. Detect changes (path-based gating for all downstream jobs) -# 1a. Frontend tests — skipped if only BE/docs changed +# 1a. Frontend tests — skipped if only BE/docs/packages changed # 1b. Go unit tests — all non-DB packages + SQLite integration (race-enabled) # 1c. Go test (PG) — PG store + handlers against Postgres (race-enabled) +# 1d. Test runners — disabled until v0.7.5 (Playwright headless fix) # 2. Build + Deploy — skipped if docs-only change # # Test coverage mapping (no package tested by zero jobs): @@ -23,7 +24,9 @@ # Path gating rules: # src/, src/editor/ → frontend tests # server/, scripts/db-* → backend tests (PG + SQLite) +# packages/ → test-runners (surface/extension tests) # Dockerfile*, k8s/, .gitea/ → all tests (infra change) +# ci/ → infra (CI scripts) # docs/, *.md → skip all tests + deploy # VERSION, scripts/* → frontend + backend tests # Tags (v*) → always full pipeline @@ -100,6 +103,7 @@ jobs: outputs: frontend: ${{ steps.filter.outputs.frontend }} backend: ${{ steps.filter.outputs.backend }} + packages: ${{ steps.filter.outputs.packages }} infra: ${{ steps.filter.outputs.infra }} docs_only: ${{ steps.filter.outputs.docs_only }} steps: @@ -137,7 +141,7 @@ jobs: echo "${CHANGED}" | sed 's/^/ /' # Classify - FE=false; BE=false; INFRA=false; DOCS=false; OTHER=false + FE=false; BE=false; PKG=false; INFRA=false; DOCS=false; OTHER=false while IFS= read -r file; do [[ -z "$file" ]] && continue case "$file" in @@ -145,19 +149,23 @@ jobs: FE=true ;; server/*|scripts/db-*) BE=true ;; + packages/*) + PKG=true ;; .gitea/*|k8s/*|Dockerfile*|docker-compose*|docker-entrypoint*|nginx.conf) INFRA=true ;; docs/*|*.md|CHANGELOG.md|LICENSE) DOCS=true ;; VERSION|scripts/*) FE=true; BE=true ;; + ci/*) + INFRA=true ;; *) OTHER=true ;; esac done <<< "${CHANGED}" # Docs-only: only docs changed, nothing else - if [[ "$DOCS" == "true" && "$FE" == "false" && "$BE" == "false" && "$INFRA" == "false" && "$OTHER" == "false" ]]; then + if [[ "$DOCS" == "true" && "$FE" == "false" && "$BE" == "false" && "$PKG" == "false" && "$INFRA" == "false" && "$OTHER" == "false" ]]; then DOCS_ONLY=true else DOCS_ONLY=false @@ -165,6 +173,7 @@ jobs: echo "frontend=${FE}" >> "$GITHUB_OUTPUT" echo "backend=${BE}" >> "$GITHUB_OUTPUT" + echo "packages=${PKG}" >> "$GITHUB_OUTPUT" echo "infra=${INFRA}" >> "$GITHUB_OUTPUT" echo "docs_only=${DOCS_ONLY}" >> "$GITHUB_OUTPUT" @@ -172,6 +181,7 @@ jobs: echo "━━━ Change Detection ━━━" echo " frontend: ${FE}" echo " backend: ${BE}" + echo " packages: ${PKG}" echo " infra: ${INFRA}" echo " docs_only: ${DOCS_ONLY}" @@ -375,12 +385,13 @@ jobs: # test-runners surface after deploy until this is resolved. # See: docker-compose.ci.yml, ci/surface-test-driver.js # - # Runs when: backend or frontend changed (runners test both tiers). + # Runs when: backend, frontend, or packages changed (runners test all tiers). # Skipped when: only docs changed. test-runners: runs-on: ubuntu-latest needs: [detect-changes] - if: false # disabled — see comment above + if: false # disabled — see comment above. Condition for v0.7.5: + # needs.detect-changes.outputs.backend == 'true' || needs.detect-changes.outputs.frontend == 'true' || needs.detect-changes.outputs.packages == 'true' || needs.detect-changes.outputs.infra == 'true' steps: - name: Checkout uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index afdec3b..558e4fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ All notable changes to Armature are documented here. +## v0.7.3 — Extension Shell Migration + +**Shell Topbar Migration** +- Migrated Chat, Notes, and Schedules from legacy `sw.shell.Topbar` component to the v0.7.0 shell topbar contract (`sw.shell.topbar.setTitle/setSlot`) +- Eliminated double topbar (shell-injected + surface-owned) on all three extension surfaces +- Chat: reactive slot updates for thread title + People button when conversation changes +- Notes: slot content for + New Note, Import .md, and Graph toggle buttons +- Schedules: reactive slot with schedule count and + New Schedule button; removed legacy fallback branch + +**Runner Test Updates** +- Added `shell-topbar` test suite to chat-runner, notes-runner, and schedules-runner +- Tests fetch surface JS and assert: no legacy `sw.shell.Topbar` reference, uses `sw.shell.topbar.setTitle/setSlot` API +- 6 new tests across 3 runners + +**Package Versions** +- Chat surface v0.3.0, Notes surface v0.9.0, Schedules surface v0.2.0 +- Chat runner v0.2.0, Notes runner v0.2.0, Schedules runner v0.2.0 + +**Roadmap** +- Headless E2E automation moved to v0.7.5 (independent from shell migration) + ## v0.7.1 — Surface Runner Framework **`sw.testing` SDK Module** diff --git a/ROADMAP.md b/ROADMAP.md index e80d9cd..90dd6dd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # Armature — Roadmap -## Current: v0.7.2 — Package Runners + CI Gate +## Current: v0.7.3 — Extension Shell Migration Self-hosted extensible platform. Auth, identity, packages, Starlark sandbox, storage, realtime, and ops are kernel primitives. Everything else is an extension. @@ -152,9 +152,7 @@ Design doc: `docs/DESIGN-surface-runners.md` | CI integration | done | `test-runners` stage in Gitea CI. Playwright driver, `wait-for-healthy.sh`. | | CI DinD networking fix | done | Resolve container IP via `docker inspect` — DinD port mapping doesn't expose to runner localhost. | -### v0.7.3 — Extension Shell Migration + Headless E2E - -**Extension Shell Migration** +### v0.7.3 — Extension Shell Migration Chat, Notes, and Schedules still use the old `sw.shell.Topbar` component, producing a double topbar (shell-injected + surface-owned). Migrate all @@ -163,20 +161,10 @@ same pattern as the kernel surface migrations. | Step | Status | Description | |------|--------|-------------| -| Chat → shell topbar | | Delete ``, use `setTitle('Chat')` + `setSlot()` for thread title/people button. | -| Notes → shell topbar | | Delete ``, use `setTitle('Notes')` + `setSlot()` for sidebar tabs/search. | -| Schedules → shell topbar | | Delete ``, use `setTitle('Schedules')`. Pattern A (title only). | -| Update package runner tests | | Assert shell topbar present, no double topbar, on migrated surfaces. | - -**Headless E2E Automation** - -| Step | Status | Description | -|------|--------|-------------| -| Playwright test harness | | `ci/e2e-surface-test.sh` — docker-compose, chromium, run-all, assert. | -| Screenshot-on-failure | | Full-page screenshot + console log. CI artifacts. | -| Navigation smoke test | | Playwright visits every surface. Asserts topbar, no JS errors, home link works. | -| Visual regression baseline | | Optional screenshot diff. Not a gate — report for review. | -| CI pipeline integration | | After unit tests + API runners. Failure blocks merge. | +| Chat → shell topbar | done | Delete ``, use `setTitle('Chat')` + `setSlot()` for thread title/people button. | +| Notes → shell topbar | done | Delete ``, use `setTitle('Notes')` + `setSlot()` for action buttons. | +| Schedules → shell topbar | done | Delete ``, use `setTitle('Schedules')` + `setSlot()` for count/new button. | +| Update package runner tests | done | Shell-topbar test suite in each runner — assert no legacy Topbar, uses shell API. | ### v0.7.4 — Documentation + Deferred Surface Work @@ -188,6 +176,16 @@ same pattern as the kernel surface migrations. | Extension config section docs | | Document `config_section` manifest field for Settings/Admin extensibility. | | Team Admin Workflows evaluation | | 723-line inline designer — extract to surface or split into files. Decision doc if needed. | +### v0.7.5 — Headless E2E + CI Gate + +| Step | Status | Description | +|------|--------|-------------| +| Playwright test harness | | `ci/e2e-surface-test.sh` — docker-compose, chromium, run-all, assert. | +| Screenshot-on-failure | | Full-page screenshot + console log. CI artifacts. | +| Navigation smoke test | | Playwright visits every surface. Asserts topbar, no JS errors, home link works. | +| Visual regression baseline | | Optional screenshot diff. Not a gate — report for review. | +| CI pipeline integration | | Enable `test-runners` stage, add `e2e-smoke` stage. Failure blocks merge. | + --- ## Post-v0.7.x diff --git a/VERSION b/VERSION index 7486fdb..f38fc53 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.2 +0.7.3 diff --git a/packages/chat-runner/js/main.js b/packages/chat-runner/js/main.js index 23a792f..bfcb41a 100644 --- a/packages/chat-runner/js/main.js +++ b/packages/chat-runner/js/main.js @@ -35,7 +35,8 @@ var modules = [ 'conversations.js', - 'messaging.js' + 'messaging.js', + 'shell-topbar.js' ]; var loaded = 0; diff --git a/packages/chat-runner/js/shell-topbar.js b/packages/chat-runner/js/shell-topbar.js new file mode 100644 index 0000000..8960640 --- /dev/null +++ b/packages/chat-runner/js/shell-topbar.js @@ -0,0 +1,31 @@ +/** + * Chat Runner — chat/shell-topbar suite + * + * Validates the Chat surface uses the v0.7.0 shell topbar contract + * and does not render the legacy sw.shell.Topbar component. + */ +(function () { + 'use strict'; + + sw.testing.suite('chat/shell-topbar', async function (s) { + + s.test('surface JS does not reference legacy Topbar', async function (t) { + var base = window.__BASE__ || ''; + var resp = await fetch(base + '/surfaces/chat/js/main.js'); + t.assert.eq(resp.status, 200, 'fetched chat main.js'); + var src = await resp.text(); + var hasLegacy = src.indexOf('sw.shell.Topbar') !== -1; + t.assert.ok(!hasLegacy, 'no sw.shell.Topbar reference (uses shell topbar API)'); + }); + + s.test('surface JS uses shell topbar API', async function (t) { + var base = window.__BASE__ || ''; + var resp = await fetch(base + '/surfaces/chat/js/main.js'); + var src = await resp.text(); + var usesAPI = src.indexOf('sw.shell.topbar.setTitle') !== -1 + || src.indexOf('sw.shell.topbar.setSlot') !== -1; + t.assert.ok(usesAPI, 'uses sw.shell.topbar.setTitle or setSlot'); + }); + + }); +})(); diff --git a/packages/chat-runner/manifest.json b/packages/chat-runner/manifest.json index 2fcf35a..5cb85e2 100644 --- a/packages/chat-runner/manifest.json +++ b/packages/chat-runner/manifest.json @@ -4,6 +4,6 @@ "type": "test-runner", "title": "Chat Runner", "auth": "admin", - "version": "0.1.0", + "version": "0.2.0", "description": "Integration tests for Chat package — conversations, messaging, search." } diff --git a/packages/chat/js/main.js b/packages/chat/js/main.js index 83c86a1..c10c152 100644 --- a/packages/chat/js/main.js +++ b/packages/chat/js/main.js @@ -1,12 +1,12 @@ /** - * Chat — Surface Entry Point (v0.2.0) + * Chat — Surface Entry Point (v0.3.0) * * Messaging surface built on chat-core library: * sw.api.ext('chat-core') — conversation/message CRUD * sw.api.ext('chat') — typing indicators * sw.realtime — live events * sw.ui.* — primitive components - * sw.shell.Topbar — navigation bar + * sw.shell.topbar — shell topbar API */ (async function () { 'use strict'; @@ -42,7 +42,6 @@ var api = sw.api.ext('chat-core'); var chatApi = sw.api.ext('chat'); var { Button, Spinner, Avatar, Dialog, Tabs } = sw.ui; - var Topbar = sw.shell.Topbar; // Import UserPicker directly (not in sw.ui index) var { UserPicker } = await import(base + '/js/sw/primitives/user-picker.js?v=' + ver); @@ -831,20 +830,33 @@ var selectedConv = conversations.find(c => c.id === selectedId); var threadTitle = selectedConv ? (selectedConv.title || 'Direct Message') : ''; + // ── Shell topbar ─────────────────────────── + useEffect(() => { + if (!sw.shell?.topbar) return; + sw.shell.topbar.setTitle('Chat'); + }, []); + + useEffect(() => { + if (!sw.shell?.topbar) return; + if (selectedId) { + sw.shell.topbar.setSlot(html` + ${threadTitle} + <${Button} size="sm" variant="secondary" + onClick=${() => setShowParticipants(!showParticipants)}> + ${showParticipants ? 'Hide' : 'People'} + + `); + } else { + sw.shell.topbar.setSlot(null); + } + }, [selectedId, threadTitle, showParticipants]); + if (loading) { return html`
<${Spinner} />
`; } return html`
- <${Topbar} title="Chat"> - ${selectedId && html` - ${threadTitle} - <${Button} size="sm" variant="secondary" - onClick=${() => setShowParticipants(!showParticipants)}> - ${showParticipants ? 'Hide' : 'People'} - `} -
<${ConversationList} selected=${selectedId} diff --git a/packages/chat/manifest.json b/packages/chat/manifest.json index 9e69328..5906bc8 100644 --- a/packages/chat/manifest.json +++ b/packages/chat/manifest.json @@ -6,7 +6,7 @@ "route": "/s/chat", "auth": "authenticated", "layout": "single", - "version": "0.2.0", + "version": "0.3.0", "icon": "\ud83d\udcac", "description": "Chat surface — conversations, messaging, typing indicators, read receipts.", "author": "armature", diff --git a/packages/notes-runner/js/main.js b/packages/notes-runner/js/main.js index fb1b035..6650ac9 100644 --- a/packages/notes-runner/js/main.js +++ b/packages/notes-runner/js/main.js @@ -36,7 +36,8 @@ var modules = [ 'notes-crud.js', 'folders.js', - 'tags-search.js' + 'tags-search.js', + 'shell-topbar.js' ]; var loaded = 0; diff --git a/packages/notes-runner/js/shell-topbar.js b/packages/notes-runner/js/shell-topbar.js new file mode 100644 index 0000000..e8dcef4 --- /dev/null +++ b/packages/notes-runner/js/shell-topbar.js @@ -0,0 +1,31 @@ +/** + * Notes Runner — notes/shell-topbar suite + * + * Validates the Notes surface uses the v0.7.0 shell topbar contract + * and does not render the legacy sw.shell.Topbar component. + */ +(function () { + 'use strict'; + + sw.testing.suite('notes/shell-topbar', async function (s) { + + s.test('surface JS does not reference legacy Topbar', async function (t) { + var base = window.__BASE__ || ''; + var resp = await fetch(base + '/surfaces/notes/js/main.js'); + t.assert.eq(resp.status, 200, 'fetched notes main.js'); + var src = await resp.text(); + var hasLegacy = src.indexOf('sw.shell.Topbar') !== -1; + t.assert.ok(!hasLegacy, 'no sw.shell.Topbar reference (uses shell topbar API)'); + }); + + s.test('surface JS uses shell topbar API', async function (t) { + var base = window.__BASE__ || ''; + var resp = await fetch(base + '/surfaces/notes/js/main.js'); + var src = await resp.text(); + var usesAPI = src.indexOf('sw.shell.topbar.setTitle') !== -1 + || src.indexOf('sw.shell.topbar.setSlot') !== -1; + t.assert.ok(usesAPI, 'uses sw.shell.topbar.setTitle or setSlot'); + }); + + }); +})(); diff --git a/packages/notes-runner/manifest.json b/packages/notes-runner/manifest.json index 07d7ba9..7eb4abf 100644 --- a/packages/notes-runner/manifest.json +++ b/packages/notes-runner/manifest.json @@ -4,6 +4,6 @@ "type": "test-runner", "title": "Notes Runner", "auth": "admin", - "version": "0.1.0", + "version": "0.2.0", "description": "Integration tests for Notes package — CRUD, folders, tags, backlinks, search." } diff --git a/packages/notes/js/main.js b/packages/notes/js/main.js index 86c0744..551fec0 100644 --- a/packages/notes/js/main.js +++ b/packages/notes/js/main.js @@ -1,10 +1,10 @@ /** - * Notes — Surface Entry Point (v0.8.0) + * Notes — Surface Entry Point (v0.9.0) * * Markdown notes surface using the SDK: * sw.api.ext('notes') — scoped API client * sw.ui.* — primitive components - * sw.shell.Topbar — navigation bar + * sw.shell.topbar — shell topbar API * window.CM — CodeMirror 6 bundle (optional) */ (async function () { @@ -59,7 +59,6 @@ // ── SDK modules ──────────────────────────── var api = sw.api.ext('notes'); var { Button, Spinner } = sw.ui; - var Topbar = sw.shell.Topbar; // ── Debounce helper ──────────────────────── var _timers = {}; @@ -1729,13 +1728,19 @@ debounce('search', function() { loadNotes(); }, 300); } - return html` - <${Topbar} title="Notes"> + // ── Shell topbar ─────────────────────────── + useEffect(() => { + if (!sw.shell?.topbar) return; + sw.shell.topbar.setTitle('Notes'); + sw.shell.topbar.setSlot(html` <${Button} variant="primary" size="sm" onClick=${handleNew}>+ New Note <${Button} variant="secondary" size="sm" onClick=${handleImport}>Import .md <${Button} variant=${showGraph ? 'primary' : 'secondary'} size="sm" onClick=${function() { setShowGraph(!showGraph); }}>Graph - + `); + }, [showGraph]); + + return html`
<${SidebarTabs} activeTab=${sidebarTab} onTabChange=${setSidebarTab} noteSelected=${!!activeNote} /> diff --git a/packages/notes/manifest.json b/packages/notes/manifest.json index 0626533..8f4954f 100644 --- a/packages/notes/manifest.json +++ b/packages/notes/manifest.json @@ -6,7 +6,7 @@ "route": "/s/notes", "auth": "authenticated", "layout": "single", - "version": "0.8.0", + "version": "0.9.0", "icon": "📝", "description": "Markdown notes surface with rich editor, folders, tags, backlinks, import/export, sidebar tabs, document outline, and note graph.", "author": "armature", diff --git a/packages/schedules-runner/js/main.js b/packages/schedules-runner/js/main.js index c91f469..7f2fe3b 100644 --- a/packages/schedules-runner/js/main.js +++ b/packages/schedules-runner/js/main.js @@ -34,7 +34,8 @@ if (window.SR.base) assetBase = window.SR.base + assetBase; var modules = [ - 'schedules-crud.js' + 'schedules-crud.js', + 'shell-topbar.js' ]; var loaded = 0; diff --git a/packages/schedules-runner/js/shell-topbar.js b/packages/schedules-runner/js/shell-topbar.js new file mode 100644 index 0000000..473e91f --- /dev/null +++ b/packages/schedules-runner/js/shell-topbar.js @@ -0,0 +1,31 @@ +/** + * Schedules Runner — schedules/shell-topbar suite + * + * Validates the Schedules surface uses the v0.7.0 shell topbar contract + * and does not render the legacy sw.shell.Topbar component. + */ +(function () { + 'use strict'; + + sw.testing.suite('schedules/shell-topbar', async function (s) { + + s.test('surface JS does not reference legacy Topbar', async function (t) { + var base = window.__BASE__ || ''; + var resp = await fetch(base + '/surfaces/schedules/js/main.js'); + t.assert.eq(resp.status, 200, 'fetched schedules main.js'); + var src = await resp.text(); + var hasLegacy = src.indexOf('sw.shell.Topbar') !== -1; + t.assert.ok(!hasLegacy, 'no sw.shell.Topbar reference (uses shell topbar API)'); + }); + + s.test('surface JS uses shell topbar API', async function (t) { + var base = window.__BASE__ || ''; + var resp = await fetch(base + '/surfaces/schedules/js/main.js'); + var src = await resp.text(); + var usesAPI = src.indexOf('sw.shell.topbar.setTitle') !== -1 + || src.indexOf('sw.shell.topbar.setSlot') !== -1; + t.assert.ok(usesAPI, 'uses sw.shell.topbar.setTitle or setSlot'); + }); + + }); +})(); diff --git a/packages/schedules-runner/manifest.json b/packages/schedules-runner/manifest.json index 0818ed1..b7f0b82 100644 --- a/packages/schedules-runner/manifest.json +++ b/packages/schedules-runner/manifest.json @@ -4,6 +4,6 @@ "type": "test-runner", "title": "Schedules Runner", "auth": "admin", - "version": "0.1.0", + "version": "0.2.0", "description": "Integration tests for Schedules package — CRUD, run, enable/disable." } diff --git a/packages/schedules/js/main.js b/packages/schedules/js/main.js index 43db5f0..add525e 100644 --- a/packages/schedules/js/main.js +++ b/packages/schedules/js/main.js @@ -5,7 +5,7 @@ * this surface talks directly to the core scheduled-tasks endpoints. * * SDK usage: - * sw.shell.Topbar — standard navigation bar + * sw.shell.topbar — shell topbar API * sw.ui.* — primitive components * sw.api.* — raw REST calls to /api/v1/schedules */ @@ -39,7 +39,6 @@ var { useState, useEffect, useCallback } = hooks; var { render } = preact; var { Button, FormField, Dialog, Spinner, Banner } = sw.ui; - var Topbar = sw.shell?.Topbar; // ── API helpers (core kernel endpoints) ──── var API = '/api/v1/schedules'; @@ -365,6 +364,16 @@ useEffect(function () { loadItems(); }, []); + // ── Shell topbar ─────────────────────────── + useEffect(function () { + if (!sw.shell?.topbar) return; + sw.shell.topbar.setTitle('Schedules'); + sw.shell.topbar.setSlot(html` + ${items.length} schedule${items.length !== 1 ? 's' : ''} + <${Button} variant="primary" size="sm" onClick=${() => setEditing({})}>+ New Schedule + `); + }, [items.length]); + function handleSave() { setEditing(null); loadItems(); @@ -372,17 +381,6 @@ return html`
- ${Topbar ? html` - <${Topbar} title="Schedules"> - ${items.length} schedule${items.length !== 1 ? 's' : ''} - <${Button} variant="primary" size="sm" onClick=${() => setEditing({})}>+ New Schedule - - ` : html` -
-

Schedules

- <${Button} variant="primary" size="sm" onClick=${() => setEditing({})}>+ New Schedule -
- `} ${error && html`<${Banner} variant="danger" text=${error} />`} diff --git a/packages/schedules/manifest.json b/packages/schedules/manifest.json index b150a0e..1c280a0 100644 --- a/packages/schedules/manifest.json +++ b/packages/schedules/manifest.json @@ -2,7 +2,7 @@ "id": "schedules", "title": "Schedules", "type": "full", - "version": "0.1.0", + "version": "0.2.0", "tier": "browser", "author": "Armature", "icon": "⏰",